diff --git a/.gitattributes b/.gitattributes index 2ab2d3c5962..2f2cad2e13a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ * text=auto *.js linguist-language=java *.css linguist-language=java -*.html linguist-language=java +*.html linguist-language=java \ No newline at end of file diff --git a/.gitignore b/.gitignore index 853f53da406..2dc9c784aa8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,4 @@ -.gradle -/build/ -/**/build/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr -/out/ -/**/out/ -.shelf/ -.ideaDataSources/ -dataSources/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ -/node_modules/ - -### OS ### +/node_modules +/package-lock.json +/dist .DS_Store diff --git a/.nojekyll b/.nojekyll old mode 100644 new mode 100755 diff --git a/HomePage.md b/HomePage.md deleted file mode 100644 index 43a338951fc..00000000000 --- a/HomePage.md +++ /dev/null @@ -1,236 +0,0 @@ -点击订阅[Java面试进阶指南](https://xiaozhuanlan.com/javainterview?rel=javaguide)(专为Java面试方向准备)。[为什么要弄这个专栏?](https://shimo.im/./9BJjNsNg7S4dCnz3/) - -

Java 学习/面试指南

-

- - - - -## 目录 - -- [Java](#java) - - [基础](#基础) - - [容器](#容器) - - [并发](#并发) - - [JVM](#jvm) - - [I/O](#io) - - [Java 8](#java-8) - - [编程规范](#编程规范) -- [网络](#网络) -- [操作系统](#操作系统) - - [Linux相关](#linux相关) -- [数据结构与算法](#数据结构与算法) - - [数据结构](#数据结构) - - [算法](#算法) -- [数据库](#数据库) - - [MySQL](#mysql) - - [Redis](#redis) -- [系统设计](#系统设计) - - [设计模式(工厂模式、单例模式 ... )](#设计模式) - - [常用框架(Spring、Zookeeper ... )](#常用框架) - - [数据通信(消息队列、Dubbo ... )](#数据通信) - - [网站架构](#网站架构) -- [面试指南](#面试指南) - - [备战面试](#备战面试) - - [常见面试题总结](#常见面试题总结) - - [面经](#面经) -- [工具](#工具) - - [Git](#git) - - [Docker](#Docker) -- [资料](#资料) - - [书单](#书单) - - [Github榜单](#Github榜单) -- [待办](#待办) -- [说明](#说明) - -## Java - -### 基础 - -* [Java 基础知识回顾](java/Java基础知识.md) -* [Java 基础知识疑难点总结](java/Java疑难点.md) -* [J2EE 基础知识回顾](java/J2EE基础知识.md) - -### 容器 - -* [Java容器常见面试题/知识点总结](java/collection/Java集合框架常见面试题.md) -* [ArrayList 源码学习](java/collection/ArrayList.md) -* [LinkedList 源码学习](java/collection/LinkedList.md) -* [HashMap(JDK1.8)源码学习](java/collection/HashMap.md) - -### 并发 - -* [Java 并发基础常见面试题总结](java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md) -* [Java 并发进阶常见面试题总结](java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md) -* [并发容器总结](java/Multithread/并发容器总结.md) -* [乐观锁与悲观锁](essential-content-for-interview/面试必备之乐观锁与悲观锁.md) -* [JUC 中的 Atomic 原子类总结](java/Multithread/Atomic.md) -* [AQS 原理以及 AQS 同步组件总结](java/Multithread/AQS.md) - -### JVM -* [一 Java内存区域](java/jvm/Java内存区域.md) -* [二 JVM垃圾回收](java/jvm/JVM垃圾回收.md) -* [三 JDK 监控和故障处理工具](java/jvm/JDK监控和故障处理工具总结.md) -* [四 类文件结构](java/jvm/类文件结构.md) -* [五 类加载过程](java/jvm/类加载过程.md) -* [六 类加载器](java/jvm/类加载器.md) - -### I/O - -* [BIO,NIO,AIO 总结 ](java/BIO-NIO-AIO.md) -* [Java IO 与 NIO系列文章](java/Java%20IO与NIO.md) - -### Java 8 - -* [Java 8 新特性总结](java/What's%20New%20in%20JDK8/Java8Tutorial.md) -* [Java 8 学习资源推荐](java/What's%20New%20in%20JDK8/Java8教程推荐.md) - -### 编程规范 - -- [Java 编程规范](java/Java编程规范.md) - -## 网络 - -* [计算机网络常见面试题](network/计算机网络.md) -* [计算机网络基础知识总结](network/干货:计算机网络知识总结.md) -* [HTTPS中的TLS](network/HTTPS中的TLS.md) - -## 操作系统 - -### Linux相关 - -* [后端程序员必备的 Linux 基础知识](operating-system/后端程序员必备的Linux基础知识.md) -* [Shell 编程入门](operating-system/Shell.md) - -## 数据结构与算法 - -### 数据结构 - -- [数据结构知识学习与面试](dataStructures-algorithms/数据结构.md) - -### 算法 - -- [算法学习资源推荐](dataStructures-algorithms/算法学习资源推荐.md) -- [几道常见的字符串算法题总结 ](dataStructures-algorithms/几道常见的子符串算法题.md) -- [几道常见的链表算法题总结 ](dataStructures-algorithms/几道常见的链表算法题.md) -- [剑指offer部分编程题](dataStructures-algorithms/剑指offer部分编程题.md) -- [公司真题](dataStructures-algorithms/公司真题.md) -- [回溯算法经典案例之N皇后问题](dataStructures-algorithms/Backtracking-NQueens.md) - -## 数据库 - -### MySQL - -* [MySQL 学习与面试](database/MySQL.md) -* [一千行MySQL学习笔记](database/一千行MySQL命令.md) -* [MySQL高性能优化规范建议](database/MySQL高性能优化规范建议.md) -* [数据库索引总结](database/MySQL%20Index.md) -* [事务隔离级别(图文详解)](database/事务隔离级别(图文详解).md) -* [一条SQL语句在MySQL中如何执行的](database/一条sql语句在mysql中如何执行的.md) - -### Redis - -* [Redis 总结](database/Redis/Redis.md) -* [Redlock分布式锁](database/Redis/Redlock分布式锁.md) -* [如何做可靠的分布式锁,Redlock真的可行么](database/Redis/如何做可靠的分布式锁,Redlock真的可行么.md) - -## 系统设计 - -### 设计模式 - -- [设计模式系列文章](system-design/设计模式.md) - -### 常用框架 - -#### Spring - -- [Spring 学习与面试](system-design/framework/spring/Spring.md) -- [Spring 常见问题总结](system-design/framework/spring/SpringInterviewQuestions.md) -- [Spring中bean的作用域与生命周期](system-design/framework/spring/SpringBean.md) -- [SpringMVC 工作原理详解](system-design/framework/spring/SpringMVC-Principle.md) -- [Spring中都用到了那些设计模式?](system-design/framework/spring/Spring-Design-Patterns.md) - -#### ZooKeeper - -- [ZooKeeper 相关概念总结](system-design/framework/ZooKeeper.md) -- [ZooKeeper 数据模型和常见命令](system-design/framework/ZooKeeper数据模型和常见命令.md) - -### 数据通信 - -- [数据通信(RESTful、RPC、消息队列)相关知识点总结](system-design/data-communication/summary.md) -- [Dubbo 总结:关于 Dubbo 的重要知识点](system-design/data-communication/dubbo.md) -- [消息队列总结](system-design/data-communication/message-queue.md) -- [RabbitMQ 入门](system-design/data-communication/rabbitmq.md) -- [RocketMQ的几个简单问题与答案](system-design/data-communication/RocketMQ-Questions.md) - -### 网站架构 - -- [一文读懂分布式应该学什么](system-design/website-architecture/分布式.md) -- [8 张图读懂大型网站技术架构](system-design/website-architecture/8%20张图读懂大型网站技术架构.md) -- [【面试精选】关于大型网站系统架构你不得不懂的10个问题](system-design/website-architecture/【面试精选】关于大型网站系统架构你不得不懂的10个问题.md) - -## 面试指南 - -### 备战面试 - -* [【备战面试1】程序员的简历就该这样写](essential-content-for-interview/PreparingForInterview/程序员的简历之道.md) -* [【备战面试2】初出茅庐的程序员该如何准备面试?](essential-content-for-interview/PreparingForInterview/interviewPrepare.md) -* [【备战面试3】7个大部分程序员在面试前很关心的问题](essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md) -* [【备战面试4】Github上开源的Java面试/学习相关的仓库推荐](essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md) -* [【备战面试5】如果面试官问你“你有什么问题问我吗?”时,你该如何回答](essential-content-for-interview/PreparingForInterview/如果面试官问你“你有什么问题问我吗?”时,你该如何回答.md) -* [【备战面试6】美团面试常见问题总结(附详解答案)](essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md) - -### 常见面试题总结 - -* [第一周(2018-8-7)](essential-content-for-interview/MostCommonJavaInterviewQuestions/第一周(2018-8-7).md) (为什么 Java 中只有值传递、==与equals、 hashCode与equals) -* [第二周(2018-8-13)](essential-content-for-interview/MostCommonJavaInterviewQuestions/第二周(2018-8-13).md)(String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的?、什么是反射机制?反射机制的应用场景有哪些?......) -* [第三周(2018-08-22)](java/collection/Java集合框架常见面试题.md) (Arraylist 与 LinkedList 异同、ArrayList 与 Vector 区别、HashMap的底层实现、HashMap 和 Hashtable 的区别、HashMap 的长度为什么是2的幂次方、HashSet 和 HashMap 区别、ConcurrentHashMap 和 Hashtable 的区别、ConcurrentHashMap线程安全的具体实现方式/底层具体实现、集合框架底层数据结构总结) -* [第四周(2018-8-30).md](essential-content-for-interview/MostCommonJavaInterviewQuestions/第四周(2018-8-30).md) (主要内容是几道面试常问的多线程基础题。) - -### 面经 - -- [5面阿里,终获offer(2018年秋招)](essential-content-for-interview/BATJrealInterviewExperience/5面阿里,终获offer.md) -- [蚂蚁金服2019实习生面经总结(已拿口头offer)](essential-content-for-interview/BATJrealInterviewExperience/蚂蚁金服实习生面经总结(已拿口头offer).md) -- [2019年蚂蚁金服、头条、拼多多的面试总结](essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md) - -## 工具 - -### Git - -* [Git入门](tools/Git.md) - -### Docker - -* [Docker 入门](tools/Docker.md) -* [一文搞懂 Docker 镜像的常用操作!](tools/Docker-Image.md) - -## 资料 - -### 书单 - -- [Java程序员必备书单](data/java-recommended-books.md) - -### Github榜单 - -- [Java 项目月榜单](github-trending/JavaGithubTrending.md) - -*** - -## 待办 - -- [x] [Java 8 新特性总结](./java/What's%20New%20in%20JDK8/Java8Tutorial.md) -- [x] [Java 8 新特性详解](./java/What's%20New%20in%20JDK8/Java8教程推荐.md) -- [ ] Java 多线程类别知识重构(---正在进行中---) -- [x] [BIO,NIO,AIO 总结 ](./java/BIO-NIO-AIO.md) -- [ ] Netty 总结(---正在进行中---) -- [ ] 数据结构总结重构(---正在进行中---) - -## 公众号 - -- 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 -- 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本公众号后台回复 **"Java面试突击"** 即可免费领取! -- 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -

- -

diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9e9f8..00000000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md old mode 100644 new mode 100755 index ba8a0184b22..f8e670983e3 --- a/README.md +++ b/README.md @@ -1,498 +1,367 @@ -> JavaGuide 的Star数量虽然比较多,但是它的价值和含金量一定是不能和 Dubbo、Nacos这些优秀的国产开源项目比的。希望国内可以出更多优秀的开源项目! -> -> 另外,希望大家对面试不要抱有侥幸的心理,打铁还需自身硬! 我希望这个文档是为你学习 Java 指明方向,而不是用来应付面试用的。加油!奥利给! - -**开始阅读之前必看** : - -1. [完结撒花!JavaGuide面试突击版来啦!](./docs/javaguide面试突击版.md) -2. [JavaGuide重大更新记录](./docs/update-history.md) - -更多原创内容和干货分享: - -1. [公众号—JavaGuide](#公众号) : 最新原创文章+免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源) - -Github用户如果访问速度缓慢的话,可以转移到[码云](https://gitee.com/SnailClimb/JavaGuide )查看,或者[在线阅读](https://snailclimb.gitee.io/javaguide )。 - -

- - - -

- -

- 阅读 - 公众号 - 公众号 - 投稿 - 投稿 -

- -

Sponsor

-

- - - -

- -## 目录 - -- [Java](#java) - - [基础](#基础) - - [容器](#容器) - - [并发](#并发) - - [JVM](#jvm) - - [其他](#其他) -- [网络](#网络) -- [操作系统](#操作系统) - - [Linux](#linux) -- **[数据结构与算法](#数据结构与算法)** - - [数据结构](#数据结构) - - [算法](#算法) -- [数据库](#数据库) - - [MySQL](#mysql) - - [Redis](#redis) -- [系统设计](#系统设计) - - [必知](#必知) - - [常用框架](#常用框架) - - [Spring](#spring) - - [SpringBoot](#springboot) - - [MyBatis](#mybatis) - - [认证授权(JWT、SSO)](#认证授权) - - [分布式](#分布式) - - [Elasticsearch(分布式搜索引擎)](#elasticsearch分布式搜索引擎) - - [RPC](#rpc) - - [消息队列](#消息队列) - - [API 网关](#api-网关) - - [分布式id](#分布式id) - - [分布式限流](#分布式限流) - - [分布式接口幂等性](#分布式接口幂等性) - - [数据库扩展](#数据库扩展) - - [ZooKeeper](#zookeeper) - - [大型网站架构](#大型网站架构) - - [性能测试](#性能测试) - - [高并发](#高并发) - - [高可用](#高可用) - - [微服务](#微服务) - - [Spring Cloud](#spring-cloud) -- [必会工具](#必会工具) - - [Git](#git) - - [Docker](#docker) -- [面试指南](#面试指南) -- [Java学习常见问题汇总](#java学习常见问题汇总) -- [资源](#资源) - - [书单推荐](#书单推荐) - - [实战项目推荐](#实战项目推荐) -- [待办](#待办) -- [说明](#说明) - -## Java - -### 基础 - -**基础知识系统总结:** - -1. **[Java 基础知识](docs/java/Java基础知识.md)** -2. **[Java 基础知识疑难点/易错点](docs/java/Java疑难点.md)** -3. [【选看】J2EE 基础知识](docs/java/J2EE基础知识.md) - -**重要知识点详解:** - -1. [枚举](docs/java/basic/用好Java中的枚举真的没有那么简单.md) (很重要的一个数据结构,用好枚举真的没有那么简单!) -2. [Java 常见关键字总结:final、static、this、super!](docs/java/basic/final,static,this,super.md) -3. [什么是反射机制?反射机制的应用场景有哪些?](docs/java/basic/reflection.md) - -**其他:** - -1. [JAD反编译](docs/java/JAD反编译tricks.md) -2. [手把手教你定位常见Java性能问题](./docs/java/手把手教你定位常见Java性能问题.md) - -### 容器 - -1. **[Java容器常见面试题/知识点总结](docs/java/collection/Java集合框架常见面试题.md)** -2. [ArrayList 源码](docs/java/collection/ArrayList.md) 、[LinkedList 源码](docs/java/collection/LinkedList.md) 、[HashMap(JDK1.8)源码](docs/java/collection/HashMap.md) - -### 并发 - -**[多线程学习指南](./docs/java/Multithread/多线程学习指南.md)** - -**面试题总结:** - -1. **[Java 并发基础常见面试题总结](docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md)** -2. **[Java 并发进阶常见面试题总结](docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md)** - -**必备知识点:** - -1. [并发容器总结](docs/java/Multithread/并发容器总结.md) -2. **[Java线程池学习总结](./docs/java/Multithread/java线程池学习总结.md)** -3. [乐观锁与悲观锁](docs/essential-content-for-interview/面试必备之乐观锁与悲观锁.md) -4. [JUC 中的 Atomic 原子类总结](docs/java/Multithread/Atomic.md) -5. [AQS 原理以及 AQS 同步组件总结](docs/java/Multithread/AQS.md) - -### JVM - -1. **[Java内存区域](docs/java/jvm/Java内存区域.md)** -2. **[JVM垃圾回收](docs/java/jvm/JVM垃圾回收.md)** -3. [JDK 监控和故障处理工具](docs/java/jvm/JDK监控和故障处理工具总结.md) -4. [类文件结构](docs/java/jvm/类文件结构.md) -5. **[类加载过程](docs/java/jvm/类加载过程.md)** -6. [类加载器](docs/java/jvm/类加载器.md) -7. **[【待完成】最重要的 JVM 参数指南(翻译完善了一半)](docs/java/jvm/最重要的JVM参数指南.md)** -8. [JVM 配置常用参数和常用 GC 调优策略](docs/java/jvm/GC调优参数.md) -9. **[【加餐】大白话带你认识JVM](docs/java/jvm/[加餐]大白话带你认识JVM.md)** - -### 其他 - -1. **I/O** :[BIO,NIO,AIO 总结 ](docs/java/BIO-NIO-AIO.md) -2. **Java 8** :[Java 8 新特性总结](docs/java/What's%20New%20in%20JDK8/Java8Tutorial.md)、[Java 8 学习资源推荐](docs/java/What's%20New%20in%20JDK8/Java8教程推荐.md)、[Java8 forEach 指南](docs/java/What's%20New%20in%20JDK8/Java8foreach指南.md) -3. **Java9~Java14** : [一文带你看遍JDK9~14的重要新特性!](./docs/java/jdk-new-features/new-features-from-jdk8-to-jdk14.md) -4. Java编程规范:**[Java 编程规范以及优雅 Java 代码实践总结](docs/java/Java编程规范.md)** 、[告别编码5分钟,命名2小时!史上最全的Java命名规范参考!](docs/java/java-naming-conventions.md) -5. 设计模式 :[设计模式系列文章](docs/system-design/设计模式.md) - -## 网络 - -1. [计算机网络常见面试题](docs/network/计算机网络.md) -2. [计算机网络基础知识总结](docs/network/干货:计算机网络知识总结.md) -3. [HTTPS中的TLS](docs/network/HTTPS中的TLS.md) - -## 操作系统 - -[最硬核的操作系统常见问题总结!](docs/operating-system/basis.md) - -### Linux - -* [后端程序员必备的 Linux 基础知识](docs/operating-system/linux.md) -* [Shell 编程入门](docs/operating-system/Shell.md) - -## 数据结构与算法 - -### 数据结构 - -- [不了解布隆过滤器?一文给你整的明明白白!](docs/dataStructures-algorithms/data-structure/bloom-filter.md) -- [数据结构知识学习与面试](docs/dataStructures-algorithms/数据结构.md) - -### 算法 - -- [硬核的算法学习书籍+资源推荐](docs/dataStructures-algorithms/算法学习资源推荐.md) -- 常见算法问题总结: - - [几道常见的字符串算法题总结 ](docs/dataStructures-algorithms/几道常见的子符串算法题.md) - - [几道常见的链表算法题总结 ](docs/dataStructures-algorithms/几道常见的链表算法题.md) - - [剑指offer部分编程题](docs/dataStructures-algorithms/剑指offer部分编程题.md) - - [公司真题](docs/dataStructures-algorithms/公司真题.md) - - [回溯算法经典案例之N皇后问题](docs/dataStructures-algorithms/Backtracking-NQueens.md) - -## 数据库 - -### MySQL - -**总结:** - -1. **[【推荐】MySQL/数据库 知识点总结](docs/database/MySQL.md)** -2. **[阿里巴巴开发手册数据库部分的一些最佳实践](docs/database/阿里巴巴开发手册数据库部分的一些最佳实践.md)** -3. **[一千行MySQL学习笔记](docs/database/一千行MySQL命令.md)** -4. [MySQL高性能优化规范建议](docs/database/MySQL高性能优化规范建议.md) - -**重要知识点:** - -1. [数据库索引总结1](docs/database/MySQL%20Index.md)、[数据库索引总结2](docs/database/数据库索引.md) -2. [事务隔离级别(图文详解)](docs/database/事务隔离级别(图文详解).md) -3. [一条SQL语句在MySQL中如何执行的](docs/database/一条sql语句在mysql中如何执行的.md) -4. **[关于数据库中如何存储时间的一点思考](docs/database/关于数据库存储时间的一点思考.md)** - -### Redis - -* [Redis 常见问题总结](docs/database/Redis/Redis.md) -* **Redis 系列文章合集:** - 1. [5种基本数据结构](docs/database/Redis/redis-collection/Redis(1)——5种基本数据结构.md) - 2. [跳跃表](docs/database/Redis/redis-collection/Redis(2)——跳跃表.md) - 3. [分布式锁深入探究](docs/database/Redis/redis-collection/Redis(3)——分布式锁深入探究.md) 、 [Redlock分布式锁](docs/database/Redis/Redlock分布式锁.md) 、[如何做可靠的分布式锁,Redlock真的可行么](docs/database/Redis/如何做可靠的分布式锁,Redlock真的可行么.md) - 4. [神奇的HyperLoglog解决统计问题](docs/database/Redis/redis-collection/Reids(4)——神奇的HyperLoglog解决统计问题.md) - 5. [亿级数据过滤和布隆过滤器](docs/database/Redis/redis-collection/Redis(5)——亿级数据过滤和布隆过滤器.md) - 6. [GeoHash查找附近的人](docs/database/Redis/redis-collection/Redis(6)——GeoHash查找附近的人.md) - 7. [持久化](docs/database/Redis/redis-collection/Redis(7)——持久化.md) - 8. [发布订阅与Stream](docs/database/Redis/redis-collection/Redis(8)——发布订阅与Stream.md) - 9. [史上最强【集群】入门实践教程](docs/database/Redis/redis-collection/Redis(9)——集群入门实践教程.md) - 10. [Redis数据类型、编码、底层数据结构的关系看这篇](docs/database/Redis/redis-collection/Redis(10)——Redis数据类型、编码、数据结构的关系.md) - -## 系统设计 - -### 必知 - -1. **[RestFul API 简明教程](docs/system-design/restful-api.md)** - -### 常用框架 - -#### Spring/SpringBoot - -1. **[Spring 常见问题总结](docs/system-design/framework/spring/SpringInterviewQuestions.md)** -2. **[Spring/Spring常用注解总结!安排!](./docs/system-design/framework/spring/spring-annotations.md)** -3. **[SpringBoot 指南/常见面试题总结](https://github.com/Snailclimb/springboot-guide)** -4. [Spring中 Bean 的作用域与生命周期](docs/system-design/framework/spring/SpringBean.md) -5. [SpringMVC 工作原理详解](docs/system-design/framework/spring/SpringMVC-Principle.md) -6. [Spring中都用到了那些设计模式?](docs/system-design/framework/spring/Spring-Design-Patterns.md) - -#### MyBatis - -- [MyBatis常见面试题总结](docs/system-design/framework/mybatis/mybatis-interview.md) - -### 认证授权 - -**[认证授权基础:搞清Authentication,Authorization以及Cookie、Session、Token、OAuth 2、SSO](docs/system-design/authority-certification/basis-of-authority-certification.md)** - -#### JWT - -- **[JWT 优缺点分析以及常见问题解决方案](docs/system-design/authority-certification/JWT-advantages-and-disadvantages.md)** -- **[适合初学者入门 Spring Security With JWT 的 Demo](https://github.com/Snailclimb/spring-security-jwt-guide)** - -#### SSO(单点登录) - -SSO(Single Sign On)即单点登录说的是用户登陆多个子系统的其中一个就有权访问与其相关的其他系统。举个例子我们在登陆了京东金融之后,我们同时也成功登陆京东的京东超市、京东家电等子系统。相关阅读:**[SSO 单点登录看这篇就够了!](docs/system-design/authority-certification/sso.md)** - -### 分布式 - -[分布式相关概念入门](docs/system-design/website-architecture/分布式.md) - -#### Elasticsearch(分布式搜索引擎) - -提高搜索效率。常见于电商购物网站的商品搜索于分类。 - -代办...... - -#### RPC - -让调用远程服务调用像调用本地方法那样简单。 - -- [Dubbo 总结:关于 Dubbo 的重要知识点](docs/system-design/data-communication/dubbo.md) -- [服务之间的调用为啥不直接用 HTTP 而用 RPC?](docs/system-design/data-communication/why-use-rpc.md) - -#### 消息队列 - -消息队列在分布式系统中主要是为了接耦和削峰。相关阅读: **[消息队列总结](docs/system-design/data-communication/message-queue.md)** 。 - -**RabbitMQ:** - -1. [RabbitMQ 入门](docs/system-design/data-communication/rabbitmq.md) - -**RocketMQ:** - -1. [RocketMQ 入门](docs/system-design/data-communication/RocketMQ.md) -2. [RocketMQ的几个简单问题与答案](docs/system-design/data-communication/RocketMQ-Questions.md) - -**Kafka:** - -1. **[Kafka 入门+SpringBoot整合Kafka系列](https://github.com/Snailclimb/springboot-kafka)** -2. **[Kafka 常见面试题总结](docs/system-design/data-communication/kafka-inverview.md)** -3. [【加餐】Kafka入门看这一篇就够了](docs/system-design/data-communication/Kafka入门看这一篇就够了.md) - -#### API 网关 - -网关主要用于请求转发、安全认证、协议转换、容灾。 - -1. [为什么要网关?你知道有哪些常见的网关系统?](docs/system-design/micro-service/api-gateway-intro.md) -2. [如何设计一个亿级网关(API Gateway)?](docs/system-design/micro-service/API网关.md) - -#### 分布式id - -1. [为什么要分布式 id ?分布式 id 生成方案有哪些?](docs/system-design/micro-service/分布式id生成方案总结.md) - -#### 分布式限流 - -1. [限流算法有哪些?](docs/system-design/micro-service/limit-request.md) - -#### 分布式接口幂等性 - -#### ZooKeeper - -> 前两篇文章可能有内容重合部分,推荐都看一遍。 - -1. [【入门】ZooKeeper 相关概念总结](docs/system-design/framework/ZooKeeper.md) -2. [【进阶】Zookeeper 原理简单入门!](docs/system-design/framework/ZooKeeper-plus.md) -3. [【拓展】ZooKeeper 数据模型和常见命令](docs/system-design/framework/ZooKeeper数据模型和常见命令.md) - -#### 其他 - -- 接口幂等性(代办):分布式系统必须要考虑接口的幂等性。 - -#### 数据库扩展 - -读写分离、分库分表。 - -代办..... - -### 大型网站架构 - -- [8 张图读懂大型网站技术架构](docs/system-design/website-architecture/8%20张图读懂大型网站技术架构.md) -- [关于大型网站系统架构你不得不懂的10个问题](docs/system-design/website-architecture/关于大型网站系统架构你不得不懂的10个问题.md) - -#### 性能测试 - -- [后端程序员也要懂的性能测试知识](https://articles.zsxq.com/id_lwl39teglv3d.html) (知识星球) - -#### 高并发 - -待办...... - -#### 高可用 - -高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的 。相关阅读: **《[如何设计一个高可用系统?要考虑哪些地方?](docs/system-design/website-architecture/如何设计一个高可用系统?要考虑哪些地方?.md)》** 。 - -### 微服务 - -#### Spring Cloud - -- [ 大白话入门 Spring Cloud](docs/system-design/micro-service/spring-cloud.md) - -## 必会工具 - -### Git - -* [Git入门](docs/tools/Git.md) - -### Docker - -1. [Docker 基本概念解读](docs/tools/Docker.md) -2. [一文搞懂 Docker 镜像的常用操作!](docs/tools/Docker-Image.md ) - -### 其他 - -- [【原创】如何使用云服务器?希望这篇文章能够对你有帮助!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485738&idx=1&sn=f97e91a50e444944076c30b0717b303a&chksm=cea246e1f9d5cff73faf6a778b147ea85162d1f3ed55ca90473c6ebae1e2c4d13e89282aeb24&token=406194678&lang=zh_CN#rd) - -## 面试指南 - -> 这部分很多内容比如大厂面经、真实面经分析被移除,详见[完结撒花!JavaGuide面试突击版来啦!](./docs/javaguide面试突击版.md)。 - -1. **[【备战面试1】程序员的简历就该这样写](docs/essential-content-for-interview/PreparingForInterview/程序员的简历之道.md)** -2. **[【备战面试2】初出茅庐的程序员该如何准备面试?](docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md)** -3. **[【备战面试3】7个大部分程序员在面试前很关心的问题](docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md)** -4. **[【备战面试4】Github上开源的Java面试/学习相关的仓库推荐](docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md)** -5. **[【备战面试5】如果面试官问你“你有什么问题问我吗?”时,你该如何回答](docs/essential-content-for-interview/PreparingForInterview/面试官-你有什么问题要问我.md)** -6. [【备战面试6】应届生面试最爱问的几道 Java 基础问题](docs/essential-content-for-interview/PreparingForInterview/应届生面试最爱问的几道Java基础问题.md) -7. **[【备战面试6】美团面试常见问题总结(附详解答案)](docs/essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md)** - -## Java学习常见问题汇总 - -1. [Java学习路线和方法推荐](docs/questions/java-learning-path-and-methods.md) -2. [Java培训四个月能学会吗?](docs/questions/java-training-4-month.md) -3. [新手学习Java,有哪些Java相关的博客,专栏,和技术学习网站推荐?](docs/questions/java-learning-website-blog.md) -4. [Java 还是大数据,你需要了解这些东西!](docs/questions/java-big-data) -5. [Java 后台开发/大数据?你需要了解这些东西!](https://articles.zsxq.com/id_wto1iwd5g72o.html)(知识星球) - -## 资源 - -### 书单推荐 - -- **[Java程序员必备书单](docs/books/java.md)** - -### 实战项目推荐 - -- **[Java、SpringBoot实战项目推荐](https://github.com/Snailclimb/awesome-java#实战项目)** - -### Github - -- [Github 上非常棒的 Java 开源项目集合](https://github.com/Snailclimb/awesome-java) -- [Github 上 Star 数最多的 10 个项目,看完之后很意外!](docs/tools/github/github-star-ranking.md) -- [年末将至,值得你关注的16个Java 开源项目!](docs/github-trending/2019-12.md) -- [Java 项目历史月榜单](docs/github-trending/JavaGithubTrending.md) - -*** - -## 待办 - -- [ ] Netty 总结(---正在进行中---) -- [ ] 数据结构总结重构(---正在进行中---) - -## 说明 - -开源项目在于大家的参与,这才使得它的价值得到提升。感谢🙏有你! - -### JavaGuide介绍 - -开源 JavaGuide 初始想法源于自己的个人那一段比较迷茫的学习经历。主要目的是为了通过这个开源平台来帮助一些在学习 Java 或者面试过程中遇到问题的小伙伴。 - -* **对于 Java 初学者来说:** 本文档倾向于给你提供一个比较详细的学习路径,让你对于Java整体的知识体系有一个初步认识。另外,本文的一些文章 -也是你学习和复习 Java 知识不错的实践; -* **对于非 Java 初学者来说:** 本文档更适合回顾知识,准备面试,搞清面试应该把重心放在那些问题上。要搞清楚这个道理:提前知道那些面试常见,不是为了背下来应付面试,而是为了让你可以更有针对的学习重点。 - -Markdown 格式参考:[Github Markdown格式](https://guides.github.com/features/mastering-markdown/),表情素材来自:[EMOJI CHEAT SHEET](https://www.webpagefx.com/tools/emoji-cheat-sheet/)。 - -利用 docsify 生成文档部署在 Github pages: [docsify 官网介绍](https://docsify.js.org/#/) ,另见[《Guide哥手把手教你搭建一个文档类型的网站!免费且高速!》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486555&idx=2&sn=8486026ee9f9ba645ff0363df6036184&chksm=cea24390f9d5ca86ff4177c0aca5e719de17dc89e918212513ee661dd56f17ca8269f4a6e303&token=298703358&lang=zh_CN#rd) 。 - -### 关于转载 - -如果你需要转载本仓库的一些文章到自己的博客的话,记得注明原文地址就可以了。 - -### 如何对该开源文档进行贡献 - -1. 笔记内容大多是手敲,所以难免会有笔误,你可以帮我找错别字。 -2. 很多知识点我可能没有涉及到,所以你可以对其他知识点进行补充。 -3. 现有的知识点难免存在不完善或者错误,所以你可以对已有知识点进行修改/补充。 - -### 联系我 - -![个人微信](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/wechat3.jpeg) - -### Contributor - -下面是笔主收集的一些对本仓库提过有价值的pr或者issue的朋友,人数较多,如果你也对本仓库提过不错的pr或者issue的话,你可以加我的微信与我联系。下面的排名不分先后! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" - - - - - - - - - - - - - - - - - - - - - - -### 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - - +👏 重大更新!!!重磅! + +- JavaGuide 在线阅读版(新版,推荐👍):https://javaguide.cn/ +- JavaGuide 在线阅读版(老版):https://snailclimb.gitee.io/javaguide/#/ + +👉 [朋友开源的面试八股文系列](https://github.com/csguide-dabai/interview-guide)。 + +> 1. **介绍**:关于 JavaGuide 的相关介绍请看:[关于 JavaGuide 的一些说明](https://www.yuque.com/snailclimb/dr6cvl/mr44yt) 。 +> 2. **贡献指南** :欢迎参与 [JavaGuide的维护工作](https://github.com/Snailclimb/JavaGuide/issues/1235),这是一件非常有意义的事情。 +> 3. **PDF版本** : [《JavaGuide 面试突击版》PDF 版本](#公众号) 。 +> 4. **图解计算机基础** :[图解计算机基础 PDF 下载](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=100021725&idx=1&sn=2db9664ca25363139a81691043e9fd8f&chksm=4ea19a1679d61300d8990f7e43bfc7f476577a81b712cf0f9c6f6552a8b219bc081efddb5c54#rd) 。 +> 5. **知识星球** : 简历指导/Java学习/面试指导/面试小册。欢迎加入[我的知识星球](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=100015911&idx=1&sn=2e8a0f5acb749ecbcbb417aa8a4e18cc&chksm=4ea1b0ec79d639fae37df1b86f196e8ce397accfd1dd2004bcadb66b4df5f582d90ae0d62448#rd) 。 +> 6. **面试专版** :准备面试的小伙伴可以考虑面试专版:[《Java面试进阶指北 》](https://www.yuque.com/docs/share/f37fc804-bfe6-4b0d-b373-9c462188fec7) (质量很高,专为面试打造,星球用户免费) +> 7. **转载须知** :以下所有文章如非文首说明皆为我(Guide哥)的原创,转载在文首注明出处,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境!⛽️ + +

+ + + +

+

+ 阅读 + stars +

+ + +

Sponsor

+ + + + + + + +
+ + +
+ +## Java + +### 基础 + +**知识点/面试题** : (必看:+1: ):[Java 基础知识点/面试题总结](docs/java/basis/java基础知识总结.md) + +**重要知识点详解:** + +- [什么是反射机制?反射机制的应用场景有哪些?](docs/java/basis/反射机制详解.md) +- [代理模式详解:静态代理+JDK/CGLIB 动态代理实战](docs/java/basis/代理模式详解.md) +- [常见的 IO 模型有哪些?Java 中的 BIO、NIO、AIO 有啥区别?](docs/java/basis/java基础知识总结) + +### 集合 + +1. **[Java 集合常见问题总结](docs/java/collection/java集合框架基础知识&面试题总结.md)** (必看 :+1:) +2. [Java 容器使用注意事项总结](docs/java/collection/java集合使用注意事项总结.md) +3. **源码分析** :[ArrayList 源码+扩容机制分析](docs/java/collection/arraylist-source-code.md) 、[HashMap(JDK1.8)源码+底层数据结构分析](docs/java/collection/hashmap-source-code.md) 、[ConcurrentHashMap 源码+底层数据结构分析](docs/java/collection/concurrent-hash-map-source-code.md) + +### 并发 + +**知识点/面试题:** (必看 :+1:) + +1. **[Java 并发基础常见面试题总结](docs/java/concurrent/java并发基础常见面试题总结.md)** +2. **[Java 并发进阶常见面试题总结](docs/java/concurrent/java并发进阶常见面试题总结.md)** + +**重要知识点详解:** + +1. **线程池**:[Java 线程池学习总结](./docs/java/concurrent/java线程池学习总结.md)、[拿来即用的 Java 线程池最佳实践](./docs/java/concurrent/拿来即用的java线程池最佳实践.md) +2. [ThreadLocal 关键字解析](docs/java/concurrent/threadlocal.md) +3. [Java 并发容器总结](docs/java/concurrent/并发容器总结.md) +4. [Atomic 原子类总结](docs/java/concurrent/atomic原子类总结.md) +5. [AQS 原理以及 AQS 同步组件总结](docs/java/concurrent/aqs原理以及aqs同步组件总结.md) +6. [CompletableFuture入门](docs/java/concurrent/completablefuture-intro.md) + +### JVM (必看 :+1:) + +JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle.com/javase/specs/jvms/se8/html/index.html) 和周志明老师的[《深入理解Java虚拟机(第3版)》](https://book.douban.com/subject/34907497/) (强烈建议阅读多遍!)。 + +1. **[Java 内存区域](docs/java/jvm/内存区域.md)** +2. **[JVM 垃圾回收](docs/java/jvm/jvm垃圾回收.md)** +3. [JDK 监控和故障处理工具](docs/java/jvm/jdk监控和故障处理工具总结.md) +4. [类文件结构](docs/java/jvm/类文件结构.md) +5. **[类加载过程](docs/java/jvm/类加载过程.md)** +6. [类加载器](docs/java/jvm/类加载器.md) +7. **[【待完成】最重要的 JVM 参数总结(翻译完善了一半)](docs/java/jvm/jvm参数指南.md)** +9. **[【加餐】大白话带你认识 JVM](docs/java/jvm/[加餐]大白话带你认识jvm.md)** + +### 新特性 + +1. **Java 8** :[Java 8 新特性总结](docs/java/new-features/Java8新特性总结.md)、[Java8常用新特性总结](docs/java/new-features/java8-common-new-features.md) +2. **Java9~Java15** : [一文带你看遍 JDK9~15 的重要新特性!](./docs/java/new-features/java新特性总结.md) + +### 小技巧 + +1. [JAD 反编译](docs/java/tips/JAD反编译tricks.md) +2. [手把手教你定位常见 Java 性能问题](./docs/java/tips/locate-performance-problems/手把手教你定位常见Java性能问题.md) + +## 计算机基础 + +👉 **[图解计算机基础 PDF 下载](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=100021725&idx=1&sn=2db9664ca25363139a81691043e9fd8f&chksm=4ea19a1679d61300d8990f7e43bfc7f476577a81b712cf0f9c6f6552a8b219bc081efddb5c54#rd)** 。 + +### 操作系统 + +1. [操作系统常见问题总结!](docs/cs-basics/operating-system/basis.md) +2. [后端程序员必备的 Linux 基础知识总结](docs/cs-basics/operating-system/linux.md) +3. [Shell 编程入门](docs/cs-basics/operating-system/Shell.md) + +### 网络 + +1. [计算机网络常见面试题](docs/cs-basics/network/计算机网络.md) +2. [计算机网络基础知识总结](docs/cs-basics/network/计算机网络知识总结.md) + +### 数据结构 + +**图解数据结构:** + +1. [线性数据结构 :数组、链表、栈、队列](docs/cs-basics/data-structure/线性数据结构.md) +2. [图](docs/cs-basics/data-structure/图.md) +3. [堆](docs/cs-basics/data-structure/堆.md) +4. [树](docs/cs-basics/data-structure/树.md) :重点关注[红黑树](docs/cs-basics/data-structure/红黑树.md)、B-,B+,B*树、LSM树 + +其他常用数据结构 : + +1. [布隆过滤器](docs/cs-basics/data-structure/bloom-filter.md) + +### 算法 + +算法这部分内容非常重要,如果你不知道如何学习算法的话,可以看下我写的: + +- [算法学习书籍+资源推荐](https://www.zhihu.com/question/323359308/answer/1545320858) 。 +- [如何刷Leetcode?](https://www.zhihu.com/question/31092580/answer/1534887374) + +**常见算法问题总结** : + +- [几道常见的字符串算法题总结 ](docs/cs-basics/algorithms/几道常见的字符串算法题.md) +- [几道常见的链表算法题总结 ](docs/cs-basics/algorithms/几道常见的链表算法题.md) +- [剑指 offer 部分编程题](docs/cs-basics/algorithms/剑指offer部分编程题.md) + +另外,[GeeksforGeeks]( https://www.geeksforgeeks.org/fundamentals-of-algorithms/) 这个网站总结了常见的算法 ,比较全面系统。 + +## 数据库 + +### MySQL + +**总结:** + +1. [数据库基础知识总结](docs/database/数据库基础知识.md) +2. **[MySQL知识点总结](docs/database/mysql/mysql知识点&面试题总结.md)** (必看 :+1:) +4. [一千行 MySQL 学习笔记](docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md) +5. [MySQL 高性能优化规范建议](docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md) + +**重要知识点:** + +1. [MySQL数据库索引总结](docs/database/mysql/mysql-index.md) +2. [事务隔离级别(图文详解)](docs/database/mysql/transaction-isolation-level.md) +3. [MySQL三大日志(binlog、redo log和undo log)详解](docs/database/mysql/mysql-logs.md) +4. [InnoDB存储引擎对MVCC的实现](docs/database/mysql/innodb-implementation-of-mvcc.md) +5. [一条 SQL 语句在 MySQL 中如何被执行的?](docs/database/mysql/how-sql-executed-in-mysql.md) +6. [字符集详解:为什么不建议在MySQL中使用 utf8 ?](docs/database/字符集.md) +7. [关于数据库中如何存储时间的一点思考](docs/database/mysql/some-thoughts-on-database-storage-time.md) + +### Redis + +1. [Redis 常见问题总结](docs/database/redis/redis-all.md) +2. [3种常用的缓存读写策略](docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md) + +## 搜索引擎 + +用于提高搜索效率,功能和浏览器搜索引擎类似。比较常见的搜索引擎是 Elasticsearch(推荐) 和 Solr。 + +## 系统设计 + +### 系统设计必备基础 + +#### RESTful API + +我们在进行后端开发的时候,主要的工作就是为前端或者其他后端服务提供 API 比如查询用户数据的 API 。RESTful API 是一种基于 REST 构建的 API,它是一种被设计的更好使用的 API。 + +相关阅读:[RestFul API 简明教程](docs/system-design/basis/RESTfulAPI.md) + +#### 命名 + +编程过程中,一定要重视命名。因为好的命名即是注释,别人一看到你的命名就知道你的变量、方法或者类是做什么的! + +相关阅读: [Java 命名之道](docs/system-design/naming.md) 。 + +### 常用框架 + +如果你没有接触过 Java Web 开发的话,可以先看一下我总结的 [《J2EE 基础知识》](docs/system-design/J2EE基础知识.md) 。虽然,这篇文章中的很多内容已经淘汰,但是可以让你对 Java 后台技术发展有更深的认识。 + +#### Spring/SpringBoot (必看 :+1:) + +**知识点/面试题:** + +1. **[Spring 常见问题总结](docs/system-design/framework/spring/Spring常见问题总结.md)** +2. **[SpringBoot 入门指南](https://github.com/Snailclimb/springboot-guide)** + +**重要知识点详解:** + +1. **[Spring/Spring Boot 常用注解总结!安排!](./docs/system-design/framework/spring/Spring&SpringBoot常用注解总结.md)** +2. **[Spring 事务总结](docs/system-design/framework/spring/Spring事务总结.md)** +3. [Spring 中都用到了那些设计模式?](docs/system-design/framework/spring/Spring设计模式总结.md) +4. **[SpringBoot 自动装配原理?”](docs/system-design/framework/spring/SpringBoot自动装配原理.md)** + +#### MyBatis + +[MyBatis 常见面试题总结](docs/system-design/framework/mybatis/mybatis-interview.md) + +#### Spring Cloud + +[ 大白话入门 Spring Cloud](docs/system-design/framework/springcloud/springcloud-intro.md) + +### 安全 + +#### 认证授权 + +**[《认证授权基础》](docs/system-design/security/basis-of-authority-certification.md)** 这篇文章中我会介绍认证授权常见概念: **Authentication**,**Authorization** 以及 **Cookie**、**Session**、Token、**OAuth 2**、**SSO** 。如果你不清楚这些概念的话,建议好好阅读一下这篇文章。 + +- **JWT** :JWT(JSON Web Token)是一种身份认证的方式,JWT 本质上就一段签名的 JSON 格式的数据。由于它是带有签名的,因此接收者便可以验证它的真实性。相关阅读: + - [JWT 优缺点分析以及常见问题解决方案](docs/system-design/security/jwt优缺点分析以及常见问题解决方案.md) + - [适合初学者入门 Spring Security With JWT 的 Demo](https://github.com/Snailclimb/spring-security-jwt-guide) + +- **SSO(单点登录)** :**SSO(Single Sign On)** 即单点登录说的是用户登陆多个子系统的其中一个就有权访问与其相关的其他系统。举个例子我们在登陆了京东金融之后,我们同时也成功登陆京东的京东超市、京东家电等子系统。相关阅读:[**SSO 单点登录看这篇就够了!**](docs/system-design/security/sso-intro.md) + +#### 数据脱敏 + +数据脱敏说的就是我们根据特定的规则对敏感信息数据进行变形,比如我们把手机号、身份证号某些位数使用 * 来代替。 + +### 定时任务 + +最近有朋友问到定时任务相关的问题。于是,我简单写了一篇文章总结一下定时任务的一些概念以及一些常见的定时任务技术选型:[《Java定时任务大揭秘》](./docs/system-design/定时任务.md) + +## 分布式 + +### CAP 理论和 BASE 理论 + +CAP 也就是 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性) 这三个单词首字母组合。 + +**BASE** 是 **Basically Available(基本可用)** 、**Soft-state(软状态)** 和 **Eventually Consistent(最终一致性)** 三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。 + +相关阅读:[CAP 理论和 BASE 理论解读](docs/distributed-system/理论&算法/cap&base理论.md) + +### Paxos 算法和 Raft 算法 + +**Paxos 算法**诞生于 1990 年,这是一种解决分布式系统一致性的经典算法 。但是,由于 Paxos 算法非常难以理解和实现,不断有人尝试简化这一算法。到了2013 年才诞生了一个比 Paxos 算法更易理解和实现的分布式一致性算法—**Raft 算法**。 + +### RPC + +RPC 让调用远程服务调用像调用本地方法那样简单。 + +Dubbo 是一款国产的 RPC 框架,由阿里开源。相关阅读: + +- [Dubbo 常见问题总结](docs/distributed-system/rpc/dubbo.md) +- [服务之间的调用为啥不直接用 HTTP 而用 RPC?](docs/distributed-system/rpc/why-use-rpc.md) + +### API 网关 + +网关主要用于请求转发、安全认证、协议转换、容灾。 + +相关阅读: + +- [为什么要网关?你知道有哪些常见的网关系统?](docs/distributed-system/api-gateway.md) +- [百亿规模API网关服务Shepherd的设计与实现](https://tech.meituan.com/2021/05/20/shepherd-api-gateway.html) + +### 分布式 id + +在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。比如数据量太大之后,往往需要对数据进行分库分表,分库分表后需要有一个唯一 ID 来标识一条数据或消息,数据库的自增 ID 显然不能满足需求。相关阅读:[为什么要分布式 id ?分布式 id 生成方案有哪些?](docs/distributed-system/distributed-id.md) + +### 分布式事务 + +**分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。** + +简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。 + +### 分布式协调 + +**ZooKeeper** : + +> 前两篇文章可能有内容重合部分,推荐都看一遍。 + +1. [【入门】ZooKeeper 相关概念总结](docs/distributed-system/分布式协调/zookeeper/zookeeper-intro.md) +2. [【进阶】ZooKeeper 相关概念总结](docs/distributed-system/分布式协调/zookeeper/zookeeper-plus.md) +3. [【实战】ZooKeeper 实战](docs/distributed-system/分布式协调/zookeeper/zookeeper-in-action.md) + +## 高性能 + +### 消息队列 + +消息队列在分布式系统中主要是为了解耦和削峰。相关阅读: [消息队列常见问题总结](docs/high-performance/message-queue/message-queue.md)。 + +1. **RabbitMQ** : [RabbitMQ 入门](docs/high-performance/message-queue/rabbitmq-intro.md) +2. **RocketMQ** : [RocketMQ 入门](docs/high-performance/message-queue/rocketmq-intro)、[RocketMQ 的几个简单问题与答案](docs/high-performance/message-queue/rocketmq-questions.md) +3. **Kafka** :[Kafka 常见问题总结](docs/high-performance/message-queue/kafka知识点&面试题总结.md) + +### 读写分离&分库分表 + +读写分离主要是为了将数据库的读和写操作分不到不同的数据库节点上。主服务器负责写,从服务器负责读。另外,一主一从或者一主多从都可以。 + +读写分离可以大幅提高读性能,小幅提高写的性能。因此,读写分离更适合单机并发读请求比较多的场景。 + +分库分表是为了解决由于库、表数据量过大,而导致数据库性能持续下降的问题。 + +常见的分库分表工具有:`sharding-jdbc`(当当)、`TSharding`(蘑菇街)、`MyCAT`(基于 Cobar)、`Cobar`(阿里巴巴)...。 推荐使用 `sharding-jdbc`。 因为,`sharding-jdbc` 是一款轻量级 `Java` 框架,以 `jar` 包形式提供服务,不要我们做额外的运维工作,并且兼容性也很好。 + +相关阅读: [读写分离&分库分表常见问题总结](docs/high-performance/读写分离&分库分表.md) + +### 负载均衡 + +负载均衡系统通常用于将任务比如用户请求处理分配到多个服务器处理以提高网站、应用或者数据库的性能和可靠性。 + +常见的负载均衡系统包括 3 种: + +1. **DNS 负载均衡** :一般用来实现地理级别的均衡。 +2. **硬件负载均衡** : 通过单独的硬件设备比如 F5 来实现负载均衡功能(硬件的价格一般很贵)。 +3. **软件负载均衡** :通过负载均衡软件比如 Nginx 来实现负载均衡功能。 + +## 高可用 + +高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的 。 + +相关阅读: **《[如何设计一个高可用系统?要考虑哪些地方?](docs/high-availability/高可用系统设计.md)》** 。 + +### 限流 + +限流是从用户访问压力的角度来考虑如何应对系统故障。 + +限流为了对服务端的接口接受请求的频率进行限制,防止服务挂掉。比如某一接口的请求限制为 100 个每秒, 对超过限制的请求放弃处理或者放到队列中等待处理。限流可以有效应对突发请求过多。相关阅读:[何为限流?限流算法有哪些?](docs/high-availability/limit-request.md) + +### 降级 + +降级是从系统功能优先级的角度考虑如何应对系统故障。 + +服务降级指的是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。 + +### 熔断 + +熔断和降级是两个比较容易混淆的概念,两者的含义并不相同。 + +降级的目的在于应对系统自身的故障,而熔断的目的在于应对当前系统依赖的外部系统或者第三方系统的故障。 + +### 排队 + +另类的一种限流,类比于现实世界的排队。玩过英雄联盟的小伙伴应该有体会,每次一有活动,就要经历一波排队才能进入游戏。 + +### 集群 + +相同的服务部署多份,避免单点故障。 + +### 超时和重试机制 + +**一旦用户的请求超过某个时间得不到响应就结束此次请求并抛出异常。** 如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法在处理请求。 + +另外,重试的次数一般设为 3 次,再多次的重试没有好处,反而会加重服务器压力(部分场景使用失败重试机制会不太适合)。 + +### 灾备设计和异地多活 + +**灾备** = 容灾+备份。 + +- **备份** : 将系统所产生的的所有重要数据多备份几份。 +- **容灾** : 在异地建立两个完全相同的系统。当某个地方的系统突然挂掉,整个应用系统可以切换到另一个,这样系统就可以正常提供服务了。 + +**异地多活** 描述的是将服务部署在异地并且服务同时对外提供服务。和传统的灾备设计的最主要区别在于“多活”,即所有站点都是同时在对外提供服务的。异地多活是为了应对突发状况比如火灾、地震等自然或者认为灾害。 + +相关阅读: + +- [搞懂异地多活,看这篇就够了](https://mp.weixin.qq.com/s/T6mMDdtTfBuIiEowCpqu6Q) +- [四步构建异地多活](https://mp.weixin.qq.com/s/hMD-IS__4JE5_nQhYPYSTg) +- [《从零开始学架构》— 28 | 业务高可用的保障:异地多活架构](http://gk.link/a/10pKZ) diff --git a/_coverpage.md b/_coverpage.md deleted file mode 100644 index 7310181ac88..00000000000 --- a/_coverpage.md +++ /dev/null @@ -1,12 +0,0 @@ -

- -

- - -

Java 学习/面试指南

- -[常用资源](https://shimo.im/docs/MuiACIg1HlYfVxrj/) -[GitHub]() -[开始阅读](#java) - - diff --git a/code/java/ThreadPoolExecutorDemo/.idea/.gitignore b/code/java/ThreadPoolExecutorDemo/.idea/.gitignore deleted file mode 100644 index 5c98b428844..00000000000 --- a/code/java/ThreadPoolExecutorDemo/.idea/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Default ignored files -/workspace.xml \ No newline at end of file diff --git a/code/java/ThreadPoolExecutorDemo/.idea/checkstyle-idea.xml b/code/java/ThreadPoolExecutorDemo/.idea/checkstyle-idea.xml deleted file mode 100644 index e61c523d5e2..00000000000 --- a/code/java/ThreadPoolExecutorDemo/.idea/checkstyle-idea.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/code/java/ThreadPoolExecutorDemo/.idea/inspectionProfiles/Project_Default.xml b/code/java/ThreadPoolExecutorDemo/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 6560a98983e..00000000000 --- a/code/java/ThreadPoolExecutorDemo/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - \ No newline at end of file diff --git a/code/java/ThreadPoolExecutorDemo/.idea/misc.xml b/code/java/ThreadPoolExecutorDemo/.idea/misc.xml deleted file mode 100644 index 05483570e04..00000000000 --- a/code/java/ThreadPoolExecutorDemo/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/code/java/ThreadPoolExecutorDemo/.idea/modules.xml b/code/java/ThreadPoolExecutorDemo/.idea/modules.xml deleted file mode 100644 index 864cd10eaf7..00000000000 --- a/code/java/ThreadPoolExecutorDemo/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/code/java/ThreadPoolExecutorDemo/.idea/uiDesigner.xml b/code/java/ThreadPoolExecutorDemo/.idea/uiDesigner.xml deleted file mode 100644 index e96534fb27b..00000000000 --- a/code/java/ThreadPoolExecutorDemo/.idea/uiDesigner.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/code/java/ThreadPoolExecutorDemo/.idea/vcs.xml b/code/java/ThreadPoolExecutorDemo/.idea/vcs.xml deleted file mode 100644 index c2365ab11f9..00000000000 --- a/code/java/ThreadPoolExecutorDemo/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/code/java/ThreadPoolExecutorDemo/ThreadPoolExecutorDemo.iml b/code/java/ThreadPoolExecutorDemo/ThreadPoolExecutorDemo.iml deleted file mode 100644 index c90834f2d60..00000000000 --- a/code/java/ThreadPoolExecutorDemo/ThreadPoolExecutorDemo.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/META-INF/ThreadPoolExecutorDemo.kotlin_module b/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/META-INF/ThreadPoolExecutorDemo.kotlin_module deleted file mode 100644 index 2983af70661..00000000000 Binary files a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/META-INF/ThreadPoolExecutorDemo.kotlin_module and /dev/null differ diff --git a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/callable/CallableDemo.class b/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/callable/CallableDemo.class deleted file mode 100644 index 70861e733a4..00000000000 Binary files a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/callable/CallableDemo.class and /dev/null differ diff --git a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/callable/MyCallable.class b/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/callable/MyCallable.class deleted file mode 100644 index 283e340bdcf..00000000000 Binary files a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/callable/MyCallable.class and /dev/null differ diff --git a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/common/ThreadPoolConstants.class b/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/common/ThreadPoolConstants.class deleted file mode 100644 index 07214fc7233..00000000000 Binary files a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/common/ThreadPoolConstants.class and /dev/null differ diff --git a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/scheduledThreadPoolExecutor/ScheduledThreadPoolExecutorDemo.class b/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/scheduledThreadPoolExecutor/ScheduledThreadPoolExecutorDemo.class deleted file mode 100644 index dc98bc8f2fe..00000000000 Binary files a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/scheduledThreadPoolExecutor/ScheduledThreadPoolExecutorDemo.class and /dev/null differ diff --git a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/scheduledThreadPoolExecutor/Task.class b/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/scheduledThreadPoolExecutor/Task.class deleted file mode 100644 index 9920a634de1..00000000000 Binary files a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/scheduledThreadPoolExecutor/Task.class and /dev/null differ diff --git a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/threadPoolExecutor/MyRunnable.class b/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/threadPoolExecutor/MyRunnable.class deleted file mode 100644 index c1cf37ca4ad..00000000000 Binary files a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/threadPoolExecutor/MyRunnable.class and /dev/null differ diff --git a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/threadPoolExecutor/ThreadPoolExecutorDemo.class b/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/threadPoolExecutor/ThreadPoolExecutorDemo.class deleted file mode 100644 index 80c34f4d66e..00000000000 Binary files a/code/java/ThreadPoolExecutorDemo/out/production/ThreadPoolExecutorDemo/threadPoolExecutor/ThreadPoolExecutorDemo.class and /dev/null differ diff --git a/code/java/ThreadPoolExecutorDemo/src/callable/CallableDemo.java b/code/java/ThreadPoolExecutorDemo/src/callable/CallableDemo.java deleted file mode 100644 index be762bda262..00000000000 --- a/code/java/ThreadPoolExecutorDemo/src/callable/CallableDemo.java +++ /dev/null @@ -1,49 +0,0 @@ -package callable; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import static common.ThreadPoolConstants.CORE_POOL_SIZE; -import static common.ThreadPoolConstants.KEEP_ALIVE_TIME; -import static common.ThreadPoolConstants.MAX_POOL_SIZE; -import static common.ThreadPoolConstants.QUEUE_CAPACITY; - -public class CallableDemo { - public static void main(String[] args) { - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy()); - - List> futureList = new ArrayList<>(); - Callable callable = new MyCallable(); - for (int i = 0; i < 10; i++) { - //提交任务到线程池 - Future future = executor.submit(callable); - //将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值 - futureList.add(future); - } - for (Future fut : futureList) { - try { - System.out.println(new Date() + "::" + fut.get()); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - } - //关闭线程池 - executor.shutdown(); - } -} - diff --git a/code/java/ThreadPoolExecutorDemo/src/callable/MyCallable.java b/code/java/ThreadPoolExecutorDemo/src/callable/MyCallable.java deleted file mode 100644 index 53d540fb75c..00000000000 --- a/code/java/ThreadPoolExecutorDemo/src/callable/MyCallable.java +++ /dev/null @@ -1,13 +0,0 @@ -package callable; - -import java.util.concurrent.Callable; - -public class MyCallable implements Callable { - - @Override - public String call() throws Exception { - Thread.sleep(1000); - //返回执行当前 Callable 的线程名字 - return Thread.currentThread().getName(); - } -} diff --git a/code/java/ThreadPoolExecutorDemo/src/common/ThreadPoolConstants.java b/code/java/ThreadPoolExecutorDemo/src/common/ThreadPoolConstants.java deleted file mode 100644 index 2018e86f6c7..00000000000 --- a/code/java/ThreadPoolExecutorDemo/src/common/ThreadPoolConstants.java +++ /dev/null @@ -1,11 +0,0 @@ -package common; - -public class ThreadPoolConstants { - public static final int CORE_POOL_SIZE = 5; - public static final int MAX_POOL_SIZE = 10; - public static final int QUEUE_CAPACITY = 100; - public static final Long KEEP_ALIVE_TIME = 1L; - private ThreadPoolConstants(){ - - } -} diff --git a/code/java/ThreadPoolExecutorDemo/src/threadPoolExecutor/MyRunnable.java b/code/java/ThreadPoolExecutorDemo/src/threadPoolExecutor/MyRunnable.java deleted file mode 100644 index 4ebf2dcbce7..00000000000 --- a/code/java/ThreadPoolExecutorDemo/src/threadPoolExecutor/MyRunnable.java +++ /dev/null @@ -1,36 +0,0 @@ -package threadPoolExecutor; - -import java.util.Date; - -/** - * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 - * @author shuang.kou - */ -public class MyRunnable implements Runnable { - - private String command; - - public MyRunnable(String s) { - this.command = s; - } - - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); - processCommand(); - System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); - } - - private void processCommand() { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - @Override - public String toString() { - return this.command; - } -} diff --git a/code/java/ThreadPoolExecutorDemo/src/threadPoolExecutor/ThreadPoolExecutorDemo.java b/code/java/ThreadPoolExecutorDemo/src/threadPoolExecutor/ThreadPoolExecutorDemo.java deleted file mode 100644 index 2e510cd6562..00000000000 --- a/code/java/ThreadPoolExecutorDemo/src/threadPoolExecutor/ThreadPoolExecutorDemo.java +++ /dev/null @@ -1,39 +0,0 @@ -package threadPoolExecutor; - -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import static common.ThreadPoolConstants.CORE_POOL_SIZE; -import static common.ThreadPoolConstants.KEEP_ALIVE_TIME; -import static common.ThreadPoolConstants.MAX_POOL_SIZE; -import static common.ThreadPoolConstants.QUEUE_CAPACITY; - - -public class ThreadPoolExecutorDemo { - - public static void main(String[] args) { - - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy()); - - for (int i = 0; i < 10; i++) { - //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) - Runnable worker = new MyRunnable("" + i); - //执行Runnable - executor.execute(worker); - } - //终止线程池 - executor.shutdown(); - while (!executor.isTerminated()) { - } - System.out.println("Finished all threads"); - } -} diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js new file mode 100644 index 00000000000..b9ce3f6d372 --- /dev/null +++ b/docs/.vuepress/config.js @@ -0,0 +1,383 @@ +const { config } = require("vuepress-theme-hope"); + +module.exports = config({ + title: "JavaGuide", + description: "Java学习&&面试指南", + dest: "./dist", + head: [ + [ + "script", + { src: "https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js" }, + ], + [ + "script", + { + src: "https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js", + }, + ], + ["script", { src: "https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js" }], + [ + "script", + { src: "https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js" }, + ], + // 添加百度统计 + [ + "script",{}, + `var _hmt = _hmt || []; + (function() { + var hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?5dd2e8c97962d57b7b8fea1737c01743"; + var s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + })();` + ] + ], + + themeConfig: { + logo: "/logo.png", + hostname: "https://javaguide.cn/", + author: "Guide哥", + repo: "https://github.com/Snailclimb/JavaGuide", + nav: [ + { text: "Java面试指南", icon: "java", link: "/", }, + { text: "Java面试指北", icon: "java", link: "https://www.yuque.com/docs/share/f37fc804-bfe6-4b0d-b373-9c462188fec7?#%20%E3%80%8A%E3%80%8AJava%E9%9D%A2%E8%AF%95%E8%BF%9B%E9%98%B6%E6%8C%87%E5%8C%97%20%20%E6%89%93%E9%80%A0%E4%B8%AA%E4%BA%BA%E7%9A%84%E6%8A%80%E6%9C%AF%E7%AB%9E%E4%BA%89%E5%8A%9B%E3%80%8B%E3%80%8B", }, + { + text: "Java精选", icon: "file", icon: "java", + items: [ + { text: "Java书单精选", icon: "book", link: "https://gitee.com/SnailClimb/awesome-cs" }, + { text: "Java学习路线", icon: "luxianchaxun", link: "https://zhuanlan.zhihu.com/p/379041500" }, + { text: "Java开源项目精选", icon: "git", link: "https://gitee.com/SnailClimb/awesome-java" } + ], + }, + { text: "IDEA指南", icon: "intellijidea", link: "/idea-tutorial/", }, + { text: "开发工具", icon: "Tools", link: "/tools/", }, + { + text: "PDF资源", icon: "pdf", + items: [ + { text: "JavaGuide面试突击版", link: "https://t.1yb.co/Fy1e", }, + { text: "消息队列常见知识点&面试题总结", link: "https://t.1yb.co/Fy0u", }, + { text: "图解计算机基础!", link: "https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=100021725&idx=1&sn=2db9664ca25363139a81691043e9fd8f&chksm=4ea19a1679d61300d8990f7e43bfc7f476577a81b712cf0f9c6f6552a8b219bc081efddb5c54#rd" } + ], + }, + { + text: "关于作者", icon: "zuozhe", link: "/about-the-author/" + }, + ], + sidebar: { + "/about-the-author/": [ + "internet-addiction-teenager", "feelings-after-one-month-of-induction-training" + ], + // 应该把更精确的路径放置在前边 + '/tools/': [ + { + title: "数据库", + icon: "database", + prefix: "database/", + collapsable: false, + children: ["CHINER", "DBeaver", "screw"] + }, + { + title: "Git", + icon: "git", + prefix: "git/", + collapsable: false, + children: ["git-intro", "github技巧"] + }, + { + title: "Docker", + icon: "docker1", + prefix: "docker/", + collapsable: false, + children: ["docker", "docker从入门到实战"] + }, + ], + '/idea-tutorial/': + [ + { + title: "IDEA小技巧", + icon: "creative", + prefix: "idea-tips/", + collapsable: false, + children: [ + "idea-refractor-intro", + "idea-plug-in-development-intro", + "idea-source-code-reading-skills", + ] + }, + { + title: "IDEA插件推荐", + icon: "plugin", + collapsable: false, + prefix: "idea-plugins/", + children: [ + "shortcut-key", "idea-themes", "improve-code", "interface-beautification", + "camel-case", "code-glance", "code-statistic", + "git-commit-template", "gson-format", "idea-features-trainer", "jclasslib", + "maven-helper", "rest-devlop", "save-actions", "sequence-diagram", "translation", + "others" + ] + }, + ], + // 必须放在最后面 + '/': [{ + title: "Java", icon: "java", prefix: "java/", + children: [ + { + title: "基础", prefix: "basis/", + children: [ + "java基础知识总结", + { + title: "重要知识点", + children: ["反射机制详解", "代理模式详解", "io模型详解"], + },], + }, + { + title: "容器", prefix: "collection/", + children: [ + "java集合框架基础知识&面试题总结", "java集合使用注意事项", + { + title: "源码分析", + children: ["arraylist-source-code", "hashmap-source-code", "concurrent-hash-map-source-code"], + },], + }, + { + title: "并发编程", prefix: "concurrent/", + children: [ + "java并发基础常见面试题总结", "java并发进阶常见面试题总结", + { + title: "重要知识点", + children: ["java线程池学习总结", "并发容器总结", "拿来即用的java线程池最佳实践", "aqs原理以及aqs同步组件总结", "reentrantlock", + "atomic原子类总结", "threadlocal", "completablefuture-intro"], + }, + ], + }, + { + title: "JVM", prefix: "jvm/", + children: ["memory-area", "jvm-garbage-collection", "class-file-structure", "class-loading-process", "classloader", "jvm-parameters-intro", "jvm-intro", "jdk-monitoring-and-troubleshooting-tools"], + }, + { + title: "新特性", prefix: "new-features/", + children: ["java8-common-new-features", "java8-tutorial-translate", "java新特性总结"], + }, + { + title: "小技巧", prefix: "tips/", + children: ["locate-performance-problems/手把手教你定位常见Java性能问题", "jad"], + }, + ], + }, + { + title: "计算机基础", icon: "computer", prefix: "cs-basics/", + children: [ + { + title: "计算机网络", prefix: "network/", icon: "network", + children: [ + "计算机网络常见面试题", "谢希仁老师的《计算机网络》内容总结", "HTTPS中的TLS" + ], + }, + { + title: "操作系统", prefix: "operating-system/", icon: "caozuoxitong", + children: [ + "操作系统常见面试题&知识点总结", "linux-intro", "shell-intro" + ], + }, + { + title: "数据结构", prefix: "data-structure/", icon: "people-network-full", + children: [ + "线性数据结构", "图", "堆", "树", "红黑树", "bloom-filter" + ], + }, + { + title: "算法", prefix: "algorithms/", icon: "suanfaku", + children: [ + "几道常见的字符串算法题", "几道常见的链表算法题", "剑指offer部分编程题" + ], + }, + ], + + }, + { + title: "数据库", icon: "database", prefix: "database/", + children: [ + "数据库基础知识", + "字符集", + { + title: "MySQL", prefix: "mysql/", + children: [ + "mysql知识点&面试题总结", + "a-thousand-lines-of-mysql-study-notes", + "mysql-high-performance-optimization-specification-recommendations", + "mysql-index", "mysql-logs", "transaction-isolation-level", + "innodb-implementation-of-mvcc", "how-sql-executed-in-mysql", + "some-thoughts-on-database-storage-time" + ], + }, + { + title: "Redis", prefix: "redis/", + children: ["redis知识点&面试题总结", "3-commonly-used-cache-read-and-write-strategies"], + }, + ], + }, + { + title: "系统设计", icon: "xitongsheji", prefix: "system-design/", + children: [ + { + title: "基础", prefix: "basis/", icon: "jibendebasic", + children: [ + "RESTfulAPI", + "naming", + ], + }, + { + title: "常用框架", prefix: "framework/", icon: "framework", + children: [{ + title: "Spring", prefix: "spring/", + children: ["Spring常见问题总结", "Spring&SpringBoot常用注解总结", "Spring事务总结", "Spring设计模式总结", "SpringBoot自动装配原理"] + }, + "mybatis/mybatis-interview", "netty", + { + title: "SpringCloud", prefix: "springcloud/", + children: ["springcloud-intro"] + }, + ], + }, + { + title: "安全", prefix: "security/", icon: "security-fill", + children: ["basis-of-authority-certification", "jwt优缺点分析以及常见问题解决方案", "sso-intro", "数据脱敏"] + }, + "定时任务" + ], + }, + { + title: "分布式", icon: "distributed-network", prefix: "distributed-system/", + children: [ + { + title: "理论&算法", prefix: "理论&算法/", + children: ["cap&base理论", "paxos&raft算法"], + }, + "api-gateway", "distributed-id", + { + title: "rpc", prefix: "rpc/", + children: ["dubbo", "why-use-rpc"] + }, + "distributed-transaction", + { + title: "分布式协调", prefix: "分布式协调/", + children: ["zookeeper/zookeeper-intro", "zookeeper/zookeeper-plus", "zookeeper/zookeeper-in-action"] + }, + ], + }, { + title: "高性能", icon: "gaojixiaozuzhibeifen", prefix: "high-performance/", + children: [ + "读写分离&分库分表", "负载均衡", + { + title: "消息队列", prefix: "message-queue/", + children: ["message-queue", "kafka知识点&面试题总结", "rocketmq-intro", "rocketmq-questions", "rabbitmq-intro"], + }, + ], + }, { + title: "高可用", icon: "CalendarAvailability-1", prefix: "high-availability/", + children: [ + "高可用系统设计", "limit-request", "降级&熔断", "超时和重试机制", "集群", "灾备设计和异地多活", "性能测试" + ], + }], + }, + blog: { + intro: "/intro/", + sidebarDisplay: "mobile", + links: { + Zhihu: "https://www.zhihu.com/people/javaguide", + Github: "https://github.com/Snailclimb", + Gitee: "https://gitee.com/SnailClimb", + }, + }, + + footer: { + display: true, + content: '鄂ICP备2020015769号-1', + }, + + copyright: { + status: "global", + }, + + git: { + timezone: "Asia/Shanghai", + }, + + mdEnhance: { + enableAll: true, + presentation: { + plugins: [ + "highlight", + "math", + "search", + "notes", + "zoom", + "anything", + "audio", + "chalkboard", + ], + }, + }, + + pwa: { + favicon: "/favicon.ico", + cachePic: true, + apple: { + icon: "/assets/icon/apple-icon-152.png", + statusBarColor: "black", + }, + msTile: { + image: "/assets/icon/ms-icon-144.png", + color: "#ffffff", + }, + manifest: { + icons: [ + { + src: "/assets/icon/chrome-mask-512.png", + sizes: "512x512", + purpose: "maskable", + type: "image/png", + }, + { + src: "/assets/icon/chrome-mask-192.png", + sizes: "192x192", + purpose: "maskable", + type: "image/png", + }, + { + src: "/assets/icon/chrome-512.png", + sizes: "512x512", + type: "image/png", + }, + { + src: "/assets/icon/chrome-192.png", + sizes: "192x192", + type: "image/png", + }, + ], + shortcuts: [ + { + name: "Guide", + short_name: "Guide", + url: "/guide/", + icons: [ + { + src: "/assets/icon/guide-maskable.png", + sizes: "192x192", + purpose: "maskable", + type: "image/png", + }, + { + src: "/assets/icon/guide-monochrome.png", + sizes: "192x192", + purpose: "monochrome", + type: "image/png", + }, + ], + }, + ], + }, + }, + }, +}); diff --git a/docs/.vuepress/public/assets/icon/apple-icon-152.png b/docs/.vuepress/public/assets/icon/apple-icon-152.png new file mode 100644 index 00000000000..3eabbeb1dc3 Binary files /dev/null and b/docs/.vuepress/public/assets/icon/apple-icon-152.png differ diff --git a/docs/.vuepress/public/assets/icon/chrome-192.png b/docs/.vuepress/public/assets/icon/chrome-192.png new file mode 100644 index 00000000000..851ad3a224d Binary files /dev/null and b/docs/.vuepress/public/assets/icon/chrome-192.png differ diff --git a/docs/.vuepress/public/assets/icon/chrome-512.png b/docs/.vuepress/public/assets/icon/chrome-512.png new file mode 100644 index 00000000000..2fb9f40be37 Binary files /dev/null and b/docs/.vuepress/public/assets/icon/chrome-512.png differ diff --git a/docs/.vuepress/public/assets/icon/chrome-mask-192.png b/docs/.vuepress/public/assets/icon/chrome-mask-192.png new file mode 100644 index 00000000000..530977a9e69 Binary files /dev/null and b/docs/.vuepress/public/assets/icon/chrome-mask-192.png differ diff --git a/docs/.vuepress/public/assets/icon/chrome-mask-512.png b/docs/.vuepress/public/assets/icon/chrome-mask-512.png new file mode 100644 index 00000000000..a4f90ae484b Binary files /dev/null and b/docs/.vuepress/public/assets/icon/chrome-mask-512.png differ diff --git a/docs/.vuepress/public/assets/icon/guide-maskable.png b/docs/.vuepress/public/assets/icon/guide-maskable.png new file mode 100644 index 00000000000..75449b6098b Binary files /dev/null and b/docs/.vuepress/public/assets/icon/guide-maskable.png differ diff --git a/docs/.vuepress/public/assets/icon/guide-monochrome.png b/docs/.vuepress/public/assets/icon/guide-monochrome.png new file mode 100644 index 00000000000..5b1dc406d6a Binary files /dev/null and b/docs/.vuepress/public/assets/icon/guide-monochrome.png differ diff --git a/docs/.vuepress/public/assets/icon/ms-icon-144.png b/docs/.vuepress/public/assets/icon/ms-icon-144.png new file mode 100644 index 00000000000..24641244228 Binary files /dev/null and b/docs/.vuepress/public/assets/icon/ms-icon-144.png differ diff --git a/docs/.vuepress/public/favicon.ico b/docs/.vuepress/public/favicon.ico new file mode 100644 index 00000000000..3a14635ac46 Binary files /dev/null and b/docs/.vuepress/public/favicon.ico differ diff --git a/docs/.vuepress/public/logo.png b/docs/.vuepress/public/logo.png new file mode 100644 index 00000000000..7675a8b5aa3 Binary files /dev/null and b/docs/.vuepress/public/logo.png differ diff --git a/docs/.vuepress/public/logo.svg b/docs/.vuepress/public/logo.svg new file mode 100644 index 00000000000..fdfe9e6c1ce --- /dev/null +++ b/docs/.vuepress/public/logo.svg @@ -0,0 +1,317 @@ + + + + diff --git a/docs/.vuepress/public/me.png b/docs/.vuepress/public/me.png new file mode 100644 index 00000000000..cfa3a6ea375 Binary files /dev/null and b/docs/.vuepress/public/me.png differ diff --git a/docs/.vuepress/styles/index.styl b/docs/.vuepress/styles/index.styl new file mode 100644 index 00000000000..e67a4cf0381 --- /dev/null +++ b/docs/.vuepress/styles/index.styl @@ -0,0 +1,2 @@ +// import icon +@import 'http://at.alicdn.com/t/font_2922463_74fu8o5xg3.css' \ No newline at end of file diff --git a/docs/about-the-author/feelings-after-one-month-of-induction-training.md b/docs/about-the-author/feelings-after-one-month-of-induction-training.md new file mode 100644 index 00000000000..9f81220150f --- /dev/null +++ b/docs/about-the-author/feelings-after-one-month-of-induction-training.md @@ -0,0 +1,19 @@ +# 入职培训一个月后的感受 + +不知不觉已经入职一个多月了,在入职之前我没有在某个公司实习过或者工作过,所以很多东西刚入职工作的我来说还是比较新颖的。学校到职场的转变,带来了角色的转变,其中的差别因人而异。对我而言,在学校的时候课堂上老师课堂上教的东西,自己会根据自己的兴趣选择性接受,甚至很多课程你不想去上的话,还可以逃掉。到了公司就不一样了,公司要求你会的技能你不得不学,除非你不想干了。在学校的时候大部分人编程的目的都是为了通过考试或者找到一份好工作,真正靠自己兴趣支撑起来的很少,到了工作岗位之后我们编程更多的是因为工作的要求,相比于学校的来说会一般会更有挑战而且压力更大。在学校的时候,我们最重要的就是对自己负责,我们不断学习知识去武装自己,但是到了公司之后我们不光要对自己负责,更要对公司负责,毕竟公司出钱请你过来,不是让你一直 on beach 的。 + +刚来公司的时候,因为公司要求,我换上了 Mac 电脑。由于之前一直用的是 Windows 系统,所以非常不习惯。刚开始用 Mac 系统的时候笨手笨脚,自己会很明显的感觉自己的编程效率降低了至少 3 成。当时内心还是挺不爽的,心里也总是抱怨为什么不直接用 Windows 系统或者 Linux 系统。不过也挺奇怪,大概一个星期之后,自己就开始慢慢适应使用 Mac 进行编程,甚至非常喜欢。我这里不想对比 Mac 和 Windows 编程体验哪一个更好,我觉得还是因人而异,相同价位的 Mac 的配置相比于 Windows确实要被甩几条街。不过 Mac 的编程和使用体验确实不错,当然你也可以选择使用 Linux 进行日常开发,相信一定很不错。 另外,Mac 不能玩一些主流网络游戏,对于一些克制不住自己想玩游戏的朋友是一个不错的选择。 + +不得不说 ThoughtWorks 的培训机制还是很不错的。应届生入职之后一般都会安排培训,与往年不同的是,今年的培训多了中国本地班(TWU-C)。作为本地班的第一期学员,说句心里话还是很不错。8周的培训,除了工作需要用到的基本技术比如ES6、SpringBoot等等之外,还会增加一些新员工基本技能的培训比如如何高效开会、如何给别人正确的提 Feedback、如何对代码进行重构、如何进行 TDD 等等。培训期间不定期的有活动,比如Weekend Trip、 City Tour、Cake time等等。最后三周还会有一个实际的模拟项目,这个项目基本和我们正式工作的实际项目差不多,我个人感觉很不错。目前这个项目已经正式完成了一个迭代,我觉得在做项目的过程中,收获最大的不是项目中使用的技术,而是如何进行团队合作、如何正确使用 Git 团队协同开发、一个完成的迭代是什么样子的、做项目的过程中可能遇到那些问题、一个项目运作的完整流程等等。 + +ThoughtWorks 非常提倡分享、提倡帮助他人成长,这一点在公司的这段时间深有感触。培训期间,我们每个人会有一个 Trainer 负责,Trainer 就是日常带我们上课和做项目的同事,一个 Trainer 大概会负责5-6个人。Trainer不定期都会给我们最近表现的 Feedback( 反馈) ,我个人觉得这个并不是这是走走形式,Trainer 们都很负责,很多时候都是在下班之后找我们聊天。同事们也都很热心,如果你遇到问题,向别人询问,其他人如果知道的话一般都会毫无保留的告诉你,如果遇到大部分都不懂的问题,甚至会组织一次技术 Session 分享。上周五我在我们小组内进行了一次关于 Feign 远程调用的技术分享,因为 team 里面大家对这部分知识都不太熟悉,但是后面的项目进展大概率会用到这部分知识。我刚好研究了这部分内容,所以就分享给了组内的其他同事,以便于项目更好的进行。 + + 另外,ThoughtWorks 也是一家非常提倡 Feedback( 反馈) 文化的公司,反馈是告诉人们我们对他们的表现的看法以及他们应该如何更好地做到这一点。刚开始我并没有太在意,慢慢地自己确实感觉到正确的进行反馈对他人会有很大的帮助。因为人在做很多事情的时候,会很难发现别人很容易看到的一些小问题。就比如一个很有趣的现象一样,假如我们在做项目的时候没有测试这个角色,如果你完成了自己的模块,并且自己对这个模块测试了很多遍,你发现已经没啥问题了。但是,到了实际使用的时候会很大概率出现你之前从来没有注意的问题。解释这个问题的说法是:每个人的视野或多或少都是有盲点的,这与我们的关注点息息相关。对于自己做的东西,很多地方自己测试很多遍都不会发现,但是如果让其他人帮你进行测试的话,就很大可能会发现很多显而易见的问题。 + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/feedback.png) + +工作之后,平时更新公众号、专栏还有维护 Github 的时间变少了。实际上,很多时候下班回来后,都有自己的时间来干自己的事情,但是自己也总是找工作太累或者时间比较零散的接口来推掉了。到了今天,翻看 Github 突然发现 14 天前别人在 Github 上给我提的 pr 我还没有处理。这一点确实是自己没有做好的地方,没有合理安排好自己的时间。实际上自己有很多想写的东西,后面会慢慢将他们提上日程。工作之后,更加发现下班后的几个小时如何度过确实很重要 ,如果你觉得自己没有完成好自己白天该做的工作的话,下班后你可以继续忙白天没有忙完的工作,如果白天的工作对于你游刃有余的话,下班回来之后,你大可去干自己感兴趣的事情,学习自己感兴趣的技术。做任何事情都要基于自身的基础,切不可好高骛远。 + +工作之后身边也会有很多厉害的人,多从他人身上学习我觉得是每个职场人都应该做的。这一届和我们一起培训的同事中,有一些技术很厉害的,也有一些技术虽然不是那么厉害,但是组织能力以及团队协作能力特别厉害的。有一个特别厉害的同事,在我们还在学 SpringBoot 各种语法的时候,他自己利用业余时间写了一个简化版的 SpringBoot ,涵盖了 Spring 的一些常用注解比如 `@RestController`、`@Autowried`、`@Pathvairable`、`@RestquestParam`等等(已经联系这位同事,想让他开源一下,后面会第一时间同步到公众号,期待一下吧!)。我觉得这位同事对于编程是真的有兴趣,他好像从初中就开始接触编程了,对于各种底层知识也非常感兴趣,自己写过实现过很多比较底层的东西。他的梦想是在 Github 上造一个 20k Star 以上的轮子。我相信以这位同事的能力一定会达成目标的,在这里祝福这位同事,希望他可以尽快实现这个目标。 + +这是我入职一个多月之后的个人感受,很多地方都是一带而过,后面我会抽时间分享自己在公司或者业余学到的比较有用的知识给各位,希望看过的人都能有所收获。 \ No newline at end of file diff --git a/docs/about-the-author/internet-addiction-teenager.md b/docs/about-the-author/internet-addiction-teenager.md new file mode 100644 index 00000000000..52458b4a0c0 --- /dev/null +++ b/docs/about-the-author/internet-addiction-teenager.md @@ -0,0 +1,104 @@ +# 我曾经也是网瘾少年 + +聊到高考,无数人都似乎有很多话说。今天就假借高考的名义,**简单**来聊聊我的求学经历吧!因为我自己的求学经历真的还不算平淡,甚至有点魔幻,所以还是有很多话想要说的。这篇文章大概会从我的初中一直介绍到大学,每一部分我都不会花太多篇幅。实际上,每一段经历我都可以增加很多“有趣”的经历,考虑到篇幅问题,以后有机会再慢慢说吧! + +整个初中我都属于有点网瘾少年的状态,不过初三的时候稍微克制一些。到了高二下学期的时候,自己才对游戏没有真的没有那么沉迷了。 + +另外,关于大学的详细经历我已经在写了。想要知道我是如何从一个普通的不能再普通的少年慢慢成长起来的朋友不要错过~ + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-08/0e370ac2-5f96-4e17-9ff4-8cc78ef72f19-20200802173544441.png) + +**以下所有内容皆是事实,没有任何夸大的地方,稍微有一点点魔幻。** + +## 01 刚开始接触电脑 + +最开始接触电脑是在我五年级的时候,那时候家里没电脑,都是在黑网吧玩的。我现在已经记不清当时是被哥哥还是姐姐带进网吧的了。 + +起初的时候,自己就是玩玩流行蝴蝶剑、单机摩托之类的单机游戏。但是,也没有到沉迷的地步,只是觉得这东西确实挺好玩的。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-08/2a6021b9-e7a0-41c4-b69e-a652f7bc3e12-20200802173601289.png) + +开始有网瘾是在小学毕业的时候,在我玩了一款叫做 **QQ 飞车**的游戏之后(好像是六年级就开始玩了)。我艹,当时真的被这游戏吸引了。**每天上课都幻想自己坐在车里面飘逸,没错,当时就觉得秋名山车神就是我啦!** + +我记得,那时候上网还不要身份证,10 元办一张网卡就行了,网费也是一元一小时。但凡,我口袋里有余钱,我都会和我的小伙伴奔跑到网吧一起玩 QQ 飞车。Guide 的青回啊!说到这,我情不自禁地打开自己的 Windows 电脑,下载了 Wegame ,然后下载了 QQ 飞车。 + +到了初二的时候,就没玩 QQ 飞车了。我的等级也永久定格在了 **120** 级,这个等级在当时那个升级难的一匹的年代,算的上非常高的等级了。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/b488618c-3c25-4bc9-afd4-7324e27553bd-20200802175534614.png) + +## 02 初二网瘾爆发 + +网瘾爆发是在上了初中之后。初二的时候,最为猖狂,自己当时真的是太痴迷 **穿越火线** 了,每天上课都在想像自己拿起枪横扫地方阵营的场景。除了周末在网吧度过之外,我经常每天早上还会起早去玩别人包夜留下的机子,毕竟那时候上学也没什么钱嘛! + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/9e94bb35-650d-4cad-8e69-40043fb4ec3d-20200802173632800.png) + +那时候成绩挺差的。这样说吧!我当时在很普通的一个县级市的高中,全年级有 500 来人,我基本都是在 280 名左右。 + +而且,整个初二我都没有学物理。因为开学不久的一次物理课,物理老师误会我在上课吃东西还狡辩,闪了我一巴掌。从此,我上物理课就睡觉,平常的物理考试就交白卷。那时候心里一直记仇,想着以后自己长大了把这个物理暴打他一顿。 + +初中时候的觉悟是在初三上学期的时候,当时就突然意识到自己马上就要升高中了。为了让自己能在家附近上学,因为当时我家就在我们当地的二中附近(_附近网吧多是主要原因,哈哈_)。年级前 80 的话基本才有可能考得上二中。**经过努力,初三上学期的第一次月考我直接从 280 多名进不到了年级 50 多名。当时,还因为进步太大,被当做进步之星在讲台上给整个年级做演讲。**那也是我第一次在这么多人面前讲话,挺紧张的,但是挺爽的。 + +**其实在初三的时候,我的网瘾还是很大。不过,我去玩游戏的前提都是自己把所有任务做完,并且上课听讲也很认真。** 我参加高中提前考试前的一个晚上,我半夜12点乘着妈妈睡着,跑去了网吧玩CF到凌晨 3点多回来。那一次我被抓了现行,到家之后发现妈妈就坐在客厅等我,训斥一顿后,我就保证以后不再晚上偷偷跑出去了(*其实整个初二我通宵了无数次,每个周五晚上都回去通宵*)。 + +_这里要说明一点:我的智商我自己有自知之明的,属于比较普通的水平吧! 前进很大的主要原因是自己基础还行,特别是英语和物理。英语是因为自己喜欢,加上小学就学了很多初中的英语课程。 物理的话就很奇怪,虽然初二也不怎么听物理课,也不会物理,但是到了初三之后自己就突然开窍了。真的!我现在都感觉很奇怪。然后,到了高中之后,我的英语和物理依然是我最好的两门课。大学的兼职,我出去做家教都是教的高中物理。_ + +后面,自己阴差阳错参加我们那个县级市的提前招生考试,然后就到了我们当地的二中,也没有参加中考。 + +## 03 高中生活 + +上了高中的之后,我上课就偷偷看小说,神印王座、斗罗大陆很多小说都是当时看的。中午和晚上回家之后,就在家里玩几把 DNF,当时家里也买了电脑。没记错的话,到我卸载 DNF 的时候已经练了 4 个满级的号。大量时间投入在游戏和小说上,我成功把自己从学校最好的小班玩到奥赛班,然后再到平行班。有点魔幻吧! + +高中觉悟是在高二下学期的时候,当时是真的觉悟了,就突然觉得游戏不香了,觉得 DNF 也不好玩了。我妈妈当时还很诧异,还奇怪地问我:“怎么不玩游戏了?”(*我妈属于不怎么管我玩游戏的,她觉得这东西还是要靠自觉*)。 + +*当时,自己就感觉这游戏没啥意思了。内心的真实写照是:“我练了再多的满级的DNF账号有啥用啊?以后有钱了,直接氪金不久能很牛逼嘛!” 就突然觉悟了!* + +然后,我就开始牟足劲学习。当时,理科平行班大概有 7 个,每次考试都是平行班之间会单独拍一个名次。 后面的话,自己基本每次都能在平行班得第一,并且很多时候都是领先第二名个 30 来分。因为成绩还算亮眼,高三上学期快结束的时候,我就向年级主任申请去了奥赛班。 + +## 04 高考前的失眠 + +> **失败之后,不要抱怨外界因素,自始至终实际都是自己的问题,自己不够强大!** 然后,高考前的失眠也是我自己问题,要怪只能怪自己,别的没有任何接口。 + +我的高考经历其实还蛮坎坷的,毫不夸张的说,高考那今天可能是我到现在为止,经历的最难熬的时候,特别是在晚上。 + +我在高考那几天晚上都经历了失眠,想睡都睡不着那种痛苦想必很多人或许都体验过。 + +其实我在之前是从来没有过失眠的经历的。高考前夕,因为害怕自己睡不着,所以,我提前让妈妈去买了几瓶老师推荐的安神补脑液。我到现在还记得这个安神补脑液是敖东牌的。 + +高考那几天的失眠,我觉得可能和我喝了老师推荐的安神补脑液有关系,又或者是我自己太过于紧张了。因为那几天睡觉总会感觉有很多蚂蚁在身上爬一样,然后还起了一些小痘痘。 + +然后,这里要格外说明一点,避免引起误导: **睡不着本身就是自身的问题,上述言论并没有责怪这个补脑液的意思。** 另外, 这款安神补脑液我去各个平台都查了一下,发现大家对他的评价都挺好,和我们老师当时推荐的理由差不多。如果大家需要改善睡眠的话,可以咨询相关医生之后尝试一下。 + +## 05 还算充实的大学生活 + +高考成绩出来之后,比一本线高了 20 多分。自己挺不满意的,因为比平时考差了太多。加上自己泪点很低,就哭了一上午之后。后面,自我安慰说以后到了大学好好努力也是一样的。然后,我的第一志愿学校就报了长江大学,第一志愿专业就报了计算机专业。 + +后面,就开始了自己还算充实的大学生活。 + +大一的时候,满腔热血,对于高考结果的不满意,化作了我每天早起的动力。雷打不动,每天早上 6点左右就出去背英语单词。这也奠定了我后面的四六级都是一次过,并且六级的成绩还算不错。大一那年的暑假,我还去了孝感当了主管,几乎从无到有办了 5 个家教点。不过,其中两个家教点的话,是去年都已经办过的,没有其他几个那么费心。 + +![被我的学生 diss](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/5a47eb4614934a25b8ea1a83cafac43d-20200802173912511.png) + +大二的时候,加了学校一个偏技术方向的传媒组织(做网站、APP 之类的工作),后面成功当了副站长。在大二的时候,我才开始因为组织需要而接触 Java,不过当时主要学的是安卓开发。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/b16201d0-37d4-462a-a5e6-bf95ca503d39-20200802174034108.png) + +大三的时候,正式确定自己要用 Java 语言找工作,并且要走 Java 后台(当时感觉安卓后台在求职时长太不吃香了)。我每天都在寝室学习 Java 后台开发,自己看视频,看书,做项目。我的开源项目 JavaGuide 和公众号都是这一年创建的。这一年,我大部分时间都是在寝室学习。带上耳机之后,即使室友在玩游戏或者追剧,都不会对我有什么影响。 + +我记得当时自己独立做项目的时候,遇到了很多问题。**就很多时候,你看书很容易就明白的东西,等到你实践的时候,总是会遇到一些小问题。我一般都是通过 Google 搜索解决的,用好搜索引擎真的能解决自己 99% 的问题。** + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-08/d30eef29-3a73-483d-9a4a-d63f41271fb4-20200802174048832.png) + +大四的时候,开始找工作。我是参加的秋招,开始的较晚,基本很多公司都没有 HC 了。这点需要 diss 一下学校了,你其他地方都很好,但是,大四的时候就不要再上课点名了吧!然后,**希望国内的学校尽量能多给学生点机会吧!很多人连春招和秋招都不清楚,毕业了连实习都没实习过。** + +## 06 一些心里话 + +关于大学要努力学习专业知识、多去读书馆这类的鸡汤,Guide 就不多说了。就谈几条自己写这篇文章的时候,想到了一些心理话吧! + +1. **不要抱怨学校** :高考之后,不论你是 985、211 还是普通一本,再或者是 二本、三本,都不重要了,好好享受高考之后的生活。如果你觉得自己考的不满意的话,就去复读,没必要天天抱怨,复读的一年在你的人生长河里根本算不了什么的! +2. **克制** :大学的时候,克制住自己,诱惑太多了。你不去上课,在寝室睡到中午,都没人管你。你的努力不要只是感动自己!追求形式的努力不过是你打得幌子而已。到了社会之后,这个说法依然适用! 说一个真实的发生在我身边的事情吧!高中的时候有一个特别特别特别努力的同班同学,家里的条件也很差,大学之前没有接触过手机和游戏。后来到了大学之后,因为接触了手机还有手机游戏,每天沉迷,不去上课。最后,直接就导致大学没读完就离开了。我听完我的好朋友给我说了之后,非常非常非常诧异!真的太可惜了! +3. **不要总抱怨自己迷茫,多和优秀的学长学姐沟通交流。** +4. **不知道做什么的时候,就把手头的事情做好比如你的专业课学习。** + +*不论以前的自己是什么样,自己未来变成什么样自己是可以决定的,未来的路也终究还是要自己走。大环境下,大部分人都挺难的,当 996 成为了常态,Life Balance 是不可能的了。我们只能试着寻求一种平衡,试着去热爱自己现在所做的事情。* + +**往后余生,爱家人,亦爱自己;好好生活,不忧不恼。** diff --git a/docs/about-the-author/readme.md b/docs/about-the-author/readme.md new file mode 100644 index 00000000000..cdf185586d7 --- /dev/null +++ b/docs/about-the-author/readme.md @@ -0,0 +1,52 @@ +# 个人介绍 Q&A + +大家好,我是 Gudie哥!这篇文章我会通过 Q&A 的形式简单介绍一下我自己。 + +## 我是什么时候毕业的? + +很多老读者应该比较清楚,我是 19 年本科毕业的,刚毕业就去了某家外企“养老”。 + +我的学校背景是比较差的,高考失利,勉强过了一本线 20 来分,去了荆州的一所很普通的双非一本。不过,还好我没有因为学校而放弃自己,反倒是比身边的同学都要更努力,整个大学还算过的比较充实。 + +下面这张是当时拍的毕业照: + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/%E4%B8%AA%E4%BA%BA%E4%BB%8B%E7%BB%8D.png) + +## 为什么要做 JavaGuide 这个项目? + +我从大二坚持写作,坚持分享让我收获了 30w+ 的读者以及一笔不错的副业收入。 + +2018 年,我还在读大三的时候,JavaGuide 开源项目&公众号诞生了。很难想到,日后,他们会陪伴我度过这么长的时间。 + +开源 JavaGuide 初始想法源于自己的个人那一段比较迷茫的学习经历。主要目的是为了通过这个开源平台来帮助一些在学习 Java 以及面试过程中遇到问题的小伙伴。 + +- **对于 Java 初学者来说:** 本文档倾向于给你提供一个比较详细的学习路径,让你对于 Java 整体的知识体系有一个初步认识。另外,本文的一些文章也是你学习和复习 Java 知识不错的实践; +- **对于非 Java 初学者来说:** 本文档更适合回顾知识,准备面试,搞清面试应该把重心放在那些问题上。要搞清楚这个道理:提前知道那些面试常见,不是为了背下来应付面试,而是为了让你可以更有针对的学习重点。 + +## 如何看待 JavaGuide 的 star 数量很多? + +[JavaGuide](https://github.com/Snailclimb) 目前已经是 Java 领域 star 数量最多的几个项目之一,登顶过很多次 Github Trending。 + +不过,这个真心没啥好嘚瑟的。因为,教程类的含金量其实是比较低的,Star 数量比较多主要也是因为受众面比较广,大家觉得不错,点个 star 就相当于收藏了。很多特别优秀的框架,star 数量可能只有几 K。所以,单纯看 star 数量没啥意思,就当看个笑话吧! + +维护这个项目的过程中,也被某些人 diss 过:“md 项目,没啥含金量,给国人丢脸!”。 + +对于说这类话的人,我觉得对我没啥影响,就持续完善,把 JavaGuide 做的更好吧!其实,国外的很多项目也是纯 MD 啊!就比如外国的朋友发起的 awesome 系列、求职面试系列。无需多说,行动自证!凎! + +开源非常重要的一点就是协作。如果你开源了一个项目之后,就不再维护,别人给你提交 issue/pr,你都不处理,那开源也没啥意义了! + +## 我在大学期间赚了多少钱? + +在校期间,我还通过接私活、技术培训、编程竞赛等方式变现 20w+,成功实现“经济独立”。我用自己赚的钱去了重庆、三亚、恩施、青岛等地旅游,还给家里补贴了很多,减轻的父母的负担。 + +如果你也想通过接私活变现的话,可以在我的公众号后台回复“**接私活**”来了解详细情况。 + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/1d38ea3b-da2a-41df-9ac4-087356e9b5b4-20200802185910087.png) + +## 为什么自称 Guide哥? + +可能是因为我的项目名字叫做 JavaGudie ,所以导致有很多人称呼我为 **Guide哥**。 + +后面,为了读者更方便称呼,我就将自己的笔名改成了 **Guide哥**。 + +我早期写文章用的笔名是 SnailClimb 。很多人不知道这个名字是啥意思,给大家拆解一下就清楚了。SnailClimb=Snail(蜗牛)+Climb(攀登)。我从小就非常喜欢听周杰伦的歌曲,特别是他的《蜗牛》🐌 这首歌曲,另外,当年我高考发挥的算是比较失常,上了大学之后还算是比较“奋青”,所以,我就给自己起的笔名叫做 SnailClimb ,寓意自己要不断向上攀登,哈哈 diff --git a/docs/books/java.md b/docs/books/java.md deleted file mode 100644 index 60fd6197ec2..00000000000 --- a/docs/books/java.md +++ /dev/null @@ -1,184 +0,0 @@ -**目录:** - - - -- [Java](#java) - - [基础](#基础) - - [并发](#并发) - - [JVM](#jvm) - - [Java8 新特性](#java8-新特性) - - [代码优化](#代码优化) - - [面试](#面试) -- [网络](#网络) -- [操作系统](#操作系统) -- [数据结构](#数据结构) -- [算法](#算法) - - [入门](#入门) - - [经典](#经典) - - [面试](#面试-1) -- [数据库](#数据库) -- [系统设计](#系统设计) - - [设计模式](#设计模式) - - [常用框架](#常用框架) - - [Spring/SpringBoot](#springspringboot) - - [Netty](#netty) - - [分布式](#分布式) - - [网站架构](#网站架构) - - [底层](#底层) -- [软件设计之道](#软件设计之道) -- [其他](#其他) - - - -## Java - -### 基础 - -- **[《Head First Java》](https://book.douban.com/subject/2000732/)** : 可以说是我的 Java 启蒙书籍了,特别适合新手读当然也适合我们用来温故 Java 知识点。 -- **[《Java 核心技术卷 1+卷 2》](https://book.douban.com/subject/25762168/)**: 很棒的两本书,建议有点 Java 基础之后再读,介绍的还是比较深入的,非常推荐。这两本书我一般也会用来巩固知识点或者当做工具书参考,是两本适合放在自己身边的好书。 -- **[《Java 编程思想 (第 4 版)》](https://book.douban.com/subject/2130190/)**(推荐,豆瓣评分 9.1,3.2K+人评价):大部分人称之为Java领域的圣经,但我不推荐初学者阅读,有点劝退的味道。稍微有点基础后阅读更好。 -- **[《JAVA 网络编程 第 4 版》](https://book.douban.com/subject/26259017/)**: 可以系统的学习一下网络的一些概念以及网络编程在 Java 中的使用。 -- **[《Java性能权威指南》](https://book.douban.com/subject/26740520/)**:O'Reilly 家族书,性能调优的入门书,我个人觉得性能调优是每个 Java 从业者必备知识,这本书的缺点就是太老了,但是这本书可以作为一个实战书,尤其是 JVM 调优!不适合初学者。前置书籍:《深入理解 Java 虚拟机》 - -### 并发 - -- **[《Java 并发编程之美》]()** :**我觉得这本书还是非常适合我们用来学习 Java 多线程的。这本书的讲解非常通俗易懂,作者从并发编程基础到实战都是信手拈来。** 另外,这本书的作者加多自身也会经常在网上发布各种技术文章。我觉得这本书也是加多大佬这么多年在多线程领域的沉淀所得的结果吧!他书中的内容基本都是结合代码讲解,非常有说服力! -- **[《实战 Java 高并发程序设计》](https://book.douban.com/subject/26663605/)**: 这个是我第二本要推荐的书籍,比较适合作为多线程入门/进阶书籍来看。这本书内容同样是理论结合实战,对于每个知识点的讲解也比较通俗易懂,整体结构也比较清。 -- **[《深入浅出 Java 多线程》](https://github.com/RedSpider1/concurrent)**:这本书是几位大厂(如阿里)的大佬开源的,Github 地址:[https://github.com/RedSpider1/concurrent](https://github.com/RedSpider1/concurrent)几位作者为了写好《深入浅出 Java 多线程》这本书阅读了大量的 Java 多线程方面的书籍和博客,然后再加上他们的经验总结、Demo 实例、源码解析,最终才形成了这本书。这本书的质量也是非常过硬!给作者们点个赞!这本书有统一的排版规则和语言风格、清晰的表达方式和逻辑。并且每篇文章初稿写完后,作者们就会互相审校,合并到主分支时所有成员会再次审校,最后再通篇修订了三遍。 -- **《Java 并发编程的艺术》** :这本书不是很适合作为 Java 多线程入门书籍,需要具备一定的 JVM 基础,有些东西讲的还是挺深入的。另外,就我自己阅读这本书的感觉来说,我觉得这本书的章节规划有点杂乱,但是,具体到某个知识点又很棒!这可能也和这本书由三名作者共同编写完成有关系吧! -- ...... - -### JVM - -- **[《深入理解 Java 虚拟机(第 3 版)》](https://book.douban.com/subject/24722612/))**:必读!必读!必读!神书,建议多刷几篇。里面不光有丰富地JVM理论知识,还有JVM实战案例!必读! -- **[《实战 JAVA 虚拟机》](https://book.douban.com/subject/26354292/)**:作为入门的了解 Java 虚拟机的知识还是不错的。 - -### Java8 新特性 - -- **[《Java 8 实战》](https://book.douban.com/subject/26772632/)**:面向 Java 8 的技能升级,包括 Lambdas、流和函数式编程特性。实战系列的一贯风格让自己快速上手应用起来。Java 8 支持的 Lambda 是精简表达在语法上提供的支持。Java 8 提供了 Stream,学习和使用可以建立流式编程的认知。 -- **[《Java 8 编程参考官方教程》](https://book.douban.com/subject/26556574/)**:建议当做工具书来用!哪里不会翻哪里! - -### 代码优化 - -- **[《重构_改善既有代码的设计》](https://book.douban.com/subject/4262627/)**:豆瓣 9.1 分,重构书籍的开山鼻祖。 -- **[《Effective java 》](https://book.douban.com/subject/3360807/)**:本书介绍了在 Java 编程中很多极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。这篇文章能够非常实际地帮助你写出更加清晰、健壮和高效的代码。本书中的每条规则都以简短、独立的小文章形式出现,并通过例子代码加以进一步说明。 -- **[《代码整洁之道》](https://book.douban.com/subject/5442024/)**:虽然是用 Java 语言作为例子,全篇都是在阐述 Java 面向对象的思想,但是其中大部分内容其它语言也能应用到。 -- **阿里巴巴 Java 开发手册** :[https://github.com/alibaba/p3c](https://github.com/alibaba/p3c) -- **Google Java 编程风格指南:** - -### 面试 - -1. **[《JavaGuide面试突击版》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486324&idx=1&sn=e8b690ddaedabc486bd399310105aad3&chksm=cea244bff9d5cda9a627fa65235be09e7b089e92cf49c0eb0ceb35b39bbed86c1fab0125f5af&token=1745528586&lang=zh_CN&scene=21#wechat_redirect)** :我的75k+ star的开源项目 [JavaGuide ](https://github.com/Snailclimb/JavaGuide) 转为面试浓缩而成的版本,不光提供了PDF版本(我的公众号JavaGuide后台回复:“面试突击”即可获取),在线阅读版本:[https://snailclimb.gitee.io/javaguide-interview/](https://snailclimb.gitee.io/javaguide-interview/)。 -2. **[《Offer来了:Java面试核心知识点精讲》](https://book.douban.com/subject/34872163/)** : 这本书基本概括了Java程序员面试必备知识点,可以拿来准备Java面试或者夯实基础。不过,我还是更推荐我的 [JavaGuide](https://github.com/Snailclimb/JavaGuide) 和 **[《JavaGuide面试突击版》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486324&idx=1&sn=e8b690ddaedabc486bd399310105aad3&chksm=cea244bff9d5cda9a627fa65235be09e7b089e92cf49c0eb0ceb35b39bbed86c1fab0125f5af&token=1745528586&lang=zh_CN&scene=21#wechat_redirect)** ,两者配合起来学习,真香! - -## 网络 - -- **[《图解 HTTP》](https://book.douban.com/subject/25863515/)**: 讲漫画一样的讲 HTTP,很有意思,不会觉得枯燥,大概也涵盖也 HTTP 常见的知识点。因为篇幅问题,内容可能不太全面。不过,如果不是专门做网络方向研究的小伙伴想研究 HTTP 相关知识的话,读这本书的话应该来说就差不多了。 -- **[《HTTP 权威指南》](https://book.douban.com/subject/10746113/)**:如果要全面了解 HTTP 非此书不可! - -## 操作系统 - -- **[《鸟哥的 Linux 私房菜》](https://book.douban.com/subject/4889838/)**:本书是最具知名度的 Linux 入门书《鸟哥的 Linux 私房菜基础学习篇》的最新版,全面而详细地介绍了 Linux 操作系统。 - -## 数据结构 - -- **[《大话数据结构》](https://book.douban.com/subject/6424904/)**:入门类型的书籍,读起来比较浅显易懂,适合没有数据结构基础或者说数据结构没学好的小伙伴用来入门数据结构。 - -## 算法 - -### 入门 - -- **[《我的第一本算法书》](https://book.douban.com/subject/30357170/) (豆瓣评分 7.1,0.2K+人评价)** 一本不那么“专业”的算法书籍。和下面两本推荐的算法书籍都是比较通俗易懂,“不那么深入”的算法书籍。我个人非常推荐,配图和讲解都非常不错! -- **[《算法图解》](https://book.douban.com/subject/26979890/)(豆瓣评分 8.4,1.5K+人评价)** :入门类型的书籍,读起来比较浅显易懂,非常适合没有算法基础或者说算法没学好的小伙伴用来入门。示例丰富,图文并茂,以让人容易理解的方式阐释了算法.读起来比较快,内容不枯燥! -- **[《啊哈!算法》](https://book.douban.com/subject/25894685/) (豆瓣评分 7.7,0.5K+人评价)** :和《算法图解》类似的算法趣味入门书籍。 - -### 经典 - -> 下面这些书籍都是经典中的经典,但是阅读起来难度也比较大,不做太多阐述,神书就完事了!推荐先看 《算法》,然后再选下面的书籍进行进一步阅读。不需要都看,找一本好好看或者找某本书的某一个章节知识点好好看。 - -- **[《算法 第四版》](https://book.douban.com/subject/10432347/)(豆瓣评分 9.3,0.4K+人评价):** 我在大二的时候被我们的一个老师强烈安利过!自己也在当时购买了一本放在宿舍,到离开大学的时候自己大概看了一半多一点。因为内容实在太多了!另外,这本书还提供了详细的Java代码,非常适合学习 Java 的朋友来看,可以说是 Java 程序员的必备书籍之一了。再来介绍一下这本书籍吧!这本书籍算的上是算法领域经典的参考书,全面介绍了关于算法和数据结构的必备知识,并特别针对排序、搜索、图处理和字符串处理进行了论述。 -- **[编程珠玑](https://book.douban.com/subject/3227098/)(豆瓣评分 9.1,2K+人评价)** :经典名著,被无数读者强烈推荐的书籍,几乎是顶级程序员必看的书籍之一了。这本书的作者也非常厉害,Java之父 James Gosling 就是他的学生。很多人都说这本书不是教你具体的算法,而是教你一种编程的思考方式。这种思考方式不仅仅在编程领域适用,在其他同样适用。 -- **[《算法设计手册》](https://book.douban.com/subject/4048566/)(豆瓣评分9.1 , 45人评价)** :被 [Teach Yourself Computer Science](https://teachyourselfcs.com/) 强烈推荐的一本算法书籍。 -- **[《算法导论》](https://book.douban.com/subject/20432061/) (豆瓣评分 9.2,0.4K+人评价)** -- **[《计算机程序设计艺术(第1卷)》](https://book.douban.com/subject/1130500/)(豆瓣评分 9.4,0.4K+人评价)** - -### 面试 - -1. **[《剑指Offer》](https://book.douban.com/subject/6966465/)(豆瓣评分 8.3,0.7K+人评价)**这本面试宝典上面涵盖了很多经典的算法面试题,如果你要准备大厂面试的话一定不要错过这本书。《剑指Offer》 对应的算法编程题部分的开源项目解析:[CodingInterviews](https://github.com/gatieme/CodingInterviews) -2. **[程序员代码面试指南:IT名企算法与数据结构题目最优解(第2版)](https://book.douban.com/subject/30422021/) (豆瓣评分 8.7,0.2K+人评价)** :题目相比于《剑指 offer》 来说要难很多,题目涵盖面相比于《剑指 offer》也更加全面。全书一共有将近300道真实出现过的经典代码面试题。 -3. **[编程之美](https://book.douban.com/subject/3004255/)(豆瓣评分 8.4,3K+人评价)**:这本书收集了约60道算法和程序设计题目,这些题目大部分在近年的笔试、面试中出现过,或者是被微软员工热烈讨论过。作者试图从书中各种有趣的问题出发,引导读者发现问题,分析问题,解决问题,寻找更优的解法。 - -## 数据库 - -**MySQL:** - -- **[《高性能 MySQL》](https://book.douban.com/subject/23008813/)**:这本书不用多说了把!MySQL 领域的经典之作,拥有广泛的影响力。不但适合数据库管理员(dba)阅读,也适合开发人员参考学习。不管是数据库新手还是专家,相信都能从本书有所收获。如果你的时间不够的话,第5章关于索引的内容和第6章关于查询的内容是必读的! -- [《MySQL 技术内幕-InnoDB 存储引擎》]()(推荐,豆瓣评分 8.7):了解 InnoDB 存储引擎底层原理必备的一本书,比较深入。 - -**Redis:** - -- **[《Redis 实战》](https://book.douban.com/subject/26612779/)**:如果你想了解 Redis 的一些概念性知识的话,这本书真的非常不错。 -- **[《Redis 设计与实现》](https://book.douban.com/subject/25900156/)**:也还行吧! - -## 系统设计 - -### 设计模式 - -- **[《设计模式 : 可复用面向对象软件的基础》](https://book.douban.com/subject/1052241/)** :设计模式的经典! -- **[《Head First 设计模式(中文版)》](https://book.douban.com/subject/2243615/)** :相当赞的一本设计模式入门书籍。用实际的编程案例讲解算法设计中会遇到的各种问题和需求变更(对的,连需求变更都考虑到了!),并以此逐步推导出良好的设计模式解决办法。 -- **[《大话设计模式》](https://book.douban.com/subject/2334288/)** :本书通篇都是以情景对话的形式,用多个小故事或编程示例来组织讲解GOF(即《设计模式 : 可复用面向对象软件的基础》这本书)),但是不像《设计模式 : 可复用面向对象软件的基础》难懂。但是设计模式只看书是不够的,还是需要在实际项目中运用,在实战中体会。 - -### 常用框架 - -#### Spring/SpringBoot - -- **[《Spring 实战(第 4 版)》](https://book.douban.com/subject/26767354/)** :不建议当做入门书籍读,入门的话可以找点国人的书或者视频看。这本定位就相当于是关于 Spring 的新华字典,只有一些基本概念的介绍和示例,涵盖了 Spring 的各个方面,但都不够深入。就像作者在最后一页写的那样:“学习 Spring,这才刚刚开始”。 -- **《[Spring源码深度解析 第2版](https://book.douban.com/subject/30452948/)》** :读Spring源码必备的一本书籍。市面上关于Spring源码分析的书籍太少了。 -- **[《Spring 5高级编程(第5版)》](https://book.douban.com/subject/30452637/)** :推荐阅读,对于Spring5的新特性介绍的很好!不过内容比较多,可以作为工具书参考。 -- **[《精通Spring4.x企业应用开发实战》](https://read.douban.com/ebook/58113975/?dcs=subject-rec&dcm=douban&dct=26767354)** :通过实战讲解,比较适合作为Spring入门书籍来看。 -- **[《Spring入门经典》](https://book.douban.com/subject/26652876/)** :适合入门,也有很多示例! -- **[《Spring Boot实战派》](https://book.douban.com/subject/34894533/)** :这本书使用的Spring Boot 2.0+的版本,还算比较新。整本书采用“知识点+实例”的形式编写。本书通过“58个基于知识的实例+2个综合性的项目”,深入地讲解Spring Boot的技术原理、知识点和具体应用;把晦涩难懂的理论用实例展现出来,使得读者对知识的理解变得非常容易,同时也立即学会如何使用它。说实话,我还是比较推荐这本书的。 -- **[《Spring Boot编程思想(核心篇)》](https://book.douban.com/subject/33390560/)** :SpringBoot深入书,不适合初学者。书尤其的厚,这本书的缺点是书的很多知识点的讲解过于啰嗦和拖沓,优点是书中对SpringBoot内部原理讲解很清楚。 - -#### Netty - -- **[《Netty进阶之路:跟着案例学Netty》](https://book.douban.com/subject/30381214/)** : 这本书的优点是有不少实际的案例的讲解,通过案例来学习是很不错的! -- **[《Netty 4.x 用户指南》](https://waylau.gitbooks.io/netty-4-user-guide/content/)** :《Netty 4.x 用户指南》中文翻译(包含了官方文档以及其他文章)。 -- **[《Netty 入门与实战:仿写微信 IM 即时通讯系统》](https://juejin.im/book/5b4bc28bf265da0f60130116?referrer=59fbb2daf265da4319559f3a)** :基于 Netty 框架实现 IM 核心系统,带你深入学习 Netty 网络编程核心知识 -- **[《Netty 实战》](https://book.douban.com/subject/27038538/)** :可以作为工具书参考! - -### 分布式 - -- **[《从 Paxos 到 Zookeeper》](https://book.douban.com/subject/26292004/)**:简要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了 Paxos 和 ZAB 协议。同时,本书深入介绍了分布式一致性问题的工业解决方案——ZooKeeper,并着重向读者展示这一分布式协调框架的使用方法、内部实现及运维技巧,旨在帮助读者全面了解 ZooKeeper,并更好地使用和运维 ZooKeeper。 -- **[《RabbitMQ 实战指南》](https://book.douban.com/subject/27591386/)**:《RabbitMQ 实战指南》从消息中间件的概念和 RabbitMQ 的历史切入,主要阐述 RabbitMQ 的安装、使用、配置、管理、运维、原理、扩展等方面的细节。如果你想浅尝 RabbitMQ 的使用,这本书是你最好的选择;如果你想深入 RabbitMQ 的原理,这本书也是你最好的选择;总之,如果你想玩转 RabbitMQ,这本书一定是最值得看的书之一 -- **[《Spring Cloud 微服务实战》](https://book.douban.com/subject/27025912/)**:从时下流行的微服务架构概念出发,详细介绍了 Spring Cloud 针对微服务架构中几大核心要素的解决方案和基础组件。对于各个组件的介绍,《Spring Cloud 微服务实战》主要以示例与源码结合的方式来帮助读者更好地理解这些组件的使用方法以及运行原理。同时,在介绍的过程中,还包含了作者在实践中所遇到的一些问题和解决思路,可供读者在实践中作为参考。 - -### 网站架构 - -- **[《大型网站技术架构:核心原理与案例分析+李智慧》](https://book.douban.com/subject/25723064/)**:这本书我读过,基本不需要你有什么基础啊~读起来特别轻松,但是却可以学到很多东西,非常推荐了。另外我写过这本书的思维导图,关注我的微信公众号:“Java 面试通关手册”回复“大型网站技术架构”即可领取思维导图。 -- **[《亿级流量网站架构核心技术》](https://book.douban.com/subject/26999243/)**:一书总结并梳理了亿级流量网站高可用和高并发原则,通过实例详细介绍了如何落地这些原则。本书分为四部分:概述、高可用原则、高并发原则、案例实战。从负载均衡、限流、降级、隔离、超时与重试、回滚机制、压测与预案、缓存、池化、异步化、扩容、队列等多方面详细介绍了亿级流量网站的架构核心技术,让读者看后能快速运用到实践项目中。 -- **[《从零开始学架构(李运华)》](https://book.douban.com/subject/30335935/)** : 这本书对应的有一个极客时间的专栏—《从零开始学架构》,里面的很多内容都是这个专栏里面的,两者买其一就可以了。我看了很小一部分,内容挺全面的,是一本真正在讲如何做架构的书籍。 -- **[《架构修炼之道——亿级网关、平台开放、分布式、微服务、容错等核心技术修炼实践》](https://book.douban.com/subject/33389549/)** :非常喜欢的一本书,对一些知识点比如消息队列、API网管讲解的很好,通俗易懂。 - -### 底层 - -- **[《深入剖析 Tomcat》](https://book.douban.com/subject/10426640/)**:本书深入剖析 Tomcat 4 和 Tomcat 5 中的每个组件,并揭示其内部工作原理。通过学习本书,你将可以自行开发 Tomcat 组件,或者扩展已有的组件。 读完这本书,基本可以摆脱背诵面试题的尴尬。 -- **[《深入理解 Nginx(第 2 版)》](https://book.douban.com/subject/26745255/)**:作者讲的非常细致,注释都写的都很工整,对于 Nginx 的开发人员非常有帮助。优点是细致,缺点是过于细致,到处都是代码片段,缺少一些抽象。 - -## 软件设计之道 - -- **[《人月神话》](https://book.douban.com/subject/1102259/)** : 非常值得阅读的一本书籍。看书名感觉的第一眼感觉不像是技术类的书籍。这本书对于现代软件尤其是复杂软件的开发的规范化有深刻的意义。 -- **《领域驱动设计:软件核心复杂性应对之道》** : 这本领域驱动设计方面的经典之作一直被各种推荐,但是我还来及读。 - -## 其他 - -- **[《黑客与画家》](https://read.douban.com/ebook/387525/?dcs=subject-rec&dcm=douban&dct=2243615)**:这本书是硅谷创业之父,Y Combinator 创始人 Paul Graham 的文集。之所以叫这个名字,是因为作者认为黑客(并非负面的那个意思)与画家有着极大的相似性,他们都是在创造,而不是完成某个任务。 - -- **[《图解密码技术》](https://book.douban.com/subject/26265544/)**:本书以**图配文**的形式,第一部分讲述了密码技术的历史沿革、对称密码、分组密码模式(包括ECB、CBC、CFB、OFB、CTR)、公钥、混合密码系统。第二部分重点介绍了认证方面的内容,涉及单向散列函数、消息认证码、数字签名、证书等。第三部分讲述了密钥、随机数、PGP、SSL/TLS 以及密码技术在现实生活中的应用。关键字:JWT 前置知识、区块链密码技术前置知识。属于密码知识入门书籍。、 -- 《程序开发心理学》 、《程序员修炼之道,从小工道专家》、 《高效程序员的45个习惯,敏捷开发修炼之道》 、《高效能程序员的修炼》 、《软技能,代码之外的生存之道》 、《程序员的职业素养》 、《程序员的思维修炼》 - - - - - - diff --git "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\220\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" "b/docs/cs-basics/algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\227\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" similarity index 91% rename from "docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\220\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" rename to "docs/cs-basics/algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\227\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" index af63c584c2f..37176650df7 100644 --- "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\220\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" +++ "b/docs/cs-basics/algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\227\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" @@ -1,20 +1,4 @@ - - -- [说明](#说明) -- [1. KMP 算法](#1-kmp-算法) -- [2. 替换空格](#2-替换空格) -- [3. 最长公共前缀](#3-最长公共前缀) -- [4. 回文串](#4-回文串) - - [4.1. 最长回文串](#41-最长回文串) - - [4.2. 验证回文串](#42-验证回文串) - - [4.3. 最长回文子串](#43-最长回文子串) - - [4.4. 最长回文子序列](#44-最长回文子序列) -- [5. 括号匹配深度](#5-括号匹配深度) -- [6. 把字符串转换成整数](#6-把字符串转换成整数) - - - - +# 几道常见的字符串算法题 > 授权转载! > @@ -22,9 +6,6 @@ > - 原文地址:https://www.weiweiblog.cn/13string/ - -考虑到篇幅问题,我会分两次更新这个内容。本篇文章只是原文的一部分,我在原文的基础上增加了部分内容以及修改了部分代码和注释。另外,我增加了爱奇艺 2018 秋招 Java:`求给定合法括号序列的深度` 这道题。所有代码均编译成功,并带有注释,欢迎各位享用! - ## 1. KMP 算法 谈到字符串问题,不得不提的就是 KMP 算法,它是用来解决字符串查找的问题,可以在一个字符串(S)中查找一个子串(W)出现的位置。KMP 算法把字符匹配的时间复杂度缩小到 O(m+n) ,而空间复杂度也只有O(m)。因为“暴力搜索”的方法会反复回溯主串,导致效率低下,而KMP算法可以利用已经部分匹配这个有效信息,保持主串上的指针不回溯,通过修改子串的指针,让模式串尽量地移动到有效的位置。 @@ -284,10 +265,7 @@ class Solution { 输出: "bb" ``` -以某个元素为中心,分别计算偶数长度的回文最大长度和奇数长度的回文最大长度。给大家大致花了个草图,不要嫌弃! - - -![](https://user-gold-cdn.xitu.io/2018/9/9/165bc32f6f1833ff?w=723&h=371&f=png&s=9305) +以某个元素为中心,分别计算偶数长度的回文最大长度和奇数长度的回文最大长度。 ```java //https://leetcode-cn.com/problems/longest-palindromic-substring/description/ @@ -401,11 +379,6 @@ class Solution { 2 ``` -思路草图: - - -![](https://user-gold-cdn.xitu.io/2018/9/9/165bc6fca94ef278?w=792&h=324&f=png&s=15868) - 代码如下: ```java diff --git "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" "b/docs/cs-basics/algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" similarity index 88% rename from "docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" rename to "docs/cs-basics/algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" index 85e2934e407..1b64653cd9f 100644 --- "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" +++ "b/docs/cs-basics/algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" @@ -1,29 +1,6 @@ - - -- [1. 两数相加](#1-两数相加) - - [题目描述](#题目描述) - - [问题分析](#问题分析) - - [Solution](#solution) -- [2. 翻转链表](#2-翻转链表) - - [题目描述](#题目描述-1) - - [问题分析](#问题分析-1) - - [Solution](#solution-1) -- [3. 链表中倒数第k个节点](#3-链表中倒数第k个节点) - - [题目描述](#题目描述-2) - - [问题分析](#问题分析-2) - - [Solution](#solution-2) -- [4. 删除链表的倒数第N个节点](#4-删除链表的倒数第n个节点) - - [问题分析](#问题分析-3) - - [Solution](#solution-3) -- [5. 合并两个排序的链表](#5-合并两个排序的链表) - - [题目描述](#题目描述-3) - - [问题分析](#问题分析-4) - - [Solution](#solution-4) - - - - -# 1. 两数相加 +# 几道常见的链表算法题 + +## 1. 两数相加 ### 题目描述 @@ -50,7 +27,7 @@ Leetcode官方详细解答地址: 我们使用变量来跟踪进位,并从包含最低有效位的表头开始模拟逐 位相加的过程。 -![图1,对两数相加方法的可视化: 342 + 465 = 807342+465=807, 每个结点都包含一个数字,并且数字按位逆序存储。](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/34910956.jpg) +![图1,对两数相加方法的可视化: 342 + 465 = 807342+465=807, 每个结点都包含一个数字,并且数字按位逆序存储。](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/34910956.jpg) ### Solution @@ -92,13 +69,13 @@ public ListNode addTwoNumbers(ListNode l1, ListNode l2) { } ``` -# 2. 翻转链表 +## 2. 翻转链表 ### 题目描述 > 剑指 offer:输入一个链表,反转链表后,输出链表的所有元素。 -![翻转链表](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/81431871.jpg) +![翻转链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/81431871.jpg) ### 问题分析 @@ -180,7 +157,7 @@ public class Solution { 1 ``` -# 3. 链表中倒数第k个节点 +## 3. 链表中倒数第k个节点 ### 题目描述 @@ -240,7 +217,7 @@ public class Solution { ``` -# 4. 删除链表的倒数第N个节点 +## 4. 删除链表的倒数第N个节点 > Leetcode:给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。 @@ -269,7 +246,7 @@ public class Solution { 我们注意到这个问题可以容易地简化成另一个问题:删除从列表开头数起的第 (L - n + 1)个结点,其中 L是列表的长度。只要我们找到列表的长度 L,这个问题就很容易解决。 -![图 1. 删除列表中的第 L - n + 1 个元素](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/94354387.jpg) +![图 1. 删除列表中的第 L - n + 1 个元素](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/94354387.jpg) ### Solution @@ -324,7 +301,7 @@ public class Solution { **进阶——一次遍历法:** -> **链表中倒数第N个节点也就是正数第(L-N+1)个节点。 +> 链表中倒数第N个节点也就是正数第(L-N+1)个节点。 其实这种方法就和我们上面第四题找“链表中倒数第k个节点”所用的思想是一样的。**基本思路就是:** 定义两个节点 node1、node2;node1 节点先跑,node1节点 跑到第 n+1 个节点的时候,node2 节点开始跑.当node1 节点跑到最后一个节点时,node2 节点所在的位置就是第 (L-n ) 个节点(L代表总链表长度,也就是倒数第 n+1 个节点) @@ -367,7 +344,7 @@ public class Solution { -# 5. 合并两个排序的链表 +## 5. 合并两个排序的链表 ### 题目描述 diff --git "a/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" "b/docs/cs-basics/algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" similarity index 70% rename from "docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" rename to "docs/cs-basics/algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" index b7b077fb041..790422342fc 100644 --- "a/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" +++ "b/docs/cs-basics/algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" @@ -1,103 +1,106 @@ -### 一 斐波那契数列 +# 剑指offer部分编程题 -#### **题目描述:** +## 斐波那契数列 + +**题目描述:** 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。 n<=39 -#### **问题分析:** +**问题分析:** 可以肯定的是这一题通过递归的方式是肯定能做出来,但是这样会有一个很大的问题,那就是递归大量的重复计算会导致内存溢出。另外可以使用迭代法,用fn1和fn2保存计算过程中的结果,并复用起来。下面我会把两个方法示例代码都给出来并给出两个方法的运行时间对比。 -#### **示例代码:** +**示例代码:** -**采用迭代法:** +采用迭代法: ```java - int Fibonacci(int number) { - if (number <= 0) { - return 0; - } - if (number == 1 || number == 2) { - return 1; - } - int first = 1, second = 1, third = 0; - for (int i = 3; i <= number; i++) { - third = first + second; - first = second; - second = third; - } - return third; - } +int Fibonacci(int number) { + if (number <= 0) { + return 0; + } + if (number == 1 || number == 2) { + return 1; + } + int first = 1, second = 1, third = 0; + for (int i = 3; i <= number; i++) { + third = first + second; + first = second; + second = third; + } + return third; +} ``` -**采用递归:** +采用递归: ```java - public int Fibonacci(int n) { - - if (n <= 0) { - return 0; - } - if (n == 1||n==2) { - return 1; - } - - return Fibonacci(n - 2) + Fibonacci(n - 1); - - } +public int Fibonacci(int n) { + if (n <= 0) { + return 0; + } + if (n == 1||n==2) { + return 1; + } + + return Fibonacci(n - 2) + Fibonacci(n - 1); +} ``` -### 二 跳台阶问题 +## 跳台阶问题 -#### **题目描述:** +**题目描述:** 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 -#### **问题分析:** +**问题分析:** + +正常分析法: -**正常分析法:** -a.如果两种跳法,1阶或者2阶,那么假定第一次跳的是一阶,那么剩下的是n-1个台阶,跳法是f(n-1); -b.假定第一次跳的是2阶,那么剩下的是n-2个台阶,跳法是f(n-2) -c.由a,b假设可以得出总跳法为: f(n) = f(n-1) + f(n-2) -d.然后通过实际的情况可以得出:只有一阶的时候 f(1) = 1 ,只有两阶的时候可以有 f(2) = 2 -**找规律分析法:** -f(1) = 1, f(2) = 2, f(3) = 3, f(4) = 5, 可以总结出f(n) = f(n-1) + f(n-2)的规律。 -但是为什么会出现这样的规律呢?假设现在6个台阶,我们可以从第5跳一步到6,这样的话有多少种方案跳到5就有多少种方案跳到6,另外我们也可以从4跳两步跳到6,跳到4有多少种方案的话,就有多少种方案跳到6,其他的不能从3跳到6什么的啦,所以最后就是f(6) = f(5) + f(4);这样子也很好理解变态跳台阶的问题了。 +> a.如果两种跳法,1阶或者2阶,那么假定第一次跳的是一阶,那么剩下的是n-1个台阶,跳法是f(n-1); +> b.假定第一次跳的是2阶,那么剩下的是n-2个台阶,跳法是f(n-2) +> c.由a,b假设可以得出总跳法为: f(n) = f(n-1) + f(n-2) +> d.然后通过实际的情况可以得出:只有一阶的时候 f(1) = 1 ,只有两阶的时候可以有 f(2) = 2 + +找规律分析法: + +> f(1) = 1, f(2) = 2, f(3) = 3, f(4) = 5, 可以总结出f(n) = f(n-1) + f(n-2)的规律。但是为什么会出现这样的规律呢?假设现在6个台阶,我们可以从第5跳一步到6,这样的话有多少种方案跳到5就有多少种方案跳到6,另外我们也可以从4跳两步跳到6,跳到4有多少种方案的话,就有多少种方案跳到6,其他的不能从3跳到6什么的啦,所以最后就是f(6) = f(5) + f(4);这样子也很好理解变态跳台阶的问题了。 **所以这道题其实就是斐波那契数列的问题。** + 代码只需要在上一题的代码稍做修改即可。和上一题唯一不同的就是这一题的初始元素变为 1 2 3 5 8.....而上一题为1 1 2 3 5 .......。另外这一题也可以用递归做,但是递归效率太低,所以我这里只给出了迭代方式的代码。 -#### **示例代码:** +**示例代码:** ```java - int jumpFloor(int number) { - if (number <= 0) { - return 0; - } - if (number == 1) { - return 1; - } - if (number == 2) { - return 2; - } - int first = 1, second = 2, third = 0; - for (int i = 3; i <= number; i++) { - third = first + second; - first = second; - second = third; - } - return third; - } +int jumpFloor(int number) { + if (number <= 0) { + return 0; + } + if (number == 1) { + return 1; + } + if (number == 2) { + return 2; + } + int first = 1, second = 2, third = 0; + for (int i = 3; i <= number; i++) { + third = first + second; + first = second; + second = third; + } + return third; +} ``` -### 三 变态跳台阶问题 +## 变态跳台阶问题 -#### **题目描述:** +**题目描述:** 一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 -#### **问题分析:** +**问题分析:** 假设n>=2,第一步有n种跳法:跳1级、跳2级、到跳n级 跳1级,剩下n-1级,则剩下跳法是f(n-1) @@ -110,33 +113,36 @@ f(n)=f(n-1)+f(n-2)+...+f(1) 因为f(n-1)=f(n-2)+f(n-3)+...+f(1) 所以f(n)=2*f(n-1) 又f(1)=1,所以可得**f(n)=2^(number-1)** -#### **示例代码:** +**示例代码:** ```java - int JumpFloorII(int number) { - return 1 << --number;//2^(number-1)用位移操作进行,更快 - } +int JumpFloorII(int number) { + return 1 << --number;//2^(number-1)用位移操作进行,更快 +} ``` -#### **补充:** +**补充:** -**java中有三种移位运算符:** +java中有三种移位运算符: 1. “<<” : **左移运算符**,等同于乘2的n次方 2. “>>”: **右移运算符**,等同于除2的n次方 -3. “>>>” **无符号右移运算符**,不管移动前最高位是0还是1,右移后左侧产生的空位部分都以0来填充。与>>类似。 - 例: - int a = 16; - int b = a << 2;//左移2,等同于16 * 2的2次方,也就是16 * 4 - int c = a >> 2;//右移2,等同于16 / 2的2次方,也就是16 / 4 +3. “>>>” : **无符号右移运算符**,不管移动前最高位是0还是1,右移后左侧产生的空位部分都以0来填充。与>>类似。 -### 四 二维数组查找 +```java +int a = 16; +int b = a << 2;//左移2,等同于16 * 2的2次方,也就是16 * 4 +int c = a >> 2;//右移2,等同于16 / 2的2次方,也就是16 / 4 +``` + + +## 二维数组查找 -#### **题目描述:** +**题目描述:** 在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 -#### **问题解析:** +**问题解析:** 这一道题还是比较简单的,我们需要考虑的是如何做,效率最快。这里有一种很好理解的思路: @@ -144,78 +150,77 @@ f(n)=f(n-1)+f(n-2)+...+f(1) > 因此从左下角开始查找,当要查找数字比左下角数字大时。右移 > 要查找数字比左下角数字小时,上移。这样找的速度最快。 -#### **示例代码:** +**示例代码:** ```java - public boolean Find(int target, int [][] array) { - //基本思路从左下角开始找,这样速度最快 - int row = array.length-1;//行 - int column = 0;//列 - //当行数大于0,当前列数小于总列数时循环条件成立 - while((row >= 0)&& (column< array[0].length)){ - if(array[row][column] > target){ - row--; - }else if(array[row][column] < target){ - column++; - }else{ - return true; - } +public boolean Find(int target, int [][] array) { + //基本思路从左下角开始找,这样速度最快 + int row = array.length-1;//行 + int column = 0;//列 + //当行数大于0,当前列数小于总列数时循环条件成立 + while((row >= 0)&& (column< array[0].length)){ + if(array[row][column] > target){ + row--; + }else if(array[row][column] < target){ + column++; + }else{ + return true; } - return false; } + return false; +} ``` -### 五 替换空格 +## 替换空格 -#### **题目描述:** +**题目描述:** 请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。 -#### **问题分析:** +**问题分析:** 这道题不难,我们可以通过循环判断字符串的字符是否为空格,是的话就利用append()方法添加追加“%20”,否则还是追加原字符。 -或者最简单的方法就是利用: replaceAll(String regex,String replacement)方法了,一行代码就可以解决。 +或者最简单的方法就是利用:replaceAll(String regex,String replacement)方法了,一行代码就可以解决。 -#### **示例代码:** +**示例代码:** -**常规做法:** +常规做法: ```java - public String replaceSpace(StringBuffer str) { - StringBuffer out=new StringBuffer(); - for (int i = 0; i < str.toString().length(); i++) { - char b=str.charAt(i); - if(String.valueOf(b).equals(" ")){ - out.append("%20"); - }else{ - out.append(b); - } +public String replaceSpace(StringBuffer str) { + StringBuffer out = new StringBuffer(); + for (int i = 0; i < str.toString().length(); i++) { + char b = str.charAt(i); + if(String.valueOf(b).equals(" ")){ + out.append("%20"); + }else{ + out.append(b); } - return out.toString(); } + return out.toString(); +} ``` -**一行代码解决:** +一行代码解决: ```java - public String replaceSpace(StringBuffer str) { - //return str.toString().replaceAll(" ", "%20"); - //public String replaceAll(String regex,String replacement) - //用给定的替换替换与给定的regular expression匹配的此字符串的每个子字符串。 - //\ 转义字符. 如果你要使用 "\" 本身, 则应该使用 "\\". String类型中的空格用“\s”表示,所以我这里猜测"\\s"就是代表空格的意思 - return str.toString().replaceAll("\\s", "%20"); - } - +public String replaceSpace(StringBuffer str) { + //return str.toString().replaceAll(" ", "%20"); + //public String replaceAll(String regex,String replacement) + //用给定的替换替换与给定的regular expression匹配的此字符串的每个子字符串。 + //\ 转义字符. 如果你要使用 "\" 本身, 则应该使用 "\\". String类型中的空格用“\s”表示,所以我这里猜测"\\s"就是代表空格的意思 + return str.toString().replaceAll("\\s", "%20"); +} ``` -### 六 数值的整数次方 +## 数值的整数次方 -#### **题目描述:** +**题目描述:** 给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。 -#### **问题解析:** +**问题解析:** 这道题算是比较麻烦和难一点的一个了。我这里采用的是**二分幂**思想,当然也可以采用**快速幂**。 更具剑指offer书中细节,该题的解题思路如下: @@ -227,7 +232,7 @@ f(n)=f(n-1)+f(n-2)+...+f(1) **时间复杂度**:O(logn) -#### **示例代码:** +**示例代码:** ```java public class Solution { @@ -279,31 +284,31 @@ public class Solution { 当然这一题也可以采用笨方法:累乘。不过这种方法的时间复杂度为O(n),这样没有前一种方法效率高。 ```java - // 使用累乘 - public double powerAnother(double base, int exponent) { - double result = 1.0; - for (int i = 0; i < Math.abs(exponent); i++) { - result *= base; - } - if (exponent >= 0) - return result; - else - return 1 / result; +// 使用累乘 +public double powerAnother(double base, int exponent) { + double result = 1.0; + for (int i = 0; i < Math.abs(exponent); i++) { + result *= base; } + if (exponent >= 0) + return result; + else + return 1 / result; +} ``` -### 七 调整数组顺序使奇数位于偶数前面 +## 调整数组顺序使奇数位于偶数前面 -#### **题目描述:** +**题目描述:** 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。 -#### **问题解析:** +**问题解析:** 这道题有挺多种解法的,给大家介绍一种我觉得挺好理解的方法: 我们首先统计奇数的个数假设为n,然后新建一个等长数组,然后通过循环判断原数组中的元素为偶数还是奇数。如果是则从数组下标0的元素开始,把该奇数添加到新数组;如果是偶数则从数组下标为n的元素开始把该偶数添加到新数组中。 -#### **示例代码:** +**示例代码:** 时间复杂度为O(n),空间复杂度为O(n)的算法 @@ -336,13 +341,13 @@ public class Solution { } ``` -### 八 链表中倒数第k个节点 +## 链表中倒数第k个节点 -#### **题目描述:** +**题目描述:** 输入一个链表,输出该链表中倒数第k个结点 -#### **问题分析:** +**问题分析:** **一句话概括:** 两个指针一个指针p1先开始跑,指针p1跑到k-1个节点后,另一个节点p2开始跑,当p1跑到最后时,p2所指的指针就是倒数第k个节点。 @@ -350,6 +355,7 @@ public class Solution { **思想的简单理解:** 前提假设:链表的结点个数(长度)为n。 规律一:要找到倒数第k个结点,需要向前走多少步呢?比如倒数第一个结点,需要走n步,那倒数第二个结点呢?很明显是向前走了n-1步,所以可以找到规律是找到倒数第k个结点,需要向前走n-k+1步。 + **算法开始:** 1. 设两个都指向head的指针p1和p2,当p1走了k-1步的时候,停下来。p2之前一直不动。 @@ -357,11 +363,11 @@ public class Solution { 3. 当p1走到链表的尾部时,即p1走了n步。由于我们知道p2是在p1走了k-1步才开始动的,也就是说p1和p2永远差k-1步。所以当p1走了n步时,p2走的应该是在n-(k-1)步。即p2走了n-k+1步,此时巧妙的是p2正好指向的是规律一的倒数第k个结点处。 这样是不是很好理解了呢? -#### **考察内容:** +**考察内容:** 链表+代码的鲁棒性 -#### **示例代码:** +**示例代码:** ```java /* @@ -404,24 +410,25 @@ public class Solution { } ``` -### 九 反转链表 +## 反转链表 -#### **题目描述:** +**题目描述:** 输入一个链表,反转链表后,输出链表的所有元素。 -#### **问题分析:** +**问题分析:** 链表的很常规的一道题,这一道题思路不算难,但自己实现起来真的可能会感觉无从下手,我是参考了别人的代码。 思路就是我们根据链表的特点,前一个节点指向下一个节点的特点,把后面的节点移到前面来。 就比如下图:我们把1节点和2节点互换位置,然后再将3节点指向2节点,4节点指向3节点,这样以来下面的链表就被反转了。 -![链表](https://img-blog.csdn.net/20160420134000174) -#### **考察内容:** +![链表](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/844773c7300e4373922bb1a6ae2a55a3~tplv-k3u1fbpfcp-zoom-1.image) + +**考察内容:** 链表+代码的鲁棒性 -#### **示例代码:** +**示例代码:** ```java /* @@ -434,32 +441,31 @@ public class ListNode { } }*/ public class Solution { -public ListNode ReverseList(ListNode head) { - ListNode next = null; - ListNode pre = null; - while (head != null) { - //保存要反转到头来的那个节点 - next = head.next; - //要反转的那个节点指向已经反转的上一个节点 - head.next = pre; - //上一个已经反转到头部的节点 - pre = head; - //一直向链表尾走 - head = next; + public ListNode ReverseList(ListNode head) { + ListNode next = null; + ListNode pre = null; + while (head != null) { + //保存要反转到头来的那个节点 + next = head.next; + //要反转的那个节点指向已经反转的上一个节点 + head.next = pre; + //上一个已经反转到头部的节点 + pre = head; + //一直向链表尾走 + head = next; + } + return pre; } - return pre; -} - } ``` -### 十 合并两个排序的链表 +## 合并两个排序的链表 -#### **题目描述:** +**题目描述:** 输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。 -#### **问题分析:** +**问题分析:** 我们可以这样分析: @@ -469,13 +475,13 @@ public ListNode ReverseList(ListNode head) { 4. A2再和B2比较。。。。。。。 就这样循环往复就行了,应该还算好理解。 -#### **考察内容:** +**考察内容:** 链表+代码的鲁棒性 -#### **示例代码:** +**示例代码:** -**非递归版本:** +非递归版本: ```java /* @@ -534,47 +540,47 @@ public class Solution { } ``` -**递归版本:** +递归版本: ```java public ListNode Merge(ListNode list1,ListNode list2) { - if(list1 == null){ - return list2; - } - if(list2 == null){ - return list1; - } - if(list1.val <= list2.val){ - list1.next = Merge(list1.next, list2); - return list1; - }else{ - list2.next = Merge(list1, list2.next); - return list2; - } - } + if(list1 == null){ + return list2; + } + if(list2 == null){ + return list1; + } + if(list1.val <= list2.val){ + list1.next = Merge(list1.next, list2); + return list1; + }else{ + list2.next = Merge(list1, list2.next); + return list2; + } +} ``` -### 十一 用两个栈实现队列 +## 用两个栈实现队列 -#### **题目描述:** +**题目描述:** 用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。 -#### 问题分析: +**问题分析:** 先来回顾一下栈和队列的基本特点: **栈:**后进先出(LIFO) **队列:** 先进先出 很明显我们需要根据JDK给我们提供的栈的一些基本方法来实现。先来看一下Stack类的一些基本方法: -![Stack类的一些常见方法](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-4-4/5985000.jpg) +![Stack类的一些常见方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-4-4/5985000.jpg) 既然题目给了我们两个栈,我们可以这样考虑当push的时候将元素push进stack1,pop的时候我们先把stack1的元素pop到stack2,然后再对stack2执行pop操作,这样就可以保证是先进先出的。(负[pop]负[pop]得正[先进先出]) -#### 考察内容: +**考察内容:** 队列+栈 -#### 示例代码: +示例代码: ```java //左程云的《程序员代码面试指南》的答案 @@ -606,13 +612,13 @@ public class Solution { } ``` -### 十二 栈的压入,弹出序列 +## 栈的压入,弹出序列 -#### **题目描述:** +**题目描述:** 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的) -#### **题目分析:** +**题目分析:** 这道题想了半天没有思路,参考了Alias的答案,他的思路写的也很详细应该很容易看懂。 作者:Alias @@ -642,13 +648,11 @@ https://www.nowcoder.com/questionTerminal/d77d11405cc7470d82554cb392585106 …. 依次执行,最后辅助栈为空。如果不为空说明弹出序列不是该栈的弹出顺序。 - - -#### **考察内容:** +**考察内容:** 栈 -#### **示例代码:** +**示例代码:** ```java import java.util.ArrayList; diff --git a/docs/dataStructures-algorithms/data-structure/bloom-filter.md b/docs/cs-basics/data-structure/bloom-filter.md similarity index 59% rename from docs/dataStructures-algorithms/data-structure/bloom-filter.md rename to docs/cs-basics/data-structure/bloom-filter.md index 901c4720d77..d013be7471f 100644 --- a/docs/dataStructures-algorithms/data-structure/bloom-filter.md +++ b/docs/cs-basics/data-structure/bloom-filter.md @@ -1,3 +1,11 @@ +--- +category: 计算机基础 +tag: + - 数据结构 +--- + +# 布隆过滤器 + 海量数据处理以及缓存穿透这两个场景让我认识了 布隆过滤器 ,我查阅了一些资料来了解它,但是很多现成资料并不满足我的需求,所以就决定自己总结一篇关于布隆过滤器的文章。希望通过这篇文章让更多人了解布隆过滤器,并且会实际去使用它! 下面我们将分为几个方面来介绍布隆过滤器: @@ -6,22 +14,22 @@ 2. 布隆过滤器的原理介绍。 3. 布隆过滤器使用场景。 4. 通过 Java 编程手动实现布隆过滤器。 -5. 利用Google开源的Guava中自带的布隆过滤器。 +5. 利用 Google 开源的 Guava 中自带的布隆过滤器。 6. Redis 中的布隆过滤器。 -### 1.什么是布隆过滤器? +## 什么是布隆过滤器? 首先,我们需要了解布隆过滤器的概念。 -布隆过滤器(Bloom Filter)是一个叫做 Bloom 的老哥于1970年提出的。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。 +布隆过滤器(Bloom Filter)是一个叫做 Bloom 的老哥于 1970 年提出的。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。 ![布隆过滤器示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-bit数组.png) -位数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 kb ≈ 122kb 的空间。 +位数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 kb ≈ 122kb 的空间。 总结:**一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大。** -### 2.布隆过滤器的原理介绍 +## 布隆过滤器的原理介绍 **当一个元素加入布隆过滤器中的时候,会进行如下操作:** @@ -35,11 +43,9 @@ 举个简单的例子: - - ![布隆过滤器hash计算](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-hash运算.png) -如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为0)。当第二次存储相同字符串时,因为先前的对应位置已设置为1,所以很容易知道此值已经存在(去重非常方便)。 +如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后将对应的位数组的下标设置为 1(当位数组初始化时,所有位置均为 0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。 如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 @@ -47,12 +53,15 @@ 综上,我们可以得出:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。** -### 3.布隆过滤器使用场景 +## 布隆过滤器使用场景 -1. 判断给定数据是否存在:比如判断一个数字是否在于包含大量数字的数字集中(数字集很大,5亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。 +1. 判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,5 亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。 2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。 -### 4.通过 Java 编程手动实现布隆过滤器 + +## 编码实战 + +### 通过 Java 编程手动实现布隆过滤器 我们上面已经说了布隆过滤器的原理,知道了布隆过滤器的原理之后就可以自己手动实现一个了。 @@ -147,15 +156,15 @@ public class MyBloomFilter { 测试: ```java - String value1 = "https://javaguide.cn/"; - String value2 = "https://github.com/Snailclimb"; - MyBloomFilter filter = new MyBloomFilter(); - System.out.println(filter.contains(value1)); - System.out.println(filter.contains(value2)); - filter.add(value1); - filter.add(value2); - System.out.println(filter.contains(value1)); - System.out.println(filter.contains(value2)); +String value1 = "https://javaguide.cn/"; +String value2 = "https://github.com/Snailclimb"; +MyBloomFilter filter = new MyBloomFilter(); +System.out.println(filter.contains(value1)); +System.out.println(filter.contains(value2)); +filter.add(value1); +filter.add(value2); +System.out.println(filter.contains(value1)); +System.out.println(filter.contains(value2)); ``` Output: @@ -170,15 +179,15 @@ true 测试: ```java - Integer value1 = 13423; - Integer value2 = 22131; - MyBloomFilter filter = new MyBloomFilter(); - System.out.println(filter.contains(value1)); - System.out.println(filter.contains(value2)); - filter.add(value1); - filter.add(value2); - System.out.println(filter.contains(value1)); - System.out.println(filter.contains(value2)); +Integer value1 = 13423; +Integer value2 = 22131; +MyBloomFilter filter = new MyBloomFilter(); +System.out.println(filter.contains(value1)); +System.out.println(filter.contains(value2)); +filter.add(value1); +filter.add(value2); +System.out.println(filter.contains(value1)); +System.out.println(filter.contains(value2)); ``` Output: @@ -190,61 +199,62 @@ true true ``` -### 5.利用Google开源的 Guava中自带的布隆过滤器 +### 利用 Google 开源的 Guava 中自带的布隆过滤器 自己实现的目的主要是为了让自己搞懂布隆过滤器的原理,Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器。 首先我们需要在项目中引入 Guava 的依赖: ```java - - com.google.guava - guava - 28.0-jre - + + com.google.guava + guava + 28.0-jre + ``` 实际使用如下: -我们创建了一个最多存放 最多 1500个整数的布隆过滤器,并且我们可以容忍误判的概率为百分之(0.01) +我们创建了一个最多存放 最多 1500 个整数的布隆过滤器,并且我们可以容忍误判的概率为百分之(0.01) ```java - // 创建布隆过滤器对象 - BloomFilter filter = BloomFilter.create( - Funnels.integerFunnel(), - 1500, - 0.01); - // 判断指定元素是否存在 - System.out.println(filter.mightContain(1)); - System.out.println(filter.mightContain(2)); - // 将元素添加进布隆过滤器 - filter.put(1); - filter.put(2); - System.out.println(filter.mightContain(1)); - System.out.println(filter.mightContain(2)); +// 创建布隆过滤器对象 +BloomFilter filter = BloomFilter.create( + Funnels.integerFunnel(), + 1500, + 0.01); +// 判断指定元素是否存在 +System.out.println(filter.mightContain(1)); +System.out.println(filter.mightContain(2)); +// 将元素添加进布隆过滤器 +filter.put(1); +filter.put(2); +System.out.println(filter.mightContain(1)); +System.out.println(filter.mightContain(2)); ``` -在我们的示例中,当`mightContain()` 方法返回*true*时,我们可以99%确定该元素在过滤器中,当过滤器返回*false*时,我们可以100%确定该元素不存在于过滤器中。 +在我们的示例中,当 `mightContain()` 方法返回 _true_ 时,我们可以 99%确定该元素在过滤器中,当过滤器返回 _false_ 时,我们可以 100%确定该元素不存在于过滤器中。 **Guava 提供的布隆过滤器的实现还是很不错的(想要详细了解的可以看一下它的源码实现),但是它有一个重大的缺陷就是只能单机使用(另外,容量扩展也不容易),而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到 Redis 中的布隆过滤器了。** -### 6.Redis 中的布隆过滤器 +## Redis 中的布隆过滤器 -#### 6.1介绍 +### 介绍 -Redis v4.0 之后有了 Module(模块/插件) 功能,Redis Modules 让 Redis 可以使用外部模块扩展其功能 。布隆过滤器就是其中的 Module。详情可以查看 Redis 官方对 Redis Modules 的介绍 :https://redis.io/modules。 +Redis v4.0 之后有了 Module(模块/插件) 功能,Redis Modules 让 Redis 可以使用外部模块扩展其功能 。布隆过滤器就是其中的 Module。详情可以查看 Redis 官方对 Redis Modules 的介绍 :https://redis.io/modules -另外,官网推荐了一个 RedisBloom 作为 Redis 布隆过滤器的 Module,地址:https://github.com/RedisBloom/RedisBloom。其他还有: +另外,官网推荐了一个 RedisBloom 作为 Redis 布隆过滤器的 Module,地址:https://github.com/RedisBloom/RedisBloom +其他还有: -- redis-lua-scaling-bloom-filter (lua 脚本实现):https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter -- pyreBloom(Python中的快速Redis 布隆过滤器) :https://github.com/seomoz/pyreBloom -- ...... +* redis-lua-scaling-bloom-filter(lua 脚本实现):https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter +* pyreBloom(Python 中的快速 Redis 布隆过滤器) :https://github.com/seomoz/pyreBloom +* ...... RedisBloom 提供了多种语言的客户端支持,包括:Python、Java、JavaScript 和 PHP。 -#### 6.2使用Docker安装 +### 使用 Docker 安装 -如果我们需要体验 Redis 中的布隆过滤器非常简单,通过 Docker 就可以了!我们直接在 Google 搜索**docker redis bloomfilter** 然后在排除广告的第一条搜素结果就找到了我们想要的答案(这是我平常解决问题的一种方式,分享一下),具体地址:https://hub.docker.com/r/redislabs/rebloom/ (介绍的很详细 )。 +如果我们需要体验 Redis 中的布隆过滤器非常简单,通过 Docker 就可以了!我们直接在 Google 搜索 **docker redis bloomfilter** 然后在排除广告的第一条搜素结果就找到了我们想要的答案(这是我平常解决问题的一种方式,分享一下),具体地址:https://hub.docker.com/r/redislabs/rebloom/ (介绍的很详细 )。 **具体操作如下:** @@ -252,35 +262,35 @@ RedisBloom 提供了多种语言的客户端支持,包括:Python、Java、Ja ➜ ~ docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest ➜ ~ docker exec -it redis-redisbloom bash root@21396d02c252:/data# redis-cli -127.0.0.1:6379> +127.0.0.1:6379> ``` -#### 6.3常用命令一览 +### 常用命令一览 -> 注意: key:布隆过滤器的名称,item : 添加的元素。 +> 注意: key : 布隆过滤器的名称,item : 添加的元素。 -1. **`BF.ADD `**:将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。格式:`BF.ADD {key} {item}`。 -2. **`BF.MADD `** : 将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式`BF.ADD`与之相同,只不过它允许多个输入并返回多个值。格式:`BF.MADD {key} {item} [item ...]` 。 -3. **`BF.EXISTS` ** : 确定元素是否在布隆过滤器中存在。格式:`BF.EXISTS {key} {item}`。 +1. **`BF.ADD`**:将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。格式:`BF.ADD {key} {item}`。 +2. **`BF.MADD`** : 将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式`BF.ADD`与之相同,只不过它允许多个输入并返回多个值。格式:`BF.MADD {key} {item} [item ...]` 。 +3. **`BF.EXISTS`** : 确定元素是否在布隆过滤器中存在。格式:`BF.EXISTS {key} {item}`。 4. **`BF.MEXISTS`** : 确定一个或者多个元素是否在布隆过滤器中存在格式:`BF.MEXISTS {key} {item} [item ...]`。 -另外,`BF.RESERVE` 命令需要单独介绍一下: +另外, `BF. RESERVE` 命令需要单独介绍一下: 这个命令的格式如下: -`BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion] `。 +`BF. RESERVE {key} {error_rate} {capacity} [EXPANSION expansion]` 。 下面简单介绍一下每个参数的具体含义: 1. key:布隆过滤器的名称 -2. error_rate :误报的期望概率。这应该是介于0到1之间的十进制值。例如,对于期望的误报率0.1%(1000中为1),error_rate应该设置为0.001。该数字越接近零,则每个项目的内存消耗越大,并且每个操作的CPU使用率越高。 -3. capacity: 过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。 +2. error_rate : 期望的误报率。该值必须介于 0 到 1 之间。例如,对于期望的误报率 0.1%(1000 中为 1),error_rate 应该设置为 0.001。该数字越接近零,则每个项目的内存消耗越大,并且每个操作的 CPU 使用率越高。 +3. capacity: 过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。 可选参数: -- expansion:如果创建了一个新的子过滤器,则其大小将是当前过滤器的大小乘以`expansion`。默认扩展值为2。这意味着每个后续子过滤器将是前一个子过滤器的两倍。 +* expansion:如果创建了一个新的子过滤器,则其大小将是当前过滤器的大小乘以`expansion`。默认扩展值为 2。这意味着每个后续子过滤器将是前一个子过滤器的两倍。 -#### 6.4实际使用 +### 实际使用 ```shell 127.0.0.1:6379> BF.ADD myFilter java @@ -294,4 +304,3 @@ root@21396d02c252:/data# redis-cli 127.0.0.1:6379> BF.EXISTS myFilter github (integer) 0 ``` - diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\233\276.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\233\276.png" new file mode 100644 index 00000000000..10547ded08d Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\233\276.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.drawio" new file mode 100644 index 00000000000..72381b7ae28 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.png" new file mode 100644 index 00000000000..46b67b62e85 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.drawio" new file mode 100644 index 00000000000..f4ad6803c4f --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.png" new file mode 100644 index 00000000000..758f84e4a10 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.drawio" new file mode 100644 index 00000000000..bbe5df10382 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.png" new file mode 100644 index 00000000000..4b254eb8077 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.drawio" new file mode 100644 index 00000000000..620445a7606 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.png" new file mode 100644 index 00000000000..072bda258e0 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.drawio" new file mode 100644 index 00000000000..8dc681fa499 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.drawio" @@ -0,0 +1 @@ +7V1fs6q2Hv00PHYPfwM8qlvPfWhnOj1t7zl9Ywsqt2yxiHtrP/1NEJCQIKgJyXHSOdNtggTIWr+sZPEDNWv2fvySBbvNL2kYJZqph0fNetVM0zB0AP+gmtO5xtf1c8U6i8PyS5eKr/G/UVlZfe0Qh9Ee+2Kepkke7/DKZbrdRsscqwuyLP3Ev7ZKE/you2AdERVfl0FC1v43DvPNudYz3Uv9f6J4vamObAD/vOU9qL5cXsl+E4TpZ6PKmmvWLEvT/Pzp/TiLEtR5Vb+c91t0bK1PLIu2+ZAd/v37y5+rP798OtHy1fvDBpn9x18/la18BMmhvGDNBAlsb/qGTjk/lf0A/jmg85yu0m3+075AaQK/YDq742Uj/LRGf/WqDXgyb1Vl2Ql1iyY8OwgiLEw/N3Eefd0FS7TlE/II1m3y9wSWDPgx2O/OyK7iYxSik4iTZJYmaVY0ZK1WEVguYf0+z9K/o8aW0PXfIN+qg39EWR4dO7vPqEGBbI7S9yjPTvAr5Q62VeJYEhn4ZfmzQYuyatNgRFUXlERc1y1fsIIfSrhugM6kQNfu4m04QTEAS9t0G+HdCq87O32DBb0qfG8WXo9Y6VSVjnH+rWoBfi72eXHK0mUnVKj2OZ9XFBKR1up7eO7pIVtGVy66HE3yIFtH+ZXvWXQsG1g5FKyquixKgjz+wE+XBmB5hF/TGF5ITRVX919833FNz3VMx/IMnDiu/qLrtukapu3awPDx5s9dULbYDOfWQYAHXhxANFOz03qxLeIUqoOc+484SMHDurvup6bFY1Qxn3FUcU18VHGB4FHF5gGd9YzQOR4OnWcLhs7hAZ39jNC1tVw4dIAHdMYzQteOOuHTMPfBaRjDyVG1PuqbHAGRkyPgeC8w4Kp5i4lPjlxA3Xrr5Mh1jBfXqKc/Lj45cn3q1pEmR548hHEGEqYjJMchjNOeItmtUB5KCsfvaYgz8P6DwFdLr2p9NWTpVS/y6hVfY68ryzyGJLMHkswRSTK7PZlz7iRZuyG3LTycSVb5Zo+zrGbW98aWHpY1iDXETGDIsqHGgC10KGuNQJ5/71Bm9jTEm2U03/DRsWwgx8SNZNaPwDFg9QxAgw2m9iKpPSTy5pgpC8fkG8WEauXzMIziU859zfc0f6HNHc2DH1xtvtCmM82fEOSDi9gcZxy+8i0Z2Vwml1VBEq+3sLiEBIlg/RQtieNlkEzKDe9xGCZda/IsPWxDtAIv2IesgPK2mcnKHnFwXAy9kpsGwar7XE2CmXo3lx5aZBvdtmQYf1y8jXPVfhdsB9kmOs02aTgvVy2wuu58OKIaOzEpqcOAKp6OU6W+vdtgik0ZivgxpdsF5c+UDp9bMYXClHqwF8aUbtOVP1NMxZQrTME9QU/4mELzeMdiSoeLr5hCYYorfEyhmbtjMaUj7UYxhcYU4WPKADd4vwl26GMevCWttS2t0/ZwuVQtBVC3LSGLgniLer/YZ5kmSbDbx0Vj529s4iT8OTilh7w6TFUi1hVFuTw3QyPu/oVB5K2od//A0oveVmxgNHQDx3Ho7b+21c8uC2uA31rhCK8zj4Pkt2iZB9v1EEhJyMIs3f1e2QOoYodWx1E2/4BduK/WhGQ85umu3JhEq2rftzTP0/eykJW9VTdadJUzhf/gNc6Q0+bAq5nBsnEpw3/o61k+S7cQ+iAugIyCff4Z7ft5NIQSV+LnqgUyLg9IR9RmSoQivTW4ECGFvbZKCutrA4fSaMsCd05gNfJdu8CiDb78wKJZiwsFVw9cRxwWYejR0guVdPZIp185I9IoJ83TU8rJXTnN/tzocXlAOnYGUyL80ENx/73EccdemmmmsLqKlSyyOSBTUckmseI0WytOd2CuMD8cByQQKt3kEN6uZLpJOkhqxYkHiTy6adFsIrXi7INLEum0BqS7KekkpNOQTTotmu+jpJO7dFr9DxCMywPSQbLUWIwFiUTSydknekasZNFNWjKW0s1bb3KK103Oro/SzWvxI5FukhaSGovxIJFINzn7RM+IlSy6qZKDGNzhFC6bNs30UbLJXzYlyw2ySftIV0MxFiTyyKbN2SN6RqwkkU1bJQYxWW6KzgyyVWaQEN20JcsMskn7SNm0eJBIpJs0j0jd4eyDSxbpVMlBLFacwpVT5QaJUU7JcoNs0kBSObV4kMijnA5nm+gZsZJENh2VGMQiMWjo+3f54cjZ9FG6eS1+5NFNh3SQVE4tHiQS6SbNJlIrzj64ZJFOlRvEwqwVL50qN0iMdEqWG+So3KC+IJFIOlVu0M1YyaKbKjeIgVMrXDYBZ9NHyea18JFHNgHpICmnFg8SeWQTcLaJnhErSWQTqNwgJk6t6DucQOUGCdFNIFluEFC5QX1BIpFuqtyge+CSRTpVbhATp1a4dKrkIDHSKVlyECAdJLXkxINEHul0VXLQzVhJopuuSg5i4dSKlk2Xs+mjZPNa+Mgjmy7pIKmnOPEgkUg21UuDbsZKFtmUPjFIhCx6Nr6apL2+nfabnxxxUok/13XxgQi97/ddq6d/2EN9gwMk7Wj6ABz92TvjwqGMnLEj74RDKgz5G5J/njEQ+82ZUeHwOJszKhC7AtEHYgPRu8HbecJA9PoTc8aFQ3k0ggLRqF7WIgz6GxJ7njES+42XceFQ+TmiItE3BUfiDcbNM0Zif9LNuHAAovfxn1WGJN2k63QbJD+nRR+hrv9flOen0ioLDnmKA9N2JGGPZKdvZXtF4TsqwFAoi6/H5sbXE9bLUbiOOvu4rMqr2O66zK530mVREuTxB34AWieXu/6KQrzhtwHcb3NBy0fbp4dsGZV7XaAiGvLbb1drN3S+QqKhAvP6eh6ggSsvDSiGNhNOdL3e90FO+B4rTpit9wa5I3PCezJOMIfa0NtYO/6Lcx/a5O/gkG3xBtxXgPcAbjAEnPjNwNEB98lb09IAznCg7/rJTdbib7AS/3ZDvGlAJvZJQwNe4t/16+Wsxf9uTrTF3xyZE+SqTBpOMKRB15vDGA8NHquhgWiINw3Ix2WkoQGvoaHrJayMh4b7OdEaGryxhwbyZVfScIIhDboeVWU9NLCyDIiGeNPAkZcGvIaGrrd+sB4aWFkG3siWgS+xmyjJCrJtGXgMLQNKW7wBl9g3ZBj3XcmwjLXAZzVNJBriTYNnswqHzA/4LB3aWnA/J1pa4I89TVRu4q1a4OvstIDSFmfADZ2S6Db3tOlcg8vIuaN5rjadaPOFNp1p/oRgRx4dcxxuPFe+vFNLuXkbJPF6C4vLCN3UhxXozmy8DJJJueE9DsPi9jHtnjHOyjbFykt6JCvfru7onmrcybT86nlbLC2fV1a+oVMy0xBUCwTS3NUmnuZNFVSmD4QjZVKQgrG00OBk+xJUvgY12F+gDxNQbIIITjTPRGjC/0/9Ek1vRsak2djmaFNd80CxH0Tes1GVD6vmqGY61SZucRBDmwKibU+bw4PPtMkEHWQyR4RCp+aiz01+lS2f9/KLvWx0UuiMHG3iNC7IQc1S5pmPc/Cc6SEZAx3T72dg9YsCI1GQ9kxjxRij/lTjOSO946cFy+sf2R1jXLQG5NahyU41/7lMw+aX2unykH0UPWcQ/XjHvI146G2xsOB/NyzSmE++gNkWZfPuyRcA/W1xn3xJ/0Dkw8+p3jZwwjmO4HcCGLp6+FHISwHKYCCpIo4JtGcjQVL1AdyyrAdH8M8hPecAW6tVMUw2qsAa/a13hmdz3v9cz5RdP/LT62XoXWXAqM+rw5kBwQD1KsJeuCR524Ch/9jvU39cXYHbnuJQ1HVcTAyK3aTUdQx17X87+shMIN0sNba24mQMuGAxS9FE5bKygT24+SUNI/SN/wM= \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.png" new file mode 100644 index 00000000000..37255d9bae6 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.drawio" new file mode 100644 index 00000000000..d1072768e67 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.png" new file mode 100644 index 00000000000..8b0b528bde5 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.drawio" new file mode 100644 index 00000000000..4e40d1522e7 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.drawio" @@ -0,0 +1 @@ +3Vrfc9o4EP5r/FiP9dPyI1C4PrQznWbuLu2bwAp4zljUVgL0rz8JS8YWJqEJhDQ8gLRarWR9n1baNQEaLTd/lXy1+CJTkQcwSjcB+hhACEBE9Y+RbGtJEkW1YF5mqVXaC26yX8IKndp9loqqo6ikzFW26gpnsijETHVkvCzluqt2J/PuqCs+FweCmxnPD6X/Zqla1FIG4738k8jmCzcyoEndsuRO2T5JteCpXLdEaBygUSmlqkvLzUjkZvHcutT9Jkdam4mVolCndPjnVrHB3+vP6MvP9bf1YvTrB5p8sFYeeH5vHziANNf2hlMzZbW160B/3pt5Du9koT5UO5QGWgGS1WbfqEtz8xs5G3oyUye0i9BYhHp2GkRdGa4XmRI3Kz4zLWvNIy1bqGWua0AXebWqkb3LNiI1k8jyfCRzWe4Mobs7QWczLa9UKf8TrZY0Tqaab27wB1EqsTm6fKABRbNZyKVQ5Var2A6QkLqLJTLBFtd1ixZWtGgxwsm4JeK8sbzHShcsXL8BHeyBzl/iIh2YPaBrhSxEd1n1c5fbW12JXOV7u/Jx06ltXW2TqVtnQZd3fUJia/tOpuL61PMS6cFO89Zez13elzPxyENbb6J4ORfqET3Uj2ULK9KDlZOVIucqe+hOtw9AO8JXmekHaahCEA6ThMSQxURXGOgSB0ZhFGEYA4hjTEHSNV8vgbXY3s7+IACEhLbMII+eKMToYA5ulHoBD0bZEbFZr+dzE13CrcD36FYI7roVCq7sVvAloEPvETocedAlV4aOXAI6/B6h8w/zq0NHLwEdeI/Q+bvu6vew+IX3sDPejlyA9NTtiF7zdoQZCiFrLi4QdC8uFLBu8zOvRzENY9Dcf+KkOwgGfa2vdDtib4cx5ETGHNmTr8MY5N2RSOLt5VNJgcEThi4MfPJC4F3w5SKsU4KvJsxrYr5Wr0cCvTOSDJ9IMnJVknnnShw9k2S+IYpel2QugHg5yxpmfW+1PMGyFrFOSSeckWWnpgbwVQ8/zwPF+Lksw08YOsIyDTvfttRWRqF6ZMJ+fEo7WUZdqC2el8J9acmXOsoTCXw9N4n+CAL7kZPv3U4+i8kT/vbSbhK+FY69PRd51YP4/TCsLwvqMcyEwtmM55/5VORfZZWpTBa6aSqVkkuNvlMY5NncNCjpReXVgq+MseVmbt7AhVNeZbOQl3Ugzks1KOa70aIQxCyOccIQ00FXjA0CQ0PxRsE4u5RXCxPd74x7Eb4gAO/ieJN+6ET+5tNE/u5FGezLBUwmSH/OkwsAiIaMdUDGFDSiFl8RoCEkh5yFaC8/e2YA9OVSr0kACDBkKGKQERjrb+oTAMcoYSABNGY00lxBfxghnKuwZEDsMDdESA8N0MWyQ+B4UrZa8SI4JbkX9SX3gjENdPiajIJxHAxxMIDBOAkGIBgOgjEx32wSjCfBcBQkg8iW2Ag0JdyUYFNq5+nr6R1JGmqIlEfDDq72uGzTxYq45fFMQyrKHoIvszTNj6UjS3lfpDs2Rj41z0AfSLsnBut5GdObXIS/Tx9d3b/7r4+c/T8o0Ph/ \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.png" new file mode 100644 index 00000000000..784add44411 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\345\271\277\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.drawio" new file mode 100644 index 00000000000..055cf29683b --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.png" new file mode 100644 index 00000000000..5d2a50290da Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.drawio" new file mode 100644 index 00000000000..053d6656491 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.png" new file mode 100644 index 00000000000..2a669ee2066 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\227\240\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.drawio" new file mode 100644 index 00000000000..d090804e912 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.drawio" @@ -0,0 +1 @@ +7V1dj5s4FP01fuwqfMNjkiHdSl2palfa9tFJnIQtg7PEmUn2169tTPgwJEwaYjJrdaTBFxuMz7n24eLbAdb0+fAxhdvNH3iJYmCOlgdgPQHTNIyRS38xyzGzBKNRZlin0VJUKgzfon+RMObV9tES7SoVCcYxibZV4wInCVqQig2mKX6tVlvhuHrXLVwjyfBtAWPZ+le0JJvM6pteYf8dRetNfmfDDbIzzzCvLJ5kt4FL/FoyWSGwpinGJDt6PkxRzAYvH5es3azl7KljKUpIlwbHWeiv/dDdup+i/WQWf4g/ff0grrIjx/yB0ZI+vyjilGzwGicwDgvrJMX7ZInYVUe0VNT5jPGWGg1q/BsRchRgwj3B1LQhz7E4iw4R+V46/sEu9ZsjSk8HcWVeOOaFhKTH7+VCqRUrFs14KW+3wgkRHTF5OYrjKY5xyp/WWq2Qu1hQ+46k+CcqnTHG7B89k40QG5bWgc9HEe/TBToz2pYgMEzXiJxDxTzxgzoWws+IPhJtmKIYkuil2hEoGL4+1StIQA8ED97ACVMlJwoe/CidaebEQ2Lrq4RWdPIFxntxJ2C6Me3uZF5B3P1nz6YlPsIfdnyIx7SC6WwPxUl6tGa/R/k1aJfmuVHmUBzTOZtx5XUTEfRtC/lwvtJlo8oEuNtmE/kqOjBGdYd16QXz0eh08xeUEnQ4D6wMhGhgOWLaFuuWny9Ir6VVQJg2pQUgt90cOvusVyY4ua0bXjM1Gw88NTsd3ddV6b5OH+5rvkf3de2a+/qK3dftAzrrPULnjKrQBa5i6Ly7zrz/O1HsP8LM6/fhvvZ7dN+6cFLuvoE0or2opPOvKi0a6ZH81DAf4Q0nl+n30smP+rraGU2l065xPj6lcu2to3n1WvwQLHB+kQWi6Rcc0S4WUi9wq2uFU1sEsn6JVjUunbrxC/Qy+1jWjfe4rNdVufJ4iNEey1pGL8WwZ6bdFiadEB01IVoixdmQ18mW3U4yVzpW4wOFhlSBr+Ip5roy+MIE42id0OKCQomofcKAjhYwHosTz9FyGbcxrTp53oApXs2p/ZHMFLuBKWZvTLEVMqVlMtBMYcywhsaU9gBb/0xpicNppjBmeENjSns8r3+mtIT9NFOYhDSGxhRPIVNaIkyaKfxlY2hMaQ8yaqYoVbQ1phgj5VQJ9PLzEFRpin7flSn5hbWkHTpTTNVMMRQyRb8md2dK016G+zKlPWyqQ2+DYoqtmikNQdrQBcEU+AEIHRCMQGDwgwmYhCD0QDAGvg3CAPgmmExYZT8EY4efmoFxwE4FPpg4rNX4iR3TA1p57A8U1/qHohvg7EiCtGFKMK27In1+f+Igd5IX3+au+Ri/2KcvvO8GUPwVr+vexZt8xBunKTyWKmzZx7ld6cq1b3xebSdernNmHev71vn6rn+2Pj3IenzTD4dmU6C4Rv/dBm7ZIYFzTvYSbZsmjR2FL+cXpxelG4wSNvvwNgscx3C7i/jFshqbKF5+hke8J/lt8pK814CVRd8a+LqEyF818tVd+Gi+utHyVP8yZDXMWqPGWau3aaspjtsCJH1QEsH4K1oQmKy7YCpjtkzx9s/cT5mBuw9KwxfEvEjeEiAWJMInRHYyRqu87RwTgp9FIRXDdbooHypnQn/o4E3ZZOfQp5nSslGU6Q+rnpIpTij2MOJIIrgjr2h3mUhdOHHGgWSmXGKC3RsR5DCtvJPoV5jA09NgwQRMh20V84SwDRUTKLkF8H2h1bIJ6IKs7A8sOVIqbxTSYDVOwh1fCfrDTg5dakfriF3HF//esMvXa43d27EzOsb3+gNPju/pSbMreB3D+P2B1xRy0zpVmU49ti6m95Wtlhxg0zNy1WmGI1steSOiBusSWAORrZYc8dGrZ0fslMtWOcijseuInXrZKgdmNHhdwVMuW5u2tGnZqlq2Nn0tvK9s1UGgS04zHNmaX1iD9QawBiJbbR30uRo71bLVlmM+GruO2CmXrbYOzFwPnmrZajdljGrZqlq2Njn1fXWrLUeBtFdXvWZAulUO+2iwLoE1FN2qt+NcjZ1y3SoHfTR2HbFTr1t1ZOZ68FTr1vy/T9K6dVi6tYEX99WtjhwG0l5d9Zrh6FZHjvtosC6BNRDd6uiwz9XYqdatjhz10fHWjtgp162OjsxcD55y3dqQhRU6LCN4HILQBpMZGBs8cXjEcodDE/j8QKR3s1EC7E9A5VlrRRK5tVoFAdskWc8r/xiRzX4u8otZEnEAfI8nI/vspmOelez7IDBLSeTZjUQWuejF0ylDPa2QrSXL3bAbs9yzFOgg5FnNNvCfbvKQX55mF3tPn5eOwMTjzzsDgVtKx6ZD4YGxVRoTjx0Enkjinox55Rk/8FgFX3a6YWRhx3CO4glc/Fxze/3mhd8ZtiiXkiAn/B+4Te62W02C9IKGlwOzwfuuSN2mxeLvi2W5r8VfabPC/wA= \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.png" new file mode 100644 index 00000000000..4ea176bdc27 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250\347\232\204\345\211\257\346\234\254.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250\347\232\204\345\211\257\346\234\254.drawio" new file mode 100644 index 00000000000..f4a1af66763 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\347\237\251\351\230\265\345\255\230\345\202\250\347\232\204\345\211\257\346\234\254.drawio" @@ -0,0 +1 @@ +3VlJl9soEP41HDNP+3KUbLnnvUnykvRhOrlhCUtKY+HGqC3n1wcksBbHiqfHjpfng6migKKWrwABc7KsHihcZR9IgjAwtKQC5hQYhq5rDv8TnG3D8TWtYaQ0T6RQy3jMfyDJVGJlnqB1T5ARglm+6jNjUhQoZj0epJRs+mILgvurrmCK9hiPMcT73H/zhGUN1zPclv83ytNMraw7ftOzhEpY7mSdwYRsOiwzAuaEEsKa1rKaICyMp+zSjJsd6N0pRlHBjhngf3yqXrzvwQspzRhZ0xn7uHgnZ1mzrdowSvj+JUkoy0hKCoijlhtSUhYJErNqnGpl3hOy4kydM78jxrbSmbBkhLMytsSyF1U5e+q0v4qp/rIlNa3kzDWxVUTB6PapS3RGCbIdVlNq3IIUTCpi1HSO8YRgQuvdmosFcuKY89eMkmfU6dED8eM9+3ZWRiMljdGIcU0Zr5CmiI05wWgEhek7K0g3PiCyRHxLXIAiDFn+2g9NKCM83cm1QcAbMg7+Q0wYl4yJNg6+dnp+HRO34FvvqlwrtX6FuJQrAcPBXP9w3vO481IKWKot/G5dmzjgAoa9qtpO3krFv6bm4CrNFXM/hjDmmC1iZZPlDD2uYG3fDS8b/UiA61UD5Iu8EhF1vFsT159r2phbXxFlqBp1hOw1HAnbsm5ZnqQ3nSogWVmnACjeyV1njWZlQYrTpuFboFm/HWi2j0xf56rS1z5H+hr3mL623U/fHX2p9HXO4TrzHl1n6X3XOeaFXef+UeS990Oxd5PI650jfa17TN/hweni6evvWfQsp6Txq8qBM9IV56m6lN7YDUcVjz91Tr6R6+rR3rwu2NXH36cuWXuH3nxzLb7GKLBPHQVy6CeSc53bU7p6HVa1whgUgUZROWoQSzs1/kd4Geco6/o9lvXhqfzi7yHG+IPIVT5dt2Dwluofl/S11l2/0seS86BGQCncdgRWAg3Wh0HFGV79/cF3kd/Ja+Pyar4D8rzRaPxWpNp+e/+cfaY/4uD5kfzzZZHPlrH6TtMFqsgGYQSCCEQWCGcg0EHkAK6bPwGRAby6IYFIRBIQ38hUVLSQxSPH901zH8UecpaVczGnFwHfA5EPPBf4fBVPLCqWs4HnAb/7ftQsJBFPajHdQSo9BlN161eYKhbzQ+BHohFYwJueZJOfprPfas/3yy0QuvV+Z8B3pC7c+MIULgjMjk1c0fDd2hcTEAa18KxuuELA2z+fcgRmfZTpJ7E85XQzXrIgztOCkzFPacT5ocDzPIY4kB3LPEnwoRrTB0MM5wiHMH5Oa/5w8RaLdEvSHZAJ69+JLpDeoNJY+5Vm91G1W2qGh4cjSg0n2w+wTa62n7HN6Cc= \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.drawio" new file mode 100644 index 00000000000..a2ee5e47f22 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.png" new file mode 100644 index 00000000000..6fd51040397 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\234\211\345\220\221\345\233\276\347\232\204\351\202\273\346\216\245\350\241\250\345\255\230\345\202\250.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.drawio" new file mode 100644 index 00000000000..bceea69589d --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.drawio" @@ -0,0 +1 @@ +7V1dk5s2FP01PGYHJPH1aHuddDLtTKdpp80ja7BNyxoHs7ve/voKDNhIwmBbQqpXmUwCAgToHN2je3WRDTh73n/Jgu36lzSMEgOY4d6AjwYAlmU6+L+i5P1Q4pvmoWCVxWF10rHgW/xvVBXWp73EYbRrnZinaZLH23bhIt1sokXeKguyLH1rn7ZMk/Zdt8Eqogq+LYKELv0zDvP1odQD7rH8pyheres7W45/OPIc1CdXb7JbB2H6dlIE5wacZWmaH7ae97MoKRqvbpfDdZ87jjYPlkWbfMgFf3zavoXL39Cj4z99/eP15cfy649PVS2vQfJSvbABnATXN30qHjl/r9rB+fFSPOd0mW7yT7sSpQk+Adjb/fEg3loV/5t1HfhhnurCqhGaGgF+Ogwi3pm+reM8+rYNFsWRN8wjXLbOnxO8Z+HNYLc9ILuM91FYPEScJLM0SbOyIrhcRs5igct3eZb+E50cCV3/CfOtvvlrlOXRvrP5rAYUzOYofY7y7B2fUl9gVzhWRG5wfTuhRVW0PmFEXRZURFw1NR+xwhsVXBdABxjQkU28CSdFH8B7m3QTtZsVv3f2/hfeMeud76c7j/vW3nu9t4/zv+oa8HZ5zYNd7R0vKnbqaw7PFYVUTyPaHj97+pItojMvXVmTPMhWUX7mPMjG8gQrm4FVXZZFSZDHr+3HZQFY3eHXNMYv0lAFAf/B920XeK4NbOiZbps5pvlgmgi4FkAuciwftm9waISqztMOTd7GdB5s56Qegp/wAcHjQ1jEXQ5NSN2lpGLTYtezE4owLECwYek2IF0mh4NhQYgwLL5kw4JEQAfvETpotqGDrmTobBHQoXuEjpRz6dA5IqCz7hE6stdJH4m5N47EOI6Pahepb3zkyBwfQdd7AF4zcAH10KQRQPbhi8dHrvXgWs0IyPXawzAI2IdHGiB56pDGHkiajm45DmkAOUxyie48lBbQ6qlIMPD+jcDXHljtZg3xwBpfr3H8Tq464+1xJBkaSDJbKsnIAR2pGUNJRlVkjUuy+na3s6xh1veTIz0sOyHWkJgCR5YNjQ8gqfpHWCAErmUZ6qlINMtY4cNbbdlAjsmzZPB/wTHSvyEN0GC5JCsiTaJojgFVOKaeFZOqlffDMEascu4YE9PwPGP+2ZjODH9CcQ77r3mbaG1nuCLiqR9cFQVJvNrg3QXmRYTLp4U3HC+CZFIdeI7DMOlyx7P0ZRMWzndJuiIKUE2aAV4THW4bDruG54RW9STXKa1I7eHmXlvdAckwfj1GNQ5Fu22wGRQwMVkBk5OYy9ngV1N2uB1V3HowJZnDgSk2IIYfkGYKYhggcUzpjn+KZ0pHhFszhcGUxsRLY0p3uFU8U4BmyhmmWG2mSLcprOjuWEzpiN9rpjCYAqTbFFZIdyymdOTcaKawmCLbpngO1djtl06zfJ2u0k2Q/Jym26r1/47y/L0a7AcvedrGhvQEOtJ7OAZK+gMgJhuWGx1P2yPg9K90PB3YU5HoKSBXXRqU9VTPZXHkBOyYebyRE67JixPIbo8/zJE54d0ZJ7hD7ZFQe/6DfR3YHmkA6KpEw+1ruM/D7fOD25cOd22VVISbo5EHHVMfvIUf8RJ+siLRNLDUpYEo4QdiBoOU8F/NCUL4m2mSsTgB1OUERxogbxTTAHmZBqoi0TSA6tJAlGlAYuSCNA3Xc4L0CcY2DUhdTnCkgTNOuADyChdQFYmmga0uDUSZBnuccMH1nCBMAxo5XOArHElUw38kwwWQX7iAUZVouBWOGHLs9S4YRQkQr0EiVZFoGtxbkHDI6ECM40AqwfWcIJVg7EGijiReqAQIclMCRlXCE/wZGf5zz5jODexCzm3Dc43pRKcvAseiJ3vHTV+saUci9bnAaO4aE8/wphopgBgfvo+MFGAgZRdpwP5jsTFxS6TwvxNj6hRJwp5p4EHFATtvVpwzxSVOge/EKQ85Bn4tf1ZcNUXGBBQbGGUPFYdwD8VH574xscoKcc3AmPrlVajoy8Xd8X3tk4RkXI4rf/w4PMHE6OUJZCW6CyQKa3kLAo+zHyVQH06b5Z/mSL3W0CVfetINONaXmW187Ot1FTq9VQnXVVa2uYa2qtblBy1dlXBoWenhHxZa4qubG6Clqhq/1w74qHq3DrbFZh48JQSuLFnb4UeulaxAeIGFLYg3hT6W1yzSJAm2u7is7HDGOk7Cn4P39CWvb1PvUbLY9oio5TLCIPKWzCXNnIUXPS35CKtLToMPXEiDdJH5LWk24KvlGkb8mnkcJL9FizzYrIYgSiMWZun29zrwUBRsC4JG2fwVt+CuHtHQA6a89J+Lg0m0rK99SvM8fa52sqq1mkrLprKn+C9+x1nhMtv4bWZ43zru47/F6Vk+SzcY+SAucYyCXf4W7fppNIQRZ7rPWUs0Lg9oj4meAruFCOVakcGRCClutWVSWv01HutGGx64CwKrK5GiJ5lZHFgMpwlXqOE6D9e+DYs09AZ4Mlo5SdQcSCQLSVfOAV6LVk4Bvbt/odFxeUC7OHSu4Yc1xf0rcoxre1kfoWqszmKlimwOWPNPyyaJGpWAP3DRTXEwDliFT8umgN7tKiabdPxIO5ztTqKObEJWlEg7nH1wKaKckI7taOXsVU7qS0XZyglZUR+tnMKVE/ZPGo3LAzp+RH/B8lFN8aGTKKScgqNE94iVKrI5YO5ay2bvDKd02RQc8tGyea77KCSbdPxIm+K+NWKk2V7BUaJ7xEoV2dSJQTymN2XLJmKFfLRsipdNxRKDEB08olfU+aimuGu5PVm2FwkOEd0jVorIJtJZQRy8TSg7KwjprCApsokUywpCdPBIB2n7Vh+SZntZISI9vdkHlyrKqRODODic8pVTJwbJUU7FEoMQHT/S+bR9aznKsr224CjRPWKliGzaOiuIQ1bQ0B+xFwej4JCPls1z3Ucd2bTp+JHOp213EoVkkxUl0g5nH1yqKKdODOIRqpWunDoxSI5yKpYYZOvEoL5OopBy6sSgi7FSRTZ1YhCPOK1s2XR0YpAc2VQsMcih40c6Ttv3wxqybK8jOEp0j1gpIpuOTgziEKdFsqc3HZ0YJEU2HcUSgxydGNTXSRSSTZ0YdA1cqiinTgziEKeVr5w6MUiOciqWGOTQ8SPtcPb9VpMs2+sKjhLdI1aKyKarE4M4xGmly6YrOOSjZfNc91FHNl06fqQ/4Oz72UtptlcvF3QxVqrIpvJZQTJk0Xba3iRr2XbmD4mJw0mn/ZzXxRt6aH9mDwvr+ssf/lBfEABS1preAEd/7s64cOhAztg9770NqTTkL0j9uceO2B+cGRUOT3BwRnfEro7oO3I7ondBbOcOO6LXn5YzLhw6RiOpI1r1Qi3SoL8grecee2J/4GVcOHR6jqye6APJPfGCwM099sT+nJtR4bDMCwI0Cs4/3R4zO/PjzrKCm5bpanM4xs8Cm4p9b2WZdNBGTx8R3WIMuPBulqb5ybEvuAXXv6RhVJzxHw== \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.png" new file mode 100644 index 00000000000..ef97be64bb5 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2421.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.drawio" new file mode 100644 index 00000000000..a25027440d0 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.png" new file mode 100644 index 00000000000..a7074d37b28 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2422.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.drawio" new file mode 100644 index 00000000000..0c727214ab0 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.png" new file mode 100644 index 00000000000..063d9ca8a6c Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2423.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.drawio" new file mode 100644 index 00000000000..b115c61ee2d --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.png" new file mode 100644 index 00000000000..72456379dc5 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2424.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.drawio" new file mode 100644 index 00000000000..a5a6d89bd82 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.png" new file mode 100644 index 00000000000..0a9d4084789 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2425.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.drawio" new file mode 100644 index 00000000000..dcf6c68ef18 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.png" new file mode 100644 index 00000000000..5393b0c06fe Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\2426.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.drawio" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.drawio" new file mode 100644 index 00000000000..ae770165460 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.drawio" @@ -0,0 +1 @@ +3VpLk6M2EP41HMclifcRv5JDtmoqU6nNHhkjYzYYsUKewfn1kYzEy2Dj8TOew1hqdUsgfV833aDpk3X+G/XT1TcS4FhDIMg1faohBCGw+I+QbAuJC0AhCGkUSKVK8Bb9i6VQqW2iAGcNRUZIzKK0KVyQJMEL1pD5lJLPptqSxM1VUz/Ee4K3hR/vS79HAVsVUgfZlfx3HIUrtTK03GJk7StleSfZyg/IZ02kzzR9QglhRWudT3AsNk/tS2E37xktL4zihA0xCN0cvE1oDl+nKZn+lX3/Mwxf5CwZ26obxgG/f9kllK1ISBI/nlXS8WJDP7CYFPIOJZsk2PUA71UGfxCSSpWfmLGtPFl/wwgXrdg6lqM4j9jf0ly0f4j2yJS9aV4bmm5VJ2F0K4xewAhAJSgsLegoQWW86zWsXzGN1phhKoUZo+QfPCExobt90Odznf/xkWKHxLb0brzaRbKhC3xgt5EEsE9DzA7ouSU8OK8w4ddJt9yO4thn0UfzOnwJ8LDUk6Yepf62ppCSKGFZbeZXIeAKiqu2BKpkKnRaeDqir+tuXZ83iitQvdqtVKIdRk/Aq9zBDz/eyG3QkBXzvRy/N2Bs/doIXo2XJGEv2Q57HldAZppXg7wVil+g5uCX9K6E+8SIY+50BAE+VxHDb6m/O+tP7veaiPaztPBEyygXzBgvoziuIWu5xNZi0YW5wHbfASgX/8CU4fww6vZRIg1Kv6OO05D9z5obk6JVzYMpWReuamd7+tHpHUfX3uIk8ITP5r2EJLjlKBTpQYPwx8gu3Qs8wb1ckPEqjB2lvNl9mLXDMjsOS8lO8wx7VDZcd+S6po0c20Sm7gC7CR3E3SwwkA2RYRsWdPXmAsUuyDkPeAzDtkamVZunBVB9ZOjVRcDWKsUW7q1yKc9iPE4kLOH5o47Ow5FQGg2lxU3CnTkQ+xB0g38wqs/yS+Y1Qgp6xpBiwmZIKR/S7xVSrIfhbD28wC8xFj4CY+3zotVtCGtfg7D6MxJWbz2i8+z7voR1HoawzSD7P6asO5Cy9pmU/VJOiawmAA1wOKc04EH96+SU7jX8ifGM/qSdU97dnyi4XPbs4DOeXTsW3L0eAGHH2Z1SELigEx1amOvLVG6UphvOCDll/oxUhlw+jncPn5qmmwYc2bBMxO1mmo5ao47dXOXKaTrsqgDeCTVDQy+6J2j0VkiFbovQQ2GhO0cmuvbJn11APOdNQ+stQ/kc2PPod0GUDc3Jel4a3AZlqP0yAHwRZe2J+Ey3RZlxKZRdIME4VsW7IMoGF6p78ogbObOWD9KNrzozeGSiHpidmgC1ExqVEF01oYFdJc1zPeVABN/PTw6tNt8VwEY7g2q7t8HvUtARh3ttP2k9CsYe0EfeNRQ/D8T6C71Z6ieD8nvQld9rM1sbe5qHZGM812aO5kHNc4TEM4RwZmn8Aded7HSMnbKjjfkoVza18UxzyhLPO61m3rdyxcxiQlP8d7j5XBtPNNcDsuVMjLKlly1UturliOLGeyoSDOesSbJmRUGSsF5+kCI/jsKEdxecE+KTmLEoNUQLP/bkwDoKgriv1tEsygZ+tirrtdr5hYsSv6omqPo1OnVWLhDop05P5YJ3qw+yChxXn7Xps/8A \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.png" "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.png" new file mode 100644 index 00000000000..95655ea4138 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\233\276/\346\267\261\345\272\246\344\274\230\345\205\210\346\220\234\347\264\242\345\233\276\347\244\272.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2401.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2401.png" new file mode 100644 index 00000000000..63381b522c1 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2401.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2402.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2402.png" new file mode 100644 index 00000000000..c23bbfa8cda Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2402.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2403.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2403.png" new file mode 100644 index 00000000000..46fdb57bd9c Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2403.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2404.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2404.png" new file mode 100644 index 00000000000..a2e3a937acf Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2404.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2405.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2405.png" new file mode 100644 index 00000000000..1129eeac765 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2405.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2406.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2406.png" new file mode 100644 index 00000000000..d18889c3e55 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\210\240\351\231\244\345\240\206\351\241\266\345\205\203\347\264\2406.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206-\346\217\222\345\205\245\345\205\203\347\264\2401.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206-\346\217\222\345\205\245\345\205\203\347\264\2401.png" new file mode 100644 index 00000000000..424fe3d91c0 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206-\346\217\222\345\205\245\345\205\203\347\264\2401.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206-\346\217\222\345\205\245\345\205\203\347\264\2402.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206-\346\217\222\345\205\245\345\205\203\347\264\2402.png" new file mode 100644 index 00000000000..f58aa672c3a Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206-\346\217\222\345\205\245\345\205\203\347\264\2402.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206-\346\217\222\345\205\245\345\205\203\347\264\2403.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206-\346\217\222\345\205\245\345\205\203\347\264\2403.png" new file mode 100644 index 00000000000..24977de86a4 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206-\346\217\222\345\205\245\345\205\203\347\264\2403.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\2061.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\2061.png" new file mode 100644 index 00000000000..fa08e776ec0 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\2061.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\2062.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\2062.png" new file mode 100644 index 00000000000..ec8a89d0943 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\2062.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2171.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2171.png" new file mode 100644 index 00000000000..655aa65a38c Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2171.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2172.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2172.png" new file mode 100644 index 00000000000..fe60da909c5 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2172.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2173.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2173.png" new file mode 100644 index 00000000000..61633b1d3d9 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2173.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2174.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2174.png" new file mode 100644 index 00000000000..e502b808f20 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2174.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2175.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2175.png" new file mode 100644 index 00000000000..9d287858305 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2175.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2176.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2176.png" new file mode 100644 index 00000000000..85bf1308cfb Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\346\216\222\345\272\2176.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\347\232\204\345\255\230\345\202\250.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\347\232\204\345\255\230\345\202\250.png" new file mode 100644 index 00000000000..de77a9af2ab Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\240\206\347\232\204\345\255\230\345\202\250.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2061.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2061.png" new file mode 100644 index 00000000000..f69153d0c06 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2061.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2062.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2062.png" new file mode 100644 index 00000000000..fcbf71ca5a9 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2062.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2063.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2063.png" new file mode 100644 index 00000000000..c4f890b18f8 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2063.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2064.png" "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2064.png" new file mode 100644 index 00000000000..6d6c57fdc0c Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\345\240\206/\345\273\272\345\240\2064.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\206.drawio" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\206.drawio" new file mode 100644 index 00000000000..f8fe81d2c06 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\206.drawio" @@ -0,0 +1 @@ +7Vtbk6I4FP41eZwukgBJHr1g727tVk1VV+30PNKSVmbQuIit7q/fBAISBLVbFHfa8sHkJORyzpcv5xwR4MFs8xj7i+lfIuARQFawAXgIEILQcuWXkmwzCWMkE0ziMNCddoKn8F+uhZaWrsKAL42OiRBREi5M4VjM53ycGDI/jsXa7PYqInPWhT/he4KnsR/tS7+FQTLNpBSRnfw3Hk6m+czQZVnLzM87650sp34g1iUR9gAexEIkWWm2GfBIKS/XS/bcqKG1WFjM58kpD6yenT8ev/39O3VCf/C2ZpuF7X/Ro7z50UpvGCA3kuP1X9SSk63Wg/vPSq2z/yrmyZdlaqWe7ICcxWbXKEsT9d3Lx5CLecmFWgnFiEiuThpRVvrraZjwp4U/Vi1riSMpmyazSNagLPrLRWbZ13DDA7WIMIoGIhJxOhB+feXueCzlyyQWP3mpJSDsxbKKyd94nPBNo/pgYRSJZi5mPIm3sot+gGBtRw1kzHR9XYKFFk1LiMhlvgbipBh5ZytZ0OZ6h+lQjemqKp4HPXUGZG0u5txUq9x3vH0uV76XK0O1b6uobXUtm4MHe6emoke5DrGKx/zABrA+zn484ckxjO7bpaR3p0bvuSzmkZ+Eb+Zy64yhZ/gqQrmRndktYpidWBV7ZtvUT5WPX3Ug5jwgaiMCkU1sN1+JHtYm1GzF5iyZkvZmSYFT6OTjWMKNNLBc+POPM0GJTDLBoJ4aClk23a/BGJSajGF3zRj2tazc/0RWdq0bs7JzLSsPP5GVHWRa2SEdW9m9lpUfP5GVmXtjVibn+nibMHkulb/nPp0s7xw8Vcn9u8IrLCpX9grt/4NX6FaAQuAHvUKKqIk4hq7q+NFzAVYbRFhH4JLD0irBEh6EZQcAc7oEGKneN+yjYYdDD4UdrNOwg52JvhZR4Z6ICtwlKiiCDy7GDrapdEgQNI3pUPjAIGO27AZtNw9c301J2D2AGIdada1XAkxOs5d3e7xP5Pa49MbcHliXljzX7zl8wdT7Pccusi78HqvelldyfMw8FWQmcCB8INAhiBIHOZhWhj/5zrJqh8nvLFq7hGsxEGpkIEU0LTKQfYSBsukaGEiSQWIeCZNB9JEp040W+VE4mcvqWCKYS3lfUUs49qOebpiFQRA1cVssVvNAMdmwLW5ilfSKtc9Ndg3e0cW4qTlX2jIA0B0AilOcyuXUOQCa06gtA8C9AyD1R28NAM0Z1pYBAO8ASC/8CgBo1wBw971R6QI+6arWoqkHESdTMRFzP/pTiIU2yA+eJFv9zoO/SkRDbsV6cDpKxuVvXHwwGXeys3meOeqypBcJCkefKCik1q0Fha3lKt+V2n5/Cr3N80dOPICdpqWgZQZsxIwKXYjrWt8bFbKDmUwJzy7zUnWZzIu4BM7dJZAthNyYS5APfHkAkDsAFBlUg4LOAdD8JmXLAMB3AKhLxb41ADQnBs/zQod9rzcalF/DOOhm/jIWJpXf2N3OLYz3/c1LhH35j+NZ1HdtlzPH8VGXM+vYVdCHarJwngP6Huh5wLOBjNV6EHguYBZgA+AhQNOCycnjInbanToZXzGG8f5BfAyT6epFjUk9wCjwGKAEMDkLVZOq6RxAKWCoiY31KoYFp8en0AK0a+8FORnrA+apQs8GdNjKJr8OR0dXL/crNSA9EbXfEWCuXotUvlIFAT1c0glRBUZSWwxAv5d2HqUFojrkx/rmOCzyX3jU98c/J6m8OrlUjT7D0Nb1UijeTz8theLQZEIb7zNhQXtnUqGs7v6SkoVHuz/2YO8/ \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\206.png" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\206.png" new file mode 100644 index 00000000000..3ad5782c8bd Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\206.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\2062.drawio" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\2062.drawio" new file mode 100644 index 00000000000..31e3097a914 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\2062.drawio" @@ -0,0 +1 @@ +5ZhNc5swEIZ/jY/uYLDBHGPsuIdmpmMfmhxlkEGtYKkQBvLrK1niq46bJm0Zpj6hfSWtpH12NSMmlheXW4bS6AECTCemEZQTaz0xzdnMsMVHKpVSXNdRQshIoAe1wp48Yy0aWs1JgLPeQA5AOUn7og9Jgn3e0xBjUPSHHYH2V01RiC+EvY/opfqFBDxS6tJ0Wv0jJmFUrzyzXdUTo3qwPkkWoQCKjmRtJpbHALhqxaWHqQxeHRc17/5Kb7MxhhP+OxN22UPg7CLH2z06MV1Pt0bxPNVeTojm+sAT06bC3+ogt8wrHQf7ey73uTpCwqfZmdKdGGAu0rLtFK1Qfu9qH2Izh1rUQWg8mmJ3AqIwVkVEON6nyJc9hcgjoUU8psKaiSbKUkX2SEocyE0QSj2gwM6OrOMR274v9Iwz+IY7PYHjHgyjWfyEGcfl1fDNGigimzHEmLNKDNET7DojdSLPa7vopIWWok5G1BrSiRg2nltWoqFxvQGdeRVdlqLk/fQ6CaAE72WcjaaW+z8oO+7IKFtDUV7dEOW5NTLK86Eor2+IsjUfGeXFUJS3N0R56bjjomwPRXlzQ5QX5shq2bmMaCAeDtpMIJGhZZAngQzaWoYBGI8ghATRTwCpDu9XzHmlnz0o59APvogOqx7l/A+L2nzq9q1L7VxZlbbey0ie4deExJEhZz5+/U3BEQsxf23cJXGGKeLk1N/HX+e3HKpK72+oSh1jZFXqDlOlJeGdIhXWU12Vot2WqDT+fYX+QUXpqZ+BCI/ty/enu7e5i2sX6kbQs7p/IN7qSF0ZF47O6JvzvJQNwmx/pKjh7e8oa/MD \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\2062.png" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\2062.png" new file mode 100644 index 00000000000..fe6956b9d09 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\346\240\221/\344\270\255\345\272\217\351\201\215\345\216\2062.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\345\205\210\345\272\217\351\201\215\345\216\206.drawio" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\205\210\345\272\217\351\201\215\345\216\206.drawio" new file mode 100644 index 00000000000..b92e5c29cc9 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\205\210\345\272\217\351\201\215\345\216\206.drawio" @@ -0,0 +1 @@ +7Vtbk6I4FP41eZwuIDfyKIq9D7tVU9u1tTOPKGllGo2L2Or8+k0kIEFQ2xvOtOWDyUnI5ZwvJ985IoDdyeo5CWbjv0TIY+BY4QrAHnAc27aI/FKSdSZhjGaCURKFutNW8BL95FpoaekiCvnc6JgKEafRzBQOxXTKh6khC5JELM1uryI2Z50FI74jeBkG8a703yhMx5nUdehW/gePRuN8ZpuwrGUS5J31TubjIBTLkgj6AHYTIdKsNFl1eayUl+sle67f0FosLOHT9JgHEuvvH2+rZPbTI6O56/T+WTrPX/Qo70G80BsGDonleN5ALTldaz2Q/xZqnd6rmKZf5hsrdWQHB89W20ZZGqnvTj6GXMwgF2olFCM6cnXSiLLiLcdRyl9mwVC1LCWOpGycTmJZs2UxmM8yy75GKx6qRURx3BWxSDYDwddXToZDKZ+niXjjpZaQsoFlFZO/8yTlq0b12YVRJJq5mPA0Wcsu+gGCtB01kCHW9WUJFlo0LiEilwUaiKNi5K2tZEGb6wOmc2pMV1XxNOyoMyBrUzHlplrlvpP1t3Lle7nSU/u2itpa17I5eLhzaip6lOsQi2TI92wA6uMcJCOeHsLorl1Kesc1es9lCY+DNHo3l1tnDD3DVxHJjWzNblPD7IRU7JltUz9VPn6VgaiFnxwXOdR2EEUkX4keFkHXbIXmLJmSdmbZAKfQyelYgo1uYD4Lpqd7gpIzyQTdetdQyLLpfg+PQZnpMVDbHgPdysreJ7Iytu/MyvhWVu59IisjaFoZw5atTG5l5edPZGWX3pmV6bkcbxWl30rl7zmnk+UtwVOVnN8VrLCo3JgVol+BFeIKUAg9lRVC10Qcdm5K/NxzAVYbRFgH4JLD0irB0t4LyxYAhtsEGKneN/hEgBHi7gs7cKthBzsTfRdEBTkSFbBNVFBoPxEIMUQulhe4bRoTI/uJ2YwhV7YgYtETXRIiexCDkVXXeiPA5Fz7+rTH/0S0B7M7oz12XVryXN6z/4Kp5z2HLrI2eI9Vb8sbER/XuDBsZl4n1H6iNqaSSGOJIrcy/NF3ll07TD4Jql3CrTyQ0+iBlKO5oAeyD3igbLoGDySdQWoeCdOD6CNTdjdaFMTRaCqrQ4lgLuWeci3RMIg7umEShWHc5NsSsZiGypP1LpV2typpd7Lrm1AN3p2r+abmXOmFAeA8AKCMS0wAoNYB0JxGvTAA8AMAGz56bwBozrBeGADwAQDl8qv0FLUNALLLRiUFfNFVrUVTDyJJx2IkpkH8pxAzbZAfPE3X+p2HYJGKhtyK9YRbSsblb1ycmIw7mmyeZ466LOlVgsL+JwoKqX1vQeHFcpUfSm1/PIV+yfNHjzyAraalmBmvUTMoxBTWtX40KHT3JjIlOttMS9UlMq/CCMiDEcgW4t4ZI8gHvj4A6AMAyhmgewNA84uUFwYAegAAlH4PuxsANOcFzyOhHa/nd/vltzD2sszfxsKk+hN76xaGu3TzGlFf/tt4FvTdmnHmOD7IOLOObcV8Tk0SzsfA80HHBz4CMlTr2MAngFmAdYHvAHdTMH3ysAidtqdOhleMQbh7EJ+jdLwYqDFdHzAX+Ay4FDA5i6smVdNh4LqAOU3eWK+iV/j05Bi3YKPae0FOxjzAfFXoIOD2LrLJr73+wdXL/UoNSCai9tsHjOi1SOUrVVDQgSWdUFVgdGOLLvA6m879TYGqDjmVuzsfFgcDHnvB8G20kVcnl6rRZ9hGul6KxL3N50KReOVdkCIfVvKEhds70xXK6vYfKVl4tP1fD/T/Bw== \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\345\205\210\345\272\217\351\201\215\345\216\206.png" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\205\210\345\272\217\351\201\215\345\216\206.png" new file mode 100644 index 00000000000..5c80cedf7d6 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\205\210\345\272\217\351\201\215\345\216\206.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\345\220\216\345\272\217\351\201\215\345\216\206.drawio" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\220\216\345\272\217\351\201\215\345\216\206.drawio" new file mode 100644 index 00000000000..c324e801447 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\220\216\345\272\217\351\201\215\345\216\206.drawio" @@ -0,0 +1 @@ +7VvLluI2EP0aLacPlqzXEoPpLJKcySHJPHZurAZnDCLGdEN/fSQsGwtsoHmZTHNYIJVkPaqurqoKA1BnvHhMgunoNxmKGMBWuACoCyB0nBZRX1qyzCSc00wwTKLQdFoL+tGbMMKWkc6jUMysjqmUcRpNbeFATiZikFqyIEnkq93tWcb2rNNgKLYE/UEQb0u/RGE6yqQM0rX8FxENR/nMDuFZyzjIO5udzEZBKF9LIuQD1EmkTLPSeNERsVZerpfsuV5Na7GwREzSQx6I2vhvtYG/fv/jz7cvuP+9P38bfTKjvATx3GwYQBKr8bwnveR0afRA/p3rdXrPcpJ+mq2s1FYdIJ4u1o2qNNTf7XwMtZinXGiUUIwI1eqUEVXFex1FqehPg4FueVU4UrJROo5VzVHFYDbNLPscLUSoFxHFcUfGMlkNhJ6fBRkMlHyWJvKHKLWElD+1WsXkLyJJxaJWfU5hFIVmIcciTZaqi3mA5HY0QIbU1F9LsDCiUQkRuSwwQBwWI69tpQrGXO8wHaww3aaKJ2FbnwFVm8iJsNWq9p0sv5Yr38qVrt53q6gtTS2bQ4Rbp2ZDj2odcp4MxI4NIHOcg2Qo0n0Y3bZLSe+4Qu+5LBFxkEYv9nKrjGFm+CwjtZHC7DjnK2N2vGnPbJvmqfLx2xiIMPwAmQupA13qknwlZliEmd2K7FkyJW3NsgJOoZPjsYRqaWA2DSbHM0GJTDJBp5oaClk23c/BGJTajIGaZgz3Wlb2PpCVXX5jVsbXsnL3I1nZsa3s4oatTK5l5ccPZGWGb8zK9FQfbxGlX0vlb7lPp8prB09Xcv+u8AqLypW9Qvd/4RVuAAXzI71C6jAbcRRe1fFjpwKsMoho7YFLDstWCZbOTlg2ADDcJMDI5n1Djw07XLYr7KCNhh38RPSdERXkQFSgJlFBHeeBIISRy7C6wB3bmC5xHrjDuctUi0ta9EhKgmQHYtS4TSImPxaX93v8D+T3YHpjfo9TlZc81fHZfcNUOz77brImHJ9WtS2v5PkQ68ZwuH2fcOeBOphCRjHEiG0MfygF4eph8kmql3AtBoK1DKSJ5owMRPcwUDZdDQMpMkjtI2EziDkyZboxoiCOhhNVHSgECyX3NLVEgyBum4ZxFIZxHbclcj4JNZN1z8VNzOamgnRKeHcr8A4vxk31ydIzAwDdAaCN69oAQI0DoD6PemYAkDsAVg7prQGgPsV6ZgA4dwCAUkp9HYM0DACy7Y0qF7BvqkaLth5kko7kUE6C+Fcpp8Yg/4g0XZqXHoJ5KmuSK60H3FA2Ln/l4shs3MHO5mnmqEqTXiQo7H2goJDwWwsKz5asfFdu+/059HOeP3rgAWw0L8XseI3aQaHLkdXKjkxMsZ2pTAXPRhNTVbnMi/gE7t0n0OxEbswnyAe+PADwHQCaDeCtAaD+XcozAwDeAaDvLnRrAKjPDJ7mhnZ9r/fYKb9Vu9PP/GksTDZ/ZW/cwmjb4bxE3Jf/PJ6Ffdf2OXMc7/U5s45NRX2wIg3nY+D5oO0D3wUqWGs7wCdAxTG8A3wI2Kpgc/KgCJ7Wp04FWJwjtH0QH6N0NH/SYzIfcAZ8DpQzy9UsTE+qp8OAMcBhHRubVXQLTk8OoQXHrbwX1GTcA9zXhbYLWPcsm/zc7e1dvdqv0oBHV/vtAU7MWpTytSooaKOSTqgucLqyRQd47VXn3qpAdYc8Yrw5DouDJxF7weDHcCXfnFypxpxhxzX1UizurT6XicUR3GbCgvZOpEJVXf8pJQuP1n/tQf5/ \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\345\220\216\345\272\217\351\201\215\345\216\206.png" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\220\216\345\272\217\351\201\215\345\216\206.png" new file mode 100644 index 00000000000..87bf512ec50 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\220\216\345\272\217\351\201\215\345\216\206.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221.drawio" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221.drawio" new file mode 100644 index 00000000000..107a75dd043 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221.drawio" @@ -0,0 +1 @@ +5VpLc+I4EP41OoaynpaOPJzsZatSNYedOW0ZWwHvGMwYM8D8+m3bsrGMCQwhhAyVQ9QtoUf3p+5PDYgOZ5un1F9M/05CHSPihBtER4gQjB0B/3LNttRIRUrFJI1CM2in+BL90kbpGO0qCvXSGpglSZxFC1sZJPO5DjJL56dpsraHvSSxverCn+g9xZfAj/e1/0RhNjWnIO5O/5eOJtNqZSxU2TPzq8HmJMupHybrhop6iA7TJMnK1mwz1HFuvMou5eceD/TWG0v1PDvlA5GOgn9/JWv68PTfdumwH6Px8sHM8tOPV+bAiIgY5huMoTHJG/Dfny2gMR8vF6VcDHhJYF04VrY1thI/VknV8bAsPNmHAYQvNrvOalanmgY2XM5Ur1apGzsg1kIEDgb+B2GwnkaZ/rLwg7xnDRAE3TSbxSBhaPrLRQmKl2ijw3xvURwPkzhJi4noy4sWQQD6ZZYm33WjJ3TV2HHqxZtmrmym00xvGipj9iedzHSWbmGI6aXCQMDcAVLJ6waijGraAFOl8w2GJ/XMOzdDw3j6N7xOOrzeNvE87OfXB6R5Mte2WcEU6fZrU/jWFEb5uZ1a2hrpgrZfJqs00K+ckJpQ4acTnR3Hvw6tKLDvyYaneIenKl2qYz+Lftqxo8t9ZoXnJCouUQUUZgOF8xYCynObTzXvensi1ZrIaU1UGmZvogJN9bHPBxg9KaxcIIyQOwojTArLqZR/cBhhB728XPjz8526hxXc7c5aVy73Z3iZUOe2vMyvdZfpHd1lzG0vM/rBXhbX8jK/Iy8zzG7Ly+5bid8myr422t8qogftHevLhYr01VSxFm6NKrI/gyqKM6kid6SNUUauShXlWyHZ+RZxjgCsArLTADJ+Fci3CEl+W5Bs57SzXy9S9ohkxMWEuUwQTFuMqLv7SoBVlwLszUZEcSL86E3Bj1PVE5RyyiQH8oGxDUasegorxST0MOG450GTMfsNxtl1H9ZVWfT931zsjt5cFN8YG8ddddm3ErXX81t3WDqWR28xK1bOvJG4RB0rXWFlJzMX91zMXSJdDiFMtqY/OWXSzmmqRVjnFq4Vs8jBmHXht6W4o7clF7cWs7qKvm9n8pcnRjX372GXNx+yPaeWD8RIEJ51GoHBdGp058ez6l58Mp7FlOwpIYjCDgOqJbkNQ+H0uMvyb0g5kVxwcV5A49QmWuzaROtwcfvCQcu9o6DV/lrqo2vb+HBx+8J0Wt4Rna5/8tCuTX2Ym7uq2+e92Xv805SZjhNl1plYTkhd/CMTEJbwlFc113WlXWTnGBK50ygPnZmBsBA9Lg5WoTilPUZ3u7gyp36PSv7VkLkHuONQ5Z8BmZRdBZntcsVe7f+dwSc6wOdxNPBQ30MeQ4NH1MfIEwjSvRoijyBZNGy6FNSI2SVPQJVSlO7n06com67G+ZzSQ0oiTyHpIgWryHzRfDmOpESKHGJTZhejOjOnp2R3zDqzOyymBkh5eaPPkBxd5JDPo8eju4fzggWANObnfURKmL2A8XNTuKhPGzZx84ZyC18M0aBfDH4sGm4+oMq4jagBOT6zQ4V9wU0oaUYDo/LjaDIHMYDrnj+PBjljiAI/7puOWRSG8SEWkyareZhzliLYxP5YxwM/+D4p9O3FwTTmp6KQw0q5EYAGxd+FSoPuccpa851mSCG/T2ZA3P0ctLysux/VUu9/ \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221.png" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221.png" new file mode 100644 index 00000000000..bc0fe0dce14 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.drawio" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.drawio" new file mode 100644 index 00000000000..11133b532ad --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.drawio" @@ -0,0 +1 @@ +7VnJdpswFP0aL+OjiWmZOEm7SIdzsmjSHQbZcAqIynKM+/UVRmIysR2SYKfuCr0r8TTcq6eHGOFJnH3ibhp8YT6NRgj42QhfjxCCEJjykSPrAnEcqwDmPPRVowq4D/9QBQKFLkOfLhoNBWORCNMm6LEkoZ5oYC7nbNVsNmNRs9fUndMt4N5zo230R+iLoEBtZFX4ZxrOA90zNJ2iJnZ1YzWTReD6bFWD8M0ITzhjoijF2YRG+eLpdSneu32mthwYp4k45IU7eumBOPrJb52v09m3AKzusgvl5cmNlmrCI2RG0t/VVBbmeUE+3TiVhWS6SAt702DGZL9yWmKt1sr8vWS64mKxYfJSNkBGmlWV2ivQbuSAC09lbxqujQA1OkJyYpJ/aVytglDQ+9T18pqVlKDEAhFH0oKy6C7SQhSzMKN+PrYwiiYsYnzjCM9m1PQ8iS8EZ79orca3nCkAZedPlAuaPbvysORTbgTKYir4WjZRL2BTSUDtAaTtVU1RCgpqYtKYqzQ8Lz1XNMuCYvoFrKMO1ttLnPiX+faRVsIS2lxWOW++fqgbj3XjOp83KK21svquPfW3tmhr5eXI2ZJ7dMeUsYodLp9TsW9DbDNZY8roYEpjnEauCJ+aw+2iT/XwnYWbTaSFQppCMYyWAoppqrfqe73tyGk5Ai1HxTpsOdqoqZx2f4Hhg8LKG4QRdEZhhLTUUarlWGGEPMvyInWT/qRuaQV201liRXf/BsslXafCsjHUXsZntJcRaLJM0JFZNodi2Tgjlg10Yixbr038slA81MqPOtGT5Srryw2d9JWpYmmcfKpIPmSqaPZMFQ1gNzVK0KCpov1aSXZ+i4A9AtNCBjUhw51C/hCSNI4pSdI+00hPSRJsj5FNkAURsYiJIG5lRN3VAwnWeRfBnnRENA+UHz6q/Bw8NjE2pDzkuYvKm0etRuCMHeg4xEYQEhNYPbXZErmBh/2y1vei7//RRc7oo6t9Q3f0RA12Xcy+NlPbfcB1Z2r7DtIPcSxC0M3+QKma0TivoNM8zUw4tqBhIdsyZDSxW+4PvvGzO93oTnDnEIaKWWiomGWeUcxqXxSVd7xHi1ldt77/Y1b3ddr+mIVON2YRuDOcvE3MImRnYOwds6RZ/XQtmle/rvHNXw== \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.png" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.png" new file mode 100644 index 00000000000..673f3e32beb Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\346\240\221/\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\346\226\234\346\240\221.drawio" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\346\226\234\346\240\221.drawio" new file mode 100644 index 00000000000..6eeaa610144 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\346\240\221/\346\226\234\346\240\221.drawio" @@ -0,0 +1 @@ +5VhNc9owEP01HMnY+rCtY0LSdibNTKc5NOnN2AJ7KixXiGD667vGsi0bSGlKcTI5sfskrbT73grBCE8WxUcV5smdjLkYIScuRvh6hJDrOh58lMimQhjzK2Cu0thMaoH79Bc3oGPQVRrzZWeillLoNO+CkcwyHukOFiol191pMym6u+bhnO8A91EodtFvaayTCg2Q3+KfeDpP6p1dj1Uji7CebDJZJmEs1xaEb0Z4oqTUlbUoJlyUxavrUq37cGC0OZjimT5mwfT2s7i+U1/V7ePDOLv8TokYj02Up1CsTMIj5AmIdzUFY14a8BkucjCy6TKv/O2EmYR9IS29MbXyfq5kPTBebpm8hAmI5kU7WEd16jBw4CpSs1sNWydAnY0QJAb8g3O1TlLN7/MwKkfWIEHAEr0Q4Llghsu8EsUsLXhcni0VYiKFVNtAeDbjXhQBvtRK/uDWSOyzqeM0mz9xpXlxsPJuwyc0ApcLrtUGppgFxDcSMD2AqPHXlqIMlFhiqrHQaHjeRG5pBsMw/Resoz2s90ucxZdl+4CXyYx3ywp5q82D7TzaznWZt9N4G+O9tPY83mnRXuXh5HKlIv5MytjcHaGac/2nhthl0mKK7mGqxhQXoU6fusfdR5/Z4YtMt01UC4V2hUJxTwFVmmaV3eu9QNTpBiJ9KVV12Am0VVOT9ssFho+6Vk5wjbjv6BqhPXVgPPA1Qg6y/G+k7mgF7afzAPNvm2UP91rXHZhleq5exu+ol323xzIbmGXvXCyT98Qy632f+wOz7A/y8ONFqh8s+9Gy2yWl8yqeiuTIpyJ+VU9FcqqnIjvvUzE4lSQdW5LOkZJ0LUm6b1+SdEhJUoYvPIwpJgGF2991u69Vn10wlzESwAjxHP+FciXBBQoIfIHCz2riod4mqDeKz6plNsj1OqgyvbegTM95VpnBaZRJn1Um/k/KBLf9D7Ga3v4Ti29+Aw== \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\346\226\234\346\240\221.png" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\346\226\234\346\240\221.png" new file mode 100644 index 00000000000..af12915808e Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\346\240\221/\346\226\234\346\240\221.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\346\273\241\344\272\214\345\217\211\346\240\221.drawio" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\346\273\241\344\272\214\345\217\211\346\240\221.drawio" new file mode 100644 index 00000000000..8379028d2a9 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\346\240\221/\346\273\241\344\272\214\345\217\211\346\240\221.drawio" @@ -0,0 +1 @@ +5VnLcpswFP0aL+PRG1gmTpq2M5nJTBZNuiNGNkwxogLHOF9fYSSMME5c2/WjXqF7JK4e5+jqCnp4MCnupZ+GDyLgcQ+BoOjh2x5CEAKmHiUyrxDPcypgLKNAN1oCT9E71yDQ6DQKeGY1zIWI8yi1waFIEj7MLcyXUszsZiMR272m/pivAE9DP15Ff0RBHlaoi5wl/pVH49D0DJlX1Ux801jPJAv9QMwaEL7r4YEUIq9Kk2LA43LxzLpU731ZU1sPTPIk3+SFZ4cW19/kzzALJmPy8P39HlxdaS9vfjzVE+4hFit/N6+qMC4L6ulPUlVIXrO0shcNRkL1q6aVz/Vasd9TYSqusgWT16oBommxrDRegXGjBlx5qnszcGMEyOoIqYkp/pVxMwujnD+l/rCsmSkJKizMJ7GyoCr6WVqJYhQVPCjHFsXxQMRCLhzh0Yiz4VDhWS7FL96oCRzvFYC68zcuc16sXXlY86k2AhcTnsu5aqJfwExLQO8BZOxZQ1EaChtiMpivNTyuPS9pVgXN9F+wjjpYby9xElyX20dZiUi4vaxq3nL+3DRemsZtOW9QW3Ntbbv2PFjZoq2VVyMXUznkH0wZ69jhyzHPP9sQq0w2mKIdTBlM8tjPozd7uF306R4eRbTYREYoxBYKpS0FVNPUbzX3etuR13IEWo6qdVhxtFBTPe3tBYY3Cit7CCPogsIIcZlFKqZHDiNkLctZ6ifbk7qiFdhNZ41V3f0fLCMMTotleqi9jC9oL0Nqs0zwkVlmh2KZXhDLBJLTYtnZNfErovy5UX4xiZ4qL7O+0jBJX50q1sbJp4rkLFNFtmWqSIFra5Sgg6aK7q6S7LyLgE8EZoQMGkKGHwr5LCRJjyrJ9pm29e3FdfvIJciBiDiEIYhbGVF39YEE6+1LsOcTEdmG8sPHlB/FXp9hTDFxqUo+ILTFCL2+Bz2PuKqGMOBsJ01C7DsYJYe9WJvPov/+zkUu6M6F4Yll47Dru+yuidrH51t3WPrsHD2LUxGCbvYPdCwC67iCnn2YObDvQOog16EqhLkt9xsfmbjTjemEdA7hUDFr/XeiPd8t2QXdLSk7tZjV9Qlh90x+/4lRnfv3oUObF9k+qO01MVIZj1xGasG41Nge4xncNKAdNc8intv3GEMeBESlWi61ZchAnzqk/ENKkUsZZdsFNIrtRIvsLdFS5vKna9V8+esa3/0B \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\346\273\241\344\272\214\345\217\211\346\240\221.png" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\346\273\241\344\272\214\345\217\211\346\240\221.png" new file mode 100644 index 00000000000..c0f30c04c56 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\346\240\221/\346\273\241\344\272\214\345\217\211\346\240\221.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\351\223\276\345\274\217\345\255\230\345\202\250\344\272\214\345\217\211\346\240\221.drawio" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\223\276\345\274\217\345\255\230\345\202\250\344\272\214\345\217\211\346\240\221.drawio" new file mode 100644 index 00000000000..37585459182 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\223\276\345\274\217\345\255\230\345\202\250\344\272\214\345\217\211\346\240\221.drawio" @@ -0,0 +1 @@ +7Zxbb9owFMc/DY+bcg95LPQyaUya2kntHl1iiFcTZ8bc+ul3HBxIloXCgMQPFkjEx5fE/h38z3EMPXc4Wz9wlCXfWIxpz7Hidc+97TmObVsBfEjLZmuJonBrmHISq0J7wxN5x8poKeuCxHheKSgYo4JkVeOYpSkei4oNcc5W1WITRqtnzdAU1wxPY0Tr1mcSi2Rr7Tvh3v4Fk2lSnNkOom3ODBWFVU/mCYrZqmRy73rukDMmtkez9RBTOXjFuGzr3Tfk7i6M41QcU2HgfX9xfi98h44e3r/es+dF9PbJUdcmNkWHcQz9V8mUpfAx4GyRxlg2Y0GKcZGwKUsRHTGWgdEG4y8sxEbRQwvBwJSIGVW5cIV88yLrf/aL5E/VXJ64XVdSG5WaC87e8JBRxvOLcy0rCPw+5ExYKkp2fyhfYN92R/ahcZSUac4WfIwPDU1fuRviUywOFfR2NOFrgNkMQx+gIscUCbKsXglS/jjdldsjgwNF7QSCriF4NkE76JKgZwge/modQbDfJUDfADwboOt0STAwBA/PjcdMop0SDA3BswmWbks7IKjEeonoQp3pFglUwwq30Zk8FOhVmsp0VgkR+ClD+WisIB7JRx5xoXh6EgUECgKRFHNVacwoRdmc5K3lsMYJofEIbdhCFOcpUjmuwqlkbUTJNIXjMVCRTQ6WmAsC4cONypiROM6vc0IoLYF27KF145/hGvJEeH3YOeooVQWvCK9UVOb1VXq1j3HsInBJSvFNaF2JftQIGnolCKKPEN2hdHoM8zrSmLPsR+H+0pAxIoHdLWHA5spWJqQmDJHPCzKT4klR95UJwWYqwdXY7BrNB8YfwBuGaijnCR96M4S0vU/DWxbnwDcFBwB/lG1gNBcrPBfX84pT7p0K1zjSM9xreUbhqqWJYSRhXNJf8rUDtPeXs12BAYgJzcP9BGYAnLbMNPo30xLEoFWGdo3hY35aA/F0iOsqwM6YOkawWxPsMNRNsO36wpNR7JYmhqZQSxvJ9oxknw7V1UyzfaPZl6Ooi2gHRrRbE+3+X6Ltel7Xol1fJjOi3dbM0PCQSRvRri/AGdH+EGqomWhHRrQvR1ET0XbqK2BGtK8l2pF2kfbuQY8R7dZFu+mRmC6i7dQX4Yxofwi1YQbobH53jWhfjqIuol1fAzOifS3R9m3dIm2nvinMiHZbM0PDhlxtRLu+CGdE+0OovmaiHRrRvhxFXUTbbEJrT7SDqCrau1/KdCfaZhdad6Kt+Ta0omEj2qdA1Wwfmmv2oV2Qoiai7dbXwIxoX020tds57pqNaJ2JdtOvtrQRbbMR7T+garYRzTUb0S5I8eqiDcn9nxrkeaW/hnDv/gA= \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\351\223\276\345\274\217\345\255\230\345\202\250\344\272\214\345\217\211\346\240\221.png" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\223\276\345\274\217\345\255\230\345\202\250\344\272\214\345\217\211\346\240\221.png" new file mode 100644 index 00000000000..c0ce15b72e4 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\223\276\345\274\217\345\255\230\345\202\250\344\272\214\345\217\211\346\240\221.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\250.drawio" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\250.drawio" new file mode 100644 index 00000000000..1ddbb02dc6e --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\250.drawio" @@ -0,0 +1 @@ +7V1dc5s4FP01PDYDSAh49FfSh3amu+nMto8yyDYtRi6WE3t//UogMGCwiSHgjZlkJuiCJXHP0dXVMSgKmKz3TyHerL5Sl/iKrrp7BUwVXdc0FfE/wnKILbZtxoZl6LnyoqPh2fuXSKMqrTvPJdvchYxSn3mbvNGhQUAclrPhMKSv+csW1M+3usFLcmJ4drB/av3Hc9kqtlq6ebR/Jt5ylbSsITs+s8bJxfJOtivs0teMCcwUMAkpZfHRej8hvnBe4pf4c48VZ9OOhSRgdT7w9OMPePm6oQxPv88OwRr9+uvwSY9recH+Tt6woiOf1zeeiy6zg/QD+rMT/RwvaMA+bSOURvwC3djsjyf50VL8HSV18M7ME6N0QlqjznvHQeSF8evKY+R5gx1x5pXziNtWbO3zksYP8XYTI7vw9sQVnfB8f0J9GkYVgcWCIMfh9i0L6W+SOeOa9lxV08ZfSMjIvtJ9WgoKZzOha8LCA79EfsBMcJREBglDXzO0kKZVhhGJDUsiLtOaj1jxAwnXG6ADJdAVXRy4IzEGeCmgAcm7ld93ePiRLfzMFqbivtW0dJCluA3inoyagh95P+gudMiZG4ByOONwSdgljp7ikvG7UeL3xBYSHzPvJd/dMjBkC9+ox28khR0l8UrCjvQCnvFtyk9lh1+hItMyHnQL6qamQxOipCcJm2wrfxbkW4mddNJKRJzUJ9dzCVaGge0GB9dHgkwwiQ2T8tCQ2uLmPkbEsMx8xIB9RwyjK5THd4SyYd8YyqgrlKf3hLJWQNnuGWWzK5Sf7ghl27gxlK2mOd7eYz8yxz+TnI4fHxM8UUjyuzQrTAsdZ4XG/yIrLBAFgSuzQkuzchUZmt5p4mc3JVjpIkK9QJeElmqGltpZWvZAMNQnwczCfJPOP29edkDrzLIDar0uO5Kbupp+LdLCrEkL2CctLE17QAAYAFoGn8G1PJqGqj3Ymm1Di5+BSDWvjEk6OkMZQ1V7pYzWVeIzu6PEBxWXqn0nPsk02Grmc36KKc98Lk1lPUxMWgWWHaU+KDdlaHaeOEB7MDXD1C3T0A1gFaqvG4KQXVpN0oha2oWuIlCZ8PouEejzHUUgCAoJj9l3BCrTRK+JQPWT3OuS6RYjEKobgcANRyDrbHBoJwIZ2tkw994RqFrIFYGmxQikXYhAcXMVEYgHA5YfEvkIIodMNtxIE/a9ZcCLDmcw4faxCC2eg/2RPLH2XNevim0h3QWuiGTTtrIjKx+bgH4am2AJ34tfFbUXm6o13pYJoA8EEMMdFtLj3glQLf+2TAAwEEBkGfqtEaBMGX4XAsCBAALcwgLZUPsmQJly+y4EMAYCKBmF/1YIkFT8/gSwBgIop98AGyUSWbcEKFNCC97frvBGHDI89wvL0zKnbfnChclnMIXbHI4s9gLh/egzDvV9vNl6UWXxFSvPd7/gA92xpJmkVNQRXEysRamOgByLzBdtBWqUgwkh4wQmpHYpJCSw1MGJ3yfzsP83cRgOlnUgO4XEDenme7KCF4aNWIeScPbCXbiVtpLxxuhGnvTJIvnsnDJG17IQSm+llUauMsb8lztvoj4YisHvZsLL2rHMf8XlIZvQgEOPvQhIgrfslWxZXdTPDIGzOkQp1vDdsK7WBmsHZbVWUPZ4dNs3CcxNKBc9ao2PlGvMJsrRX/iRkLbiUZ0EzWih16BFl6yofryyZVaoAyPexIh9ng29EaQFQa8eQRoJevdLEK1kvd8tQ1pQ/OoxpJHid8cMsfpmSAuSYD2GNJIE75chKfS9MaQFzbAeQxpphvfLEFCiKXXLkBZExXoMaSQq3jFD+s5UQQuqYz2GoIEh1zCk7JupbhlS/YBmywwxB4ZcxZC+M1VQJoi+C0MafXNxvwwx+s5UQY132wfJvFXJ/JCHtDcFHXSmlcYtDAGiegDejoIOygTSAY4bka9BZ+Jkxc4yw3g9T5De5WvQmTg5HhhyFUN6XxR0Jk5e2oJmYMhtytegM3FyOjDkGob0Ll/DzsTJS+8GDwy5TfkadiZOPg4MuYYhvcvXsDNx8tLGSgNDblO+htXPeJ7A2IwynyvZUGN71ftlSO/yNYQnaIjXs59lUXow/4YCDdmKLmmA/S80cq1A5xdh7CCfx8c7RvPYHTeseDCUfjbrksHy2s26ar8I3gyOzva9vDTpf6QNG8zivpd9bxkDy1TIoo9r7WWmvmU0vX2LvRbHH6y7Z0yvu1bZ+b0UzPyGDQYAZWffumGDfX6jM7vXXatgZ+9rN3ps5sO8rGeifHDq/W1N2Nn72o2eivkwBLD1GyOAUaJNzQxlPFNGM2UGFZ47jDRlhhQ+jdoTZaYrVnSQJ4mTzuVHHvD53rZFSlykxpPHVru5qNOaKbalzGzFMhWbt2KJRkVzhmJZiq1X0UP2ItVD52EdomqwlKi8MXus2DNxMIKKNW3lJr9NHy/2nt8v9wAfGeJ+HxUbyb5w5wtXmMoIZHxiigPbjLCYKONRdPFjdGCKC5IE5uaGjY/nxB9j5/cyshcb566RSwkNynImNRxHP+0MPlTYLgWVDL50pDUcfbx4/Bcq8XR9/Ec0YPYf \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\250.png" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\250.png" new file mode 100644 index 00000000000..33f3c6e3ff3 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\250.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\2502.drawio" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\2502.drawio" new file mode 100644 index 00000000000..8048a1a724c --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\2502.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\2502.png" "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\2502.png" new file mode 100644 index 00000000000..70c6da26d22 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\346\240\221/\351\241\272\345\272\217\345\255\230\345\202\2502.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\215\225\351\223\276\350\241\2502.png" "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\215\225\351\223\276\350\241\2502.png" new file mode 100644 index 00000000000..9fe82753c80 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\215\225\351\223\276\350\241\2502.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\217\214\345\220\221\345\276\252\347\216\257\351\223\276\350\241\250.png" "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\217\214\345\220\221\345\276\252\347\216\257\351\223\276\350\241\250.png" new file mode 100644 index 00000000000..9d134bcbea0 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\217\214\345\220\221\345\276\252\347\216\257\351\223\276\350\241\250.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\217\214\345\220\221\351\223\276\350\241\250.png" "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\217\214\345\220\221\351\223\276\350\241\250.png" new file mode 100644 index 00000000000..ffb3b3efa0e Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\217\214\345\220\221\351\223\276\350\241\250.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\276\252\347\216\257\351\230\237\345\210\227-\345\240\206\346\273\241.png" "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\276\252\347\216\257\351\230\237\345\210\227-\345\240\206\346\273\241.png" new file mode 100644 index 00000000000..225c46d7797 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\345\276\252\347\216\257\351\230\237\345\210\227-\345\240\206\346\273\241.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\225\260\347\273\204.png" "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\225\260\347\273\204.png" new file mode 100644 index 00000000000..923e8c042d8 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\225\260\347\273\204.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\240\210.png" "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\240\210.png" new file mode 100644 index 00000000000..2d704d564c6 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\240\210.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\240\210\345\256\236\347\216\260\346\265\217\350\247\210\345\231\250\345\200\222\351\200\200\345\222\214\345\211\215\350\277\233.drawio" "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\240\210\345\256\236\347\216\260\346\265\217\350\247\210\345\231\250\345\200\222\351\200\200\345\222\214\345\211\215\350\277\233.drawio" new file mode 100644 index 00000000000..25a0512ed99 --- /dev/null +++ "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\240\210\345\256\236\347\216\260\346\265\217\350\247\210\345\231\250\345\200\222\351\200\200\345\222\214\345\211\215\350\277\233.drawio" @@ -0,0 +1 @@ +7Vpbc6M2FP41PCbDHfNobCedTne60+xMm77JIGN1BaJCvu2vrwTiToLt4Mt0sw8bdJCOpO87+o4koxizaP9MQbL+QgKIFV0N9ooxV3Rd01Sb/xGWQ25xVTU3hBQFslJleEE/oDQW1TYogGmjIiMEM5Q0jT6JY+izhg1QSnbNaiuCm70mIIQdw4sPcNf6JwrYOrdOdKey/wJRuC561mw3fxOBorKcSboGAdnVTMZCMWaUEJY/RfsZxAK8Ape83dMbb8uBURizYxq8vk42//y9/DZPbY8eovTLD8d6kF62AG/khOVg2aFAYLdGDL4kwBflHWdZMbw1izAvafwRpEmO+wrtIe/KWyGMZwQTmjU3VquV7vvcnjJKvsPam8Be2pbN38gxQMrg/s3JaSVkPNYgiSCjB15FNtAdibIMM82W5V1FWhlT6xphpRHIQAlL3xWW/EHCeQK0+jC0MA6mIkZ5KSYxbAJ7Lo4w6ET0IIo1lKwekAobhRgwtG267wNO9vCVIN7xMEmFi5RsqA9lq3ooDzhyW34YoCFkHT8ZjeWsz2fW+GS2RYjpuI/WONyak5tya94tt3wMGYRDan6rGDBai1JXR1rdHUdvhAAnBRxq1RJRIX0naCf9/VQRlXscNb6su42vm2lHmwZzrLhpO7pU3JhXiBv7M24GaDCMkeKm4+hCcVP0c9G4cT7jZmj5tncYZ+vNkVuVD+uNe4W4mQzHDSWbOBBHvrnKaR84Jt7kVGg2gStwHDoTtvPGaEdC9/8AqqU1UXVvjWrR2ZEa52OQpshvItlBTM3+8TdCyF6kI0LZmoQkBnhRWT1/Q7cZY8IRB5Ee/hL0PRpuUX7l5Qf1UXWswjLfS4bz0qFe+gop4shAKo3HCqqu5oLzHlTmLZW39FOcEc89IRhtRxfK2O0BW+rAsPR3ql9Gp7XuDV6ySdedBcDXO3sv5GXmryuKNAGMwlisGx5tIiY9oR7IB3gqX0QoCLKF0KdWTT1bkZjJO17dGunmz25lx64cOT0h3Y6Y8dRI7xJCkp+HD1Mz7ouP027rTswObQDPzhb1VKE+DuWJEVOCe8uMYKoDQn70raHVv9UbeyuunpQQ2lulqySEI24w73+T2fmpoEdGrrzL7Lm5W9jKVFUmE2XhKlNN8Wyx4M4QewGe4y5zRWnDDe0M7juXffuNhVzjSzOvqvs9N2YVX5biTRWXU6ouCWMk+qSsuva6HWWnXVZ9MFUfm0Nvdlpq5RrDNIvf2D58w2m2HF34RzXtiNukn5dYrc3GubR2HF2a1r77LBsL3QzQtkGv/e9GfOfiCVl9kLo45TUwXLHqLX8K5d/MS5qAuNeNoPwhzTgXXjQr2fd4WZiKN1fcmbKYKHxDOLGUxZPizRR3+hIDhGcYRUteGURCbuNlmtS65oDkvTdHxM3Z1JpWMZwPjPMCmBUIWGLW01kGBZ+4k1k4CE457/z/Z8TWm2WJz69gC57Fl1dnwcGtOSKFeTC3jpkpy++shONQSIl89kkkRCXf1NY0QzQi3CNiYh25oojBEmIP+N/DzHf7UM4b9wlR3orQANKeFk8gQlj08AwoiEgcFMOQwKiS6t/LoThqzqeP4vAb31Yac6My/CaioGHx8p1Mw/aHVC9jpLvf1l3LBXflvFh9G5crVvWFobH4Dw== \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\240\210\345\256\236\347\216\260\346\265\217\350\247\210\345\231\250\345\200\222\351\200\200\345\222\214\345\211\215\350\277\233.png" "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\240\210\345\256\236\347\216\260\346\265\217\350\247\210\345\231\250\345\200\222\351\200\200\345\222\214\345\211\215\350\277\233.png" new file mode 100644 index 00000000000..07bdb36a2c6 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\346\240\210\345\256\236\347\216\260\346\265\217\350\247\210\345\231\250\345\200\222\351\200\200\345\222\214\345\211\215\350\277\233.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\351\230\237\345\210\227.png" "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\351\230\237\345\210\227.png" new file mode 100644 index 00000000000..2fa16dcdb3a Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\351\230\237\345\210\227.png" differ diff --git "a/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\351\241\272\345\272\217\351\230\237\345\210\227\345\201\207\346\272\242\345\207\272.png" "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\351\241\272\345\272\217\351\230\237\345\210\227\345\201\207\346\272\242\345\207\272.png" new file mode 100644 index 00000000000..f3b01def266 Binary files /dev/null and "b/docs/cs-basics/data-structure/pictures/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204/\351\241\272\345\272\217\351\230\237\345\210\227\345\201\207\346\272\242\345\207\272.png" differ diff --git "a/docs/cs-basics/data-structure/\345\233\276.md" "b/docs/cs-basics/data-structure/\345\233\276.md" new file mode 100644 index 00000000000..cb9f55c1f88 --- /dev/null +++ "b/docs/cs-basics/data-structure/\345\233\276.md" @@ -0,0 +1,159 @@ +--- +category: 计算机基础 +tag: + - 数据结构 +--- + +# 图 + +> 开头还是求点赞,求转发!原创优质公众号,希望大家能让更多人看到我们的文章。 +> +> 图片都是我们手绘的,可以说非常用心了! + +图是一种较为复杂的非线性结构。 **为啥说其较为复杂呢?** + +根据前面的内容,我们知道: + +- 线性数据结构的元素满足唯一的线性关系,每个元素(除第一个和最后一个外)只有一个直接前趋和一个直接后继。 +- 树形数据结构的元素之间有着明显的层次关系。 + +但是,图形结构的元素之间的关系是任意的。 + +**何为图呢?** 简单来说,图就是由顶点的有穷非空集合和顶点之间的边组成的集合。通常表示为:**G(V,E)**,其中,G表示一个图,V表示顶点的集合,E表示边的集合。 + +下图所展示的就是图这种数据结构,并且还是一张有向图。 + +![图](pictures/图/图.png) + +图在我们日常生活中的例子很多!比如我们在社交软件上好友关系就可以用图来表示。 + +## 图的基本概念 + +### 顶点 +图中的数据元素,我们称之为顶点,图至少有一个顶点(非空有穷集合) + +对应到好友关系图,每一个用户就代表一个顶点。 + +### 边 +顶点之间的关系用边表示。 + +对应到好友关系图,两个用户是好友的话,那两者之间就存在一条边。 + +### 度 +度表示一个顶点包含多少条边,在有向图中,还分为出度和入度,出度表示从该顶点出去的边的条数,入度表示进入该顶点的边的条数。 + +对应到好友关系图,度就代表了某个人的好友数量。 + +### 无向图和有向图 +边表示的是顶点之间的关系,有的关系是双向的,比如同学关系,A是B的同学,那么B也肯定是A的同学,那么在表示A和B的关系时,就不用关注方向,用不带箭头的边表示,这样的图就是无向图。 + +有的关系是有方向的,比如父子关系,师生关系,微博的关注关系,A是B的爸爸,但B肯定不是A的爸爸,A关注B,B不一定关注A。在这种情况下,我们就用带箭头的边表示二者的关系,这样的图就是有向图。 + +### 无权图和带权图 + +对于一个关系,如果我们只关心关系的有无,而不关心关系有多强,那么就可以用无权图表示二者的关系。 + +对于一个关系,如果我们既关心关系的有无,也关心关系的强度,比如描述地图上两个城市的关系,需要用到距离,那么就用带权图来表示,带权图中的每一条边一个数值表示权值,代表关系的强度。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/1*FvCzzcpYVwyB759QKoDCOQ.png) + +## 图的存储 +### 邻接矩阵存储 +邻接矩阵将图用二维矩阵存储,是一种较为直观的表示方式。 + +如果第i个顶点和第j个顶点之间有关系,且关系权值为n,则 `A[i][j]=n` 。 + +在无向图中,我们只关心关系的有无,所以当顶点i和顶点j有关系时,`A[i][j]`=1,当顶点i和顶点j没有关系时,`A[i][j]`=0。如下图所示: + +![无向图的邻接矩阵存储](pictures/图/无向图的邻接矩阵存储.png) + +值得注意的是:**无向图的邻接矩阵是一个对称矩阵,因为在无向图中,顶点i和顶点j有关系,则顶点j和顶点i必有关系。** + +![有向图的邻接矩阵存储](pictures/图/有向图的邻接矩阵存储.png) + +邻接矩阵存储的方式优点是简单直接(直接使用一个二维数组即可),并且,在获取两个定点之间的关系的时候也非常高效(直接获取指定位置的数组元素的值即可)。但是,这种存储方式的缺点也比较明显,那就是比较浪费空间, + +### 邻接表存储 + +针对上面邻接矩阵比较浪费内存空间的问题,诞生了图的另外一种存储方法—**邻接表** 。 + +邻接链表使用一个链表来存储某个顶点的所有后继相邻顶点。对于图中每个顶点Vi,把所有邻接于Vi的顶点Vj链成一个单链表,这个单链表称为顶点Vi的 **邻接表**。如下图所示: + + + +![无向图的邻接表存储](pictures/图/无向图的邻接表存储.png) + + + +![有向图的邻接表存储](pictures/图/有向图的邻接表存储.png) + +大家可以数一数邻接表中所存储的元素的个数以及图中边的条数,你会发现: + +- 在无向图中,邻接表元素个数等于边的条数的两倍,如左图所示的无向图中,边的条数为7,邻接表存储的元素个数为14。 +- 在有向图中,邻接表元素个数等于边的条数,如右图所示的有向图中,边的条数为8,邻接表存储的元素个数为8。 + +## 图的搜索 +### 广度优先搜索 +广度优先搜索就像水面上的波纹一样一层一层向外扩展,如下图所示: + +![广度优先搜索图示](pictures/图/广度优先搜索图示.png) + +**广度优先搜索的具体实现方式用到了之前所学过的线性数据结构——队列** 。具体过程如下图所示: + +**第1步:** + +![广度优先搜索1](pictures/图/广度优先搜索1.png) + +**第2步:** + +![广度优先搜索2](pictures/图/广度优先搜索2.png) + +**第3步:** + +![广度优先搜索3](pictures/图/广度优先搜索3.png) + +**第4步:** + +![广度优先搜索4](pictures/图/广度优先搜索4.png) + +**第5步:** + +![广度优先搜索5](pictures/图/广度优先搜索5.png) + +**第6步:** + +![广度优先搜索6](pictures/图/广度优先搜索6.png) + +### 深度优先搜索 + +深度优先搜索就是“一条路走到黑”,从源顶点开始,一直走到没有后继节点,才回溯到上一顶点,然后继续“一条路走到黑”,如下图所示: + +![深度优先搜索图示](pictures/图/深度优先搜索图示.png) + + +**和广度优先搜索类似,深度优先搜索的具体实现用到了另一种线性数据结构——栈** 。具体过程如下图所示: + +**第1步:** + +![深度优先搜索1](pictures/图/深度优先搜索1.png) + +**第2步:** + +![深度优先搜索1](pictures/图/深度优先搜索2.png) + +**第3步:** + +![深度优先搜索1](pictures/图/深度优先搜索3.png) + +**第4步:** + +![深度优先搜索1](pictures/图/深度优先搜索4.png) + +**第5步:** + +![深度优先搜索1](pictures/图/深度优先搜索5.png) + +**第6步:** + +![深度优先搜索1](pictures/图/深度优先搜索6.png) + diff --git "a/docs/cs-basics/data-structure/\345\240\206.md" "b/docs/cs-basics/data-structure/\345\240\206.md" new file mode 100644 index 00000000000..f86308fafe4 --- /dev/null +++ "b/docs/cs-basics/data-structure/\345\240\206.md" @@ -0,0 +1,198 @@ +--- +category: 计算机基础 +tag: + - 数据结构 +--- + +# 堆 + +## 什么是堆 + +堆是一种满足以下条件的树: + +堆中的每一个节点值都大于等于(或小于等于)子树中所有节点的值。或者说,任意一个节点的值都大于等于(或小于等于)所有子节点的值。 + +> 大家可以把堆(最大堆)理解为一个公司,这个公司很公平,谁能力强谁就当老大,不存在弱的人当老大,老大手底下的人一定不会比他强。这样有助于理解后续堆的操作。 + +**!!!特别提示:** + +- 很多博客说堆是完全二叉树,其实并非如此,**堆不一定是完全二叉树**,只是为了方便存储和索引,我们通常用完全二叉树的形式来表示堆,事实上,广为人知的斐波那契堆和二项堆就不是完全二叉树,它们甚至都不是二叉树。 +- (**二叉**)堆是一个数组,它可以被看成是一个 **近似的完全二叉树**。——《算法导论》第三版 + +大家可以尝试判断下面给出的图是否是堆? + +![](pictures/堆/堆1.png) + +第1个和第2个是堆。第1个是最大堆,每个节点都比子树中所有节点大。第2个是最小堆,每个节点都比子树中所有节点小。 + +第3个不是,第三个中,根结点1比2和15小,而15却比3大,19比5大,不满足堆的性质。 + +## 堆的用途 +当我们只关心所有数据中的最大值或者最小值,存在多次获取最大值或者最小值,多次插入或删除数据时,就可以使用堆。 + +有小伙伴可能会想到用有序数组,初始化一个有序数组时间复杂度是 `O(nlog(n))`,查找最大值或者最小值时间复杂度都是 `O(1)`,但是,涉及到更新(插入或删除)数据时,时间复杂度为 `O(n)`,即使是使用复杂度为 `O(log(n))` 的二分法找到要插入或者删除的数据,在移动数据时也需要 `O(n)` 的时间复杂度。 + +**相对于有序数组而言,堆的主要优势在于更新数据效率较高。** 堆的初始化时间复杂度为 `O(nlog(n))`,堆可以做到`O(1)`时间复杂度取出最大值或者最小值,`O(log(n))`时间复杂度插入或者删除数据,具体操作在后续章节详细介绍。 + +## 堆的分类 + +堆分为 **最大堆** 和 **最小堆**。二者的区别在于节点的排序方式。 +- **最大堆** :堆中的每一个节点的值都大于等于子树中所有节点的值 +- **最小堆** :堆中的每一个节点的值都小于等于子树中所有节点的值 + +如下图所示,图1是最大堆,图2是最小堆 + +![](pictures/堆/堆2.png) + + +## 堆的存储 +之前介绍树的时候说过,由于完全二叉树的优秀性质,利用数组存储二叉树即节省空间,又方便索引(若根结点的序号为1,那么对于树中任意节点i,其左子节点序号为 `2*i`,右子节点序号为 `2*i+1`)。 + +为了方便存储和索引,(二叉)堆可以用完全二叉树的形式进行存储。存储的方式如下图所示: + +![堆的存储](pictures/堆/堆的存储.png) + +## 堆的操作 +堆的更新操作主要包括两种 : **插入元素** 和 **删除堆顶元素**。操作过程需要着重掌握和理解。 +> 在进入正题之前,再重申一遍,堆是一个公平的公司,有能力的人自然会走到与他能力所匹配的位置 +### 插入元素 +> 插入元素,作为一个新入职的员工,初来乍到,这个员工需要从基层做起 + +**1.将要插入的元素放到最后** + +![堆-插入元素-1](pictures/堆/堆-插入元素1.png) + +> 有能力的人会逐渐升职加薪,是金子总会发光的!!! + +**2.从底向上,如果父结点比该元素大,则该节点和父结点交换,直到无法交换** + +![堆-插入元素2](pictures/堆/堆-插入元素2.png) + +![堆-插入元素3](pictures/堆/堆-插入元素3.png) + +### 删除堆顶元素 + +根据堆的性质可知,最大堆的堆顶元素为所有元素中最大的,最小堆的堆顶元素是所有元素中最小的。当我们需要多次查找最大元素或者最小元素的时候,可以利用堆来实现。 + +删除堆顶元素后,为了保持堆的性质,需要对堆的结构进行调整,我们将这个过程称之为"**堆化**",堆化的方法分为两种: + +- 一种是自底向上的堆化,上述的插入元素所使用的就是自底向上的堆化,元素从最底部向上移动。 +- 另一种是自顶向下堆化,元素由最顶部向下移动。在讲解删除堆顶元素的方法时,我将阐述这两种操作的过程,大家可以体会一下二者的不同。 + +#### 自底向上堆化 + +> 在堆这个公司中,会出现老大离职的现象,老大离职之后,他的位置就空出来了 + +首先删除堆顶元素,使得数组中下标为1的位置空出。 + + + +![删除堆顶元素1](pictures/堆/删除堆顶元素1.png) + + +> 那么他的位置由谁来接替呢,当然是他的直接下属了,谁能力强就让谁上呗 + +比较根结点的左子节点和右子节点,也就是下标为2,3的数组元素,将较大的元素填充到根结点(下标为1)的位置。 + +![删除堆顶元素2](pictures/堆/删除堆顶元素2.png) + + +> 这个时候又空出一个位置了,老规矩,谁有能力谁上 + +一直循环比较空出位置的左右子节点,并将较大者移至空位,直到堆的最底部 + +![删除堆顶元素3](pictures/堆/删除堆顶元素3.png) + +这个时候已经完成了自底向上的堆化,没有元素可以填补空缺了,但是,我们可以看到数组中出现了“气泡”,这会导致存储空间的浪费。接下来我们试试自顶向下堆化。 + +#### 自顶向下堆化 +自顶向下的堆化用一个词形容就是“石沉大海”,那么第一件事情,就是把石头抬起来,从海面扔下去。这个石头就是堆的最后一个元素,我们将最后一个元素移动到堆顶。 + +![删除堆顶元素4](pictures/堆/删除堆顶元素4.png) + +然后开始将这个石头沉入海底,不停与左右子节点的值进行比较,和较大的子节点交换位置,直到无法交换位置。 + +![删除堆顶元素5](pictures/堆/删除堆顶元素5.png) + +![删除堆顶元素6](pictures/堆/删除堆顶元素6.png) + + + +### 堆的操作总结 + +- **插入元素** :先将元素放至数组末尾,再自底向上堆化,将末尾元素上浮 +- **删除堆顶元素** :删除堆顶元素,将末尾元素放至堆顶,再自顶向下堆化,将堆顶元素下沉。也可以自底向上堆化,只是会产生“气泡”,浪费存储空间。最好采用自顶向下堆化的方式。 + + +## 堆排序 + +堆排序的过程分为两步: + +- 第一步是建堆,将一个无序的数组建立为一个堆 +- 第二步是排序,将堆顶元素取出,然后对剩下的元素进行堆化,反复迭代,直到所有元素被取出为止。 + +### 建堆 + +如果你已经足够了解堆化的过程,那么建堆的过程掌握起来就比较容易了。建堆的过程就是一个对所有非叶节点的自顶向下堆化过程。 + +首先要了解哪些是非叶节点,最后一个节点的父结点及它之前的元素,都是非叶节点。也就是说,如果节点个数为n,那么我们需要对n/2到1的节点进行自顶向下(沉底)堆化。 + +具体过程如下图: + +![建堆1](pictures/堆/建堆1.png) + +将初始的无序数组抽象为一棵树,图中的节点个数为6,所以4,5,6节点为叶节点,1,2,3节点为非叶节点,所以要对1-3号节点进行自顶向下(沉底)堆化,注意,顺序是从后往前堆化,从3号节点开始,一直到1号节点。 +3号节点堆化结果: + +![建堆1](pictures/堆/建堆2.png) + +2号节点堆化结果: + +![建堆1](pictures/堆/建堆3.png) + +1号节点堆化结果: + +![建堆1](pictures/堆/建堆4.png) + +至此,数组所对应的树已经成为了一个最大堆,建堆完成! + +### 排序 + +由于堆顶元素是所有元素中最大的,所以我们重复取出堆顶元素,将这个最大的堆顶元素放至数组末尾,并对剩下的元素进行堆化即可。 + +现在思考两个问题: + +- 删除堆顶元素后需要执行自顶向下(沉底)堆化还是自底向上(上浮)堆化? +- 取出的堆顶元素存在哪,新建一个数组存? + +先回答第一个问题,我们需要执行自顶向下(沉底)堆化,这个堆化一开始要将末尾元素移动至堆顶,这个时候末尾的位置就空出来了,由于堆中元素已经减小,这个位置不会再被使用,所以我们可以将取出的元素放在末尾。 + +机智的小伙伴已经发现了,这其实是做了一次交换操作,将堆顶和末尾元素调换位置,从而将取出堆顶元素和堆化的第一步(将末尾元素放至根结点位置)进行合并。 + +详细过程如下图所示: + +取出第一个元素并堆化: + +![堆排序1](pictures/堆/堆排序1.png) + +取出第二个元素并堆化: + +![堆排序2](pictures/堆/堆排序2.png) + +取出第三个元素并堆化: + +![堆排序3](pictures/堆/堆排序3.png) + +取出第四个元素并堆化: + +![堆排序4](pictures/堆/堆排序4.png) + +取出第五个元素并堆化: + +![堆排序5](pictures/堆/堆排序5.png) + +取出第六个元素并堆化: + +![堆排序6](pictures/堆/堆排序6.png) + +堆排序完成! diff --git "a/docs/cs-basics/data-structure/\346\240\221.md" "b/docs/cs-basics/data-structure/\346\240\221.md" new file mode 100644 index 00000000000..34f7d49dba9 --- /dev/null +++ "b/docs/cs-basics/data-structure/\346\240\221.md" @@ -0,0 +1,180 @@ +--- +category: 计算机基础 +tag: + - 数据结构 +--- + +# 树 + +树就是一种类似现实生活中的树的数据结构(倒置的树)。任何一颗非空树只有一个根节点。 + +一棵树具有以下特点: + +1. 一棵树中的任意两个结点有且仅有唯一的一条路径连通。 +2. 一棵树如果有 n 个结点,那么它一定恰好有 n-1 条边。 +3. 一棵树不包含回路。 + +下图就是一颗树,并且是一颗二叉树。 + +![二叉树](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/二叉树-2.png) + +如上图所示,通过上面这张图说明一下树中的常用概念: + +- **节点** :树中的每个元素都可以统称为节点。 +- **根节点** :顶层节点或者说没有父节点的节点。上图中 A 节点就是根节点。 +- **父节点** :若一个节点含有子节点,则这个节点称为其子节点的父节点。上图中的 B 节点是 D 节点、E 节点的父节点。 +- **子节点** :一个节点含有的子树的根节点称为该节点的子节点。上图中 D 节点、E 节点是 B 节点的子节点。 +- **兄弟节点** :具有相同父节点的节点互称为兄弟节点。上图中 D 节点、E 节点的共同父节点是 B 节点,故 D 和 E 为兄弟节点。 +- **叶子节点** :没有子节点的节点。上图中的 D、F、H、I 都是叶子节点。 +- **节点的高度** :该节点到叶子节点的最长路径所包含的边数。 +- **节点的深度** :根节点到该节点的路径所包含的边数 +- **节点的层数** :节点的深度+1。 +- **树的高度** :根节点的高度。 + +## 二叉树的分类 + +**二叉树**(Binary tree)是每个节点最多只有两个分支(即不存在分支度大于 2 的节点)的树结构。 + +**二叉树** 的分支通常被称作“**左子树**”或“**右子树**”。并且,**二叉树** 的分支具有左右次序,不能随意颠倒。 + +**二叉树** 的第 i 层至多拥有 `2^(i-1)` 个节点,深度为 k 的二叉树至多总共有 `2^k-1` 个节点 + +### 满二叉树 + +一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是 **满二叉树**。也就是说,如果一个二叉树的层数为 K,且结点总数是(2^k) -1 ,则它就是 **满二叉树**。如下图所示: + +![](./pictures/树/满二叉树.png) + +### 完全二叉树 + +除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点,则这个二叉树就是 **完全二叉树** 。 + +大家可以想象为一棵树从根结点开始扩展,扩展完左子节点才能开始扩展右子节点,每扩展完一层,才能继续扩展下一层。如下图所示: + +![](./pictures/树/完全二叉树.png) + +完全二叉树有一个很好的性质:**父结点和子节点的序号有着对应关系。** + +细心的小伙伴可能发现了,当根节点的值为 1 的情况下,若父结点的序号是 i,那么左子节点的序号就是 2i,右子节点的序号是 2i+1。这个性质使得完全二叉树利用数组存储时可以极大地节省空间,以及利用序号找到某个节点的父结点和子节点,后续二叉树的存储会详细介绍。 + +### 平衡二叉树 + +**平衡二叉树** 是一棵二叉排序树,且具有以下性质: + +1. 可以是一棵空树 +2. 如果不是空树,它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。 + +平衡二叉树的常用实现方法有 **红黑树**、**AVL 树**、**替罪羊树**、**加权平衡树**、**伸展树** 等。 + +在给大家展示平衡二叉树之前,先给大家看一棵树: + +![](./pictures/树/斜树.png) + +**你管这玩意儿叫树???** + +没错,这玩意儿还真叫树,只不过这棵树已经退化为一个链表了,我们管它叫 **斜树**。 + +**如果这样,那我为啥不直接用链表呢?** + +谁说不是呢? + +二叉树相比于链表,由于父子节点以及兄弟节点之间往往具有某种特殊的关系,这种关系使得我们在树中对数据进行**搜索**和**修改**时,相对于链表更加快捷便利。 + +但是,如果二叉树退化为一个链表了,那么那么树所具有的优秀性质就难以表现出来,效率也会大打折,为了避免这样的情况,我们希望每个做 “家长”(父结点) 的,都 **一碗水端平**,分给左儿子和分给右儿子的尽可能一样多,相差最多不超过一层,如下图所示: + +![](./pictures/树/平衡二叉树.png) + +## 二叉树的存储 + +二叉树的存储主要分为 **链式存储** 和 **顺序存储** 两种: + +### 链式存储 + +和链表类似,二叉树的链式存储依靠指针将各个节点串联起来,不需要连续的存储空间。 + +每个节点包括三个属性: + +- 数据 data。data 不一定是单一的数据,根据不同情况,可以是多个具有不同类型的数据。 +- 左节点指针 left +- 右节点指针 right。 + +可是 JAVA 没有指针啊! + +那就直接引用对象呗(别问我对象哪里找) + +![](./pictures/树/链式存储二叉树.png) + +### 顺序存储 + +顺序存储就是利用数组进行存储,数组中的每一个位置仅存储节点的 data,不存储左右子节点的指针,子节点的索引通过数组下标完成。根结点的序号为 1,对于每个节点 Node,假设它存储在数组中下标为 i 的位置,那么它的左子节点就存储在 2 _ i 的位置,它的右子节点存储在下标为 2 _ i+1 的位置。 + +一棵完全二叉树的数组顺序存储如下图所示: + +![](./pictures/树/顺序存储.png) + +大家可以试着填写一下存储如下二叉树的数组,比较一下和完全二叉树的顺序存储有何区别: + +![](./pictures/树/顺序存储2.png) + +可以看到,如果我们要存储的二叉树不是完全二叉树,在数组中就会出现空隙,导致内存利用率降低 + +## 二叉树的遍历 + +### 先序遍历 + +![](./pictures/树/先序遍历.png) + +二叉树的先序遍历,就是先输出根结点,再遍历左子树,最后遍历右子树,遍历左子树和右子树的时候,同样遵循先序遍历的规则,也就是说,我们可以递归实现先序遍历。 + +代码如下: + +```java +public void preOrder(TreeNode root){ + if(root == null){ + return; + } + system.out.println(root.data); + preOrder(root.left); + preOrder(root.right); +} +``` + +### 中序遍历 + +![](./pictures/树/中序遍历.png) + +二叉树的中序遍历,就是先递归中序遍历左子树,再输出根结点的值,再递归中序遍历右子树,大家可以想象成一巴掌把树压扁,父结点被拍到了左子节点和右子节点的中间,如下图所示: + +![](./pictures/树/中序遍历2.png) + +代码如下: + +```java +public void inOrder(TreeNode root){ + if(root == null){ + return; + } + inOrder(root.left); + system.out.println(root.data); + inOrder(root.right); +} +``` + +### 后序遍历 + +![](./pictures/树/后序遍历.png) + +二叉树的后序遍历,就是先递归后序遍历左子树,再递归后序遍历右子树,最后输出根结点的值 + +代码如下: + +```java +public void postOrder(TreeNode root){ + if(root == null){ + return; + } + postOrder(root.left); + postOrder(root.right); + system.out.println(root.data); +} +``` \ No newline at end of file diff --git "a/docs/cs-basics/data-structure/\347\272\242\351\273\221\346\240\221.md" "b/docs/cs-basics/data-structure/\347\272\242\351\273\221\346\240\221.md" new file mode 100644 index 00000000000..ce18d6e0138 --- /dev/null +++ "b/docs/cs-basics/data-structure/\347\272\242\351\273\221\346\240\221.md" @@ -0,0 +1,22 @@ +--- +category: 计算机基础 +tag: + - 数据结构 +--- + +# 红黑树 + +**红黑树特点** : + +1. 每个节点非红即黑; +2. 根节点总是黑色的; +3. 每个叶子节点都是黑色的空节点(NIL节点); +4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); +5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。 + +**红黑树的应用** :TreeMap、TreeSet以及JDK1.8的HashMap底层都用到了红黑树。 + +**为什么要用红黑树?** 简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。详细了解可以查看 [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐) + +**相关阅读** :[《红黑树深入剖析及Java实现》](https://zhuanlan.zhihu.com/p/24367771)(美团点评技术团队) + diff --git "a/docs/cs-basics/data-structure/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/docs/cs-basics/data-structure/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204.md" new file mode 100644 index 00000000000..5a55d496696 --- /dev/null +++ "b/docs/cs-basics/data-structure/\347\272\277\346\200\247\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -0,0 +1,316 @@ +--- +category: 计算机基础 +tag: + - 数据结构 +--- + +# 线性数据结构 :数组、链表、栈、队列 + +> 开头还是求点赞,求转发!原创优质公众号,希望大家能让更多人看到我们的文章。 +> +> 图片都是我们手绘的,可以说非常用心了! + +## 1. 数组 + +**数组(Array)** 是一种很常见的数据结构。它由相同类型的元素(element)组成,并且是使用一块连续的内存来存储。 + +我们直接可以利用元素的索引(index)可以计算出该元素对应的存储地址。 + +数组的特点是:**提供随机访问** 并且容量有限。 + +```java +假如数组的长度为 n。 +访问:O(1)//访问特定位置的元素 +插入:O(n )//最坏的情况发生在插入发生在数组的首部并需要移动所有元素时 +删除:O(n)//最坏的情况发生在删除数组的开头发生并需要移动第一元素后面所有的元素时 +``` + +![数组](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数组.png) + +## 2. 链表 + +### 2.1. 链表简介 + +**链表(LinkedList)** 虽然是一种线性表,但是并不会按线性的顺序存储数据,使用的不是连续的内存空间来存储数据。 + +链表的插入和删除操作的复杂度为 O(1) ,只需要知道目标位置元素的上一个元素即可。但是,在查找一个节点或者访问特定位置的节点的时候复杂度为 O(n) 。 + +使用链表结构可以克服数组需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但链表不会节省空间,相比于数组会占用更多的空间,因为链表中每个节点存放的还有指向其他节点的指针。除此之外,链表不具有数组随机读取的优点。 + +### 2.2. 链表分类 + +**常见链表分类:** + +1. 单链表 +2. 双向链表 +3. 循环链表 +4. 双向循环链表 + +```java +假如链表中有n个元素。 +访问:O(n)//访问特定位置的元素 +插入删除:O(1)//必须要要知道插入元素的位置 +``` + +#### 2.2.1. 单链表 + +**单链表** 单向链表只有一个方向,结点只有一个后继指针 next 指向后面的节点。因此,链表这种数据结构通常在物理内存上是不连续的。我们习惯性地把第一个结点叫作头结点,链表通常有一个不保存任何值的 head 节点(头结点),通过头结点我们可以遍历整个链表。尾结点通常指向 null。 + +![单链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/单链表2.png) + +#### 2.2.2. 循环链表 + +**循环链表** 其实是一种特殊的单链表,和单链表不同的是循环链表的尾结点不是指向 null,而是指向链表的头结点。 + +![循环链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/循环链表2.png) + +#### 2.2.3. 双向链表 + +**双向链表** 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。 + +![双向链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/双向链表.png) + +#### 2.2.4. 双向循环链表 + +**双向循环链表** 最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环。 + +![双向循环链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/双向循环链表.png) + +### 2.3. 应用场景 + +- 如果需要支持随机访问的话,链表没办法做到。 +- 如果需要存储的数据元素的个数不确定,并且需要经常添加和删除数据的话,使用链表比较合适。 +- 如果需要存储的数据元素的个数确定,并且不需要经常添加和删除数据的话,使用数组比较合适。 + +### 2.4. 数组 vs 链表 + +- 数组支持随机访问,而链表不支持。 +- 数组使用的是连续内存空间对 CPU 的缓存机制友好,链表则相反。 +- 数组的大小固定,而链表则天然支持动态扩容。如果声明的数组过小,需要另外申请一个更大的内存空间存放数组元素,然后将原数组拷贝进去,这个操作是比较耗时的! + +## 3. 栈 + +### 3.1. 栈简介 + +**栈** (stack)只允许在有序的线性数据集合的一端(称为栈顶 top)进行加入数据(push)和移除数据(pop)。因而按照 **后进先出(LIFO, Last In First Out)** 的原理运作。**在栈中,push 和 pop 的操作都发生在栈顶。** + +栈常用一维数组或链表来实现,用数组实现的栈叫作 **顺序栈** ,用链表实现的栈叫作 **链式栈** 。 + +```java +假设堆栈中有n个元素。 +访问:O(n)//最坏情况 +插入删除:O(1)//顶端插入和删除元素 +``` + +![栈](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/栈.png) + +### 3.2. 栈的常见应用常见应用场景 + +当我们我们要处理的数据只涉及在一端插入和删除数据,并且满足 **后进先出(LIFO, Last In First Out)** 的特性时,我们就可以使用栈这个数据结构。 + +#### 3.2.1. 实现浏览器的回退和前进功能 + +我们只需要使用两个栈(Stack1 和 Stack2)和就能实现这个功能。比如你按顺序查看了 1,2,3,4 这四个页面,我们依次把 1,2,3,4 这四个页面压入 Stack1 中。当你想回头看 2 这个页面的时候,你点击回退按钮,我们依次把 4,3 这两个页面从 Stack1 弹出,然后压入 Stack2 中。假如你又想回到页面 3,你点击前进按钮,我们将 3 页面从 Stack2 弹出,然后压入到 Stack1 中。示例图如下: + +![栈实现浏览器倒退和前进](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/栈实现浏览器倒退和前进.png) + +#### 3.2.2. 检查符号是否成对出现 + +> 给定一个只包括 `'('`,`')'`,`'{'`,`'}'`,`'['`,`']'` 的字符串,判断该字符串是否有效。 +> +> 有效字符串需满足: +> +> 1. 左括号必须用相同类型的右括号闭合。 +> 2. 左括号必须以正确的顺序闭合。 +> +> 比如 "()"、"()[]{}"、"{[]}" 都是有效字符串,而 "(]" 、"([)]" 则不是。 + +这个问题实际是 Leetcode 的一道题目,我们可以利用栈 `Stack` 来解决这个问题。 + +1. 首先我们将括号间的对应规则存放在 `Map` 中,这一点应该毋容置疑; +2. 创建一个栈。遍历字符串,如果字符是左括号就直接加入`stack`中,否则将`stack` 的栈顶元素与这个括号做比较,如果不相等就直接返回 false。遍历结束,如果`stack`为空,返回 `true`。 + +```java +public boolean isValid(String s){ + // 括号之间的对应规则 + HashMap mappings = new HashMap(); + mappings.put(')', '('); + mappings.put('}', '{'); + mappings.put(']', '['); + Stack stack = new Stack(); + char[] chars = s.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (mappings.containsKey(chars[i])) { + char topElement = stack.empty() ? '#' : stack.pop(); + if (topElement != mappings.get(chars[i])) { + return false; + } + } else { + stack.push(chars[i]); + } + } + return stack.isEmpty(); +} +``` + +#### 3.2.3. 反转字符串 + +将字符串中的每个字符先入栈再出栈就可以了。 + +#### 3.2.4. 维护函数调用 + +最后一个被调用的函数必须先完成执行,符合栈的 **后进先出(LIFO, Last In First Out)** 特性。 + +### 3.3. 栈的实现 + +栈既可以通过数组实现,也可以通过链表来实现。不管基于数组还是链表,入栈、出栈的时间复杂度都为 O(1)。 + +下面我们使用数组来实现一个栈,并且这个栈具有`push()`、`pop()`(返回栈顶元素并出栈)、`peek()` (返回栈顶元素不出栈)、`isEmpty()`、`size()`这些基本的方法。 + +> 提示:每次入栈之前先判断栈的容量是否够用,如果不够用就用`Arrays.copyOf()`进行扩容; + +```java +public class MyStack { + private int[] storage;//存放栈中元素的数组 + private int capacity;//栈的容量 + private int count;//栈中元素数量 + private static final int GROW_FACTOR = 2; + + //不带初始容量的构造方法。默认容量为8 + public MyStack() { + this.capacity = 8; + this.storage=new int[8]; + this.count = 0; + } + + //带初始容量的构造方法 + public MyStack(int initialCapacity) { + if (initialCapacity < 1) + throw new IllegalArgumentException("Capacity too small."); + + this.capacity = initialCapacity; + this.storage = new int[initialCapacity]; + this.count = 0; + } + + //入栈 + public void push(int value) { + if (count == capacity) { + ensureCapacity(); + } + storage[count++] = value; + } + + //确保容量大小 + private void ensureCapacity() { + int newCapacity = capacity * GROW_FACTOR; + storage = Arrays.copyOf(storage, newCapacity); + capacity = newCapacity; + } + + //返回栈顶元素并出栈 + private int pop() { + if (count == 0) + throw new IllegalArgumentException("Stack is empty."); + count--; + return storage[count]; + } + + //返回栈顶元素不出栈 + private int peek() { + if (count == 0){ + throw new IllegalArgumentException("Stack is empty."); + }else { + return storage[count-1]; + } + } + + //判断栈是否为空 + private boolean isEmpty() { + return count == 0; + } + + //返回栈中元素的个数 + private int size() { + return count; + } + +} +``` + +验证 + +```java +MyStack myStack = new MyStack(3); +myStack.push(1); +myStack.push(2); +myStack.push(3); +myStack.push(4); +myStack.push(5); +myStack.push(6); +myStack.push(7); +myStack.push(8); +System.out.println(myStack.peek());//8 +System.out.println(myStack.size());//8 +for (int i = 0; i < 8; i++) { + System.out.println(myStack.pop()); +} +System.out.println(myStack.isEmpty());//true +myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty. +``` + +## 4. 队列 + +### 4.1. 队列简介 + +**队列** 是 **先进先出( FIFO,First In, First Out)** 的线性表。在具体应用中通常用链表或者数组来实现,用数组实现的队列叫作 **顺序队列** ,用链表实现的队列叫作 **链式队列** 。**队列只允许在后端(rear)进行插入操作也就是 入队 enqueue,在前端(front)进行删除操作也就是出队 dequeue** + +队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。 + +```java +假设队列中有n个元素。 +访问:O(n)//最坏情况 +插入删除:O(1)//后端插入前端删除元素 +``` + +![队列](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/队列.png) + +### 4.2. 队列分类 + +#### 4.2.1. 单队列 + +单队列就是常见的队列, 每次添加元素时,都是添加到队尾。单队列又分为 **顺序队列(数组实现)** 和 **链式队列(链表实现)**。 + +**顺序队列存在“假溢出”的问题也就是明明有位置却不能添加的情况。** + +假设下图是一个顺序队列,我们将前两个元素 1,2 出队,并入队两个元素 7,8。当进行入队、出队操作的时候,front 和 rear 都会持续往后移动,当 rear 移动到最后的时候,我们无法再往队列中添加数据,即使数组中还有空余空间,这种现象就是 **”假溢出“** 。除了假溢出问题之外,如下图所示,当添加元素 8 的时候,rear 指针移动到数组之外(越界)。 + +> 为了避免当只有一个元素的时候,队头和队尾重合使处理变得麻烦,所以引入两个指针,front 指针指向对头元素,rear 指针指向队列最后一个元素的下一个位置,这样当 front 等于 rear 时,此队列不是还剩一个元素,而是空队列。——From 《大话数据结构》 + +![顺序队列假溢出](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/顺序队列假溢出1.png) + +#### 4.2.2. 循环队列 + +循环队列可以解决顺序队列的假溢出和越界问题。解决办法就是:从头开始,这样也就会形成头尾相接的循环,这也就是循环队列名字的由来。 + +还是用上面的图,我们将 rear 指针指向数组下标为 0 的位置就不会有越界问题了。当我们再向队列中添加元素的时候, rear 向后移动。 + +![循环队列](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/循环队列.png) + +顺序队列中,我们说 `front==rear` 的时候队列为空,循环队列中则不一样,也可能为满,如上图所示。解决办法有两种: + +1. 可以设置一个标志变量 `flag`,当 `front==rear` 并且 `flag=0` 的时候队列为空,当`front==rear` 并且 `flag=1` 的时候队列为满。 +2. 队列为空的时候就是 `front==rear` ,队列满的时候,我们保证数组还有一个空闲的位置,rear 就指向这个空闲位置,如下图所示,那么现在判断队列是否为满的条件就是: `(rear+1) % QueueSize= front` 。 + +![循环队列-队满](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/循环队列-堆满.png) + +### 4.3. 常见应用场景 + +当我们需要按照一定顺序来处理数据的时候可以考虑使用队列这个数据结构。 + +- **阻塞队列:** 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。 +- **线程池中的请求/任务队列:** 线程池中没有空闲线程时,新的任务请求线程资源时,线程池该如何处理呢?答案是将这些请求放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如 :`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`。但是有界队列就不一样了,当队列满的话后面再有任务/请求就会拒绝,在 Java 中的体现就是会抛出`java.util.concurrent.RejectedExecutionException` 异常。 +- Linux 内核进程队列(按优先级排队) +- 现实生活中的排队,播放器上的播放列表; +- 消息队列 +- 等等...... diff --git "a/docs/network/HTTPS\344\270\255\347\232\204TLS.md" "b/docs/cs-basics/network/HTTPS\344\270\255\347\232\204TLS.md" similarity index 83% rename from "docs/network/HTTPS\344\270\255\347\232\204TLS.md" rename to "docs/cs-basics/network/HTTPS\344\270\255\347\232\204TLS.md" index 4132144608c..f665eb22da4 100644 --- "a/docs/network/HTTPS\344\270\255\347\232\204TLS.md" +++ "b/docs/cs-basics/network/HTTPS\344\270\255\347\232\204TLS.md" @@ -1,22 +1,9 @@ - - -- [1. SSL 与 TLS](#1-ssl-%E4%B8%8E-tls) -- [2. 从网络协议的角度理解 HTTPS](#2-%E4%BB%8E%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E7%9A%84%E8%A7%92%E5%BA%A6%E7%90%86%E8%A7%A3-https) -- [3. 从密码学的角度理解 HTTPS](#3-%E4%BB%8E%E5%AF%86%E7%A0%81%E5%AD%A6%E7%9A%84%E8%A7%92%E5%BA%A6%E7%90%86%E8%A7%A3-https) - - [3.1. TLS 工作流程](#31-tls-%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B) - - [3.2. 密码基础](#32-%E5%AF%86%E7%A0%81%E5%9F%BA%E7%A1%80) - - [3.2.1. 伪随机数生成器](#321-%E4%BC%AA%E9%9A%8F%E6%9C%BA%E6%95%B0%E7%94%9F%E6%88%90%E5%99%A8) - - [3.2.2. 消息认证码](#322-%E6%B6%88%E6%81%AF%E8%AE%A4%E8%AF%81%E7%A0%81) - - [3.2.3. 数字签名](#323-%E6%95%B0%E5%AD%97%E7%AD%BE%E5%90%8D) - - [3.2.4. 公钥密码](#324-%E5%85%AC%E9%92%A5%E5%AF%86%E7%A0%81) - - [3.2.5. 证书](#325-%E8%AF%81%E4%B9%A6) - - [3.2.6. 密码小结](#326-%E5%AF%86%E7%A0%81%E5%B0%8F%E7%BB%93) - - [3.3. TLS 使用的密码技术](#33-tls-%E4%BD%BF%E7%94%A8%E7%9A%84%E5%AF%86%E7%A0%81%E6%8A%80%E6%9C%AF) - - [3.4. TLS 总结](#34-tls-%E6%80%BB%E7%BB%93) -- [4. RSA 简单示例](#4-rsa-%E7%AE%80%E5%8D%95%E7%A4%BA%E4%BE%8B) -- [5. 参考](#5-%E5%8F%82%E8%80%83) - - +--- +title: HTTPS中的TLS +category: 计算机基础 +tag: + - 计算机网络 +--- # 1. SSL 与 TLS @@ -61,7 +48,7 @@ HTTPS 使用 TLS 保证安全,这里的“安全”分两部分,一是传输 3. 发送者发送消息和 MAC 值 4. 接收者根据接收到的消息计算 MAC 值 5. 接收者根据自己计算的 MAC 值与收到的 MAC 对比 -6. 如果对比成功,说明消息完整,并来自与正确的发送者 +6. 如果对比成功,说明消息完整,并来自于正确的发送者 ### 3.2.3. 数字签名 @@ -77,7 +64,7 @@ HTTPS 使用 TLS 保证安全,这里的“安全”分两部分,一是传输 ### 3.2.4. 公钥密码 -公钥密码也叫非对称密码,由公钥和私钥组成,它是最开始是为了解决秘钥的配送传输安全问题,即,我们不配送私钥,只配送公钥,私钥由本人保管 +公钥密码也叫非对称密码,由公钥和私钥组成,它最开始是为了解决秘钥的配送传输安全问题,即,我们不配送私钥,只配送公钥,私钥由本人保管 它与数字签名相反,公钥密码的私钥用于解密、公钥用于加密,每个人都可以用别人的公钥加密,但只有对应的私钥才能解开密文 client:明文 + 公钥 = 密文 server:密文 + 私钥 = 明文 @@ -95,7 +82,7 @@ server:密文 + 私钥 = 明文 ### 3.2.6. 密码小结 -| 密码 | 作用 | 组成 | +| 密码 | 作用 | 组成 | | :-- | :-- | :-- | | 消息认证码 | 确认消息的完整、并对消息的来源认证 | 共享秘钥+消息的散列值 | | 数字签名 | 对消息的散列值签名 | 公钥+私钥+消息的散列值 | diff --git a/docs/cs-basics/network/images/Cut-Trough-Switching_0.gif b/docs/cs-basics/network/images/Cut-Trough-Switching_0.gif new file mode 100644 index 00000000000..170dc3bbb19 Binary files /dev/null and b/docs/cs-basics/network/images/Cut-Trough-Switching_0.gif differ diff --git a/docs/cs-basics/network/images/isp.png b/docs/cs-basics/network/images/isp.png new file mode 100644 index 00000000000..fe5c0e96adb Binary files /dev/null and b/docs/cs-basics/network/images/isp.png differ diff --git "a/docs/cs-basics/network/images/\344\270\203\345\261\202\344\275\223\347\263\273\347\273\223\346\236\204\345\233\276.png" "b/docs/cs-basics/network/images/\344\270\203\345\261\202\344\275\223\347\263\273\347\273\223\346\236\204\345\233\276.png" new file mode 100644 index 00000000000..a2d24300fcb Binary files /dev/null and "b/docs/cs-basics/network/images/\344\270\203\345\261\202\344\275\223\347\263\273\347\273\223\346\236\204\345\233\276.png" differ diff --git "a/docs/cs-basics/network/images/\344\274\240\350\276\223\345\261\202.png" "b/docs/cs-basics/network/images/\344\274\240\350\276\223\345\261\202.png" new file mode 100644 index 00000000000..192af24536b Binary files /dev/null and "b/docs/cs-basics/network/images/\344\274\240\350\276\223\345\261\202.png" differ diff --git "a/docs/cs-basics/network/images/\345\272\224\347\224\250\345\261\202.png" "b/docs/cs-basics/network/images/\345\272\224\347\224\250\345\261\202.png" new file mode 100644 index 00000000000..31e1e447b73 Binary files /dev/null and "b/docs/cs-basics/network/images/\345\272\224\347\224\250\345\261\202.png" differ diff --git "a/docs/cs-basics/network/images/\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.png" "b/docs/cs-basics/network/images/\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.png" new file mode 100644 index 00000000000..c2b51a7c589 Binary files /dev/null and "b/docs/cs-basics/network/images/\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.png" differ diff --git "a/docs/cs-basics/network/images/\347\211\251\347\220\206\345\261\202.png" "b/docs/cs-basics/network/images/\347\211\251\347\220\206\345\261\202.png" new file mode 100644 index 00000000000..abb979261c1 Binary files /dev/null and "b/docs/cs-basics/network/images/\347\211\251\347\220\206\345\261\202.png" differ diff --git "a/docs/cs-basics/network/images/\347\275\221\347\273\234\345\261\202.png" "b/docs/cs-basics/network/images/\347\275\221\347\273\234\345\261\202.png" new file mode 100644 index 00000000000..376479d7989 Binary files /dev/null and "b/docs/cs-basics/network/images/\347\275\221\347\273\234\345\261\202.png" differ diff --git "a/docs/cs-basics/network/images/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223/\344\270\207\347\273\264\347\275\221\347\232\204\345\244\247\350\207\264\345\267\245\344\275\234\345\267\245\347\250\213.png" "b/docs/cs-basics/network/images/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223/\344\270\207\347\273\264\347\275\221\347\232\204\345\244\247\350\207\264\345\267\245\344\275\234\345\267\245\347\250\213.png" new file mode 100644 index 00000000000..6af03daa96a Binary files /dev/null and "b/docs/cs-basics/network/images/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223/\344\270\207\347\273\264\347\275\221\347\232\204\345\244\247\350\207\264\345\267\245\344\275\234\345\267\245\347\250\213.png" differ diff --git "a/docs/cs-basics/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" "b/docs/cs-basics/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 00000000000..ef63192cb18 --- /dev/null +++ "b/docs/cs-basics/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,309 @@ +--- +title: 计算机网络常见面试题 +category: 计算机基础 +tag: + - 计算机网络 +--- + +## 一 OSI 与 TCP/IP 各层的结构与功能, 都有哪些协议? + +学习计算机网络时我们一般采用折中的办法,也就是中和 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚。 + +![五层体系结构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/五层体系结构.png) + +结合互联网的情况,自上而下地,非常简要的介绍一下各层的作用。 + +### 1.1 应用层 + +**应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。**应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如**域名系统 DNS**,支持万维网应用的 **HTTP 协议**,支持电子邮件的 **SMTP 协议**等等。我们把应用层交互的数据单元称为报文。 + +**域名系统** + +> 域名系统(Domain Name System 缩写 DNS,Domain Name 被译为域名)是因特网的一项核心服务,它作为可以将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的 IP 数串。(百度百科)例如:一个公司的 Web 网站可看作是它在网上的门户,而域名就相当于其门牌地址,通常域名都使用该公司的名称或简称。例如上面提到的微软公司的域名,类似的还有:IBM 公司的域名是 www.ibm.com、Oracle 公司的域名是 www.oracle.com、Cisco 公司的域名是 www.cisco.com 等。 + +**HTTP 协议** + +> 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW(万维网) 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。(百度百科) + +### 1.2 运输层 + +**运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务**。应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。由于一台主机可同时运行多个线程,因此运输层有复用和分用的功能。所谓复用就是指多个应用层进程可同时使用下面运输层的服务,分用和复用相反,是运输层把收到的信息分别交付上面应用层中的相应进程。 + +**运输层主要使用以下两种协议:** + +1. **传输控制协议 TCP**(Transmission Control Protocol)--提供**面向连接**的,**可靠的**数据传输服务。 +2. **用户数据协议 UDP**(User Datagram Protocol)--提供**无连接**的,尽最大努力的数据传输服务(**不保证数据传输的可靠性**)。 + +**TCP 与 UDP 的对比见问题三。** + +### 1.3 网络层 + +**在计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。** 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 **IP 协议**,因此分组也叫 **IP 数据报** ,简称 **数据报**。 + +这里要注意:**不要把运输层的“用户数据报 UDP ”和网络层的“ IP 数据报”弄混**。另外,无论是哪一层的数据单元,都可笼统地用“分组”来表示。 + +这里强调指出,网络层中的“网络”二字已经不是我们通常谈到的具体网络,而是指计算机网络体系结构模型中第三层的名称. + +互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Internet Protocol)和许多路由选择协议,因此互联网的网络层也叫做**网际层**或**IP 层**。 + +### 1.4 数据链路层 + +**数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。** 在两个相邻节点之间传送数据时,**数据链路层将网络层交下来的 IP 数据报组装成帧**,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。 + +在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。这样,数据链路层在收到一个帧后,就可从中提出数据部分,上交给网络层。 +控制信息还使接收端能够检测到所收到的帧中有无差错。如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在链路层传输时出现差错(这就是说,数据链路层不仅要检错,而且还要纠错),那么就要采用可靠性传输协议来纠正出现的差错。这种方法会使链路层的协议复杂些。 + +### 1.5 物理层 + +在物理层上所传送的数据单位是比特。 + +**物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异,** 使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。 + +在互联网使用的各种协议中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的 TCP/IP 并不一定单指 TCP 和 IP 这两个具体的协议,而往往表示互联网所使用的整个 TCP/IP 协议族。 + +### 1.6 总结一下 + +上面我们对计算机网络的五层体系结构有了初步的了解,下面附送一张七层体系结构图总结一下(图片来源于网络)。 + +![七层体系结构图](images/七层体系结构图.png) + +## 二 TCP 三次握手和四次挥手(面试常客) + +为了准确无误地把数据送达目标处,TCP 协议采用了三次握手策略。 + +### 2.1 TCP 三次握手漫画图解 + +如下图所示,下面的两个机器人通过 3 次握手确定了对方能正确接收和发送消息(图片来源:《图解 HTTP》)。 + +![TCP三次握手](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/三次握手.png) + +**简单示意图:** + +![TCP三次握手](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/三次握手2.png) + +* 客户端–发送带有 SYN 标志的数据包–一次握手–服务端 +* 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端 +* 客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端 + +**详细示意图(图片来源不详)** + +![](https://img-blog.csdnimg.cn/img_convert/0c9f470819684156cfdc27c682db4def.png) + +### 2.2 为什么要三次握手 + +**三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。** + +第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常 + +第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常 + +第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常 + +所以三次握手就能确认双发收发功能都正常,缺一不可。 + +### 2.3 第 2 次握手传回了 ACK,为什么还要传回 SYN? + +接收端传回发送端所发送的 ACK 是为了告诉客户端,我接收到的信息确实就是你所发送的信号了,这表明从客户端到服务端的通信是正常的。而回传 SYN 则是为了建立并确认从服务端到客户端的通信。” + +> SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务器之间传递。 + +### 2.5 为什么要四次挥手 + +![TCP四次挥手](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/TCP四次挥手.png) + +断开一个 TCP 连接则需要“四次挥手”: + +* 客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送 +* 服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加 1 。和 SYN 一样,一个 FIN 将占用一个序号 +* 服务器-关闭与客户端的连接,发送一个 FIN 给客户端 +* 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加 1 + +任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。 + +举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B 回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。 + +上面讲的比较概括,推荐一篇讲的比较细致的文章:[https://blog.csdn.net/qzcsu/article/details/72861891](https://blog.csdn.net/qzcsu/article/details/72861891) + +## 三 TCP, UDP 协议的区别 + +![TCP、UDP协议的区别](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/tcp-vs-udp.jpg) + +UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 却是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等 + +TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP 的可靠体现在 TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。 + +## 四 TCP 协议如何保证可靠传输 + +1. 应用数据被分割成 TCP 认为最适合发送的数据块。 +2. TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。 +3. **校验和:** TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。 +4. TCP 的接收端会丢弃重复的数据。 +5. **流量控制:** TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制) +6. **拥塞控制:** 当网络拥塞时,减少数据的发送。 +7. **ARQ 协议:** 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 +8. **超时重传:** 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。 + +### 4.1 ARQ 协议 + +**自动重传请求**(Automatic Repeat-reQuest,ARQ)是 OSI 模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。 + +#### 停止等待 ARQ 协议 + +停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复 ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组。 + +在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认。 + +**优缺点:** + +* **优点:** 简单 +* **缺点:** 信道利用率低,等待时间长 + +**1) 无差错情况:** + +发送方发送分组, 接收方在规定时间内收到, 并且回复确认. 发送方再次发送。 + +**2) 出现差错情况(超时重传):** + +停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为 **自动重传请求 ARQ** 。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。**连续 ARQ 协议** 可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。 + +**3) 确认丢失和确认迟到** + +* **确认丢失** :确认消息在传输过程丢失。当 A 发送 M1 消息,B 收到后,B 向 A 发送了一个 M1 确认消息,但却在传输过程中丢失。而 A 并不知道,在超时计时过后,A 重传 M1 消息,B 再次收到该消息后采取以下两点措施:1. 丢弃这个重复的 M1 消息,不向上层交付。 2. 向 A 发送确认消息。(不会认为已经发送过了,就不再发送。A 能重传,就证明 B 的确认消息丢失)。 +* **确认迟到** :确认消息在传输过程中迟到。A 发送 M1 消息,B 收到并发送确认。在超时时间内没有收到确认消息,A 重传 M1 消息,B 仍然收到并继续发送确认消息(B 收到了 2 份 M1)。此时 A 收到了 B 第二次发送的确认消息。接着发送其他数据。过了一会,A 收到了 B 第一次发送的对 M1 的确认消息(A 也收到了 2 份确认消息)。处理如下:1. A 收到重复的确认后,直接丢弃。2. B 收到重复的 M1 后,也直接丢弃重复的 M1。 + +#### 连续 ARQ 协议 + +连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。 + +**优缺点:** + +* **优点:** 信道利用率高,容易实现,即使确认丢失,也不必重传。 +* **缺点:** 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5 条 消息,中间第三条丢失(3 号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。 + +### 4.2 滑动窗口和流量控制 + +**TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。** 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。 + +### 4.3 拥塞控制 + +在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。 + +为了进行拥塞控制,TCP 发送方要维持一个 **拥塞窗口(cwnd)** 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。 + +TCP 的拥塞控制采用了四种算法,即 **慢开始** 、 **拥塞避免** 、**快重传** 和 **快恢复**。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。 + +* **慢开始:** 慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd 初始值为 1,每经过一个传播轮次,cwnd 加倍。 +* **拥塞避免:** 拥塞避免算法的思路是让拥塞窗口 cwnd 缓慢增大,即每经过一个往返时间 RTT 就把发送放的 cwnd 加 1. +* **快重传与快恢复:** + 在 TCP/IP 中,快速重传和恢复(fast retransmit and recovery,FRR)是一种拥塞控制算法,它能快速恢复丢失的数据包。没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误。  当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。 + +## 五 在浏览器中输入 url 地址 ->> 显示主页的过程(面试常客) + +百度好像最喜欢问这个问题。 + +> 打开一个网页,整个过程会使用哪些协议? + +图解(图片来源:《图解 HTTP》): + + + +> 上图有一个错误,请注意,是 OSPF 不是 OPSF。 OSPF(Open Shortest Path First,ospf)开放最短路径优先协议, 是由 Internet 工程任务组开发的路由选择协议 + +总体来说分为以下几个过程: + +1. DNS 解析 +2. TCP 连接 +3. 发送 HTTP 请求 +4. 服务器处理请求并返回 HTTP 报文 +5. 浏览器解析渲染页面 +6. 连接结束 + +具体可以参考下面这篇文章: + +* [https://segmentfault.com/a/1190000006879700](https://segmentfault.com/a/1190000006879700) + +## 六 状态码 + +![状态码](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/状态码.png) + +## 七 各种协议与 HTTP 协议之间的关系 + +一般面试官会通过这样的问题来考察你对计算机网络知识体系的理解。 + +图片来源:《图解 HTTP》 + +![各种协议与HTTP协议之间的关系](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/各种协议与HTTP协议之间的关系.png) + +## 八 HTTP 长连接, 短连接 + +在 HTTP/1.0 中默认使用短连接。也就是说,客户端和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个 HTML 或其他类型的 Web 页中包含有其他的 Web 资源(如 JavaScript 文件、图像文件、CSS 文件等),每遇到这样一个 Web 资源,浏览器就会重新建立一个 HTTP 会话。 + +而从 HTTP/1.1 起,默认使用长连接,用以保持连接特性。使用长连接的 HTTP 协议,会在响应头加入这行代码: + +``` +Connection:keep-alive +``` + +在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive 不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如 Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。 + +**HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接。** + +—— [《HTTP 长连接、短连接究竟是什么?》](https://www.cnblogs.com/gotodsp/p/6366163.html) + +## 九 HTTP 是不保存状态的协议, 如何保存用户状态? + +HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个 Session)。 + +在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如是使用内存数据库 redis 保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。 + +**Cookie 被禁用怎么办?** + +最常用的就是利用 URL 重写把 Session ID 直接附加在 URL 路径的后面。 + +![HTTP是无状态协议](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/HTTP是无状态的.png) + +## 十 Cookie 的作用是什么? 和 Session 有什么区别? + +Cookie 和 Session 都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。 + +**Cookie 一般用来保存用户信息** 比如 ① 我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了;② 一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了,这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);③ 登录一次网站后访问网站其他页面不需要重新登录。**Session 的主要作用就是通过服务端记录用户的状态。** 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。 + +Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。 + +Cookie 存储在客户端中,而 Session 存储在服务器上,相对来说 Session 安全性更高。如果要在 Cookie 中存储一些敏感信息,不要直接写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。 + +## 十一 HTTP 1.0 和 HTTP 1.1 的主要区别是什么? + +> 这部分回答引用这篇文章 的一些内容。 + +HTTP1.0 最早在网页中使用是在 1996 年,那个时候只是使用一些较为简单的网页上和网络请求上,而 HTTP1.1 则在 1999 年才开始广泛应用于现在的各大浏览器网络请求中,同时 HTTP1.1 也是当前使用最为广泛的 HTTP 协议。 主要区别主要体现在: + +1. **长连接** : **在 HTTP/1.0 中,默认使用的是短连接**,也就是说每次请求都要重新建立一次连接。HTTP 是基于 TCP/IP 协议的,每一次建立或者断开连接都需要三次握手四次挥手的开销,如果每次请求都要这样的话,开销会比较大。因此最好能维持一个长连接,可以用个长连接来发多个请求。**HTTP 1.1 起,默认使用长连接** ,默认开启 Connection: keep-alive。 **HTTP/1.1 的持续连接有非流水线方式和流水线方式** 。流水线方式是客户在收到 HTTP 的响应报文之前就能接着发送新的请求报文。与之相对应的非流水线方式是客户在收到前一个响应后才能发送下一个请求。 +1. **错误状态响应码** :在 HTTP1.1 中新增了 24 个错误状态响应码,如 409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。 +1. **缓存处理** :在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。 +1. **带宽优化及网络连接的使用** :HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。 + +## 十二 URI 和 URL 的区别是什么? + +* URI(Uniform Resource Identifier) 是统一资源标志符,可以唯一标识一个资源。 +* URL(Uniform Resource Locator) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。 + +URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。 + +## 十三 HTTP 和 HTTPS 的区别? + +1. **端口** :HTTP 的 URL 由“http://”起始且默认使用端口80,而HTTPS的URL由“https://”起始且默认使用端口443。 +2. **安全性和资源消耗:** HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。 + - 对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有 DES、AES 等; + - 非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有 RSA、DSA 等。 + +## 建议 + +非常推荐大家看一下 《图解 HTTP》 这本书,这本书页数不多,但是内容很是充实,不管是用来系统的掌握网络方面的一些知识还是说纯粹为了应付面试都有很大帮助。下面的一些文章只是参考。大二学习这门课程的时候,我们使用的教材是 《计算机网络第七版》(谢希仁编著),不推荐大家看这本教材,书非常厚而且知识偏理论,不确定大家能不能心平气和的读完。 + +## 参考 + +* [https://blog.csdn.net/qq_16209077/article/details/52718250](https://blog.csdn.net/qq_16209077/article/details/52718250) +* [https://blog.csdn.net/zixiaomuwu/article/details/60965466](https://blog.csdn.net/zixiaomuwu/article/details/60965466) +* [https://blog.csdn.net/turn\_\_back/article/details/73743641](https://blog.csdn.net/turn__back/article/details/73743641) +* diff --git "a/docs/cs-basics/network/\350\260\242\345\270\214\344\273\201\350\200\201\345\270\210\347\232\204\343\200\212\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\343\200\213\345\206\205\345\256\271\346\200\273\347\273\223.md" "b/docs/cs-basics/network/\350\260\242\345\270\214\344\273\201\350\200\201\345\270\210\347\232\204\343\200\212\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\343\200\213\345\206\205\345\256\271\346\200\273\347\273\223.md" new file mode 100644 index 00000000000..df0b07537fd --- /dev/null +++ "b/docs/cs-basics/network/\350\260\242\345\270\214\344\273\201\350\200\201\345\270\210\347\232\204\343\200\212\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\343\200\213\345\206\205\345\256\271\346\200\273\347\273\223.md" @@ -0,0 +1,355 @@ +--- +title: 谢希仁老师的《计算机网络》内容总结 +category: 计算机基础 +tag: + - 计算机网络 +--- + + +本文是我在大二学习计算机网络期间整理, 大部分内容都来自于谢希仁老师的《计算机网络》这本书。 + +为了内容更容易理解,我对之前的整理进行了一波重构,并配上了一些相关的示意图便于理解。 + +![](https://img-blog.csdnimg.cn/img_convert/e7177b00248e30dc49bd6061093a0590.png) + + + + + +- [1. 计算机网络概述](#1-计算机网络概述) + - [1.1. 基本术语](#11-基本术语) + - [1.2. 重要知识点总结](#12-重要知识点总结) +- [2. 物理层(Physical Layer)](#2-物理层physical-layer) + - [2.1. 基本术语](#21-基本术语) + - [2.2. 重要知识点总结](#22-重要知识点总结) + - [2.3. 补充](#23-补充) + - [2.3.1. 物理层主要做啥?](#231-物理层主要做啥) + - [2.3.2. 几种常用的信道复用技术](#232-几种常用的信道复用技术) + - [2.3.3. 几种常用的宽带接入技术,主要是 ADSL 和 FTTx](#233-几种常用的宽带接入技术主要是-adsl-和-fttx) +- [3. 数据链路层(Data Link Layer)](#3-数据链路层data-link-layer) + - [3.1. 基本术语](#31-基本术语) + - [3.2. 重要知识点总结](#32-重要知识点总结) + - [3.3. 补充](#33-补充) +- [4. 网络层(Network Layer)](#4-网络层network-layer) + - [4.1. 基本术语](#41-基本术语) + - [4.2. 重要知识点总结](#42-重要知识点总结) +- [5. 传输层(Transport Layer)](#5-传输层transport-layer) + - [5.1. 基本术语](#51-基本术语) + - [5.2. 重要知识点总结](#52-重要知识点总结) + - [5.3. 补充(重要)](#53-补充重要) +- [6. 应用层(Application Layer)](#6-应用层application-layer) + - [6.1. 基本术语](#61-基本术语) + - [6.2. 重要知识点总结](#62-重要知识点总结) + - [6.3. 补充(重要)](#63-补充重要) + + + + +## 1. 计算机网络概述 + +### 1.1. 基本术语 + +1. **结点 (node)** :网络中的结点可以是计算机,集线器,交换机或路由器等。 +2. **链路(link )** : 从一个结点到另一个结点的一段物理线路。中间没有任何其他交点。 +3. **主机(host)** :连接在因特网上的计算机。 +4. **ISP(Internet Service Provider)** :因特网服务提供者(提供商)。 + +![ISP (Internet Service Provider) Definition](https://img-blog.csdnimg.cn/img_convert/b83f6951e3f8f4bcde5b227257d603a8.png) + +5. **IXP(Internet eXchange Point)** : 互联网交换点 IXP 的主要作用就是允许两个网络直接相连并交换分组,而不需要再通过第三个网络来转发分组。 + +![IXP Traffic Levels During the Stratos Skydive — RIPE Labs](https://img-blog.csdnimg.cn/img_convert/7a9568a9e94001fc110801addc8c4ec0.png) + +

https://labs.ripe.net/Members/fergalc/ixp-traffic-during-stratos-skydive

+ +6. **RFC(Request For Comments)** :意思是“请求评议”,包含了关于 Internet 几乎所有的重要的文字资料。 +7. **广域网 WAN(Wide Area Network)** :任务是通过长距离运送主机发送的数据。 +8. **城域网 MAN(Metropolitan Area Network)**:用来将多个局域网进行互连。 +9. **局域网 LAN(Local Area Network)** : 学校或企业大多拥有多个互连的局域网。 + +![MAN & WMAN | Red de área metropolitana, Redes informaticas, Par trenzado](https://img-blog.csdnimg.cn/img_convert/25a5789f8e18995c649f2f864d51e7a9.png) + +

http://conexionesmanwman.blogspot.com/

+ +10. **个人区域网 PAN(Personal Area Network)** :在个人工作的地方把属于个人使用的电子设备用无线技术连接起来的网络 。 + +![Advantages and disadvantages of personal area network (PAN) - IT Release](https://img-blog.csdnimg.cn/img_convert/5c99dd6011439b1fab6cd2fece155dd5.png) + +

https://www.itrelease.com/2018/07/advantages-and-disadvantages-of-personal-area-network-pan/

+ +12. **分组(packet )** :因特网中传送的数据单元。由首部 header 和数据段组成。分组又称为包,首部可称为包头。 +13. **存储转发(store and forward )** :路由器收到一个分组,先检查分组是否正确,并过滤掉冲突包错误。确定包正确后,取出目的地址,通过查找表找到想要发送的输出端口地址,然后将该包发送出去。 + +![](https://img-blog.csdnimg.cn/20201025142342169.gif#pic_center) + +14. **带宽(bandwidth)** :在计算机网络中,表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”。常用来表示网络的通信线路所能传送数据的能力。单位是“比特每秒”,记为 b/s。 +15. **吞吐量(throughput )** :表示在单位时间内通过某个网络(或信道、接口)的数据量。吞吐量更经常地用于对现实世界中的网络的一种测量,以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或网络的额定速率的限制。 + +### 1.2. 重要知识点总结 + +1. **计算机网络(简称网络)把许多计算机连接在一起,而互联网把许多网络连接在一起,是网络的网络。** +2. 小写字母 i 开头的 internet(互联网)是通用名词,它泛指由多个计算机网络相互连接而成的网络。在这些网络之间的通信协议(即通信规则)可以是任意的。大写字母 I 开头的 Internet(互联网)是专用名词,它指全球最大的,开放的,由众多网络相互连接而成的特定的互联网,并采用 TCP/IP 协议作为通信规则,其前身为 ARPANET。Internet 的推荐译名为因特网,现在一般流行称为互联网。 +3. 路由器是实现分组交换的关键构件,其任务是转发收到的分组,这是网络核心部分最重要的功能。分组交换采用存储转发技术,表示把一个报文(要发送的整块数据)分为几个分组后再进行传送。在发送报文之前,先把较长的报文划分成为一个个更小的等长数据段。在每个数据端的前面加上一些由必要的控制信息组成的首部后,就构成了一个分组。分组又称为包。分组是在互联网中传送的数据单元,正是由于分组的头部包含了诸如目的地址和源地址等重要控制信息,每一个分组才能在互联网中独立的选择传输路径,并正确地交付到分组传输的终点。 +4. 互联网按工作方式可划分为边缘部分和核心部分。主机在网络的边缘部分,其作用是进行信息处理。由大量网络和连接这些网络的路由器组成核心部分,其作用是提供连通性和交换。 +5. 计算机通信是计算机中进程(即运行着的程序)之间的通信。计算机网络采用的通信方式是客户-服务器方式(C/S 方式)和对等连接方式(P2P 方式)。 +6. 客户和服务器都是指通信中所涉及的应用进程。客户是服务请求方,服务器是服务提供方。 +7. 按照作用范围的不同,计算机网络分为广域网 WAN,城域网 MAN,局域网 LAN,个人区域网 PAN。 +8. **计算机网络最常用的性能指标是:速率,带宽,吞吐量,时延(发送时延,处理时延,排队时延),时延带宽积,往返时间和信道利用率。** +9. 网络协议即协议,是为进行网络中的数据交换而建立的规则。计算机网络的各层以及其协议集合,称为网络的体系结构。 +10. **五层体系结构由应用层,运输层,网络层(网际层),数据链路层,物理层组成。运输层最主要的协议是 TCP 和 UDP 协议,网络层最重要的协议是 IP 协议。** + +![s](https://img-blog.csdnimg.cn/2020102514243717.png#pic_center) + +下面的内容会介绍计算机网络的五层体系结构:**物理层+数据链路层+网络层(网际层)+运输层+应用层**。 + +## 2. 物理层(Physical Layer) + +![物理层](https://img-blog.csdnimg.cn/img_convert/4749289d6e152bab1c8a8ccfc946a797.png) + +### 2.1. 基本术语 + +1. **数据(data)** :运送消息的实体。 +2. **信号(signal)** :数据的电气的或电磁的表现。或者说信号是适合在传输介质上传输的对象。 +3. **码元( code)** :在使用时间域(或简称为时域)的波形来表示数字信号时,代表不同离散数值的基本波形。 +4. **单工(simplex )** : 只能有一个方向的通信而没有反方向的交互。 +5. **半双工(half duplex )** :通信的双方都可以发送信息,但不能双方同时发送(当然也就不能同时接收)。 +6. **全双工(full duplex)** : 通信的双方可以同时发送和接收信息。 + +![](https://img-blog.csdnimg.cn/img_convert/c5be4756d2d6f46cbb6d785d5b86faf1.png) + +7. **失真**:失去真实性,主要是指接受到的信号和发送的信号不同,有磨损和衰减。影响失真程度的因素:1.码元传输速率 2.信号传输距离 3.噪声干扰 4.传输媒体质量 + +![](https://img-blog.csdnimg.cn/img_convert/aef3aac72e86c1ee6ccb8a91647f656c.png) + +8. **奈氏准则** : 在任何信道中,码元的传输的效率是有上限的,传输速率超过此上限,就会出现严重的码间串扰问题,使接收端对码元的判决(即识别)成为不可能。 +9. **香农定理** :在带宽受限且有噪声的信道中,为了不产生误差,信息的数据传输速率有上限值。 +10. **基带信号(baseband signal)** : 来自信源的信号。指没有经过调制的数字信号或模拟信号。 +11. **带通(频带)信号(bandpass signal)** :把基带信号经过载波调制后,把信号的频率范围搬移到较高的频段以便在信道中传输(即仅在一段频率范围内能够通过信道),这里调制过后的信号就是带通信号。 +12. **调制(modulation )** : 对信号源的信息进行处理后加到载波信号上,使其变为适合在信道传输的形式的过程。 +13. **信噪比(signal-to-noise ratio )** : 指信号的平均功率和噪声的平均功率之比,记为 S/N。信噪比(dB)=10\*log10(S/N)。 +14. **信道复用(channel multiplexing )** :指多个用户共享同一个信道。(并不一定是同时)。 + +![信道复用技术](https://img-blog.csdnimg.cn/img_convert/a4889adaad3314f882e2cfab5f382064.png) + +15. **比特率(bit rate )** :单位时间(每秒)内传送的比特数。 +16. **波特率(baud rate)** :单位时间载波调制状态改变的次数。针对数据信号对载波的调制速率。 +17. **复用(multiplexing)** :共享信道的方法。 +18. **ADSL(Asymmetric Digital Subscriber Line )** :非对称数字用户线。 +19. **光纤同轴混合网(HFC 网)** :在目前覆盖范围很广的有线电视网的基础上开发的一种居民宽带接入网 + +### 2.2. 重要知识点总结 + +1. **物理层的主要任务就是确定与传输媒体接口有关的一些特性,如机械特性,电气特性,功能特性,过程特性。** +2. 一个数据通信系统可划分为三大部分,即源系统,传输系统,目的系统。源系统包括源点(或源站,信源)和发送器,目的系统包括接收器和终点。 +3. **通信的目的是传送消息。如话音,文字,图像等都是消息,数据是运送消息的实体。信号则是数据的电气或电磁的表现。** +4. 根据信号中代表消息的参数的取值方式不同,信号可分为模拟信号(或连续信号)和数字信号(或离散信号)。在使用时间域(简称时域)的波形表示数字信号时,代表不同离散数值的基本波形称为码元。 +5. 根据双方信息交互的方式,通信可划分为单向通信(或单工通信),双向交替通信(或半双工通信),双向同时通信(全双工通信)。 +6. 来自信源的信号称为基带信号。信号要在信道上传输就要经过调制。调制有基带调制和带通调制之分。最基本的带通调制方法有调幅,调频和调相。还有更复杂的调制方法,如正交振幅调制。 +7. 要提高数据在信道上的传递速率,可以使用更好的传输媒体,或使用先进的调制技术。但数据传输速率不可能任意被提高。 +8. 传输媒体可分为两大类,即导引型传输媒体(双绞线,同轴电缆,光纤)和非导引型传输媒体(无线,红外,大气激光)。 +9. 为了有效利用光纤资源,在光纤干线和用户之间广泛使用无源光网络 PON。无源光网络无需配备电源,其长期运营成本和管理成本都很低。最流行的无源光网络是以太网无源光网络 EPON 和吉比特无源光网络 GPON。 + +### 2.3. 补充 + +#### 2.3.1. 物理层主要做啥? + +物理层主要做的事情就是 **透明地传送比特流**。也可以将物理层的主要任务描述为确定与传输媒体的接口的一些特性,即:机械特性(接口所用接线器的一些物理属性如形状和尺寸),电气特性(接口电缆的各条线上出现的电压的范围),功能特性(某条线上出现的某一电平的电压的意义),过程特性(对于不同功能的各种可能事件的出现顺序)。 + +**物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流,而不是指具体的传输媒体。** 现有的计算机网络中的硬件设备和传输媒体的种类非常繁多,而且通信手段也有许多不同的方式。物理层的作用正是尽可能地屏蔽掉这些传输媒体和通信手段的差异,使物理层上面的数据链路层感觉不到这些差异,这样就可以使数据链路层只考虑完成本层的协议和服务,而不必考虑网络的具体传输媒体和通信手段是什么。 + +#### 2.3.2. 几种常用的信道复用技术 + +1. **频分复用(FDM)** :所有用户在同样的时间占用不同的带宽资源。 +2. **时分复用(TDM)** :所有用户在不同的时间占用同样的频带宽度(分时不分频)。 +3. **统计时分复用 (Statistic TDM)** :改进的时分复用,能够明显提高信道的利用率。 +4. **码分复用(CDM)** : 用户使用经过特殊挑选的不同码型,因此各用户之间不会造成干扰。这种系统发送的信号有很强的抗干扰能力,其频谱类似于白噪声,不易被敌人发现。 +5. **波分复用( WDM)** :波分复用就是光的频分复用。 + +#### 2.3.3. 几种常用的宽带接入技术,主要是 ADSL 和 FTTx + +用户到互联网的宽带接入方法有非对称数字用户线 ADSL(用数字技术对现有的模拟电话线进行改造,而不需要重新布线。ADSL 的快速版本是甚高速数字用户线 VDSL。),光纤同轴混合网 HFC(是在目前覆盖范围很广的有线电视网的基础上开发的一种居民宽带接入网)和 FTTx(即光纤到······)。 + +## 3. 数据链路层(Data Link Layer) + +![数据链路层](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/2-data-link-layer.svg) + +### 3.1. 基本术语 + +1. **链路(link)** :一个结点到相邻结点的一段物理链路。 +2. **数据链路(data link)** :把实现控制数据运输的协议的硬件和软件加到链路上就构成了数据链路。 +3. **循环冗余检验 CRC(Cyclic Redundancy Check)** :为了保证数据传输的可靠性,CRC 是数据链路层广泛使用的一种检错技术。 +4. **帧(frame)** :一个数据链路层的传输单元,由一个数据链路层首部和其携带的封包所组成协议数据单元。 +5. **MTU(Maximum Transfer Uint )** :最大传送单元。帧的数据部分的的长度上限。 +6. **误码率 BER(Bit Error Rate )** :在一段时间内,传输错误的比特占所传输比特总数的比率。 +7. **PPP(Point-to-Point Protocol )** :点对点协议。即用户计算机和 ISP 进行通信时所使用的数据链路层协议。以下是 PPP 帧的示意图: + ![PPP](https://img-blog.csdnimg.cn/img_convert/298dbdeb16f98cec02c3954d8d95c1d6.png) +8. **MAC 地址(Media Access Control 或者 Medium Access Control)** :意译为媒体访问控制,或称为物理地址、硬件地址,用来定义网络设备的位置。在 OSI 模型中,第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC 地址。因此一个主机会有一个 MAC 地址,而每个网络位置会有一个专属于它的 IP 地址 。地址是识别某个系统的重要标识符,“名字指出我们所要寻找的资源,地址指出资源所在的地方,路由告诉我们如何到达该处。” + +![ARP (Address Resolution Protocol) explained](https://img-blog.csdnimg.cn/img_convert/002b2e6e45d66e805008fafc310afef0.png) + +9. **网桥(bridge)** :一种用于数据链路层实现中继,连接两个或多个局域网的网络互连设备。 +10. **交换机(switch )** :广义的来说,交换机指的是一种通信系统中完成信息交换的设备。这里工作在数据链路层的交换机指的是交换式集线器,其实质是一个多接口的网桥 + +### 3.2. 重要知识点总结 + +1. 链路是从一个结点到相邻结点的一段物理链路,数据链路则在链路的基础上增加了一些必要的硬件(如网络适配器)和软件(如协议的实现) +2. 数据链路层使用的主要是**点对点信道**和**广播信道**两种。 +3. 数据链路层传输的协议数据单元是帧。数据链路层的三个基本问题是:**封装成帧**,**透明传输**和**差错检测** +4. **循环冗余检验 CRC** 是一种检错方法,而帧检验序列 FCS 是添加在数据后面的冗余码 +5. **点对点协议 PPP** 是数据链路层使用最多的一种协议,它的特点是:简单,只检测差错而不去纠正差错,不使用序号,也不进行流量控制,可同时支持多种网络层协议 +6. PPPoE 是为宽带上网的主机使用的链路层协议 +7. **局域网的优点是:具有广播功能,从一个站点可方便地访问全网;便于系统的扩展和逐渐演变;提高了系统的可靠性,可用性和生存性。** +8. 计算机与外接局域网通信需要通过通信适配器(或网络适配器),它又称为网络接口卡或网卡。**计算器的硬件地址就在适配器的 ROM 中**。 +9. 以太网采用的无连接的工作方式,对发送的数据帧不进行编号,也不要求对方发回确认。目的站收到有差错帧就把它丢掉,其他什么也不做 +10. 以太网采用的协议是具有冲突检测的**载波监听多点接入 CSMA/CD**。协议的特点是:**发送前先监听,边发送边监听,一旦发现总线上出现了碰撞,就立即停止发送。然后按照退避算法等待一段随机时间后再次发送。** 因此,每一个站点在自己发送数据之后的一小段时间内,存在着遭遇碰撞的可能性。以太网上的各站点平等地争用以太网信道 +11. 以太网的适配器具有过滤功能,它只接收单播帧,广播帧和多播帧。 +12. 使用集线器可以在物理层扩展以太网(扩展后的以太网仍然是一个网络) + +### 3.3. 补充 + +1. 数据链路层的点对点信道和广播信道的特点,以及这两种信道所使用的协议(PPP 协议以及 CSMA/CD 协议)的特点 +2. 数据链路层的三个基本问题:**封装成帧**,**透明传输**,**差错检测** +3. 以太网的 MAC 层硬件地址 +4. 适配器,转发器,集线器,网桥,以太网交换机的作用以及适用场合 + +## 4. 网络层(Network Layer) + +![网络层](https://img-blog.csdnimg.cn/img_convert/fbf78bdcf3db11526ac1a234a8b98234.png) + +### 4.1. 基本术语 + +1. **虚电路(Virtual Circuit)** : 在两个终端设备的逻辑或物理端口之间,通过建立的双向的透明传输通道。虚电路表示这只是一条逻辑上的连接,分组都沿着这条逻辑连接按照存储转发方式传送,而并不是真正建立了一条物理连接。 +2. **IP(Internet Protocol )** : 网际协议 IP 是 TCP/IP 体系中两个最主要的协议之一,是 TCP/IP 体系结构网际层的核心。配套的有 ARP,RARP,ICMP,IGMP。 +3. **ARP(Address Resolution Protocol)** : 地址解析协议。地址解析协议 ARP 把 IP 地址解析为硬件地址。 +4. **ICMP(Internet Control Message Protocol )** :网际控制报文协议 (ICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告)。 +5. **子网掩码(subnet mask )** :它是一种用来指明一个 IP 地址的哪些位标识的是主机所在的子网以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合 IP 地址一起使用。 +6. **CIDR( Classless Inter-Domain Routing )**:无分类域间路由选择 (特点是消除了传统的 A 类、B 类和 C 类地址以及划分子网的概念,并使用各种长度的“网络前缀”(network-prefix)来代替分类地址中的网络号和子网号)。 +7. **默认路由(default route)** :当在路由表中查不到能到达目的地址的路由时,路由器选择的路由。默认路由还可以减小路由表所占用的空间和搜索路由表所用的时间。 +8. **路由选择算法(Virtual Circuit)** :路由选择协议的核心部分。因特网采用自适应的,分层次的路由选择协议。 + +### 4.2. 重要知识点总结 + +1. **TCP/IP 协议中的网络层向上只提供简单灵活的,无连接的,尽最大努力交付的数据报服务。网络层不提供服务质量的承诺,不保证分组交付的时限所传送的分组可能出错,丢失,重复和失序。进程之间通信的可靠性由运输层负责** +2. 在互联网的交付有两种,一是在本网络直接交付不用经过路由器,另一种是和其他网络的间接交付,至少经过一个路由器,但最后一次一定是直接交付 +3. 分类的 IP 地址由网络号字段(指明网络)和主机号字段(指明主机)组成。网络号字段最前面的类别指明 IP 地址的类别。IP 地址是一种分等级的地址结构。IP 地址管理机构分配 IP 地址时只分配网络号,主机号由得到该网络号的单位自行分配。路由器根据目的主机所连接的网络号来转发分组。一个路由器至少连接到两个网络,所以一个路由器至少应当有两个不同的 IP 地址 +4. IP 数据报分为首部和数据两部分。首部的前一部分是固定长度,共 20 字节,是所有 IP 数据包必须具有的(源地址,目的地址,总长度等重要地段都固定在首部)。一些长度可变的可选字段固定在首部的后面。IP 首部中的生存时间给出了 IP 数据报在互联网中所能经过的最大路由器数。可防止 IP 数据报在互联网中无限制的兜圈子。 +5. **地址解析协议 ARP 把 IP 地址解析为硬件地址。ARP 的高速缓存可以大大减少网络上的通信量。因为这样可以使主机下次再与同样地址的主机通信时,可以直接从高速缓存中找到所需要的硬件地址而不需要再去以广播方式发送 ARP 请求分组** +6. 无分类域间路由选择 CIDR 是解决目前 IP 地址紧缺的一个好办法。CIDR 记法在 IP 地址后面加上斜线“/”,然后写上前缀所占的位数。前缀(或网络前缀)用来指明网络,前缀后面的部分是后缀,用来指明主机。CIDR 把前缀都相同的连续的 IP 地址组成一个“CIDR 地址块”,IP 地址分配都以 CIDR 地址块为单位。 +7. 网际控制报文协议是 IP 层的协议。ICMP 报文作为 IP 数据报的数据,加上首部后组成 IP 数据报发送出去。使用 ICMP 数据报并不是为了实现可靠传输。ICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告。ICMP 报文的种类有两种,即 ICMP 差错报告报文和 ICMP 询问报文。 +8. **要解决 IP 地址耗尽的问题,最根本的办法是采用具有更大地址空间的新版本 IP 协议-IPv6。** IPv6 所带来的变化有 ① 更大的地址空间(采用 128 位地址)② 灵活的首部格式 ③ 改进的选项 ④ 支持即插即用 ⑤ 支持资源的预分配 ⑥IPv6 的首部改为 8 字节对齐。 +9. **虚拟专用网络 VPN 利用公用的互联网作为本机构专用网之间的通信载体。VPN 内使用互联网的专用地址。一个 VPN 至少要有一个路由器具有合法的全球 IP 地址,这样才能和本系统的另一个 VPN 通过互联网进行通信。所有通过互联网传送的数据都需要加密。** +10. MPLS 的特点是:① 支持面向连接的服务质量 ② 支持流量工程,平衡网络负载 ③ 有效的支持虚拟专用网 VPN。MPLS 在入口节点给每一个 IP 数据报打上固定长度的“标记”,然后根据标记在第二层(链路层)用硬件进行转发(在标记交换路由器中进行标记交换),因而转发速率大大加快。 + +## 5. 传输层(Transport Layer) + +![传输层](https://img-blog.csdnimg.cn/img_convert/09eb87a29bed99775ef5bde5eb216971.png) + +### 5.1. 基本术语 + +1. **进程(process)** :指计算机中正在运行的程序实体。 +2. **应用进程互相通信** :一台主机的进程和另一台主机中的一个进程交换数据的过程(另外注意通信真正的端点不是主机而是主机中的进程,也就是说端到端的通信是应用进程之间的通信)。 +3. **传输层的复用与分用** :复用指发送方不同的进程都可以通过同一个运输层协议传送数据。分用指接收方的运输层在剥去报文的首部后能把这些数据正确的交付到目的应用进程。 +4. **TCP(Transmission Control Protocol)** :传输控制协议。 +5. **UDP(User Datagram Protocol)** :用户数据报协议。 + +![TCP和UDP](https://img-blog.csdnimg.cn/img_convert/2bd5bf90676c338864807ade87b7bdea.png) + +6. **端口(port)** :端口的目的是为了确认对方机器的哪个进程在与自己进行交互,比如 MSN 和 QQ 的端口不同,如果没有端口就可能出现 QQ 进程和 MSN 交互错误。端口又称协议端口号。 +7. **停止等待协议(stop-and-wait)** :指发送方每发送完一个分组就停止发送,等待对方确认,在收到确认之后在发送下一个分组。 +8. **流量控制** : 就是让发送方的发送速率不要太快,既要让接收方来得及接收,也不要使网络发生拥塞。 +9. **拥塞控制** :防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。 + +### 5.2. 重要知识点总结 + +1. **运输层提供应用进程之间的逻辑通信,也就是说,运输层之间的通信并不是真正在两个运输层之间直接传输数据。运输层向应用层屏蔽了下面网络的细节(如网络拓补,所采用的路由选择协议等),它使应用进程之间看起来好像两个运输层实体之间有一条端到端的逻辑通信信道。** +2. **网络层为主机提供逻辑通信,而运输层为应用进程之间提供端到端的逻辑通信。** +3. 运输层的两个重要协议是用户数据报协议 UDP 和传输控制协议 TCP。按照 OSI 的术语,两个对等运输实体在通信时传送的数据单位叫做运输协议数据单元 TPDU(Transport Protocol Data Unit)。但在 TCP/IP 体系中,则根据所使用的协议是 TCP 或 UDP,分别称之为 TCP 报文段或 UDP 用户数据报。 +4. **UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式。 TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务,难以避免地增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。** +5. 硬件端口是不同硬件设备进行交互的接口,而软件端口是应用层各种协议进程与运输实体进行层间交互的一种地址。UDP 和 TCP 的首部格式中都有源端口和目的端口这两个重要字段。当运输层收到 IP 层交上来的运输层报文时,就能够根据其首部中的目的端口号把数据交付应用层的目的应用层。(两个进程之间进行通信不光要知道对方 IP 地址而且要知道对方的端口号(为了找到对方计算机中的应用进程)) +6. 运输层用一个 16 位端口号标志一个端口。端口号只有本地意义,它只是为了标志计算机应用层中的各个进程在和运输层交互时的层间接口。在互联网的不同计算机中,相同的端口号是没有关联的。协议端口号简称端口。虽然通信的终点是应用进程,但只要把所发送的报文交到目的主机的某个合适端口,剩下的工作(最后交付目的进程)就由 TCP 和 UDP 来完成。 +7. 运输层的端口号分为服务器端使用的端口号(0˜1023 指派给熟知端口,1024˜49151 是登记端口号)和客户端暂时使用的端口号(49152˜65535) +8. **UDP 的主要特点是 ① 无连接 ② 尽最大努力交付 ③ 面向报文 ④ 无拥塞控制 ⑤ 支持一对一,一对多,多对一和多对多的交互通信 ⑥ 首部开销小(只有四个字段:源端口,目的端口,长度和检验和)** +9. **TCP 的主要特点是 ① 面向连接 ② 每一条 TCP 连接只能是一对一的 ③ 提供可靠交付 ④ 提供全双工通信 ⑤ 面向字节流** +10. **TCP 用主机的 IP 地址加上主机上的端口号作为 TCP 连接的端点。这样的端点就叫做套接字(socket)或插口。套接字用(IP 地址:端口号)来表示。每一条 TCP 连接唯一地被通信两端的两个端点所确定。** +11. 停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 +12. 为了提高传输效率,发送方可以不使用低效率的停止等待协议,而是采用流水线传输。流水线传输就是发送方可连续发送多个分组,不必每发完一个分组就停下来等待对方确认。这样可使信道上一直有数据不间断的在传送。这种传输方式可以明显提高信道利用率。 +13. 停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求 ARQ。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续 ARQ 协议可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。 +14. TCP 报文段的前 20 个字节是固定的,后面有 4n 字节是根据需要增加的选项。因此,TCP 首部的最小长度是 20 字节。 +15. **TCP 使用滑动窗口机制。发送窗口里面的序号表示允许发送的序号。发送窗口后沿的后面部分表示已发送且已收到确认,而发送窗口前沿的前面部分表示不允许发送。发送窗口后沿的变化情况有两种可能,即不动(没有收到新的确认)和前移(收到了新的确认)。发送窗口的前沿通常是不断向前移动的。一般来说,我们总是希望数据传输更快一些。但如果发送方把数据发送的过快,接收方就可能来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。** +16. 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。 +17. **为了进行拥塞控制,TCP 发送方要维持一个拥塞窗口 cwnd 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。** +18. **TCP 的拥塞控制采用了四种算法,即慢开始,拥塞避免,快重传和快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。** +19. 运输连接的三个阶段,即:连接建立,数据传送和连接释放。 +20. **主动发起 TCP 连接建立的应用进程叫做客户,而被动等待连接建立的应用进程叫做服务器。TCP 连接采用三报文握手机制。服务器要确认用户的连接请求,然后客户要对服务器的确认进行确认。** +21. TCP 的连接释放采用四报文握手机制。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送时,则发送连接释放通知,对方确认后就完全关闭了 TCP 连接 + +### 5.3. 补充(重要) + +以下知识点需要重点关注: + +1. 端口和套接字的意义 +2. UDP 和 TCP 的区别以及两者的应用场景 +3. 在不可靠的网络上实现可靠传输的工作原理,停止等待协议和 ARQ 协议 +4. TCP 的滑动窗口,流量控制,拥塞控制和连接管理 +5. TCP 的三次握手,四次挥手机制 + +## 6. 应用层(Application Layer) + +![应用层](https://img-blog.csdnimg.cn/img_convert/3ff57c0632bc7f4017723b1d1b7d3a52.png) + +### 6.1. 基本术语 + +1. **域名系统(DNS)** :域名系统(DNS,Domain Name System)将人类可读的域名 (例如,www.baidu.com) 转换为机器可读的 IP 地址 (例如,220.181.38.148)。我们可以将其理解为专为互联网设计的电话薄。 + +![](https://img-blog.csdnimg.cn/img_convert/6af26a3293530061785df50e70d53e07.png) + +

https://www.seobility.net/en/wiki/HTTP_headers

+ +2. **文件传输协议(FTP)** :FTP 是 File Transfer Protocol(文件传输协议)的英文简称,而中文简称为“文传协议”。用于 Internet 上的控制文件的双向传输。同时,它也是一个应用程序(Application)。基于不同的操作系统有不同的 FTP 应用程序,而所有这些应用程序都遵守同一种协议以传输文件。在 FTP 的使用当中,用户经常遇到两个概念:"下载"(Download)和"上传"(Upload)。 "下载"文件就是从远程主机拷贝文件至自己的计算机上;"上传"文件就是将文件从自己的计算机中拷贝至远程主机上。用 Internet 语言来说,用户可通过客户机程序向(从)远程主机上传(下载)文件。 + +![FTP工作过程](https://img-blog.csdnimg.cn/img_convert/3f1abf8adba4aa317eca69c489e3db23.png) + +3. **简单文件传输协议(TFTP)** :TFTP(Trivial File Transfer Protocol,简单文件传输协议)是 TCP/IP 协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为 69。 +4. **远程终端协议(TELNET)** :Telnet 协议是 TCP/IP 协议族中的一员,是 Internet 远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用 telnet 程序,用它连接到服务器。终端使用者可以在 telnet 程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个 telnet 会话,必须输入用户名和密码来登录服务器。Telnet 是常用的远程控制 Web 服务器的方法。 +5. **万维网(WWW)** :WWW 是环球信息网的缩写,(亦作“Web”、“WWW”、“'W3'”,英文全称为“World Wide Web”),中文名字为“万维网”,"环球网"等,常简称为 Web。分为 Web 客户端和 Web 服务器程序。WWW 可以让 Web 客户端(常用浏览器)访问浏览 Web 服务器上的页面。是一个由许多互相链接的超文本组成的系统,通过互联网访问。在这个系统中,每个有用的事物,称为一样“资源”;并且由一个全局“统一资源标识符”(URI)标识;这些资源通过超文本传输协议(Hypertext Transfer Protocol)传送给用户,而后者通过点击链接来获得资源。万维网联盟(英语:World Wide Web Consortium,简称 W3C),又称 W3C 理事会。1994 年 10 月在麻省理工学院(MIT)计算机科学实验室成立。万维网联盟的创建者是万维网的发明者蒂姆·伯纳斯-李。万维网并不等同互联网,万维网只是互联网所能提供的服务其中之一,是靠着互联网运行的一项服务。 +6. **万维网的大致工作工程:** + +![万维网的大致工作工程](https://img-blog.csdnimg.cn/img_convert/735f55501e81898aa61b8032f7dbcb73.png) + +7. **统一资源定位符(URL)** :统一资源定位符是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的 URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。 +8. **超文本传输协议(HTTP)** :超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。1960 年美国人 Ted Nelson 构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext),这成为了 HTTP 超文本传输协议标准架构的发展根基。 + +HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格式。HTTP 的原理如下图所示: + +![](https://img-blog.csdnimg.cn/img_convert/b273efef5f2388e26414135672b00295.png) + +10. **代理服务器(Proxy Server)** : 代理服务器(Proxy Server)是一种网络实体,它又称为万维网高速缓存。 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时,若代理服务器发现这个请求与暂时存放的的请求相同,就返回暂存的响应,而不需要按 URL 的地址再次去互联网访问该资源。代理服务器可在客户端或服务器工作,也可以在中间系统工作。 +11. **简单邮件传输协议(SMTP)** : SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。 通过 SMTP 协议所指定的服务器,就可以把 E-mail 寄到收信人的服务器上了,整个过程只要几分钟。SMTP 服务器则是遵循 SMTP 协议的发送邮件服务器,用来发送或中转发出的电子邮件。 + +![一个电子邮件被发送的过程](https://img-blog.csdnimg.cn/img_convert/b16da4d4fea63de5fce53f54973967d7.png) + +

https://www.campaignmonitor.com/resources/knowledge-base/what-is-the-code-that-makes-bcc-or-cc-operate-in-an-email/

+ +11. **搜索引擎** :搜索引擎(Search Engine)是指根据一定的策略、运用特定的计算机程序从互联网上搜集信息,在对信息进行组织和处理后,为用户提供检索服务,将用户检索相关的信息展示给用户的系统。搜索引擎包括全文索引、目录索引、元搜索引擎、垂直搜索引擎、集合式搜索引擎、门户搜索引擎与免费链接列表等。 + +![搜索引擎](https://img-blog.csdnimg.cn/img_convert/68fe865a9d87de361c45f4a42d624035.png) + +12. **垂直搜索引擎** :垂直搜索引擎是针对某一个行业的专业搜索引擎,是搜索引擎的细分和延伸,是对网页库中的某类专门的信息进行一次整合,定向分字段抽取出需要的数据进行处理后再以某种形式返回给用户。垂直搜索是相对通用搜索引擎的信息量大、查询不准确、深度不够等提出来的新的搜索引擎服务模式,通过针对某一特定领域、某一特定人群或某一特定需求提供的有一定价值的信息和相关服务。其特点就是“专、精、深”,且具有行业色彩,相比较通用搜索引擎的海量信息无序化,垂直搜索引擎则显得更加专注、具体和深入。 +13. **全文索引** :全文索引技术是目前搜索引擎的关键技术。试想在 1M 大小的文件中搜索一个词,可能需要几秒,在 100M 的文件中可能需要几十秒,如果在更大的文件中搜索那么就需要更大的系统开销,这样的开销是不现实的。所以在这样的矛盾下出现了全文索引技术,有时候有人叫倒排文档技术。 +14. **目录索引** :目录索引( search index/directory),顾名思义就是将网站分门别类地存放在相应的目录中,因此用户在查询信息时,可选择关键词搜索,也可按分类目录逐层查找。 + +### 6.2. 重要知识点总结 + +1. 文件传输协议(FTP)使用 TCP 可靠的运输服务。FTP 使用客户服务器方式。一个 FTP 服务器进程可以同时为多个用户提供服务。在进行文件传输时,FTP 的客户和服务器之间要先建立两个并行的 TCP 连接:控制连接和数据连接。实际用于传输文件的是数据连接。 +2. 万维网客户程序与服务器之间进行交互使用的协议是超文本传输协议 HTTP。HTTP 使用 TCP 连接进行可靠传输。但 HTTP 本身是无连接、无状态的。HTTP/1.1 协议使用了持续连接(分为非流水线方式和流水线方式) +3. 电子邮件把邮件发送到收件人使用的邮件服务器,并放在其中的收件人邮箱中,收件人可随时上网到自己使用的邮件服务器读取,相当于电子邮箱。 +4. 一个电子邮件系统有三个重要组成构件:用户代理、邮件服务器、邮件协议(包括邮件发送协议,如 SMTP,和邮件读取协议,如 POP3 和 IMAP)。用户代理和邮件服务器都要运行这些协议。 + +### 6.3. 补充(重要) + +以下知识点需要重点关注: + +1. 应用层的常见协议(重点关注 HTTP 协议) +2. 域名系统-从域名解析出 IP 地址 +3. 访问一个网站大致的过程 +4. 系统调用和应用编程接口概念 diff --git a/docs/cs-basics/operating-system/images/Linux-Logo.png b/docs/cs-basics/operating-system/images/Linux-Logo.png new file mode 100644 index 00000000000..40e75aaac8a Binary files /dev/null and b/docs/cs-basics/operating-system/images/Linux-Logo.png differ diff --git "a/docs/cs-basics/operating-system/images/Linux\344\271\213\347\210\266.png" "b/docs/cs-basics/operating-system/images/Linux\344\271\213\347\210\266.png" new file mode 100644 index 00000000000..33145373b68 Binary files /dev/null and "b/docs/cs-basics/operating-system/images/Linux\344\271\213\347\210\266.png" differ diff --git "a/docs/cs-basics/operating-system/images/Linux\346\235\203\351\231\220\345\221\275\344\273\244.png" "b/docs/cs-basics/operating-system/images/Linux\346\235\203\351\231\220\345\221\275\344\273\244.png" new file mode 100644 index 00000000000..f59b2e638c6 Binary files /dev/null and "b/docs/cs-basics/operating-system/images/Linux\346\235\203\351\231\220\345\221\275\344\273\244.png" differ diff --git "a/docs/cs-basics/operating-system/images/Linux\346\235\203\351\231\220\350\247\243\350\257\273.png" "b/docs/cs-basics/operating-system/images/Linux\346\235\203\351\231\220\350\247\243\350\257\273.png" new file mode 100644 index 00000000000..1292c125aca Binary files /dev/null and "b/docs/cs-basics/operating-system/images/Linux\346\235\203\351\231\220\350\247\243\350\257\273.png" differ diff --git "a/docs/cs-basics/operating-system/images/Linux\347\233\256\345\275\225\346\240\221.png" "b/docs/cs-basics/operating-system/images/Linux\347\233\256\345\275\225\346\240\221.png" new file mode 100644 index 00000000000..beef42034bf Binary files /dev/null and "b/docs/cs-basics/operating-system/images/Linux\347\233\256\345\275\225\346\240\221.png" differ diff --git a/docs/cs-basics/operating-system/images/linux.png b/docs/cs-basics/operating-system/images/linux.png new file mode 100644 index 00000000000..20ead246915 Binary files /dev/null and b/docs/cs-basics/operating-system/images/linux.png differ diff --git a/docs/cs-basics/operating-system/images/macos.png b/docs/cs-basics/operating-system/images/macos.png new file mode 100644 index 00000000000..332945774ee Binary files /dev/null and b/docs/cs-basics/operating-system/images/macos.png differ diff --git a/docs/cs-basics/operating-system/images/unix.png b/docs/cs-basics/operating-system/images/unix.png new file mode 100644 index 00000000000..0afabcd8621 Binary files /dev/null and b/docs/cs-basics/operating-system/images/unix.png differ diff --git a/docs/cs-basics/operating-system/images/windows.png b/docs/cs-basics/operating-system/images/windows.png new file mode 100644 index 00000000000..c2687dc72f7 Binary files /dev/null and b/docs/cs-basics/operating-system/images/windows.png differ diff --git "a/docs/cs-basics/operating-system/images/\344\277\256\346\224\271\346\226\207\344\273\266\346\235\203\351\231\220.png" "b/docs/cs-basics/operating-system/images/\344\277\256\346\224\271\346\226\207\344\273\266\346\235\203\351\231\220.png" new file mode 100644 index 00000000000..de9409410c8 Binary files /dev/null and "b/docs/cs-basics/operating-system/images/\344\277\256\346\224\271\346\226\207\344\273\266\346\235\203\351\231\220.png" differ diff --git "a/docs/cs-basics/operating-system/images/\346\226\207\344\273\266inode\344\277\241\346\201\257.png" "b/docs/cs-basics/operating-system/images/\346\226\207\344\273\266inode\344\277\241\346\201\257.png" new file mode 100644 index 00000000000..b47551e8314 Binary files /dev/null and "b/docs/cs-basics/operating-system/images/\346\226\207\344\273\266inode\344\277\241\346\201\257.png" differ diff --git "a/docs/cs-basics/operating-system/images/\347\224\250\346\210\267\346\200\201\344\270\216\345\206\205\346\240\270\346\200\201.png" "b/docs/cs-basics/operating-system/images/\347\224\250\346\210\267\346\200\201\344\270\216\345\206\205\346\240\270\346\200\201.png" new file mode 100644 index 00000000000..aa0dafc2f02 Binary files /dev/null and "b/docs/cs-basics/operating-system/images/\347\224\250\346\210\267\346\200\201\344\270\216\345\206\205\346\240\270\346\200\201.png" differ diff --git a/docs/cs-basics/operating-system/linux-intro.md b/docs/cs-basics/operating-system/linux-intro.md new file mode 100644 index 00000000000..723cd9b7e88 --- /dev/null +++ b/docs/cs-basics/operating-system/linux-intro.md @@ -0,0 +1,412 @@ +--- +title: 后端程序员必备的 Linux 基础知识总结 +category: 计算机基础 +tag: + - 操作系统 + - Linux +--- + +简单介绍一下 Java 程序员必知的 Linux 的一些概念以及常见命令。 + +_如果文章有任何需要改善和完善的地方,欢迎在评论区指出,共同进步!笔芯!_ + +## 1. 从认识操作系统开始 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/image-20200807161118901.png) + +正式开始 Linux 之前,简单花一点点篇幅科普一下操作系统相关的内容。 + +### 1.1. 操作系统简介 + +我通过以下四点介绍什么是操作系统: + +1. **操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机的基石。** +2. **操作系统本质上是一个运行在计算机上的软件程序 ,用于管理计算机硬件和软件资源。** 举例:运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。 +3. **操作系统存在屏蔽了硬件层的复杂性。** 操作系统就像是硬件使用的负责人,统筹着各种相关事项。 +4. **操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理**。 + +> 内核(Kernel)在后文中会提到。 + +![Kernel_Layout](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/Kernel_Layout.png) + +### 1.2. 操作系统简单分类 + +#### 1.2.1. Windows + +目前最流行的个人桌面操作系统 ,不做多的介绍,大家都清楚。界面简单易操作,软件生态非常好。 + +_玩玩电脑游戏还是必须要有 Windows 的,所以我现在是一台 Windows 用于玩游戏,一台 Mac 用于平时日常开发和学习使用。_ + +![windows](images/windows.png) + +#### 1.2.2. Unix + +最早的多用户、多任务操作系统 。后面崛起的 Linux 在很多方面都参考了 Unix。 + +目前这款操作系统已经逐渐逐渐退出操作系统的舞台。 + +![unix](images/unix.png) + +#### 1.2.3. Linux + +**Linux 是一套免费使用、开源的类 Unix 操作系统。** Linux 存在着许多不同的发行版本,但它们都使用了 **Linux 内核** 。 + +> 严格来讲,Linux 这个词本身只表示 Linux 内核,在 GNU/Linux 系统中,Linux 实际就是 Linux 内核,而该系统的其余部分主要是由 GNU 工程编写和提供的程序组成。单独的 Linux 内核并不能成为一个可以正常工作的操作系统。 +> +> **很多人更倾向使用 “GNU/Linux” 一词来表达人们通常所说的 “Linux”。** + +![linux](images/linux.png) + +#### 1.2.4. Mac OS + +苹果自家的操作系统,编程体验和 Linux 相当,但是界面、软件生态以及用户体验各方面都要比 Linux 操作系统更好。 + +![macos](images/macos.png) + +### 1.3. 操作系统的内核(Kernel) + +我们先来看看维基百科对于内核的解释,我觉得总结的非常好! + +> **内核**(英语:Kernel,又称核心)在计算机科学中是一个用来管理软件发出的数据 I/O(输入与输出)要求的电脑程序,将这些要求转译为数据处理的指令并交由中央处理器(CPU)及电脑中其他电子组件进行处理,是现代操作系统中最基本的部分。它是为众多应用程序提供对计算机硬件的安全访问的一部分软件,这种访问是有限的,并由内核决定一个程序在什么时候对某部分硬件操作多长时间。 **直接对硬件操作是非常复杂的。所以内核通常提供一种硬件抽象的方法,来完成这些操作。有了这个,通过进程间通信机制及系统调用,应用进程可间接控制所需的硬件资源(特别是处理器及 IO 设备)。** +> +> 早期计算机系统的设计中,还没有操作系统的内核这个概念。随着计算机系统的发展,操作系统内核的概念才渐渐明晰起来了! + +简单概括两点: + +1. **操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理。** +2. **操作系统的内核是连接应用程序和硬件的桥梁,决定着操作系统的性能和稳定性。** + +### 1.4. 中央处理器(CPU,Central Processing Unit) + +关于 CPU 简单概括三点: + +1. **CPU 是一台计算机的运算核心(Core)+控制核心( Control Unit),可以称得上是计算机的大脑。** +2. **CPU 主要包括两个部分:控制器+运算器。** +3. **CPU 的根本任务就是执行指令,对计算机来说最终都是一串由“0”和“1”组成的序列。** + +### 1.5. CPU vs Kernel(内核) + +很多人容易无法区分操作系统的内核(Kernel)和中央处理器(CPU),你可以简单从下面两点来区别: + +1. 操作系统的内核(Kernel)属于操作系统层面,而 CPU 属于硬件。 +2. CPU 主要提供运算,处理各种指令的能力。内核(Kernel)主要负责系统管理比如内存管理,它屏蔽了对硬件的操作。 + +下图清晰说明了应用程序、内核、CPU 这三者的关系。 + +![Kernel_Layout](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/Kernel_Layout.png) + +### 1.6. 系统调用 + +介绍系统调用之前,我们先来了解一下用户态和系统态。 + +根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别: + +1. **用户态(user mode)** : 用户态运行的进程或可以直接读取用户程序的数据。 +2. **系统态(kernel mode)**: 可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。 + +**说了用户态和系统态之后,那么什么是系统调用呢?** + +我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的系统态级别的子功能咋办呢?那就需要系统调用了! + +也就是说在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。 + +这些系统调用按功能大致可分为如下几类: + +- **设备管理** :完成设备的请求或释放,以及设备启动等功能。 +- **文件管理** :完成文件的读、写、创建及删除等功能。 +- **进程控制** :完成进程的创建、撤销、阻塞及唤醒等功能。 +- **进程通信** :完成进程之间的消息传递或信号传递等功能。 +- **内存管理** :完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。 + +我在网上找了一个图,通过这个图可以很清晰的说明用户程序、系统调用、内核和硬件之间的关系。(_太难了~木有自己画_) + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/L181kk2Eou-compress.jpg) + +## 2. 初探 Linux + +### 2.1. Linux 简介 + +我们上面已经简单了 Linux,这里只强调三点。 + +- **类 Unix 系统** : Linux 是一种自由、开放源码的类似 Unix 的操作系统 +- **Linux 本质是指 Linux 内核** : 严格来讲,Linux 这个词本身只表示 Linux 内核,单独的 Linux 内核并不能成为一个可以正常工作的操作系统。所以,就有了各种 Linux 发行版。 +- **Linux 之父(林纳斯·本纳第克特·托瓦兹 Linus Benedict Torvalds)** : 一个编程领域的传奇式人物,真大佬!我辈崇拜敬仰之楷模。他是 **Linux 内核** 的最早作者,随后发起了这个开源项目,担任 Linux 内核的首要架构师。他还发起了 Git 这个开源项目,并为主要的开发者。 + +![Linux](images/Linux之父.png) + +### 2.2. Linux 诞生 + +1989 年,Linus Torvalds 进入芬兰陆军新地区旅,服 11 个月的国家义务兵役,军衔为少尉,主要服务于计算机部门,任务是弹道计算。服役期间,购买了安德鲁·斯图尔特·塔能鲍姆所著的教科书及 minix 源代码,开始研究操作系统。1990 年,他退伍后回到大学,开始接触 Unix。 + +> **Minix** 是一个迷你版本的类 Unix 操作系统,由塔能鲍姆教授为了教学之用而创作,采用微核心设计。它启发了 Linux 内核的创作。 + +1991 年,Linus Torvalds 开源了 Linux 内核。Linux 以一只可爱的企鹅作为标志,象征着敢作敢为、热爱生活。 + +![OPINION: Make the switch to a Linux operating system | Opinion ...](images/Linux-Logo.png) + +### 2.3. 常见 Linux 发行版本有哪些? + +Linus Torvalds 开源的只是 Linux 内核,我们上面也提到了操作系统内核的作用。一些组织或厂商将 Linux 内核与各种软件和文档包装起来,并提供系统安装界面和系统配置、设定与管理工具,就构成了 Linux 的发行版本。 + +> 内核主要负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理。 + +Linux 的发行版本可以大体分为两类: + +- 商业公司维护的发行版本,以著名的 Red Hat 为代表,比较典型的有 CentOS 。 +- 社区组织维护的发行版本,以 Debian 为代表,比较典型的有 Ubuntu、Debian。 + +对于初学者学习 Linux ,推荐选择 CentOS 。 + +## 3. Linux 文件系统概览 + +### 3.1. Linux 文件系统简介 + +**在 Linux 操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文件或是目录都被看作是一个文件。** 也就是说在 Linux 系统中有一个重要的概念:**一切都是文件**。 + +其实这是 UNIX 哲学的一个体现,在 UNIX 系统中,把一切资源都看作是文件,Linux 的文件系统也是借鉴 UNIX 文件系统而来。 + +### 3.2. inode 介绍 + +**inode 是 linux/unix 文件系统的基础。那么,inode 是什么?有什么作用呢?** + +硬盘的最小存储单位是扇区(Sector),块(block)由多个扇区组成。文件数据存储在块中。块的最常见的大小是 4kb,约为 8 个连续的扇区组成(每个扇区存储 512 字节)。一个文件可能会占用多个 block,但是一个块只能存放一个文件。 + +虽然,我们将文件存储在了块(block)中,但是我们还需要一个空间来存储文件的 **元信息 metadata** :如某个文件被分成几块、每一块在的地址、文件拥有者,创建时间,权限,大小等。这种 **存储文件元信息的区域就叫 inode**,译为索引节点:**i(index)+node**。 每个文件都有一个 inode,存储文件的元信息。 + +可以使用 `stat` 命令可以查看文件的 inode 信息。每个 inode 都有一个号码,Linux/Unix 操作系统不使用文件名来区分文件,而是使用 inode 号码区分不同的文件。 + +简单来说:inode 就是用来维护某个文件被分成几块、每一块在的地址、文件拥有者,创建时间,权限,大小等信息。 + +简单总结一下: + +- **inode** :记录文件的属性信息,可以使用 stat 命令查看 inode 信息。 +- **block** :实际文件的内容,如果一个文件大于一个块时候,那么将占用多个 block,但是一个块只能存放一个文件。(因为数据是由 inode 指向的,如果有两个文件的数据存放在同一个块中,就会乱套了) + +![文件inode信息](images/文件inode信息.png) + +### 3.3. Linux 文件类型 + +Linux 支持很多文件类型,其中非常重要的文件类型有: **普通文件**,**目录文件**,**链接文件**,**设备文件**,**管道文件**,**Socket 套接字文件**等。 + +- **普通文件(-)** : 用于存储信息和数据, Linux 用户可以根据访问权限对普通文件进行查看、更改和删除。比如:图片、声音、PDF、text、视频、源代码等等。 +- **目录文件(d,directory file)** :目录也是文件的一种,用于表示和管理系统中的文件,目录文件中包含一些文件名和子目录名。打开目录事实上就是打开目录文件。 +- **符号链接文件(l,symbolic link)** :保留了指向文件的地址而不是文件本身。 +- **字符设备(c,char)** :用来访问字符设备比如键盘。 +- **设备文件(b,block)** : 用来访问块设备比如硬盘、软盘。 +- **管道文件(p,pipe)** : 一种特殊类型的文件,用于进程之间的通信。 +- **套接字(s,socket)** :用于进程间的网络通信,也可以用于本机之间的非网络通信。 + +### 3.4. Linux 目录树 + +所有可操作的计算机资源都存在于目录树这个结构中,对计算资源的访问,可以看做是对这棵目录树的访问。 + +**Linux 的目录结构如下:** + +Linux 文件系统的结构层次鲜明,就像一棵倒立的树,最顶层是其根目录: +![Linux的目录结构](images/Linux目录树.png) + +**常见目录说明:** + +- **/bin:** 存放二进制可执行文件(ls、cat、mkdir 等),常用命令一般都在这里; +- **/etc:** 存放系统管理和配置文件; +- **/home:** 存放所有用户文件的根目录,是用户主目录的基点,比如用户 user 的主目录就是/home/user,可以用~user 表示; +- **/usr :** 用于存放系统应用程序; +- **/opt:** 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把 tomcat 等都安装到这里; +- **/proc:** 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息; +- **/root:** 超级用户(系统管理员)的主目录(特权阶级^o^); +- **/sbin:** 存放二进制可执行文件,只有 root 才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如 ifconfig 等; +- **/dev:** 用于存放设备文件; +- **/mnt:** 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统; +- **/boot:** 存放用于系统引导时使用的各种文件; +- **/lib :** 存放着和系统运行相关的库文件 ; +- **/tmp:** 用于存放各种临时文件,是公用的临时文件存储点; +- **/var:** 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启动日志等。)等; +- **/lost+found:** 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows 下叫什么.chk)就在这里。 + +## 4. Linux 基本命令 + +下面只是给出了一些比较常用的命令。推荐一个 Linux 命令快查网站,非常不错,大家如果遗忘某些命令或者对某些命令不理解都可以在这里得到解决。 + +Linux 命令大全:[http://man.linuxde.net/](http://man.linuxde.net/) + +### 4.1. 目录切换命令 + +- **`cd usr`:** 切换到该目录下 usr 目录 +- **`cd ..(或cd../)`:** 切换到上一层目录 +- **`cd /`:** 切换到系统根目录 +- **`cd ~`:** 切换到用户主目录 +- **`cd -`:** 切换到上一个操作所在目录 + +### 4.2. 目录的操作命令(增删改查) + +- **`mkdir 目录名称`:** 增加目录。 +- **`ls/ll`**(ll 是 ls -l 的别名,ll 命令可以看到该目录下的所有目录和文件的详细信息):查看目录信息。 +- **`find 目录 参数`:** 寻找目录(查)。示例:① 列出当前目录及子目录下所有文件和文件夹: `find .`;② 在`/home`目录下查找以.txt 结尾的文件名:`find /home -name "*.txt"` ,忽略大小写: `find /home -iname "*.txt"` ;③ 当前目录及子目录下查找所有以.txt 和.pdf 结尾的文件:`find . \( -name "*.txt" -o -name "*.pdf" \)`或`find . -name "*.txt" -o -name "*.pdf"`。 +- **`mv 目录名称 新目录名称`:** 修改目录的名称(改)。注意:mv 的语法不仅可以对目录进行重命名而且也可以对各种文件,压缩包等进行 重命名的操作。mv 命令用来对文件或目录重新命名,或者将文件从一个目录移到另一个目录中。后面会介绍到 mv 命令的另一个用法。 +- **`mv 目录名称 目录的新位置`:** 移动目录的位置---剪切(改)。注意:mv 语法不仅可以对目录进行剪切操作,对文件和压缩包等都可执行剪切操作。另外 mv 与 cp 的结果不同,mv 好像文件“搬家”,文件个数并未增加。而 cp 对文件进行复制,文件个数增加了。 +- **`cp -r 目录名称 目录拷贝的目标位置`:** 拷贝目录(改),-r 代表递归拷贝 。注意:cp 命令不仅可以拷贝目录还可以拷贝文件,压缩包等,拷贝文件和压缩包时不 用写-r 递归。 +- **`rm [-rf] 目录` :** 删除目录(删)。注意:rm 不仅可以删除目录,也可以删除其他文件或压缩包,为了增强大家的记忆, 无论删除任何目录或文件,都直接使用`rm -rf` 目录/文件/压缩包。 + +### 4.3. 文件的操作命令(增删改查) + +- **`touch 文件名称`:** 文件的创建(增)。 +- **`cat/more/less/tail 文件名称`** :文件的查看(查) 。命令 `tail -f 文件` 可以对某个文件进行动态监控,例如 tomcat 的日志文件, 会随着程序的运行,日志会变化,可以使用 `tail -f catalina-2016-11-11.log` 监控 文 件的变化 。 +- **`vim 文件`:** 修改文件的内容(改)。vim 编辑器是 Linux 中的强大组件,是 vi 编辑器的加强版,vim 编辑器的命令和快捷方式有很多,但此处不一一阐述,大家也无需研究的很透彻,使用 vim 编辑修改文件的方式基本会使用就可以了。在实际开发中,使用 vim 编辑器主要作用就是修改配置文件,下面是一般步骤: `vim 文件------>进入文件----->命令模式------>按i进入编辑模式----->编辑文件 ------->按Esc进入底行模式----->输入:wq/q!` (输入 wq 代表写入内容并退出,即保存;输入 q!代表强制退出不保存)。 +- **`rm -rf 文件`:** 删除文件(删)。 + +### 4.4. 压缩文件的操作命令 + +**1)打包并压缩文件:** + +Linux 中的打包文件一般是以.tar 结尾的,压缩的命令一般是以.gz 结尾的。而一般情况下打包和压缩是一起进行的,打包并压缩后的文件的后缀名一般.tar.gz。 +命令:`tar -zcvf 打包压缩后的文件名 要打包压缩的文件` ,其中: + +- z:调用 gzip 压缩命令进行压缩 +- c:打包文件 +- v:显示运行过程 +- f:指定文件名 + +比如:假如 test 目录下有三个文件分别是:aaa.txt bbb.txt ccc.txt,如果我们要打包 test 目录并指定压缩后的压缩包名称为 test.tar.gz 可以使用命令:**`tar -zcvf test.tar.gz aaa.txt bbb.txt ccc.txt` 或 `tar -zcvf test.tar.gz /test/`** + +**2)解压压缩包:** + +命令:`tar [-xvf] 压缩文件` + +其中:x:代表解压 + +示例: + +- 将 /test 下的 test.tar.gz 解压到当前目录下可以使用命令:**`tar -xvf test.tar.gz`** +- 将 /test 下的 test.tar.gz 解压到根目录/usr 下:**`tar -xvf test.tar.gz -C /usr`**(- C 代表指定解压的位置) + +### 4.5. Linux 的权限命令 + +操作系统中每个文件都拥有特定的权限、所属用户和所属组。权限是操作系统用来限制资源访问的机制,在 Linux 中权限一般分为读(readable)、写(writable)和执行(excutable),分为三组。分别对应文件的属主(owner),属组(group)和其他用户(other),通过这样的机制来限制哪些用户、哪些组可以对特定的文件进行什么样的操作。 + +通过 **`ls -l`** 命令我们可以 查看某个目录下的文件或目录的权限 + +示例:在随意某个目录下`ls -l` + +![](images/Linux权限命令.png) + +第一列的内容的信息解释如下: + +![](images/Linux权限解读.png) + +> 下面将详细讲解文件的类型、Linux 中权限以及文件有所有者、所在组、其它组具体是什么? + +**文件的类型:** + +- d: 代表目录 +- -: 代表文件 +- l: 代表软链接(可以认为是 window 中的快捷方式) + +**Linux 中权限分为以下几种:** + +- r:代表权限是可读,r 也可以用数字 4 表示 +- w:代表权限是可写,w 也可以用数字 2 表示 +- x:代表权限是可执行,x 也可以用数字 1 表示 + +**文件和目录权限的区别:** + +对文件和目录而言,读写执行表示不同的意义。 + +对于文件: + +| 权限名称 | 可执行操作 | +| :------- | --------------------------: | +| r | 可以使用 cat 查看文件的内容 | +| w | 可以修改文件的内容 | +| x | 可以将其运行为二进制文件 | + +对于目录: + +| 权限名称 | 可执行操作 | +| :------- | -----------------------: | +| r | 可以查看目录下列表 | +| w | 可以创建和删除目录下文件 | +| x | 可以使用 cd 进入目录 | + +需要注意的是: **超级用户可以无视普通用户的权限,即使文件目录权限是 000,依旧可以访问。** + +**在 linux 中的每个用户必须属于一个组,不能独立于组外。在 linux 中每个文件有所有者、所在组、其它组的概念。** + +- **所有者(u)** :一般为文件的创建者,谁创建了该文件,就天然的成为该文件的所有者,用 `ls ‐ahl` 命令可以看到文件的所有者 也可以使用 chown 用户名 文件名来修改文件的所有者 。 +- **文件所在组(g)** :当某个用户创建了一个文件后,这个文件的所在组就是该用户所在的组用 `ls ‐ahl`命令可以看到文件的所有组也可以使用 chgrp 组名 文件名来修改文件所在的组。 +- **其它组(o)** :除开文件的所有者和所在组的用户外,系统的其它用户都是文件的其它组。 + +> 我们再来看看如何修改文件/目录的权限。 + +**修改文件/目录的权限的命令:`chmod`** + +示例:修改/test 下的 aaa.txt 的权限为文件所有者有全部权限,文件所有者所在的组有读写权限,其他用户只有读的权限。 + +**`chmod u=rwx,g=rw,o=r aaa.txt`** 或者 **`chmod 764 aaa.txt`** + +![](images/修改文件权限.png) + +**补充一个比较常用的东西:** + +假如我们装了一个 zookeeper,我们每次开机到要求其自动启动该怎么办? + +1. 新建一个脚本 zookeeper +2. 为新建的脚本 zookeeper 添加可执行权限,命令是:`chmod +x zookeeper` +3. 把 zookeeper 这个脚本添加到开机启动项里面,命令是:`chkconfig --add zookeeper` +4. 如果想看看是否添加成功,命令是:`chkconfig --list` + +### 4.6. Linux 用户管理 + +Linux 系统是一个多用户多任务的分时操作系统,任何一个要使用系统资源的用户,都必须首先向系统管理员申请一个账号,然后以这个账号的身份进入系统。 + +用户的账号一方面可以帮助系统管理员对使用系统的用户进行跟踪,并控制他们对系统资源的访问;另一方面也可以帮助用户组织文件,并为用户提供安全性保护。 + +**Linux 用户管理相关命令:** + +- `useradd 选项 用户名`:添加用户账号 +- `userdel 选项 用户名`:删除用户帐号 +- `usermod 选项 用户名`:修改帐号 +- `passwd 用户名`:更改或创建用户的密码 +- `passwd -S 用户名` :显示用户账号密码信息 +- `passwd -d 用户名`: 清除用户密码 + +`useradd` 命令用于 Linux 中创建的新的系统用户。`useradd`可用来建立用户帐号。帐号建好之后,再用`passwd`设定帐号的密码.而可用`userdel`删除帐号。使用`useradd`指令所建立的帐号,实际上是保存在 `/etc/passwd`文本文件中。 + +`passwd`命令用于设置用户的认证信息,包括用户密码、密码过期时间等。系统管理者则能用它管理系统用户的密码。只有管理者可以指定用户名称,一般用户只能变更自己的密码。 + +### 4.7. Linux 系统用户组的管理 + +每个用户都有一个用户组,系统可以对一个用户组中的所有用户进行集中管理。不同 Linux 系统对用户组的规定有所不同,如 Linux 下的用户属于与它同名的用户组,这个用户组在创建用户时同时创建。 + +用户组的管理涉及用户组的添加、删除和修改。组的增加、删除和修改实际上就是对`/etc/group`文件的更新。 + +**Linux 系统用户组的管理相关命令:** + +- `groupadd 选项 用户组` :增加一个新的用户组 +- `groupdel 用户组`:要删除一个已有的用户组 +- `groupmod 选项 用户组` : 修改用户组的属性 + +### 4.8. 其他常用命令 + +- **`pwd`:** 显示当前所在位置 + +- `sudo + 其他命令`:以系统管理者的身份执行指令,也就是说,经由 sudo 所执行的指令就好像是 root 亲自执行。 + +- **`grep 要搜索的字符串 要搜索的文件 --color`:** 搜索命令,--color 代表高亮显示 + +- **`ps -ef`/`ps -aux`:** 这两个命令都是查看当前系统正在运行进程,两者的区别是展示格式不同。如果想要查看特定的进程可以使用这样的格式:**`ps aux|grep redis`** (查看包括 redis 字符串的进程),也可使用 `pgrep redis -a`。 + + 注意:如果直接用 ps((Process Status))命令,会显示所有进程的状态,通常结合 grep 命令查看某进程的状态。 + +- **`kill -9 进程的pid`:** 杀死进程(-9 表示强制终止。) + + 先用 ps 查找进程,然后用 kill 杀掉 + +- **网络通信命令:** + - 查看当前系统的网卡信息:ifconfig + - 查看与某台机器的连接情况:ping + - 查看当前系统的端口使用:netstat -an +- **net-tools 和 iproute2 :** + `net-tools`起源于 BSD 的 TCP/IP 工具箱,后来成为老版本 LinuxLinux 中配置网络功能的工具。但自 2001 年起,Linux 社区已经对其停止维护。同时,一些 Linux 发行版比如 Arch Linux 和 CentOS/RHEL 7 则已经完全抛弃了 net-tools,只支持`iproute2`。linux ip 命令类似于 ifconfig,但功能更强大,旨在替代它。更多详情请阅读[如何在 Linux 中使用 IP 命令和示例](https://linoxide.com/linux-command/use-ip-command-linux) +- **`shutdown`:** `shutdown -h now`: 指定现在立即关机;`shutdown +5 "System will shutdown after 5 minutes"`:指定 5 分钟后关机,同时送出警告信息给登入用户。 + +- **`reboot`:** **`reboot`:** 重开机。**`reboot -w`:** 做个重开机的模拟(只有纪录并不会真的重开机)。 diff --git a/docs/operating-system/Shell.md b/docs/cs-basics/operating-system/shell-intro.md similarity index 78% rename from docs/operating-system/Shell.md rename to docs/cs-basics/operating-system/shell-intro.md index 4a89061fc27..074f7bfbe55 100644 --- a/docs/operating-system/Shell.md +++ b/docs/cs-basics/operating-system/shell-intro.md @@ -1,33 +1,10 @@ - - - -- [Shell 编程入门](#shell-编程入门) - - [走进 Shell 编程的大门](#走进-shell-编程的大门) - - [为什么要学Shell?](#为什么要学shell) - - [什么是 Shell?](#什么是-shell) - - [Shell 编程的 Hello World](#shell-编程的-hello-world) - - [Shell 变量](#shell-变量) - - [Shell 编程中的变量介绍](#shell-编程中的变量介绍) - - [Shell 字符串入门](#shell-字符串入门) - - [Shell 字符串常见操作](#shell-字符串常见操作) - - [Shell 数组](#shell-数组) - - [Shell 基本运算符](#shell-基本运算符) - - [算数运算符](#算数运算符) - - [关系运算符](#关系运算符) - - [逻辑运算符](#逻辑运算符) - - [布尔运算符](#布尔运算符) - - [字符串运算符](#字符串运算符) - - [文件相关运算符](#文件相关运算符) - - [shell流程控制](#shell流程控制) - - [if 条件语句](#if-条件语句) - - [for 循环语句](#for-循环语句) - - [while 语句](#while-语句) - - [shell 函数](#shell-函数) - - [不带参数没有返回值的函数](#不带参数没有返回值的函数) - - [有返回值的函数](#有返回值的函数) - - [带参数的函数](#带参数的函数) - - +--- +title: Shell 编程入门 +category: 计算机基础 +tag: + - 操作系统 + - Linux +--- # Shell 编程入门 @@ -45,20 +22,19 @@ 另外,了解 shell 编程也是大部分互联网公司招聘后端开发人员的要求。下图是我截取的一些知名互联网公司对于 Shell 编程的要求。 -![大型互联网公司对于shell编程技能的要求](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-16/60190220.jpg) +![大型互联网公司对于shell编程技能的要求](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-16/60190220.jpg) ### 什么是 Shell? 简单来说“Shell编程就是对一堆Linux命令的逻辑化处理”。 - W3Cschool 上的一篇文章是这样介绍 Shell的,如下图所示。 -![什么是 Shell?](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-26/19456505.jpg) +![什么是 Shell?](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-26/19456505.jpg) ### Shell 编程的 Hello World -学习任何一门编程语言第一件事就是输出HelloWord了!下面我会从新建文件到shell代码编写来说下Shell 编程如何输出Hello World。 +学习任何一门编程语言第一件事就是输出HelloWorld了!下面我会从新建文件到shell代码编写来说下Shell 编程如何输出Hello World。 (1)新建一个文件 helloworld.sh :`touch helloworld.sh`,扩展名为 sh(sh代表Shell)(扩展名并不影响脚本执行,见名知意就好,如果你用 php 写 shell 脚本,扩展名就用 php 好了) @@ -80,7 +56,7 @@ shell中 # 符号表示注释。**shell 的第一行比较特殊,一般都会 (4) 运行脚本:`./helloworld.sh` 。(注意,一定要写成 `./helloworld.sh` ,而不是 `helloworld.sh` ,运行其它二进制的程序也一样,直接写 `helloworld.sh` ,linux 系统会去 PATH 里寻找有没有叫 helloworld.sh 的,而只有 /bin, /sbin, /usr/bin,/usr/sbin 等在 PATH 里,你的当前目录通常不在 PATH 里,所以写成 `helloworld.sh` 是会找不到命令的,要用`./helloworld.sh` 告诉系统说,就在当前目录找。) -![shell 编程Hello World](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-16/55296212.jpg) +![shell 编程Hello World](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-16/55296212.jpg) ## Shell 变量 @@ -91,18 +67,18 @@ shell中 # 符号表示注释。**shell 的第一行比较特殊,一般都会 **Shell编程中一般分为三种变量:** 1. **我们自己定义的变量(自定义变量):** 仅在当前 Shell 实例中有效,其他 Shell 启动的程序不能访问局部变量。 -2. **Linux已定义的环境变量**(环境变量, 例如:$PATH, $HOME 等..., 这类变量我们可以直接使用),使用 `env` 命令可以查看所有的环境变量,而set命令既可以查看环境变量也可以查看自定义变量。 +2. **Linux已定义的环境变量**(环境变量, 例如:`PATH`, ​`HOME` 等..., 这类变量我们可以直接使用),使用 `env` 命令可以查看所有的环境变量,而set命令既可以查看环境变量也可以查看自定义变量。 3. **Shell变量** :Shell变量是由 Shell 程序设置的特殊变量。Shell 变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了 Shell 的正常运行 **常用的环境变量:** -> PATH 决定了shell将到哪些目录中寻找命令或程序 -HOME 当前用户主目录 -HISTSIZE 历史记录数 -LOGNAME 当前用户的登录名 -HOSTNAME 指主机的名称 -SHELL 当前用户Shell类型 -LANGUGE  语言相关的环境变量,多语言可以修改此环境变量 -MAIL 当前用户的邮件存放目录 +> PATH 决定了shell将到哪些目录中寻找命令或程序 +HOME 当前用户主目录 +HISTSIZE 历史记录数 +LOGNAME 当前用户的登录名 +HOSTNAME 指主机的名称 +SHELL 当前用户Shell类型 +LANGUAGE  语言相关的环境变量,多语言可以修改此环境变量 +MAIL 当前用户的邮件存放目录 PS1 基本提示符,对于root用户是#,对于普通用户是$ **使用 Linux 已定义的环境变量:** @@ -118,7 +94,7 @@ hello="hello world" echo $hello echo "helloworld!" ``` -![使用自己定义的变量](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-17/19835037.jpg) +![使用自己定义的变量](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-17/19835037.jpg) **Shell 编程中的变量名的命名的注意事项:** @@ -183,7 +159,7 @@ echo $greeting_2 $greeting_3 输出结果: -![输出结果](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-17/51148933.jpg) +![输出结果](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-17/51148933.jpg) **获取字符串长度:** @@ -234,13 +210,17 @@ echo ${str:0:10} #输出:SnailClimb #!bin/bash #author:amau -var="http://www.runoob.com/linux/linux-shell-variable.html" - -s1=${var%%t*}#h -s2=${var%t*}#http://www.runoob.com/linux/linux-shell-variable.h -s3=${var%%.*}#http://www -s4=${var#*/}#/www.runoob.com/linux/linux-shell-variable.html -s5=${var##*/}#linux-shell-variable.html +var="https://www.runoob.com/linux/linux-shell-variable.html" +# %表示删除从后匹配, 最短结果 +# %%表示删除从后匹配, 最长匹配结果 +# #表示删除从头匹配, 最短结果 +# ##表示删除从头匹配, 最长匹配结果 +# 注: *为通配符, 意为匹配任意数量的任意字符 +s1=${var%%t*} #h +s2=${var%t*} #https://www.runoob.com/linux/linux-shell-variable.h +s3=${var%%.*} #http://www +s4=${var#*/} #/www.runoob.com/linux/linux-shell-variable.html +s5=${var##*/} #linux-shell-variable.html ``` ### Shell 数组 @@ -262,7 +242,7 @@ echo $length2 #输出:5 echo ${array[2]} #输出:3 unset array[1]# 删除下标为1的元素也就是删除第二个元素 for i in ${array[@]};do echo $i ;done # 遍历数组,输出: 1 3 4 5 -unset arr_number; # 删除数组中的所有元素 +unset array; # 删除数组中的所有元素 for i in ${array[@]};do echo $i ;done # 遍历数组,数组元素为空,没有任何输出内容 ``` @@ -281,7 +261,7 @@ for i in ${array[@]};do echo $i ;done # 遍历数组,数组元素为空,没 ### 算数运算符 -![算数运算符](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/4937342.jpg) +![算数运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/4937342.jpg) 我以加法运算符做一个简单的示例(注意:不是单引号,是反引号): @@ -298,7 +278,7 @@ echo "Total value : $val" 关系运算符只支持数字,不支持字符串,除非字符串的值是数字。 -![shell关系运算符](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/64391380.jpg) +![shell关系运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/64391380.jpg) 通过一个简单的示例演示关系运算符的使用,下面shell程序的作用是当score=100的时候输出A否则输出B。 @@ -322,7 +302,7 @@ B ### 逻辑运算符 -![逻辑运算符](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/60545848.jpg) +![逻辑运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/60545848.jpg) 示例: @@ -336,18 +316,17 @@ echo $a; ### 布尔运算符 -![布尔运算符](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/93961425.jpg) +![布尔运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/93961425.jpg) 这里就不做演示了,应该挺简单的。 ### 字符串运算符 -![ 字符串运算符](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/309094.jpg) +![ 字符串运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/309094.jpg) 简单示例: ```shell - #!/bin/bash a="abc"; b="efg"; @@ -366,7 +345,7 @@ a 不等于 b ### 文件相关运算符 -![文件相关运算符](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/60359774.jpg) +![文件相关运算符](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-22/60359774.jpg) 使用方式很简单,比如我们定义好了一个文件路径`file="/usr/learnshell/test.sh"` 如果我们想判断这个文件是否可读,可以这样`if [ -r $file ]` 如果想判断这个文件是否可写,可以这样`-w $file`,是不是很简单。 @@ -530,8 +509,6 @@ echo "输入的两个数字之和为 $?" ### 带参数的函数 - - ```shell #!/bin/bash funWithParam(){ @@ -544,7 +521,6 @@ funWithParam(){ echo "作为一个字符串输出所有参数 $* !" } funWithParam 1 2 3 4 5 6 7 8 9 34 73 - ``` 输出结果: @@ -557,5 +533,4 @@ funWithParam 1 2 3 4 5 6 7 8 9 34 73 第十一个参数为 73 ! 参数总数有 11 个! 作为一个字符串输出所有参数 1 2 3 4 5 6 7 8 9 34 73 ! - ``` diff --git a/docs/operating-system/basis.md "b/docs/cs-basics/operating-system/\346\223\215\344\275\234\347\263\273\347\273\237\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230&\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" similarity index 81% rename from docs/operating-system/basis.md rename to "docs/cs-basics/operating-system/\346\223\215\344\275\234\347\263\273\347\273\237\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230&\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" index 6865b873c6b..db158c68faf 100644 --- a/docs/operating-system/basis.md +++ "b/docs/cs-basics/operating-system/\346\223\215\344\275\234\347\263\273\347\273\237\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230&\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" @@ -1,14 +1,26 @@ -大家好,我是 Guide 哥!很多读者抱怨计算操作系统的知识点比较繁杂,自己也没有多少耐心去看,但是面试的时候又经常会遇到。所以,我带着我整理好的操作系统的常见问题来啦!这篇文章总结了一些我觉得比较重要的操作系统相关的问题比如**进程管理**、**内存管理**、**虚拟内存**等等。 +--- +title: 操作系统常见面试题总结 +category: 计算机基础 +tag: + - 操作系统 +--- -文章形式通过大部分比较喜欢的面试官和求职者之间的对话形式展开。另外,Guide 哥也只是在大学的时候学习过操作系统,不过基本都忘了,为了写这篇文章这段时间看了很多相关的书籍和博客。如果文中有任何需要补充和完善的地方,你都可以在评论区指出。如果觉得内容不错的话,不要忘记点个在看哦! +大家好,我是 Guide 哥! -我个人觉得学好操作系统还是非常有用的,具体可以看我昨天在星球分享的一段话: +很多读者抱怨计算操作系统的知识点比较繁杂,自己也没有多少耐心去看,但是面试的时候又经常会遇到。所以,我带着我整理好的操作系统的常见问题来啦!这篇文章总结了一些我觉得比较重要的操作系统相关的问题比如**进程管理**、**内存管理**、**虚拟内存**等等。 - +文章形式通过大部分比较喜欢的面试官和求职者之间的对话形式展开。另外,Guide哥 也只是在大学的时候学习过操作系统,不过基本都忘了,为了写这篇文章这段时间看了很多相关的书籍和博客。如果文中有任何需要补充和完善的地方,你都可以在 issue 中指出! 这篇文章只是对一些操作系统比较重要概念的一个概览,深入学习的话,建议大家还是老老实实地去看书。另外, 这篇文章的很多内容参考了《现代操作系统》第三版这本书,非常感谢。 +开始本文的内容之前,我们先聊聊为什么要学习操作系统。 +- **从对个人能力方面提升来说** :操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。比如说我们开发的系统使用的缓存(比如 Redis)和操作系统的高速缓存就很像。CPU 中的高速缓存有很多种,不过大部分都是为了解决 CPU 处理速度和内存处理速度不对等的问题。我们还可以把内存看作外存的高速缓存,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。同样地,我们使用的 Redis 缓存就是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。高速缓存一般会按照局部性原理(2-8 原则)根据相应的淘汰算法保证缓存中的数据是经常会被访问的。我们平常使用的 Redis 缓存很多时候也会按照 2-8 原则去做,很多淘汰算法都和操作系统中的类似。既说了 2-8 原则,那就不得不提命中率了,这是所有缓存概念都通用的。简单来说也就是你要访问的数据有多少能直接在缓存中直接找到。命中率高的话,一般表明你的缓存设计比较合理,系统处理速度也相对较快。 +- **从面试角度来说** :尤其是校招,对于操作系统方面知识的考察是非常非常多的。 + +**简单来说,学习操作系统能够提高自己思考的深度以及对技术的理解力,并且,操作系统方面的知识也是面试必备。** + +关于如何学习操作系统,可以看这篇回答:[https://www.zhihu.com/question/270998611/answer/1640198217](https://www.zhihu.com/question/270998611/answer/1640198217)。 ## 一 操作系统基础 @@ -22,15 +34,12 @@ 🙋 **我** :我通过以下四点向您介绍一下什么是操作系统吧! -1. **操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机系统的内核与基石;** -2. **操作系统本质上是运行在计算机上的软件程序 ;** -3. **操作系统为用户提供一个与系统交互的操作界面 ;** -4. **操作系统分内核与外壳(我们可以把外壳理解成围绕着内核的应用程序,而内核就是能操作硬件的程序)。** +1. **操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机的基石。** +2. **操作系统本质上是一个运行在计算机上的软件程序 ,用于管理计算机硬件和软件资源。** 举例:运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。 +3. **操作系统存在屏蔽了硬件层的复杂性。** 操作系统就像是硬件使用的负责人,统筹着各种相关事项。 +4. **操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理**。 内核是连接应用程序和硬件的桥梁,决定着系统的性能和稳定性。 -> 关于内核多插一嘴:内核负责管理系统的进程、内存、设备驱动程序、文件和网络系统等等,决定着系统的性能和稳定性。是连接应用程序和硬件的桥梁。 -> 内核就是操作系统背后黑盒的核心。 - -![操作系统分内核与外壳](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/a72a930ca61bc2b424bb900c94c063cf.png) +![Kernel_Layout](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/Kernel_Layout.png) ### 1.2 系统调用 @@ -38,11 +47,9 @@ 🙋 **我** :介绍系统调用之前,我们先来了解一下用户态和系统态。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/006r3PQBjw1fbimb5c3srj30b40b40t9-20200404224750646.jpg) - 根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别: -1. 用户态(user mode) : 用户态运行的进程或可以直接读取用户程序的数据。 +1. 用户态(user mode) : 用户态运行的进程可以直接读取用户程序的数据。 2. 系统态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。 说了用户态和系统态之后,那么什么是系统调用呢? @@ -67,9 +74,9 @@ 🙋 **我:** 好的! 下图是 Java 内存区域,我们从 JVM 的角度来说一下线程和进程之间的关系吧! -> 如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](<[https://snailclimb.gitee.io/javaguide/#/docs/java/jvm/Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F](https://snailclimb.gitee.io/javaguide/#/docs/java/jvm/Java内存区域)>) +> 如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://snailclimb.gitee.io/javaguide/#/docs/java/jvm/Java内存区域) -![jvm运行时数据区域](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/ff96fed0e2a354bb16bbc84dcedf503a.png) +![](https://oscimg.oschina.net/oscnet/up-cd8ac705f6f004c01e0a1312f1599430ba5.png) 从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 @@ -87,6 +94,8 @@ - **阻塞状态(waiting)** :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。 - **结束状态(terminated)** :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。 +> 订正:下图中 running 状态被 interrupt 向 ready 状态转换的箭头方向反了。 + ![process-state](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/d38202593012b457debbcd74994c6292.png) ### 2.3 进程间的通信方式 @@ -100,7 +109,7 @@ 1. **管道/匿名管道(Pipes)** :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。 1. **有名管道(Names Pipes)** : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循**先进先出(first in first out)**。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。 1. **信号(Signal)** :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生; -1. **消息队列(Message Queuing)** :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。**消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。** +1. **消息队列(Message Queuing)** :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。**消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。** 1. **信号量(Semaphores)** :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。 1. **共享内存(Shared memory)** :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。 1. **套接字(Sockets)** : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。 @@ -111,9 +120,9 @@ 🙋 **我** :线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。操作系统一般有下面三种线程同步的方式: -1. **互斥量(Mutex)**:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。 -1. **信号量(Semphares)** :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量 -1. **事件(Event)** :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操 +1. **互斥量(Mutex)** :采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。 +1. **信号量(Semphares)** :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。 +1. **事件(Event)** :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便地实现多线程优先级的比较操作。 ### 2.5 进程的调度算法 @@ -129,6 +138,25 @@ - **多级反馈队列调度算法** :前面介绍的几种进程调度的算法都有一定的局限性。如**短进程优先的调度算法,仅照顾了短进程而忽略了长进程** 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前**被公认的一种较好的进程调度算法**,UNIX 操作系统采取的便是这种调度算法。 - **优先级调度** : 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。 +### 2.6 什么是死锁 + +👨‍💻**面试官** :**你知道什么是死锁吗?** + +🙋 **我** :多个进程可以竞争有限数量的资源。当一个进程申请资源时,如果这时没有可用资源,那么这个进程进入等待状态。有时,如果所申请的资源被其他等待进程占有,那么该等待进程有可能再也无法改变状态。这种情况称为**死锁**。 + +### 2.7 死锁的四个条件 + +👨‍💻**面试官** :**产生死锁的四个必要条件是什么?** + +🙋 **我** :如果系统中以下四个条件同时成立,那么就能引起死锁: + +- **互斥**:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。 +- **占有并等待**:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。 +- **非抢占**:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。 +- **循环等待**:有一组等待进程 `{P0, P1,..., Pn}`, `P0` 等待的资源被 `P1` 占有,`P1` 等待的资源被 `P2` 占有,......,`Pn-1` 等待的资源被 `Pn` 占有,`Pn` 等待的资源被 `P0` 占有。 + +注意,只有四个条件同时成立时,死锁才会出现。 + ## 三 操作系统内存管理基础 ### 3.1 内存管理介绍 @@ -153,8 +181,6 @@ 🙋 **我** :谢谢面试官!刚刚把这个给忘记了~ -这就很尴尬了_尴尬表情 - ### 3.3 快表和多级页表 👨‍💻**面试官** : 页表管理机制中有两个很重要的概念:快表和多级页表,这两个东西分别解决了页表管理中很重要的两个问题。你给我简单介绍一下吧! @@ -166,7 +192,7 @@ #### 快表 -为了解决虚拟地址到物理地址的转换速度,操作系统在 **页表方案** 基础之上引入了 **快表** 来加速虚拟地址到物理地址的转换。我们可以把块表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。 +为了解决虚拟地址到物理地址的转换速度,操作系统在 **页表方案** 基础之上引入了 **快表** 来加速虚拟地址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。 使用快表之后的地址转换流程是这样的: @@ -193,10 +219,8 @@ 🙋 **我** : - - 1. **共同点** : - - 分页机制和分段机制都是为了提高内存利用率,较少内存碎片。 + - 分页机制和分段机制都是为了提高内存利用率,减少内存碎片。 - 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。 2. **区别** : - 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。 @@ -214,11 +238,9 @@ 🙋 **我** :这部分我真不清楚! - - 于是面试完之后我默默去查阅了相关文档!留下了没有技术的泪水。。。 -> 这部分内容参考了 Microsoft 官网的介绍,地址: +> 这部分内容参考了 Microsoft 官网的介绍,地址: 现代处理器使用的是一种称为 **虚拟寻址(Virtual Addressing)** 的寻址方式。**使用虚拟寻址,CPU 需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。** 实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为 **内存管理单元(Memory Management Unit, MMU)** 的硬件。如下图所示: @@ -272,7 +294,9 @@ ### 4.3 虚拟存储器 -👨‍💻**面试官** :都说了虚拟内存了。你再讲讲**虚拟存储器**把! +> **勘误:虚拟存储器又叫做虚拟内存,都是 Virtual Memory 的翻译,属于同一个概念。** + +👨‍💻**面试官** :~~都说了虚拟内存了。你再讲讲**虚拟存储器**把!~~ 🙋 **我** : @@ -320,7 +344,7 @@ - **OPT 页面置换算法(最佳页面置换算法)** :最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。一般作为衡量其他置换算法的方法。 - **FIFO(First In First Out) 页面置换算法(先进先出页面置换算法)** : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。 -- **LRU (Least Currently Used)页面置换算法(最近最久未使用页面置换算法)** :LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。 +- **LRU (Least Recently Used)页面置换算法(最近最久未使用页面置换算法)** :LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。 - **LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法)** : 该置换算法选择在之前时期使用最少的页面作为淘汰页。 ## Reference @@ -333,4 +357,17 @@ - - - -- 王道考研操作系统知识点整理: https://wizardforcel.gitbooks.io/wangdaokaoyan-os/content/13.html \ No newline at end of file +- 王道考研操作系统知识点整理: https://wizardforcel.gitbooks.io/wangdaokaoyan-os/content/13.html + + + + + + + + + + + + + diff --git a/docs/dataStructures-algorithms/Backtracking-NQueens.md b/docs/dataStructures-algorithms/Backtracking-NQueens.md deleted file mode 100644 index 1e1367d3f98..00000000000 --- a/docs/dataStructures-algorithms/Backtracking-NQueens.md +++ /dev/null @@ -1,145 +0,0 @@ -# N皇后 -[51. N皇后](https://leetcode-cn.com/problems/n-queens/) -### 题目描述 -> n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 -> -![ANUzjA.png](https://s2.ax1x.com/2019/03/26/ANUzjA.png) -> -上图为 8 皇后问题的一种解法。 -> -给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。 -> -每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。 - -示例: - -``` -输入: 4 -输出: [ - [".Q..", // 解法 1 - "...Q", - "Q...", - "..Q."], - - ["..Q.", // 解法 2 - "Q...", - "...Q", - ".Q.."] -] -解释: 4 皇后问题存在两个不同的解法。 -``` - -### 问题分析 -约束条件为每个棋子所在的行、列、对角线都不能有另一个棋子。 - -使用一维数组表示一种解法,下标(index)表示行,值(value)表示该行的Q(皇后)在哪一列。 -每行只存储一个元素,然后递归到下一行,这样就不用判断行了,只需要判断列和对角线。 -### Solution1 -当result[row] = column时,即row行的棋子在column列。 - -对于[0, row-1]的任意一行(i 行),若 row 行的棋子和 i 行的棋子在同一列,则有result[i] == column; -若 row 行的棋子和 i 行的棋子在同一对角线,等腰直角三角形两直角边相等,即 row - i == Math.abs(result[i] - column) - -布尔类型变量 isValid 的作用是剪枝,减少不必要的递归。 -```java -public List> solveNQueens(int n) { - // 下标代表行,值代表列。如result[0] = 3 表示第1行的Q在第3列 - int[] result = new int[n]; - List> resultList = new LinkedList<>(); - dfs(resultList, result, 0, n); - return resultList; -} - -void dfs(List> resultList, int[] result, int row, int n) { - // 递归终止条件 - if (row == n) { - List list = new LinkedList<>(); - for (int x = 0; x < n; ++x) { - StringBuilder sb = new StringBuilder(); - for (int y = 0; y < n; ++y) - sb.append(result[x] == y ? "Q" : "."); - list.add(sb.toString()); - } - resultList.add(list); - return; - } - for (int column = 0; column < n; ++column) { - boolean isValid = true; - result[row] = column; - /* - * 逐行往下考察每一行。同列,result[i] == column - * 同对角线,row - i == Math.abs(result[i] - column) - */ - for (int i = row - 1; i >= 0; --i) { - if (result[i] == column || row - i == Math.abs(result[i] - column)) { - isValid = false; - break; - } - } - if (isValid) dfs(resultList, result, row + 1, n); - } -} -``` -### Solution2 -使用LinkedList表示一种解法,下标(index)表示行,值(value)表示该行的Q(皇后)在哪一列。 - -解法二和解法一的不同在于,相同列以及相同对角线的校验。 -将对角线抽象成【一次函数】这个简单的数学模型,根据一次函数的截距是常量这一特性进行校验。 - -这里,我将右上-左下对角线,简称为“\”对角线;左上-右下对角线简称为“/”对角线。 - -“/”对角线斜率为1,对应方程为y = x + b,其中b为截距。 -对于线上任意一点,均有y - x = b,即row - i = b; -定义一个布尔类型数组anti_diag,将b作为下标,当anti_diag[b] = true时,表示相应对角线上已经放置棋子。 -但row - i有可能为负数,负数不能作为数组下标,row - i 的最小值为-n(当row = 0,i = n时),可以加上n作为数组下标,即将row -i + n 作为数组下标。 -row - i + n 的最大值为 2n(当row = n,i = 0时),故anti_diag的容量设置为 2n 即可。 - -![ANXG79.png](https://s2.ax1x.com/2019/03/26/ANXG79.png) - -“\”对角线斜率为-1,对应方程为y = -x + b,其中b为截距。 -对于线上任意一点,均有y + x = b,即row + i = b; -同理,定义数组main_diag,将b作为下标,当main_diag[row + i] = true时,表示相应对角线上已经放置棋子。 - -有了两个校验对角线的数组,再来定义一个用于校验列的数组cols,这个太简单啦,不解释。 - -**解法二时间复杂度为O(n!),在校验相同列和相同对角线时,引入三个布尔类型数组进行判断。相比解法一,少了一层循环,用空间换时间。** - -```java -List> resultList = new LinkedList<>(); - -public List> solveNQueens(int n) { - boolean[] cols = new boolean[n]; - boolean[] main_diag = new boolean[2 * n]; - boolean[] anti_diag = new boolean[2 * n]; - LinkedList result = new LinkedList<>(); - dfs(result, 0, cols, main_diag, anti_diag, n); - return resultList; -} - -void dfs(LinkedList result, int row, boolean[] cols, boolean[] main_diag, boolean[] anti_diag, int n) { - if (row == n) { - List list = new LinkedList<>(); - for (int x = 0; x < n; ++x) { - StringBuilder sb = new StringBuilder(); - for (int y = 0; y < n; ++y) - sb.append(result.get(x) == y ? "Q" : "."); - list.add(sb.toString()); - } - resultList.add(list); - return; - } - for (int i = 0; i < n; ++i) { - if (cols[i] || main_diag[row + i] || anti_diag[row - i + n]) - continue; - result.add(i); - cols[i] = true; - main_diag[row + i] = true; - anti_diag[row - i + n] = true; - dfs(result, row + 1, cols, main_diag, anti_diag, n); - result.removeLast(); - cols[i] = false; - main_diag[row + i] = false; - anti_diag[row - i + n] = false; - } -} -``` \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" "b/docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" deleted file mode 100644 index c78ed8f3e5a..00000000000 --- "a/docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" +++ /dev/null @@ -1,254 +0,0 @@ -# 网易 2018 - -下面三道编程题来自网易2018校招编程题,这三道应该来说是非常简单的编程题了,这些题目大家稍微有点编程和数学基础的话应该没什么问题。看答案之前一定要自己先想一下如果是自己做的话会怎么去做,然后再对照这我的答案看看,和你自己想的有什么区别?那一种方法更好? - -## 问题 - -### 一 获得特定数量硬币问题 - -小易准备去魔法王国采购魔法神器,购买魔法神器需要使用魔法币,但是小易现在一枚魔法币都没有,但是小易有两台魔法机器可以通过投入x(x可以为0)个魔法币产生更多的魔法币。 - -魔法机器1:如果投入x个魔法币,魔法机器会将其变为2x+1个魔法币 - -魔法机器2:如果投入x个魔法币,魔法机器会将其变为2x+2个魔法币 - -小易采购魔法神器总共需要n个魔法币,所以小易只能通过两台魔法机器产生恰好n个魔法币,小易需要你帮他设计一个投入方案使他最后恰好拥有n个魔法币。 - -**输入描述:** 输入包括一行,包括一个正整数n(1 ≤ n ≤ 10^9),表示小易需要的魔法币数量。 - -**输出描述:** 输出一个字符串,每个字符表示该次小易选取投入的魔法机器。其中只包含字符'1'和'2'。 - -**输入例子1:** 10 - -**输出例子1:** 122 - -### 二 求“相反数”问题 - -为了得到一个数的"相反数",我们将这个数的数字顺序颠倒,然后再加上原先的数得到"相反数"。例如,为了得到1325的"相反数",首先我们将该数的数字顺序颠倒,我们得到5231,之后再加上原先的数,我们得到5231+1325=6556.如果颠倒之后的数字有前缀零,前缀零将会被忽略。例如n = 100, 颠倒之后是1. - -**输入描述:** 输入包括一个整数n,(1 ≤ n ≤ 10^5) - -**输出描述:** 输出一个整数,表示n的相反数 - -**输入例子1:** 1325 - -**输出例子1:** 6556 - -### 三 字符串碎片的平均长度 - -一个由小写字母组成的字符串可以看成一些同一字母的最大碎片组成的。例如,"aaabbaaac"是由下面碎片组成的:'aaa','bb','c'。牛牛现在给定一个字符串,请你帮助计算这个字符串的所有碎片的平均长度是多少。 - -**输入描述:** 输入包括一个字符串s,字符串s的长度length(1 ≤ length ≤ 50),s只含小写字母('a'-'z') - -**输出描述:** 输出一个整数,表示所有碎片的平均长度,四舍五入保留两位小数。 - -**如样例所示:** s = "aaabbaaac" -所有碎片的平均长度 = (3 + 2 + 3 + 1) / 4 = 2.25 - -**输入例子1:** aaabbaaac - -**输出例子1:** 2.25 - -## 答案 - -### 一 获得特定数量硬币问题 - -#### 分析: - -作为该试卷的第一题,这道题应该只要思路正确就很简单了。 - -解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - -#### 示例代码 - -注意:由于用户的输入不确定性,一般是为了程序高可用性使需要将捕获用户输入异常然后友好提示用户输入类型错误并重新输入的。所以下面我给了两个版本,这两个版本都是正确的。这里只是给大家演示如何捕获输入类型异常,后面的题目中我给的代码没有异常处理的部分,参照下面两个示例代码,应该很容易添加。(PS:企业面试中没有明确就不用添加异常处理,当然你有的话也更好) - -**不带输入异常处理判断的版本:** - -```java -import java.util.Scanner; - -public class Main2 { - // 解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - - public static void main(String[] args) { - System.out.println("请输入要获得的硬币数量:"); - Scanner scanner = new Scanner(System.in); - int coincount = scanner.nextInt(); - StringBuilder sb = new StringBuilder(); - while (coincount >= 1) { - // 偶数的情况 - if (coincount % 2 == 0) { - coincount = (coincount - 2) / 2; - sb.append("2"); - // 奇数的情况 - } else { - coincount = (coincount - 1) / 2; - sb.append("1"); - } - } - // 输出反转后的字符串 - System.out.println(sb.reverse()); - - } -} -``` - -**带输入异常处理判断的版本(当输入的不是整数的时候会提示重新输入):** - -```java -import java.util.InputMismatchException; -import java.util.Scanner; - - -public class Main { - // 解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - - public static void main(String[] args) { - System.out.println("请输入要获得的硬币数量:"); - Scanner scanner = new Scanner(System.in); - boolean flag = true; - while (flag) { - try { - int coincount = scanner.nextInt(); - StringBuilder sb = new StringBuilder(); - while (coincount >= 1) { - // 偶数的情况 - if (coincount % 2 == 0) { - coincount = (coincount - 2) / 2; - sb.append("2"); - // 奇数的情况 - } else { - coincount = (coincount - 1) / 2; - sb.append("1"); - } - } - // 输出反转后的字符串 - System.out.println(sb.reverse()); - flag=false;//程序结束 - } catch (InputMismatchException e) { - System.out.println("输入数据类型不匹配,请您重新输入:"); - scanner.nextLine(); - continue; - } - } - - } -} - -``` - -### 二 求“相反数”问题 - -#### 分析: - -解决本道题有几种不同的方法,但是最快速的方法就是利用reverse()方法反转字符串然后再将字符串转换成int类型的整数,这个方法是快速解决本题关键。我们先来回顾一下下面两个知识点: - -**1)String转int;** - -在 Java 中要将 String 类型转化为 int 类型时,需要使用 Integer 类中的 parseInt() 方法或者 valueOf() 方法进行转换. - -```java - String str = "123"; - int a = Integer.parseInt(str); -``` - - 或 - -```java - String str = "123"; - int a = Integer.valueOf(str).intValue(); -``` - -**2)next()和nextLine()的区别** - -在Java中输入字符串有两种方法,就是next()和nextLine().两者的区别就是:nextLine()的输入是碰到回车就终止输入,而next()方法是碰到空格,回车,Tab键都会被视为终止符。所以next()不会得到带空格的字符串,而nextLine()可以得到带空格的字符串。 - -#### 示例代码: - -```java -import java.util.Scanner; - -/** - * 本题关键:①String转int;②next()和nextLine()的区别 - */ -public class Main { - - public static void main(String[] args) { - - System.out.println("请输入一个整数:"); - Scanner scanner = new Scanner(System.in); - String s=scanner.next(); - //将字符串转换成数字 - int number1=Integer.parseInt(s); - //将字符串倒序后转换成数字 - //因为Integer.parseInt()的参数类型必须是字符串所以必须加上toString() - int number2=Integer.parseInt(new StringBuilder(s).reverse().toString()); - System.out.println(number1+number2); - - } -} -``` - -### 三 字符串碎片的平均长度 - -#### 分析: - -这道题的意思也就是要求:(字符串的总长度)/(相同字母团构成的字符串的个数)。 - -这样就很简单了,就变成了字符串的字符之间的比较。如果需要比较字符串的字符的话,我们可以利用charAt(i)方法:取出特定位置的字符与后一个字符比较,或者利用toCharArray()方法将字符串转换成字符数组采用同样的方法做比较。 - -#### 示例代码 - -**利用charAt(i)方法:** - -```java -import java.util.Scanner; - -public class Main { - - public static void main(String[] args) { - - Scanner sc = new Scanner(System.in); - while (sc.hasNext()) { - String s = sc.next(); - //个数至少为一个 - float count = 1; - for (int i = 0; i < s.length() - 1; i++) { - if (s.charAt(i) != s.charAt(i + 1)) { - count++; - } - } - System.out.println(s.length() / count); - } - } - -} -``` - -**利用toCharArray()方法:** - -```java -import java.util.Scanner; - -public class Main2 { - - public static void main(String[] args) { - - Scanner sc = new Scanner(System.in); - while (sc.hasNext()) { - String s = sc.next(); - //个数至少为一个 - float count = 1; - char [] stringArr = s.toCharArray(); - for (int i = 0; i < stringArr.length - 1; i++) { - if (stringArr[i] != stringArr[i + 1]) { - count++; - } - } - System.out.println(s.length() / count); - } - } - -} -``` \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" deleted file mode 100644 index e1169ede68e..00000000000 --- "a/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ /dev/null @@ -1,204 +0,0 @@ -下面只是简单地总结,给了一些参考文章,后面会对这部分内容进行重构。 - - -- [Queue](#queue) - - [什么是队列](#什么是队列) - - [队列的种类](#队列的种类) - - [Java 集合框架中的队列 Queue](#java-集合框架中的队列-queue) - - [推荐文章](#推荐文章) -- [Set](#set) - - [什么是 Set](#什么是-set) - - [补充:有序集合与无序集合说明](#补充:有序集合与无序集合说明) - - [HashSet 和 TreeSet 底层数据结构](#hashset-和-treeset-底层数据结构) - - [推荐文章](#推荐文章-1) -- [List](#list) - - [什么是List](#什么是list) - - [List的常见实现类](#list的常见实现类) - - [ArrayList 和 LinkedList 源码学习](#arraylist-和-linkedlist-源码学习) - - [推荐阅读](#推荐阅读) -- [Map](#map) -- [树](#树) - - - - -## Queue - -### 什么是队列 -队列是数据结构中比较重要的一种类型,它支持 FIFO,尾部添加、头部删除(先进队列的元素先出队列),跟我们生活中的排队类似。 - -### 队列的种类 - -- **单队列**(单队列就是常见的队列, 每次添加元素时,都是添加到队尾,存在“假溢出”的问题也就是明明有位置却不能添加的情况) -- **循环队列**(避免了“假溢出”的问题) - -### Java 集合框架中的队列 Queue - -Java 集合中的 Queue 继承自 Collection 接口 ,Deque, LinkedList, PriorityQueue, BlockingQueue 等类都实现了它。 -Queue 用来存放 等待处理元素 的集合,这种场景一般用于缓冲、并发访问。 -除了继承 Collection 接口的一些方法,Queue 还添加了额外的 添加、删除、查询操作。 - -### 推荐文章 - -- [Java 集合深入理解(9):Queue 队列](https://blog.csdn.net/u011240877/article/details/52860924) - -## Set - -### 什么是 Set -Set 继承于 Collection 接口,是一个不允许出现重复元素,并且无序的集合,主要 HashSet 和 TreeSet 两大实现类。 - -在判断重复元素的时候,HashSet 集合会调用 hashCode()和 equal()方法来实现;TreeSet 集合会调用compareTo方法来实现。 - -### 补充:有序集合与无序集合说明 -- 有序集合:集合里的元素可以根据 key 或 index 访问 (List、Map) -- 无序集合:集合里的元素只能遍历。(Set) - - -### HashSet 和 TreeSet 底层数据结构 - -**HashSet** 是哈希表结构,主要利用 HashMap 的 key 来存储元素,计算插入元素的 hashCode 来获取元素在集合中的位置; - -**TreeSet** 是红黑树结构,每一个元素都是树中的一个节点,插入的元素都会进行排序; - - -### 推荐文章 - -- [Java集合--Set(基础)](https://www.jianshu.com/p/b48c47a42916) - -## List - -### 什么是List - -在 List 中,用户可以精确控制列表中每个元素的插入位置,另外用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。 与 Set 不同,List 通常允许重复的元素。 另外 List 是有序集合而 Set 是无序集合。 - -### List的常见实现类 - -**ArrayList** 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。 - -**LinkedList** 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。 - -**Vector** 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。 - -**Stack** 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。相关阅读:[java数据结构与算法之栈(Stack)设计与实现](https://blog.csdn.net/javazejian/article/details/53362993) - -### ArrayList 和 LinkedList 源码学习 - -- [ArrayList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList.md) -- [LinkedList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/LinkedList.md) - -### 推荐阅读 - -- [java 数据结构与算法之顺序表与链表深入分析](https://blog.csdn.net/javazejian/article/details/52953190) - - -## Map - - -- [集合框架源码学习之 HashMap(JDK1.8)](https://juejin.im/post/5ab0568b5188255580020e56) -- [ConcurrentHashMap 实现原理及源码分析](https://link.juejin.im/?target=http%3A%2F%2Fwww.cnblogs.com%2Fchengxiao%2Fp%2F6842045.html) - -## 树 - -### 1 二叉树 - -[二叉树](https://baike.baidu.com/item/%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科) - -(1)[完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。 - -(2)[满二叉树](https://baike.baidu.com/item/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91)——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。 - -(3)[平衡二叉树](https://baike.baidu.com/item/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/10421057)——平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 - -### 2 完全二叉树 - -[完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科) - -完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。 - -### 3 满二叉树 - -[满二叉树](https://baike.baidu.com/item/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科,国内外的定义不同) - -国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。 - -### 堆 - -[数据结构之堆的定义](https://blog.csdn.net/qq_33186366/article/details/51876191) - -堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。 - -### 4 二叉查找树(BST) - -[浅谈算法和数据结构: 七 二叉查找树](http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html) - -二叉查找树的特点: - -1. 若任意节点的左子树不空,则左子树上所有结点的 值均小于它的根结点的值; -2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值; -3. 任意节点的左、右子树也分别为二叉查找树; -4. 没有键值相等的节点(no duplicate nodes)。 - -### 5 平衡二叉树(Self-balancing binary search tree) - -[ 平衡二叉树](https://baike.baidu.com/item/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科,平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等) - -### 6 红黑树 - -红黑树特点: - -1. 每个节点非红即黑; -2. 根节点总是黑色的; -3. 每个叶子节点都是黑色的空节点(NIL节点); -4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); -5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。 - - -红黑树的应用: - -TreeMap、TreeSet以及JDK1.8的HashMap底层都用到了红黑树。 - -**为什么要用红黑树?** - - -简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。详细了解可以查看 [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐) - -推荐文章: - -- [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐) -- [寻找红黑树的操作手册](http://dandanlove.com/2018/03/18/red-black-tree/)(文章排版以及思路真的不错) -- [红黑树深入剖析及Java实现](https://zhuanlan.zhihu.com/p/24367771)(美团点评技术团队) - -### 7 B-,B+,B*树 - -[二叉树学习笔记之B树、B+树、B*树 ](https://yq.aliyun.com/articles/38345) - -[《B-树,B+树,B*树详解》](https://blog.csdn.net/aqzwss/article/details/53074186) - -[《B-树,B+树与B*树的优缺点比较》](https://blog.csdn.net/bigtree_3721/article/details/73632405) - -B-树(或B树)是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance) - -1. B+ 树的叶子节点链表结构相比于 B- 树便于扫库,和范围检索。 -2. B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因。 -3. B\*树 是B+树的变体,B\*树分配新结点的概率比B+树要低,空间使用率更高; - -### 8 LSM 树 - -[[HBase] LSM树 VS B+树](https://blog.csdn.net/dbanote/article/details/8897599) - -B+树最大的性能问题是会产生大量的随机IO - -为了克服B+树的弱点,HBase引入了LSM树的概念,即Log-Structured Merge-Trees。 - -[LSM树由来、设计思想以及应用到HBase的索引](http://www.cnblogs.com/yanghuahui/p/3483754.html) - - -## 图 - - - - -## BFS及DFS - -- [《使用BFS及DFS遍历树和图的思路及实现》](https://blog.csdn.net/Gene1994/article/details/85097507) - diff --git "a/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" "b/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" deleted file mode 100644 index 9781d85b725..00000000000 --- "a/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" +++ /dev/null @@ -1,142 +0,0 @@ -先占个坑,说一下我觉得算法这部分学习比较好的规划: - -1. 未入门(对算法和基本数据结构不了解)之前建议先找一本入门书籍看; -2. 如果时间比较多可以看一下我推荐的经典部分的书籍,《算法》这本书是首要要看的,其他推荐的神书看自己时间和心情就好,不要太纠结。 -3. 如果要准备面试,时间比较紧的话,就不需要再去看《算法》这本书了,时间来不及,当然你也可以选取其特定的章节查看。我也推荐了几本不错的专门为算法面试准备的书籍比如《剑指offer》和《程序员代码面试指南》。除了这两本书籍的话,我在下面推荐了 Leetcode 和牛客网这两个常用的刷题网站以及一些比较好的题目资源。 - -## 书籍推荐 - -> 以下提到的部分书籍的 PDF 高清阅读版本在我的公众号“JavaGuide”后台回复“书籍”即可获取。 - -先来看三本入门书籍,这三本入门书籍中的任何一本拿来作为入门学习都非常好。我个人比较倾向于 **《我的第一本算法书》** 这本书籍,虽然它相比于其他两本书集它的豆瓣评分略低一点。我觉得它的配图以及讲解是这三本书中最优秀,唯一比较明显的问题就是没有代码示例。但是,我觉得这不影响它是一本好的算法书籍。因为本身下面这三本入门书籍的目的就不是通过代码来让你的算法有多厉害,只是作为一本很好的入门书籍让你进入算法学习的大门。 - -### 入门 - - - -**[我的第一本算法书](https://book.douban.com/subject/30357170/) (豆瓣评分 7.1,0.2K+人评价)** - -一本不那么“专业”的算法书籍。和下面两本推荐的算法书籍都是比较通俗易懂,“不那么深入”的算法书籍。我个人非常推荐,配图和讲解都非常不错! - -img - -**[《算法图解》](https://book.douban.com/subject/26979890/)(豆瓣评分 8.4,1.5K+人评价)** - -入门类型的书籍,读起来比较浅显易懂,非常适合没有算法基础或者说算法没学好的小伙伴用来入门。示例丰富,图文并茂,以让人容易理解的方式阐释了算法.读起来比较快,内容不枯燥! - -![啊哈!算法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/592bf169N864816a5.jpg) - -**[啊哈!算法](https://book.douban.com/subject/25894685/) (豆瓣评分 7.7,0.5K+人评价)** - -和《算法图解》类似的算法趣味入门书籍。 - -### 经典 - - - -**[《算法 第四版》](https://book.douban.com/subject/10432347/)(豆瓣评分 9.3,0.4K+人评价)** - -我在大二的时候被我们的一个老师强烈安利过!自己也在当时购买了一本放在宿舍,到离开大学的时候自己大概看了一半多一点。因为内容实在太多了!另外,这本书还提供了详细的Java代码,非常适合学习 Java 的朋友来看,可以说是 Java 程序员的必备书籍之一了。 - -再来介绍一下这本书籍吧!这本书籍算的上是算法领域经典的参考书,全面介绍了关于算法和数据结构的必备知识,并特别针对排序、搜索、图处理和字符串处理进行了论述。 - -> **下面这些书籍都是经典中的经典,但是阅读起来难度也比较大,不做太多阐述,神书就完事了!推荐先看 《算法》,然后再选下面的书籍进行进一步阅读。不需要都看,找一本好好看或者找某本书的某一个章节知识点好好看。** - - - -**[编程珠玑](https://book.douban.com/subject/3227098/)(豆瓣评分 9.1,2K+人评价)** - -经典名著,被无数读者强烈推荐的书籍,几乎是顶级程序员必看的书籍之一了。这本书的作者也非常厉害,Java之父 James Gosling 就是他的学生。 - -很多人都说这本书不是教你具体的算法,而是教你一种编程的思考方式。这种思考方式不仅仅在编程领域适用,在其他同样适用。 - - - - - -**[《算法设计手册》](https://book.douban.com/subject/4048566/)(豆瓣评分9.1 , 45人评价)** - -被 [Teach Yourself Computer Science](https://teachyourselfcs.com/) 强烈推荐的一本算法书籍。 - - - -**[《算法导论》](https://book.douban.com/subject/20432061/) (豆瓣评分 9.2,0.4K+人评价)** - -![](https://imgkr.cn-bj.ufileos.com/a9f4281f-2d24-47f7-930e-9d050223894a.jpg) - -**[《计算机程序设计艺术(第1卷)》](https://book.douban.com/subject/1130500/)(豆瓣评分 9.4,0.4K+人评价)** - -### 面试 - -![](https://imgkr.cn-bj.ufileos.com/ed6b05c6-9a02-4d56-846b-6eb9cdd9285e.png) - -**[《剑指Offer》](https://book.douban.com/subject/6966465/)(豆瓣评分 8.3,0.7K+人评价)** - -这本面试宝典上面涵盖了很多经典的算法面试题,如果你要准备大厂面试的话一定不要错过这本书。 - -《剑指Offer》 对应的算法编程题部分的开源项目解析:[CodingInterviews](https://github.com/gatieme/CodingInterviews) - -![](https://imgkr.cn-bj.ufileos.com/5ad3afa8-801f-4649-88c5-8bc480ad5196.png) - - - - - -**[程序员代码面试指南:IT名企算法与数据结构题目最优解(第2版)](https://book.douban.com/subject/30422021/) (豆瓣评分 8.7,0.2K+人评价)** - -题目相比于《剑指 offer》 来说要难很多,题目涵盖面相比于《剑指 offer》也更加全面。全书一共有将近300道真实出现过的经典代码面试题。 - - - - - - - -**[编程之美](https://book.douban.com/subject/3004255/)(豆瓣评分 8.4,3K+人评价)** - -这本书收集了约60道算法和程序设计题目,这些题目大部分在近年的笔试、面试中出现过,或者是被微软员工热烈讨论过。作者试图从书中各种有趣的问题出发,引导读者发现问题,分析问题,解决问题,寻找更优的解法。 - -## 网站推荐 - -我比较推荐大家可以刷一下 Leetcode ,我自己平时没事也会刷一下,我觉得刷 Leetcode 不仅是为了能让你更从容地面对面试中的手撕算法问题,更可以提高你的编程思维能力、解决问题的能力以及你对某门编程语言 API 的熟练度。当然牛客网也有一些算法题,我下面也整理了一些。 - -### [LeetCode](https://leetcode-cn.com/) - -[如何高效地使用 LeetCode](https://leetcode-cn.com/articles/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%9C%B0%E4%BD%BF%E7%94%A8-leetcode/) - -- [《程序员代码面试指南》](https://leetcode-cn.com/problemset/lcci/) -- [《剑指offer》](https://leetcode-cn.com/problemset/lcof/) - - -### [牛客网](https://www.nowcoder.com) - -**[在线编程](https://www.nowcoder.com/activity/oj):** - -- [《剑指offer》](https://www.nowcoder.com/ta/coding-interviews) -- [《程序员代码面试指南》](https://www.nowcoder.com/ta/programmer-code-interview-guide) -- [2019 校招真题](https://www.nowcoder.com/ta/2019test) -- [大一大二编程入门训练](https://www.nowcoder.com/ta/beginner-programmers) -- ....... - -**[大厂编程面试真题](https://www.nowcoder.com/contestRoom?filter=0&orderByHotValue=3&target=content&categories=-1&mutiTagIds=2491&page=1)** - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/database/MySQL Index.md b/docs/database/MySQL Index.md deleted file mode 100644 index 14ea593e8b0..00000000000 --- a/docs/database/MySQL Index.md +++ /dev/null @@ -1,172 +0,0 @@ -## 为什么要使用索引? - -1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 -2. 可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。 -3. 帮助服务器避免排序和临时表。 -4. 将随机IO变为顺序IO -5. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。 - -## 索引这么多优点,为什么不对表中的每一个列创建一个索引呢? - -1. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。 -2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。 -3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。 - -## 使用索引的注意事项? - -1. 在经常需要搜索的列上,可以加快搜索的速度; -2. 在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。 -3. 在经常需要排序的列上创 建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间; -4. 对于中到大型表索引都是非常有效的,但是特大型表的话维护开销会很大,不适合建索引 -5. 在经常用在连接的列上,这 些列主要是一些外键,可以加快连接的速度; -6. 避免 where 子句中对宇段施加函数,这会造成无法命中索引。 -7. 在使用InnoDB时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。 -8. 将打算加索引的列设置为 NOT NULL ,否则将导致引擎放弃使用索引而进行全表扫描 -9. 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 chema_unused_indexes 视图来查询哪些索引从未被使用 -10. 在使用 limit offset 查询缓慢时,可以借助索引来提高性能 - -## Mysql索引主要使用的两种数据结构 - -### 哈希索引 - -对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。 - -### BTree索引 - -## MyISAM和InnoDB实现BTree索引方式的区别 - -### MyISAM - -B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。 - -### InnoDB - -其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”,而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,在走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。 PS:整理自《Java工程师修炼之道》 - -## 覆盖索引介绍 - -### 什么是覆盖索引 - -如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。我们知道InnoDB存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! - -### 覆盖索引使用实例 - -现在我创建了索引(username,age),我们执行下面的 sql 语句 - -```sql -select username , age from user where username = 'Java' and age = 22 -``` - -在查询数据的时候:要查询出的列在叶子节点都存在!所以,就不用回表。 - -## 选择索引和编写利用这些索引的查询的3个原则 - -1. 单行访问是很慢的。特别是在机械硬盘存储中(SSD的随机I/O要快很多,不过这一点仍然成立)。如果服务器从存储中读取一个数据块只是为了获取其中一行,那么就浪费了很多工作。最好读取的块中能包含尽可能多所需要的行。使用索引可以创建位置引,用以提升效率。 -2. 按顺序访问范围数据是很快的,这有两个原因。第一,顺序1/0不需要多次磁盘寻道,所以比随机I/O要快很多(特别是对机械硬盘)。第二,如果服务器能够按需要顺序读取数据,那么就不再需要额外的排序操作,并且GROUPBY查询也无须再做排序和将行按组进行聚合计算了。 -3. 索引覆盖查询是很快的。如果一个索引包含了查询需要的所有列,那么存储引擎就 - 不需要再回表查找行。这避免了大量的单行访问,而上面的第1点已经写明单行访 - 问是很慢的。 - -## 为什么索引能提高查询速度 - -> 以下内容整理自: -> 地址: https://juejin.im/post/5b55b842f265da0f9e589e79 -> 作者 :Java3y - -### 先从 MySQL 的基本存储结构说起 - -MySQL的基本存储结构是页(记录都存在页里边): - -![MySQL的基本存储结构是页](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-2/28559421.jpg) - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-2/82053134.jpg) - - - **各个数据页可以组成一个双向链表** - - **每个数据页中的记录又可以组成一个单向链表** - - 每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录 - - 以其他列(非主键)作为搜索条件:只能从最小记录开始依次遍历单链表中的每条记录。 - -所以说,如果我们写select * from user where indexname = 'xxx'这样没有进行任何优化的sql语句,默认会这样做: - -1. **定位到记录所在的页:需要遍历双向链表,找到所在的页** -2. **从所在的页内中查找相应的记录:由于不是根据主键查询,只能遍历所在页的单链表了** - -很明显,在数据量很大的情况下这样查找会很慢!这样的时间复杂度为O(n)。 - - -### 使用索引之后 - -索引做了些什么可以让我们查询加快速度呢?其实就是将无序的数据变成有序(相对): - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-2/5373082.jpg) - -要找到id为8的记录简要步骤: - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-2/89338047.jpg) - -很明显的是:没有用索引我们是需要遍历双向链表来定位对应的页,现在通过 **“目录”** 就可以很快地定位到对应的页上了!(二分查找,时间复杂度近似为O(logn)) - -其实底层结构就是B+树,B+树作为树的一种实现,能够让我们很快地查找出对应的记录。 - -## 关于索引其他重要的内容补充 - -> 以下内容整理自:《Java工程师修炼之道》 - - -### 最左前缀原则 - -MySQL中的索引可以以一定顺序引用多列,这种索引叫作联合索引。如User表的name和city加联合索引就是(name,city),而最左前缀原则指的是,如果查询的时候查询条件精确匹配索引的左边连续一列或几列,则此列就可以被用到。如下: - -``` -select * from user where name=xx and city=xx ; //可以命中索引 -select * from user where name=xx ; // 可以命中索引 -select * from user where city=xx ; // 无法命中索引 -``` -这里需要注意的是,查询的时候如果两个条件都用上了,但是顺序不同,如 `city= xx and name =xx`,那么现在的查询引擎会自动优化为匹配联合索引的顺序,这样是能够命中索引的。 - -由于最左前缀原则,在创建联合索引时,索引字段的顺序需要考虑字段值去重之后的个数,较多的放前面。ORDER BY子句也遵循此规则。 - -### 注意避免冗余索引 - -冗余索引指的是索引的功能相同,能够命中 就肯定能命中 ,那么 就是冗余索引如(name,city )和(name )这两个索引就是冗余索引,能够命中后者的查询肯定是能够命中前者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。 - -MySQL 5.7 版本后,可以通过查询 sys 库的 `schema_redundant_indexes` 表来查看冗余索引 - -### Mysql如何为表字段添加索引??? - -1.添加PRIMARY KEY(主键索引) - -``` -ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` ) -``` -2.添加UNIQUE(唯一索引) - -``` -ALTER TABLE `table_name` ADD UNIQUE ( `column` ) -``` - -3.添加INDEX(普通索引) - -``` -ALTER TABLE `table_name` ADD INDEX index_name ( `column` ) -``` - -4.添加FULLTEXT(全文索引) - -``` -ALTER TABLE `table_name` ADD FULLTEXT ( `column`) -``` - -5.添加多列索引 - -``` -ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` ) -``` - - -## 参考 - -- 《Java工程师修炼之道》 -- 《MySQL高性能书籍_第3版》 -- https://juejin.im/post/5b55b842f265da0f9e589e79 - diff --git a/docs/database/MySQL.md b/docs/database/MySQL.md deleted file mode 100644 index 8e74cb89b61..00000000000 --- a/docs/database/MySQL.md +++ /dev/null @@ -1,328 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - -- [书籍推荐](#书籍推荐) -- [文字教程推荐](#文字教程推荐) -- [视频教程推荐](#视频教程推荐) -- [常见问题总结](#常见问题总结) - - [什么是MySQL?](#什么是mysql) - - [存储引擎](#存储引擎) - - [一些常用命令](#一些常用命令) - - [MyISAM和InnoDB区别](#myisam和innodb区别) - - [字符集及校对规则](#字符集及校对规则) - - [索引](#索引) - - [查询缓存的使用](#查询缓存的使用) - - [什么是事务?](#什么是事务) - - [事物的四大特性(ACID)](#事物的四大特性acid) - - [并发事务带来哪些问题?](#并发事务带来哪些问题) - - [事务隔离级别有哪些?MySQL的默认隔离级别是?](#事务隔离级别有哪些mysql的默认隔离级别是) - - [锁机制与InnoDB锁算法](#锁机制与innodb锁算法) - - [大表优化](#大表优化) - - [1. 限定数据的范围](#1-限定数据的范围) - - [2. 读/写分离](#2-读写分离) - - [3. 垂直分区](#3-垂直分区) - - [4. 水平分区](#4-水平分区) - - [一条SQL语句在MySQL中如何执行的](#一条sql语句在mysql中如何执行的) - - [MySQL高性能优化规范建议](#mysql高性能优化规范建议) - - [一条SQL语句执行得很慢的原因有哪些?](#一条sql语句执行得很慢的原因有哪些) - - - -## 书籍推荐 - -- 《SQL基础教程(第2版)》 (入门级) -- 《高性能MySQL : 第3版》 (进阶) - -## 文字教程推荐 - -- [SQL Tutorial](https://www.w3schools.com/sql/default.asp) (SQL语句学习,英文)、[SQL Tutorial](https://www.w3school.com.cn/sql/index.asp)(SQL语句学习,中文)、[SQL语句在线练习](https://www.w3schools.com/sql/exercise.asp) (非常不错) -- [Github-MySQL入门教程(MySQL tutorial book)](https://github.com/jaywcjlove/mysql-tutorial) (从零开始学习MySQL,主要是面向MySQL数据库管理系统初学者) -- [官方教程](https://dev.mysql.com/doc/refman/5.7/) -- [MySQL 教程(菜鸟教程)](http://www.runoob.com/MySQL/MySQL-tutorial.html) - -## 相关资源推荐 - -- [中国5级行政区域mysql库](https://github.com/kakuilan/china_area_mysql) - -## 视频教程推荐 - -**基础入门:** [与MySQL的零距离接触-慕课网](https://www.imooc.com/learn/122) - -**MySQL开发技巧:** [MySQL开发技巧(一)](https://www.imooc.com/learn/398)  [MySQL开发技巧(二)](https://www.imooc.com/learn/427)  [MySQL开发技巧(三)](https://www.imooc.com/learn/449) - -**MySQL5.7新特性及相关优化技巧:** [MySQL5.7版本新特性](https://www.imooc.com/learn/533)  [性能优化之MySQL优化](https://www.imooc.com/learn/194) - -[MySQL集群(PXC)入门](https://www.imooc.com/learn/993)  [MyCAT入门及应用](https://www.imooc.com/learn/951) - -## 常见问题总结 - -### 什么是MySQL? - -MySQL 是一种关系型数据库,在Java企业级开发中非常常用,因为 MySQL 是开源免费的,并且方便扩展。阿里巴巴数据库系统也大量用到了 MySQL,因此它的稳定性是有保障的。MySQL是开放源代码的,因此任何人都可以在 GPL(General Public License) 的许可下下载并根据个性化的需要对其进行修改。MySQL的默认端口号是**3306**。 - -### 存储引擎 - -#### 一些常用命令 - -**查看MySQL提供的所有存储引擎** - -```sql -mysql> show engines; -``` - -![查看MySQL提供的所有存储引擎](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/mysql-engines.png) - -从上图我们可以查看出 MySQL 当前默认的存储引擎是InnoDB,并且在5.7版本所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。 - -**查看MySQL当前默认的存储引擎** - -我们也可以通过下面的命令查看默认的存储引擎。 - -```sql -mysql> show variables like '%storage_engine%'; -``` - -**查看表的存储引擎** - -```sql -show table status like "table_name" ; -``` - -![查看表的存储引擎](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/查看表的存储引擎.png) - -#### MyISAM和InnoDB区别 - -MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。不过,5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。 - -大多数时候我们使用的都是 InnoDB 存储引擎,但是在某些情况下使用 MyISAM 也是合适的比如读密集的情况下。(如果你不介意 MyISAM 崩溃恢复问题的话)。 - -**两者的对比:** - -1. **是否支持行级锁** : MyISAM 只有表级锁(table-level locking),而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。 -2. **是否支持事务和崩溃后的安全恢复: MyISAM** 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是**InnoDB** 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 -3. **是否支持外键:** MyISAM不支持,而InnoDB支持。 -4. **是否支持MVCC** :仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 `READ COMMITTED` 和 `REPEATABLE READ` 两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。推荐阅读:[MySQL-InnoDB-MVCC多版本并发控制](https://segmentfault.com/a/1190000012650596) -5. ...... - -《MySQL高性能》上面有一句话这样写到: - -> 不要轻易相信“MyISAM比InnoDB快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB的速度都可以让MyISAM望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。 - -一般情况下我们选择 InnoDB 都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择MyISAM也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。 - -### 字符集及校对规则 - -字符集指的是一种从二进制编码到某类字符符号的映射。校对规则则是指某种字符集下的排序规则。MySQL中每一种字符集都会对应一系列的校对规则。 - -MySQL采用的是类似继承的方式指定字符集的默认值,每个数据库以及每张数据表都有自己的默认值,他们逐层继承。比如:某个库中所有表的默认字符集将是该数据库所指定的字符集(这些表在没有指定字符集的情况下,才会采用默认字符集) PS:整理自《Java工程师修炼之道》 - -详细内容可以参考: [MySQL字符集及校对规则的理解](https://www.cnblogs.com/geaozhang/p/6724393.html#MySQLyuzifuji) - -### 索引 - -MySQL索引使用的数据结构主要有**BTree索引** 和 **哈希索引** 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。 - -MySQL的BTree索引使用的是B树中的B+Tree,但对于主要的两种存储引擎的实现方式是不同的。 - -- **MyISAM:** B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。 -- **InnoDB:** 其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。**在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。** **因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。** PS:整理自《Java工程师修炼之道》 - -**更多关于索引的内容可以查看文档首页MySQL目录下关于索引的详细总结。** - -### 查询缓存的使用 - -> 执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用 - -my.cnf加入以下配置,重启MySQL开启查询缓存 -```properties -query_cache_type=1 -query_cache_size=600000 -``` - -MySQL执行以下命令也可以开启查询缓存 - -```properties -set global query_cache_type=1; -set global query_cache_size=600000; -``` -如上,**开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果**。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。因此任何两个查询在任何字符上的不同都会导致缓存不命中。此外,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL库中的系统表,其查询结果也不会被缓存。 - -缓存建立之后,MySQL的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。 - -**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启缓存查询要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。此外,**还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存:** -```sql -select sql_no_cache count(*) from usr; -``` - -### 什么是事务? - -**事务是逻辑上的一组操作,要么都执行,要么都不执行。** - -事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。 - -### 事物的四大特性(ACID) - -![事物的特性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/事务特性.png) - -1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; -2. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的; -3. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; -4. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 - -### 并发事务带来哪些问题? - -在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。 - -- **脏读(Dirty read):** 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。 -- **丢失修改(Lost to modify):** 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。 -- **不可重复读(Unrepeatableread):** 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。 -- **幻读(Phantom read):** 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。 - -**不可重复读和幻读区别:** - -不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。 - -### 事务隔离级别有哪些?MySQL的默认隔离级别是? - -**SQL 标准定义了四个隔离级别:** - -- **READ-UNCOMMITTED(读取未提交):** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读**。 -- **READ-COMMITTED(读取已提交):** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生**。 -- **REPEATABLE-READ(可重复读):** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生**。 -- **SERIALIZABLE(可串行化):** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。 - ------- - -| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | -| :--------------: | :--: | :--------: | :----: | -| READ-UNCOMMITTED | √ | √ | √ | -| READ-COMMITTED | × | √ | √ | -| REPEATABLE-READ | × | × | √ | -| SERIALIZABLE | × | × | × | - -MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看 - -```sql -mysql> SELECT @@tx_isolation; -+-----------------+ -| @@tx_isolation | -+-----------------+ -| REPEATABLE-READ | -+-----------------+ -``` - -这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 **REPEATABLE-READ(可重读)** -事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server) -是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)** 已经可以完全保证事务的隔离性要求,即达到了 - SQL标准的 **SERIALIZABLE(可串行化)** 隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 **READ-COMMITTED(读取提交内容)** ,但是你要知道的是InnoDB 存储引擎默认使用 **REPEAaTABLE-READ(可重读)** 并不会有任何性能损失。 - -InnoDB 存储引擎在 **分布式事务** 的情况下一般会用到 **SERIALIZABLE(可串行化)** 隔离级别。 - -### 锁机制与InnoDB锁算法 - -**MyISAM和InnoDB存储引擎使用的锁:** - -- MyISAM采用表级锁(table-level locking)。 -- InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁 - -**表级锁和行级锁对比:** - -- **表级锁:** MySQL中锁定 **粒度最大** 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。 -- **行级锁:** MySQL中锁定 **粒度最小** 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 - -详细内容可以参考: MySQL锁机制简单了解一下:[https://blog.csdn.net/qq_34337272/article/details/80611486](https://blog.csdn.net/qq_34337272/article/details/80611486) - -**InnoDB存储引擎的锁的算法有三种:** - -- Record lock:单个行记录上的锁 -- Gap lock:间隙锁,锁定一个范围,不包括记录本身 -- Next-key lock:record+gap 锁定一个范围,包含记录本身 - -**相关知识点:** - -1. innodb对于行的查询使用next-key lock -2. Next-locking keying为了解决Phantom Problem幻读问题 -3. 当查询的索引含有唯一属性时,将next-key lock降级为record key -4. Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生 -5. 有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1 - -### 大表优化 - -当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下: - -#### 1. 限定数据的范围 - -务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内; - -#### 2. 读/写分离 - -经典的数据库拆分方案,主库负责写,从库负责读; - -#### 3. 垂直分区 - - **根据数据库里面数据表的相关性进行拆分。** 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。 - - **简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。** 如下图所示,这样来说大家应该就更容易理解了。 - ![数据库垂直分区](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数据库垂直分区.png) - -- **垂直拆分的优点:** 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。 -- **垂直拆分的缺点:** 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂; - -#### 4. 水平分区 - -**保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。** - - 水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。 - -![数据库水平拆分](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数据库水平拆分.png) - -水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 **水平拆分最好分库** 。 - -水平拆分能够 **支持非常大的数据量存储,应用端改造也少**,但 **分片事务难以解决** ,跨节点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐 **尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度** ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。 - -**下面补充一下数据库分片的两种常见方案:** - -- **客户端代理:** **分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。** 当当网的 **Sharding-JDBC** 、阿里的TDDL是两种比较常用的实现。 -- **中间件代理:** **在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。** 我们现在谈的 **Mycat** 、360的Atlas、网易的DDB等等都是这种架构的实现。 - -详细内容可以参考: MySQL大表优化方案: [https://segmentfault.com/a/1190000006158186](https://segmentfault.com/a/1190000006158186) - -### 解释一下什么是池化设计思想。什么是数据库连接池?为什么需要数据库连接池? - -池化设计应该不是一个新名词。我们常见的如java线程池、jdbc连接池、redis连接池等就是这类设计的代表实现。这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。就好比你去食堂打饭,打饭的大妈会先把饭盛好几份放那里,你来了就直接拿着饭盒加菜即可,不用再临时又盛饭又打菜,效率就高了。除了初始化资源,池化设计还包括如下这些特征:池子的初始值、池子的活跃值、池子的最大值等,这些特征可以直接映射到java线程池和数据库连接池的成员属性中。这篇文章对[池化设计思想](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485679&idx=1&sn=57dbca8c9ad49e1f3968ecff04a4f735&chksm=cea24724f9d5ce3212292fac291234a760c99c0960b5430d714269efe33554730b5f71208582&token=1141994790&lang=zh_CN#rd)介绍的还不错,直接复制过来,避免重复造轮子了。 - -数据库连接本质就是一个 socket 的连接。数据库服务端还要维护一些缓存和用户权限信息之类的 所以占用了一些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序的请求,既昂贵又浪费资源。**在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中**。 连接池还减少了用户必须等待建立与数据库的连接的时间。 - -### 分库分表之后,id 主键如何处理? - -因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要一个全局唯一的 id 来支持。 - -生成全局 id 有下面这几种方式: - -- **UUID**:不适合作为主键,因为太长了,并且无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字。 -- **数据库自增 id** : 两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。这种方式生成的 id 有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。 -- **利用 redis 生成 id :** 性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本。 -- **Twitter的snowflake算法** :Github 地址:https://github.com/twitter-archive/snowflake。 -- **美团的[Leaf](https://tech.meituan.com/2017/04/21/mt-leaf.html)分布式ID生成系统** :Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、Zookeeper等中间件。感觉还不错。美团技术团队的一篇文章:https://tech.meituan.com/2017/04/21/mt-leaf.html 。 -- ...... - -### 一条SQL语句在MySQL中如何执行的 - -[一条SQL语句在MySQL中如何执行的]() - -### MySQL高性能优化规范建议 - -[MySQL高性能优化规范建议]() - -### 一条SQL语句执行得很慢的原因有哪些? - -[腾讯面试:一条SQL语句执行得很慢的原因有哪些?---不看后悔系列](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485185&idx=1&sn=66ef08b4ab6af5757792223a83fc0d45&chksm=cea248caf9d5c1dc72ec8a281ec16aa3ec3e8066dbb252e27362438a26c33fbe842b0e0adf47&token=79317275&lang=zh_CN#rd) - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/database/Redis/Redis.md b/docs/database/Redis/Redis.md deleted file mode 100644 index a17c7f042dd..00000000000 --- a/docs/database/Redis/Redis.md +++ /dev/null @@ -1,370 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [redis 简介](#redis-简介) -- [为什么要用 redis/为什么要用缓存](#为什么要用-redis为什么要用缓存) -- [为什么要用 redis 而不用 map/guava 做缓存?](#为什么要用-redis-而不用-mapguava-做缓存) -- [redis 和 memcached 的区别](#redis-和-memcached-的区别) -- [redis 常见数据结构以及使用场景分析](#redis-常见数据结构以及使用场景分析) - - [1.String](#1string) - - [2.Hash](#2hash) - - [3.List](#3list) - - [4.Set](#4set) - - [5.Sorted Set](#5sorted-set) -- [redis 设置过期时间](#redis-设置过期时间) -- [redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?)](#redis-内存淘汰机制mysql里有2000w数据redis中只存20w的数据如何保证redis中的数据都是热点数据) -- [redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)](#redis-持久化机制怎么保证-redis-挂掉之后再重启数据可以进行恢复) -- [redis 事务](#redis-事务) -- [缓存雪崩和缓存穿透问题解决方案](#缓存雪崩和缓存穿透问题解决方案) -- [如何解决 Redis 的并发竞争 Key 问题](#如何解决-redis-的并发竞争-key-问题) -- [如何保证缓存与数据库双写时的数据一致性?](#如何保证缓存与数据库双写时的数据一致性) - - - -### redis 简介 - -简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。 - -### 为什么要用 redis/为什么要用缓存 - -主要从“高性能”和“高并发”这两点来看待这个问题。 - -**高性能:** - -假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可! - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-24/54316596.jpg) - - -**高并发:** - -直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。 - - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-24/85146760.jpg) - - -### 为什么要用 redis 而不用 map/guava 做缓存? - - ->下面的内容来自 segmentfault 一位网友的提问,地址:https://segmentfault.com/q/1010000009106416 - -缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。 - -使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。 - -### redis 的线程模型 - -> 参考地址:https://www.javazhiyin.com/22943.html - -redis 内部使用文件事件处理器 `file event handler`,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。 - -文件事件处理器的结构包含 4 个部分: - -- 多个 socket -- IO 多路复用程序 -- 文件事件分派器 -- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) - -多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。 - - -### redis 和 memcached 的区别 - -对于 redis 和 memcached 我总结了下面四点。现在公司一般都是用 redis 来实现缓存,而且 redis 自身也越来越强大了! - -1. **redis支持更丰富的数据类型(支持更复杂的应用场景)**:Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。 -2. **Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。** -3. **集群模式**:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的. -4. **Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。** - - -> 来自网络上的一张图,这里分享给大家! - -![redis 和 memcached 的区别](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-24/61603179.jpg) - - -### redis 常见数据结构以及使用场景分析 - -#### 1.String - -> **常用命令:** set,get,decr,incr,mget 等。 - - -String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 -常规key-value缓存应用; -常规计数:微博数,粉丝数等。 - -#### 2.Hash -> **常用命令:** hget,hset,hgetall 等。 - -hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息: - -``` -key=JavaUser293847 -value={ - “id”: 1, - “name”: “SnailClimb”, - “age”: 22, - “location”: “Wuhan, Hubei” -} - -``` - - -#### 3.List -> **常用命令:** lpush,rpush,lpop,rpop,lrange等 - -list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。 - -Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。 - -另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。 - -#### 4.Set - -> **常用命令:** -sadd,spop,smembers,sunion 等 - -set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。 - -当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。 - -比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下: - -``` -sinterstore key1 key2 key3 将交集存在key1内 -``` - -#### 5.Sorted Set -> **常用命令:** zadd,zrange,zrem,zcard等 - - -和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。 - -**举例:** 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。 - - -### redis 设置过期时间 - -Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。 - -我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。 - -如果假设你设置了一批 key 只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的? - -**定期删除+惰性删除。** - -通过名字大概就能猜出这两个删除方式的意思了。 - -- **定期删除**:redis默认是每隔 100ms 就**随机抽取**一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载! -- **惰性删除** :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的哈! - - -但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? **redis 内存淘汰机制。** - -### redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?) - -redis 配置文件 redis.conf 中有相关注释,我这里就不贴了,大家可以自行查阅或者通过这个网址查看: [http://download.redis.io/redis-stable/redis.conf](http://download.redis.io/redis-stable/redis.conf) - -**redis 提供 6种数据淘汰策略:** - -1. **volatile-lru**:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 -2. **volatile-ttl**:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 -3. **volatile-random**:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 -4. **allkeys-lru**:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的) -5. **allkeys-random**:从数据集(server.db[i].dict)中任意选择数据淘汰 -6. **no-eviction**:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧! - -4.0版本后增加以下两种: - -7. **volatile-lfu**:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰 -8. **allkeys-lfu**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key - -**备注: 关于 redis 设置过期时间以及内存淘汰机制,我这里只是简单的总结一下,后面会专门写一篇文章来总结!** - - -### redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复) - -很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。 - -Redis不同于Memcached的很重一点就是,Redis支持持久化,而且支持两种不同的持久化操作。**Redis的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)**。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。 - -**快照(snapshotting)持久化(RDB)** - -Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。 - -快照持久化是Redis默认采用的持久化方式,在redis.conf配置文件中默认有此下配置: - -```conf - -save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 - -save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 - -save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 -``` - -**AOF(append-only file)持久化** - -与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启: - -```conf -appendonly yes -``` - -开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。 - -在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是: - -```conf -appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 -appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘 -appendfsync no #让操作系统决定何时进行同步 -``` - -为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。 - -**Redis 4.0 对于持久化机制的优化** - -Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。 - -如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。 - -**补充内容:AOF 重写** - -AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。 - -AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入、分析或者写入操作。 - -在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作 - -**更多内容可以查看我的这篇文章:** - -- [Redis持久化](Redis持久化.md) - - -### redis 事务 - -Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。 - -在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。 - -补充内容: - -> 1. redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。(来自[issue:关于Redis事务不是原子性问题](https://github.com/Snailclimb/JavaGuide/issues/452) ) - -### 缓存雪崩和缓存穿透问题解决方案 - -#### **缓存雪崩** - -**什么是缓存雪崩?** - -简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。 - -**有哪些解决办法?** - -(中华石杉老师在他的视频中提到过,视频地址在最后一个问题中有提到): - -- 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。 -- 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉 -- 事后:利用 redis 持久化机制保存的数据尽快恢复缓存 - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-25/6078367.jpg) - -#### **缓存穿透** - -**什么是缓存穿透?** - -缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。下面用图片展示一下(这两张图片不是我画的,为了省事直接在网上找的,这里说明一下): - -**正常缓存处理流程:** - - - -**缓存穿透情况处理流程:** - - - -一般MySQL 默认的最大连接数在 150 左右,这个可以通过 `show variables like '%max_connections%'; `命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等无力条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 个并发请求就能打死大部分数据库了。 - -**有哪些解决办法?** - -最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。 - -**1)缓存无效 key** : 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求key,会导致 redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。 - -另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值`。 - - 如果用 Java 代码展示的话,差不多是下面这样的: - -```java -public Object getObjectInclNullById(Integer id) { - // 从缓存中获取数据 - Object cacheValue = cache.get(id); - // 缓存为空 - if (cacheValue == null) { - // 从数据库中获取 - Object storageValue = storage.get(key); - // 缓存空对象 - cache.set(key, storageValue); - // 如果存储数据为空,需要设置一个过期时间(300秒) - if (storageValue == null) { - // 必须设置过期时间,否则有被攻击的风险 - cache.expire(key, 60 * 5); - } - return storageValue; - } - return cacheValue; -} -``` - -**2)布隆过滤器:**布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在与海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。总结一下就是下面这张图(这张图片不是我画的,为了省事直接在网上找的): - - - -更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md) ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。 - -### 如何解决 Redis 的并发竞争 Key 问题 - -所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同! - -推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能) - -基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。 - -在实践中,当然是从以可靠性为主。所以首推Zookeeper。 - -参考: - -- https://www.jianshu.com/p/8bddd381de06 - -### 如何保证缓存与数据库双写时的数据一致性? - -> 一般情况下我们都是这样使用缓存的:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。这种方式很明显会存在缓存和数据库的数据不一致的情况。 - -你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题? - -一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况 - -串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。 - -更多内容可以查看:https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md - -**参考:** Java工程师面试突击第1季(可能是史上最好的Java面试突击课程)-中华石杉老师!公众号后台回复关键字“1”即可获取该视频内容。 - -### 参考 - -- 《Redis开发与运维》 -- Redis 命令总结:http://redisdoc.com/string/set.html - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/database/Redis/Redis\346\214\201\344\271\205\345\214\226.md" "b/docs/database/Redis/Redis\346\214\201\344\271\205\345\214\226.md" deleted file mode 100644 index fbad95556a0..00000000000 --- "a/docs/database/Redis/Redis\346\214\201\344\271\205\345\214\226.md" +++ /dev/null @@ -1,118 +0,0 @@ - - -非常感谢《redis实战》真本书,本文大多内容也参考了书中的内容。非常推荐大家看一下《redis实战》这本书,感觉书中的很多理论性东西还是很不错的。 - -为什么本文的名字要加上春夏秋冬又一春,哈哈 ,这是一部韩国的电影,我感觉电影不错,所以就用在文章名字上了,没有什么特别的含义,然后下面的有些配图也是电影相关镜头。 - -![春夏秋冬又一春](https://user-gold-cdn.xitu.io/2018/6/13/163f97071d71f6de?w=1280&h=720&f=jpeg&s=205252) - -**很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后回复数据),或者是为了防止系统故障而将数据备份到一个远程位置。** - -Redis不同于Memcached的很重一点就是,**Redis支持持久化**,而且支持两种不同的持久化操作。Redis的一种持久化方式叫**快照(snapshotting,RDB)**,另一种方式是**只追加文件(append-only file,AOF)**.这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。 - - -## 快照(snapshotting)持久化 - -Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。 - - -![春夏秋冬又一春](https://user-gold-cdn.xitu.io/2018/6/13/163f97568281782a?w=600&h=329&f=jpeg&s=88616) - -**快照持久化是Redis默认采用的持久化方式**,在redis.conf配置文件中默认有此下配置: -``` - -save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 - -save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 - -save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 -``` - -根据配置,快照将被写入dbfilename选项指定的文件里面,并存储在dir选项指定的路径上面。如果在新的快照文件创建完毕之前,Redis、系统或者硬件这三者中的任意一个崩溃了,那么Redis将丢失最近一次创建快照写入的所有数据。 - -举个例子:假设Redis的上一个快照是2:35开始创建的,并且已经创建成功。下午3:06时,Redis又开始创建新的快照,并且在下午3:08快照创建完毕之前,有35个键进行了更新。如果在下午3:06到3:08期间,系统发生了崩溃,导致Redis无法完成新快照的创建工作,那么Redis将丢失下午2:35之后写入的所有数据。另一方面,如果系统恰好在新的快照文件创建完毕之后崩溃,那么Redis将丢失35个键的更新数据。 - -**创建快照的办法有如下几种:** - -- **BGSAVE命令:** 客户端向Redis发送 **BGSAVE命令** 来创建一个快照。对于支持BGSAVE命令的平台来说(基本上所有平台支持,除了Windows平台),Redis会调用fork来创建一个子进程,然后子进程负责将快照写入硬盘,而父进程则继续处理命令请求。 -- **SAVE命令:** 客户端还可以向Redis发送 **SAVE命令** 来创建一个快照,接到SAVE命令的Redis服务器在快照创建完毕之前不会再响应任何其他命令。SAVE命令不常用,我们通常只会在没有足够内存去执行BGSAVE命令的情况下,又或者即使等待持久化操作执行完毕也无所谓的情况下,才会使用这个命令。 -- **save选项:** 如果用户设置了save选项(一般会默认设置),比如 **save 60 10000**,那么从Redis最近一次创建快照之后开始算起,当“60秒之内有10000次写入”这个条件被满足时,Redis就会自动触发BGSAVE命令。 -- **SHUTDOWN命令:** 当Redis通过SHUTDOWN命令接收到关闭服务器的请求时,或者接收到标准TERM信号时,会执行一个SAVE命令,阻塞所有客户端,不再执行客户端发送的任何命令,并在SAVE命令执行完毕之后关闭服务器。 -- **一个Redis服务器连接到另一个Redis服务器:** 当一个Redis服务器连接到另一个Redis服务器,并向对方发送SYNC命令来开始一次复制操作的时候,如果主服务器目前没有执行BGSAVE操作,或者主服务器并非刚刚执行完BGSAVE操作,那么主服务器就会执行BGSAVE命令 - -如果系统真的发生崩溃,用户将丢失最近一次生成快照之后更改的所有数据。因此,快照持久化只适用于即使丢失一部分数据也不会造成一些大问题的应用程序。不能接受这个缺点的话,可以考虑AOF持久化。 - - - -## **AOF(append-only file)持久化** -与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启: -``` -appendonly yes -``` - -开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。 - -![春夏秋冬又一春](https://user-gold-cdn.xitu.io/2018/6/13/163f976818876166?w=400&h=219&f=jpeg&s=91022) - -**在Redis的配置文件中存在三种同步方式,它们分别是:** - -``` - -appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 -appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘 -appendfsync no #让操作系统决定何时进行同步 -``` - -**appendfsync always** 可以实现将数据丢失减到最少,不过这种方式需要对硬盘进行大量的写入而且每次只写入一个命令,十分影响Redis的速度。另外使用固态硬盘的用户谨慎使用appendfsync always选项,因为这会明显降低固态硬盘的使用寿命。 - -为了兼顾数据和写入性能,用户可以考虑 **appendfsync everysec选项** ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。 - - -**appendfsync no** 选项一般不推荐,这种方案会使Redis丢失不定量的数据而且如果用户的硬盘处理写入操作的速度不够的话,那么当缓冲区被等待写入的数据填满时,Redis的写入操作将被阻塞,这会导致Redis的请求速度变慢。 - -**虽然AOF持久化非常灵活地提供了多种不同的选项来满足不同应用程序对数据安全的不同要求,但AOF持久化也有缺陷——AOF文件的体积太大。** - -## 重写/压缩AOF - -AOF虽然在某个角度可以将数据丢失降低到最小而且对性能影响也很小,但是极端的情况下,体积不断增大的AOF文件很可能会用完硬盘空间。另外,如果AOF体积过大,那么还原操作执行时间就可能会非常长。 - -为了解决AOF体积过大的问题,用户可以向Redis发送 **BGREWRITEAOF命令** ,这个命令会通过移除AOF文件中的冗余命令来重写(rewrite)AOF文件来减小AOF文件的体积。BGREWRITEAOF命令和BGSAVE创建快照原理十分相似,所以AOF文件重写也需要用到子进程,这样会导致性能问题和内存占用问题,和快照持久化一样。更糟糕的是,如果不加以控制的话,AOF文件的体积可能会比快照文件大好几倍。 - -**文件重写流程:** - -![文件重写流程](https://user-gold-cdn.xitu.io/2018/6/13/163f97f9bd0eea50?w=380&h=345&f=jpeg&s=14501) -和快照持久化可以通过设置save选项来自动执行BGSAVE一样,AOF持久化也可以通过设置 - -``` -auto-aof-rewrite-percentage -``` - -选项和 - -``` -auto-aof-rewrite-min-size -``` - -选项自动执行BGREWRITEAOF命令。举例:假设用户对Redis设置了如下配置选项并且启用了AOF持久化。那么当AOF文件体积大于64mb,并且AOF的体积比上一次重写之后的体积大了至少一倍(100%)的时候,Redis将执行BGREWRITEAOF命令。 - -``` -auto-aof-rewrite-percentage 100 -auto-aof-rewrite-min-size 64mb -``` - -无论是AOF持久化还是快照持久化,将数据持久化到硬盘上都是非常有必要的,但除了进行持久化外,用户还必须对持久化得到的文件进行备份(最好是备份到不同的地方),这样才能尽量避免数据丢失事故发生。如果条件允许的话,最好能将快照文件和重新重写的AOF文件备份到不同的服务器上面。 - -随着负载量的上升,或者数据的完整性变得 越来越重要时,用户可能需要使用到复制特性。 - -## Redis 4.0 对于持久化机制的优化 -Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。 - -如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分就是压缩格式不再是 AOF 格式,可读性较差。 - -参考: - -《Redis实战》 - -[深入学习Redis(2):持久化](https://www.cnblogs.com/kismetv/p/9137897.html) - - diff --git "a/docs/database/Redis/Redlock\345\210\206\345\270\203\345\274\217\351\224\201.md" "b/docs/database/Redis/Redlock\345\210\206\345\270\203\345\274\217\351\224\201.md" deleted file mode 100644 index 86a15ff6faf..00000000000 --- "a/docs/database/Redis/Redlock\345\210\206\345\270\203\345\274\217\351\224\201.md" +++ /dev/null @@ -1,47 +0,0 @@ -这篇文章主要是对 Redis 官方网站刊登的 [Distributed locks with Redis](https://redis.io/topics/distlock) 部分内容的总结和翻译。 - -## 什么是 RedLock - -Redis 官方站这篇文章提出了一种权威的基于 Redis 实现分布式锁的方式名叫 *Redlock*,此种方式比原先的单节点的方法更安全。它可以保证以下特性: - -1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁 -2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区 -3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务 - -## 怎么在单节点上实现分布式锁 - -> SET resource_name my_random_value NX PX 30000 - -主要依靠上述命令,该命令仅当 Key 不存在时(NX保证)set 值,并且设置过期时间 3000ms (PX保证),值 my_random_value 必须是所有 client 和所有锁请求发生期间唯一的,释放锁的逻辑是: - -```lua -if redis.call("get",KEYS[1]) == ARGV[1] then - return redis.call("del",KEYS[1]) -else - return 0 -end -``` - -上述实现可以避免释放另一个client创建的锁,如果只有 del 命令的话,那么如果 client1 拿到 lock1 之后因为某些操作阻塞了很长时间,此时 Redis 端 lock1 已经过期了并且已经被重新分配给了 client2,那么 client1 此时再去释放这把锁就会造成 client2 原本获取到的锁被 client1 无故释放了,但现在为每个 client 分配一个 unique 的 string 值可以避免这个问题。至于如何去生成这个 unique string,方法很多随意选择一种就行了。 - -## Redlock 算法 - -算法很易懂,起 5 个 master 节点,分布在不同的机房尽量保证可用性。为了获得锁,client 会进行如下操作: - -1. 得到当前的时间,微秒单位 -2. 尝试顺序地在 5 个实例上申请锁,当然需要使用相同的 key 和 random value,这里一个 client 需要合理设置与 master 节点沟通的 timeout 大小,避免长时间和一个 fail 了的节点浪费时间 -3. 当 client 在大于等于 3 个 master 上成功申请到锁的时候,且它会计算申请锁消耗了多少时间,这部分消耗的时间采用获得锁的当下时间减去第一步获得的时间戳得到,如果锁的持续时长(lock validity time)比流逝的时间多的话,那么锁就真正获取到了。 -4. 如果锁申请到了,那么锁真正的 lock validity time 应该是 origin(lock validity time) - 申请锁期间流逝的时间 -5. 如果 client 申请锁失败了,那么它就会在少部分申请成功锁的 master 节点上执行释放锁的操作,重置状态 - -## 失败重试 - -如果一个 client 申请锁失败了,那么它需要稍等一会在重试避免多个 client 同时申请锁的情况,最好的情况是一个 client 需要几乎同时向 5 个 master 发起锁申请。另外就是如果 client 申请锁失败了它需要尽快在它曾经申请到锁的 master 上执行 unlock 操作,便于其他 client 获得这把锁,避免这些锁过期造成的时间浪费,当然如果这时候网络分区使得 client 无法联系上这些 master,那么这种浪费就是不得不付出的代价了。 - -## 放锁 - -放锁操作很简单,就是依次释放所有节点上的锁就行了 - -## 性能、崩溃恢复和 fsync - -如果我们的节点没有持久化机制,client 从 5 个 master 中的 3 个处获得了锁,然后其中一个重启了,这是注意 **整个环境中又出现了 3 个 master 可供另一个 client 申请同一把锁!** 违反了互斥性。如果我们开启了 AOF 持久化那么情况会稍微好转一些,因为 Redis 的过期机制是语义层面实现的,所以在 server 挂了的时候时间依旧在流逝,重启之后锁状态不会受到污染。但是考虑断电之后呢,AOF部分命令没来得及刷回磁盘直接丢失了,除非我们配置刷回策略为 fsnyc = always,但这会损伤性能。解决这个问题的方法是,当一个节点重启之后,我们规定在 max TTL 期间它是不可用的,这样它就不会干扰原本已经申请到的锁,等到它 crash 前的那部分锁都过期了,环境不存在历史锁了,那么再把这个节点加进来正常工作。 diff --git a/docs/database/Redis/images/redis-all/redis-list.drawio b/docs/database/Redis/images/redis-all/redis-list.drawio new file mode 100644 index 00000000000..afa767154b7 --- /dev/null +++ b/docs/database/Redis/images/redis-all/redis-list.drawio @@ -0,0 +1 @@ +7VlNc5swFPw1PiaDENjmmNjpx6Gdjt1J2lNHAzKoEYgRcmzn11cYyRgJT1w3DnScU3grIaHd5b2HM4CTdP2Rozz5wiJMB64TrQdwOnBdEEAo/5TIpkICx6mAmJNITaqBOXnGCtTTliTCRWOiYIwKkjfBkGUZDkUDQ5yzVXPagtHmrjmKsQXMQ0Rt9IFEIqnQsTuq8U+YxIneGQyDaiRFerI6SZGgiK32IHg3gBPOmKiu0vUE05I8zUt134cDo7sH4zgTx9wwu/+V/H4g/myezsD35/vZ56/ulVrlCdGlOrB6WLHRDHC2zCJcLgIG8HaVEIHnOQrL0ZXUXGKJSKkaLh6xCBMVLFgmlKJgJGO1F+YCrw8eAuyokZ7CLMWCb+QUdcOVp9hUdnKhile1OL62WLInzEhhSPkh3i1dUyYvFGt/waBrMbi9BD3lUfPm2bwFLbT556INttPWc/sB035d0+i10wj7TaM77hmNw5fTIM6im7KeyCikqChI2ORMHp1vfsjA0cHPMrh2fR1P1/uj042KjiAbR1aRMqiWVRHxGIuXUr0tSSNxHqacY4oEeWo+RpsOaodvjMgHrPM2gKbk4+YaBVvyEKvb9suYuZLpHdcwRUWEtdDWF7tzn26V0f9ula4s4A0N4cbOtX+aB3zfWupNPaCb01cxAXjPF0cofGq68AJjIT8wXXdusxzRY/fbLJ0VDbPbOj1j2PXnrVOG/Z3A82WRWFaQXZUwujHB2SOeMMq4RDKW4VJLQqkBIUrirHSQVA9L/Lbs0Yj8lL1RAymJonKb1vavbhCP9M0/dYA+OFDF95zltTjLLPav1gECu5PmOcvf5aloD7qWx7fkoRf8+siiYggEuhbI/oSil/v+2Pp0/gLZ3y18u20/BTqDJp5O8VoSp2tJxvYrgxcXpIjV351PERnWv7RXPV39/wp49wc= \ No newline at end of file diff --git a/docs/database/Redis/images/redis-all/redis-list.png b/docs/database/Redis/images/redis-all/redis-list.png new file mode 100644 index 00000000000..4fb4e36cb49 Binary files /dev/null and b/docs/database/Redis/images/redis-all/redis-list.png differ diff --git a/docs/database/Redis/images/redis-all/redis-rollBack.png b/docs/database/Redis/images/redis-all/redis-rollBack.png new file mode 100644 index 00000000000..91f7f46d66d Binary files /dev/null and b/docs/database/Redis/images/redis-all/redis-rollBack.png differ diff --git a/docs/database/Redis/images/redis-all/redis-vs-memcached.png b/docs/database/Redis/images/redis-all/redis-vs-memcached.png new file mode 100644 index 00000000000..23844d67e6f Binary files /dev/null and b/docs/database/Redis/images/redis-all/redis-vs-memcached.png differ diff --git a/docs/database/Redis/images/redis-all/redis4.0-more-thread.png b/docs/database/Redis/images/redis-all/redis4.0-more-thread.png new file mode 100644 index 00000000000..e7e19e52e17 Binary files /dev/null and b/docs/database/Redis/images/redis-all/redis4.0-more-thread.png differ diff --git "a/docs/database/Redis/images/redis-all/redis\344\272\213\344\273\266\345\244\204\347\220\206\345\231\250.png" "b/docs/database/Redis/images/redis-all/redis\344\272\213\344\273\266\345\244\204\347\220\206\345\231\250.png" new file mode 100644 index 00000000000..fc280fffaba Binary files /dev/null and "b/docs/database/Redis/images/redis-all/redis\344\272\213\344\273\266\345\244\204\347\220\206\345\231\250.png" differ diff --git "a/docs/database/Redis/images/redis-all/redis\344\272\213\345\212\241.png" "b/docs/database/Redis/images/redis-all/redis\344\272\213\345\212\241.png" new file mode 100644 index 00000000000..eb0c404cafd Binary files /dev/null and "b/docs/database/Redis/images/redis-all/redis\344\272\213\345\212\241.png" differ diff --git "a/docs/database/Redis/images/redis-all/redis\350\277\207\346\234\237\346\227\266\351\227\264.png" "b/docs/database/Redis/images/redis-all/redis\350\277\207\346\234\237\346\227\266\351\227\264.png" new file mode 100644 index 00000000000..27df6ead8e4 Binary files /dev/null and "b/docs/database/Redis/images/redis-all/redis\350\277\207\346\234\237\346\227\266\351\227\264.png" differ diff --git a/docs/database/Redis/images/redis-all/try-redis.png b/docs/database/Redis/images/redis-all/try-redis.png new file mode 100644 index 00000000000..cd21a6518e4 Binary files /dev/null and b/docs/database/Redis/images/redis-all/try-redis.png differ diff --git a/docs/database/Redis/images/redis-all/what-is-redis.png b/docs/database/Redis/images/redis-all/what-is-redis.png new file mode 100644 index 00000000000..913881ac6cf Binary files /dev/null and b/docs/database/Redis/images/redis-all/what-is-redis.png differ diff --git "a/docs/database/Redis/images/redis-all/\344\275\277\347\224\250\347\274\223\345\255\230\344\271\213\345\220\216.png" "b/docs/database/Redis/images/redis-all/\344\275\277\347\224\250\347\274\223\345\255\230\344\271\213\345\220\216.png" new file mode 100644 index 00000000000..2c73bd90276 Binary files /dev/null and "b/docs/database/Redis/images/redis-all/\344\275\277\347\224\250\347\274\223\345\255\230\344\271\213\345\220\216.png" differ diff --git "a/docs/database/Redis/images/redis-all/\345\212\240\345\205\245\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250\345\220\216\347\232\204\347\274\223\345\255\230\345\244\204\347\220\206\346\265\201\347\250\213.png" "b/docs/database/Redis/images/redis-all/\345\212\240\345\205\245\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250\345\220\216\347\232\204\347\274\223\345\255\230\345\244\204\347\220\206\346\265\201\347\250\213.png" new file mode 100644 index 00000000000..a2c2ed6906f Binary files /dev/null and "b/docs/database/Redis/images/redis-all/\345\212\240\345\205\245\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250\345\220\216\347\232\204\347\274\223\345\255\230\345\244\204\347\220\206\346\265\201\347\250\213.png" differ diff --git "a/docs/database/Redis/images/redis-all/\345\215\225\344\275\223\346\236\266\346\236\204.png" "b/docs/database/Redis/images/redis-all/\345\215\225\344\275\223\346\236\266\346\236\204.png" new file mode 100644 index 00000000000..648a404af8c Binary files /dev/null and "b/docs/database/Redis/images/redis-all/\345\215\225\344\275\223\346\236\266\346\236\204.png" differ diff --git "a/docs/database/Redis/images/redis-all/\347\274\223\345\255\230\347\232\204\345\244\204\347\220\206\346\265\201\347\250\213.png" "b/docs/database/Redis/images/redis-all/\347\274\223\345\255\230\347\232\204\345\244\204\347\220\206\346\265\201\347\250\213.png" new file mode 100644 index 00000000000..11860ae1f02 Binary files /dev/null and "b/docs/database/Redis/images/redis-all/\347\274\223\345\255\230\347\232\204\345\244\204\347\220\206\346\265\201\347\250\213.png" differ diff --git "a/docs/database/Redis/images/redis-all/\347\274\223\345\255\230\347\251\277\351\200\217\346\203\205\345\206\265.png" "b/docs/database/Redis/images/redis-all/\347\274\223\345\255\230\347\251\277\351\200\217\346\203\205\345\206\265.png" new file mode 100644 index 00000000000..e7298c15ed6 Binary files /dev/null and "b/docs/database/Redis/images/redis-all/\347\274\223\345\255\230\347\251\277\351\200\217\346\203\205\345\206\265.png" differ diff --git "a/docs/database/Redis/images/redis-all/\351\233\206\344\270\255\345\274\217\347\274\223\345\255\230\346\236\266\346\236\204.png" "b/docs/database/Redis/images/redis-all/\351\233\206\344\270\255\345\274\217\347\274\223\345\255\230\346\236\266\346\236\204.png" new file mode 100644 index 00000000000..5aff414baa4 Binary files /dev/null and "b/docs/database/Redis/images/redis-all/\351\233\206\344\270\255\345\274\217\347\274\223\345\255\230\346\236\266\346\236\204.png" differ diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.drawio" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.drawio" new file mode 100644 index 00000000000..bc4c6d0cca7 --- /dev/null +++ "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.drawio" @@ -0,0 +1 @@ +7Vpbc+o2EP41ekzHl/j2iAmk02nmdA6dpn3qCFsYTWSLY0Qg/fWVbMnYluE4B4jTQh4Se6WVVt+32pU2BvY43T3mcLV8ojEiwDLiHbAfgGWZluPzP0LyVkoCwygFSY5j2WkvmOF/kBSqbhsco3WjI6OUMLxqCiOaZShiDRnMc7ptdltQ0px1BROkCWYRJLr0GcdsWUp9y9vLf0Y4WaqZTTcoW1KoOsuVrJcwptuayJ4Ae5xTysqndDdGRICncCn1pgdaK8NylLE+Ck/Pf/xthHPraf7NePliLeLZ1+c7OcorJBu5YGkse1MI5HSTxUgMYgI73C4xQ7MVjETrlnPOZUuWEtm8wISMKaF5oWsvFgsrirh8zXL6gmotsTt3HVe0KFiEekRTzLs/GPw5IXC9ls/SSpQztDu4fLMClXsjoili+RvvIhVs5VHSEX3fKd+3e1pNxdWyRqkrZVB6UlINvQebP0i834G9db3YW0Njb2vYx5BBDX++ZNYEuQlmRjN0BHlIcJJxWcRxQrwxFChiHlxGsiHFcSzm6qR2T75RTaxiUME3zZgMmFbQIq1O5jkIvG8RqN5rBLod/NmX4u9e4y8TVN3468dflYWH4s+5mtjnGJ8t77jXi/3gece75Z13EWi3845OYFfcUjyfnT//lndO4W/wvBNcTexzg8+Wd9Ql4BrBHzzxmPpN/5Z5jjDoWZ/sxmPq5YJb6nkHgYOnHlOvOfxfw5/vf7rcoxcMrgb84XOPftu/5Z4jDAbtcunguUcvGdxyzzsIHD736HUHjTyUxSPxDzNBgIBGANKIeC38mgjbXIJ2mP0pe4vnvwSiPzny7WEnAS5e3hpoo1j7L1wLa24q3eQROrbIbk5qmDsdmCtZjghk+LVpRhcRcobfKOYGHiw1BSrjqCFK86XWns4eA7VqGQzmCWLaQIVfVMs+wVX0Esd/zFWGcgG/vevbzPV1gXYG/3AX6FElublAF3Nu+9Lxo1GgXUDQBrqwC6h1NFzAJUV2t/bK7reN+HwhBJN7EPrAD8DEAf4U+GPxwNcQmGASgMAG4QRMfDAywcivqdVdSgmF59ytC9cZ8Q62vdrVNdyknJDP8wACp3N4UnadClNlfymM8esJk8pB5vnpY3DrCluU9PSTlBSd9RBVv7IcOi/Vdrptn+f8ZPt+w/s9Vz8/OeZHHqAsvXz2PZcSFN5JNoQ/ELRgh/1hvYJZH68yne7twPcf3w5jsQt49uD37ckUhGMQjGYZxGRMcDrnnWEqSM7m61XTFcvZj3toKRXmnGDnBTCrBQQHjMYFFHzhngxFoVetu/z9iNlyM6/w+QW+wkfxkdkPwcGlJSK99/HF92e9vNDOxEKJ8hExEzsrEK8EzhEJYfSSFGO3AwxXrl3VjOKn0qJ5jPIOjSlMMREzPMIcpjSLlRkSGENS/aUyxTNKPiOcJb/TlTwiSMGvwgsakpAyRtOm7KsMA2eKQZ6qWakM7OhlFKsrBt1fLAbpFeDbsazXscw0vnep6nsuMw1v2LO5pVeRs41Q+9jAM2R1RefgcuUV/rr/QrfkcP+dsz35Fw== \ No newline at end of file diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.png" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.png" new file mode 100644 index 00000000000..f8b9589d6d7 Binary files /dev/null and "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.png" differ diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.drawio" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.drawio" new file mode 100644 index 00000000000..6fddf10f064 --- /dev/null +++ "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.png" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.png" new file mode 100644 index 00000000000..c976cc99b91 Binary files /dev/null and "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.png" differ diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.drawio" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.drawio" new file mode 100644 index 00000000000..7f7bfd71641 --- /dev/null +++ "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.png" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.png" new file mode 100644 index 00000000000..f8f457c7490 Binary files /dev/null and "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.png" differ diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.drawio" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.drawio" new file mode 100644 index 00000000000..7626c8d1f50 --- /dev/null +++ "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.png" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.png" new file mode 100644 index 00000000000..ecdbd6d2c2e Binary files /dev/null and "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.png" differ diff --git "a/docs/database/Redis/redis-collection/Redis(1)\342\200\224\342\200\2245\347\247\215\345\237\272\346\234\254\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/docs/database/Redis/redis-collection/Redis(1)\342\200\224\342\200\2245\347\247\215\345\237\272\346\234\254\346\225\260\346\215\256\347\273\223\346\236\204.md" deleted file mode 100644 index a42332f6c2d..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(1)\342\200\224\342\200\2245\347\247\215\345\237\272\346\234\254\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ /dev/null @@ -1,515 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-2899175817bb0069.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、Redis 简介 - -> **"Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker."** —— Redis是一个开放源代码(BSD许可)的内存中数据结构存储,用作数据库,缓存和消息代理。 *(摘自官网)* - -**Redis** 是一个开源,高级的键值存储和一个适用的解决方案,用于构建高性能,可扩展的 Web 应用程序。**Redis** 也被作者戏称为 *数据结构服务器* ,这意味着使用者可以通过一些命令,基于带有 TCP 套接字的简单 *服务器-客户端* 协议来访问一组 **可变数据结构** 。*(在 Redis 中都采用键值对的方式,只不过对应的数据结构不一样罢了)* - -## Redis 的优点 - -以下是 Redis 的一些优点: - -- **异常快** - Redis 非常快,每秒可执行大约 110000 次的设置(SET)操作,每秒大约可执行 81000 次的读取/获取(GET)操作。 -- **支持丰富的数据类型** - Redis 支持开发人员常用的大多数数据类型,例如列表,集合,排序集和散列等等。这使得 Redis 很容易被用来解决各种问题,因为我们知道哪些问题可以更好使用地哪些数据类型来处理解决。 -- **操作具有原子性** - 所有 Redis 操作都是原子操作,这确保如果两个客户端并发访问,Redis 服务器能接收更新的值。 -- **多实用工具** - Redis 是一个多实用工具,可用于多种用例,如:缓存,消息队列(Redis 本地支持发布/订阅),应用程序中的任何短期数据,例如,web应用程序中的会话,网页命中计数等。 - -## Redis 的安装 - -这一步比较简单,你可以在网上搜到许多满意的教程,这里就不再赘述。 - -给一个菜鸟教程的安装教程用作参考:[https://www.runoob.com/redis/redis-install.html](https://www.runoob.com/redis/redis-install.html) - -## 测试本地 Redis 性能 - -当你安装完成之后,你可以先执行 `redis-server` 让 Redis 启动起来,然后运行命令 `redis-benchmark -n 100000 -q` 来检测本地同时执行 10 万个请求时的性能: - -![](https://upload-images.jianshu.io/upload_images/7896890-7aea279e8fcdafe5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -当然不同电脑之间由于各方面的原因会存在性能差距,这个测试您可以权当是一种 **「乐趣」** 就好。 - -# 二、Redis 五种基本数据结构 - -**Redis** 有 5 种基础数据结构,它们分别是:**string(字符串)**、**list(列表)**、**hash(字典)**、**set(集合)** 和 **zset(有序集合)**。这 5 种是 Redis 相关知识中最基础、最重要的部分,下面我们结合源码以及一些实践来给大家分别讲解一下。 - -注意: - -> 每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码。 -> -> 可以看到每种数据结构都有两种以上的内部编码实现,例如string数据结构就包含了raw、int和embstr三种内部编码。 -> -> 同时,有些内部编码可以作为多种外部数据结构的内部实现,例如ziplist就是hash、list和zset共有的内部编码。 - -## 1)字符串 string - -Redis 中的字符串是一种 **动态字符串**,这意味着使用者可以修改,它的底层实现有点类似于 Java 中的 **ArrayList**,有一个字符数组,从源码的 **sds.h/sdshdr 文件** 中可以看到 Redis 底层对于字符串的定义 **SDS**,即 *Simple Dynamic String* 结构: - -```c -/* Note: sdshdr5 is never used, we just access the flags byte directly. - * However is here to document the layout of type 5 SDS strings. */ -struct __attribute__ ((__packed__)) sdshdr5 { - unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr8 { - uint8_t len; /* used */ - uint8_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr16 { - uint16_t len; /* used */ - uint16_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr32 { - uint32_t len; /* used */ - uint32_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr64 { - uint64_t len; /* used */ - uint64_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -``` - -你会发现同样一组结构 Redis 使用泛型定义了好多次,**为什么不直接使用 int 类型呢?** - -因为当字符串比较短的时候,len 和 alloc 可以使用 byte 和 short 来表示,**Redis 为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。** - -### SDS 与 C 字符串的区别 - -为什么不考虑直接使用 C 语言的字符串呢?因为 C 语言这种简单的字符串表示方式 **不符合 Redis 对字符串在安全性、效率以及功能方面的要求**。我们知道,C 语言使用了一个长度为 N+1 的字符数组来表示长度为 N 的字符串,并且字符数组最后一个元素总是 `'\0'`。*(下图就展示了 C 语言中值为 "Redis" 的一个字符数组)* - -![](https://upload-images.jianshu.io/upload_images/7896890-3a7d503c81b6e6e4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这样简单的数据结构可能会造成以下一些问题: - -- **获取字符串长度为 O(N) 级别的操作** → 因为 C 不保存数组的长度,每次都需要遍历一遍整个数组; -- 不能很好的杜绝 **缓冲区溢出/内存泄漏** 的问题 → 跟上述问题原因一样,如果执行拼接 or 缩短字符串的操作,如果操作不当就很容易造成上述问题; -- C 字符串 **只能保存文本数据** → 因为 C 语言中的字符串必须符合某种编码(比如 ASCII),例如中间出现的 `'\0'` 可能会被判定为提前结束的字符串而识别不了; - -我们以追加字符串的操作举例,Redis 源码如下: - -```c -/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the - * end of the specified sds string 's'. - * - * After the call, the passed sds string is no longer valid and all the - * references must be substituted with the new pointer returned by the call. */ -sds sdscatlen(sds s, const void *t, size_t len) { - // 获取原字符串的长度 - size_t curlen = sdslen(s); - - // 按需调整空间,如果容量不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中 - s = sdsMakeRoomFor(s,len); - if (s == NULL) return NULL; // 内存不足 - memcpy(s+curlen, t, len); // 追加目标字符串到字节数组中 - sdssetlen(s, curlen+len); // 设置追加后的长度 - s[curlen+len] = '\0'; // 让字符串以 \0 结尾,便于调试打印 - return s; -} -``` - -- **注:Redis 规定了字符串的长度不得超过 512 MB。** - -### 对字符串的基本操作 - -安装好 Redis,我们可以使用 `redis-cli` 来对 Redis 进行命令行的操作,当然 Redis 官方也提供了在线的调试器,你也可以在里面敲入命令进行操作:[http://try.redis.io/#run](http://try.redis.io/#run) - -#### 设置和获取键值对 - -```console -> SET key value -OK -> GET key -"value" -``` - -正如你看到的,我们通常使用 `SET` 和 `GET` 来设置和获取字符串值。 - -值可以是任何种类的字符串(包括二进制数据),例如你可以在一个键下保存一张 `.jpeg` 图片,只需要注意不要超过 512 MB 的最大限度就好了。 - -当 key 存在时,`SET` 命令会覆盖掉你上一次设置的值: - -```console -> SET key newValue -OK -> GET key -"newValue" -``` - -另外你还可以使用 `EXISTS` 和 `DEL` 关键字来查询是否存在和删除键值对: - -```console -> EXISTS key -(integer) 1 -> DEL key -(integer) 1 -> GET key -(nil) -``` - -#### 批量设置键值对 - -```console -> SET key1 value1 -OK -> SET key2 value2 -OK -> MGET key1 key2 key3 # 返回一个列表 -1) "value1" -2) "value2" -3) (nil) -> MSET key1 value1 key2 value2 -> MGET key1 key2 -1) "value1" -2) "value2" -``` - -#### 过期和 SET 命令扩展 - -可以对 key 设置过期时间,到时间会被自动删除,这个功能常用来控制缓存的失效时间。*(过期可以是任意数据结构)* - -```console -> SET key value1 -> GET key -"value1" -> EXPIRE name 5 # 5s 后过期 -... # 等待 5s -> GET key -(nil) -``` - -等价于 `SET` + `EXPIRE` 的 `SETEX` 命令: - -```console -> SETEX key 5 value1 -... # 等待 5s 后获取 -> GET key -(nil) - -> SETNX key value1 # 如果 key 不存在则 SET 成功 -(integer) 1 -> SETNX key value1 # 如果 key 存在则 SET 失败 -(integer) 0 -> GET key -"value" # 没有改变 -``` - -#### 计数 - -如果 value 是一个整数,还可以对它使用 `INCR` 命令进行 **原子性** 的自增操作,这意味着及时多个客户端对同一个 key 进行操作,也决不会导致竞争的情况: - -```console -> SET counter 100 -> INCR counter -(integer) 101 -> INCRBY counter 50 -(integer) 151 -``` - -#### 返回原值的 GETSET 命令 - -对字符串,还有一个 `GETSET` 比较让人觉得有意思,它的功能跟它名字一样:为 key 设置一个值并返回原值: - -```console -> SET key value -> GETSET key value1 -"value" -``` - -这可以对于某一些需要隔一段时间就统计的 key 很方便的设置和查看,例如:系统每当由用户进入的时候你就是用 `INCR` 命令操作一个 key,当需要统计时候你就把这个 key 使用 `GETSET` 命令重新赋值为 0,这样就达到了统计的目的。 - -## 2)列表 list - -Redis 的列表相当于 Java 语言中的 **LinkedList**,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。 - -我们可以从源码的 `adlist.h/listNode` 来看到对其的定义: - -```c -/* Node, List, and Iterator are the only data structures used currently. */ - -typedef struct listNode { - struct listNode *prev; - struct listNode *next; - void *value; -} listNode; - -typedef struct listIter { - listNode *next; - int direction; -} listIter; - -typedef struct list { - listNode *head; - listNode *tail; - void *(*dup)(void *ptr); - void (*free)(void *ptr); - int (*match)(void *ptr, void *key); - unsigned long len; -} list; -``` - -可以看到,多个 listNode 可以通过 `prev` 和 `next` 指针组成双向链表: - -![](https://upload-images.jianshu.io/upload_images/7896890-8f569f06506845c1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -虽然仅仅使用多个 listNode 结构就可以组成链表,但是使用 `adlist.h/list` 结构来持有链表的话,操作起来会更加方便: - -![](https://upload-images.jianshu.io/upload_images/7896890-c6fb10cdbb32f517.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -### 链表的基本操作 - -- `LPUSH` 和 `RPUSH` 分别可以向 list 的左边(头部)和右边(尾部)添加一个新元素; -- `LRANGE` 命令可以从 list 中取出一定范围的元素; -- `LINDEX` 命令可以从 list 中取出指定下表的元素,相当于 Java 链表操作中的 `get(int index)` 操作; - -示范: - -```console -> rpush mylist A -(integer) 1 -> rpush mylist B -(integer) 2 -> lpush mylist first -(integer) 3 -> lrange mylist 0 -1 # -1 表示倒数第一个元素, 这里表示从第一个元素到最后一个元素,即所有 -1) "first" -2) "A" -3) "B" -``` - -#### list 实现队列 - -队列是先进先出的数据结构,常用于消息排队和异步逻辑处理,它会确保元素的访问顺序: - -```console -> RPUSH books python java golang -(integer) 3 -> LPOP books -"python" -> LPOP books -"java" -> LPOP books -"golang" -> LPOP books -(nil) -``` - -#### list 实现栈 - -栈是先进后出的数据结构,跟队列正好相反: - -```console -> RPUSH books python java golang -> RPOP books -"golang" -> RPOP books -"java" -> RPOP books -"python" -> RPOP books -(nil) -``` - -## 3)字典 hash - -Redis 中的字典相当于 Java 中的 **HashMap**,内部实现也差不多类似,都是通过 **"数组 + 链表"** 的链地址法来解决部分 **哈希冲突**,同时这样的结构也吸收了两种不同数据结构的优点。源码定义如 `dict.h/dictht` 定义: - -```c -typedef struct dictht { - // 哈希表数组 - dictEntry **table; - // 哈希表大小 - unsigned long size; - // 哈希表大小掩码,用于计算索引值,总是等于 size - 1 - unsigned long sizemask; - // 该哈希表已有节点的数量 - unsigned long used; -} dictht; - -typedef struct dict { - dictType *type; - void *privdata; - // 内部有两个 dictht 结构 - dictht ht[2]; - long rehashidx; /* rehashing not in progress if rehashidx == -1 */ - unsigned long iterators; /* number of iterators currently running */ -} dict; -``` - -`table` 属性是一个数组,数组中的每个元素都是一个指向 `dict.h/dictEntry` 结构的指针,而每个 `dictEntry` 结构保存着一个键值对: - -```c -typedef struct dictEntry { - // 键 - void *key; - // 值 - union { - void *val; - uint64_t u64; - int64_t s64; - double d; - } v; - // 指向下个哈希表节点,形成链表 - struct dictEntry *next; -} dictEntry; -``` - -可以从上面的源码中看到,**实际上字典结构的内部包含两个 hashtable**,通常情况下只有一个 hashtable 是有值的,但是在字典扩容缩容时,需要分配新的 hashtable,然后进行 **渐进式搬迁** *(下面说原因)*。 - -### 渐进式 rehash - -大字典的扩容是比较耗时间的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,这是一个 O(n) 级别的操作,作为单线程的 Redis 很难承受这样耗时的过程,所以 Redis 使用 **渐进式 rehash** 小步搬迁: - -![](https://upload-images.jianshu.io/upload_images/7896890-325d968300c47100.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,如上图所示,查询时会同时查询两个 hash 结构,然后在后续的定时任务以及 hash 操作指令中,循序渐进的把旧字典的内容迁移到新字典中。当搬迁完成了,就会使用新的 hash 结构取而代之。 - -### 扩缩容的条件 - -正常情况下,当 hash 表中 **元素的个数等于第一维数组的长度时**,就会开始扩容,扩容的新数组是 **原数组大小的 2 倍**。不过如果 Redis 正在做 `bgsave(持久化命令)`,为了减少内存也得过多分离,Redis 尽量不去扩容,但是如果 hash 表非常满了,**达到了第一维数组长度的 5 倍了**,这个时候就会 **强制扩容**。 - -当 hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是 **元素个数低于数组长度的 10%**,缩容不会考虑 Redis 是否在做 `bgsave`。 - -### 字典的基本操作 - -hash 也有缺点,hash 结构的存储消耗要高于单个字符串,所以到底该使用 hash 还是字符串,需要根据实际情况再三权衡: - -```shell -> HSET books java "think in java" # 命令行的字符串如果包含空格则需要使用引号包裹 -(integer) 1 -> HSET books python "python cookbook" -(integer) 1 -> HGETALL books # key 和 value 间隔出现 -1) "java" -2) "think in java" -3) "python" -4) "python cookbook" -> HGET books java -"think in java" -> HSET books java "head first java" -(integer) 0 # 因为是更新操作,所以返回 0 -> HMSET books java "effetive java" python "learning python" # 批量操作 -OK -``` - -## 4)集合 set - -Redis 的集合相当于 Java 语言中的 **HashSet**,它内部的键值对是无序、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。 - -### 集合 set 的基本使用 - -由于该结构比较简单,我们直接来看看是如何使用的: - -```shell -> SADD books java -(integer) 1 -> SADD books java # 重复 -(integer) 0 -> SADD books python golang -(integer) 2 -> SMEMBERS books # 注意顺序,set 是无序的 -1) "java" -2) "python" -3) "golang" -> SISMEMBER books java # 查询某个 value 是否存在,相当于 contains -(integer) 1 -> SCARD books # 获取长度 -(integer) 3 -> SPOP books # 弹出一个 -"java" -``` - -## 5)有序列表 zset - -这可能使 Redis 最具特色的一个数据结构了,它类似于 Java 中 **SortedSet** 和 **HashMap** 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以为每个 value 赋予一个 score 值,用来代表排序的权重。 - -它的内部实现用的是一种叫做 **「跳跃表」** 的数据结构,由于比较复杂,所以在这里简单提一下原理就好了: - -![](https://upload-images.jianshu.io/upload_images/7896890-efd5114939a651ed.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -想象你是一家创业公司的老板,刚开始只有几个人,大家都平起平坐。后来随着公司的发展,人数越来越多,团队沟通成本逐渐增加,渐渐地引入了组长制,对团队进行划分,于是有一些人**又是员工又有组长的身份**。 - -再后来,公司规模进一步扩大,公司需要再进入一个层级:部门。于是每个部门又会从组长中推举一位选出部长。 - -跳跃表就类似于这样的机制,最下面一层所有的元素都会串起来,都是员工,然后每隔几个元素就会挑选出一个代表,再把这几个代表使用另外一级指针串起来。然后再在这些代表里面挑出二级代表,再串起来。**最终形成了一个金字塔的结构。** - -想一下你目前所在的地理位置:亚洲 > 中国 > 某省 > 某市 > ....,**就是这样一个结构!** - -### 有序列表 zset 基础操作 - -```console -> ZADD books 9.0 "think in java" -> ZADD books 8.9 "java concurrency" -> ZADD books 8.6 "java cookbook" - -> ZRANGE books 0 -1 # 按 score 排序列出,参数区间为排名范围 -1) "java cookbook" -2) "java concurrency" -3) "think in java" - -> ZREVRANGE books 0 -1 # 按 score 逆序列出,参数区间为排名范围 -1) "think in java" -2) "java concurrency" -3) "java cookbook" - -> ZCARD books # 相当于 count() -(integer) 3 - -> ZSCORE books "java concurrency" # 获取指定 value 的 score -"8.9000000000000004" # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题 - -> ZRANK books "java concurrency" # 排名 -(integer) 1 - -> ZRANGEBYSCORE books 0 8.91 # 根据分值区间遍历 zset -1) "java cookbook" -2) "java concurrency" - -> ZRANGEBYSCORE books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。 -1) "java cookbook" -2) "8.5999999999999996" -3) "java concurrency" -4) "8.9000000000000004" - -> ZREM books "java concurrency" # 删除 value -(integer) 1 -> ZRANGE books 0 -1 -1) "java cookbook" -2) "think in java" -``` - -# 扩展/相关阅读 - -### 优秀文章 - -1. 阿里云 Redis 开发规范 - [https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px](https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px) -2. 为什么要防止 bigkey? - [https://mp.weixin.qq.com/s?__biz=Mzg2NTEyNzE0OA==&mid=2247483677&idx=1&sn=5c320b46f0e06ce9369a29909d62b401&chksm=ce5f9e9ef928178834021b6f9b939550ac400abae5c31e1933bafca2f16b23d028cc51813aec&scene=21#wechat_redirect](https://mp.weixin.qq.com/s?__biz=Mzg2NTEyNzE0OA==&mid=2247483677&idx=1&sn=5c320b46f0e06ce9369a29909d62b401&chksm=ce5f9e9ef928178834021b6f9b939550ac400abae5c31e1933bafca2f16b23d028cc51813aec&scene=21#wechat_redirect) -3. Redis【入门】就这一篇! - [https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/](https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/) - -#### Redis数据结构源码分析 - -1. Redis 数据结构-字符串源码分析:[https://my.oschina.net/mengyuankan/blog/1926320](https://my.oschina.net/mengyuankan/blog/1926320) -2. Redis 数据结构-字典源码分析: [https://my.oschina.net/mengyuankan/blog/1929593](https://my.oschina.net/mengyuankan/blog/1929593) - - - -# 参考资料 - -1. 《Redis 设计与实现》 - [http://redisbook.com/](http://redisbook.com/) -2. 【官方文档】Redis 数据类型介绍 - [http://www.redis.cn/topics/data-types-intro.html](http://www.redis.cn/topics/data-types-intro.html) -3. 《Redis 深度历险》 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -4. 阿里云 Redis 开发规范 - [https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px](https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px) -5. Redis 快速入门 - 易百教程 - [https://www.yiibai.com/redis/redis_quick_guide.html](https://www.yiibai.com/redis/redis_quick_guide.html) -6. Redis【入门】就这一篇! - [https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/](https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/) - diff --git "a/docs/database/Redis/redis-collection/Redis(10)\342\200\224\342\200\224Redis\346\225\260\346\215\256\347\261\273\345\236\213\343\200\201\347\274\226\347\240\201\343\200\201\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\345\205\263\347\263\273.md" "b/docs/database/Redis/redis-collection/Redis(10)\342\200\224\342\200\224Redis\346\225\260\346\215\256\347\261\273\345\236\213\343\200\201\347\274\226\347\240\201\343\200\201\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\345\205\263\347\263\273.md" deleted file mode 100644 index 2252d9c50b9..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(10)\342\200\224\342\200\224Redis\346\225\260\346\215\256\347\261\273\345\236\213\343\200\201\347\274\226\347\240\201\343\200\201\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\345\205\263\347\263\273.md" +++ /dev/null @@ -1,599 +0,0 @@ -## Redis构建的类型系统 - -Redis构建了自己的类型系统,主要包括 - -+ redisObject对象 -+ 基于redisObject对象的类型检查 -+ 基于redisObject对象的显示多态函数 -+ 对redisObject进行分配、共享和销毁的机制 - -__C语言不是面向对象语言,这里将redisObject称呼为对象是为了讲述方便,让里面的内容更容易被理解,redisObject其实是一个结构体。__ - -### redisObject对象 - -Redis内部使用一个redisObject对象来表示所有的key和value,每次在Redis数据块中创建一个键值对时,一个是键对象,一个是值对象,而Redis中的每个对象都是由redisObject结构来表示。 - -__在Redis中,键总是一个字符串对象,而值可以是字符串、列表、集合等对象,所以我们通常说键为字符串键,表示这个键对应的值为字符串对象,我们说一个键为集合键时,表示这个键对应的值为集合对象__ - -redisobject最主要的信息: - -``` -redisobject源码 -typedef struct redisObject{ - //类型 - unsigned type:4; - //编码 - unsigned encoding:4; - //指向底层数据结构的指针 - void *ptr; - //引用计数 - int refcount; - //记录最后一次被程序访问的时间 - unsigned lru:22; -}robj -``` - -+ type代表一个value对象具体是何种数据类型 - - + type key :判断对象的数据类型 -+ encoding属性和*prt指针 - + prt指针指向对象底层的数据结构,而数据结构由encoding属性来决定 - - ![数据结构和编码的对应](https://own-pic-bed.oss-cn-beijing.aliyuncs.com/encoding对应数据结构.png) - - + 每种类型的对象至少使用了两种不同的编码,而这些编码对用户是完全透明的。 - - ![数据类型和编码的对应](https://own-pic-bed.oss-cn-beijing.aliyuncs.com/类型和编码对应.png) - - + object encoding key命令可以查看值对象的编码 - -### 命令的类型检查和多态 - -#### Redis命令分类 - -+ 一种是只能用于对应数据类型的命令,例如LPUSH和LLEN只能用于列表键, SADD 和 SRANDMEMBER只能用于集合键。 -+ 另一种是可以用于任何类型键的命令。比如TTL。 - -当执行一个处理数据类型的命令时,Redis执行以下步骤: - -+ 根据给定 `key` ,在数据库字典中查找和它相对应的 `redisObject` ,如果没找到,就返回 `NULL` 。 -+ 检查 `redisObject` 的 `type` 属性和执行命令所需的类型是否相符,如果不相符,返回类型错误。 -+ 根据 `redisObject` 的 `encoding` 属性所指定的编码,选择合适的操作函数来处理底层的数据结构。 -+ 返回数据结构的操作结果作为命令的返回值。 - -## 5种数据类型对应的编码和数据结构 - -### string - -__string 是最常用的一种数据类型,普通的key/value存储都可以归结为string类型,value不仅是string,也可以是数字。其他几种数据类型的构成元素也都是字符串,注意Redis规定字符串的长度不能超过512M__ - -+ 编码 - __字符串对象的编码可以是int raw embstr__ - + int编码 - + 保存的是可以用long类型表示的整数值 - + raw编码 - + 保存长度大于44字节的字符串 - + embstr编码 - + 保存长度小于44字节的字符串 - - int用来保存整数值,raw用来保存长字符串,embstr用来保存短字符串。embstr编码是用来专门保存短字符串的一种优化编码。 - - Redis中对于浮点型也是作为字符串保存的,在需要时再将其转换成浮点数类型 - -+ 编码的转换 - + 当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw - + 对于 embstr 编码,由于 Redis 没有对其编写任何的修改程序(embstr 是只读的),在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。 - -+ 常用命令 - - + set/get - - + set:设置key对应的值为string类型的value (多次set name会覆盖) - + get:获取key对应的值 - - + mset /mget - - + mset 批量设置多个key的值,如果成功表示所有值都被设置,否则返回0表示没有任何值被设置 - + mget批量获取多个key的值,如果不存在则返回null - - ```shell - 127.0.0.1:6379> mset user1:name redis user1:age 22 - OK - 127.0.0.1:6379> mget user1:name user1:age - 1) "redis" - 2) "22" - ``` - - + 应用场景 - + 类似于哈希操作,存储对象 - - + incr && incrby<原子操作> - - + incr对key对应的值进行加加操作,并返回新的值,incrby加指定的值 - - + decr && decrby<原子操作> - - + decr对key对应的值进行减减操做,并返回新的值,decrby减指定的值 - - + setnx <小小体验一把分布式锁,真香> - - + 设置Key对应的值为string类型的值,如果已经存在则返回0 - - + setex - - + 设置key对应的值为string类型的value,并设定有效期 - - + setrange/getrange - - + setrange从指定位置替换字符串 - + getrange获取key对应value子字符串 - -+ 其他命令 - - + msetnx 同mset,不存在就设置,不会覆盖已有的key - + getset 设置key的值,并返回key旧的值 - + append 给指定的key的value追加字符串,并返回新字符串的长度 - + strlen 返回key对应的value字符串的长度 - -+ 应用场景 - - + 因为string类型是二进制安全的,可以用来存放图片,视频等内容。 - + 由于redis的高性能的读写功能,而string类型的value也可以是数字,可以用做计数器(使用INCR,DECR指令)。比如分布式环境中统计系统的在线人数,秒杀等。 - + 除了上面提到的,还有用于SpringSession实现分布式session - + 分布式系统全局序列号 - -### list - -__list列表,它是简单的字符串列表,你可以添加一个元素到列表的头部,或者尾部__。 - -+ 编码 - - + 列表对象的编码可以是ziplist(压缩列表)和linkedlist(双端链表)。 - + 编码转换 - + 同时满足下面两个条件时使用压缩列表: - + 列表保存元素个数小于512个 - + 每个元素长度小于64字节 - + 不能满足上面两个条件使用linkedlist(双端列表)编码 - -+ 常用命令 - - + lpush: 从头部加入元素 - - ```shell - 127.0.0.1:6379> lpush list1 hello - (integer) 1 - 127.0.0.1:637 9> lpush list1 world - (integer) 2 - 127.0.0.1:6379> lrange list1 0 -1 - 1) "world" - 2) "hello" - ``` - - + rpush:从尾部加入元素 - - ```shell - 127.0.0.1:6379> rpush list2 world - (integer) 1 - 127.0.0.1:6379> rpush list2 hello - (integer) 2 - 127.0.0.1:6379> lrange list2 0 -1 - 1) "world" - 2) "hello" - ``` - - + lpop: 从list的头部删除元素,并返回删除的元素 - - ```shell - 127.0.0.1:6379> lrange list1 0 -1 - 1) "world" - 2) "hello" - 127.0.0.1:6379> lpop list1 - "world" - 127.0.0.1:6379> lrange list1 0 -1 - 1) "hello" - ``` - - + rpop:从list的尾部删除元素,并返回删除的元素 - - ```shell - 127.0.0.1:6379> lrange list2 0 -1 - 1) "hello" - 2) "world" - 127.0.0.1:6379> rpop list2 - "world" - 127.0.0.1:6379> lrange list2 0 -1 - 1) "hello" - ``` - - + rpoplpush: 第一步从尾部删除元素,第二步从首部插入元素 结合着使用 - + linsert :插入方法 linsert listname before [集合的元素] [插入的元素] - - ```shell - 127.0.0.1:6379> lpush list3 hello - (integer) 1 - 127.0.0.1:6379> lpush list3 world - (integer) 2 - 127.0.0.1:6379> linsert list3 before hello start - (integer) 3 - 127.0.0.1:6379> lrange list3 0 -1 - 1) "world" - 2) "start" - 3) "hello" - ``` - + lset :替换指定下标的元素 - ```shell - 127.0.0.1:6379> lrange list1 0 -1 - 1) "a" - 2) "b" - 127.0.0.1:6379> lset list1 0 v - OK - 127.0.0.1:6379> lrange list1 0 -1 - 1) "v" - 2) "b" - ``` - + lrm : 删除元素,返回删除的个数 - ```shell - 127.0.0.1:6379> lrange list1 0 -1 - 1) "b" - 2) "b" - 3) "a" - 4) "b" - 127.0.0.1:6379> lrange list1 0 -1 - 1) "a" - 2) "b" - ``` - - + lindex: 返回list中指定位置的元素 - + llen: 返回list中的元素的个数 -+ 实现数据结构 - - + Stack(栈) - + LPUSH+LPOP - + Queue(队列) - + LPUSH + RPOP - + Blocking MQ(阻塞队列) - + LPUSH+BRPOP - -+ 应用场景 - - + 实现简单的消息队列 - + 利用LRANGE命令,实现基于Redis的分页功能 - -### set - -__集合对象set是string类型(整数也会转成string类型进行存储)的无序集合。注意集合和列表的区别:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。__ - -+ 编码 - - + 集合对象的编码可以是intset或者hashtable - + intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。 - + hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值全部设置为null。__当使用HT编码时,Redis中的集合SET相当于Java中的HashSet,内部的键值对是无序的,唯一的。内部实现相当于一个特殊的字典,字典中所有value都是NULL。__ - - + 编码转换 - + 当集合满足下列两个条件时,使用intset编码: - + 集合对象中的所有元素都是整数 - + 集合对象所有元素数量不超过512 - -+ 常用命令 - - + sadd: 向集合中添加元素 (set不允许元素重复) - + smembers: 查看集合中的元素 - - ```shell - 127.0.0.1:6379> sadd set1 aaa - (integer) 1 - 127.0.0.1:6379> sadd set1 bbb - (integer) 1 - 127.0.0.1:6379> sadd set1 ccc - (integer) 1 - 127.0.0.1:6379> smembers set1 - 1) "aaa" - 2) "ccc" - 3) "bbb" - ``` - - + srem: 删除集合元素 - + spop: 随机返回删除的key - - + sdiff :返回两个集合的不同元素 (哪个集合在前就以哪个集合为标准) - - ```shell - 127.0.0.1:6379> smembers set1 - 1) "ccc" - 2) "bbb" - 127.0.0.1:6379> smembers set2 - 1) "fff" - 2) "rrr" - 3) "bbb" - 127.0.0.1:6379> sdiff set1 set2 - 1) "ccc" - 127.0.0.1:6379> sdiff set2 set1 - 1) "fff" - 2) "rrr" - ``` - - + sinter: 返回两个集合的交集 - + sinterstore: 返回交集结果,存入目标集合 - - ```shell - 127.0.0.1:6379> sinterstore set3 set1 set2 - (integer) 1 - 127.0.0.1:6379> smembers set3 - 1) "bbb" - ``` - - + sunion: 取两个集合的并集 - + sunionstore: 取两个集合的并集,并存入目标集合 - - + smove: 将一个集合中的元素移动到另一个集合中 - + scard: 返回集合中的元素个数 - + sismember: 判断某元素是否存在某集合中,0代表否 1代表是 - + srandmember: 随机返回一个元素 - - ```shell - 127.0.0.1:6379> srandmember set1 1 - 1) "bbb" - 127.0.0.1:6379> srandmember set1 2 - 1) "ccc" - 2) "bbb" - ``` -+ 应用场景 - - + 对于 set 数据类型,由于底层是字典实现的,查找元素特别快,另外set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册;微信点赞,微信抽奖小程序 - + 另外就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好,可能认识的人等功能。 - -### zset - -__和集合对象相比,有序集合对象是有序的。与列表使用索引下表作为排序依据不同,有序集合为每一个元素设置一个分数(score)作为排序依据。__ - -+ 编码 - - + 有序集合的编码可以使ziplist或者skiplist - - + ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。 - + skiplist编码的依序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表 - - ``` - typedef struct zset{ - //跳跃表 - zskiplist *zsl; - //字典 - dict *dice; - }zset - 字典的键保存元素的值,字典的值保存元素的分值,跳跃表节点的object属性保存元素的成员,跳跃表节点的score属性保存元素的分值。这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。 - ``` - - + 编码转换 - - + 当有序结合对象同时满足以下两个条件时,对象使用ziplist编码,否则使用skiplist编码 - + 保存的元素数量小于128 - + 保存的所有元素长度都小于64字节 - -+ 常用命令 - - + zrem: 删除集合中名称为key的元素member - + zincrby: 以指定值去自动递增 - + zcard: 查看元素集合的个数 - + zcount: 返回score在给定区间中的数量 - - ```shell - 127.0.0.1:6379> zrange zset 0 -1 - 1) "one" - 2) "three" - 3) "two" - 4) "four" - 5) "five" - 6) "six" - 127.0.0.1:6379> zcard zset - (integer) 6 - 127.0.0.1:6379> zcount zset 1 4 - (integer) 4 - ``` - - + zrangebyscore: 找到指定区间范围的数据进行返回 - - ```shell - 127.0.0.1:6379> zrangebyscore zset 0 4 withscores - 1) "one" - 2) "1" - 3) "three" - 4) "2" - 5) "two" - 6) "2" - 7) "four" - 8) "4" - ``` - - + zremrangebyrank zset from to: 删除索引 - - ```shell - 127.0.0.1:6379> zrange zset 0 -1 - 1) "one" - 2) "three" - 3) "two" - 4) "four" - 5) "five" - 6) "six" - 127.0.0.1:6379> zremrangebyrank zset 1 3 - (integer) 3 - 127.0.0.1:6379> zrange zset 0 -1 - 1) "one" - 2) "five" - 3) "six" - - ``` - - + zremrangebyscore zset from to: 删除指定序号 - - ```shell - 127.0.0.1:6379> zrange zset 0 -1 withscores - 1) "one" - 2) "1" - 3) "five" - 4) "5" - 5) "six" - 6) "6" - 127.0.0.1:6379> zremrangebyscore zset 3 6 - (integer) 2 - 127.0.0.1:6379> zrange zset 0 -1 withscores - 1) "one" - 2) "1" - ``` - - + zrank: 返回排序索引 (升序之后再找索引) - + zrevrank: 返回排序索引 (降序之后再找索引) - -+ 应用场景 - - + 对于 zset 数据类型,有序的集合,可以做范围查找,排行榜应用,取 TOP N 操作等。 - -### hash - -__hash对象的键是一个字符串类型,值是一个键值对集合__ - -+ 编码 - - + hash对象的编码可以是ziplist或者hashtable - + 当使用ziplist,也就是压缩列表作为底层实现时,新增的键值是保存到压缩列表的表尾。 - + hashtable 编码的hash表对象底层使用字典数据结构,哈希对象中的每个键值对都使用一个字典键值对。__Redis中的字典相当于Java里面的HashMap,内部实现也差不多类似,都是通过“数组+链表”的链地址法来解决哈希冲突的,这样的结构吸收了两种不同数据结构的优点。__ - - + 编码转换 - + 当同时满足下面两个条件使用ziplist编码,否则使用hashtable编码 - + 列表保存元素个数小于512个 - + 每个元素长度小于64字节 - -+ hash是一个String类型的field和value之间的映射表 - -+ Hash特别适合存储对象 - -+ 所存储的成员较少时数据存储为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht - -+ Hash命令详解 - - + hset/hget - - + hset hashname hashkey hashvalue - + hget hashname hashkey - - ```shell - 127.0.0.1:6379> hset user id 1 - (integer) 1 - 127.0.0.1:6379> hset user name z3 - (integer) 1 - 127.0.0.1:6379> hset user add shanxi - (integer) 1 - 127.0.0.1:6379> hget user id - "1" - 127.0.0.1:6379> hget user name - "z3" - 127.0.0.1:6379> hget user add - "shanxi" - - ``` - - - - + hmset/hmget - - + hmset hashname hashkey1hashvalue1 hashkey2 hashvalue2 hashkey3 hashvalue3 - + hget hashname hashkey1 hashkey2 hashkey3 - - ```shell - 127.0.0.1:6379> hmset user id 1 name z3 add shanxi - OK - 127.0.0.1:6379> hmget user id name add - 1) "1" - 2) "z3" - 3) "shanxi" - ``` - - + hsetnx/hgetnx - - + hincrby/hdecrby - - ```shell - 127.0.0.1:6379> hincrby user2 id 3 - (integer) 6 - 127.0.0.1:6379> hget user2 id - "6" - ``` - - + hexist 判断是否存在key,不存在返回0 - - ```shell - 127.0.0.1:6379> hget user2 id - "6" - ``` - - + hlen 返回hash集合里所有的键值数 - - ```shell - 127.0.0.1:6379> hmset user3 id 3 name w5 - OK - 127.0.0.1:6379> hlen user3 - (integer) 2 - ``` - - + hdel :删除指定的hash的key - + hkeys 返回hash里所有的字段 - + hvals 返回hash里所有的value - + hgetall:返回hash集合里所有的key和value - - ```shell - 127.0.0.1:6379> hgetall user3 - 1) "id" - 2) "3" - 3) "name" - 4) "w3" - 5) "add" - 6) "beijing" - ``` - -+ 优点 - - + 同类数据归类整合存储,方便数据管理,比如单个用户的所有商品都放在一个hash表里面。 - + 相比string操作消耗内存cpu更小 - -+ 缺点 - - + hash结构的存储消耗要高于单个字符串 - + 过期功能不能使用在field上,只能用在key上 - + redis集群架构不适合大规模使用 - -+ 应用场景 - - + 对于 hash 数据类型,value 存放的是键值对,比如可以做单点登录存放用户信息。 - + 存放商品信息,实现购物车 - -## 内存回收和内存共享 - -``` -typedef struct redisObject{ - //类型 - unsigned type:4; - //编码 - unsigned encoding:4; - //指向底层数据结构的指针 - void *ptr; - //引用计数 - int refcount; - //记录最后一次被程序访问的时间 - unsigned lru:22; - -}robj -``` - -+ 内存回收 - __因为c语言不具备自动内存回收功能,当将redisObject对象作为数据库的键或值而不是作为参数存储时其生命周期是非常长的,为了解决这个问题,Redis自己构建了一个内存回收机制,通过redisobject结构中的refcount实现.这个属性会随着对象的使用状态而不断变化。__ - 1. 创建一个新对象,属性初始化为1 - 2. 对象被一个新程序使用,属性refcount加1 - 3. 对象不再被一个程序使用,属性refcount减1 - 4. 当对象的引用计数值变为0时,对象所占用的内存就会被释放 -+ 内存共享 - __refcount属性除了能实现内存回收以外,还能实现内存共享__ - 1. 将数据块的键的值指针指向一个现有值的对象 - 2. 将被共享的值对象引用refcount加1 - Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为o(1),对于普通字符串,判断复杂度为o(n);而对于哈希,列表,集合和有序集合,判断的复杂度为o(n^2).虽然共享的对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象。 - - - diff --git "a/docs/database/Redis/redis-collection/Redis(2)\342\200\224\342\200\224\350\267\263\350\267\203\350\241\250.md" "b/docs/database/Redis/redis-collection/Redis(2)\342\200\224\342\200\224\350\267\263\350\267\203\350\241\250.md" deleted file mode 100644 index 451c91bd028..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(2)\342\200\224\342\200\224\350\267\263\350\267\203\350\241\250.md" +++ /dev/null @@ -1,392 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - - - -![](https://upload-images.jianshu.io/upload_images/7896890-97a4ce9191464f62.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、跳跃表简介 - -跳跃表(skiplist)是一种随机化的数据结构,由 **William Pugh** 在论文[《Skip lists: a probabilistic alternative to balanced trees》](https://www.cl.cam.ac.uk/teaching/0506/Algorithms/skiplists.pdf)中提出,是一种可以于平衡树媲美的层次化链表结构——查找、删除、添加等操作都可以在对数期望时间下完成,以下是一个典型的跳跃表例子: - -![](https://upload-images.jianshu.io/upload_images/7896890-65a5b1a2849fb91c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -我们在上一篇中提到了 Redis 的五种基本结构中,有一个叫做 **有序列表 zset** 的数据结构,它类似于 Java 中的 **SortedSet** 和 **HashMap** 的结合体,一方面它是一个 set 保证了内部 value 的唯一性,另一方面又可以给每个 value 赋予一个排序的权重值 score,来达到 **排序** 的目的。 - -它的内部实现就依赖了一种叫做 **「跳跃列表」** 的数据结构。 - -## 为什么使用跳跃表 - -首先,因为 zset 要支持随机的插入和删除,所以它 **不宜使用数组来实现**,关于排序问题,我们也很容易就想到 **红黑树/ 平衡树** 这样的树形结构,为什么 Redis 不使用这样一些结构呢? - -1. **性能考虑:** 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部 _(下面详细说)_; -2. **实现考虑:** 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观; - -基于以上的一些考虑,Redis 基于 **William Pugh** 的论文做出一些改进后采用了 **跳跃表** 这样的结构。 - -## 本质是解决查找问题 - -我们先来看一个普通的链表结构: - -![](https://upload-images.jianshu.io/upload_images/7896890-11b7eebde1779904.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -我们需要这个链表按照 score 值进行排序,这也就意味着,当我们需要添加新的元素时,我们需要定位到插入点,这样才可以继续保证链表是有序的,通常我们会使用 **二分查找法**,但二分查找是有序数组的,链表没办法进行位置定位,我们除了遍历整个找到第一个比给定数据大的节点为止 _(时间复杂度 O(n))_ 似乎没有更好的办法。 - -但假如我们每相邻两个节点之间就增加一个指针,让指针指向下一个节点,如下图: - -![](https://upload-images.jianshu.io/upload_images/7896890-8cae2c261c950b32.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这样所有新增的指针连成了一个新的链表,但它包含的数据却只有原来的一半 _(图中的为 3,11)_。 - -现在假设我们想要查找数据时,可以根据这条新的链表查找,如果碰到比待查找数据大的节点时,再回到原来的链表中进行查找,比如,我们想要查找 7,查找的路径则是沿着下图中标注出的红色指针所指向的方向进行的: - -![](https://upload-images.jianshu.io/upload_images/7896890-9c0262c7a85c120e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这是一个略微极端的例子,但我们仍然可以看到,通过新增加的指针查找,我们不再需要与链表上的每一个节点逐一进行比较,这样改进之后需要比较的节点数大概只有原来的一半。 - -利用同样的方式,我们可以在新产生的链表上,继续为每两个相邻的节点增加一个指针,从而产生第三层链表: - -![](https://upload-images.jianshu.io/upload_images/7896890-22036e274bedaa5a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -在这个新的三层链表结构中,我们试着 **查找 13**,那么沿着最上层链表首先比较的是 11,发现 11 比 13 小,于是我们就知道只需要到 11 后面继续查找,**从而一下子跳过了 11 前面的所有节点。** - -可以想象,当链表足够长,这样的多层链表结构可以帮助我们跳过很多下层节点,从而加快查找的效率。 - -## 更进一步的跳跃表 - -**跳跃表 skiplist** 就是受到这种多层链表结构的启发而设计出来的。按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 _O(logn)_。 - -但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的 2:1 的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点 _(也包括新插入的节点)_ 重新进行调整,这会让时间复杂度重新蜕化成 _O(n)_。删除数据也有同样的问题。 - -**skiplist** 为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是 **为每个节点随机出一个层数(level)**。比如,一个节点随机出的层数是 3,那么就把它链入到第 1 层到第 3 层这三层链表中。为了表达清楚,下图展示了如何通过一步步的插入操作从而形成一个 skiplist 的过程: - -![](https://upload-images.jianshu.io/upload_images/7896890-1e0626c013de095e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -从上面的创建和插入的过程中可以看出,每一个节点的层数(level)是随机出来的,而且新插入一个节点并不会影响到其他节点的层数,因此,**插入操作只需要修改节点前后的指针,而不需要对多个节点都进行调整**,这就降低了插入操作的复杂度。 - -现在我们假设从我们刚才创建的这个结构中查找 23 这个不存在的数,那么查找路径会如下图: - -![](https://upload-images.jianshu.io/upload_images/7896890-a8f66d808e8a4d1e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 二、跳跃表的实现 - -Redis 中的跳跃表由 `server.h/zskiplistNode` 和 `server.h/zskiplist` 两个结构定义,前者为跳跃表节点,后者则保存了跳跃节点的相关信息,同之前的 `集合 list` 结构类似,其实只有 `zskiplistNode` 就可以实现了,但是引入后者是为了更加方便的操作: - -```c -/* ZSETs use a specialized version of Skiplists */ -typedef struct zskiplistNode { - // value - sds ele; - // 分值 - double score; - // 后退指针 - struct zskiplistNode *backward; - // 层 - struct zskiplistLevel { - // 前进指针 - struct zskiplistNode *forward; - // 跨度 - unsigned long span; - } level[]; -} zskiplistNode; - -typedef struct zskiplist { - // 跳跃表头指针 - struct zskiplistNode *header, *tail; - // 表中节点的数量 - unsigned long length; - // 表中层数最大的节点的层数 - int level; -} zskiplist; -``` - -正如文章开头画出来的那张标准的跳跃表那样。 - -## 随机层数 - -对于每一个新插入的节点,都需要调用一个随机算法给它分配一个合理的层数,源码在 `t_zset.c/zslRandomLevel(void)` 中被定义: - -```c -int zslRandomLevel(void) { - int level = 1; - while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) - level += 1; - return (level-63 的概率被分配到最顶层,因为这里每一层的晋升率都是 50%。 - -**Redis 跳跃表默认允许最大的层数是 32**,被源码中 `ZSKIPLIST_MAXLEVEL` 定义,当 `Level[0]` 有 264 个元素时,才能达到 32 层,所以定义 32 完全够用了。 - -## 创建跳跃表 - -这个过程比较简单,在源码中的 `t_zset.c/zslCreate` 中被定义: - -```c -zskiplist *zslCreate(void) { - int j; - zskiplist *zsl; - - // 申请内存空间 - zsl = zmalloc(sizeof(*zsl)); - // 初始化层数为 1 - zsl->level = 1; - // 初始化长度为 0 - zsl->length = 0; - // 创建一个层数为 32,分数为 0,没有 value 值的跳跃表头节点 - zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); - - // 跳跃表头节点初始化 - for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) { - // 将跳跃表头节点的所有前进指针 forward 设置为 NULL - zsl->header->level[j].forward = NULL; - // 将跳跃表头节点的所有跨度 span 设置为 0 - zsl->header->level[j].span = 0; - } - // 跳跃表头节点的后退指针 backward 置为 NULL - zsl->header->backward = NULL; - // 表头指向跳跃表尾节点的指针置为 NULL - zsl->tail = NULL; - return zsl; -} - -``` - -即执行完之后创建了如下结构的初始化跳跃表: - -![](https://upload-images.jianshu.io/upload_images/7896890-551660604afd1041.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -## 插入节点实现 - -这几乎是最重要的一段代码了,但总体思路也比较清晰简单,如果理解了上面所说的跳跃表的原理,那么很容易理清楚插入节点时发生的几个动作 *(几乎跟链表类似)*: - -1. 找到当前我需要插入的位置 *(其中包括相同 score 时的处理)*; -2. 创建新节点,调整前后的指针指向,完成插入; - -为了方便阅读,我把源码 `t_zset.c/zslInsert` 定义的插入函数拆成了几个部分 - -### 第一部分:声明需要存储的变量 - -```c -// 存储搜索路径 -zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; -// 存储经过的节点跨度 -unsigned int rank[ZSKIPLIST_MAXLEVEL]; -int i, level; -``` - -### 第二部分:搜索当前节点插入位置 - -```c -serverAssert(!isnan(score)); -x = zsl->header; -// 逐步降级寻找目标节点,得到 "搜索路径" -for (i = zsl->level-1; i >= 0; i--) { - /* store rank that is crossed to reach the insert position */ - rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; - // 如果 score 相等,还需要比较 value 值 - while (x->level[i].forward && - (x->level[i].forward->score < score || - (x->level[i].forward->score == score && - sdscmp(x->level[i].forward->ele,ele) < 0))) - { - rank[i] += x->level[i].span; - x = x->level[i].forward; - } - // 记录 "搜索路径" - update[i] = x; -} -``` - -**讨论:** 有一种极端的情况,就是跳跃表中的所有 score 值都是一样,zset 的查找性能会不会退化为 O(n) 呢? - -从上面的源码中我们可以发现 zset 的排序元素不只是看 score 值,也会比较 value 值 *(字符串比较)* - -### 第三部分:生成插入节点 - -```c -/* we assume the element is not already inside, since we allow duplicated - * scores, reinserting the same element should never happen since the - * caller of zslInsert() should test in the hash table if the element is - * already inside or not. */ -level = zslRandomLevel(); -// 如果随机生成的 level 超过了当前最大 level 需要更新跳跃表的信息 -if (level > zsl->level) { - for (i = zsl->level; i < level; i++) { - rank[i] = 0; - update[i] = zsl->header; - update[i]->level[i].span = zsl->length; - } - zsl->level = level; -} -// 创建新节点 -x = zslCreateNode(level,score,ele); -``` - -### 第四部分:重排前向指针 - -```c -for (i = 0; i < level; i++) { - x->level[i].forward = update[i]->level[i].forward; - update[i]->level[i].forward = x; - - /* update span covered by update[i] as x is inserted here */ - x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); - update[i]->level[i].span = (rank[0] - rank[i]) + 1; -} - -/* increment span for untouched levels */ -for (i = level; i < zsl->level; i++) { - update[i]->level[i].span++; -} -``` - -### 第五部分:重排后向指针并返回 - -```c -x->backward = (update[0] == zsl->header) ? NULL : update[0]; -if (x->level[0].forward) - x->level[0].forward->backward = x; -else - zsl->tail = x; -zsl->length++; -return x; -``` - -## 节点删除实现 - -删除过程由源码中的 `t_zset.c/zslDeleteNode` 定义,和插入过程类似,都需要先把这个 **"搜索路径"** 找出来,然后对于每个层的相关节点重排一下前向后向指针,同时还要注意更新一下最高层数 `maxLevel`,直接放源码 *(如果理解了插入这里还是很容易理解的)*: - -```c -/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */ -void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) { - int i; - for (i = 0; i < zsl->level; i++) { - if (update[i]->level[i].forward == x) { - update[i]->level[i].span += x->level[i].span - 1; - update[i]->level[i].forward = x->level[i].forward; - } else { - update[i]->level[i].span -= 1; - } - } - if (x->level[0].forward) { - x->level[0].forward->backward = x->backward; - } else { - zsl->tail = x->backward; - } - while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL) - zsl->level--; - zsl->length--; -} - -/* Delete an element with matching score/element from the skiplist. - * The function returns 1 if the node was found and deleted, otherwise - * 0 is returned. - * - * If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise - * it is not freed (but just unlinked) and *node is set to the node pointer, - * so that it is possible for the caller to reuse the node (including the - * referenced SDS string at node->ele). */ -int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) { - zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; - int i; - - x = zsl->header; - for (i = zsl->level-1; i >= 0; i--) { - while (x->level[i].forward && - (x->level[i].forward->score < score || - (x->level[i].forward->score == score && - sdscmp(x->level[i].forward->ele,ele) < 0))) - { - x = x->level[i].forward; - } - update[i] = x; - } - /* We may have multiple elements with the same score, what we need - * is to find the element with both the right score and object. */ - x = x->level[0].forward; - if (x && score == x->score && sdscmp(x->ele,ele) == 0) { - zslDeleteNode(zsl, x, update); - if (!node) - zslFreeNode(x); - else - *node = x; - return 1; - } - return 0; /* not found */ -} -``` - -## 节点更新实现 - -当我们调用 `ZADD` 方法时,如果对应的 value 不存在,那就是插入过程,如果这个 value 已经存在,只是调整一下 score 的值,那就需要走一个更新流程。 - -假设这个新的 score 值并不会带来排序上的变化,那么就不需要调整位置,直接修改元素的 score 值就可以了,但是如果排序位置改变了,那就需要调整位置,该如何调整呢? - -从源码 `t_zset.c/zsetAdd` 函数 `1350` 行左右可以看到,Redis 采用了一个非常简单的策略: - -```c -/* Remove and re-insert when score changed. */ -if (score != curscore) { - zobj->ptr = zzlDelete(zobj->ptr,eptr); - zobj->ptr = zzlInsert(zobj->ptr,ele,score); - *flags |= ZADD_UPDATED; -} -``` - -**把这个元素删除再插入这个**,需要经过两次路径搜索,从这一点上来看,Redis 的 `ZADD` 代码似乎还有进一步优化的空间。 - -## 元素排名的实现 - -跳跃表本身是有序的,Redis 在 skiplist 的 forward 指针上进行了优化,给每一个 forward 指针都增加了 `span` 属性,用来 **表示从前一个节点沿着当前层的 forward 指针跳到当前这个节点中间会跳过多少个节点**。在上面的源码中我们也可以看到 Redis 在插入、删除操作时都会小心翼翼地更新 `span` 值的大小。 - -所以,沿着 **"搜索路径"**,把所有经过节点的跨度 `span` 值进行累加就可以算出当前元素的最终 rank 值了: - -```c -/* Find the rank for an element by both score and key. - * Returns 0 when the element cannot be found, rank otherwise. - * Note that the rank is 1-based due to the span of zsl->header to the - * first element. */ -unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) { - zskiplistNode *x; - unsigned long rank = 0; - int i; - - x = zsl->header; - for (i = zsl->level-1; i >= 0; i--) { - while (x->level[i].forward && - (x->level[i].forward->score < score || - (x->level[i].forward->score == score && - sdscmp(x->level[i].forward->ele,ele) <= 0))) { - // span 累加 - rank += x->level[i].span; - x = x->level[i].forward; - } - - /* x might be equal to zsl->header, so test if obj is non-NULL */ - if (x->ele && sdscmp(x->ele,ele) == 0) { - return rank; - } - } - return 0; -} -``` - - -# 扩展阅读 - -1. 跳跃表 Skip List 的原理和实现(Java) - [https://blog.csdn.net/DERRANTCM/article/details/79063312](https://blog.csdn.net/DERRANTCM/article/details/79063312) -2. 【算法导论33】跳跃表(Skip list)原理与java实现 - [https://blog.csdn.net/brillianteagle/article/details/52206261](https://blog.csdn.net/brillianteagle/article/details/52206261) - -# 参考资料 - -1. 《Redis 设计与实现》 - [http://redisbook.com/](http://redisbook.com/) -2. 【官方文档】Redis 数据类型介绍 - [http://www.redis.cn/topics/data-types-intro.html](http://www.redis.cn/topics/data-types-intro.html) -3. 《Redis 深度历险》 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -4. Redis 源码 - [https://github.com/antirez/redis](https://github.com/antirez/redis) -5. Redis 快速入门 - 易百教程 - [https://www.yiibai.com/redis/redis_quick_guide.html](https://www.yiibai.com/redis/redis_quick_guide.html) -6. Redis【入门】就这一篇! - [https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/](https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/) -7. Redis为什么用跳表而不用平衡树? - [https://mp.weixin.qq.com/s?__biz=MzA4NTg1MjM0Mg==&mid=2657261425&idx=1&sn=d840079ea35875a8c8e02d9b3e44cf95&scene=21#wechat_redirect](https://mp.weixin.qq.com/s?__biz=MzA4NTg1MjM0Mg==&mid=2657261425&idx=1&sn=d840079ea35875a8c8e02d9b3e44cf95&scene=21#wechat_redirect) -8. 为啥 redis 使用跳表(skiplist)而不是使用 red-black? - 知乎@于康 - [https://www.zhihu.com/question/20202931](https://www.zhihu.com/question/20202931) - diff --git "a/docs/database/Redis/redis-collection/Redis(3)\342\200\224\342\200\224\345\210\206\345\270\203\345\274\217\351\224\201\346\267\261\345\205\245\346\216\242\347\251\266.md" "b/docs/database/Redis/redis-collection/Redis(3)\342\200\224\342\200\224\345\210\206\345\270\203\345\274\217\351\224\201\346\267\261\345\205\245\346\216\242\347\251\266.md" deleted file mode 100644 index d05ff59401f..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(3)\342\200\224\342\200\224\345\210\206\345\270\203\345\274\217\351\224\201\346\267\261\345\205\245\346\216\242\347\251\266.md" +++ /dev/null @@ -1,228 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-5fe2adf61ccf11aa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、分布式锁简介 - -**锁** 是一种用来解决多个执行线程 **访问共享资源** 错误或数据不一致问题的工具。 - -如果 *把一台服务器比作一个房子*,那么 *线程就好比里面的住户*,当他们想要共同访问一个共享资源,例如厕所的时候,如果厕所门上没有锁...更甚者厕所没装门...这是会出原则性的问题的.. - -![](https://upload-images.jianshu.io/upload_images/7896890-26a364bddb9218eb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -装上了锁,大家用起来就安心多了,本质也就是 **同一时间只允许一个住户使用**。 - -而随着互联网世界的发展,单体应用已经越来越无法满足复杂互联网的高并发需求,转而慢慢朝着分布式方向发展,慢慢进化成了 **更大一些的住户**。所以同样,我们需要引入分布式锁来解决分布式应用之间访问共享资源的并发问题。 - -## 为何需要分布式锁 - -一般情况下,我们使用分布式锁主要有两个场景: - -1. **避免不同节点重复相同的工作**:比如用户执行了某个操作有可能不同节点会发送多封邮件; -2. **避免破坏数据的正确性**:如果两个节点在同一条数据上同时进行操作,可能会造成数据错误或不一致的情况出现; - -## Java 中实现的常见方式 - -上面我们用简单的比喻说明了锁的本质:**同一时间只允许一个用户操作**。所以理论上,能够满足这个需求的工具我们都能够使用 *(就是其他应用能帮我们加锁的)*: - -1. **基于 MySQL 中的锁**:MySQL 本身有自带的悲观锁 `for update` 关键字,也可以自己实现悲观/乐观锁来达到目的; -2. **基于 Zookeeper 有序节点**:Zookeeper 允许临时创建有序的子节点,这样客户端获取节点列表时,就能够当前子节点列表中的序号判断是否能够获得锁; -3. **基于 Redis 的单线程**:由于 Redis 是单线程,所以命令会以串行的方式执行,并且本身提供了像 `SETNX(set if not exists)` 这样的指令,本身具有互斥性; - -每个方案都有各自的优缺点,例如 MySQL 虽然直观理解容易,但是实现起来却需要额外考虑 **锁超时**、**加事务** 等,并且性能局限于数据库,诸如此类我们在此不作讨论,重点关注 Redis。 - -## Redis 分布式锁的问题 - -### 1)锁超时 - -假设现在我们有两台平行的服务 A B,其中 A 服务在 **获取锁之后** 由于未知神秘力量突然 **挂了**,那么 B 服务就永远无法获取到锁了: - -![](https://upload-images.jianshu.io/upload_images/7896890-4ea386c23ef0eec9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -所以我们需要额外设置一个超时时间,来保证服务的可用性。 - -但是另一个问题随即而来:**如果在加锁和释放锁之间的逻辑执行得太长,以至于超出了锁的超时限制**,也会出现问题。因为这时候第一个线程持有锁过期了,而临界区的逻辑还没有执行完,与此同时第二个线程就提前拥有了这把锁,导致临界区的代码不能得到严格的串行执行。 - -为了避免这个问题,**Redis 分布式锁不要用于较长时间的任务**。如果真的偶尔出现了问题,造成的数据小错乱可能就需要人工的干预。 - -有一个稍微安全一点的方案是 **将锁的 `value` 值设置为一个随机数**,释放锁时先匹配随机数是否一致,然后再删除 key,这是为了 **确保当前线程占有的锁不会被其他线程释放**,除非这个锁是因为过期了而被服务器自动释放的。 - -但是匹配 `value` 和删除 `key` 在 Redis 中并不是一个原子性的操作,也没有类似保证原子性的指令,所以可能需要使用像 Lua 这样的脚本来处理了,因为 Lua 脚本可以 **保证多个指令的原子性执行**。 - -### 延伸的讨论:GC 可能引发的安全问题 - -[Martin Kleppmann](https://martin.kleppmann.com/) 曾与 Redis 之父 Antirez 就 Redis 实现分布式锁的安全性问题进行过深入的讨论,其中有一个问题就涉及到 **GC**。 - -熟悉 Java 的同学肯定对 GC 不陌生,在 GC 的时候会发生 **STW(Stop-The-World)**,这本身是为了保障垃圾回收器的正常执行,但可能会引发如下的问题: - -![](https://upload-images.jianshu.io/upload_images/7896890-cf3a403968a23be4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -服务 A 获取了锁并设置了超时时间,但是服务 A 出现了 STW 且时间较长,导致了分布式锁进行了超时释放,在这个期间服务 B 获取到了锁,待服务 A STW 结束之后又恢复了锁,这就导致了 **服务 A 和服务 B 同时获取到了锁**,这个时候分布式锁就不安全了。 - -不仅仅局限于 Redis,Zookeeper 和 MySQL 有同样的问题。 - -想吃更多瓜的童鞋,可以访问下列网站看看 Redis 之父 Antirez 怎么说:[http://antirez.com/news/101](http://antirez.com/news/101) - -### 2)单点/多点问题 - -如果 Redis 采用单机部署模式,那就意味着当 Redis 故障了,就会导致整个服务不可用。 - -而如果采用主从模式部署,我们想象一个这样的场景:*服务 A* 申请到一把锁之后,如果作为主机的 Redis 宕机了,那么 *服务 B* 在申请锁的时候就会从从机那里获取到这把锁,为了解决这个问题,Redis 作者提出了一种 **RedLock 红锁** 的算法 *(Redission 同 Jedis)*: - -```java -// 三个 Redis 集群 -RLock lock1 = redissionInstance1.getLock("lock1"); -RLock lock2 = redissionInstance2.getLock("lock2"); -RLock lock3 = redissionInstance3.getLock("lock3"); - -RedissionRedLock lock = new RedissionLock(lock1, lock2, lock2); -lock.lock(); -// do something.... -lock.unlock(); -``` - -# 二、Redis 分布式锁的实现 - -分布式锁类似于 "占坑",而 `SETNX(SET if Not eXists)` 指令就是这样的一个操作,只允许被一个客户端占有,我们来看看 **源码(t_string.c/setGenericCommand)** 吧: - -```c -// SET/ SETEX/ SETTEX/ SETNX 最底层实现 -void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) { - long long milliseconds = 0; /* initialized to avoid any harmness warning */ - - // 如果定义了 key 的过期时间则保存到上面定义的变量中 - // 如果过期时间设置错误则返回错误信息 - if (expire) { - if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK) - return; - if (milliseconds <= 0) { - addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name); - return; - } - if (unit == UNIT_SECONDS) milliseconds *= 1000; - } - - // lookupKeyWrite 函数是为执行写操作而取出 key 的值对象 - // 这里的判断条件是: - // 1.如果设置了 NX(不存在),并且在数据库中找到了 key 值 - // 2.或者设置了 XX(存在),并且在数据库中没有找到该 key - // => 那么回复 abort_reply 给客户端 - if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) || - (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL)) - { - addReply(c, abort_reply ? abort_reply : shared.null[c->resp]); - return; - } - - // 在当前的数据库中设置键为 key 值为 value 的数据 - genericSetKey(c->db,key,val,flags & OBJ_SET_KEEPTTL); - // 服务器每修改一个 key 后都会修改 dirty 值 - server.dirty++; - if (expire) setExpire(c,c->db,key,mstime()+milliseconds); - notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id); - if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC, - "expire",key,c->db->id); - addReply(c, ok_reply ? ok_reply : shared.ok); -} -``` - -就像上面介绍的那样,其实在之前版本的 Redis 中,由于 `SETNX` 和 `EXPIRE` 并不是 **原子指令**,所以在一起执行会出现问题。 - -也许你会想到使用 Redis 事务来解决,但在这里不行,因为 `EXPIRE` 命令依赖于 `SETNX` 的执行结果,而事务中没有 `if-else` 的分支逻辑,如果 `SETNX` 没有抢到锁,`EXPIRE` 就不应该执行。 - -为了解决这个疑难问题,Redis 开源社区涌现了许多分布式锁的 library,为了治理这个乱象,后来在 Redis 2.8 的版本中,加入了 `SET` 指令的扩展参数,使得 `SETNX` 可以和 `EXPIRE` 指令一起执行了: - -```console -> SET lock:test true ex 5 nx -OK -... do something critical ... -> del lock:test -``` - -你只需要符合 `SET key value [EX seconds | PX milliseconds] [NX | XX] [KEEPTTL]` 这样的格式就好了,你也在下方右拐参照官方的文档: - -- 官方文档:[https://redis.io/commands/set](https://redis.io/commands/set) - -另外,官方文档也在 [`SETNX` 文档](https://redis.io/commands/setnx)中提到了这样一种思路:**把 SETNX 对应 key 的 value 设置为 **,这样在其他客户端访问时就能够自己判断是否能够获取下一个 value 为上述格式的锁了。 - -## 代码实现 - -下面用 Jedis 来模拟实现以下,关键代码如下: - -```java -private static final String LOCK_SUCCESS = "OK"; -private static final Long RELEASE_SUCCESS = 1L; -private static final String SET_IF_NOT_EXIST = "NX"; -private static final String SET_WITH_EXPIRE_TIME = "PX"; - -@Override -public String acquire() { - try { - // 获取锁的超时时间,超过这个时间则放弃获取锁 - long end = System.currentTimeMillis() + acquireTimeout; - // 随机生成一个 value - String requireToken = UUID.randomUUID().toString(); - while (System.currentTimeMillis() < end) { - String result = jedis - .set(lockKey, requireToken, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); - if (LOCK_SUCCESS.equals(result)) { - return requireToken; - } - try { - Thread.sleep(100); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } catch (Exception e) { - log.error("acquire lock due to error", e); - } - - return null; -} - -@Override -public boolean release(String identify) { - if (identify == null) { - return false; - } - - String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; - Object result = new Object(); - try { - result = jedis.eval(script, Collections.singletonList(lockKey), - Collections.singletonList(identify)); - if (RELEASE_SUCCESS.equals(result)) { - log.info("release lock success, requestToken:{}", identify); - return true; - } - } catch (Exception e) { - log.error("release lock due to error", e); - } finally { - if (jedis != null) { - jedis.close(); - } - } - - log.info("release lock failed, requestToken:{}, result:{}", identify, result); - return false; -} -``` - -- 引用自下方 *参考资料 3*,其中还有 RedLock 的实现和测试,有兴趣的童鞋可以戳一下 - -# 推荐阅读 - -1. 【官方文档】Distributed locks with Redis - [https://redis.io/topics/distlock](https://redis.io/topics/distlock) -2. Redis【入门】就这一篇! - [https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/](https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/) -3. Redission - Redis Java Client 源码 - [https://github.com/redisson/redisson](https://github.com/redisson/redisson) -4. 手写一个 Jedis 以及 JedisPool - [https://juejin.im/post/5e5101c46fb9a07cab3a953a](https://juejin.im/post/5e5101c46fb9a07cab3a953a) - -# 参考资料 - -1. 再有人问你分布式锁,这篇文章扔给他 - [https://juejin.im/post/5bbb0d8df265da0abd3533a5#heading-0](https://juejin.im/post/5bbb0d8df265da0abd3533a5#heading-0) -2. 【官方文档】Distributed locks with Redis - [https://redis.io/topics/distlock](https://redis.io/topics/distlock) -3. 【分布式缓存系列】Redis实现分布式锁的正确姿势 - [https://www.cnblogs.com/zhili/p/redisdistributelock.html](https://www.cnblogs.com/zhili/p/redisdistributelock.html) -4. Redis源码剖析和注释(九)--- 字符串命令的实现(t_string) - [https://blog.csdn.net/men_wen/article/details/70325566](https://blog.csdn.net/men_wen/article/details/70325566) -5. 《Redis 深度历险》 - 钱文品/ 著 - diff --git "a/docs/database/Redis/redis-collection/Redis(5)\342\200\224\342\200\224\344\272\277\347\272\247\346\225\260\346\215\256\350\277\207\346\273\244\345\222\214\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" "b/docs/database/Redis/redis-collection/Redis(5)\342\200\224\342\200\224\344\272\277\347\272\247\346\225\260\346\215\256\350\277\207\346\273\244\345\222\214\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" deleted file mode 100644 index b522f656de1..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(5)\342\200\224\342\200\224\344\272\277\347\272\247\346\225\260\346\215\256\350\277\207\346\273\244\345\222\214\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" +++ /dev/null @@ -1,361 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-59d043fad3a66d7f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、布隆过滤器简介 - -[上一次](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/) 我们学会了使用 **HyperLogLog** 来对大数据进行一个估算,它非常有价值,可以解决很多精确度不高的统计需求。但是如果我们想知道某一个值是不是已经在 **HyperLogLog** 结构里面了,它就无能为力了,它只提供了 `pfadd` 和 `pfcount` 方法,没有提供类似于 `contains` 的这种方法。 - -就举一个场景吧,比如你 **刷抖音**: - -![](https://upload-images.jianshu.io/upload_images/7896890-c7b6b5c8a47caf4a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -你有 **刷到过重复的推荐内容** 吗?这么多的推荐内容要推荐给这么多的用户,它是怎么保证每个用户在看推荐内容时,保证不会出现之前已经看过的推荐视频呢?也就是说,抖音是如何实现 **推送去重** 的呢? - -你会想到服务器 **记录** 了用户看过的 **所有历史记录**,当推荐系统推荐短视频时会从每个用户的历史记录里进行 **筛选**,过滤掉那些已经存在的记录。问题是当 **用户量很大**,每个用户看过的短视频又很多的情况下,这种方式,推荐系统的去重工作 **在性能上跟的上么?** - -实际上,如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行 `exists` 查询,当系统并发量很高时,数据库是很难抗住压力的。 - -![image](https://upload-images.jianshu.io/upload_images/7896890-099f3600b7022ef6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -你可能又想到了 **缓存**,但是这么多用户这么多的历史记录,如果全部缓存起来,那得需要 **浪费多大的空间** 啊.. *(可能老板看一眼账单,看一眼你..)* 并且这个存储空间会随着时间呈线性增长,就算你用缓存撑得住一个月,但是又能继续撑多久呢?不缓存性能又跟不上,咋办呢? - -![](https://upload-images.jianshu.io/upload_images/7896890-204e7440395a31b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -如上图所示,**布隆过滤器(Bloom Filter)** 就是这样一种专门用来解决去重问题的高级数据结构。但是跟 **HyperLogLog** 一样,它也一样有那么一点点不精确,也存在一定的误判概率,但它能在解决去重的同时,在 **空间上能节省 90%** 以上,也是非常值得的。 - -## 布隆过滤器是什么 - -**布隆过滤器(Bloom Filter)** 是 1970 年由布隆提出的。它 **实际上** 是一个很长的二进制向量和一系列随机映射函数 *(下面详细说)*,实际上你也可以把它 **简单理解** 为一个不怎么精确的 **set** 结构,当你使用它的 `contains` 方法判断某个对象是否存在时,它可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。 - -当布隆过滤器说某个值存在时,这个值 **可能不存在**;当它说不存在时,那么 **一定不存在**。打个比方,当它说不认识你时,那就是真的不认识,但是当它说认识你的时候,可能是因为你长得像它认识的另外一个朋友 *(脸长得有些相似)*,所以误判认识你。 - -![image](https://upload-images.jianshu.io/upload_images/7896890-757891d52045869d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 布隆过滤器的使用场景 - -基于上述的功能,我们大致可以把布隆过滤器用于以下的场景之中: - -- **大数据判断是否存在**:这就可以实现出上述的去重功能,如果你的服务器内存足够大的话,那么使用 HashMap 可能是一个不错的解决方案,理论上时间复杂度可以达到 O(1 的级别,但是当数据量起来之后,还是只能考虑布隆过滤器。 -- **解决缓存穿透**:我们经常会把一些热点数据放在 Redis 中当作缓存,例如产品详情。 通常一个请求过来之后我们会先查询缓存,而不用直接读取数据库,这是提升性能最简单也是最普遍的做法,但是 **如果一直请求一个不存在的缓存**,那么此时一定不存在缓存,那就会有 **大量请求直接打到数据库** 上,造成 **缓存穿透**,布隆过滤器也可以用来解决此类问题。 -- **爬虫/ 邮箱等系统的过滤**:平时不知道你有没有注意到有一些正常的邮件也会被放进垃圾邮件目录中,这就是使用布隆过滤器 **误判** 导致的。 - -# 二、布隆过滤器原理解析 - -布隆过滤器 **本质上** 是由长度为 `m` 的位向量或位列表(仅包含 `0` 或 `1` 位值的列表)组成,最初所有的值均设置为 `0`,所以我们先来创建一个稍微长一些的位向量用作展示: - -![](https://upload-images.jianshu.io/upload_images/7896890-362a693c82af3c8e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -当我们向布隆过滤器中添加数据时,会使用 **多个** `hash` 函数对 `key` 进行运算,算得一个证书索引值,然后对位数组长度进行取模运算得到一个位置,每个 `hash` 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 `1` 就完成了 `add` 操作,例如,我们添加一个 `wmyskxz`: - -![](https://upload-images.jianshu.io/upload_images/7896890-fdbf75a56fb03c02.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -向布隆过滤器查查询 `key` 是否存在时,跟 `add` 操作一样,会把这个 `key` 通过相同的多个 `hash` 函数进行运算,查看 **对应的位置** 是否 **都** 为 `1`,**只要有一个位为 `0`**,那么说明布隆过滤器中这个 `key` 不存在。如果这几个位置都是 `1`,并不能说明这个 `key` 一定存在,只能说极有可能存在,因为这些位置的 `1` 可能是因为其他的 `key` 存在导致的。 - -就比如我们在 `add` 了一定的数据之后,查询一个 **不存在** 的 `key`: - -![](https://upload-images.jianshu.io/upload_images/7896890-0beb6acc89d5c927.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -很明显,`1/3/5` 这几个位置的 `1` 是因为上面第一次添加的 `wmyskxz` 而导致的,所以这里就存在 **误判**。幸运的是,布隆过滤器有一个可以预判误判率的公式,比较复杂,感兴趣的朋友可以自行去阅读,比较烧脑.. 只需要记住以下几点就好了: - -- 使用时 **不要让实际元素数量远大于初始化数量**; -- 当实际元素数量超过初始化数量时,应该对布隆过滤器进行 **重建**,重新分配一个 `size` 更大的过滤器,再将所有的历史元素批量 `add` 进行; - -# 三、布隆过滤器的使用 - -**Redis 官方** 提供的布隆过滤器到了 **Redis 4.0** 提供了插件功能之后才正式登场。布隆过滤器作为一个插件加载到 Redis Server 中,给 Redis 提供了强大的布隆去重功能。下面我们来体验一下 Redis 4.0 的布隆过滤器,为了省去繁琐安装过程,我们直接用 -Docker 吧。 - -```bash -> docker pull redislabs/rebloom # 拉取镜像 -> docker run -p6379:6379 redislabs/rebloom # 运行容器 -> redis-cli # 连接容器中的 redis 服务 -``` - -如果上面三条指令执行没有问题,下面就可以体验布隆过滤器了。 - -- 当然,如果你不想使用 Docker,也可以在检查本机 Redis 版本合格之后自行安装插件,可以参考这里: [https://blog.csdn.net/u013030276/article/details/88350641](https://blog.csdn.net/u013030276/article/details/88350641) - -## 布隆过滤器的基本用法 - -布隆过滤器有两个基本指令,`bf.add` 添加元素,`bf.exists` 查询元素是否存在,它的用法和 set 集合的 `sadd` 和 `sismember` 差不多。注意 `bf.add` 只能一次添加一个元素,如果想要一次添加多个,就需要用到 `bf.madd` 指令。同样如果需要一次查询多个元素是否存在,就需要用到 `bf.mexists` 指令。 - -```bash -127.0.0.1:6379> bf.add codehole user1 -(integer) 1 -127.0.0.1:6379> bf.add codehole user2 -(integer) 1 -127.0.0.1:6379> bf.add codehole user3 -(integer) 1 -127.0.0.1:6379> bf.exists codehole user1 -(integer) 1 -127.0.0.1:6379> bf.exists codehole user2 -(integer) 1 -127.0.0.1:6379> bf.exists codehole user3 -(integer) 1 -127.0.0.1:6379> bf.exists codehole user4 -(integer) 0 -127.0.0.1:6379> bf.madd codehole user4 user5 user6 -1) (integer) 1 -2) (integer) 1 -3) (integer) 1 -127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7 -1) (integer) 1 -2) (integer) 1 -3) (integer) 1 -4) (integer) 0 -``` - -上面使用的布隆过过滤器只是默认参数的布隆过滤器,它在我们第一次 `add` 的时候自动创建。Redis 也提供了可以自定义参数的布隆过滤器,只需要在 `add` 之前使用 `bf.reserve` 指令显式创建就好了。如果对应的 `key` 已经存在,`bf.reserve` 会报错。 - -`bf.reserve` 有三个参数,分别是 `key`、`error_rate` *(错误率)* 和 `initial_size`: - -- **`error_rate` 越低,需要的空间越大**,对于不需要过于精确的场合,设置稍大一些也没有关系,比如上面说的推送系统,只会让一小部分的内容被过滤掉,整体的观看体验还是不会受到很大影响的; -- **`initial_size` 表示预计放入的元素数量**,当实际数量超过这个值时,误判率就会提升,所以需要提前设置一个较大的数值避免超出导致误判率升高; - -如果不适用 `bf.reserve`,默认的 `error_rate` 是 `0.01`,默认的 `initial_size` 是 `100`。 - -# 四、布隆过滤器代码实现 - -## 自己简单模拟实现 - -根据上面的基础理论,我们很容易就可以自己实现一个用于 `简单模拟` 的布隆过滤器数据结构: - -```java -public static class BloomFilter { - - private byte[] data; - - public BloomFilter(int initSize) { - this.data = new byte[initSize * 2]; // 默认创建大小 * 2 的空间 - } - - public void add(int key) { - int location1 = Math.abs(hash1(key) % data.length); - int location2 = Math.abs(hash2(key) % data.length); - int location3 = Math.abs(hash3(key) % data.length); - - data[location1] = data[location2] = data[location3] = 1; - } - - public boolean contains(int key) { - int location1 = Math.abs(hash1(key) % data.length); - int location2 = Math.abs(hash2(key) % data.length); - int location3 = Math.abs(hash3(key) % data.length); - - return data[location1] * data[location2] * data[location3] == 1; - } - - private int hash1(Integer key) { - return key.hashCode(); - } - - private int hash2(Integer key) { - int hashCode = key.hashCode(); - return hashCode ^ (hashCode >>> 3); - } - - private int hash3(Integer key) { - int hashCode = key.hashCode(); - return hashCode ^ (hashCode >>> 16); - } -} -``` - -这里很简单,内部仅维护了一个 `byte` 类型的 `data` 数组,实际上 `byte` 仍然占有一个字节之多,可以优化成 `bit` 来代替,这里也仅仅是用于方便模拟。另外我也创建了三个不同的 `hash` 函数,其实也就是借鉴 `HashMap` 哈希抖动的办法,分别使用自身的 `hash` 和右移不同位数相异或的结果。并且提供了基础的 `add` 和 `contains` 方法。 - -下面我们来简单测试一下这个布隆过滤器的效果如何: - -```java -public static void main(String[] args) { - Random random = new Random(); - // 假设我们的数据有 1 百万 - int size = 1_000_000; - // 用一个数据结构保存一下所有实际存在的值 - LinkedList existentNumbers = new LinkedList<>(); - BloomFilter bloomFilter = new BloomFilter(size); - - for (int i = 0; i < size; i++) { - int randomKey = random.nextInt(); - existentNumbers.add(randomKey); - bloomFilter.add(randomKey); - } - - // 验证已存在的数是否都存在 - AtomicInteger count = new AtomicInteger(); - AtomicInteger finalCount = count; - existentNumbers.forEach(number -> { - if (bloomFilter.contains(number)) { - finalCount.incrementAndGet(); - } - }); - System.out.printf("实际的数据量: %d, 判断存在的数据量: %d \n", size, count.get()); - - // 验证10个不存在的数 - count = new AtomicInteger(); - while (count.get() < 10) { - int key = random.nextInt(); - if (existentNumbers.contains(key)) { - continue; - } else { - // 这里一定是不存在的数 - System.out.println(bloomFilter.contains(key)); - count.incrementAndGet(); - } - } -} -``` - -输出如下: - -```bash -实际的数据量: 1000000, 判断存在的数据量: 1000000 -false -true -false -true -true -true -false -false -true -false -``` - -这就是前面说到的,当布隆过滤器说某个值 **存在时**,这个值 **可能不存在**,当它说某个值 **不存在时**,那就 **肯定不存在**,并且还有一定的误判率... - - -## 手动实现参考 - -当然上面的版本特别 low,不过主体思想是不差的,这里也给出一个好一些的版本用作自己实现测试的参考: - -```java -import java.util.BitSet; - -public class MyBloomFilter { - - /** - * 位数组的大小 - */ - private static final int DEFAULT_SIZE = 2 << 24; - /** - * 通过这个数组可以创建 6 个不同的哈希函数 - */ - private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134}; - - /** - * 位数组。数组中的元素只能是 0 或者 1 - */ - private BitSet bits = new BitSet(DEFAULT_SIZE); - - /** - * 存放包含 hash 函数的类的数组 - */ - private SimpleHash[] func = new SimpleHash[SEEDS.length]; - - /** - * 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样 - */ - public MyBloomFilter() { - // 初始化多个不同的 Hash 函数 - for (int i = 0; i < SEEDS.length; i++) { - func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]); - } - } - - /** - * 添加元素到位数组 - */ - public void add(Object value) { - for (SimpleHash f : func) { - bits.set(f.hash(value), true); - } - } - - /** - * 判断指定元素是否存在于位数组 - */ - public boolean contains(Object value) { - boolean ret = true; - for (SimpleHash f : func) { - ret = ret && bits.get(f.hash(value)); - } - return ret; - } - - /** - * 静态内部类。用于 hash 操作! - */ - public static class SimpleHash { - - private int cap; - private int seed; - - public SimpleHash(int cap, int seed) { - this.cap = cap; - this.seed = seed; - } - - /** - * 计算 hash 值 - */ - public int hash(Object value) { - int h; - return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16))); - } - - } -} -``` - -## 使用 Google 开源的 Guava 中自带的布隆过滤器 - -自己实现的目的主要是为了让自己搞懂布隆过滤器的原理,Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器。 - -首先我们需要在项目中引入 Guava 的依赖: - -```xml - - com.google.guava - guava - 28.0-jre - -``` - -实际使用如下: - -我们创建了一个最多存放 最多 1500 个整数的布隆过滤器,并且我们可以容忍误判的概率为百分之(0.01) - -```java -// 创建布隆过滤器对象 -BloomFilter filter = BloomFilter.create( - Funnels.integerFunnel(), - 1500, - 0.01); -// 判断指定元素是否存在 -System.out.println(filter.mightContain(1)); -System.out.println(filter.mightContain(2)); -// 将元素添加进布隆过滤器 -filter.put(1); -filter.put(2); -System.out.println(filter.mightContain(1)); -System.out.println(filter.mightContain(2)); -``` - -在我们的示例中,当 `mightContain()` 方法返回 `true` 时,我们可以 **99%** 确定该元素在过滤器中,当过滤器返回 `false` 时,我们可以 **100%** 确定该元素不存在于过滤器中。 - -Guava 提供的布隆过滤器的实现还是很不错的 *(想要详细了解的可以看一下它的源码实现)*,但是它有一个重大的缺陷就是只能单机使用 *(另外,容量扩展也不容易)*,而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到 **Redis** 中的布隆过滤器了。 - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) -4. Reids(4)——神奇的HyperLoglog解决统计问题 - [https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/)\ - -# 参考资料 - -1. 《Redis 深度历险》 - 钱文品/ 著 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -2. 5 分钟搞懂布隆过滤器,亿级数据过滤算法你值得拥有! - [https://juejin.im/post/5de1e37c5188256e8e43adfc](https://juejin.im/post/5de1e37c5188256e8e43adfc) -3. 【原创】不了解布隆过滤器?一文给你整的明明白白! - [https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md) - diff --git "a/docs/database/Redis/redis-collection/Redis(6)\342\200\224\342\200\224GeoHash\346\237\245\346\211\276\351\231\204\350\277\221\347\232\204\344\272\272.md" "b/docs/database/Redis/redis-collection/Redis(6)\342\200\224\342\200\224GeoHash\346\237\245\346\211\276\351\231\204\350\277\221\347\232\204\344\272\272.md" deleted file mode 100644 index 47152bf19db..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(6)\342\200\224\342\200\224GeoHash\346\237\245\346\211\276\351\231\204\350\277\221\347\232\204\344\272\272.md" +++ /dev/null @@ -1,227 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-8ccb98beab9aff6a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -像微信 **"附近的人"**,美团 **"附近的餐厅"**,支付宝共享单车 **"附近的车"** 是怎么设计实现的呢? - -# 一、使用数据库实现查找附近的人 - -我们都知道,地球上的任何一个位置都可以使用二维的 **经纬度** 来表示,经度范围 *[-180, 180]*,纬度范围 *[-90, 90]*,纬度正负以赤道为界,北正南负,经度正负以本初子午线 *(英国格林尼治天文台)* 为界,东正西负。比如说,北京人民英雄纪念碑的经纬度坐标就是 *(39.904610, 116.397724)*,都是正数,因为中国位于东北半球。 - -所以,当我们使用数据库存储了所有人的 **经纬度** 信息之后,我们就可以基于当前的坐标节点,来划分出一个矩形的范围,来得知附近的人,如下图: - -![](https://upload-images.jianshu.io/upload_images/7896890-c5e82d3cab59ad22.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -所以,我们很容易写出下列的伪 SQL 语句: - -```sql -SELECT id FROM positions WHERE x0 - r < x < x0 + r AND y0 - r < y < y0 + r -``` - -如果我们还想进一步地知道与每个坐标元素的距离并排序的话,就需要一定的计算。 - -当两个坐标元素的距离不是很远的时候,我们就可以简单利用 **勾股定理** 就能够得出他们之间的 **距离**。不过需要注意的是,地球不是一个标准的球体,**经纬度的密度** 是 **不一样** 的,所以我们使用勾股定理计算平方之后再求和时,需要按照一定的系数 **加权** 再进行求和。当然,如果不准求精确的话,加权也不必了。 - -参考下方 *参考资料 2* 我们能够差不多能写出如下优化之后的 SQL 语句来:*(仅供参考)* - -```sql -SELECT - * -FROM - users_location -WHERE - latitude > '.$lat.' - 1 - AND latitude < '.$lat.' + 1 AND longitude > '.$lon.' - 1 - AND longitude < '.$lon.' + 1 -ORDER BY - ACOS( - SIN( ( '.$lat.' * 3.1415 ) / 180 ) * SIN( ( latitude * 3.1415 ) / 180 ) + COS( ( '.$lat.' * 3.1415 ) / 180 ) * COS( ( latitude * 3.1415 ) / 180 ) * COS( ( '.$lon.' * 3.1415 ) / 180 - ( longitude * 3.1415 ) / 180 ) - ) * 6380 ASC - LIMIT 10 '; -``` - -为了满足高性能的矩形区域算法,数据表也需要把经纬度坐标加上 **双向复合索引 (x, y)**,这样可以满足最大优化查询性能。 - -# 二、GeoHash 算法简述 - -这是业界比较通用的,用于 **地理位置距离排序** 的一个算法,**Redis** 也采用了这样的算法。GeoHash 算法将 **二维的经纬度** 数据映射到 **一维** 的整数,这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算 **「附近的人时」**,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。 - -它的核心思想就是把整个地球看成是一个 **二维的平面**,然后把这个平面不断地等分成一个一个小的方格,**每一个** 坐标元素都位于其中的 **唯一一个方格** 中,等分之后的 **方格越小**,那么坐标也就 **越精确**,类似下图: - -![](https://upload-images.jianshu.io/upload_images/7896890-6396ae153a485857.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -经过划分的地球,我们需要对其进行编码: - -![](https://upload-images.jianshu.io/upload_images/7896890-573525c3f1179bbc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -经过这样顺序的编码之后,如果你仔细观察一会儿,你就会发现一些规律: - -- 横着的所有编码中,**第 2 位和第 4 位都是一样的**,例如第一排第一个 `0101` 和第二个 `0111`,他们的第 2 位和第 4 位都是 `1`; -- 竖着的所有编码中,**第 1 位和第 3 位是递增的**,例如第一排第一个 `0101`,如果单独把第 1 位和第 3 位拎出来的话,那就是 `00`,同理看第一排第二个 `0111`,同样的方法第 1 位和第 3 位拎出来是 `01`,刚好是 `00` 递增一个; - -通过这样的规律我们就把每一个小方块儿进行了一定顺序的编码,这样做的 **好处** 是显而易见的:每一个元素坐标既能够被 **唯一标识** 在这张被编码的地图上,也不至于 **暴露特别的具体的位置**,因为区域是共享的,我可以告诉你我就在公园附近,但是在具体的哪个地方你就无从得知了。 - -总之,我们通过上面的思想,能够把任意坐标变成一串二进制的编码了,类似于 `11010010110001000100` 这样 *(注意经度和维度是交替出现的哦..)*,通过这个整数我们就可以还原出元素的坐标,整数越长,还原出来的坐标值的损失程序就越小。对于 **"附近的人"** 这个功能来说,损失的一点经度可以忽略不计。 - -最后就是一个 `Base32` *(0~9, a~z, 去掉 a/i/l/o 四个字母)* 的编码操作,让它变成一个字符串,例如上面那一串儿就变成了 `wx4g0ec1`。 - -在 **Redis** 中,经纬度使用 `52` 位的整数进行编码,放进了 zset 里面,zset 的 `value` 是元素的 `key`,`score` 是 **GeoHash** 的 `52` 位整数值。zset 的 `score` 虽然是浮点数,但是对于 `52` 位的整数值来说,它可以无损存储。 - -# 三、在 Redis 中使用 Geo - -> 下方内容引自 *参考资料 1 - 《Redis 深度历险》* - -在使用 **Redis** 进行 **Geo 查询** 时,我们要时刻想到它的内部结构实际上只是一个 **zset(skiplist)**。通过 zset 的 `score` 排序就可以得到坐标附近的其他元素 *(实际情况要复杂一些,不过这样理解足够了)*,通过将 `score` 还原成坐标值就可以得到元素的原始坐标了。 - -Redis 提供的 Geo 指令只有 6 个,很容易就可以掌握。 - -## 增加 - -`geoadd` 指令携带集合名称以及多个经纬度名称三元组,注意这里可以加入多个三元组。 - -```bash -127.0.0.1:6379> geoadd company 116.48105 39.996794 juejin -(integer) 1 -127.0.0.1:6379> geoadd company 116.514203 39.905409 ireader -(integer) 1 -127.0.0.1:6379> geoadd company 116.489033 40.007669 meituan -(integer) 1 -127.0.0.1:6379> geoadd company 116.562108 39.787602 jd 116.334255 40.027400 xiaomi -(integer) 2 -``` - -不过很奇怪.. Redis 没有直接提供 Geo 的删除指令,但是我们可以通过 zset 相关的指令来操作 Geo 数据,所以元素删除可以使用 `zrem` 指令即可。 - -## 距离 - -`geodist` 指令可以用来计算两个元素之间的距离,携带集合名称、2 个名称和距离单位。 - -```bash -127.0.0.1:6379> geodist company juejin ireader km -"10.5501" -127.0.0.1:6379> geodist company juejin meituan km -"1.3878" -127.0.0.1:6379> geodist company juejin jd km -"24.2739" -127.0.0.1:6379> geodist company juejin xiaomi km -"12.9606" -127.0.0.1:6379> geodist company juejin juejin km -"0.0000" -``` - -我们可以看到掘金离美团最近,因为它们都在望京。距离单位可以是 `m`、`km`、`ml`、`ft`,分别代表米、千米、英里和尺。 - -## 获取元素位置 - -`geopos` 指令可以获取集合中任意元素的经纬度坐标,可以一次获取多个。 - -```bash -127.0.0.1:6379> geopos company juejin -1) 1) "116.48104995489120483" - 2) "39.99679348858259686" -127.0.0.1:6379> geopos company ireader -1) 1) "116.5142020583152771" - 2) "39.90540918662494363" -127.0.0.1:6379> geopos company juejin ireader -1) 1) "116.48104995489120483" - 2) "39.99679348858259686" -2) 1) "116.5142020583152771" - 2) "39.90540918662494363" -``` - -我们观察到获取的经纬度坐标和 `geoadd` 进去的坐标有轻微的误差,原因是 **Geohash** 对二维坐标进行的一维映射是有损的,通过映射再还原回来的值会出现较小的差别。对于 **「附近的人」** 这种功能来说,这点误差根本不是事。 - -## 获取元素的 hash 值 - -`geohash` 可以获取元素的经纬度编码字符串,上面已经提到,它是 `base32` 编码。 你可以使用这个编码值去 `http://geohash.org/${hash}` 中进行直接定位,它是 **Geohash** 的标准编码值。 - -```bash -127.0.0.1:6379> geohash company ireader -1) "wx4g52e1ce0" -127.0.0.1:6379> geohash company juejin -1) "wx4gd94yjn0" -``` - -让我们打开地址 `http://geohash.org/wx4g52e1ce0`,观察地图指向的位置是否正确: - -![](https://upload-images.jianshu.io/upload_images/7896890-b5d4215d6397729c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -很好,就是这个位置,非常准确。 - -## 附近的公司 -`georadiusbymember` 指令是最为关键的指令,它可以用来查询指定元素附近的其它元素,它的参数非常复杂。 - -```bash -# 范围 20 公里以内最多 3 个元素按距离正排,它不会排除自身 -127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc -1) "ireader" -2) "juejin" -3) "meituan" -# 范围 20 公里以内最多 3 个元素按距离倒排 -127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc -1) "jd" -2) "meituan" -3) "juejin" -# 三个可选参数 withcoord withdist withhash 用来携带附加参数 -# withdist 很有用,它可以用来显示距离 -127.0.0.1:6379> georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc -1) 1) "ireader" - 2) "0.0000" - 3) (integer) 4069886008361398 - 4) 1) "116.5142020583152771" - 2) "39.90540918662494363" -2) 1) "juejin" - 2) "10.5501" - 3) (integer) 4069887154388167 - 4) 1) "116.48104995489120483" - 2) "39.99679348858259686" -3) 1) "meituan" - 2) "11.5748" - 3) (integer) 4069887179083478 - 4) 1) "116.48903220891952515" - 2) "40.00766997707732031" -``` - -除了 `georadiusbymember` 指令根据元素查询附近的元素,**Redis** 还提供了根据坐标值来查询附近的元素,这个指令更加有用,它可以根据用户的定位来计算「附近的车」,「附近的餐馆」等。它的参数和 `georadiusbymember` 基本一致,除了将目标元素改成经纬度坐标值: - -```bash -127.0.0.1:6379> georadius company 116.514202 39.905409 20 km withdist count 3 asc -1) 1) "ireader" - 2) "0.0000" -2) 1) "juejin" - 2) "10.5501" -3) 1) "meituan" - 2) "11.5748" -``` - -## 注意事项 - -在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用 **Redis** 的 **Geo** 数据结构,它们将 **全部放在一个** zset 集合中。在 **Redis** 的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行。 - -所以,这里建议 **Geo** 的数据使用 **单独的 Redis 实例部署**,不使用集群环境。 - -如果数据量过亿甚至更大,就需要对 **Geo** 数据进行拆分,按国家拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。 - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) -4. Reids(4)——神奇的HyperLoglog解决统计问题 - [https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/) -5. Redis(5)——亿级数据过滤和布隆过滤器 - [https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/](https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/) - -# 参考资料 - -1. 《Redis 深度历险》 - 钱文品/ 著 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -2. mysql经纬度查询并且计算2KM范围内附近用户的sql查询性能优化实例教程 - [https://www.cnblogs.com/mgbert/p/4146538.html](https://www.cnblogs.com/mgbert/p/4146538.html) -3. Geohash算法原理及实现 - [https://www.jianshu.com/p/2fd0cf12e5ba](https://www.jianshu.com/p/2fd0cf12e5ba) -4. GeoHash算法学习讲解、解析及原理分析 - [https://zhuanlan.zhihu.com/p/35940647](https://zhuanlan.zhihu.com/p/35940647) - -> - 本文已收录至我的 Github 程序员成长系列 **【More Than Java】,学习,不止 Code,欢迎 star:[https://github.com/wmyskxz/MoreThanJava](https://github.com/wmyskxz/MoreThanJava)** -> - **个人公众号** :wmyskxz,**个人独立域名博客**:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长! - -![](https://upload-images.jianshu.io/upload_images/7896890-fca34cfd601e7449.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -非常感谢各位人才能 **看到这里**,如果觉得本篇文章写得不错,觉得 **「我没有三颗心脏」有点东西** 的话,**求点赞,求关注,求分享,求留言!** - -创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见! \ No newline at end of file diff --git "a/docs/database/Redis/redis-collection/Redis(7)\342\200\224\342\200\224\346\214\201\344\271\205\345\214\226.md" "b/docs/database/Redis/redis-collection/Redis(7)\342\200\224\342\200\224\346\214\201\344\271\205\345\214\226.md" deleted file mode 100644 index a2eb1d61b6f..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(7)\342\200\224\342\200\224\346\214\201\344\271\205\345\214\226.md" +++ /dev/null @@ -1,215 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-7879862264eeea7b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、持久化简介 - -**Redis** 的数据 **全部存储** 在 **内存** 中,如果 **突然宕机**,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 **持久化机制**,它会将内存中的数据库状态 **保存到磁盘** 中。 - -## 持久化发生了什么 | 从内存到磁盘 - -我们来稍微考虑一下 **Redis** 作为一个 **"内存数据库"** 要做的关于持久化的事情。通常来说,从客户端发起请求开始,到服务器真实地写入磁盘,需要发生如下几件事情: - -![](https://upload-images.jianshu.io/upload_images/7896890-5c209bc08da11abb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**详细版** 的文字描述大概就是下面这样: - -1. 客户端向数据库 **发送写命令** *(数据在客户端的内存中)* -2. 数据库 **接收** 到客户端的 **写请求** *(数据在服务器的内存中)* -3. 数据库 **调用系统 API** 将数据写入磁盘 *(数据在内核缓冲区中)* -4. 操作系统将 **写缓冲区** 传输到 **磁盘控控制器** *(数据在磁盘缓存中)* -5. 操作系统的磁盘控制器将数据 **写入实际的物理媒介** 中 *(数据在磁盘中)* - -**注意:** 上面的过程其实是 **极度精简** 的,在实际的操作系统中,**缓存** 和 **缓冲区** 会比这 **多得多**... - -## 如何尽可能保证持久化的安全 - -如果我们故障仅仅涉及到 **软件层面** *(该进程被管理员终止或程序崩溃)* 并且没有接触到内核,那么在 *上述步骤 3* 成功返回之后,我们就认为成功了。即使进程崩溃,操作系统仍然会帮助我们把数据正确地写入磁盘。 - -如果我们考虑 **停电/ 火灾** 等 **更具灾难性** 的事情,那么只有在完成了第 **5** 步之后,才是安全的。 - -![机房”火了“](https://upload-images.jianshu.io/upload_images/7896890-de083f477fe1bce4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -所以我们可以总结得出数据安全最重要的阶段是:**步骤三、四、五**,即: - -- 数据库软件调用写操作将用户空间的缓冲区转移到内核缓冲区的频率是多少? -- 内核多久从缓冲区取数据刷新到磁盘控制器? -- 磁盘控制器多久把数据写入物理媒介一次? -- **注意:** 如果真的发生灾难性的事件,我们可以从上图的过程中看到,任何一步都可能被意外打断丢失,所以只能 **尽可能地保证** 数据的安全,这对于所有数据库来说都是一样的。 - -我们从 **第三步** 开始。Linux 系统提供了清晰、易用的用于操作文件的 `POSIX file API`,`20` 多年过去,仍然还有很多人对于这一套 `API` 的设计津津乐道,我想其中一个原因就是因为你光从 `API` 的命名就能够很清晰地知道这一套 API 的用途: - -```c -int open(const char *path, int oflag, .../*,mode_t mode */); -int close (int filedes);int remove( const char *fname ); -ssize_t write(int fildes, const void *buf, size_t nbyte); -ssize_t read(int fildes, void *buf, size_t nbyte); -``` - -- 参考自:API 设计最佳实践的思考 - [https://www.cnblogs.com/yuanjiangw/p/10846560.html](https://www.cnblogs.com/yuanjiangw/p/10846560.html) - -所以,我们有很好的可用的 `API` 来完成 **第三步**,但是对于成功返回之前,我们对系统调用花费的时间没有太多的控制权。 - -然后我们来说说 **第四步**。我们知道,除了早期对电脑特别了解那帮人 *(操作系统就这帮人搞的)*,实际的物理硬件都不是我们能够 **直接操作** 的,都是通过 **操作系统调用** 来达到目的的。为了防止过慢的 I/O 操作拖慢整个系统的运行,操作系统层面做了很多的努力,譬如说 **上述第四步** 提到的 **写缓冲区**,并不是所有的写操作都会被立即写入磁盘,而是要先经过一个缓冲区,默认情况下,Linux 将在 **30 秒** 后实际提交写入。 - -![image](https://upload-images.jianshu.io/upload_images/7896890-c08b7572ef02d67b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -但是很明显,**30 秒** 并不是 Redis 能够承受的,这意味着,如果发生故障,那么最近 30 秒内写入的所有数据都可能会丢失。幸好 `PROSIX API` 提供了另一个解决方案:`fsync`,该命令会 **强制** 内核将 **缓冲区** 写入 **磁盘**,但这是一个非常消耗性能的操作,每次调用都会 **阻塞等待** 直到设备报告 IO 完成,所以一般在生产环境的服务器中,**Redis** 通常是每隔 1s 左右执行一次 `fsync` 操作。 - -到目前为止,我们了解到了如何控制 `第三步` 和 `第四步`,但是对于 **第五步**,我们 **完全无法控制**。也许一些内核实现将试图告诉驱动实际提交物理介质上的数据,或者控制器可能会为了提高速度而重新排序写操作,不会尽快将数据真正写到磁盘上,而是会等待几个多毫秒。这完全是我们无法控制的。 - - -# 二、Redis 中的两种持久化方式 - -## 方式一:快照 - -![image](https://upload-images.jianshu.io/upload_images/7896890-9a4d234c53120b33.gif?imageMogr2/auto-orient/strip) - -**Redis 快照** 是最简单的 Redis 持久性模式。当满足特定条件时,它将生成数据集的时间点快照,例如,如果先前的快照是在2分钟前创建的,并且现在已经至少有 *100* 次新写入,则将创建一个新的快照。此条件可以由用户配置 Redis 实例来控制,也可以在运行时修改而无需重新启动服务器。快照作为包含整个数据集的单个 `.rdb` 文件生成。 - -但我们知道,Redis 是一个 **单线程** 的程序,这意味着,我们不仅仅要响应用户的请求,还需要进行内存快照。而后者要求 Redis 必须进行 IO 操作,这会严重拖累服务器的性能。 - -还有一个重要的问题是,我们在 **持久化的同时**,**内存数据结构** 还可能在 **变化**,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,可是这才刚持久化结束,咋办? - -![](https://upload-images.jianshu.io/upload_images/7896890-fbfcbd606e95f105.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -### 使用系统多进程 COW(Copy On Write) 机制 | fork 函数 - -操作系统多进程 **COW(Copy On Write) 机制** 拯救了我们。**Redis** 在持久化时会调用 `glibc` 的函数 `fork` 产生一个子进程,简单理解也就是基于当前进程 **复制** 了一个进程,主进程和子进程会共享内存里面的代码块和数据段: - -![](https://upload-images.jianshu.io/upload_images/7896890-bc264b6a9f0c3404.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这里多说一点,**为什么 fork 成功调用后会有两个返回值呢?** 因为子进程在复制时复制了父进程的堆栈段,所以两个进程都停留在了 `fork` 函数中 *(都在同一个地方往下继续"同时"执行)*,等待返回,所以 **一次在父进程中返回子进程的 pid,另一次在子进程中返回零,系统资源不够时返回负数**。 *(伪代码如下)* - -```python -pid = os.fork() -if pid > 0: - handle_client_request() # 父进程继续处理客户端请求 -if pid == 0: - handle_snapshot_write() # 子进程处理快照写磁盘 -if pid < 0: - # fork error -``` - -所以 **快照持久化** 可以完全交给 **子进程** 来处理,**父进程** 则继续 **处理客户端请求**。**子进程** 做数据持久化,它 **不会修改现有的内存数据结构**,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是 **父进程** 不一样,它必须持续服务客户端请求,然后对 **内存数据结构进行不间断的修改**。 - -这个时候就会使用操作系统的 COW 机制来进行 **数据段页面** 的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复 -制一份分离出来,然后 **对这个复制的页面进行修改**。这时 **子进程** 相应的页面是 **没有变化的**,还是进程产生时那一瞬间的数据。 - -子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 **Redis** 的持久化 **叫「快照」的原因**。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。 - -## 方式二:AOF - -![](https://upload-images.jianshu.io/upload_images/7896890-e4e08ebef2cf0144.gif?imageMogr2/auto-orient/strip) - -**快照不是很持久**。如果运行 Redis 的计算机停止运行,电源线出现故障或者您 `kill -9` 的实例意外发生,则写入 Redis 的最新数据将丢失。尽管这对于某些应用程序可能不是什么大问题,但有些使用案例具有充分的耐用性,在这些情况下,快照并不是可行的选择。 - -**AOF(Append Only File - 仅追加文件)** 它的工作方式非常简单:每次执行 **修改内存** 中数据集的写操作时,都会 **记录** 该操作。假设 AOF 日志记录了自 Redis 实例创建以来 **所有的修改性指令序列**,那么就可以通过对一个空的 Redis 实例 **顺序执行所有的指令**,也就是 **「重放」**,来恢复 Redis 当前实例的内存数据结构的状态。 - -为了展示 AOF 在实际中的工作方式,我们来做一个简单的实验: - -```bash -./redis-server --appendonly yes # 设置一个新实例为 AOF 模式 -``` - -然后我们执行一些写操作: - -```bash -redis 127.0.0.1:6379> set key1 Hello -OK -redis 127.0.0.1:6379> append key1 " World!" -(integer) 12 -redis 127.0.0.1:6379> del key1 -(integer) 1 -redis 127.0.0.1:6379> del non_existing_key -(integer) 0 -``` - -前三个操作实际上修改了数据集,第四个操作没有修改,因为没有指定名称的键。这是 AOF 日志保存的文本: - -```bash -$ cat appendonly.aof -*2 -$6 -SELECT -$1 -0 -*3 -$3 -set -$4 -key1 -$5 -Hello -*3 -$6 -append -$4 -key1 -$7 - World! -*2 -$3 -del -$4 -key1 -``` - -如您所见,最后的那一条 `DEL` 指令不见了,因为它没有对数据集进行任何修改。 - -就是这么简单。当 Redis 收到客户端修改指令后,会先进行参数校验、逻辑处理,如果没问题,就 **立即** 将该指令文本 **存储** 到 AOF 日志中,也就是说,**先执行指令再将日志存盘**。这一点不同于 `MySQL`、`LevelDB`、`HBase` 等存储引擎,如果我们先存储日志再做逻辑处理,这样就可以保证即使宕机了,我们仍然可以通过之前保存的日志恢复到之前的数据状态,但是 **Redis 为什么没有这么做呢?** - -> Emmm... 没找到特别满意的答案,引用一条来自知乎上的回答吧: -> - **@缘于专注** - 我甚至觉得没有什么特别的原因。仅仅是因为,由于AOF文件会比较大,为了避免写入无效指令(错误指令),必须先做指令检查?如何检查,只能先执行了。因为语法级别检查并不能保证指令的有效性,比如删除一个不存在的key。而MySQL这种是因为它本身就维护了所有的表的信息,所以可以语法检查后过滤掉大部分无效指令直接记录日志,然后再执行。 -> - 更多讨论参见:[为什么Redis先执行指令,再记录AOF日志,而不是像其它存储引擎一样反过来呢? - https://www.zhihu.com/question/342427472](https://www.zhihu.com/question/342427472) - -### AOF 重写 - -![](https://upload-images.jianshu.io/upload_images/7896890-c21e2a37892ee989.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**Redis** 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 **AOF 日志 "瘦身"**。 - -**Redis** 提供了 `bgrewriteaof` 指令用于对 AOF 日志进行瘦身。其 **原理** 就是 **开辟一个子进程** 对内存进行 **遍历** 转换成一系列 Redis 的操作指令,**序列化到一个新的 AOF 日志文件** 中。序列化完毕后再将操作期间发生的 **增量 AOF 日志** 追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。 - -### fsync - -![](https://upload-images.jianshu.io/upload_images/7896890-384a546b5bf6b86d.gif?imageMogr2/auto-orient/strip) - -AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘的。 - -就像我们 *上方第四步* 描述的那样,我们需要借助 `glibc` 提供的 `fsync(int fd)` 函数来讲指定的文件内容 **强制从内核缓存刷到磁盘**。但 **"强制开车"** 仍然是一个很消耗资源的一个过程,需要 **"节制"**!通常来说,生产环境的服务器,Redis 每隔 1s 左右执行一次 `fsync` 操作就可以了。 - -Redis 同样也提供了另外两种策略,一个是 **永不 `fsync`**,来让操作系统来决定合适同步磁盘,很不安全,另一个是 **来一个指令就 `fsync` 一次**,非常慢。但是在生产环境基本不会使用,了解一下即可。 - -## Redis 4.0 混合持久化 - -![](https://upload-images.jianshu.io/upload_images/7896890-7de9f7706be6216c.gif?imageMogr2/auto-orient/strip) - -重启 Redis 时,我们很少使用 `rdb` 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 `rdb` 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 - -**Redis 4.0** 为了解决这个问题,带来了一个新的持久化选项——**混合持久化**。将 `rdb` 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 **自持久化开始到持久化结束** 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小: - -![](https://upload-images.jianshu.io/upload_images/7896890-2f7887f84eaa34d9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -于是在 Redis 重启的时候,可以先加载 `rdb` 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。 - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) -4. Reids(4)——神奇的HyperLoglog解决统计问题 - [https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/) -5. Redis(5)——亿级数据过滤和布隆过滤器 - [https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/](https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/) -6. Redis(6)——GeoHash查找附近的人[https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/](https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/) - -# 扩展阅读 - -1. Redis 数据备份与恢复 | 菜鸟教程 - [https://www.runoob.com/redis/redis-backup.html](https://www.runoob.com/redis/redis-backup.html) -2. Java Fork/Join 框架 - [https://www.cnblogs.com/cjsblog/p/9078341.html](https://www.cnblogs.com/cjsblog/p/9078341.html) - -# 参考资料 - -1. Redis persistence demystified | antirez weblog (作者博客) - [http://oldblog.antirez.com/post/redis-persistence-demystified.html](http://oldblog.antirez.com/post/redis-persistence-demystified.html) -2. 操作系统 — fork()函数的使用与底层原理 - [https://blog.csdn.net/Dawn_sf/article/details/78709839](https://blog.csdn.net/Dawn_sf/article/details/78709839) -3. 磁盘和内存读写简单原理 - [https://blog.csdn.net/zhanghongzheng3213/article/details/54141202](https://blog.csdn.net/zhanghongzheng3213/article/details/54141202) - diff --git "a/docs/database/Redis/redis-collection/Redis(8)\342\200\224\342\200\224\345\217\221\345\270\203\350\256\242\351\230\205\344\270\216Stream.md" "b/docs/database/Redis/redis-collection/Redis(8)\342\200\224\342\200\224\345\217\221\345\270\203\350\256\242\351\230\205\344\270\216Stream.md" deleted file mode 100644 index 55dad166603..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(8)\342\200\224\342\200\224\345\217\221\345\270\203\350\256\242\351\230\205\344\270\216Stream.md" +++ /dev/null @@ -1,531 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-31406a824536c54a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、Redis 中的发布/订阅功能 - -**发布/ 订阅系统** 是 Web 系统中比较常用的一个功能。简单点说就是 **发布者发布消息,订阅者接受消息**,这有点类似于我们的报纸/ 杂志社之类的: *(借用前边的一张图)* - -![](https://upload-images.jianshu.io/upload_images/7896890-13aa5cb2668368fe.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -- 图片引用自:「消息队列」看过来! - [https://www.wmyskxz.com/2019/07/16/xiao-xi-dui-lie-kan-guo-lai/](https://www.wmyskxz.com/2019/07/16/xiao-xi-dui-lie-kan-guo-lai/) - -从我们 *前面(下方相关阅读)* 学习的知识来看,我们虽然可以使用一个 `list` 列表结构结合 `lpush` 和 `rpop` 来实现消息队列的功能,但是似乎很难实现实现 **消息多播** 的功能: - -![](https://upload-images.jianshu.io/upload_images/7896890-526a5b110a7c4ea2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -为了支持消息多播,**Redis** 不能再依赖于那 5 种基础的数据结构了,它单独使用了一个模块来支持消息多播,这个模块就是 **PubSub**,也就是 **PublisherSubscriber** *(发布者/ 订阅者模式)*。 - -## PubSub 简介 - -我们从 *上面的图* 中可以看到,基于 `list` 结构的消息队列,是一种 `Publisher` 与 `Consumer` 点对点的强关联关系,**Redis** 为了消除这样的强关联,引入了另一种概念:**频道** *(channel)*: - -![](https://upload-images.jianshu.io/upload_images/7896890-cc3bb012eeca9fca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -当 `Publisher` 往 `channel` 中发布消息时,关注了指定 `channel` 的 `Consumer` 就能够同时受到消息。但这里的 **问题** 是,消费者订阅一个频道是必须 **明确指定频道名称** 的,这意味着,如果我们想要 **订阅多个** 频道,那么就必须 **显式地关注多个** 名称。 - -为了简化订阅的繁琐操作,**Redis** 提供了 **模式订阅** 的功能 **Pattern Subscribe**,这样就可以 **一次性关注多个频道** 了,即使生产者新增了同模式的频道,消费者也可以立即受到消息: - -![](https://upload-images.jianshu.io/upload_images/7896890-18ac258e4e9387da.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -例如上图中,**所有** 位于图片下方的 **`Consumer` 都能够受到消息**。 - -`Publisher` 往 `wmyskxz.chat` 这个 `channel` 中发送了一条消息,不仅仅关注了这个频道的 `Consumer 1` 和 `Consumer 2` 能够受到消息,图片中的两个 `channel` 都和模式 `wmyskxz.*` 匹配,所以 **Redis** 此时会同样发送消息给订阅了 `wmyskxz.*` 这个模式的 `Consumer 3` 和关注了在这个模式下的另一个频道 `wmyskxz.log` 下的 `Consumer 4` 和 `Consumer 5`。 - -另一方面,如果接收消息的频道是 `wmyskxz.chat`,那么 `Consumer 3` 也会受到消息。 - -## 快速体验 - -在 **Redis** 中,**PubSub** 模块的使用非常简单,常用的命令也就下面这么几条: - -```bash -# 订阅频道: -SUBSCRIBE channel [channel ....] # 订阅给定的一个或多个频道的信息 -PSUBSCRIBE pattern [pattern ....] # 订阅一个或多个符合给定模式的频道 -# 发布频道: -PUBLISH channel message # 将消息发送到指定的频道 -# 退订频道: -UNSUBSCRIBE [channel [channel ....]] # 退订指定的频道 -PUNSUBSCRIBE [pattern [pattern ....]] #退订所有给定模式的频道 -``` - -我们可以在本地快速地来体验一下 **PubSub**: - -![](https://upload-images.jianshu.io/upload_images/7896890-518e0d1e93135775.gif?imageMogr2/auto-orient/strip) - -具体步骤如下: - -1. 开启本地 Redis 服务,新建两个控制台窗口; -2. 在其中一个窗口输入 `SUBSCRIBE wmyskxz.chat` 关注 `wmyskxz.chat` 频道,让这个窗口成为 **消费者**。 -3. 在另一个窗口输入 `PUBLISH wmyskxz.chat 'message'` 往这个频道发送消息,这个时候就会看到 **另一个窗口实时地出现** 了发送的测试消息。 - -## 实现原理 - -可以看到,我们通过很简单的两条命令,几乎就可以简单使用这样的一个 **发布/ 订阅系统** 了,但是具体是怎么样实现的呢? - -**每个 Redis 服务器进程维持着一个标识服务器状态** 的 `redis.h/redisServer` 结构,其中就 **保存着有订阅的频道** 以及 **订阅模式** 的信息: - -```c -struct redisServer { - // ... - dict *pubsub_channels; // 订阅频道 - list *pubsub_patterns; // 订阅模式 - // ... -}; -``` - -### 订阅频道原理 - -当客户端订阅某一个频道之后,Redis 就会往 `pubsub_channels` 这个字典中新添加一条数据,实际上这个 `dict` 字典维护的是一张链表,比如,下图展示的 `pubsub_channels` 示例中,`client 1`、`client 2` 就订阅了 `channel 1`,而其他频道也分别被其他客户端订阅: - -![](https://upload-images.jianshu.io/upload_images/7896890-218fc15f7c368eee.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -#### SUBSCRIBE 命令 - -`SUBSCRIBE` 命令的行为可以用下列的伪代码表示: - -```python -def SUBSCRIBE(client, channels): - # 遍历所有输入频道 - for channel in channels: - # 将客户端添加到链表的末尾 - redisServer.pubsub_channels[channel].append(client) -``` - -通过 `pubsub_channels` 字典,程序只要检查某个频道是否为字典的键,就可以知道该频道是否正在被客户端订阅;只要取出某个键的值,就可以得到所有订阅该频道的客户端的信息。 - -#### PUBLISH 命令 - -了解 `SUBSCRIBE`,那么 `PUBLISH` 命令的实现也变得十分简单了,只需要通过上述字典定位到具体的客户端,再把消息发送给它们就好了:*(伪代码实现如下)* - -```python -def PUBLISH(channel, message): - # 遍历所有订阅频道 channel 的客户端 - for client in server.pubsub_channels[channel]: - # 将信息发送给它们 - send_message(client, message) -``` - -#### UNSUBSCRIBE 命令 - -使用 `UNSUBSCRIBE` 命令可以退订指定的频道,这个命令执行的是订阅的反操作:它从 `pubsub_channels` 字典的给定频道(键)中,删除关于当前客户端的信息,这样被退订频道的信息就不会再发送给这个客户端。 - -### 订阅模式原理 - -![](https://upload-images.jianshu.io/upload_images/7896890-18ac258e4e9387da.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -正如我们上面说到了,当发送一条消息到 `wmyskxz.chat` 这个频道时,Redis 不仅仅会发送到当前的频道,还会发送到匹配于当前模式的所有频道,实际上,`pubsub_patterns` 背后还维护了一个 `redis.h/pubsubPattern` 结构: - -```c -typedef struct pubsubPattern { - redisClient *client; // 订阅模式的客户端 - robj *pattern; // 订阅的模式 -} pubsubPattern; -``` - -每当调用 `PSUBSCRIBE` 命令订阅一个模式时,程序就创建一个包含客户端信息和被订阅模式的 `pubsubPattern` 结构,并将该结构添加到 `redisServer.pubsub_patterns` 链表中。 - -我们来看一个 `pusub_patterns` 链表的示例: - -![](https://upload-images.jianshu.io/upload_images/7896890-d0d3b1849fdb6162.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这个时候客户端 `client 3` 执行 `PSUBSCRIBE wmyskxz.java.*`,那么 `pubsub_patterns` 链表就会被更新成这样: - -![](https://upload-images.jianshu.io/upload_images/7896890-edbf11995590de50.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -通过遍历整个 `pubsub_patterns` 链表,程序可以检查所有正在被订阅的模式,以及订阅这些模式的客户端。 - -#### PUBLISH 命令 - -上面给出的伪代码并没有 **完整描述** `PUBLISH` 命令的行为,因为 `PUBLISH` 除了将 `message` 发送到 **所有订阅 `channel` 的客户端** 之外,它还会将 `channel` 和 `pubsub_patterns` 中的 **模式** 进行对比,如果 `channel` 和某个模式匹配的话,那么也将 `message` 发送到 **订阅那个模式的客户端**。 - -完整描述 `PUBLISH` 功能的伪代码定于如下: - -```python -def PUBLISH(channel, message): - # 遍历所有订阅频道 channel 的客户端 - for client in server.pubsub_channels[channel]: - # 将信息发送给它们 - send_message(client, message) - # 取出所有模式,以及订阅模式的客户端 - for pattern, client in server.pubsub_patterns: - # 如果 channel 和模式匹配 - if match(channel, pattern): - # 那么也将信息发给订阅这个模式的客户端 - send_message(client, message) -``` - -#### PUNSUBSCRIBE 命令 - -使用 `PUNSUBSCRIBE` 命令可以退订指定的模式,这个命令执行的是订阅模式的反操作:序会删除 `redisServer.pubsub_patterns` 链表中,所有和被退订模式相关联的 `pubsubPattern` 结构,这样客户端就不会再收到和模式相匹配的频道发来的信息。 - -## PubSub 的缺点 - -尽管 **Redis** 实现了 **PubSub** 模式来达到了 **多播消息队列** 的目的,但在实际的消息队列的领域,几乎 **找不到特别合适的场景**,因为它的缺点十分明显: - -- **没有 Ack 机制,也不保证数据的连续:** PubSub 的生产者传递过来一个消息,Redis 会直接找到相应的消费者传递过去。如果没有一个消费者,那么消息会被直接丢弃。如果开始有三个消费者,其中一个突然挂掉了,过了一会儿等它再重连时,那么重连期间的消息对于这个消费者来说就彻底丢失了。 -- **不持久化消息:** 如果 Redis 停机重启,PubSub 的消息是不会持久化的,毕竟 Redis 宕机就相当于一个消费者都没有,所有的消息都会被直接丢弃。 - - -基于上述缺点,Redis 的作者甚至单独开启了一个 Disque 的项目来专门用来做多播消息队列,不过该项目目前好像都没有成熟。不过后来在 2018 年 6 月,**Redis 5.0** 新增了 `Stream` 数据结构,这个功能给 Redis 带来了 **持久化消息队列**,从此 PubSub 作为消息队列的功能可以说是就消失了.. - -![image](https://upload-images.jianshu.io/upload_images/7896890-3a144fda1a0dafcb.gif?imageMogr2/auto-orient/strip) - -# 二、更为强大的 Stream | 持久化的发布/订阅系统 - -**Redis Stream** 从概念上来说,就像是一个 **仅追加内容** 的 **消息链表**,把所有加入的消息都一个一个串起来,每个消息都有一个唯一的 ID 和内容,这很简单,让它复杂的是从 Kafka 借鉴的另一种概念:**消费者组(Consumer Group)** *(思路一致,实现不同)*: - -![](https://upload-images.jianshu.io/upload_images/7896890-b9d8afde068a165f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -上图就展示了一个典型的 **Stream** 结构。每个 Stream 都有唯一的名称,它就是 Redis 的 `key`,在我们首次使用 `xadd` 指令追加消息时自动创建。我们对图中的一些概念做一下解释: - -- **Consumer Group**:消费者组,可以简单看成记录流状态的一种数据结构。消费者既可以选择使用 `XREAD` 命令进行 **独立消费**,也可以多个消费者同时加入一个消费者组进行 **组内消费**。同一个消费者组内的消费者共享所有的 Stream 信息,**同一条消息只会有一个消费者消费到**,这样就可以应用在分布式的应用场景中来保证消息的唯一性。 -- **last_delivered_id**:用来表示消费者组消费在 Stream 上 **消费位置** 的游标信息。每个消费者组都有一个 Stream 内 **唯一的名称**,消费者组不会自动创建,需要使用 `XGROUP CREATE` 指令来显式创建,并且需要指定从哪一个消息 ID 开始消费,用来初始化 `last_delivered_id` 这个变量。 -- **pending_ids**:每个消费者内部都有的一个状态变量,用来表示 **已经** 被客户端 **获取**,但是 **还没有 ack** 的消息。记录的目的是为了 **保证客户端至少消费了消息一次**,而不会在网络传输的中途丢失而没有对消息进行处理。如果客户端没有 ack,那么这个变量里面的消息 ID 就会越来越多,一旦某个消息被 ack,它就会对应开始减少。这个变量也被 Redis 官方称为 **PEL** *(Pending Entries List)*。 - - - -## 消息 ID 和消息内容 - -#### 消息 ID - -消息 ID 如果是由 `XADD` 命令返回自动创建的话,那么它的格式会像这样:`timestampInMillis-sequence` *(毫秒时间戳-序列号)*,例如 `1527846880585-5`,它表示当前的消息是在毫秒时间戳 `1527846880585` 时产生的,并且是该毫秒内产生的第 5 条消息。 - -这些 ID 的格式看起来有一些奇怪,**为什么要使用时间来当做 ID 的一部分呢?** 一方面,我们要 **满足 ID 自增** 的属性,另一方面,也是为了 **支持范围查找** 的功能。由于 ID 和生成消息的时间有关,这样就使得在根据时间范围内查找时基本上是没有额外损耗的。 - -当然消息 ID 也可以由客户端自定义,但是形式必须是 **"整数-整数"**,而且后面加入的消息的 ID 必须要大于前面的消息 ID。 - -#### 消息内容 - -消息内容就是普通的键值对,形如 hash 结构的键值对。 - -## 增删改查示例 - -增删改查命令很简单,详情如下: - -1. `xadd`:追加消息 -2. `xdel`:删除消息,这里的删除仅仅是设置了标志位,不影响消息总长度 -3. `xrange`:获取消息列表,会自动过滤已经删除的消息 -4. `xlen`:消息长度 -5. `del`:删除Stream - -使用示例: - -```bash -# *号表示服务器自动生成ID,后面顺序跟着一堆key/value -127.0.0.1:6379> xadd codehole * name laoqian age 30 # 名字叫laoqian,年龄30岁 -1527849609889-0 # 生成的消息ID -127.0.0.1:6379> xadd codehole * name xiaoyu age 29 -1527849629172-0 -127.0.0.1:6379> xadd codehole * name xiaoqian age 1 -1527849637634-0 -127.0.0.1:6379> xlen codehole -(integer) 3 -127.0.0.1:6379> xrange codehole - + # -表示最小值, +表示最大值 -1) 1) 1527849609889-0 - 2) 1) "name" - 2) "laoqian" - 3) "age" - 4) "30" -2) 1) 1527849629172-0 - 2) 1) "name" - 2) "xiaoyu" - 3) "age" - 4) "29" -3) 1) 1527849637634-0 - 2) 1) "name" - 2) "xiaoqian" - 3) "age" - 4) "1" -127.0.0.1:6379> xrange codehole 1527849629172-0 + # 指定最小消息ID的列表 -1) 1) 1527849629172-0 - 2) 1) "name" - 2) "xiaoyu" - 3) "age" - 4) "29" -2) 1) 1527849637634-0 - 2) 1) "name" - 2) "xiaoqian" - 3) "age" - 4) "1" -127.0.0.1:6379> xrange codehole - 1527849629172-0 # 指定最大消息ID的列表 -1) 1) 1527849609889-0 - 2) 1) "name" - 2) "laoqian" - 3) "age" - 4) "30" -2) 1) 1527849629172-0 - 2) 1) "name" - 2) "xiaoyu" - 3) "age" - 4) "29" -127.0.0.1:6379> xdel codehole 1527849609889-0 -(integer) 1 -127.0.0.1:6379> xlen codehole # 长度不受影响 -(integer) 3 -127.0.0.1:6379> xrange codehole - + # 被删除的消息没了 -1) 1) 1527849629172-0 - 2) 1) "name" - 2) "xiaoyu" - 3) "age" - 4) "29" -2) 1) 1527849637634-0 - 2) 1) "name" - 2) "xiaoqian" - 3) "age" - 4) "1" -127.0.0.1:6379> del codehole # 删除整个Stream -(integer) 1 -``` - -## 独立消费示例 - -我们可以在不定义消费组的情况下进行 Stream 消息的 **独立消费**,当 Stream 没有新消息时,甚至可以阻塞等待。Redis 设计了一个单独的消费指令 `xread`,可以将 Stream 当成普通的消息队列(list)来使用。使用 `xread` 时,我们可以完全忽略 **消费组(Consumer Group)** 的存在,就好比 Stream 就是一个普通的列表(list): - -```bash -# 从Stream头部读取两条消息 -127.0.0.1:6379> xread count 2 streams codehole 0-0 -1) 1) "codehole" - 2) 1) 1) 1527851486781-0 - 2) 1) "name" - 2) "laoqian" - 3) "age" - 4) "30" - 2) 1) 1527851493405-0 - 2) 1) "name" - 2) "yurui" - 3) "age" - 4) "29" -# 从Stream尾部读取一条消息,毫无疑问,这里不会返回任何消息 -127.0.0.1:6379> xread count 1 streams codehole $ -(nil) -# 从尾部阻塞等待新消息到来,下面的指令会堵住,直到新消息到来 -127.0.0.1:6379> xread block 0 count 1 streams codehole $ -# 我们从新打开一个窗口,在这个窗口往Stream里塞消息 -127.0.0.1:6379> xadd codehole * name youming age 60 -1527852774092-0 -# 再切换到前面的窗口,我们可以看到阻塞解除了,返回了新的消息内容 -# 而且还显示了一个等待时间,这里我们等待了93s -127.0.0.1:6379> xread block 0 count 1 streams codehole $ -1) 1) "codehole" - 2) 1) 1) 1527852774092-0 - 2) 1) "name" - 2) "youming" - 3) "age" - 4) "60" -(93.11s) -``` - -客户端如果想要使用 `xread` 进行 **顺序消费**,一定要 **记住当前消费** 到哪里了,也就是返回的消息 ID。下次继续调用 `xread` 时,将上次返回的最后一个消息 ID 作为参数传递进去,就可以继续消费后续的消息。 - -`block 0` 表示永远阻塞,直到消息到来,`block 1000` 表示阻塞 `1s`,如果 `1s` 内没有任何消息到来,就返回 `nil`: - -```bash -127.0.0.1:6379> xread block 1000 count 1 streams codehole $ -(nil) -(1.07s) -``` - -## 创建消费者示例 - -Stream 通过 `xgroup create` 指令创建消费组(Consumer Group),需要传递起始消息 ID 参数用来初始化 `last_delivered_id` 变量: - -```bash -127.0.0.1:6379> xgroup create codehole cg1 0-0 # 表示从头开始消费 -OK -# $表示从尾部开始消费,只接受新消息,当前Stream消息会全部忽略 -127.0.0.1:6379> xgroup create codehole cg2 $ -OK -127.0.0.1:6379> xinfo codehole # 获取Stream信息 - 1) length - 2) (integer) 3 # 共3个消息 - 3) radix-tree-keys - 4) (integer) 1 - 5) radix-tree-nodes - 6) (integer) 2 - 7) groups - 8) (integer) 2 # 两个消费组 - 9) first-entry # 第一个消息 -10) 1) 1527851486781-0 - 2) 1) "name" - 2) "laoqian" - 3) "age" - 4) "30" -11) last-entry # 最后一个消息 -12) 1) 1527851498956-0 - 2) 1) "name" - 2) "xiaoqian" - 3) "age" - 4) "1" -127.0.0.1:6379> xinfo groups codehole # 获取Stream的消费组信息 -1) 1) name - 2) "cg1" - 3) consumers - 4) (integer) 0 # 该消费组还没有消费者 - 5) pending - 6) (integer) 0 # 该消费组没有正在处理的消息 -2) 1) name - 2) "cg2" - 3) consumers # 该消费组还没有消费者 - 4) (integer) 0 - 5) pending - 6) (integer) 0 # 该消费组没有正在处理的消息 -``` - -## 组内消费示例 - -Stream 提供了 `xreadgroup` 指令可以进行消费组的组内消费,需要提供 **消费组名称、消费者名称和起始消息 ID**。它同 `xread` 一样,也可以阻塞等待新消息。读到新消息后,对应的消息 ID 就会进入消费者的 **PEL** *(正在处理的消息)* 结构里,客户端处理完毕后使用 `xack` 指令 **通知服务器**,本条消息已经处理完毕,该消息 ID 就会从 **PEL** 中移除,下面是示例: - -```bash -# >号表示从当前消费组的last_delivered_id后面开始读 -# 每当消费者读取一条消息,last_delivered_id变量就会前进 -127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole > -1) 1) "codehole" - 2) 1) 1) 1527851486781-0 - 2) 1) "name" - 2) "laoqian" - 3) "age" - 4) "30" -127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole > -1) 1) "codehole" - 2) 1) 1) 1527851493405-0 - 2) 1) "name" - 2) "yurui" - 3) "age" - 4) "29" -127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 2 streams codehole > -1) 1) "codehole" - 2) 1) 1) 1527851498956-0 - 2) 1) "name" - 2) "xiaoqian" - 3) "age" - 4) "1" - 2) 1) 1527852774092-0 - 2) 1) "name" - 2) "youming" - 3) "age" - 4) "60" -# 再继续读取,就没有新消息了 -127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole > -(nil) -# 那就阻塞等待吧 -127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole > -# 开启另一个窗口,往里塞消息 -127.0.0.1:6379> xadd codehole * name lanying age 61 -1527854062442-0 -# 回到前一个窗口,发现阻塞解除,收到新消息了 -127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole > -1) 1) "codehole" - 2) 1) 1) 1527854062442-0 - 2) 1) "name" - 2) "lanying" - 3) "age" - 4) "61" -(36.54s) -127.0.0.1:6379> xinfo groups codehole # 观察消费组信息 -1) 1) name - 2) "cg1" - 3) consumers - 4) (integer) 1 # 一个消费者 - 5) pending - 6) (integer) 5 # 共5条正在处理的信息还有没有ack -2) 1) name - 2) "cg2" - 3) consumers - 4) (integer) 0 # 消费组cg2没有任何变化,因为前面我们一直在操纵cg1 - 5) pending - 6) (integer) 0 -# 如果同一个消费组有多个消费者,我们可以通过xinfo consumers指令观察每个消费者的状态 -127.0.0.1:6379> xinfo consumers codehole cg1 # 目前还有1个消费者 -1) 1) name - 2) "c1" - 3) pending - 4) (integer) 5 # 共5条待处理消息 - 5) idle - 6) (integer) 418715 # 空闲了多长时间ms没有读取消息了 -# 接下来我们ack一条消息 -127.0.0.1:6379> xack codehole cg1 1527851486781-0 -(integer) 1 -127.0.0.1:6379> xinfo consumers codehole cg1 -1) 1) name - 2) "c1" - 3) pending - 4) (integer) 4 # 变成了5条 - 5) idle - 6) (integer) 668504 -# 下面ack所有消息 -127.0.0.1:6379> xack codehole cg1 1527851493405-0 1527851498956-0 1527852774092-0 1527854062442-0 -(integer) 4 -127.0.0.1:6379> xinfo consumers codehole cg1 -1) 1) name - 2) "c1" - 3) pending - 4) (integer) 0 # pel空了 - 5) idle - 6) (integer) 745505 -``` - -## QA 1:Stream 消息太多怎么办? | Stream 的上限 - -很容易想到,要是消息积累太多,Stream 的链表岂不是很长,内容会不会爆掉就是个问题了。`xdel` 指令又不会删除消息,它只是给消息做了个标志位。 - -Redis 自然考虑到了这一点,所以它提供了一个定长 Stream 功能。在 `xadd` 的指令提供一个定长长度 `maxlen`,就可以将老的消息干掉,确保最多不超过指定长度,使用起来也很简单: - -```bash -> XADD mystream MAXLEN 2 * value 1 -1526654998691-0 -> XADD mystream MAXLEN 2 * value 2 -1526654999635-0 -> XADD mystream MAXLEN 2 * value 3 -1526655000369-0 -> XLEN mystream -(integer) 2 -> XRANGE mystream - + -1) 1) 1526654999635-0 - 2) 1) "value" - 2) "2" -2) 1) 1526655000369-0 - 2) 1) "value" - 2) "3" -``` - -如果使用 `MAXLEN` 选项,当 Stream 的达到指定长度后,老的消息会自动被淘汰掉,因此 Stream 的大小是恒定的。目前还没有选项让 Stream 只保留给定数量的条目,因为为了一致地运行,这样的命令必须在很长一段时间内阻塞以淘汰消息。*(例如在添加数据的高峰期间,你不得不长暂停来淘汰旧消息和添加新的消息)* - -另外使用 `MAXLEN` 选项的花销是很大的,Stream 为了节省内存空间,采用了一种特殊的结构表示,而这种结构的调整是需要额外的花销的。所以我们可以使用一种带有 `~` 的特殊命令: - -```bash -XADD mystream MAXLEN ~ 1000 * ... entry fields here ... -``` - -它会基于当前的结构合理地对节点执行裁剪,来保证至少会有 `1000` 条数据,可能是 `1010` 也可能是 `1030`。 - -## QA 2:PEL 是如何避免消息丢失的? - - -在客户端消费者读取 Stream 消息时,Redis 服务器将消息回复给客户端的过程中,客户端突然断开了连接,消息就丢失了。但是 PEL 里已经保存了发出去的消息 ID,待客户端重新连上之后,可以再次收到 PEL 中的消息 ID 列表。不过此时 `xreadgroup` 的起始消息 ID 不能为参数 `>` ,而必须是任意有效的消息 ID,一般将参数设为 `0-0`,表示读取所有的 PEL 消息以及自 `last_delivered_id` 之后的新消息。 - - -## Redis Stream Vs Kafka - -Redis 基于内存存储,这意味着它会比基于磁盘的 Kafka 快上一些,也意味着使用 Redis 我们 **不能长时间存储大量数据**。不过如果您想以 **最小延迟** 实时处理消息的话,您可以考虑 Redis,但是如果 **消息很大并且应该重用数据** 的话,则应该首先考虑使用 Kafka。 - -另外从某些角度来说,`Redis Stream` 也更适用于小型、廉价的应用程序,因为 `Kafka` 相对来说更难配置一些。 - - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) -4. Reids(4)——神奇的HyperLoglog解决统计问题 - [https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/) -5. Redis(5)——亿级数据过滤和布隆过滤器 - [https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/](https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/) -6. Redis(6)——GeoHash查找附近的人[https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/](https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/) -7. Redis(7)——持久化【一文了解】 - [https://www.wmyskxz.com/2020/03/13/redis-7-chi-jiu-hua-yi-wen-liao-jie/](https://www.wmyskxz.com/2020/03/13/redis-7-chi-jiu-hua-yi-wen-liao-jie/) - - -# 参考资料 - -1. 订阅与发布——Redis 设计与实现 - [https://redisbook.readthedocs.io/en/latest/feature/pubsub.html](https://redisbook.readthedocs.io/en/latest/feature/pubsub.html) -2. 《Redis 深度历险》 - 钱文品/ 著 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -3. Introduction to Redis Streams【官方文档】 - [https://redis.io/topics/streams-intro](https://redis.io/topics/streams-intro) -4. Kafka vs. Redis: Log Aggregation Capabilities and Performance - [https://logz.io/blog/kafka-vs-redis/](https://logz.io/blog/kafka-vs-redis/) diff --git "a/docs/database/Redis/redis-collection/Redis(9)\342\200\224\342\200\224\351\233\206\347\276\244\345\205\245\351\227\250\345\256\236\350\267\265\346\225\231\347\250\213.md" "b/docs/database/Redis/redis-collection/Redis(9)\342\200\224\342\200\224\351\233\206\347\276\244\345\205\245\351\227\250\345\256\236\350\267\265\346\225\231\347\250\213.md" deleted file mode 100644 index b90f00793ae..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(9)\342\200\224\342\200\224\351\233\206\347\276\244\345\205\245\351\227\250\345\256\236\350\267\265\346\225\231\347\250\213.md" +++ /dev/null @@ -1,648 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-80c61b0ae541a750.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、Redis 集群概述 - -#### Redis 主从复制 - -到 [目前](#相关阅读) 为止,我们所学习的 Redis 都是 **单机版** 的,这也就意味着一旦我们所依赖的 Redis 服务宕机了,我们的主流程也会受到一定的影响,这当然是我们不能够接受的。 - -所以一开始我们的想法是:搞一台备用机。这样我们就可以在一台服务器出现问题的时候切换动态地到另一台去: - -![](https://upload-images.jianshu.io/upload_images/7896890-c48d255bc0b13672.gif?imageMogr2/auto-orient/strip) - -幸运的是,两个节点数据的同步我们可以使用 Redis 的 **主从同步** 功能帮助到我们,这样一来,有个备份,心里就踏实多了。 - -![](https://upload-images.jianshu.io/upload_images/7896890-4a32b9efa3885655.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -#### Redis 哨兵 - -后来因为某种神秘力量,Redis 老会在莫名其妙的时间点出问题 *(比如半夜 2 点)*,我总不能 24 小时时刻守在电脑旁边切换节点吧,于是另一个想法又开始了:给所有的节点找一个 **"管家"**,自动帮我监听照顾节点的状态并切换: - -![](https://upload-images.jianshu.io/upload_images/7896890-de8d9ce9e77bf211.gif?imageMogr2/auto-orient/strip) - -这大概就是 **Redis 哨兵** *(Sentinel)* 的简单理解啦。什么?管家宕机了怎么办?相较于有大量请求的 Redis 服务来说,管家宕机的概率就要小得多啦.. 如果真的宕机了,我们也可以直接切换成当前可用的节点保证可用.. - -![](https://upload-images.jianshu.io/upload_images/7896890-c7657fb8140d7cc6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -#### Redis 集群化 - -好了,通过上面的一些解决方案我们对 Redis 的 **稳定性** 稍微有了一些底气了,但单台节点的计算能力始终有限,所谓人多力量大,如果我们把 **多个节点组合** 成 **一个可用的工作节点**,那就大大增加了 Redis 的 **高可用、可扩展、分布式、容错** 等特性: - -![](https://upload-images.jianshu.io/upload_images/7896890-8957aa6d1484c5de.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 二、主从复制 - -![](https://upload-images.jianshu.io/upload_images/7896890-4956a718c124a81f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**主从复制**,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 **主节点(master)**,后者称为 **从节点(slave)**。且数据的复制是 **单向** 的,只能由主节点到从节点。Redis 主从复制支持 **主从同步** 和 **从从同步** 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。 - -#### 主从复制主要的作用 - -- **数据冗余:** 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。 -- **故障恢复:** 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 *(实际上是一种服务的冗余)*。 -- **负载均衡:** 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 *(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点)*,分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。 -- **高可用基石:** 除了上述作用以外,主从复制还是哨兵和集群能够实施的 **基础**,因此说主从复制是 Redis 高可用的基础。 - -## 快速体验 - -在 **Redis** 中,用户可以通过执行 `SLAVEOF` 命令或者设置 `slaveof` 选项,让一个服务器去复制另一个服务器,以下三种方式是 **完全等效** 的: - -- **配置文件**:在从服务器的配置文件中加入:`slaveof ` -- **启动命令**:redis-server 启动命令后加入 `--slaveof ` -- **客户端命令**:Redis 服务器启动后,直接通过客户端执行命令:`slaveof `,让该 Redis 实例成为从节点。 - -需要注意的是:**主从复制的开启,完全是在从节点发起的,不需要我们在主节点做任何事情。** - -#### 第一步:本地启动两个节点 - -在正确安装好 Redis 之后,我们可以使用 `redis-server --port ` 的方式指定创建两个不同端口的 Redis 实例,例如,下方我分别创建了一个 `6379` 和 `6380` 的两个 Redis 实例: - -```bash -# 创建一个端口为 6379 的 Redis 实例 -redis-server --port 6379 -# 创建一个端口为 6380 的 Redis 实例 -redis-server --port 6380 -``` - -此时两个 Redis 节点启动后,都默认为 **主节点**。 - -#### 第二步:建立复制 - -我们在 `6380` 端口的节点中执行 `slaveof` 命令,使之变为从节点: - -```bash -# 在 6380 端口的 Redis 实例中使用控制台 -redis-cli -p 6380 -# 成为本地 6379 端口实例的从节点 -127.0.0.1:6380> SLAVEOF 127.0.0.1ø 6379 -OK -``` - -#### 第三步:观察效果 - -下面我们来验证一下,主节点的数据是否会复制到从节点之中: - -- 先在 **从节点** 中查询一个 **不存在** 的 key: -```bash -127.0.0.1:6380> GET mykey -(nil) -``` -- 再在 **主节点** 中添加这个 key: -```bash -127.0.0.1:6379> SET mykey myvalue -OK -``` -- 此时再从 **从节点** 中查询,会发现已经从 **主节点** 同步到 **从节点**: -```bash -127.0.0.1:6380> GET mykey -"myvalue" -``` - -#### 第四步:断开复制 - -通过 `slaveof ` 命令建立主从复制关系以后,可以通过 `slaveof no one` 断开。需要注意的是,从节点断开复制后,**不会删除已有的数据**,只是不再接受主节点新的数据变化。 - -从节点执行 `slaveof no one` 之后,从节点和主节点分别打印日志如下:、 - -```bash -# 从节点打印日志 -61496:M 17 Mar 2020 08:10:22.749 # Connection with master lost. -61496:M 17 Mar 2020 08:10:22.749 * Caching the disconnected master state. -61496:M 17 Mar 2020 08:10:22.749 * Discarding previously cached master state. -61496:M 17 Mar 2020 08:10:22.749 * MASTER MODE enabled (user request from 'id=4 addr=127.0.0.1:55096 fd=8 name= age=1664 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=34 qbuf-free=32734 obl=0 oll=0 omem=0 events=r cmd=slaveof') - -# 主节点打印日志 -61467:M 17 Mar 2020 08:10:22.749 # Connection with replica 127.0.0.1:6380 lost. -``` - -## 实现原理简析 - -![](https://upload-images.jianshu.io/upload_images/7896890-c97a6bcc0936cd17.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -为了节省篇幅,我把主要的步骤都 **浓缩** 在了上图中,其实也可以 **简化成三个阶段:准备阶段-数据同步阶段-命令传播阶段**。下面我们来进行一些必要的说明。 - -#### 身份验证 | 主从复制安全问题 - -在上面的 **快速体验** 过程中,你会发现 `slaveof` 这个命令居然不需要验证?这意味着只要知道了 ip 和端口就可以随意拷贝服务器上的数据了? - -![](https://upload-images.jianshu.io/upload_images/7896890-d0c7a74da972fca3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -那当然不能够了,我们可以通过在 **主节点** 配置 `requirepass` 来设置密码,这样就必须在 **从节点** 中对应配置好 `masterauth` 参数 *(与主节点 `requirepass` 保持一致)* 才能够进行正常复制了。 - -#### SYNC 命令是一个非常耗费资源的操作 - -每次执行 `SYNC` 命令,主从服务器需要执行如下动作: - -1. **主服务器** 需要执行 `BGSAVE` 命令来生成 RDB 文件,这个生成操作会 **消耗** 主服务器大量的 **CPU、内存和磁盘 I/O 的资源**; -2. **主服务器** 需要将自己生成的 RDB 文件 发送给从服务器,这个发送操作会 **消耗** 主服务器 **大量的网络资源** *(带宽和流量)*,并对主服务器响应命令请求的时间产生影响; -3. 接收到 RDB 文件的 **从服务器** 需要载入主服务器发来的 RBD 文件,并且在载入期间,从服务器 **会因为阻塞而没办法处理命令请求**; - -特别是当出现 **断线重复制** 的情况是时,为了让从服务器补足断线时确实的那一小部分数据,却要执行一次如此耗资源的 `SYNC` 命令,显然是不合理的。 - -#### PSYNC 命令的引入 - -所以在 **Redis 2.8** 中引入了 `PSYNC` 命令来代替 `SYNC`,它具有两种模式: - -1. **全量复制:** 用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作; -2. **部分复制:** 用于网络中断等情况后的复制,只将 **中断期间主节点执行的写命令** 发送给从节点,与全量复制相比更加高效。**需要注意** 的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制; - - -部分复制的原理主要是靠主从节点分别维护一个 **复制偏移量**,有了这个偏移量之后断线重连之后一比较,之后就可以仅仅把从服务器断线之后确实的这部分数据给补回来了。 - -> 更多的详细内容可以参考下方 *参考资料 3* - -# 三、Redis Sentinel 哨兵 - -![](https://upload-images.jianshu.io/upload_images/7896890-884d5be9a2ddfebc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -*上图* 展示了一个典型的哨兵架构图,它由两部分组成,哨兵节点和数据节点: - -- **哨兵节点:** 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据; -- **数据节点:** 主节点和从节点都是数据节点; - -在复制的基础上,哨兵实现了 **自动化的故障恢复** 功能,下方是官方对于哨兵功能的描述: - -- **监控(Monitoring):** 哨兵会不断地检查主节点和从节点是否运作正常。 -- **自动故障转移(Automatic failover):** 当 **主节点** 不能正常工作时,哨兵会开始 **自动故障转移操作**,它会将失效主节点的其中一个 **从节点升级为新的主节点**,并让其他从节点改为复制新的主节点。 -- **配置提供者(Configuration provider):** 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。 -- **通知(Notification):** 哨兵可以将故障转移的结果发送给客户端。 - -其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能,则需要在与客户端的交互中才能体现。 - -## 快速体验 - -#### 第一步:创建主从节点配置文件并启动 - -正确安装好 Redis 之后,我们去到 Redis 的安装目录 *(mac 默认在 `/usr/local/`)*,找到 `redis.conf` 文件复制三份分别命名为 `redis-master.conf`/`redis-slave1.conf`/`redis-slave2.conf`,分别作为 `1` 个主节点和 `2` 个从节点的配置文件 *(下图演示了我本机的 `redis.conf` 文件的位置)* - -![](https://upload-images.jianshu.io/upload_images/7896890-34de77bfca56d32e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -打开可以看到这个 `.conf` 后缀的文件里面有很多说明的内容,全部删除然后分别改成下面的样子: - -```bash -#redis-master.conf -port 6379 -daemonize yes -logfile "6379.log" -dbfilename "dump-6379.rdb" - -#redis-slave1.conf -port 6380 -daemonize yes -logfile "6380.log" -dbfilename "dump-6380.rdb" -slaveof 127.0.0.1 6379 - -#redis-slave2.conf -port 6381 -daemonize yes -logfile "6381.log" -dbfilename "dump-6381.rdb" -slaveof 127.0.0.1 6379 -``` - -然后我们可以执行 `redis-server ` 来根据配置文件启动不同的 Redis 实例,依次启动主从节点: - -```bash -redis-server /usr/local/redis-5.0.3/redis-master.conf -redis-server /usr/local/redis-5.0.3/redis-slave1.conf -redis-server /usr/local/redis-5.0.3/redis-slave2.conf -``` - -节点启动后,我们执行 `redis-cli` 默认连接到我们端口为 `6379` 的主节点执行 `info Replication` 检查一下主从状态是否正常:*(可以看到下方正确地显示了两个从节点)* - -![](https://upload-images.jianshu.io/upload_images/7896890-a1c935f094240cac.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -#### 第二步:创建哨兵节点配置文件并启动 - -按照上面同样的方法,我们给哨兵节点也创建三个配置文件。*(哨兵节点本质上是特殊的 Redis 节点,所以配置几乎没什么差别,只是在端口上做区分就好)* - -```bash -# redis-sentinel-1.conf -port 26379 -daemonize yes -logfile "26379.log" -sentinel monitor mymaster 127.0.0.1 6379 2 - -# redis-sentinel-2.conf -port 26380 -daemonize yes -logfile "26380.log" -sentinel monitor mymaster 127.0.0.1 6379 2 - -# redis-sentinel-3.conf -port 26381 -daemonize yes -logfile "26381.log" -sentinel monitor mymaster 127.0.0.1 6379 2 -``` - -其中,`sentinel monitor mymaster 127.0.0.1 6379 2` 配置的含义是:该哨兵节点监控 `127.0.0.1:6379` 这个主节点,该主节点的名称是 `mymaster`,最后的 `2` 的含义与主节点的故障判定有关:至少需要 `2` 个哨兵节点同意,才能判定主节点故障并进行故障转移。 - -执行下方命令将哨兵节点启动起来: - -```bash -redis-server /usr/local/redis-5.0.3/redis-sentinel-1.conf --sentinel -redis-server /usr/local/redis-5.0.3/redis-sentinel-2.conf --sentinel -redis-server /usr/local/redis-5.0.3/redis-sentinel-3.conf --sentinel -``` - -使用 `redis-cil` 工具连接哨兵节点,并执行 `info Sentinel` 命令来查看是否已经在监视主节点了: - -```bash -# 连接端口为 26379 的 Redis 节点 -➜ ~ redis-cli -p 26379 -127.0.0.1:26379> info Sentinel -# Sentinel -sentinel_masters:1 -sentinel_tilt:0 -sentinel_running_scripts:0 -sentinel_scripts_queue_length:0 -sentinel_simulate_failure_flags:0 -master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3 -``` - -此时你打开刚才写好的哨兵配置文件,你还会发现出现了一些变化: - -#### 第三步:演示故障转移 - -首先,我们使用 `kill -9` 命令来杀掉主节点,**同时** 在哨兵节点中执行 `info Sentinel` 命令来观察故障节点的过程: - -```bash -➜ ~ ps aux | grep 6379 -longtao 74529 0.3 0.0 4346936 2132 ?? Ss 10:30上午 0:03.09 redis-server *:26379 [sentinel] -longtao 73541 0.2 0.0 4348072 2292 ?? Ss 10:18上午 0:04.79 redis-server *:6379 -longtao 75521 0.0 0.0 4286728 728 s008 S+ 10:39上午 0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn 6379 -longtao 74836 0.0 0.0 4289844 944 s006 S+ 10:32上午 0:00.01 redis-cli -p 26379 -➜ ~ kill -9 73541 -``` - -如果 **刚杀掉瞬间** 在哨兵节点中执行 `info` 命令来查看,会发现主节点还没有切换过来,因为哨兵发现主节点故障并转移需要一段时间: - -```bash -# 第一时间查看哨兵节点发现并未转移,还在 6379 端口 -127.0.0.1:26379> info Sentinel -# Sentinel -sentinel_masters:1 -sentinel_tilt:0 -sentinel_running_scripts:0 -sentinel_scripts_queue_length:0 -sentinel_simulate_failure_flags:0 -master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3 -``` - -一段时间之后你再执行 `info` 命令,查看,你就会发现主节点已经切换成了 `6381` 端口的从节点: - -```bash -# 过一段时间之后在执行,发现已经切换了 6381 端口 -127.0.0.1:26379> info Sentinel -# Sentinel -sentinel_masters:1 -sentinel_tilt:0 -sentinel_running_scripts:0 -sentinel_scripts_queue_length:0 -sentinel_simulate_failure_flags:0 -master0:name=mymaster,status=ok,address=127.0.0.1:6381,slaves=2,sentinels=3 -``` - -但同时还可以发现,**哨兵节点认为新的主节点仍然有两个从节点** *(上方 slaves=2)*,这是因为哨兵在将 `6381` 切换成主节点的同时,将 `6379` 节点置为其从节点。虽然 `6379` 从节点已经挂掉,但是由于 **哨兵并不会对从节点进行客观下线**,因此认为该从节点一直存在。当 `6379` 节点重新启动后,会自动变成 `6381` 节点的从节点。 - -另外,在故障转移的阶段,哨兵和主从节点的配置文件都会被改写: - -- **对于主从节点:** 主要是 `slaveof` 配置的变化,新的主节点没有了 `slaveof` 配置,其从节点则 `slaveof` 新的主节点。 -- **对于哨兵节点:** 除了主从节点信息的变化,纪元(epoch) *(记录当前集群状态的参数)* 也会变化,纪元相关的参数都 +1 了。 - -## 客户端访问哨兵系统代码演示 - -上面我们在 *快速体验* 中主要感受到了服务端自己对于当前主从节点的自动化治理,下面我们以 Java 代码为例,来演示一下客户端如何访问我们的哨兵系统: - -```java -public static void testSentinel() throws Exception { - String masterName = "mymaster"; - Set sentinels = new HashSet<>(); - sentinels.add("127.0.0.1:26379"); - sentinels.add("127.0.0.1:26380"); - sentinels.add("127.0.0.1:26381"); - - // 初始化过程做了很多工作 - JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels); - Jedis jedis = pool.getResource(); - jedis.set("key1", "value1"); - pool.close(); -} -``` - -#### 客户端原理 - -Jedis 客户端对哨兵提供了很好的支持。如上述代码所示,我们只需要向 Jedis 提供哨兵节点集合和 `masterName` ,构造 `JedisSentinelPool` 对象,然后便可以像使用普通 Redis 连接池一样来使用了:通过 `pool.getResource()` 获取连接,执行具体的命令。 - -在整个过程中,我们的代码不需要显式的指定主节点的地址,就可以连接到主节点;代码中对故障转移没有任何体现,就可以在哨兵完成故障转移后自动的切换主节点。之所以可以做到这一点,是因为在 `JedisSentinelPool` 的构造器中,进行了相关的工作;主要包括以下两点: - -1. **遍历哨兵节点,获取主节点信息:** 遍历哨兵节点,通过其中一个哨兵节点 + `masterName` 获得主节点的信息;该功能是通过调用哨兵节点的 `sentinel get-master-addr-by-name` 命令实现; -2. **增加对哨兵的监听:** 这样当发生故障转移时,客户端便可以收到哨兵的通知,从而完成主节点的切换。具体做法是:利用 Redis 提供的 **发布订阅** 功能,为每一个哨兵节点开启一个单独的线程,订阅哨兵节点的 + switch-master 频道,当收到消息时,重新初始化连接池。 - -## 新的主服务器是怎样被挑选出来的? - -**故障转移操作的第一步** 要做的就是在已下线主服务器属下的所有从服务器中,挑选出一个状态良好、数据完整的从服务器,然后向这个从服务器发送 `slaveof no one` 命令,将这个从服务器转换为主服务器。但是这个从服务器是怎么样被挑选出来的呢? - -![](https://upload-images.jianshu.io/upload_images/7896890-02dfea57f44fc27e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -简单来说 Sentinel 使用以下规则来选择新的主服务器: - -1. 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被 **淘汰**。 -2. 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被 **淘汰**。 -3. 在 **经历了以上两轮淘汰之后** 剩下来的从服务器中, 我们选出 **复制偏移量(replication offset)最大** 的那个 **从服务器** 作为新的主服务器;如果复制偏移量不可用,或者从服务器的复制偏移量相同,那么 **带有最小运行 ID** 的那个从服务器成为新的主服务器。 - -# 四、Redis 集群 - -![](https://upload-images.jianshu.io/upload_images/7896890-516eb4a9465451a6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -*上图* 展示了 **Redis Cluster** 典型的架构图,集群中的每一个 Redis 节点都 **互相两两相连**,客户端任意 **直连** 到集群中的 **任意一台**,就可以对其他 Redis 节点进行 **读写** 的操作。 - -#### 基本原理 - -![](https://upload-images.jianshu.io/upload_images/7896890-f65c71ca6811c634.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -Redis 集群中内置了 `16384` 个哈希槽。当客户端连接到 Redis 集群之后,会同时得到一份关于这个 **集群的配置信息**,当客户端具体对某一个 `key` 值进行操作时,会计算出它的一个 Hash 值,然后把结果对 `16384` **求余数**,这样每个 `key` 都会对应一个编号在 `0-16383` 之间的哈希槽,Redis 会根据节点数量 **大致均等** 的将哈希槽映射到不同的节点。 - -再结合集群的配置信息就能够知道这个 `key` 值应该存储在哪一个具体的 Redis 节点中,如果不属于自己管,那么就会使用一个特殊的 `MOVED` 命令来进行一个跳转,告诉客户端去连接这个节点以获取数据: - -```bash -GET x --MOVED 3999 127.0.0.1:6381 -``` - -`MOVED` 指令第一个参数 `3999` 是 `key` 对应的槽位编号,后面是目标节点地址,`MOVED` 命令前面有一个减号,表示这是一个错误的消息。客户端在收到 `MOVED` 指令后,就立即纠正本地的 **槽位映射表**,那么下一次再访问 `key` 时就能够到正确的地方去获取了。 - -#### 集群的主要作用 - -1. **数据分区:** 数据分区 *(或称数据分片)* 是集群最核心的功能。集群将数据分散到多个节点,**一方面** 突破了 Redis 单机内存大小的限制,**存储容量大大增加**;**另一方面** 每个主节点都可以对外提供读服务和写服务,**大大提高了集群的响应能力**。Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及,例如,如果单机内存太大,`bgsave` 和 `bgrewriteaof` 的 `fork` 操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出…… -2. **高可用:** 集群支持主从复制和主节点的 **自动故障转移** *(与哨兵类似)*,当任一节点发生故障时,集群仍然可以对外提供服务。 - -## 快速体验 - -#### 第一步:创建集群节点配置文件 - -首先我们找一个地方创建一个名为 `redis-cluster` 的目录: - -```bash -mkdir -p ~/Desktop/redis-cluster -``` - -然后按照上面的方法,创建六个配置文件,分别命名为:`redis_7000.conf`/`redis_7001.conf`.....`redis_7005.conf`,然后根据不同的端口号修改对应的端口值就好了: - -```bash -# 后台执行 -daemonize yes -# 端口号 -port 7000 -# 为每一个集群节点指定一个 pid_file -pidfile ~/Desktop/redis-cluster/redis_7000.pid -# 启动集群模式 -cluster-enabled yes -# 每一个集群节点都有一个配置文件,这个文件是不能手动编辑的。确保每一个集群节点的配置文件不通 -cluster-config-file nodes-7000.conf -# 集群节点的超时时间,单位:ms,超时后集群会认为该节点失败 -cluster-node-timeout 5000 -# 最后将 appendonly 改成 yes(AOF 持久化) -appendonly yes -``` - -记得把对应上述配置文件中根端口对应的配置都修改掉 *(port/ pidfile/ cluster-config-file)*。 - -#### 第二步:分别启动 6 个 Redis 实例 - -```bash -redis-server ~/Desktop/redis-cluster/redis_7000.conf -redis-server ~/Desktop/redis-cluster/redis_7001.conf -redis-server ~/Desktop/redis-cluster/redis_7002.conf -redis-server ~/Desktop/redis-cluster/redis_7003.conf -redis-server ~/Desktop/redis-cluster/redis_7004.conf -redis-server ~/Desktop/redis-cluster/redis_7005.conf -``` - -然后执行 `ps -ef | grep redis` 查看是否启动成功: - -![](https://upload-images.jianshu.io/upload_images/7896890-452c3152054c36f1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -可以看到 `6` 个 Redis 节点都以集群的方式成功启动了,**但是现在每个节点还处于独立的状态**,也就是说它们每一个都各自成了一个集群,还没有互相联系起来,我们需要手动地把他们之间建立起联系。 - -#### 第三步:建立集群 - -执行下列命令: - -```bash -redis-cli --cluster create --cluster-replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 -``` - -- 这里稍微解释一下这个 `--replicas 1` 的意思是:我们希望为集群中的每个主节点创建一个从节点。 - -观察控制台输出: - -![](https://upload-images.jianshu.io/upload_images/7896890-d5ab644e76e9cc87.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -看到 `[OK]` 的信息之后,就表示集群已经搭建成功了,可以看到,这里我们正确地创建了三主三从的集群。 - -#### 第四步:验证集群 - -我们先使用 `redic-cli` 任意连接一个节点: - -```bash -redis-cli -c -h 127.0.0.1 -p 7000 -127.0.0.1:7000> -``` - -- `-c`表示集群模式;`-h` 指定 ip 地址;`-p` 指定端口。 - -然后随便 `set` 一些值观察控制台输入: - -```bash -127.0.0.1:7000> SET name wmyskxz --> Redirected to slot [5798] located at 127.0.0.1:7001 -OK -127.0.0.1:7001> -``` - -可以看到这里 Redis 自动帮我们进行了 `Redirected` 操作跳转到了 `7001` 这个实例上。 - -我们再使用 `cluster info` *(查看集群信息)* 和 `cluster nodes` *(查看节点列表)* 来分别看看:*(任意节点输入均可)* - -```bash -127.0.0.1:7001> CLUSTER INFO -cluster_state:ok -cluster_slots_assigned:16384 -cluster_slots_ok:16384 -cluster_slots_pfail:0 -cluster_slots_fail:0 -cluster_known_nodes:6 -cluster_size:3 -cluster_current_epoch:6 -cluster_my_epoch:2 -cluster_stats_messages_ping_sent:1365 -cluster_stats_messages_pong_sent:1358 -cluster_stats_messages_meet_sent:4 -cluster_stats_messages_sent:2727 -cluster_stats_messages_ping_received:1357 -cluster_stats_messages_pong_received:1369 -cluster_stats_messages_meet_received:1 -cluster_stats_messages_received:2727 - -127.0.0.1:7001> CLUSTER NODES -56a04742f36c6e84968cae871cd438935081e86f 127.0.0.1:7003@17003 slave 4ec8c022e9d546c9b51deb9d85f6cf867bf73db6 0 1584428884000 4 connected -4ec8c022e9d546c9b51deb9d85f6cf867bf73db6 127.0.0.1:7000@17000 master - 0 1584428884000 1 connected 0-5460 -e2539c4398b8258d3f9ffa714bd778da107cb2cd 127.0.0.1:7005@17005 slave a3406db9ae7144d17eb7df5bffe8b70bb5dd06b8 0 1584428885222 6 connected -d31cd1f423ab1e1849cac01ae927e4b6950f55d9 127.0.0.1:7004@17004 slave 236cefaa9cdc295bc60a5bd1aed6a7152d4f384d 0 1584428884209 5 connected -236cefaa9cdc295bc60a5bd1aed6a7152d4f384d 127.0.0.1:7001@17001 myself,master - 0 1584428882000 2 connected 5461-10922 -a3406db9ae7144d17eb7df5bffe8b70bb5dd06b8 127.0.0.1:7002@17002 master - 0 1584428884000 3 connected 10923-16383 -127.0.0.1:7001> -``` - -## 数据分区方案简析 - -#### 方案一:哈希值 % 节点数 - -哈希取余分区思路非常简单:计算 `key` 的 hash 值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。 - -不过该方案最大的问题是,**当新增或删减节点时**,节点数量发生变化,系统中所有的数据都需要 **重新计算映射关系**,引发大规模数据迁移。 - -#### 方案二:一致性哈希分区 - -一致性哈希算法将 **整个哈希值空间** 组织成一个虚拟的圆环,范围是 *[0 , 232-1]*,对于每一个数据,根据 `key` 计算 hash 值,确数据在环上的位置,然后从此位置沿顺时针行走,找到的第一台服务器就是其应该映射到的服务器: - -![](https://upload-images.jianshu.io/upload_images/7896890-40e8a2c096c8da92.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -与哈希取余分区相比,一致性哈希分区将 **增减节点的影响限制在相邻节点**。以上图为例,如果在 `node1` 和 `node2` 之间增加 `node5`,则只有 `node2` 中的一部分数据会迁移到 `node5`;如果去掉 `node2`,则原 `node2` 中的数据只会迁移到 `node4` 中,只有 `node4` 会受影响。 - -一致性哈希分区的主要问题在于,当 **节点数量较少** 时,增加或删减节点,**对单个节点的影响可能很大**,造成数据的严重不平衡。还是以上图为例,如果去掉 `node2`,`node4` 中的数据由总数据的 `1/4` 左右变为 `1/2` 左右,与其他节点相比负载过高。 - -#### 方案三:带有虚拟节点的一致性哈希分区 - -该方案在 **一致性哈希分区的基础上**,引入了 **虚拟节点** 的概念。Redis 集群使用的便是该方案,其中的虚拟节点称为 **槽(slot)**。槽是介于数据和实际节点之间的虚拟概念,每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。 - -在使用了槽的一致性哈希分区中,**槽是数据管理和迁移的基本单位**。槽 **解耦** 了 **数据和实际节点** 之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有 `4` 个实际节点,假设为其分配 `16` 个槽(0-15); - -- 槽 0-3 位于 node1;4-7 位于 node2;以此类推.... - -如果此时删除 `node2`,只需要将槽 4-7 重新分配即可,例如槽 4-5 分配给 `node1`,槽 6 分配给 `node3`,槽 7 分配给 `node4`;可以看出删除 `node2` 后,数据在其他节点的分布仍然较为均衡。 - -## 节点通信机制简析 - -集群的建立离不开节点之间的通信,例如我们上访在 *快速体验* 中刚启动六个集群节点之后通过 `redis-cli` 命令帮助我们搭建起来了集群,实际上背后每个集群之间的两两连接是通过了 `CLUSTER MEET ` 命令发送 `MEET` 消息完成的,下面我们展开详细说说。 - -#### 两个端口 - -在 **哨兵系统** 中,节点分为 **数据节点** 和 **哨兵节点**:前者存储数据,后者实现额外的控制功能。在 **集群** 中,没有数据节点与非数据节点之分:**所有的节点都存储数据,也都参与集群状态的维护**。为此,集群中的每个节点,都提供了两个 TCP 端口: - -- **普通端口:** 即我们在前面指定的端口 *(7000等)*。普通端口主要用于为客户端提供服务 *(与单机节点类似)*;但在节点间数据迁移时也会使用。 -- **集群端口:** 端口号是普通端口 + 10000 *(10000是固定值,无法改变)*,如 `7000` 节点的集群端口为 `17000`。**集群端口只用于节点之间的通信**,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。 - -#### Gossip 协议 - -节点间通信,按照通信协议可以分为几种类型:单对单、广播、Gossip 协议等。重点是广播和 Gossip 的对比。 - -- 广播是指向集群内所有节点发送消息。**优点** 是集群的收敛速度快(集群收敛是指集群内所有节点获得的集群信息是一致的),**缺点** 是每条消息都要发送给所有节点,CPU、带宽等消耗较大。 -- Gossip 协议的特点是:在节点数量有限的网络中,**每个节点都 “随机” 的与部分节点通信** *(并不是真正的随机,而是根据特定的规则选择通信的节点)*,经过一番杂乱无章的通信,每个节点的状态很快会达到一致。Gossip 协议的 **优点** 有负载 *(比广播)* 低、去中心化、容错性高 *(因为通信有冗余)* 等;**缺点** 主要是集群的收敛速度慢。 - -#### 消息类型 - -集群中的节点采用 **固定频率(每秒10次)** 的 **定时任务** 进行通信相关的工作:判断是否需要发送消息及消息类型、确定接收节点、发送消息等。如果集群状态发生了变化,如增减节点、槽状态变更,通过节点间的通信,所有节点会很快得知整个集群的状态,使集群收敛。 - -节点间发送的消息主要分为 `5` 种:`meet 消息`、`ping 消息`、`pong 消息`、`fail 消息`、`publish 消息`。不同的消息类型,通信协议、发送的频率和时机、接收节点的选择等是不同的: - -- **MEET 消息:** 在节点握手阶段,当节点收到客户端的 `CLUSTER MEET` 命令时,会向新加入的节点发送 `MEET` 消息,请求新节点加入到当前集群;新节点收到 MEET 消息后会回复一个 `PONG` 消息。 -- **PING 消息:** 集群里每个节点每秒钟会选择部分节点发送 `PING` 消息,接收者收到消息后会回复一个 `PONG` 消息。**PING 消息的内容是自身节点和部分其他节点的状态信息**,作用是彼此交换信息,以及检测节点是否在线。`PING` 消息使用 Gossip 协议发送,接收节点的选择兼顾了收敛速度和带宽成本,**具体规则如下**:(1)随机找 5 个节点,在其中选择最久没有通信的 1 个节点;(2)扫描节点列表,选择最近一次收到 `PONG` 消息时间大于 `cluster_node_timeout / 2` 的所有节点,防止这些节点长时间未更新。 -- **PONG消息:** `PONG` 消息封装了自身状态数据。可以分为两种:**第一种** 是在接到 `MEET/PING` 消息后回复的 `PONG` 消息;**第二种** 是指节点向集群广播 `PONG` 消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播 `PONG` 消息。 -- **FAIL 消息:** 当一个主节点判断另一个主节点进入 `FAIL` 状态时,会向集群广播这一 `FAIL` 消息;接收节点会将这一 `FAIL` 消息保存起来,便于后续的判断。 -- **PUBLISH 消息:** 节点收到 `PUBLISH` 命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该 `PUBLISH` 命令。 - -## 数据结构简析 - -节点需要专门的数据结构来存储集群的状态。所谓集群的状态,是一个比较大的概念,包括:集群是否处于上线状态、集群中有哪些节点、节点是否可达、节点的主从状态、槽的分布…… - -节点为了存储集群状态而提供的数据结构中,最关键的是 `clusterNode` 和 `clusterState` 结构:前者记录了一个节点的状态,后者记录了集群作为一个整体的状态。 - -#### clusterNode 结构 - -`clusterNode` 结构保存了 **一个节点的当前状态**,包括创建时间、节点 id、ip 和端口号等。每个节点都会用一个 `clusterNode` 结构记录自己的状态,并为集群内所有其他节点都创建一个 `clusterNode` 结构来记录节点状态。 - -下面列举了 `clusterNode` 的部分字段,并说明了字段的含义和作用: - -```c -typedef struct clusterNode { - //节点创建时间 - mstime_t ctime; - //节点id - char name[REDIS_CLUSTER_NAMELEN]; - //节点的ip和端口号 - char ip[REDIS_IP_STR_LEN]; - int port; - //节点标识:整型,每个bit都代表了不同状态,如节点的主从状态、是否在线、是否在握手等 - int flags; - //配置纪元:故障转移时起作用,类似于哨兵的配置纪元 - uint64_t configEpoch; - //槽在该节点中的分布:占用16384/8个字节,16384个比特;每个比特对应一个槽:比特值为1,则该比特对应的槽在节点中;比特值为0,则该比特对应的槽不在节点中 - unsigned char slots[16384/8]; - //节点中槽的数量 - int numslots; - ………… -} clusterNode; -``` - -除了上述字段,`clusterNode` 还包含节点连接、主从复制、故障发现和转移需要的信息等。 - -#### clusterState 结构 - -`clusterState` 结构保存了在当前节点视角下,集群所处的状态。主要字段包括: - -```c -typedef struct clusterState { - //自身节点 - clusterNode *myself; - //配置纪元 - uint64_t currentEpoch; - //集群状态:在线还是下线 - int state; - //集群中至少包含一个槽的节点数量 - int size; - //哈希表,节点名称->clusterNode节点指针 - dict *nodes; - //槽分布信息:数组的每个元素都是一个指向clusterNode结构的指针;如果槽还没有分配给任何节点,则为NULL - clusterNode *slots[16384]; - ………… -} clusterState; -``` - -除此之外,`clusterState` 还包括故障转移、槽迁移等需要的信息。 - -> 更多关于集群内容请自行阅读《Redis 设计与实现》,其中有更多细节方面的介绍 - [http://redisbook.com/](http://redisbook.com/) - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) -4. Reids(4)——神奇的HyperLoglog解决统计问题 - [https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/) -5. Redis(5)——亿级数据过滤和布隆过滤器 - [https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/](https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/) -6. Redis(6)——GeoHash查找附近的人[https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/](https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/) -7. Redis(7)——持久化【一文了解】 - [https://www.wmyskxz.com/2020/03/13/redis-7-chi-jiu-hua-yi-wen-liao-jie/](https://www.wmyskxz.com/2020/03/13/redis-7-chi-jiu-hua-yi-wen-liao-jie/) -8. Redis(8)——发布/订阅与Stream - [https://www.wmyskxz.com/2020/03/15/redis-8-fa-bu-ding-yue-yu-stream/](https://www.wmyskxz.com/2020/03/15/redis-8-fa-bu-ding-yue-yu-stream/) - -# 参考资料 - -1. 《Redis 设计与实现》 | 黄健宏 著 - [http://redisbook.com/](http://redisbook.com/) -2. 《Redis 深度历险》 | 钱文品 著 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -3. 深入学习Redis(3):主从复制 - [https://www.cnblogs.com/kismetv/p/9236731.html](https://www.cnblogs.com/kismetv/p/9236731.html) -4. Redis 主从复制 原理与用法 - [https://blog.csdn.net/Stubborn_Cow/article/details/50442950](https://blog.csdn.net/Stubborn_Cow/article/details/50442950) -5. 深入学习Redis(4):哨兵 - [https://www.cnblogs.com/kismetv/p/9609938.html](https://www.cnblogs.com/kismetv/p/9609938.html) -6. Redis 5 之后版本的高可用集群搭建 - [https://www.jianshu.com/p/8045b92fafb2](https://www.jianshu.com/p/8045b92fafb2) - -> - 本文已收录至我的 Github 程序员成长系列 **【More Than Java】,学习,不止 Code,欢迎 star:[https://github.com/wmyskxz/MoreThanJava](https://github.com/wmyskxz/MoreThanJava)** -> - **个人公众号** :wmyskxz,**个人独立域名博客**:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长! - -![](https://upload-images.jianshu.io/upload_images/7896890-fca34cfd601e7449.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -非常感谢各位人才能 **看到这里**,如果觉得本篇文章写得不错,觉得 **「我没有三颗心脏」有点东西** 的话,**求点赞,求关注,求分享,求留言!** - -创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见! \ No newline at end of file diff --git "a/docs/database/Redis/redis-collection/Reids(4)\342\200\224\342\200\224\347\245\236\345\245\207\347\232\204HyperLoglog\350\247\243\345\206\263\347\273\237\350\256\241\351\227\256\351\242\230.md" "b/docs/database/Redis/redis-collection/Reids(4)\342\200\224\342\200\224\347\245\236\345\245\207\347\232\204HyperLoglog\350\247\243\345\206\263\347\273\237\350\256\241\351\227\256\351\242\230.md" deleted file mode 100644 index 98847b842ab..00000000000 --- "a/docs/database/Redis/redis-collection/Reids(4)\342\200\224\342\200\224\347\245\236\345\245\207\347\232\204HyperLoglog\350\247\243\345\206\263\347\273\237\350\256\241\351\227\256\351\242\230.md" +++ /dev/null @@ -1,469 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-a408d790b0b4f4b9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、HyperLogLog 简介 - -**HyperLogLog** 是最早由 [Flajolet](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf) 及其同事在 2007 年提出的一种 **估算基数的近似最优算法**。但跟原版论文不同的是,好像很多书包括 Redis 作者都把它称为一种 **新的数据结构(new datastruct)** *(算法实现确实需要一种特定的数据结构来实现)*。 - -![](http://wx2.sinaimg.cn/large/006oOWahly1fpsc3t7fnng30ab05tkjl.gif) - -## 关于基数统计 - -**基数统计(Cardinality Counting)** 通常是用来统计一个集合中不重复的元素个数。 - -**思考这样的一个场景:** 如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站上每个网页的 **UV(独立访客,每个用户每天只记录一次)**,然后让你来开发这个统计模块,你会如何实现? - -![](https://upload-images.jianshu.io/upload_images/7896890-a9dbcf6374d482ba.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -如果统计 **PV(浏览量,用户没点一次记录一次)**,那非常好办,给每个页面配置一个独立的 Redis 计数器就可以了,把这个计数器的 key 后缀加上当天的日期。这样每来一个请求,就执行 `INCRBY` 指令一次,最终就可以统计出所有的 **PV** 数据了。 - -但是 **UV** 不同,它要去重,**同一个用户一天之内的多次访问请求只能计数一次**。这就要求了每一个网页请求都需要带上用户的 ID,无论是登录用户还是未登录的用户,都需要一个唯一 ID 来标识。 - -你也许马上就想到了一个 *简单的解决方案*:那就是 **为每一个页面设置一个独立的 set 集合** 来存储所有当天访问过此页面的用户 ID。但这样的 **问题** 就是: - -1. **存储空间巨大:** 如果网站访问量一大,你需要用来存储的 set 集合就会非常大,如果页面再一多.. 为了一个去重功能耗费的资源就可以直接让你 **老板打死你**; -2. **统计复杂:** 这么多 set 集合如果要聚合统计一下,又是一个复杂的事情; - -![](https://upload-images.jianshu.io/upload_images/7896890-b8ddfcd39cb46cb5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -## 基数统计的常用方法 - -对于上述这样需要 **基数统计** 的事情,通常来说有两种比 set 集合更好的解决方案: - -### 第一种:B 树 - -**B 树最大的优势就是插入和查找效率很高**,如果用 B 树存储要统计的数据,可以快速判断新来的数据是否存在,并快速将元素插入 B 树。要计算基础值,只需要计算 B 树的节点个数就行了。 - -不过将 B 树结构维护到内存中,能够解决统计和计算的问题,但是 **并没有节省内存**。 - -### 第二种:bitmap - -**bitmap** 可以理解为通过一个 bit 数组来存储特定数据的一种数据结构,**每一个 bit 位都能独立包含信息**,bit 是数据的最小存储单位,因此能大量节省空间,也可以将整个 bit 数据一次性 load 到内存计算。如果定义一个很大的 bit 数组,基础统计中 **每一个元素对应到 bit 数组中的一位**,例如: - -![](https://upload-images.jianshu.io/upload_images/7896890-fb4283ad7dbd89a2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -bitmap 还有一个明显的优势是 **可以轻松合并多个统计结果**,只需要对多个结果求异或就可以了,也可以大大减少存储内存。可以简单做一个计算,如果要统计 **1 亿** 个数据的基数值,**大约需要的内存**:`100_000_000/ 8/ 1024/ 1024 ≈ 12 M`,如果用 **32 bit** 的 int 代表 **每一个** 统计的数据,**大约需要内存**:`32 * 100_000_000/ 8/ 1024/ 1024 ≈ 381 M` - -可以看到 bitmap 对于内存的节省显而易见,但仍然不够。统计一个对象的基数值就需要 `12 M`,如果统计 1 万个对象,就需要接近 `120 G`,对于大数据的场景仍然不适用。 - -![](https://upload-images.jianshu.io/upload_images/7896890-1ebb3265b4297fa1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 概率算法 - -实际上目前还没有发现更好的在 **大数据场景** 中 **准确计算** 基数的高效算法,因此在不追求绝对精确的情况下,使用概率算法算是一个不错的解决方案。 - -概率算法 **不直接存储** 数据集合本身,通过一定的 **概率统计方法预估基数值**,这种方法可以大大节省内存,同时保证误差控制在一定范围内。目前用于基数计数的概率算法包括: - -- **Linear Counting(LC)**:早期的基数估计算法,LC 在空间复杂度方面并不算优秀,实际上 LC 的空间复杂度与上文中简单 bitmap 方法是一样的(但是有个常数项级别的降低),都是 O(Nmax) -- **LogLog Counting(LLC)**:LogLog Counting 相比于 LC 更加节省内存,空间复杂度只有 O(log2(log2(Nmax))) -- **HyperLogLog Counting(HLL)**:HyperLogLog Counting 是基于 LLC 的优化和改进,在同样空间复杂度情况下,能够比 LLC 的基数估计误差更小 - -其中,**HyperLogLog** 的表现是惊人的,上面我们简单计算过用 **bitmap** 存储 **1 个亿** 统计数据大概需要 `12 M` 内存,而在 **HyperLoglog** 中,只需要不到 **1 K** 内存就能够做到!在 Redis 中实现的 **HyperLoglog** 也只需要 **12 K** 内存,在 **标准误差 0.81%** 的前提下,**能够统计 264 个数据**! - -![](https://upload-images.jianshu.io/upload_images/7896890-439fe643e2dc081a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**这是怎么做到的?!** 下面赶紧来了解一下! - -# 二、HyperLogLog 原理 - -我们来思考一个抛硬币的游戏:你连续掷 n 次硬币,然后说出其中**连续掷为正面的最大次数,我来猜你一共抛了多少次**。 - -这很容易理解吧,例如:你说你这一次 *最多连续出现了 2 次* 正面,那么我就可以知道你这一次投掷的次数并不多,所以 *我可能会猜是 5* 或者是其他小一些的数字,但如果你说你这一次 *最多连续出现了 20 次* 正面,虽然我觉得不可能,但我仍然知道你花了特别多的时间,所以 *我说 GUN...*。 - -![](https://upload-images.jianshu.io/upload_images/7896890-2042926c4383c027.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -这期间我可能会要求你重复实验,然后我得到了更多的数据之后就会估计得更准。**我们来把刚才的游戏换一种说法**: - -![](https://upload-images.jianshu.io/upload_images/7896890-24e8f48f5e3eb81f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这张图的意思是,我们给定一系列的随机整数,**记录下低位连续零位的最大长度 K**,即为图中的 `maxbit`,**通过这个 K 值我们就可以估算出随机数的数量 N**。 - -## 代码实验 - -我们可以简单编写代码做一个实验,来探究一下 `K` 和 `N` 之间的关系: - -```java -public class PfTest { - - static class BitKeeper { - - private int maxbit; - - public void random() { - long value = ThreadLocalRandom.current().nextLong(2L << 32); - int bit = lowZeros(value); - if (bit > this.maxbit) { - this.maxbit = bit; - } - } - - private int lowZeros(long value) { - int i = 0; - for (; i < 32; i++) { - if (value >> i << i != value) { - break; - } - } - return i - 1; - } - } - - static class Experiment { - - private int n; - private BitKeeper keeper; - - public Experiment(int n) { - this.n = n; - this.keeper = new BitKeeper(); - } - - public void work() { - for (int i = 0; i < n; i++) { - this.keeper.random(); - } - } - - public void debug() { - System.out - .printf("%d %.2f %d\n", this.n, Math.log(this.n) / Math.log(2), this.keeper.maxbit); - } - } - - public static void main(String[] args) { - for (int i = 1000; i < 100000; i += 100) { - Experiment exp = new Experiment(i); - exp.work(); - exp.debug(); - } - } -} -``` - -跟上图中的过程是一致的,话说为啥叫 `PfTest` 呢,包括 Redis 中的命令也一样带有一个 `PF` 前缀,还记得嘛,因为 **HyperLogLog** 的提出者上文提到过的,叫 `Philippe Flajolet`。 - -截取部分输出查看: - -```java -//n n/log2 maxbit -34000 15.05 13 -35000 15.10 13 -36000 15.14 16 -37000 15.18 17 -38000 15.21 14 -39000 15.25 16 -40000 15.29 14 -41000 15.32 16 -42000 15.36 18 -``` - -会发现 `K` 和 `N` 的对数之间存在显著的线性相关性:**N 约等于 2k** - -## 更近一步:分桶平均 - -**如果 `N` 介于 2k 和 2k+1 之间,用这种方式估计的值都等于 2k,这明显是不合理的**,所以我们可以使用多个 `BitKeeper` 进行加权估计,就可以得到一个比较准确的值了: - -```java -public class PfTest { - - static class BitKeeper { - // 无变化, 代码省略 - } - - static class Experiment { - - private int n; - private int k; - private BitKeeper[] keepers; - - public Experiment(int n) { - this(n, 1024); - } - - public Experiment(int n, int k) { - this.n = n; - this.k = k; - this.keepers = new BitKeeper[k]; - for (int i = 0; i < k; i++) { - this.keepers[i] = new BitKeeper(); - } - } - - public void work() { - for (int i = 0; i < this.n; i++) { - long m = ThreadLocalRandom.current().nextLong(1L << 32); - BitKeeper keeper = keepers[(int) (((m & 0xfff0000) >> 16) % keepers.length)]; - keeper.random(); - } - } - - public double estimate() { - double sumbitsInverse = 0.0; - for (BitKeeper keeper : keepers) { - sumbitsInverse += 1.0 / (float) keeper.maxbit; - } - double avgBits = (float) keepers.length / sumbitsInverse; - return Math.pow(2, avgBits) * this.k; - } - } - - public static void main(String[] args) { - for (int i = 100000; i < 1000000; i += 100000) { - Experiment exp = new Experiment(i); - exp.work(); - double est = exp.estimate(); - System.out.printf("%d %.2f %.2f\n", i, est, Math.abs(est - i) / i); - } - } -} -``` - -这个过程有点 **类似于选秀节目里面的打分**,一堆专业评委打分,但是有一些评委因为自己特别喜欢所以给高了,一些评委又打低了,所以一般都要 **屏蔽最高分和最低分**,然后 **再计算平均值**,这样的出来的分数就差不多是公平公正的了。 - -![](https://upload-images.jianshu.io/upload_images/7896890-6c927d25750f20d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -上述代码就有 **1024** 个 "评委",并且在计算平均值的时候,采用了 **调和平均数**,也就是倒数的平均值,它能有效地平滑离群值的影响: - -```java -avg = (3 + 4 + 5 + 104) / 4 = 29 -avg = 4 / (1/3 + 1/4 + 1/5 + 1/104) = 5.044 -``` - -观察脚本的输出,误差率百分比控制在个位数: - -```java -100000 94274.94 0.06 -200000 194092.62 0.03 -300000 277329.92 0.08 -400000 373281.66 0.07 -500000 501551.60 0.00 -600000 596078.40 0.01 -700000 687265.72 0.02 -800000 828778.96 0.04 -900000 944683.53 0.05 -``` - -真实的 HyperLogLog 要比上面的示例代码更加复杂一些,也更加精确一些。上面这个算法在随机次数很少的情况下会出现除零错误,因为 `maxbit = 0` 是不可以求倒数的。 - -## 真实的 HyperLogLog - -有一个神奇的网站,可以动态地让你观察到 HyperLogLog 的算法到底是怎么执行的:[http://content.research.neustar.biz/blog/hll.html](http://content.research.neustar.biz/blog/hll.html) - -![](https://upload-images.jianshu.io/upload_images/7896890-72f00a9983a1395e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -其中的一些概念这里稍微解释一下,您就可以自行去点击 `step` 来观察了: - -- **m 表示分桶个数:** 从图中可以看到,这里分成了 64 个桶; -- **蓝色的 bit 表示在桶中的位置:** 例如图中的 `101110` 实则表示二进制的 `46`,所以该元素被统计在中间大表格 `Register Values` 中标红的第 46 个桶之中; -- **绿色的 bit 表示第一个 1 出现的位置**: 从图中可以看到标绿的 bit 中,从右往左数,第一位就是 1,所以在 `Register Values` 第 46 个桶中写入 1; -- **红色 bit 表示绿色 bit 的值的累加:** 下一个出现在第 46 个桶的元素值会被累加; - - -### 为什么要统计 Hash 值中第一个 1 出现的位置? - -因为第一个 1 出现的位置可以同我们抛硬币的游戏中第一次抛到正面的抛掷次数对应起来,根据上面掷硬币实验的结论,记录每个数据的第一个出现的位置 `K`,就可以通过其中最大值 Kmax 来推导出数据集合中的基数:**N = 2Kmax** - -### PF 的内存占用为什么是 12 KB? - -我们上面的算法中使用了 **1024** 个桶,网站演示也只有 **64** 个桶,不过在 Redis 的 HyperLogLog 实现中,用的是 **16384** 个桶,即:214,也就是说,就像上面网站中间那个 `Register Values` 大表格有 **16384** 格。 - -**而Redis 最大能够统计的数据量是 264**,即每个桶的 `maxbit` 需要 **6** 个 bit 来存储,最大可以表示 `maxbit = 63`,于是总共占用内存就是:**(214) x 6 / 8** *(每个桶 6 bit,而这么多桶本身要占用 16384 bit,再除以 8 转换成 KB)*,算出来的结果就是 `12 KB`。 - -# 三、Redis 中的 HyperLogLog 实现 - -从上面我们算是对 **HyperLogLog** 的算法和思想有了一定的了解,并且知道了一个 **HyperLogLog** 实际占用的空间大约是 `12 KB`,但 Redis 对于内存的优化非常变态,当 **计数比较小** 的时候,大多数桶的计数值都是 **零**,这个时候 Redis 就会适当节约空间,转换成另外一种 **稀疏存储方式**,与之相对的,正常的存储模式叫做 **密集存储**,这种方式会恒定地占用 `12 KB`。 - -## 密集型存储结构 - -密集型的存储结构非常简单,就是 **16384 个 6 bit 连续串成** 的字符串位图: - -![](https://upload-images.jianshu.io/upload_images/7896890-0ba2adb0214afd0c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -我们都知道,一个字节是由 8 个 bit 组成的,这样 6 bit 排列的结构就会导致,有一些桶会 **跨越字节边界**,我们需要 **对这一个或者两个字节进行适当的移位拼接** 才可以得到具体的计数值。 - -假设桶的编号为 `index`,这个 6 bity 计数值的起始字节偏移用 `offset_bytes` 表示,它在这个字节的其实比特位置偏移用 `offset_bits` 表示,于是我们有: - -```python -offset_bytes = (index * 6) / 8 -offset_bits = (index * 6) % 8 -``` - -前者是商,后者是余数。比如 `bucket 2` 的字节偏移是 1,也就是第 2 个字节。它的位偏移是 4,也就是第 2 个字节的第 5 个位开始是 bucket 2 的计数值。需要注意的是 **字节位序是左边低位右边高位**,而通常我们使用的字节都是左边高位右边低位。 - -这里就涉及到两种情况,**如果 `offset_bits` 小于等于 2**,说明这 **6 bit 在一个字节的内部**,可以直接使用下面的表达式得到计数值 `val`: - -```python -val = buffer[offset_bytes] >> offset_bits # 向右移位 -``` - -**如果 `offset_bits` 大于 2**,那么就会涉及到 **跨越字节边界**,我们需要拼接两个字节的位片段: - -```python -# 低位值 -low_val = buffer[offset_bytes] >> offset_bits -# 低位个数 -low_bits = 8 - offset_bits -# 拼接,保留低6位 -val = (high_val << low_bits | low_val) & 0b111111 -``` - -不过下面 Redis 的源码要晦涩一点,看形式它似乎只考虑了跨越字节边界的情况。这是因为如果 6 bit 在单个字节内,上面代码中的 `high_val` 的值是零,所以这一份代码可以同时照顾单字节和双字节: - -```c -// 获取指定桶的计数值 -#define HLL_DENSE_GET_REGISTER(target,p,regnum) do { \ - uint8_t *_p = (uint8_t*) p; \ - unsigned long _byte = regnum*HLL_BITS/8; \ - unsigned long _fb = regnum*HLL_BITS&7; \ # %8 = &7 - unsigned long _fb8 = 8 - _fb; \ - unsigned long b0 = _p[_byte]; \ - unsigned long b1 = _p[_byte+1]; \ - target = ((b0 >> _fb) | (b1 << _fb8)) & HLL_REGISTER_MAX; \ -} while(0) - -// 设置指定桶的计数值 -#define HLL_DENSE_SET_REGISTER(p,regnum,val) do { \ - uint8_t *_p = (uint8_t*) p; \ - unsigned long _byte = regnum*HLL_BITS/8; \ - unsigned long _fb = regnum*HLL_BITS&7; \ - unsigned long _fb8 = 8 - _fb; \ - unsigned long _v = val; \ - _p[_byte] &= ~(HLL_REGISTER_MAX << _fb); \ - _p[_byte] |= _v << _fb; \ - _p[_byte+1] &= ~(HLL_REGISTER_MAX >> _fb8); \ - _p[_byte+1] |= _v >> _fb8; \ -} while(0) -``` - -## 稀疏存储结构 - -稀疏存储适用于很多计数值都是零的情况。下图表示了一般稀疏存储计数值的状态: - -![](https://upload-images.jianshu.io/upload_images/7896890-9d5a9018d2eedbd8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -当 **多个连续桶的计数值都是零** 时,Redis 提供了几种不同的表达形式: - -- `00xxxxxx`:前缀两个零表示接下来的 6bit 整数值加 1 就是零值计数器的数量,注意这里要加 1 是因为数量如果为零是没有意义的。比如 `00010101` 表示连续 `22` 个零值计数器。 -- `01xxxxxx yyyyyyyy`:6bit 最多只能表示连续 `64` 个零值计数器,这样扩展出的 14bit 可以表示最多连续 `16384` 个零值计数器。这意味着 HyperLogLog 数据结构中 `16384` 个桶的初始状态,所有的计数器都是零值,可以直接使用 2 个字节来表示。 -- `1vvvvvxx`:中间 5bit 表示计数值,尾部 2bit 表示连续几个桶。它的意思是连续 `(xx +1)` 个计数值都是 `(vvvvv + 1)`。比如 `10101011` 表示连续 `4` 个计数值都是 `11`。 - -注意 *上面第三种方式* 的计数值最大只能表示到 `32`,而 HyperLogLog 的密集存储单个计数值用 6bit 表示,最大可以表示到 `63`。**当稀疏存储的某个计数值需要调整到大于 `32` 时,Redis 就会立即转换 HyperLogLog 的存储结构,将稀疏存储转换成密集存储。** - -## 对象头 - -HyperLogLog 除了需要存储 16384 个桶的计数值之外,它还有一些附加的字段需要存储,比如总计数缓存、存储类型。所以它使用了一个额外的对象头来表示: - -```c -struct hllhdr { - char magic[4]; /* 魔术字符串"HYLL" */ - uint8_t encoding; /* 存储类型 HLL_DENSE or HLL_SPARSE. */ - uint8_t notused[3]; /* 保留三个字节未来可能会使用 */ - uint8_t card[8]; /* 总计数缓存 */ - uint8_t registers[]; /* 所有桶的计数器 */ -}; -``` - -所以 **HyperLogLog** 整体的内部结构就是 **HLL 对象头** 加上 **16384** 个桶的计数值位图。它在 Redis 的内部结构表现就是一个字符串位图。你可以把 **HyperLogLog 对象当成普通的字符串来进行处理:** - -```console -> PFADD codehole python java golang -(integer) 1 -> GET codehole -"HYLL\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80C\x03\x84MK\x80P\xb8\x80^\xf3" -``` - -但是 **不可以** 使用 **HyperLogLog** 指令来 **操纵普通的字符串**,**因为它需要检查对象头魔术字符串是否是 "HYLL"**。 - -# 四、HyperLogLog 的使用 - -**HyperLogLog** 提供了两个指令 `PFADD` 和 `PFCOUNT`,字面意思就是一个是增加,另一个是获取计数。`PFADD` 和 `set` 集合的 `SADD` 的用法是一样的,来一个用户 ID,就将用户 ID 塞进去就是,`PFCOUNT` 和 `SCARD` 的用法是一致的,直接获取计数值: - -```console -> PFADD codehole user1 -(interger) 1 -> PFCOUNT codehole -(integer) 1 -> PFADD codehole user2 -(integer) 1 -> PFCOUNT codehole -(integer) 2 -> PFADD codehole user3 -(integer) 1 -> PFCOUNT codehole -(integer) 3 -> PFADD codehole user4 user 5 -(integer) 1 -> PFCOUNT codehole -(integer) 5 -``` - -我们可以用 Java 编写一个脚本来试试 HyperLogLog 的准确性到底有多少: - -```java -public class JedisTest { - public static void main(String[] args) { - for (int i = 0; i < 100000; i++) { - jedis.pfadd("codehole", "user" + i); - } - long total = jedis.pfcount("codehole"); - System.out.printf("%d %d\n", 100000, total); - jedis.close(); - } -} -``` - -结果输出如下: - -```java -100000 99723 -``` - -发现 `10` 万条数据只差了 `277`,按照百分比误差率是 `0.277%`,对于巨量的 UV 需求来说,这个误差率真的不算高。 - -当然,除了上面的 `PFADD` 和 `PFCOUNT` 之外,还提供了第三个 `PFMEGER` 指令,用于将多个计数值累加在一起形成一个新的 `pf` 值: - -```console -> PFADD nosql "Redis" "MongoDB" "Memcached" -(integer) 1 - -> PFADD RDBMS "MySQL" "MSSQL" "PostgreSQL" -(integer) 1 - -> PFMERGE databases nosql RDBMS -OK - -> PFCOUNT databases -(integer) 6 -``` - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) - -# 扩展阅读 - -1. 【算法原文】HyperLogLog: the analysis of a near-optimal -cardinality estimation algorithm - [http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf) - -# 参考资料 - -1. 【Redis 作者博客】Redis new data structure: the HyperLogLog - [http://antirez.com/news/75](http://antirez.com/news/75) -2. 神奇的HyperLogLog算法 - [http://www.rainybowe.com/blog/2017/07/13/%E7%A5%9E%E5%A5%87%E7%9A%84HyperLogLog%E7%AE%97%E6%B3%95/index.html](http://www.rainybowe.com/blog/2017/07/13/%E7%A5%9E%E5%A5%87%E7%9A%84HyperLogLog%E7%AE%97%E6%B3%95/index.html) -3. 深度探索 Redis HyperLogLog 内部数据结构 - [https://zhuanlan.zhihu.com/p/43426875](https://zhuanlan.zhihu.com/p/43426875) -4. 《Redis 深度历险》 - 钱文品/ 著 - - -> - 本文已收录至我的 Github 程序员成长系列 **【More Than Java】,学习,不止 Code,欢迎 star:[https://github.com/wmyskxz/MoreThanJava](https://github.com/wmyskxz/MoreThanJava)** -> - **个人公众号** :wmyskxz,**个人独立域名博客**:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长! - -![](https://upload-images.jianshu.io/upload_images/7896890-fca34cfd601e7449.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -非常感谢各位人才能 **看到这里**,如果觉得本篇文章写得不错,觉得 **「我没有三颗心脏」有点东西** 的话,**求点赞,求关注,求分享,求留言!** - -创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见! \ No newline at end of file diff --git "a/docs/database/Redis/redis\351\233\206\347\276\244\344\273\245\345\217\212\345\272\224\347\224\250\345\234\272\346\231\257.md" "b/docs/database/Redis/redis\351\233\206\347\276\244\344\273\245\345\217\212\345\272\224\347\224\250\345\234\272\346\231\257.md" deleted file mode 100644 index dfa0d40e834..00000000000 --- "a/docs/database/Redis/redis\351\233\206\347\276\244\344\273\245\345\217\212\345\272\224\347\224\250\345\234\272\346\231\257.md" +++ /dev/null @@ -1,269 +0,0 @@ -相关阅读: - -- [史上最全Redis高可用技术解决方案大全](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484850&idx=1&sn=3238360bfa8105cf758dcf7354af2814&chksm=cea24a79f9d5c36fb2399aafa91d7fb2699b5006d8d037fe8aaf2e5577ff20ae322868b04a87&token=1082669959&lang=zh_CN&scene=21#wechat_redirect) -- [Raft协议实战之Redis Sentinel的选举Leader源码解析](http://weizijun.cn/2015/04/30/Raft%E5%8D%8F%E8%AE%AE%E5%AE%9E%E6%88%98%E4%B9%8BRedis%20Sentinel%E7%9A%84%E9%80%89%E4%B8%BELeader%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/) - -目录: - - - -- [Redis 集群以及应用](#redis-集群以及应用) - - [集群](#集群) - - [主从复制](#主从复制) - - [主从链(拓扑结构)](#主从链拓扑结构) - - [复制模式](#复制模式) - - [问题点](#问题点) - - [哨兵机制](#哨兵机制) - - [拓扑图](#拓扑图) - - [节点下线](#节点下线) - - [Leader选举](#Leader选举) - - [故障转移](#故障转移) - - [读写分离](#读写分离) - - [定时任务](#定时任务) - - [分布式集群(Cluster)](#分布式集群cluster) - - [拓扑图](#拓扑图) - - [通讯](#通讯) - - [集中式](#集中式) - - [Gossip](#gossip) - - [寻址分片](#寻址分片) - - [hash取模](#hash取模) - - [一致性hash](#一致性hash) - - [hash槽](#hash槽) - - [使用场景](#使用场景) - - [热点数据](#热点数据) - - [会话维持 Session](#会话维持-session) - - [分布式锁 SETNX](#分布式锁-setnx) - - [表缓存](#表缓存) - - [消息队列 list](#消息队列-list) - - [计数器 string](#计数器-string) - - [缓存设计](#缓存设计) - - [更新策略](#更新策略) - - [更新一致性](#更新一致性) - - [缓存粒度](#缓存粒度) - - [缓存穿透](#缓存穿透) - - [解决方案](#解决方案) - - [缓存雪崩](#缓存雪崩) - - [出现后应对](#出现后应对) - - [请求过程](#请求过程) - - - -# Redis 集群以及应用 - -## 集群 - -### 主从复制 - -#### 主从链(拓扑结构) - - - -![主从](https://user-images.githubusercontent.com/26766909/67539461-d1a26c00-f714-11e9-81ae-61fa89faf156.png) - -![主从](https://user-images.githubusercontent.com/26766909/67539485-e0891e80-f714-11e9-8980-d253239fcd8b.png) - -#### 复制模式 -- 全量复制:Master 全部同步到 Slave -- 部分复制:Slave 数据丢失进行备份 - -#### 问题点 -- 同步故障 - - 复制数据延迟(不一致) - - 读取过期数据(Slave 不能删除数据) - - 从节点故障 - - 主节点故障 -- 配置不一致 - - maxmemory 不一致:丢失数据 - - 优化参数不一致:内存不一致. -- 避免全量复制 - - 选择小主节点(分片)、低峰期间操作. - - 如果节点运行 id 不匹配(如主节点重启、运行 id 发送变化),此时要执行全量复制,应该配合哨兵和集群解决. - - 主从复制挤压缓冲区不足产生的问题(网络中断,部分复制无法满足),可增大复制缓冲区( rel_backlog_size 参数). -- 复制风暴 - -### 哨兵机制 - -#### 拓扑图 - -![哨兵机制-拓扑图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/哨兵机制-拓扑图.png) - -#### 节点下线 - -- 主观下线 - - 即 Sentinel 节点对 Redis 节点失败的偏见,超出超时时间认为 Master 已经宕机。 - - Sentinel 集群的每一个 Sentinel 节点会定时对 Redis 集群的所有节点发心跳包检测节点是否正常。如果一个节点在 `down-after-milliseconds` 时间内没有回复 Sentinel 节点的心跳包,则该 Redis 节点被该 Sentinel 节点主观下线。 -- 客观下线 - - 所有 Sentinel 节点对 Redis 节点失败要达成共识,即超过 quorum 个统一。 - - 当节点被一个 Sentinel 节点记为主观下线时,并不意味着该节点肯定故障了,还需要 Sentinel 集群的其他 Sentinel 节点共同判断为主观下线才行。 - - 该 Sentinel 节点会询问其它 Sentinel 节点,如果 Sentinel 集群中超过 quorum 数量的 Sentinel 节点认为该 Redis 节点主观下线,则该 Redis 客观下线。 - -#### Leader选举 - -- 选举出一个 Sentinel 作为 Leader:集群中至少有三个 Sentinel 节点,但只有其中一个节点可完成故障转移.通过以下命令可以进行失败判定或领导者选举。 -- 选举流程 - 1. 每个主观下线的 Sentinel 节点向其他 Sentinel 节点发送命令,要求设置它为领导者. - 2. 收到命令的 Sentinel 节点如果没有同意通过其他 Sentinel 节点发送的命令,则同意该请求,否则拒绝。 - 3. 如果该 Sentinel 节点发现自己的票数已经超过 Sentinel 集合半数且超过 quorum,则它成为领导者。 - 4. 如果此过程有多个 Sentinel 节点成为领导者,则等待一段时间再重新进行选举。 - -#### 故障转移 - -- 转移流程 - 1. Sentinel 选出一个合适的 Slave 作为新的 Master(slaveof no one 命令)。 - 2. 向其余 Slave 发出通知,让它们成为新 Master 的 Slave( parallel-syncs 参数)。 - 3. 等待旧 Master 复活,并使之称为新 Master 的 Slave。 - 4. 向客户端通知 Master 变化。 -- 从 Slave 中选择新 Master 节点的规则(slave 升级成 master 之后) - 1. 选择 slave-priority 最高的节点。 - 2. 选择复制偏移量最大的节点(同步数据最多)。 - 3. 选择 runId 最小的节点。 - ->Sentinel 集群运行过程中故障转移完成,所有 Sentinel 又会恢复平等。Leader 仅仅是故障转移操作出现的角色。 - -#### 读写分离 - -#### 定时任务 - -- 每 1s 每个 Sentinel 对其他 Sentinel 和 Redis 执行 ping,进行心跳检测。 -- 每 2s 每个 Sentinel 通过 Master 的 Channel 交换信息(pub - sub)。 -- 每 10s 每个 Sentinel 对 Master 和 Slave 执行 info,目的是发现 Slave 节点、确定主从关系。 - -### 分布式集群(Cluster) - -#### 拓扑图 - -![image](https://user-images.githubusercontent.com/26766909/67539510-f8f93900-f714-11e9-9d8d-08afdecff95a.png) - -#### 通讯 - -##### 集中式 - -> 将集群元数据(节点信息、故障等等)几种存储在某个节点上。 -- 优势 - 1. 元数据的更新读取具有很强的时效性,元数据修改立即更新 -- 劣势 - 1. 数据集中存储 - -##### Gossip - -![image](https://user-images.githubusercontent.com/26766909/67539546-16c69e00-f715-11e9-9891-1e81b6af624c.png) - -- [Gossip 协议](https://www.jianshu.com/p/8279d6fd65bb) - -#### 寻址分片 - -##### hash取模 - -- hash(key)%机器数量 -- 问题 - 1. 机器宕机,造成数据丢失,数据读取失败 - 1. 伸缩性 - -##### 一致性hash - -- ![image](https://user-images.githubusercontent.com/26766909/67539595-352c9980-f715-11e9-8e4a-9d9c04027785.png) - -- 问题 - 1. 一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。 - - 解决方案 - - 可以通过引入虚拟节点机制解决:即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。 - -##### hash槽 - -- CRC16(key)%16384 -- -![image](https://user-images.githubusercontent.com/26766909/67539610-3fe72e80-f715-11e9-8e0d-ea58bc965795.png) - -## 使用场景 - -### 热点数据 - -存取数据优先从 Redis 操作,如果不存在再从文件(例如 MySQL)中操作,从文件操作完后将数据存储到 Redis 中并返回。同时有个定时任务后台定时扫描 Redis 的 key,根据业务规则进行淘汰,防止某些只访问一两次的数据一直存在 Redis 中。 ->例如使用 Zset 数据结构,存储 Key 的访问次数/最后访问时间作为 Score,最后做排序,来淘汰那些最少访问的 Key。 - -如果企业级应用,可以参考:[阿里云的 Redis 混合存储版][1] - -### 会话维持 Session - -会话维持 Session 场景,即使用 Redis 作为分布式场景下的登录中心存储应用。每次不同的服务在登录的时候,都会去统一的 Redis 去验证 Session 是否正确。但是在微服务场景,一般会考虑 Redis + JWT 做 Oauth2 模块。 ->其中 Redis 存储 JWT 的相关信息主要是留出口子,方便以后做统一的防刷接口,或者做登录设备限制等。 - -### 分布式锁 SETNX - -命令格式:`SETNX key value`:当且仅当 key 不存在,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。 - -1. 超时时间设置:获取锁的同时,启动守护线程,使用 expire 进行定时更新超时时间。如果该业务机器宕机,守护线程也挂掉,这样也会自动过期。如果该业务不是宕机,而是真的需要这么久的操作时间,那么增加超时时间在业务上也是可以接受的,但是肯定有个最大的阈值。 -2. 但是为了增加高可用,需要使用多台 Redis,就增加了复杂性,就可以参考 Redlock:[Redlock分布式锁](Redlock分布式锁.md#怎么在单节点上实现分布式锁) - -### 表缓存 - -Redis 缓存表的场景有黑名单、禁言表等。访问频率较高,即读高。根据业务需求,可以使用后台定时任务定时刷新 Redis 的缓存表数据。 - -### 消息队列 list - -主要使用了 List 数据结构。 -List 支持在头部和尾部操作,因此可以实现简单的消息队列。 -1. 发消息:在 List 尾部塞入数据。 -2. 消费消息:在 List 头部拿出数据。 - -同时可以使用多个 List,来实现多个队列,根据不同的业务消息,塞入不同的 List,来增加吞吐量。 - -### 计数器 string - -主要使用了 INCR、DECR、INCRBY、DECRBY 方法。 - -INCR key:给 key 的 value 值增加一 -DECR key:给 key 的 value 值减去一 - -## 缓存设计 - -### 更新策略 - -- LRU、LFU、FIFO 算法自动清除:一致性最差,维护成本低。 -- 超时自动清除(key expire):一致性较差,维护成本低。 -- 主动更新:代码层面控制生命周期,一致性最好,维护成本高。 - -在 Redis 根据在 redis.conf 的参数 `maxmemory` 来做更新淘汰策略: -1. noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL 命令)。 -2. allkeys-lru: 所有 key 通用; 优先删除最近最少使用(less recently used ,LRU) 的 key。 -3. volatile-lru: 只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key。 -4. allkeys-random: 所有key通用; 随机删除一部分 key。 -5. volatile-random: 只限于设置了 expire 的部分; 随机删除一部分 key。 -6. volatile-ttl: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key。 - -### 更新一致性 - -- 读请求:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。 -- 写请求:先删除缓存,然后再更新数据库(避免大量地写、却又不经常读的数据导致缓存频繁更新)。 - -### 缓存粒度 - -- 通用性:全量属性更好。 -- 占用空间:部分属性更好。 -- 代码维护成本。 - -### 缓存穿透 - -> 当大量的请求无命中缓存、直接请求到后端数据库(业务代码的 bug、或恶意攻击),同时后端数据库也没有查询到相应的记录、无法添加缓存。 -> 这种状态会一直维持,流量一直打到存储层上,无法利用缓存、还会给存储层带来巨大压力。 - -#### 解决方案 - -1. 请求无法命中缓存、同时数据库记录为空时在缓存添加该 key 的空对象(设置过期时间),缺点是可能会在缓存中添加大量的空值键(比如遭到恶意攻击或爬虫),而且缓存层和存储层数据短期内不一致; -2. 使用布隆过滤器在缓存层前拦截非法请求、自动为空值添加黑名单(同时可能要为误判的记录添加白名单).但需要考虑布隆过滤器的维护(离线生成/ 实时生成)。 - -### 缓存雪崩 - -> 缓存崩溃时请求会直接落到数据库上,很可能由于无法承受大量的并发请求而崩溃,此时如果只重启数据库,或因为缓存重启后没有数据,新的流量进来很快又会把数据库击倒。 - -#### 出现后应对 - -- 事前:Redis 高可用,主从 + 哨兵,Redis Cluster,避免全盘崩溃。 -- 事中:本地 ehcache 缓存 + hystrix 限流 & 降级,避免数据库承受太多压力。 -- 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。 - -#### 请求过程 - -1. 用户请求先访问本地缓存,无命中后再访问 Redis,如果本地缓存和 Redis 都没有再查数据库,并把数据添加到本地缓存和 Redis; -2. 由于设置了限流,一段时间范围内超出的请求走降级处理(返回默认值,或给出友情提示)。 - diff --git "a/docs/database/Redis/\345\246\202\344\275\225\345\201\232\345\217\257\351\235\240\347\232\204\345\210\206\345\270\203\345\274\217\351\224\201\357\274\214Redlock\347\234\237\347\232\204\345\217\257\350\241\214\344\271\210.md" "b/docs/database/Redis/\345\246\202\344\275\225\345\201\232\345\217\257\351\235\240\347\232\204\345\210\206\345\270\203\345\274\217\351\224\201\357\274\214Redlock\347\234\237\347\232\204\345\217\257\350\241\214\344\271\210.md" deleted file mode 100644 index 043df96566d..00000000000 --- "a/docs/database/Redis/\345\246\202\344\275\225\345\201\232\345\217\257\351\235\240\347\232\204\345\210\206\345\270\203\345\274\217\351\224\201\357\274\214Redlock\347\234\237\347\232\204\345\217\257\350\241\214\344\271\210.md" +++ /dev/null @@ -1,91 +0,0 @@ -本文是对 [Martin Kleppmann](https://martin.kleppmann.com/) 的文章 [How to do distributed locking](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html) 部分内容的翻译和总结,上次写 Redlock 的原因就是看到了 Martin 的这篇文章,写得很好,特此翻译和总结。感兴趣的同学可以翻看原文,相信会收获良多。 - -开篇作者认为现在 Redis 逐渐被使用到数据管理领域,这个领域需要更强的数据一致性和耐久性,这使得他感到担心,因为这不是 Redis 最初设计的初衷(事实上这也是很多业界程序员的误区,越来越把 Redis 当成数据库在使用),其中基于 Redis 的分布式锁就是令人担心的其一。 - -Martin 指出首先你要明确你为什么使用分布式锁,为了性能还是正确性?为了帮你区分这二者,在这把锁 fail 了的时候你可以询问自己以下问题: -1. **要性能的:** 拥有这把锁使得你不会重复劳动(例如一个 job 做了两次),如果这把锁 fail 了,两个节点同时做了这个 Job,那么这个 Job 增加了你的成本。 -2. **要正确性的:** 拥有锁可以防止并发操作污染你的系统或者数据,如果这把锁 fail 了两个节点同时操作了一份数据,结果可能是数据不一致、数据丢失、file 冲突等,会导致严重的后果。 - -上述二者都是需求锁的正确场景,但是你必须清楚自己是因为什么原因需要分布式锁。 - -如果你只是为了性能,那没必要用 Redlock,它成本高且复杂,你只用一个 Redis 实例也够了,最多加个从防止主挂了。当然,你使用单节点的 Redis 那么断电或者一些情况下,你会丢失锁,但是你的目的只是加速性能且断电这种事情不会经常发生,这并不是什么大问题。并且如果你使用了单节点 Redis,那么很显然你这个应用需要的锁粒度是很模糊粗糙的,也不会是什么重要的服务。 - -那么是否 Redlock 对于要求正确性的场景就合适呢?Martin 列举了若干场景证明 Redlock 这种算法是不可靠的。 - -## 用锁保护资源 -这节里 Martin 先将 Redlock 放在了一边而是仅讨论总体上一个分布式锁是怎么工作的。在分布式环境下,锁比 mutex 这类复杂,因为涉及到不同节点、网络通信并且他们随时可能无征兆的 fail 。 -Martin 假设了一个场景,一个 client 要修改一个文件,它先申请得到锁,然后修改文件写回,放锁。另一个 client 再申请锁 ... 代码流程如下: - -```java -// THIS CODE IS BROKEN -function writeData(filename, data) { - var lock = lockService.acquireLock(filename); - if (!lock) { - throw 'Failed to acquire lock'; - } - - try { - var file = storage.readFile(filename); - var updated = updateContents(file, data); - storage.writeFile(filename, updated); - } finally { - lock.release(); - } -} -``` - -可惜即使你的锁服务非常完美,上述代码还是可能跪,下面的流程图会告诉你为什么: - -![](https://martin.kleppmann.com/2016/02/unsafe-lock.png) - -上述图中,得到锁的 client1 在持有锁的期间 pause 了一段时间,例如 GC 停顿。锁有过期时间(一般叫租约,为了防止某个 client 崩溃之后一直占有锁),但是如果 GC 停顿太长超过了锁租约时间,此时锁已经被另一个 client2 所得到,原先的 client1 还没有感知到锁过期,那么奇怪的结果就会发生,曾经 HBase 就发生过这种 Bug。即使你在 client1 写回之前检查一下锁是否过期也无助于解决这个问题,因为 GC 可能在任何时候发生,即使是你非常不便的时候(在最后的检查与写操作期间)。 -如果你认为自己的程序不会有长时间的 GC 停顿,还有其他原因会导致你的进程 pause。例如进程可能读取尚未进入内存的数据,所以它得到一个 page fault 并且等待 page 被加载进缓存;还有可能你依赖于网络服务;或者其他进程占用 CPU;或者其他人意外发生 SIGSTOP 等。 - -... .... 这里 Martin 又增加了一节列举各种进程 pause 的例子,为了证明上面的代码是不安全的,无论你的锁服务多完美。 - -## 使用 Fencing (栅栏)使得锁变安全 -修复问题的方法也很简单:你需要在每次写操作时加入一个 fencing token。这个场景下,fencing token 可以是一个递增的数字(lock service 可以做到),每次有 client 申请锁就递增一次: - -![](https://martin.kleppmann.com/2016/02/fencing-tokens.png) - -client1 申请锁同时拿到 token33,然后它进入长时间的停顿锁也过期了。client2 得到锁和 token34 写入数据,紧接着 client1 活过来之后尝试写入数据,自身 token33 比 34 小因此写入操作被拒绝。注意这需要存储层来检查 token,但这并不难实现。如果你使用 Zookeeper 作为 lock service 的话那么你可以使用 zxid 作为递增数字。 -但是对于 Redlock 你要知道,没什么生成 fencing token 的方式,并且怎么修改 Redlock 算法使其能产生 fencing token 呢?好像并不那么显而易见。因为产生 token 需要单调递增,除非在单节点 Redis 上完成但是这又没有高可靠性,你好像需要引进一致性协议来让 Redlock 产生可靠的 fencing token。 - -## 使用时间来解决一致性 -Redlock 无法产生 fencing token 早该成为在需求正确性的场景下弃用它的理由,但还有一些值得讨论的地方。 - -学术界有个说法,算法对时间不做假设:因为进程可能pause一段时间、数据包可能因为网络延迟延后到达、时钟可能根本就是错的。而可靠的算法依旧要在上述假设下做正确的事情。 - -对于 failure detector 来说,timeout 只能作为猜测某个节点 fail 的依据,因为网络延迟、本地时钟不正确等其他原因的限制。考虑到 Redis 使用 gettimeofday,而不是单调的时钟,会受到系统时间的影响,可能会突然前进或者后退一段时间,这会导致一个 key 更快或更慢地过期。 - -可见,Redlock 依赖于许多时间假设,它假设所有 Redis 节点都能对同一个 Key 在其过期前持有差不多的时间、跟过期时间相比网络延迟很小、跟过期时间相比进程 pause 很短。 - -## 用不可靠的时间打破 Redlock -这节 Martin 举了个因为时间问题,Redlock 不可靠的例子。 - -1. client1 从 ABC 三个节点处申请到锁,DE由于网络原因请求没有到达 -2. C节点的时钟往前推了,导致 lock 过期 -3. client2 在CDE处获得了锁,AB由于网络原因请求未到达 -4. 此时 client1 和 client2 都获得了锁 - -**在 Redlock 官方文档中也提到了这个情况,不过是C崩溃的时候,Redlock 官方本身也是知道 Redlock 算法不是完全可靠的,官方为了解决这种问题建议使用延时启动,相关内容可以看之前的[这篇文章](https://zhuanlan.zhihu.com/p/40915772)。但是 Martin 这里分析得更加全面,指出延时启动不也是依赖于时钟的正确性的么?** - -接下来 Martin 又列举了进程 Pause 时而不是时钟不可靠时会发生的问题: - -1. client1 从 ABCDE 处获得了锁 -2. 当获得锁的 response 还没到达 client1 时 client1 进入 GC 停顿 -3. 停顿期间锁已经过期了 -4. client2 在 ABCDE 处获得了锁 -5. client1 GC 完成收到了获得锁的 response,此时两个 client 又拿到了同一把锁 - -**同时长时间的网络延迟也有可能导致同样的问题。** - -## Redlock 的同步性假设 -这些例子说明了,仅有在你假设了一个同步性系统模型的基础上,Redlock 才能正常工作,也就是系统能满足以下属性: - -1. 网络延时边界,即假设数据包一定能在某个最大延时之内到达 -2. 进程停顿边界,即进程停顿一定在某个最大时间之内 -3. 时钟错误边界,即不会从一个坏的 NTP 服务器处取得时间 - -## 结论 -Martin 认为 Redlock 实在不是一个好的选择,对于需求性能的分布式锁应用它太重了且成本高;对于需求正确性的应用来说它不够安全。因为它对高危的时钟或者说其他上述列举的情况进行了不可靠的假设,如果你的应用只需要高性能的分布式锁不要求多高的正确性,那么单节点 Redis 够了;如果你的应用想要保住正确性,那么不建议 Redlock,建议使用一个合适的一致性协调系统,例如 Zookeeper,且保证存在 fencing token。 diff --git "a/docs/database/\344\270\200\345\215\203\350\241\214MySQL\345\221\275\344\273\244.md" b/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md similarity index 98% rename from "docs/database/\344\270\200\345\215\203\350\241\214MySQL\345\221\275\344\273\244.md" rename to docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md index 385aa37dc7a..53350ec5ae7 100644 --- "a/docs/database/\344\270\200\345\215\203\350\241\214MySQL\345\221\275\344\273\244.md" +++ b/docs/database/mysql/a-thousand-lines-of-mysql-study-notes.md @@ -1,35 +1,15 @@ +--- +title: 一千行 MySQL 学习笔记 +category: 数据库 +tag: + - MySQL +--- + > 原文地址:https://shockerli.net/post/1000-line-mysql-note/ ,JavaGuide 对本文进行了简答排版,新增了目录。 > 作者:格物 非常不错的总结,强烈建议保存下来,需要的时候看一看。 - -- [基本操作](#基本操作) -- [数据库操作](#数据库操作) -- [表的操作](#表的操作) -- [数据操作](#数据操作) -- [字符集编码](#字符集编码) -- [数据类型(列类型)](#数据类型列类型) -- [列属性(列约束)](#列属性列约束) -- [建表规范](#建表规范) -- [SELECT](#select) -- [UNION](#union) -- [子查询](#子查询) -- [连接查询(join)](#连接查询join) -- [TRUNCATE](#truncate) -- [备份与还原](#备份与还原) -- [视图](#视图) -- [事务(transaction)](#事务transaction) -- [锁表](#锁表) -- [触发器](#触发器) -- [SQL编程](#sql编程) -- [存储过程](#存储过程) -- [用户和权限管理](#用户和权限管理) -- [表维护](#表维护) -- [杂项](#杂项) - - - ### 基本操作 ```mysql @@ -609,7 +589,7 @@ CREATE [OR REPLACE] [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] VIEW view_name - 事务开始和结束时,外部数据一致 - 在整个事务过程中,操作是连续的 3. 隔离性(Isolation) - 多个用户并发访问数据库时,一个用户的事务不能被其它用户的事物所干扰,多个并发事务之间的数据要相互隔离。 + 多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间的数据要相互隔离。 4. 持久性(Durability) 一个事务一旦被提交,它对数据库中的数据改变就是永久性的。 -- 事务的实现 diff --git "a/docs/database/\344\270\200\346\235\241sql\350\257\255\345\217\245\345\234\250mysql\344\270\255\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" b/docs/database/mysql/how-sql-executed-in-mysql.md similarity index 77% rename from "docs/database/\344\270\200\346\235\241sql\350\257\255\345\217\245\345\234\250mysql\344\270\255\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" rename to docs/database/mysql/how-sql-executed-in-mysql.md index 261a0c0b975..07404d0e930 100644 --- "a/docs/database/\344\270\200\346\235\241sql\350\257\255\345\217\245\345\234\250mysql\344\270\255\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" +++ b/docs/database/mysql/how-sql-executed-in-mysql.md @@ -1,26 +1,15 @@ -本文来自[木木匠](https://github.com/kinglaw1204)投稿。 - - +--- +title: 一条 SQL 语句在 MySQL 中如何被执行的? +category: 数据库 +tag: + - MySQL +--- -- [一 MySQL 基础架构分析](#一-mysql-基础架构分析) - - [1.1 MySQL 基本架构概览](#11-mysql-基本架构概览) - - [1.2 Server 层基本组件介绍](#12-server-层基本组件介绍) - - [1) 连接器](#1-连接器) - - [2) 查询缓存(MySQL 8.0 版本后移除)](#2-查询缓存mysql-80-版本后移除) - - [3) 分析器](#3-分析器) - - [4) 优化器](#4-优化器) - - [5) 执行器](#5-执行器) -- [二 语句分析](#二-语句分析) - - [2.1 查询语句](#21-查询语句) - - [2.2 更新语句](#22-更新语句) -- [三 总结](#三-总结) -- [四 参考](#四-参考) - - +本文来自[木木匠](https://github.com/kinglaw1204)投稿。 本篇文章会分析下一个 sql 语句在 MySQL 中的执行流程,包括 sql 的查询在 MySQL 内部会怎么流转,sql 语句的更新是怎么完成的。 -在分析之前我会先带着你看看 MySQL 的基础架构,知道了 MySQL 由那些组件组成已经这些组件的作用是什么,可以帮助我们理解和解决这些问题。 +在分析之前我会先带着你看看 MySQL 的基础架构,知道了 MySQL 由那些组件组成以及这些组件的作用是什么,可以帮助我们理解和解决这些问题。 ## 一 MySQL 基础架构分析 @@ -31,12 +20,12 @@ 先简单介绍一下下图涉及的一些组件的基本作用帮助大家理解这幅图,在 1.2 节中会详细介绍到这些组件的作用。 - **连接器:** 身份认证和权限相关(登录 MySQL 的时候)。 -- **查询缓存:** 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。 -- **分析器:** 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。 -- **优化器:** 按照 MySQL 认为最优的方案去执行。 -- **执行器:** 执行语句,然后从存储引擎返回数据。 +- **查询缓存:** 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。 +- **分析器:** 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。 +- **优化器:** 按照 MySQL 认为最优的方案去执行。 + -![](https://user-gold-cdn.xitu.io/2019/3/23/169a8bc60a083849?w=950&h=1062&f=jpeg&s=38189) +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/13526879-3037b144ed09eb88.png) 简单来说 MySQL 主要分为 Server 层和存储引擎层: @@ -96,7 +85,7 @@ select * from tb_student A where A.age='18' and A.name=' 张三 '; 结合上面的说明,我们分析下这个语句的执行流程: * 先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 sql 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。 -* 通过分析器进行词法分析,提取 sql 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id='1'。然后判断这个 sql 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。 +* 通过分析器进行词法分析,提取 sql 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id='1'。然后判断这个 sql 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。 * 接下来就是优化器进行确定执行方案,上面的 sql 语句,可以有两种执行方案: a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。 @@ -112,7 +101,7 @@ select * from tb_student A where A.age='18' and A.name=' 张三 '; ``` update tb_student A set A.age='19' where A.name=' 张三 '; ``` -我们来给张三修改下年龄,在实际数据库肯定不会设置年龄这个字段的,不然要被技术负责人打的。其实条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,MySQL 自带的日志模块式 **binlog(归档日志)** ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 **redo log(重做日志)**,我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下: +我们来给张三修改下年龄,在实际数据库肯定不会设置年龄这个字段的,不然要被技术负责人打的。其实这条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,MySQL 自带的日志模块是 **binlog(归档日志)** ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 **redo log(重做日志)**,我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下: * 先查询到张三这一条数据,如果有缓存,也是会用到缓存。 * 然后拿到查询的语句,把 age 改为 19,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。 @@ -121,7 +110,7 @@ update tb_student A set A.age='19' where A.name=' 张三 '; **这里肯定有同学会问,为什么要用两个日志模块,用一个日志模块不行吗?** -这是因为最开始 MySQL 并没与 InnoDB 引擎( InnoDB 引擎是其他公司以插件形式插入 MySQL 的) ,MySQL 自带的引擎是 MyISAM,但是我们知道 redo log 是 InnoDB 引擎特有的,其他存储引擎都没有,这就导致会没有 crash-safe 的能力(crash-safe 的能力即使数据库发生异常重启,之前提交的记录都不会丢失),binlog 日志只能用来归档。 +这是因为最开始 MySQL 并没有 InnoDB 引擎(InnoDB 引擎是其他公司以插件形式插入 MySQL 的),MySQL 自带的引擎是 MyISAM,但是我们知道 redo log 是 InnoDB 引擎特有的,其他存储引擎都没有,这就导致会没有 crash-safe 的能力(crash-safe 的能力即使数据库发生异常重启,之前提交的记录都不会丢失),binlog 日志只能用来归档。 并不是说只用一个日志模块不可以,只是 InnoDB 引擎就是通过 redo log 来支持事务的。那么,又会有同学问,我用两个日志模块,但是不要这么复杂行不行,为什么 redo log 要引入 prepare 预提交状态?这里我们用反证法来说明下为什么要这么做? @@ -138,10 +127,10 @@ update tb_student A set A.age='19' where A.name=' 张三 '; ## 三 总结 -* MySQL 主要分为 Server 层和引擎层,Server 层主要包括连接器、查询缓存、分析器、优化器、执行器,同时还有一个日志模块(binlog),这个日志模块所有执行引擎都可以共用,redolog 只有 InnoDB 有。 +* MySQL 主要分为 Server 层和引擎层,Server 层主要包括连接器、查询缓存、分析器、优化器、执行器,同时还有一个日志模块(binlog),这个日志模块所有执行引擎都可以共用,redolog 只有 InnoDB 有。 * 引擎层是插件式的,目前主要包括,MyISAM,InnoDB,Memory 等。 -* 查询语句的执行流程如下:权限校验(如果命中缓存)---》查询缓存---》分析器---》优化器---》权限校验---》执行器---》引擎 -* 更新语句执行流程如下:分析器----》权限校验----》执行器---》引擎---redo log(prepare 状态---》binlog---》redo log(commit状态) +* 查询语句的执行流程如下:权限校验(如果命中缓存)--->查询缓存--->分析器--->优化器--->权限校验--->执行器--->引擎 +* 更新语句执行流程如下:分析器---->权限校验---->执行器--->引擎---redo log(prepare 状态)--->binlog--->redo log(commit状态) ## 四 参考 diff --git a/docs/database/mysql/innodb-implementation-of-mvcc.md b/docs/database/mysql/innodb-implementation-of-mvcc.md new file mode 100644 index 00000000000..96fa1f7982b --- /dev/null +++ b/docs/database/mysql/innodb-implementation-of-mvcc.md @@ -0,0 +1,226 @@ +--- +title: InnoDB存储引擎对MVCC的实现 +category: 数据库 +tag: + - MySQL +--- + +## 一致性非锁定读和锁定读 + +### 一致性非锁定读 + +对于 [**一致性非锁定读(Consistent Nonlocking Reads)** ](https://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html)的实现,通常做法是加一个版本号或者时间戳字段,在更新数据的同时版本号 + 1 或者更新时间戳。查询时,将当前可见的版本号与对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见 + +在 `InnoDB` 存储引擎中,[多版本控制 (multi versioning)](https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html) 就是对非锁定读的实现。如果读取的行正在执行 `DELETE` 或 `UPDATE` 操作,这时读取操作不会去等待行上锁的释放。相反地,`InnoDB` 存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读 (snapshot read) + +在 `Repeatable Read` 和 `Read Committed` 两个隔离级别下,如果是执行普通的 `select` 语句(不包括 `select ... lock in share mode` ,`select ... for update`)则会使用 `一致性非锁定读(MVCC)`。并且在 `Repeatable Read` 下 `MVCC` 实现了可重复读和防止部分幻读 + +### 锁定读 + +如果执行的是下列语句,就是 [**锁定读(Locking Reads)**](https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html) + +- `select ... lock in share mode` +- `select ... for update` +- `insert`、`update`、`delete` 操作 + +在锁定读下,读取的是数据的最新版本,这种读也被称为 `当前读(current read)`。锁定读会对读取到的记录加锁: + +- `select ... lock in share mode`:对记录加 `S` 锁,其它事务也可以加`S`锁,如果加 `x` 锁则会被阻塞 + +- `select ... for update`、`insert`、`update`、`delete`:对记录加 `X` 锁,且其它事务不能加任何锁 + +在一致性非锁定读下,即使读取的记录已被其它事务加上 `X` 锁,这时记录也是可以被读取的,即读取的快照数据。上面说了,在 `Repeatable Read` 下 `MVCC` 防止了部分幻读,这边的 “部分” 是指在 `一致性非锁定读` 情况下,只能读取到第一次查询之前所插入的数据(根据 Read View 判断数据可见性,Read View 在第一次查询时生成)。但是!如果是 `当前读` ,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。所以, **`InnoDB` 在实现`Repeatable Read` 时,如果执行的是当前读,则会对读取的记录使用 `Next-key Lock` ,来防止其它事务在间隙间插入数据** + +## InnoDB 对 MVCC 的实现 + +`MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,`InnoDB` 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 `undo log` 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改 + +### 隐藏字段 + +在内部,`InnoDB` 存储引擎为每行数据添加了三个 [隐藏字段](https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html): + +- `DB_TRX_ID(6字节)`:表示最后一次插入或更新该行的事务 id。此外,`delete` 操作在内部被视为更新,只不过会在记录头 `Record header` 中的 `deleted_flag` 字段将其标记为已删除 +- `DB_ROLL_PTR(7字节)` 回滚指针,指向该行的 `undo log` 。如果该行未被更新,则为空 +- `DB_ROW_ID(6字节)`:如果没有设置主键且该表没有唯一非空索引时,`InnoDB` 会使用该 id 来生成聚簇索引 + +### ReadView + +```c +class ReadView { + /* ... */ +private: + trx_id_t m_low_limit_id; /* 大于等于这个 ID 的事务均不可见 */ + + trx_id_t m_up_limit_id; /* 小于这个 ID 的事务均可见 */ + + trx_id_t m_creator_trx_id; /* 创建该 Read View 的事务ID */ + + trx_id_t m_low_limit_no; /* 事务 Number, 小于该 Number 的 Undo Logs 均可以被 Purge */ + + ids_t m_ids; /* 创建 Read View 时的活跃事务列表 */ + + m_closed; /* 标记 Read View 是否 close */ +} +``` + +[`Read View`](https://github.com/facebook/mysql-8.0/blob/8.0/storage/innobase/include/read0types.h#L298) 主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务” + +主要有以下字段: + +- `m_low_limit_id`:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见 +- `m_up_limit_id`:活跃事务列表 `m_ids` 中最小的事务 ID,如果 `m_ids` 为空,则 `m_up_limit_id` 为 `m_low_limit_id`。小于这个 ID 的数据版本均可见 +- `m_ids`:`Read View` 创建时其他未提交的活跃事务 ID 列表。创建 `Read View`时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。`m_ids` 不包括当前事务自己和已提交的事务(正在内存中) +- `m_creator_trx_id`:创建该 `Read View` 的事务 ID + +**事务可见性示意图**([图源](https://leviathan.vip/2019/03/20/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-MVCC/#MVCC-1)): + +![trans_visible](https://leviathan.vip/2019/03/20/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-MVCC/trans_visible.jpg) + +### undo-log + +`undo log` 主要有两个作用: + +- 当事务回滚时用于将数据恢复到修改前的样子 +- 另一个作用是 `MVCC` ,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 `undo log` 读取之前的版本数据,以此实现非锁定读 + +**在 `InnoDB` 存储引擎中 `undo log` 分为两种: `insert undo log` 和 `update undo log`:** + +1. **`insert undo log`** :指在 `insert` 操作中产生的 `undo log`。因为 `insert` 操作的记录只对事务本身可见,对其他事务不可见,故该 `undo log` 可以在事务提交后直接删除。不需要进行 `purge` 操作 + +**`insert` 时的数据初始状态:** + +![](https://ddmcc-1255635056.file.myqcloud.com/317e91e1-1ee1-42ad-9412-9098d5c6a9ad.png) + +2. **`update undo log`** :`update` 或 `delete` 操作中产生的 `undo log`。该 `undo log`可能需要提供 `MVCC` 机制,因此不能在事务提交时就进行删除。提交时放入 `undo log` 链表,等待 `purge线程` 进行最后的删除 + +**数据第一次被修改时:** + +![](https://ddmcc-1255635056.file.myqcloud.com/c52ff79f-10e6-46cb-b5d4-3c9cbcc1934a.png) + +**数据第二次被修改时:** + +![](https://ddmcc-1255635056.file.myqcloud.com/6a276e7a-b0da-4c7b-bdf7-c0c7b7b3b31c.png) + +不同事务或者相同事务的对同一记录行的修改,会使该记录行的 `undo log` 成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。 + +### 数据可见性算法 + +在 `InnoDB` 存储引擎中,创建一个新事务后,执行每个 `select` 语句前,都会创建一个快照(Read View),**快照中保存了当前数据库系统中正处于活跃(没有 commit)的事务的 ID 号**。其实简单的说保存的是系统中当前不应该被本事务看到的其他事务 ID 列表(即 m_ids)。当用户在这个事务中要读取某个记录行的时候,`InnoDB` 会将该记录行的 `DB_TRX_ID` 与 `Read View` 中的一些变量及当前事务 ID 进行比较,判断是否满足可见性条件 + +[具体的比较算法](https://github.com/facebook/mysql-8.0/blob/8.0/storage/innobase/include/read0types.h#L161)如下:[图源](https://leviathan.vip/2019/03/20/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-MVCC/#MVCC-1) + +![](https://ddmcc-1255635056.file.myqcloud.com/8778836b-34a8-480b-b8c7-654fe207a8c2.png) + +1. 如果记录 DB_TRX_ID < m_up_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之前就提交了,所以该记录行的值对当前事务是可见的 + +2. 如果 DB_TRX_ID >= m_low_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤 5 + +3. m_ids 为空,则表明在当前事务创建快照之前,修改该行的事务就已经提交了,所以该记录行的值对当前事务是可见的 + +4. 如果 m_up_limit_id <= DB_TRX_ID < m_low_limit_id,表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表 m_ids 进行查找(源码中是用的二分查找,因为是有序的) + + - 如果在活跃事务列表 m_ids 中能找到 DB_TRX_ID,表明:① 在当前事务创建快照前,该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了,但没有提交;或者 ② 在当前事务创建快照后,该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了。这些情况下,这个记录行的值对当前事务都是不可见的。跳到步骤 5 + + - 在活跃事务列表中找不到,则表明“id 为 trx_id 的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见 + +5. 在该记录行的 DB_ROLL_PTR 指针所指向的 `undo log` 取出快照记录,用快照记录的 DB_TRX_ID 跳到步骤 1 重新开始判断,直到找到满足的快照版本或返回空 + +## RC 和 RR 隔离级别下 MVCC 的差异 + +在事务隔离级别 `RC` 和 `RR` (InnoDB 存储引擎的默认事务隔离级别)下,`InnoDB` 存储引擎使用 `MVCC`(非锁定一致性读),但它们生成 `Read View` 的时机却不同 + +- 在 RC 隔离级别下的 **`每次select`** 查询前都生成一个`Read View` (m_ids 列表) +- 在 RR 隔离级别下只在事务开始后 **`第一次select`** 数据前生成一个`Read View`(m_ids 列表) + +## MVCC 解决不可重复读问题 + +虽然 RC 和 RR 都通过 `MVCC` 来读取快照数据,但由于 **生成 Read View 时机不同**,从而在 RR 级别下实现可重复读 + +举个例子: + +![](https://ddmcc-1255635056.file.myqcloud.com/6fb2b9a1-5f14-4dec-a797-e4cf388ed413.png) + +### 在 RC 下 ReadView 生成情况 + +1. **`假设时间线来到 T4 ,那么此时数据行 id = 1 的版本链为`:** + + ![](https://ddmcc-1255635056.file.myqcloud.com/a3fd1ec6-8f37-42fa-b090-7446d488fd04.png) + +由于 RC 级别下每次查询都会生成`Read View` ,并且事务 101、102 并未提交,此时 `103` 事务生成的 `Read View` 中活跃的事务 **`m_ids` 为:[101,102]** ,`m_low_limit_id`为:104,`m_up_limit_id`为:101,`m_creator_trx_id` 为:103 + +- 此时最新记录的 `DB_TRX_ID` 为 101,m_up_limit_id <= 101 < m_low_limit_id,所以要在 `m_ids` 列表中查找,发现 `DB_TRX_ID` 存在列表中,那么这个记录不可见 +- 根据 `DB_ROLL_PTR` 找到 `undo log` 中的上一版本记录,上一条记录的 `DB_TRX_ID` 还是 101,不可见 +- 继续找上一条 `DB_TRX_ID`为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为 `name = 菜花` + +2. **`时间线来到 T6 ,数据的版本链为`:** + + ![markdown](https://ddmcc-1255635056.file.myqcloud.com/528559e9-dae8-4d14-b78d-a5b657c88391.png) + +因为在 RC 级别下,重新生成 `Read View`,这时事务 101 已经提交,102 并未提交,所以此时 `Read View` 中活跃的事务 **`m_ids`:[102]** ,`m_low_limit_id`为:104,`m_up_limit_id`为:102,`m_creator_trx_id`为:103 + +- 此时最新记录的 `DB_TRX_ID` 为 102,m_up_limit_id <= 102 < m_low_limit_id,所以要在 `m_ids` 列表中查找,发现 `DB_TRX_ID` 存在列表中,那么这个记录不可见 + +- 根据 `DB_ROLL_PTR` 找到 `undo log` 中的上一版本记录,上一条记录的 `DB_TRX_ID` 为 101,满足 101 < m_up_limit_id,记录可见,所以在 `T6` 时间点查询到数据为 `name = 李四`,与时间 T4 查询到的结果不一致,不可重复读! + +3. **`时间线来到 T9 ,数据的版本链为`:** + +![markdown](https://ddmcc-1255635056.file.myqcloud.com/6f82703c-36a1-4458-90fe-d7f4edbac71a.png) + +重新生成 `Read View`, 这时事务 101 和 102 都已经提交,所以 **m_ids** 为空,则 m_up_limit_id = m_low_limit_id = 104,最新版本事务 ID 为 102,满足 102 < m_low_limit_id,可见,查询结果为 `name = 赵六` + +> **总结:** **在 RC 隔离级别下,事务在每次查询开始时都会生成并设置新的 Read View,所以导致不可重复读** + +### 在 RR 下 ReadView 生成情况 + +**在可重复读级别下,只会在事务开始后第一次读取数据时生成一个 Read View(m_ids 列表)** + +1. **`在 T4 情况下的版本链为`:** + +![markdown](https://ddmcc-1255635056.file.myqcloud.com/0e906b95-c916-4f30-beda-9cb3e49746bf.png) + +在当前执行 `select` 语句时生成一个 `Read View`,此时 **`m_ids`:[101,102]** ,`m_low_limit_id`为:104,`m_up_limit_id`为:101,`m_creator_trx_id` 为:103 + +此时和 RC 级别下一样: + +- 最新记录的 `DB_TRX_ID` 为 101,m_up_limit_id <= 101 < m_low_limit_id,所以要在 `m_ids` 列表中查找,发现 `DB_TRX_ID` 存在列表中,那么这个记录不可见 +- 根据 `DB_ROLL_PTR` 找到 `undo log` 中的上一版本记录,上一条记录的 `DB_TRX_ID` 还是 101,不可见 +- 继续找上一条 `DB_TRX_ID`为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为 `name = 菜花` + +2. **`时间点 T6 情况下`:** + + ![markdown](https://ddmcc-1255635056.file.myqcloud.com/79ed6142-7664-4e0b-9023-cf546586aa39.png) + + 在 RR 级别下只会生成一次`Read View`,所以此时依然沿用 **`m_ids` :[101,102]** ,`m_low_limit_id`为:104,`m_up_limit_id`为:101,`m_creator_trx_id` 为:103 + +- 最新记录的 `DB_TRX_ID` 为 102,m_up_limit_id <= 102 < m_low_limit_id,所以要在 `m_ids` 列表中查找,发现 `DB_TRX_ID` 存在列表中,那么这个记录不可见 + +- 根据 `DB_ROLL_PTR` 找到 `undo log` 中的上一版本记录,上一条记录的 `DB_TRX_ID` 为 101,不可见 + +- 继续根据 `DB_ROLL_PTR` 找到 `undo log` 中的上一版本记录,上一条记录的 `DB_TRX_ID` 还是 101,不可见 + +- 继续找上一条 `DB_TRX_ID`为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为 `name = 菜花` + +3. **时间点 T9 情况下:** + +![markdown](https://ddmcc-1255635056.file.myqcloud.com/cbbedbc5-0e3c-4711-aafd-7f3d68a4ed4e.png) + +此时情况跟 T6 完全一样,由于已经生成了 `Read View`,此时依然沿用 **`m_ids` :[101,102]** ,所以查询结果依然是 `name = 菜花` + +## MVCC➕Next-key-Lock 防止幻读 + +`InnoDB`存储引擎在 RR 级别下通过 `MVCC`和 `Next-key Lock` 来解决幻读问题: + +**1、执行普通 `select`,此时会以 `MVCC` 快照读的方式读取数据** + +在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 `Read View` ,并使用至事务提交。所以在生成 `Read View` 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读” + +**2、执行 select...for update/lock in share mode、insert、update、delete 等当前读** + +在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!`InnoDB` 使用 [Next-key Lock](https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-next-key-locks) 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读 + +## 参考 + +- **《MySQL 技术内幕 InnoDB 存储引擎第 2 版》** +- [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html) +- [MySQL 事务与 MVCC 如何实现的隔离级别](https://blog.csdn.net/qq_35190492/article/details/109044141) +- [InnoDB 事务分析-MVCC](https://leviathan.vip/2019/03/20/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-MVCC/) diff --git "a/docs/database/MySQL\351\253\230\346\200\247\350\203\275\344\274\230\345\214\226\350\247\204\350\214\203\345\273\272\350\256\256.md" b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md similarity index 75% rename from "docs/database/MySQL\351\253\230\346\200\247\350\203\275\344\274\230\345\214\226\350\247\204\350\214\203\345\273\272\350\256\256.md" rename to docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md index ce3d3688305..7c2b7def8ad 100644 --- "a/docs/database/MySQL\351\253\230\346\200\247\350\203\275\344\274\230\345\214\226\350\247\204\350\214\203\345\273\272\350\256\256.md" +++ b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md @@ -1,58 +1,11 @@ -> 作者: 听风,原文地址: 。JavaGuide 已获得作者授权。 +--- +title: MySQL 高性能优化规范建议 +category: 数据库 +tag: + - MySQL +--- - - -- [数据库命令规范](#数据库命令规范) -- [数据库基本设计规范](#数据库基本设计规范) - - [1. 所有表必须使用 Innodb 存储引擎](#1-所有表必须使用-innodb-存储引擎) - - [2. 数据库和表的字符集统一使用 UTF8](#2-数据库和表的字符集统一使用-utf8) - - [3. 所有表和字段都需要添加注释](#3-所有表和字段都需要添加注释) - - [4. 尽量控制单表数据量的大小,建议控制在 500 万以内。](#4-尽量控制单表数据量的大小建议控制在-500-万以内) - - [5. 谨慎使用 MySQL 分区表](#5-谨慎使用-mysql-分区表) - - [6.尽量做到冷热数据分离,减小表的宽度](#6尽量做到冷热数据分离减小表的宽度) - - [7. 禁止在表中建立预留字段](#7-禁止在表中建立预留字段) - - [8. 禁止在数据库中存储图片,文件等大的二进制数据](#8-禁止在数据库中存储图片文件等大的二进制数据) - - [9. 禁止在线上做数据库压力测试](#9-禁止在线上做数据库压力测试) - - [10. 禁止从开发环境,测试环境直接连接生成环境数据库](#10-禁止从开发环境测试环境直接连接生成环境数据库) -- [数据库字段设计规范](#数据库字段设计规范) - - [1. 优先选择符合存储需要的最小的数据类型](#1-优先选择符合存储需要的最小的数据类型) - - [2. 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据](#2-避免使用-textblob-数据类型最常见的-text-类型可以存储-64k-的数据) - - [3. 避免使用 ENUM 类型](#3-避免使用-enum-类型) - - [4. 尽可能把所有列定义为 NOT NULL](#4-尽可能把所有列定义为-not-null) - - [5. 使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间](#5-使用-timestamp4-个字节-或-datetime-类型-8-个字节-存储时间) - - [6. 同财务相关的金额类数据必须使用 decimal 类型](#6-同财务相关的金额类数据必须使用-decimal-类型) -- [索引设计规范](#索引设计规范) - - [1. 限制每张表上的索引数量,建议单张表索引不超过 5 个](#1-限制每张表上的索引数量建议单张表索引不超过-5-个) - - [2. 禁止给表中的每一列都建立单独的索引](#2-禁止给表中的每一列都建立单独的索引) - - [3. 每个 Innodb 表必须有个主键](#3-每个-innodb-表必须有个主键) - - [4. 常见索引列建议](#4-常见索引列建议) - - [5.如何选择索引列的顺序](#5如何选择索引列的顺序) - - [6. 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)](#6-避免建立冗余索引和重复索引增加了查询优化器生成执行计划的时间) - - [7. 对于频繁的查询优先考虑使用覆盖索引](#7-对于频繁的查询优先考虑使用覆盖索引) - - [8.索引 SET 规范](#8索引-set-规范) -- [数据库 SQL 开发规范](#数据库-sql-开发规范) - - [1. 建议使用预编译语句进行数据库操作](#1-建议使用预编译语句进行数据库操作) - - [2. 避免数据类型的隐式转换](#2-避免数据类型的隐式转换) - - [3. 充分利用表上已经存在的索引](#3-充分利用表上已经存在的索引) - - [4. 数据库设计时,应该要对以后扩展进行考虑](#4-数据库设计时应该要对以后扩展进行考虑) - - [5. 程序连接不同的数据库使用不同的账号,禁止跨库查询](#5-程序连接不同的数据库使用不同的账号禁止跨库查询) - - [6. 禁止使用 SELECT * 必须使用 SELECT <字段列表> 查询](#6-禁止使用-select--必须使用-select-字段列表-查询) - - [7. 禁止使用不含字段列表的 INSERT 语句](#7-禁止使用不含字段列表的-insert-语句) - - [8. 避免使用子查询,可以把子查询优化为 join 操作](#8-避免使用子查询可以把子查询优化为-join-操作) - - [9. 避免使用 JOIN 关联太多的表](#9-避免使用-join-关联太多的表) - - [10. 减少同数据库的交互次数](#10-减少同数据库的交互次数) - - [11. 对应同一列进行 or 判断时,使用 in 代替 or](#11-对应同一列进行-or-判断时使用-in-代替-or) - - [12. 禁止使用 order by rand() 进行随机排序](#12-禁止使用-order-by-rand-进行随机排序) - - [13. WHERE 从句中禁止对列进行函数转换和计算](#13-where-从句中禁止对列进行函数转换和计算) - - [14. 在明显不会有重复值时使用 UNION ALL 而不是 UNION](#14-在明显不会有重复值时使用-union-all-而不是-union) - - [15. 拆分复杂的大 SQL 为多个小 SQL](#15-拆分复杂的大-sql-为多个小-sql) -- [数据库操作行为规范](#数据库操作行为规范) - - [1. 超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作](#1-超-100-万行的批量写-updatedeleteinsert-操作要分批多次进行操作) - - [2. 对于大表使用 pt-online-schema-change 修改表结构](#2-对于大表使用-pt-online-schema-change-修改表结构) - - [3. 禁止为程序使用的账号赋予 super 权限](#3-禁止为程序使用的账号赋予-super-权限) - - [4. 对于程序连接数据库账号,遵循权限最小原则](#4-对于程序连接数据库账号遵循权限最小原则) - - +> 作者: 听风,原文地址: 。JavaGuide 已获得作者授权。 ## 数据库命令规范 @@ -76,6 +29,8 @@ Innodb 支持事务,支持行级锁,更好的恢复性,高并发下性能 兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效,如果数据库中有存储 emoji 表情的需要,字符集需要采用 utf8mb4 字符集。 +参考文章:[MySQL 字符集不一致导致索引失效的一个真实案例](https://blog.csdn.net/horses/article/details/107243447) + ### 3. 所有表和字段都需要添加注释 使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护 @@ -166,7 +121,7 @@ MySQL 内存临时表不支持 TEXT、BLOB 这样的大数据类型,如果查 **2、TEXT 或 BLOB 类型只能使用前缀索引** -因为[MySQL](http://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247487885&idx=1&sn=65b1bf5f7d4505502620179669a9c2df&chksm=ebd62ea1dca1a7b7bf884bcd9d538d78ba064ee03c09436ca8e57873b1d98a55afd6d7884cfc&scene=21#wechat_redirect) 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引,并且 TEXT 列上是不能有默认值的 +因为[MySQL](https://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247487885&idx=1&sn=65b1bf5f7d4505502620179669a9c2df&chksm=ebd62ea1dca1a7b7bf884bcd9d538d78ba064ee03c09436ca8e57873b1d98a55afd6d7884cfc&scene=21#wechat_redirect) 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引,并且 TEXT 列上是不能有默认值的 ### 3. 避免使用 ENUM 类型 @@ -264,7 +219,7 @@ Innodb 是按照主键索引的顺序来组织表的 ### 7. 对于频繁的查询优先考虑使用覆盖索引 -> 覆盖索引:就是包含了所有查询字段 (where,select,ordery by,group by 包含的字段) 的索引 +> 覆盖索引:就是包含了所有查询字段 (where,select,order by,group by 包含的字段) 的索引 **覆盖索引的好处:** diff --git a/docs/database/mysql/mysql-index.md b/docs/database/mysql/mysql-index.md new file mode 100644 index 00000000000..5dd4e526492 --- /dev/null +++ b/docs/database/mysql/mysql-index.md @@ -0,0 +1,266 @@ +--- +title: MySQL 索引详解 +category: 数据库 +tag: + - MySQL +--- + + + +## 何为索引?有什么作用? + +**索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B 树, B+树和 Hash。** + +索引的作用就相当于目录的作用。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。 + +## 索引的优缺点 + +**优点** : + +- 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。 +- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 + +**缺点** : + +- 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。 +- 索引需要使用物理文件存储,也会耗费一定空间。 + +但是,**使用索引一定能提高查询性能吗?** + +大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。 + +## 索引的底层数据结构 + +### Hash表 & B+树 + +哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。 + +**为何能够通过 key 快速取出 value呢?** 原因在于 **哈希算法**(也叫散列算法)。通过哈希算法,我们可以快速找到 value 对应的 index,找到了 index 也就找到了对应的 value。 + +```java +hash = hashfunc(key) +index = hash % array_size +``` + + + +![](https://img-blog.csdnimg.cn/20210513092328171.png) + +但是!哈希算法有个 **Hash 冲突** 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 **链地址法**。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 `HashMap` 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后`HashMap`为了减少链表过长的时候搜索时间过长引入了红黑树。 + +![](https://img-blog.csdnimg.cn/20210513092224836.png) + +为了减少 Hash 冲突的发生,一个好的哈希函数应该“均匀地”将数据分布在整个可能的哈希值集合中。 + +既然哈希表这么快,**为什么MySQL 没有使用其作为索引的数据结构呢?** + +**1.Hash 冲突问题** :我们上面也提到过Hash 冲突了,不过对于数据库来说这还不算最大的缺点。 + +**2.Hash 索引不支持顺序和范围查询(Hash 索引不支持顺序和范围查询是它最大的缺点:** 假如我们要对表中的数据进行排序或者进行范围查询,那 Hash 索引可就不行了。 + +试想一种情况: + +```java +SELECT * FROM tb1 WHERE id < 500;Copy to clipboardErrorCopied +``` + +在这种范围查询中,优势非常大,直接遍历比 500 小的叶子节点就够了。而 Hash 索引是根据 hash 算法来定位的,难不成还要把 1 - 499 的数据,每个都进行一次 hash 计算来定位吗?这就是 Hash 最大的缺点了。 + +### B 树& B+树 + +B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 `Balanced` (平衡)的意思。 + +目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。 + +**B 树& B+树两者有何异同呢?** + +- B 树的所有节点既存放键(key) 也存放 数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。 +- B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。 +- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。 + +![](https://img-blog.csdnimg.cn/20210420165409106.png) + +在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是,两者的实现方式不太一样。(下面的内容整理自《Java 工程师修炼之道》) + +MyISAM 引擎中,B+Tree 叶节点的 data 域存放的是数据记录的地址。在索引检索的时候,首先按照 B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。 + +InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”,而其余的索引都作为辅助索引,辅助索引的 data 域存储相应记录主键的值而不是地址,这也是和 MyISAM 不同的地方。在根据主索引搜索时,直接找到 key 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,在走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。 + +## 索引类型 + +### 主键索引(Primary Key) + +数据表的主键列使用的就是主键索引。 + +一张数据表有只能有一个主键,并且主键不能为 null,不能重复。 + +在 MySQL 的 InnoDB 的表中,当没有显示的指定表的主键时,InnoDB 会自动先检查表中是否有唯一索引的字段,如果有,则选择该字段为默认的主键,否则 InnoDB 将会自动创建一个 6Byte 的自增主键。 + +### 二级索引(辅助索引) + +**二级索引又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。** + +唯一索引,普通索引,前缀索引等索引属于二级索引。 + +**PS:不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。** + +1. **唯一索引(Unique Key)** :唯一索引也是一种约束。**唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。** 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。 +2. **普通索引(Index)** :**普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL。** +3. **前缀索引(Prefix)** :前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小, + 因为只取前几个字符。 +4. **全文索引(Full Text)** :全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。 + +二级索引: +![](https://img-blog.csdnimg.cn/20210420165254215.png) + +## 聚集索引与非聚集索引 + +### 聚集索引 + +**聚集索引即索引结构和数据一起存放的索引。主键索引属于聚集索引。** + +在 Mysql 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。 + +#### 聚集索引的优点 + +聚集索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。 + +#### 聚集索引的缺点 + +1. **依赖于有序的数据** :因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。 +2. **更新代价大** : 如果对索引列的数据被修改时,那么对应的索引也将会被修改, + 而且况聚集索引的叶子节点还存放着数据,修改代价肯定是较大的, + 所以对于主键索引来说,主键一般都是不可被修改的。 + +### 非聚集索引 + +**非聚集索引即索引结构和数据分开存放的索引。** + +**二级索引属于非聚集索引。** + +> MYISAM 引擎的表的.MYI 文件包含了表的索引, +> 该表的索引(B+树)的每个叶子非叶子节点存储索引, +> 叶子节点存储索引和索引对应数据的指针,指向.MYD 文件的数据。 +> +> **非聚集索引的叶子节点并不一定存放数据的指针, +> 因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。** + +#### 非聚集索引的优点 + +**更新代价比聚集索引要小** 。非聚集索引的更新代价就没有聚集索引那么大了,非聚集索引的叶子节点是不存放数据的 + +#### 非聚集索引的缺点 + +1. 跟聚集索引一样,非聚集索引也依赖于有序的数据 +2. **可能会二次查询(回表)** :这应该是非聚集索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。 + +这是 MySQL 的表的文件截图: + +![](https://img-blog.csdnimg.cn/20210420165311654.png) + +聚集索引和非聚集索引: + +![](https://img-blog.csdnimg.cn/20210420165326946.png) + +### 非聚集索引一定回表查询吗(覆盖索引)? + +**非聚集索引不一定回表查询。** + +> 试想一种情况,用户准备使用 SQL 查询用户名,而用户名字段正好建立了索引。 + +```text + SELECT name FROM table WHERE name='guang19'; +``` + +> 那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。 + +**即使是 MYISAM 也是这样,虽然 MYISAM 的主键索引确实需要回表, +因为它的主键索引的叶子节点存放的是指针。但是如果 SQL 查的就是主键呢?** + +```text +SELECT id FROM table WHERE id=1; +``` + +主键索引本身的 key 就是主键,查到返回就行了。这种情况就称之为覆盖索引了。 + +## 覆盖索引 + +如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! + +**覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了, +而无需回表查询。** + +> 如主键索引,如果一条 SQL 需要查询主键,那么正好根据主键索引就可以查到主键。 +> +> 再如普通索引,如果一条 SQL 需要查询 name,name 字段正好有索引, +> 那么直接根据这个索引就可以查到数据,也无需回表。 + +覆盖索引: +![](https://img-blog.csdnimg.cn/20210420165341868.png) + +## 创建索引的注意事项 + +**1.选择合适的字段创建索引:** + +- **不为 NULL 的字段** :索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。 +- **被频繁查询的字段** :我们创建索引的字段应该是查询操作非常频繁的字段。 +- **被作为条件查询的字段** :被作为 WHERE 条件查询的字段,应该被考虑建立索引。 +- **频繁需要排序的字段** :索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。 +- **被经常频繁用于连接的字段** :经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。 + +**2.被频繁更新的字段应该慎重建立索引。** + +虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。 +如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。 + +**3.尽可能的考虑建立联合索引而不是单列索引。** + +因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。 + +**4.注意避免冗余索引** 。 + +冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city )和(name )这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。 + +**5.考虑在字符串类型的字段上使用前缀索引代替普通索引。** + +前缀索引仅限于字符串类型,较普通索引会占用更小的空间,所以可以考虑使用前缀索引带替普通索引。 + +## 使用索引的一些建议 + +- 对于中到大型表索引都是非常有效的,但是特大型表的话维护开销会很大,不适合建索引 +- 避免 where 子句中对字段施加函数,这会造成无法命中索引。 +- 在使用 InnoDB 时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。 +- 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用 +- 在使用 limit offset 查询缓慢时,可以借助索引来提高性能 + +## MySQL 如何为表字段添加索引? + +1.添加 PRIMARY KEY(主键索引) + +```sql +ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` ) +``` + +2.添加 UNIQUE(唯一索引) + +```sqlite +ALTER TABLE `table_name` ADD UNIQUE ( `column` ) +``` + +3.添加 INDEX(普通索引) + +```sql +ALTER TABLE `table_name` ADD INDEX index_name ( `column` ) +``` + +4.添加 FULLTEXT(全文索引) + +```sql +ALTER TABLE `table_name` ADD FULLTEXT ( `column`) +``` + +5.添加多列索引 + +```sql +ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` ) +``` \ No newline at end of file diff --git a/docs/database/mysql/mysql-logs.md b/docs/database/mysql/mysql-logs.md new file mode 100644 index 00000000000..697cf30f6a1 --- /dev/null +++ b/docs/database/mysql/mysql-logs.md @@ -0,0 +1,289 @@ +--- +title: MySQL三大日志(binlog、redo log和undo log)详解 +category: 数据库 +tag: + - MySQL +--- + + + +> 本文来自公号程序猿阿星投稿,JavaGuide 对其做了补充完善。 + +## 前言 + +`MySQL` 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 `binlog`(归档日志)和事务日志 `redo log`(重做日志)和 `undo log`(回滚日志)。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03/01.png) + +今天就来聊聊 `redo log`(重做日志)、`binlog`(归档日志)、两阶段提交、`undo log` (回滚日志)。 + +## redo log + +`redo log`(重做日志)是`InnoDB`存储引擎独有的,它让`MySQL`拥有了崩溃恢复能力。 + +比如 `MySQL` 实例挂了或宕机了,重启时,`InnoDB`存储引擎会使用`redo log`恢复数据,保证数据的持久性与完整性。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03/02.png) + +`MySQL` 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 `Buffer Pool` 中。 + +后续的查询都是先从 `Buffer Pool` 中找,没有命中再去硬盘加载,减少硬盘 `IO` 开销,提升性能。 + +更新表数据的时候,也是如此,发现 `Buffer Pool` 里存在要更新的数据,就直接在 `Buffer Pool` 里更新。 + +然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(`redo log buffer`)里,接着刷盘到 `redo log` 文件里。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03/03.png) + +理想情况,事务一提交就会进行刷盘操作,但实际上,刷盘的时机是根据策略来进行的。 + +> 小贴士:每条 redo 记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成 + +### 刷盘时机 + +`InnoDB` 存储引擎为 `redo log` 的刷盘策略提供了 `innodb_flush_log_at_trx_commit` 参数,它支持三种策略: + +- **0** :设置为 0 的时候,表示每次事务提交时不进行刷盘操作 +- **1** :设置为 1 的时候,表示每次事务提交时都将进行刷盘操作(默认值) +- **2** :设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache + +`innodb_flush_log_at_trx_commit` 参数默认为 1 ,也就是说当事务提交时会调用 `fsync` 对 redo log 进行刷盘 + +另外,`InnoDB` 存储引擎有一个后台线程,每隔`1` 秒,就会把 `redo log buffer` 中的内容写到文件系统缓存(`page cache`),然后调用 `fsync` 刷盘。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03/04.png) + +也就是说,一个没有提交事务的 `redo log` 记录,也可能会刷盘。 + +**为什么呢?** + +因为在事务执行过程 `redo log` 记录是会写入`redo log buffer` 中,这些 `redo log` 记录会被后台线程刷盘。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03/05.png) + +除了后台线程每秒`1`次的轮询操作,还有一种情况,当 `redo log buffer` 占用的空间即将达到 `innodb_log_buffer_size` 一半的时候,后台线程会主动刷盘。 + +下面是不同刷盘策略的流程图。 + +#### innodb_flush_log_at_trx_commit=0 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03/06.png) + +为`0`时,如果`MySQL`挂了或宕机可能会有`1`秒数据的丢失。 + +#### innodb_flush_log_at_trx_commit=1 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03/07.png) + +为`1`时, 只要事务提交成功,`redo log`记录就一定在硬盘里,不会有任何数据丢失。 + +如果事务执行期间`MySQL`挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。 + +#### innodb_flush_log_at_trx_commit=2 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03/09.png) + +为`2`时, 只要事务提交成功,`redo log buffer`中的内容只写入文件系统缓存(`page cache`)。 + +如果仅仅只是`MySQL`挂了不会有任何数据丢失,但是宕机可能会有`1`秒数据的丢失。 + +### 日志文件组 + +硬盘上存储的 `redo log` 日志文件不只一个,而是以一个**日志文件组**的形式出现的,每个的`redo`日志文件大小都是一样的。 + +比如可以配置为一组`4`个文件,每个文件的大小是 `1GB`,整个 `redo log` 日志文件组可以记录`4G`的内容。 + +它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03/10.png) + +在个**日志文件组**中还有两个重要的属性,分别是 `write pos、checkpoint` + +- **write pos** 是当前记录的位置,一边写一边后移 +- **checkpoint** 是当前要擦除的位置,也是往后推移 + +每次刷盘 `redo log` 记录到**日志文件组**中,`write pos` 位置就会后移更新。 + +每次 `MySQL` 加载**日志文件组**恢复数据时,会清空加载过的 `redo log` 记录,并把 `checkpoint` 后移更新。 + +`write pos` 和 `checkpoint` 之间的还空着的部分可以用来写入新的 `redo log` 记录。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03/11.png) + +如果 `write pos` 追上 `checkpoint` ,表示**日志文件组**满了,这时候不能再写入新的 `redo log` 记录,`MySQL` 得停下来,清空一些记录,把 `checkpoint` 推进一下。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/03/12.png) + +### redo log 小结 + +相信大家都知道 `redo log` 的作用和它的刷盘时机、存储形式。 + +现在我们来思考一个问题: **只要每次把修改后的数据页直接刷盘不就好了,还有 `redo log` 什么事?** + +它们不都是刷盘么?差别在哪里? + +```java +1 Byte = 8bit +1 KB = 1024 Byte +1 MB = 1024 KB +1 GB = 1024 MB +1 TB = 1024 GB +``` + +实际上,数据页大小是`16KB`,刷盘比较耗时,可能就修改了数据页里的几 `Byte` 数据,有必要把完整的数据页刷盘吗? + +而且数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。 + +如果是写 `redo log`,一行记录可能就占几十 `Byte`,只包含表空间号、数据页号、磁盘文件偏移 +量、更新值,再加上是顺序写,所以刷盘速度很快。 + +所以用 `redo log` 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。 + +> 其实内存的数据页在一定时机也会刷盘,我们把这称为页合并,讲 `Buffer Pool`的时候会对这块细说 + +## binlog + +`redo log` 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 `InnoDB` 存储引擎。 + +而 `binlog` 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于`MySQL Server` 层。 + +不管用什么存储引擎,只要发生了表数据更新,都会产生 `binlog` 日志。 + +那 `binlog` 到底是用来干嘛的? + +可以说`MySQL`数据库的**数据备份、主备、主主、主从**都离不开`binlog`,需要依靠`binlog`来同步数据,保证数据一致性。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/01.png) + +`binlog`会记录所有涉及更新数据的逻辑操作,并且是顺序写。 + +### 记录格式 + +`binlog` 日志有三种格式,可以通过`binlog_format`参数指定。 + +- **statement** +- **row** +- **mixed** + +指定`statement`,记录的内容是`SQL`语句原文,比如执行一条`update T set update_time=now() where id=1`,记录的内容如下。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/02.png) + +同步数据时,会执行记录的`SQL`语句,但是有个问题,`update_time=now()`这里会获取当前系统时间,直接执行会导致与原库的数据不一致。 + +为了解决这种问题,我们需要指定为`row`,记录的内容不再是简单的`SQL`语句了,还包含操作的具体数据,记录内容如下。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/03.png) + +`row`格式记录的内容看不到详细信息,要通过`mysqlbinlog`工具解析出来。 + +`update_time=now()`变成了具体的时间`update_time=1627112756247`,条件后面的@1、@2、@3 都是该行数据第 1 个~3 个字段的原始值(**假设这张表只有 3 个字段**)。 + +这样就能保证同步数据的一致性,通常情况下都是指定为`row`,这样可以为数据库的恢复与同步带来更好的可靠性。 + +但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗`IO`资源,影响执行速度。 + +所以就有了一种折中的方案,指定为`mixed`,记录的内容是前两者的混合。 + +`MySQL`会判断这条`SQL`语句是否可能引起数据不一致,如果是,就用`row`格式,否则就用`statement`格式。 + +### 写入机制 + +`binlog`的写入时机也非常简单,事务执行过程中,先把日志写到`binlog cache`,事务提交的时候,再把`binlog cache`写到`binlog`文件中。 + +因为一个事务的`binlog`不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为`binlog cache`。 + +我们可以通过`binlog_cache_size`参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(`Swap`)。 + +`binlog`日志刷盘流程如下 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/04.png) + +- **上图的 write,是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快** +- **上图的 fsync,才是将数据持久化到磁盘的操作** + +`write`和`fsync`的时机,可以由参数`sync_binlog`控制,默认是`0`。 + +为`0`的时候,表示每次提交事务都只`write`,由系统自行判断什么时候执行`fsync`。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/05.png) + +虽然性能得到提升,但是机器宕机,`page cache`里面的 binglog 会丢失。 + +为了安全起见,可以设置为`1`,表示每次提交事务都会执行`fsync`,就如同**binlog 日志刷盘流程**一样。 + +最后还有一种折中方式,可以设置为`N(N>1)`,表示每次提交事务都`write`,但累积`N`个事务后才`fsync`。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/04/06.png) + +在出现`IO`瓶颈的场景里,将`sync_binlog`设置成一个比较大的值,可以提升性能。 + +同样的,如果机器宕机,会丢失最近`N`个事务的`binlog`日志。 + +## 两阶段提交 + +`redo log`(重做日志)让`InnoDB`存储引擎拥有了崩溃恢复能力。 + +`binlog`(归档日志)保证了`MySQL`集群架构的数据一致性。 + +虽然它们都属于持久化的保证,但是侧重点不同。 + +在执行更新语句过程,会记录`redo log`与`binlog`两块日志,以基本的事务为单位,`redo log`在事务执行过程中可以不断写入,而`binlog`只有在提交事务时才写入,所以`redo log`与`binlog`的写入时机不一样。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/01.png) + +回到正题,`redo log`与`binlog`两份日志之间的逻辑不一致,会出现什么问题? + +我们以`update`语句为例,假设`id=2`的记录,字段`c`值是`0`,把字段`c`值更新成`1`,`SQL`语句为`update T set c=1 where id=2`。 + +假设执行过程中写完`redo log`日志后,`binlog`日志写期间发生了异常,会出现什么情况呢? + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/02.png) + +由于`binlog`没写完就异常,这时候`binlog`里面没有对应的修改记录。因此,之后用`binlog`日志恢复数据时,就会少这一次更新,恢复出来的这一行`c`值是`0`,而原库因为`redo log`日志恢复,这一行`c`值是`1`,最终数据不一致。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/03.png) + +为了解决两份日志之间的逻辑一致问题,`InnoDB`存储引擎使用**两阶段提交**方案。 + +原理很简单,将`redo log`的写入拆成了两个步骤`prepare`和`commit`,这就是**两阶段提交**。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/04.png) + +使用**两阶段提交**后,写入`binlog`时发生异常也不会有影响,因为`MySQL`根据`redo log`日志恢复数据时,发现`redo log`还处于`prepare`阶段,并且没有对应`binlog`日志,就会回滚该事务。 + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/05.png) + +再看一个场景,`redo log`设置`commit`阶段发生异常,那会不会回滚事务呢? + +![](https://cdn.jsdelivr.net/gh/18702524676/CND5/image/mysql/05/06.png) + +并不会回滚事务,它会执行上图框住的逻辑,虽然`redo log`是处于`prepare`阶段,但是能通过事务`id`找到对应的`binlog`日志,所以`MySQL`认为是完整的,就会提交事务恢复数据。 + +## undo log + +> 这部分内容为 JavaGuide 的补充: + +我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行**回滚**,在 MySQL 中,恢复机制是通过 **回滚日志(undo log)** 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 **回滚日志** 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。 + +另外,`MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,`InnoDB` 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 `undo log` 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改 + +## 总结 + +> 这部分内容为 JavaGuide 的补充: + +MySQL InnoDB 引擎使用 **redo log(重做日志)** 保证事务的**持久性**,使用 **undo log(回滚日志)** 来保证事务的**原子性**。 + +`MySQL`数据库的**数据备份、主备、主主、主从**都离不开`binlog`,需要依靠`binlog`来同步数据,保证数据一致性。 + +## 站在巨人的肩膀上 + +- 《MySQL 实战 45 讲》 +- 《从零开始带你成为 MySQL 实战优化高手》 +- 《MySQL 是怎样运行的:从根儿上理解 MySQL》 +- 《MySQL 技术 Innodb 存储引擎》 + +## MySQL 好文推荐 + +- [CURD 这么多年,你有了解过 MySQL 的架构设计吗?](https://mp.weixin.qq.com/s/R-1km7r0z3oWfwYQV8iiqA) +- [浅谈 MySQL InnoDB 的内存组件](https://mp.weixin.qq.com/s/7Kab4IQsNcU_bZdbv_MuOg) diff --git "a/docs/database/mysql/mysql\347\237\245\350\257\206\347\202\271&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" "b/docs/database/mysql/mysql\347\237\245\350\257\206\347\202\271&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" new file mode 100644 index 00000000000..b07247b20f5 --- /dev/null +++ "b/docs/database/mysql/mysql\347\237\245\350\257\206\347\202\271&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" @@ -0,0 +1,292 @@ +--- +title: MySQL知识点&面试题总结 +category: 数据库 +tag: + - MySQL + - 大厂面试 +--- + + +## MySQL 基础 + +### 关系型数据库介绍 + +顾名思义,关系型数据库就是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。 + +关系型数据库中,我们的数据都被存放在了各种表中(比如用户表),表中的每一行就存放着一条数据(比如一个用户的信息)。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/5e3c1a71724a38245aa43b02_99bf70d46cc247be878de9d3a88f0c44.png) + +大部分关系型数据库都使用 SQL 来操作数据库中的数据。并且,大部分关系型数据库都支持事务的四大特性(ACID)。 + +**有哪些常见的关系型数据库呢?** + +MySQL、PostgreSQL、Oracle、SQL Server、SQLite(微信本地的聊天记录的存储就是用的 SQLite) ......。 + +### MySQL 介绍 + +![](https://img-blog.csdnimg.cn/20210327143351823.png) + +**MySQL 是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户信息。** + +由于 MySQL 是开源免费并且比较成熟的数据库,因此,MySQL 被大量使用在各种系统中。任何人都可以在 GPL(General Public License) 的许可下下载并根据个性化的需要对其进行修改。MySQL 的默认端口号是**3306**。 + +## 存储引擎 + +### 存储引擎相关的命令 + +**查看 MySQL 提供的所有存储引擎** + +```sql +mysql> show engines; +``` + +![查看MySQL提供的所有存储引擎](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/mysql-engines.png) + +从上图我们可以查看出 MySQL 当前默认的存储引擎是 InnoDB,并且在 5.7 版本所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。 + +**查看 MySQL 当前默认的存储引擎** + +我们也可以通过下面的命令查看默认的存储引擎。 + +```sql +mysql> show variables like '%storage_engine%'; +``` + +**查看表的存储引擎** + +```sql +show table status like "table_name" ; +``` + +![查看表的存储引擎](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/查看表的存储引擎.png) + +### MyISAM 和 InnoDB 的区别 + +![](https://img-blog.csdnimg.cn/20210327145248960.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70) + +MySQL 5.5 之前,MyISAM 引擎是 MySQL 的默认存储引擎,可谓是风光一时。 + +虽然,MyISAM 的性能还行,各种特性也还不错(比如全文索引、压缩、空间函数等)。但是,MyISAM 不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。 + +5.5 版本之后,MySQL 引入了 InnoDB(事务性数据库引擎),MySQL 5.5 版本后默认的存储引擎为 InnoDB。小伙子,一定要记好这个 InnoDB ,你每次使用 MySQL 数据库都是用的这个存储引擎吧? + +言归正传!咱们下面还是来简单对比一下两者: + +**1.是否支持行级锁** + +MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。 + +也就说,MyISAM 一锁就是锁住了整张表,这在并发写的情况下是多么滴憨憨啊!这也是为什么 InnoDB 在并发写的时候,性能更牛皮了! + +**2.是否支持事务** + +MyISAM 不提供事务支持。 + +InnoDB 提供事务支持,具有提交(commit)和回滚(rollback)事务的能力。 + +**3.是否支持外键** + +MyISAM 不支持,而 InnoDB 支持。 + +🌈 拓展一下: + +一般我们也是不建议在数据库层面使用外键的,应用层面可以解决。不过,这样会对数据的一致性造成威胁。具体要不要使用外键还是要根据你的项目来决定。 + +**4.是否支持数据库异常崩溃后的安全恢复** + +MyISAM 不支持,而 InnoDB 支持。 + +使用 InnoDB 的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于 `redo log` 。 + +🌈 拓展一下: + +- MySQL InnoDB 引擎使用 **redo log(重做日志)** 保证事务的**持久性**,使用 **undo log(回滚日志)** 来保证事务的**原子性**。 +- MySQL InnoDB 引擎通过 **锁机制**、**MVCC** 等手段来保证事务的隔离性( 默认支持的隔离级别是 **`REPEATABLE-READ`** )。 +- 保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。 + +**5.是否支持 MVCC** + +MyISAM 不支持,而 InnoDB 支持。 + +讲真,这个对比有点废话,毕竟 MyISAM 连行级锁都不支持。 + +MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提供性能。 + +### 关于 MyISAM 和 InnoDB 的选择问题 + +大多数时候我们使用的都是 InnoDB 存储引擎,在某些读密集的情况下,使用 MyISAM 也是合适的。不过,前提是你的项目不介意 MyISAM 不支持事务、崩溃恢复等缺点(可是~我们一般都会介意啊!)。 + +《MySQL 高性能》上面有一句话这样写到: + +> 不要轻易相信“MyISAM 比 InnoDB 快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB 的速度都可以让 MyISAM 望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。 + +一般情况下我们选择 InnoDB 都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择 MyISAM 也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。 + +因此,对于咱们日常开发的业务系统来说,你几乎找不到什么理由再使用 MyISAM 作为自己的 MySQL 数据库的存储引擎。 + +## 锁机制与 InnoDB 锁算法 + +**MyISAM 和 InnoDB 存储引擎使用的锁:** + +- MyISAM 采用表级锁(table-level locking)。 +- InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁 + +**表级锁和行级锁对比:** + +- **表级锁:** MySQL 中锁定 **粒度最大** 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。 +- **行级锁:** MySQL 中锁定 **粒度最小** 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 + +**InnoDB 存储引擎的锁的算法有三种:** + +- Record lock:记录锁,单个行记录上的锁 +- Gap lock:间隙锁,锁定一个范围,不包括记录本身 +- Next-key lock:record+gap 临键锁,锁定一个范围,包含记录本身 + +## 查询缓存 + +执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用 + +`my.cnf` 加入以下配置,重启 MySQL 开启查询缓存 + +```properties +query_cache_type=1 +query_cache_size=600000 +``` + +MySQL 执行以下命令也可以开启查询缓存 + +```properties +set global query_cache_type=1; +set global query_cache_size=600000; +``` + +如上,**开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果**。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。(**查询缓存不命中的情况:(1)**)因此任何两个查询在任何字符上的不同都会导致缓存不命中。此外,(**查询缓存不命中的情况:(2)**)如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表,其查询结果也不会被缓存。 + +(**查询缓存不命中的情况:(3)**)**缓存建立之后**,MySQL 的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。 + +**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启查询缓存要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十 MB 比较合适。此外,**还可以通过 sql_cache 和 sql_no_cache 来控制某个查询语句是否需要缓存:** + +```sql +select sql_no_cache count(*) from usr; +``` + +## 事务 + +### 何为事务? + +一言蔽之,**事务是逻辑上的一组操作,要么都执行,要么都不执行。** + +**可以简单举一个例子不?** + +事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作就是: + +1. 将小明的余额减少 1000 元 +2. 将小红的余额增加 1000 元。 + +事务会把这两个操作就可以看成逻辑上的一个整体,这个整体包含的操作要么都成功,要么都要失败。 + +这样就不会出现小明余额减少而小红的余额却并没有增加的情况。 + +### 何为数据库事务? + +数据库事务在我们日常开发中接触的最多了。如果你的项目属于单体架构的话,你接触到的往往就是数据库事务了。 + +平时,我们在谈论事务的时候,如果没有特指**分布式事务**,往往指的就是**数据库事务**。 + +**那数据库事务有什么作用呢?** + +简单来说:数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循:**要么全部执行成功,要么全部不执行** 。 + +```sql +# 开启一个事务 +START TRANSACTION; +# 多条 SQL 语句 +SQL1,SQL2... +## 提交事务 +COMMIT; +``` + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12/640-20201207160554677.png) + +另外,关系型数据库(例如:`MySQL`、`SQL Server`、`Oracle` 等)事务都有 **ACID** 特性: + +![事务的特性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/%E4%BA%8B%E5%8A%A1%E7%89%B9%E6%80%A7.png) + +### 何为 ACID 特性呢? + +1. **原子性**(`Atomicity`) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; +2. **一致性**(`Consistency`): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的; +3. **隔离性**(`Isolation`): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; +4. **持久性**(`Durability`): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 + +**数据事务的实现原理呢?** + +我们这里以 MySQL 的 InnoDB 引擎为例来简单说一下。 + +MySQL InnoDB 引擎使用 **redo log(重做日志)** 保证事务的**持久性**,使用 **undo log(回滚日志)** 来保证事务的**原子性**。 + +MySQL InnoDB 引擎通过 **锁机制**、**MVCC** 等手段来保证事务的隔离性( 默认支持的隔离级别是 **`REPEATABLE-READ`** )。 + +保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。 + +### 并发事务带来哪些问题? + +在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。 + +- **脏读(Dirty read):** 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。 +- **丢失修改(Lost to modify):** 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。 +- **不可重复读(Unrepeatable read):** 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。 +- **幻读(Phantom read):** 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。 + +**不可重复读和幻读区别:** + +不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。 + +### 事务隔离级别有哪些? + +SQL 标准定义了四个隔离级别: + +- **READ-UNCOMMITTED(读取未提交):** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读**。 +- **READ-COMMITTED(读取已提交):** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生**。 +- **REPEATABLE-READ(可重复读):** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生**。 +- **SERIALIZABLE(可串行化):** 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。 + +--- + +| 隔离级别 | 脏读 | 不可重复读 | 幻读 | +| :--------------: | :--: | :--------: | :--: | +| READ-UNCOMMITTED | √ | √ | √ | +| READ-COMMITTED | × | √ | √ | +| REPEATABLE-READ | × | × | √ | +| SERIALIZABLE | × | × | × | + +### MySQL 的默认隔离级别是什么? + +MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;` + +```sql +mysql> SELECT @@tx_isolation; ++-----------------+ +| @@tx_isolation | ++-----------------+ +| REPEATABLE-READ | ++-----------------+ +``` + +~~这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 **REPEATABLE-READ(可重读)** 事务隔离级别下使用的是 Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说 InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)** 已经可以完全保证事务的隔离性要求,即达到了 SQL 标准的 **SERIALIZABLE(可串行化)** 隔离级别。~~ + +🐛 问题更正:**MySQL InnoDB 的 REPEATABLE-READ(可重读)并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是 Next-Key Locks。** + +因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 **READ-COMMITTED(读取提交内容)** ,但是你要知道的是 InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)** 并不会有任何性能损失。 + +InnoDB 存储引擎在 **分布式事务** 的情况下一般会用到 **SERIALIZABLE(可串行化)** 隔离级别。 + +🌈 拓展一下(以下内容摘自《MySQL 技术内幕:InnoDB 存储引擎(第 2 版)》7.7 章): + +> InnoDB 存储引擎提供了对 XA 事务的支持,并通过 XA 事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的 ACID 要求又有了提高。另外,在使用分布式事务时,InnoDB 存储引擎的事务隔离级别必须设置为 SERIALIZABLE。 + +## 参考 + +- 《高性能 MySQL》 +- https://www.omnisci.com/technical-glossary/relational-database diff --git "a/docs/database/\345\205\263\344\272\216\346\225\260\346\215\256\345\272\223\345\255\230\345\202\250\346\227\266\351\227\264\347\232\204\344\270\200\347\202\271\346\200\235\350\200\203.md" b/docs/database/mysql/some-thoughts-on-database-storage-time.md similarity index 88% rename from "docs/database/\345\205\263\344\272\216\346\225\260\346\215\256\345\272\223\345\255\230\345\202\250\346\227\266\351\227\264\347\232\204\344\270\200\347\202\271\346\200\235\350\200\203.md" rename to docs/database/mysql/some-thoughts-on-database-storage-time.md index ab4e62c6a9f..ccad646e5d4 100644 --- "a/docs/database/\345\205\263\344\272\216\346\225\260\346\215\256\345\272\223\345\255\230\345\202\250\346\227\266\351\227\264\347\232\204\344\270\200\347\202\271\346\200\235\350\200\203.md" +++ b/docs/database/mysql/some-thoughts-on-database-storage-time.md @@ -1,4 +1,13 @@ -我们平时开发中不可避免的就是要存储时间,比如我们要记录操作表中这条记录的时间、记录转账的交易时间、记录出发时间等等。你会发现这个时间这个东西与我们开发的联系还是非常紧密的,用的好与不好会给我们的业务甚至功能带来很大的影响。所以,我们有必要重新出发,好好认识一下这个东西。 +--- +title: 关于数据库中如何存储时间的一点思考 +category: 数据库 +tag: + - MySQL +--- + + + +我们平时开发中不可避免的就是要存储时间,比如我们要记录操作表中这条记录的时间、记录转账的交易时间、记录出发时间等等。你会发现时间这个东西与我们开发的联系还是非常紧密的,用的好与不好会给我们的业务甚至功能带来很大的影响。所以,我们有必要重新出发,好好认识一下这个东西。 这是一篇短小精悍的文章,仔细阅读一定能学到不少东西! @@ -9,7 +18,7 @@ 但是,这是不正确的做法,主要会有下面两个问题: 1. 字符串占用的空间更大! -2. 字符串存储的日期比较效率比较低(逐个字符进行比对),无法用日期相关的 API 进行计算和比较。 +2. 字符串存储的日期效率比较低(逐个字符进行比对),无法用日期相关的 API 进行计算和比较。 ### 2.Datetime 和 Timestamp 之间抉择 @@ -17,7 +26,7 @@ Datetime 和 Timestamp 是 MySQL 提供的两种比较相似的保存时间的 **通常我们都会首选 Timestamp。** 下面说一下为什么这样做! -#### 2.1 DateTime 类型没有时区信息的 +#### 2.1 DateTime 类型没有时区信息 **DateTime 类型是没有时区信息的(时区无关)** ,DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。这样就会有什么问题呢?当你的时区更换之后,比如你的服务器更换地址或者更换客户端连接时区设置的话,就会导致你从数据库中读出的时间错误。不要小看这个问题,很多系统就是因为这个问题闹出了很多笑话。 @@ -106,7 +115,7 @@ Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗 ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/FhRGUVHFK0ujRPNA75f6CuOXQHTE.jpeg) -可以看出 5.6.4 之后的 MySQL 多出了一个需要 0 ~ 3 字节的小数位。Datatime 和 Timestamp 会有几种不同的存储空间占用。 +可以看出 5.6.4 之后的 MySQL 多出了一个需要 0 ~ 3 字节的小数位。DateTime 和 Timestamp 会有几种不同的存储空间占用。 为了方便,本文我们还是默认 Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗费 8 个字节的存储空间。 @@ -150,11 +159,4 @@ MySQL 中时间到底怎么存储才好?Datetime?Timestamp? 数值保存的时 每种方式都有各自的优势,根据实际场景才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型: - - -如果还有什么问题欢迎给我留言!如果文章有什么问题的话,也劳烦指出,Guide 哥感激不尽! - -后面的文章我会介绍: - -- [ ] Java8 对日期的支持以及为啥不能用 SimpleDateFormat。 -- [ ] SpringBoot 中如何实际使用(JPA 为例) \ No newline at end of file + \ No newline at end of file diff --git "a/docs/database/\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253(\345\233\276\346\226\207\350\257\246\350\247\243).md" b/docs/database/mysql/transaction-isolation-level.md similarity index 81% rename from "docs/database/\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253(\345\233\276\346\226\207\350\257\246\350\247\243).md" rename to docs/database/mysql/transaction-isolation-level.md index eb60ac8b98c..4be2f5fde44 100644 --- "a/docs/database/\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253(\345\233\276\346\226\207\350\257\246\350\247\243).md" +++ b/docs/database/mysql/transaction-isolation-level.md @@ -1,20 +1,12 @@ +--- +title: 事务隔离级别(图文详解) +category: 数据库 +tag: + - MySQL +--- + + > 本文由 [SnailClimb](https://github.com/Snailclimb) 和 [guang19](https://github.com/guang19) 共同完成。 - - -- [事务隔离级别(图文详解)](#事务隔离级别图文详解) - - [什么是事务?](#什么是事务) - - [事务的特性(ACID)](#事务的特性acid) - - [并发事务带来的问题](#并发事务带来的问题) - - [事务隔离级别](#事务隔离级别) - - [实际情况演示](#实际情况演示) - - [脏读(读未提交)](#脏读读未提交) - - [避免脏读(读已提交)](#避免脏读读已提交) - - [不可重复读](#不可重复读) - - [可重复读](#可重复读) - - [防止幻读(可重复读)](#防止幻读可重复读) - - [参考](#参考) - - ## 事务隔离级别(图文详解) @@ -69,7 +61,7 @@ | REPEATABLE-READ | × | × | √ | | SERIALIZABLE | × | × | × | -MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;` +MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;` ```sql mysql> SELECT @@tx_isolation; @@ -80,11 +72,17 @@ mysql> SELECT @@tx_isolation; +-----------------+ ``` -这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 **REPEATABLE-READ(可重读)**事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)** 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的**SERIALIZABLE(可串行化)**隔离级别。 +~~这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 **REPEATABLE-READ(可重读)** 事务隔离级别下使用的是 Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说 InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)** 已经可以完全保证事务的隔离性要求,即达到了 SQL 标准的 **SERIALIZABLE(可串行化)** 隔离级别。~~ + +🐛 问题更正:**MySQL InnoDB 的 REPEATABLE-READ(可重读)并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是 Next-Key Locks。** + +因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 **READ-COMMITTED(读取提交内容)** ,但是你要知道的是 InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)** 并不会有任何性能损失。 + +InnoDB 存储引擎在 **分布式事务** 的情况下一般会用到 **SERIALIZABLE(可串行化)** 隔离级别。 -因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是**READ-COMMITTED(读取提交内容):**,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。 +🌈 拓展一下(以下内容摘自《MySQL 技术内幕:InnoDB 存储引擎(第 2 版)》7.7 章): -InnoDB 存储引擎在 **分布式事务** 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。 +> InnoDB 存储引擎提供了对 XA 事务的支持,并通过 XA 事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的 ACID 要求又有了提高。另外,在使用分布式事务时,InnoDB 存储引擎的事务隔离级别必须设置为 SERIALIZABLE。 ### 实际情况演示 diff --git a/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md b/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md new file mode 100644 index 00000000000..841067861f2 --- /dev/null +++ b/docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md @@ -0,0 +1,116 @@ +--- +title: 3种常用的缓存读写策略 +category: 数据库 +tag: + - Redis +--- + + +看到很多小伙伴简历上写了“**熟练使用缓存**”,但是被我问到“**缓存常用的3种读写策略**”的时候却一脸懵逼。 + +在我看来,造成这个问题的原因是我们在学习 Redis 的时候,可能只是简单了写一些 Demo,并没有去关注缓存的读写策略,或者说压根不知道这回事。 + +但是,搞懂3种常见的缓存读写策略对于实际工作中使用缓存以及面试中被问到缓存都是非常有帮助的! + +下面我会简单介绍一下自己对于这 3 种缓存读写策略的理解。 + +另外,**这3 种缓存读写策略各有优劣,不存在最佳,需要我们根据具体的业务场景选择更适合的。** + +*个人能力有限。如果文章有任何需要补充/完善/修改的地方,欢迎在评论区指出,共同进步!——爱你们的 Guide 哥* + +### Cache Aside Pattern(旁路缓存模式) + +**Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。** + +Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准。 + +下面我们来看一下这个策略模式下的缓存读写步骤。 + +**写** : + +- 先更新 DB +- 然后直接删除 cache 。 + +简单画了一张图帮助大家理解写的步骤。 + +![](https://img-blog.csdnimg.cn/img_convert/5687fe759a1dac9ed9554d27e3a23b6d.png) + +**读** : + +- 从 cache 中读取数据,读取到就直接返回 +- cache中读取不到的话,就从 DB 中读取数据返回 +- 再把数据放到 cache 中。 + +简单画了一张图帮助大家理解读的步骤。 + +![](https://img-blog.csdnimg.cn/img_convert/a8c18b5f5b1aed03234bcbbd8c173a87.png) + + +你仅仅了解了上面这些内容的话是远远不够的,我们还要搞懂其中的原理。 + +比如说面试官很可能会追问:“**在写数据的过程中,可以先删除 cache ,后更新 DB 么?**” + +**答案:** 那肯定是不行的!因为这样可能会造成**数据库(DB)和缓存(Cache)数据不一致**的问题。为什么呢?比如说请求1 先写数据A,请求2随后读数据A的话就很有可能产生数据不一致性的问题。这个过程可以简单描述为: + +> 请求1先把cache中的A数据删除 -> 请求2从DB中读取数据->请求1再把DB中的A数据更新。 + +当你这样回答之后,面试官可能会紧接着就追问:“**在写数据的过程中,先更新DB,后删除cache就没有问题了么?**” + +**答案:** 理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多! + +比如请求1先读数据 A,请求2随后写数据A,并且数据A不在缓存中的话也有可能产生数据不一致性的问题。这个过程可以简单描述为: + +> 请求1从DB读数据A->请求2写更新数据 A 到数据库并把删除cache中的A数据->请求1将数据A写入cache。 + +现在我们再来分析一下 **Cache Aside Pattern 的缺陷**。 + +**缺陷1:首次请求数据一定不在 cache 的问题** + +解决办法:可以将热点数据可以提前放入cache 中。 + +**缺陷2:写操作比较频繁的话导致cache中的数据会被频繁被删除,这样会影响缓存命中率 。** + +解决办法: + +- 数据库和缓存数据强一致场景 :更新DB的时候同样更新cache,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。 +- 可以短暂地允许数据库和缓存数据不一致的场景 :更新DB的时候同样更新cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。 + +### Read/Write Through Pattern(读写穿透) + +Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。 + +这种缓存读写策略小伙伴们应该也发现了在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入DB的功能。 + +**写(Write Through):** + +- 先查 cache,cache 中不存在,直接更新 DB。 +- cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(**同步更新 cache 和 DB**)。 + +简单画了一张图帮助大家理解写的步骤。 + +![](https://img-blog.csdnimg.cn/20210201100340808.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70) + +**读(Read Through):** + +- 从 cache 中读取数据,读取到就直接返回 。 +- 读取不到的话,先从 DB 加载,写入到 cache 后返回响应。 + +简单画了一张图帮助大家理解读的步骤。 + +![](https://img-blog.csdnimg.cn/img_convert/9ada757c78614934aca11306f334638d.png) + +Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。 + +和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。 + +### Write Behind Pattern(异步缓存写入) + +Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。 + +但是,两个又有很大的不同:**Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。** + +很明显,这种方式对数据一致性带来了更大的挑战,比如cache数据可能还没异步更新DB的话,cache服务可能就就挂掉了。 + +这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 InnoDB Buffer Pool 机制都用到了这种策略。 + +Write Behind Pattern 下 DB 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。 diff --git a/docs/database/redis/images/redis-all/redis-list.drawio b/docs/database/redis/images/redis-all/redis-list.drawio new file mode 100644 index 00000000000..afa767154b7 --- /dev/null +++ b/docs/database/redis/images/redis-all/redis-list.drawio @@ -0,0 +1 @@ +7VlNc5swFPw1PiaDENjmmNjpx6Gdjt1J2lNHAzKoEYgRcmzn11cYyRgJT1w3DnScU3grIaHd5b2HM4CTdP2Rozz5wiJMB64TrQdwOnBdEEAo/5TIpkICx6mAmJNITaqBOXnGCtTTliTCRWOiYIwKkjfBkGUZDkUDQ5yzVXPagtHmrjmKsQXMQ0Rt9IFEIqnQsTuq8U+YxIneGQyDaiRFerI6SZGgiK32IHg3gBPOmKiu0vUE05I8zUt134cDo7sH4zgTx9wwu/+V/H4g/myezsD35/vZ56/ulVrlCdGlOrB6WLHRDHC2zCJcLgIG8HaVEIHnOQrL0ZXUXGKJSKkaLh6xCBMVLFgmlKJgJGO1F+YCrw8eAuyokZ7CLMWCb+QUdcOVp9hUdnKhile1OL62WLInzEhhSPkh3i1dUyYvFGt/waBrMbi9BD3lUfPm2bwFLbT556INttPWc/sB035d0+i10wj7TaM77hmNw5fTIM6im7KeyCikqChI2ORMHp1vfsjA0cHPMrh2fR1P1/uj042KjiAbR1aRMqiWVRHxGIuXUr0tSSNxHqacY4oEeWo+RpsOaodvjMgHrPM2gKbk4+YaBVvyEKvb9suYuZLpHdcwRUWEtdDWF7tzn26V0f9ula4s4A0N4cbOtX+aB3zfWupNPaCb01cxAXjPF0cofGq68AJjIT8wXXdusxzRY/fbLJ0VDbPbOj1j2PXnrVOG/Z3A82WRWFaQXZUwujHB2SOeMMq4RDKW4VJLQqkBIUrirHSQVA9L/Lbs0Yj8lL1RAymJonKb1vavbhCP9M0/dYA+OFDF95zltTjLLPav1gECu5PmOcvf5aloD7qWx7fkoRf8+siiYggEuhbI/oSil/v+2Pp0/gLZ3y18u20/BTqDJp5O8VoSp2tJxvYrgxcXpIjV351PERnWv7RXPV39/wp49wc= \ No newline at end of file diff --git a/docs/database/redis/images/redis-all/redis-list.png b/docs/database/redis/images/redis-all/redis-list.png new file mode 100644 index 00000000000..4fb4e36cb49 Binary files /dev/null and b/docs/database/redis/images/redis-all/redis-list.png differ diff --git a/docs/database/redis/images/redis-all/redis-rollBack.png b/docs/database/redis/images/redis-all/redis-rollBack.png new file mode 100644 index 00000000000..91f7f46d66d Binary files /dev/null and b/docs/database/redis/images/redis-all/redis-rollBack.png differ diff --git a/docs/database/redis/images/redis-all/redis-vs-memcached.png b/docs/database/redis/images/redis-all/redis-vs-memcached.png new file mode 100644 index 00000000000..23844d67e6f Binary files /dev/null and b/docs/database/redis/images/redis-all/redis-vs-memcached.png differ diff --git a/docs/database/redis/images/redis-all/redis4.0-more-thread.png b/docs/database/redis/images/redis-all/redis4.0-more-thread.png new file mode 100644 index 00000000000..e7e19e52e17 Binary files /dev/null and b/docs/database/redis/images/redis-all/redis4.0-more-thread.png differ diff --git "a/docs/database/redis/images/redis-all/redis\344\272\213\344\273\266\345\244\204\347\220\206\345\231\250.png" "b/docs/database/redis/images/redis-all/redis\344\272\213\344\273\266\345\244\204\347\220\206\345\231\250.png" new file mode 100644 index 00000000000..fc280fffaba Binary files /dev/null and "b/docs/database/redis/images/redis-all/redis\344\272\213\344\273\266\345\244\204\347\220\206\345\231\250.png" differ diff --git "a/docs/database/redis/images/redis-all/redis\344\272\213\345\212\241.png" "b/docs/database/redis/images/redis-all/redis\344\272\213\345\212\241.png" new file mode 100644 index 00000000000..eb0c404cafd Binary files /dev/null and "b/docs/database/redis/images/redis-all/redis\344\272\213\345\212\241.png" differ diff --git "a/docs/database/redis/images/redis-all/redis\350\277\207\346\234\237\346\227\266\351\227\264.png" "b/docs/database/redis/images/redis-all/redis\350\277\207\346\234\237\346\227\266\351\227\264.png" new file mode 100644 index 00000000000..27df6ead8e4 Binary files /dev/null and "b/docs/database/redis/images/redis-all/redis\350\277\207\346\234\237\346\227\266\351\227\264.png" differ diff --git a/docs/database/redis/images/redis-all/try-redis.png b/docs/database/redis/images/redis-all/try-redis.png new file mode 100644 index 00000000000..cd21a6518e4 Binary files /dev/null and b/docs/database/redis/images/redis-all/try-redis.png differ diff --git a/docs/database/redis/images/redis-all/what-is-redis.png b/docs/database/redis/images/redis-all/what-is-redis.png new file mode 100644 index 00000000000..913881ac6cf Binary files /dev/null and b/docs/database/redis/images/redis-all/what-is-redis.png differ diff --git "a/docs/database/redis/images/redis-all/\344\275\277\347\224\250\347\274\223\345\255\230\344\271\213\345\220\216.png" "b/docs/database/redis/images/redis-all/\344\275\277\347\224\250\347\274\223\345\255\230\344\271\213\345\220\216.png" new file mode 100644 index 00000000000..2c73bd90276 Binary files /dev/null and "b/docs/database/redis/images/redis-all/\344\275\277\347\224\250\347\274\223\345\255\230\344\271\213\345\220\216.png" differ diff --git "a/docs/database/redis/images/redis-all/\345\212\240\345\205\245\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250\345\220\216\347\232\204\347\274\223\345\255\230\345\244\204\347\220\206\346\265\201\347\250\213.png" "b/docs/database/redis/images/redis-all/\345\212\240\345\205\245\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250\345\220\216\347\232\204\347\274\223\345\255\230\345\244\204\347\220\206\346\265\201\347\250\213.png" new file mode 100644 index 00000000000..a2c2ed6906f Binary files /dev/null and "b/docs/database/redis/images/redis-all/\345\212\240\345\205\245\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250\345\220\216\347\232\204\347\274\223\345\255\230\345\244\204\347\220\206\346\265\201\347\250\213.png" differ diff --git "a/docs/database/redis/images/redis-all/\345\215\225\344\275\223\346\236\266\346\236\204.png" "b/docs/database/redis/images/redis-all/\345\215\225\344\275\223\346\236\266\346\236\204.png" new file mode 100644 index 00000000000..648a404af8c Binary files /dev/null and "b/docs/database/redis/images/redis-all/\345\215\225\344\275\223\346\236\266\346\236\204.png" differ diff --git "a/docs/database/redis/images/redis-all/\347\274\223\345\255\230\347\232\204\345\244\204\347\220\206\346\265\201\347\250\213.png" "b/docs/database/redis/images/redis-all/\347\274\223\345\255\230\347\232\204\345\244\204\347\220\206\346\265\201\347\250\213.png" new file mode 100644 index 00000000000..11860ae1f02 Binary files /dev/null and "b/docs/database/redis/images/redis-all/\347\274\223\345\255\230\347\232\204\345\244\204\347\220\206\346\265\201\347\250\213.png" differ diff --git "a/docs/database/redis/images/redis-all/\347\274\223\345\255\230\347\251\277\351\200\217\346\203\205\345\206\265.png" "b/docs/database/redis/images/redis-all/\347\274\223\345\255\230\347\251\277\351\200\217\346\203\205\345\206\265.png" new file mode 100644 index 00000000000..e7298c15ed6 Binary files /dev/null and "b/docs/database/redis/images/redis-all/\347\274\223\345\255\230\347\251\277\351\200\217\346\203\205\345\206\265.png" differ diff --git "a/docs/database/redis/images/redis-all/\351\233\206\344\270\255\345\274\217\347\274\223\345\255\230\346\236\266\346\236\204.png" "b/docs/database/redis/images/redis-all/\351\233\206\344\270\255\345\274\217\347\274\223\345\255\230\346\236\266\346\236\204.png" new file mode 100644 index 00000000000..5aff414baa4 Binary files /dev/null and "b/docs/database/redis/images/redis-all/\351\233\206\344\270\255\345\274\217\347\274\223\345\255\230\346\236\266\346\236\204.png" differ diff --git "a/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.drawio" "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.drawio" new file mode 100644 index 00000000000..bc4c6d0cca7 --- /dev/null +++ "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.drawio" @@ -0,0 +1 @@ +7Vpbc+o2EP41ekzHl/j2iAmk02nmdA6dpn3qCFsYTWSLY0Qg/fWVbMnYluE4B4jTQh4Se6WVVt+32pU2BvY43T3mcLV8ojEiwDLiHbAfgGWZluPzP0LyVkoCwygFSY5j2WkvmOF/kBSqbhsco3WjI6OUMLxqCiOaZShiDRnMc7ptdltQ0px1BROkCWYRJLr0GcdsWUp9y9vLf0Y4WaqZTTcoW1KoOsuVrJcwptuayJ4Ae5xTysqndDdGRICncCn1pgdaK8NylLE+Ck/Pf/xthHPraf7NePliLeLZ1+c7OcorJBu5YGkse1MI5HSTxUgMYgI73C4xQ7MVjETrlnPOZUuWEtm8wISMKaF5oWsvFgsrirh8zXL6gmotsTt3HVe0KFiEekRTzLs/GPw5IXC9ls/SSpQztDu4fLMClXsjoili+RvvIhVs5VHSEX3fKd+3e1pNxdWyRqkrZVB6UlINvQebP0i834G9db3YW0Njb2vYx5BBDX++ZNYEuQlmRjN0BHlIcJJxWcRxQrwxFChiHlxGsiHFcSzm6qR2T75RTaxiUME3zZgMmFbQIq1O5jkIvG8RqN5rBLod/NmX4u9e4y8TVN3468dflYWH4s+5mtjnGJ8t77jXi/3gece75Z13EWi3845OYFfcUjyfnT//lndO4W/wvBNcTexzg8+Wd9Ql4BrBHzzxmPpN/5Z5jjDoWZ/sxmPq5YJb6nkHgYOnHlOvOfxfw5/vf7rcoxcMrgb84XOPftu/5Z4jDAbtcunguUcvGdxyzzsIHD736HUHjTyUxSPxDzNBgIBGANKIeC38mgjbXIJ2mP0pe4vnvwSiPzny7WEnAS5e3hpoo1j7L1wLa24q3eQROrbIbk5qmDsdmCtZjghk+LVpRhcRcobfKOYGHiw1BSrjqCFK86XWns4eA7VqGQzmCWLaQIVfVMs+wVX0Esd/zFWGcgG/vevbzPV1gXYG/3AX6FElublAF3Nu+9Lxo1GgXUDQBrqwC6h1NFzAJUV2t/bK7reN+HwhBJN7EPrAD8DEAf4U+GPxwNcQmGASgMAG4QRMfDAywcivqdVdSgmF59ytC9cZ8Q62vdrVNdyknJDP8wACp3N4UnadClNlfymM8esJk8pB5vnpY3DrCluU9PSTlBSd9RBVv7IcOi/Vdrptn+f8ZPt+w/s9Vz8/OeZHHqAsvXz2PZcSFN5JNoQ/ELRgh/1hvYJZH68yne7twPcf3w5jsQt49uD37ckUhGMQjGYZxGRMcDrnnWEqSM7m61XTFcvZj3toKRXmnGDnBTCrBQQHjMYFFHzhngxFoVetu/z9iNlyM6/w+QW+wkfxkdkPwcGlJSK99/HF92e9vNDOxEKJ8hExEzsrEK8EzhEJYfSSFGO3AwxXrl3VjOKn0qJ5jPIOjSlMMREzPMIcpjSLlRkSGENS/aUyxTNKPiOcJb/TlTwiSMGvwgsakpAyRtOm7KsMA2eKQZ6qWakM7OhlFKsrBt1fLAbpFeDbsazXscw0vnep6nsuMw1v2LO5pVeRs41Q+9jAM2R1RefgcuUV/rr/QrfkcP+dsz35Fw== \ No newline at end of file diff --git "a/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.png" "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.png" new file mode 100644 index 00000000000..f8b9589d6d7 Binary files /dev/null and "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.png" differ diff --git "a/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.drawio" "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.drawio" new file mode 100644 index 00000000000..6fddf10f064 --- /dev/null +++ "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.png" "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.png" new file mode 100644 index 00000000000..c976cc99b91 Binary files /dev/null and "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.png" differ diff --git "a/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.drawio" "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.drawio" new file mode 100644 index 00000000000..7f7bfd71641 --- /dev/null +++ "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.png" "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.png" new file mode 100644 index 00000000000..f8f457c7490 Binary files /dev/null and "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.png" differ diff --git "a/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.drawio" "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.drawio" new file mode 100644 index 00000000000..7626c8d1f50 --- /dev/null +++ "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.png" "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.png" new file mode 100644 index 00000000000..ecdbd6d2c2e Binary files /dev/null and "b/docs/database/redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.png" differ diff --git "a/docs/database/redis/redis\347\237\245\350\257\206\347\202\271&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" "b/docs/database/redis/redis\347\237\245\350\257\206\347\202\271&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" new file mode 100644 index 00000000000..f2ef0eb8756 --- /dev/null +++ "b/docs/database/redis/redis\347\237\245\350\257\206\347\202\271&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" @@ -0,0 +1,799 @@ +--- +title: Redis知识点&面试题总结 +category: 数据库 +tag: + - Redis +--- + +### 简单介绍一下 Redis 呗! + +简单来说 **Redis 就是一个使用 C 语言开发的数据库**,不过与传统数据库不同的是 **Redis 的数据是存在内存中的** ,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。 + +另外,**Redis 除了做缓存之外,也经常用来做分布式锁,甚至是消息队列。** + +**Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。** + +### 分布式缓存常见的技术选型方案有哪些? + +分布式缓存的话,使用的比较多的主要是 **Memcached** 和 **Redis**。不过,现在基本没有看过还有项目使用 **Memcached** 来做缓存,都是直接用 **Redis**。 + +Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。 + +分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用信息的问题。因为,本地缓存只在当前服务里有效,比如如果你部署了两个相同的服务,他们两者之间的缓存数据是无法共同的。 + +### 说一下 Redis 和 Memcached 的区别和共同点 + +现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!不过,了解 Redis 和 Memcached 的区别和共同点,有助于我们在做相应的技术选型的时候,能够做到有理有据! + +**共同点** : + +1. 都是基于内存的数据库,一般都用来当做缓存使用。 +2. 都有过期策略。 +3. 两者的性能都非常高。 + +**区别** : + +1. **Redis 支持更丰富的数据类型(支持更复杂的应用场景)**。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。 +2. **Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。** +3. **Redis 有灾难恢复机制。** 因为可以把缓存中的数据持久化到磁盘上。 +4. **Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。** +5. **Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的。** +6. **Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。** (Redis 6.0 引入了多线程 IO ) +7. **Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。** +8. **Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。** + +相信看了上面的对比之后,我们已经没有什么理由可以选择使用 Memcached 来作为自己项目的分布式缓存了。 + +### 缓存数据的处理流程是怎样的? + +作为暖男一号,我给大家画了一个草图。 + +![正常缓存处理流程](images/redis-all/缓存的处理流程.png) + +简单来说就是: + +1. 如果用户请求的数据在缓存中就直接返回。 +2. 缓存中不存在的话就看数据库中是否存在。 +3. 数据库中存在的话就更新缓存中的数据。 +4. 数据库中不存在的话就返回空数据。 + +### 为什么要用 Redis/为什么要用缓存? + +_简单,来说使用缓存主要是为了提升用户体验以及应对更多的用户。_ + +下面我们主要从“高性能”和“高并发”这两点来看待这个问题。 + +![](./images/redis-all/使用缓存之后.png) + +**高性能** : + +对照上面 👆 我画的图。我们设想这样的场景: + +假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。 + +**这样有什么好处呢?** 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。 + +不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可! + +**高并发:** + +一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的情况,redis 集群的话会更高)。 + +> QPS(Query Per Second):服务器每秒可以执行的查询次数; + +由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。 + +### Redis 除了做缓存,还能做什么? + +- **分布式锁** : 通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。相关阅读:[《分布式锁中的王者方案 - Redisson》](https://mp.weixin.qq.com/s/CbnPRfvq4m1sqo2uKI6qQw)。 +- **限流** :一般是通过 Redis + Lua 脚本的方式来实现限流。相关阅读:[《我司用了 6 年的 Redis 分布式限流器,可以说是非常厉害了!》](https://mp.weixin.qq.com/s/kyFAWH3mVNJvurQDt4vchA)。 +- **消息队列** :Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。 +- **复杂业务场景** :通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。 +- ...... + +### Redis 常见数据结构以及使用场景分析 + +你可以自己本机安装 redis 或者通过 redis 官网提供的[在线 redis 环境](https://try.redis.io/)。 + +![try-redis](./images/redis-all/try-redis.png) + +#### string + +1. **介绍** :string 数据结构是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 **简单动态字符串**(simple dynamic string,**SDS**)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。 +2. **常用命令:** `set,get,strlen,exists,decr,incr,setex` 等等。 +3. **应用场景:** 一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。 + +下面我们简单看看它的使用! + +**普通字符串的基本操作:** + +```bash +127.0.0.1:6379> set key value #设置 key-value 类型的值 +OK +127.0.0.1:6379> get key # 根据 key 获得对应的 value +"value" +127.0.0.1:6379> exists key # 判断某个 key 是否存在 +(integer) 1 +127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的长度。 +(integer) 5 +127.0.0.1:6379> del key # 删除某个 key 对应的值 +(integer) 1 +127.0.0.1:6379> get key +(nil) +``` + +**批量设置** : + +```bash +127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置 key-value 类型的值 +OK +127.0.0.1:6379> mget key1 key2 # 批量获取多个 key 对应的 value +1) "value1" +2) "value2" +``` + +**计数器(字符串的内容为整数的时候可以使用):** + +```bash +127.0.0.1:6379> set number 1 +OK +127.0.0.1:6379> incr number # 将 key 中储存的数字值增一 +(integer) 2 +127.0.0.1:6379> get number +"2" +127.0.0.1:6379> decr number # 将 key 中储存的数字值减一 +(integer) 1 +127.0.0.1:6379> get number +"1" +``` + +**过期(默认为永不过期)**: + +```bash +127.0.0.1:6379> expire key 60 # 数据在 60s 后过期 +(integer) 1 +127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire) +OK +127.0.0.1:6379> ttl key # 查看数据还有多久过期 +(integer) 56 +``` + +#### list + +1. **介绍** :**list** 即是 **链表**。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且可以灵活调整链表长度,但是链表的随机访问困难。许多高级编程语言都内置了链表的实现比如 Java 中的 **LinkedList**,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 list 的实现为一个 **双向链表**,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。 +2. **常用命令:** `rpush,lpop,lpush,rpop,lrange,llen` 等。 +3. **应用场景:** 发布与订阅或者说消息队列、慢查询。 + +下面我们简单看看它的使用! + +**通过 `rpush/lpop` 实现队列:** + +```bash +127.0.0.1:6379> rpush myList value1 # 向 list 的头部(右边)添加元素 +(integer) 1 +127.0.0.1:6379> rpush myList value2 value3 # 向list的头部(最右边)添加多个元素 +(integer) 3 +127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出 +"value1" +127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end +1) "value2" +2) "value3" +127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第一 +1) "value2" +2) "value3" +``` + +**通过 `rpush/rpop` 实现栈:** + +```bash +127.0.0.1:6379> rpush myList2 value1 value2 value3 +(integer) 3 +127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出 +"value3" +``` + +我专门画了一个图方便小伙伴们来理解: + +![redis list](./images/redis-all/redis-list.png) + +**通过 `lrange` 查看对应下标范围的列表元素:** + +```bash +127.0.0.1:6379> rpush myList value1 value2 value3 +(integer) 3 +127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end +1) "value1" +2) "value2" +127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第一 +1) "value1" +2) "value2" +3) "value3" +``` + +通过 `lrange` 命令,你可以基于 list 实现分页查询,性能非常高! + +**通过 `llen` 查看链表长度:** + +```bash +127.0.0.1:6379> llen myList +(integer) 3 +``` + +#### hash + +1. **介绍** :hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 hash 做了更多优化。另外,hash 是一个 string 类型的 field 和 value 的映射表,**特别适合用于存储对象**,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。 +2. **常用命令:** `hset,hmset,hexists,hget,hgetall,hkeys,hvals` 等。 +3. **应用场景:** 系统中对象数据的存储。 + +下面我们简单看看它的使用! + +```bash +127.0.0.1:6379> hmset userInfoKey name "guide" description "dev" age "24" +OK +127.0.0.1:6379> hexists userInfoKey name # 查看 key 对应的 value中指定的字段是否存在。 +(integer) 1 +127.0.0.1:6379> hget userInfoKey name # 获取存储在哈希表中指定字段的值。 +"guide" +127.0.0.1:6379> hget userInfoKey age +"24" +127.0.0.1:6379> hgetall userInfoKey # 获取在哈希表中指定 key 的所有字段和值 +1) "name" +2) "guide" +3) "description" +4) "dev" +5) "age" +6) "24" +127.0.0.1:6379> hkeys userInfoKey # 获取 key 列表 +1) "name" +2) "description" +3) "age" +127.0.0.1:6379> hvals userInfoKey # 获取 value 列表 +1) "guide" +2) "dev" +3) "24" +127.0.0.1:6379> hset userInfoKey name "GuideGeGe" # 修改某个字段对应的值 +127.0.0.1:6379> hget userInfoKey name +"GuideGeGe" +``` + +#### set + +1. **介绍 :** set 类似于 Java 中的 `HashSet` 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。 +2. **常用命令:** `sadd,spop,smembers,sismember,scard,sinterstore,sunion` 等。 +3. **应用场景:** 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景 + +下面我们简单看看它的使用! + +```bash +127.0.0.1:6379> sadd mySet value1 value2 # 添加元素进去 +(integer) 2 +127.0.0.1:6379> sadd mySet value1 # 不允许有重复元素 +(integer) 0 +127.0.0.1:6379> smembers mySet # 查看 set 中所有的元素 +1) "value1" +2) "value2" +127.0.0.1:6379> scard mySet # 查看 set 的长度 +(integer) 2 +127.0.0.1:6379> sismember mySet value1 # 检查某个元素是否存在set 中,只能接收单个元素 +(integer) 1 +127.0.0.1:6379> sadd mySet2 value2 value3 +(integer) 2 +127.0.0.1:6379> sinterstore mySet3 mySet mySet2 # 获取 mySet 和 mySet2 的交集并存放在 mySet3 中 +(integer) 1 +127.0.0.1:6379> smembers mySet3 +1) "value2" +``` + +#### sorted set + +1. **介绍:** 和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。 +2. **常用命令:** `zadd,zcard,zscore,zrange,zrevrange,zrem` 等。 +3. **应用场景:** 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。 + +```bash +127.0.0.1:6379> zadd myZset 3.0 value1 # 添加元素到 sorted set 中 3.0 为权重 +(integer) 1 +127.0.0.1:6379> zadd myZset 2.0 value2 1.0 value3 # 一次添加多个元素 +(integer) 2 +127.0.0.1:6379> zcard myZset # 查看 sorted set 中的元素数量 +(integer) 3 +127.0.0.1:6379> zscore myZset value1 # 查看某个 value 的权重 +"3" +127.0.0.1:6379> zrange myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出所有元素 +1) "value3" +2) "value2" +3) "value1" +127.0.0.1:6379> zrange myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start 1 为 stop +1) "value3" +2) "value2" +127.0.0.1:6379> zrevrange myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start 1 为 stop +1) "value1" +2) "value2" +``` + +#### bitmap + +1. **介绍:** bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 bitmap 本身会极大的节省储存空间。 +2. **常用命令:** `setbit` 、`getbit` 、`bitcount`、`bitop` +3. **应用场景:** 适合需要保存状态信息(比如是否签到、是否登录...)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。 + +```bash +# SETBIT 会返回之前位的值(默认是 0)这里会生成 7 个位 +127.0.0.1:6379> setbit mykey 7 1 +(integer) 0 +127.0.0.1:6379> setbit mykey 7 0 +(integer) 1 +127.0.0.1:6379> getbit mykey 7 +(integer) 0 +127.0.0.1:6379> setbit mykey 6 1 +(integer) 0 +127.0.0.1:6379> setbit mykey 8 1 +(integer) 0 +# 通过 bitcount 统计被被设置为 1 的位的数量。 +127.0.0.1:6379> bitcount mykey +(integer) 2 +``` + +针对上面提到的一些场景,这里进行进一步说明。 + +**使用场景一:用户行为分析** +很多网站为了分析你的喜好,需要研究你点赞过的内容。 + +```bash +# 记录你喜欢过 001 号小姐姐 +127.0.0.1:6379> setbit beauty_girl_001 uid 1 +``` + +**使用场景二:统计活跃用户** + +使用时间作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1 + +那么我该如何计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只要有一天在线就称为活跃),有请下一个 redis 的命令 + +```bash +# 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。 +# BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数 +BITOP operation destkey key [key ...] +``` + +初始化数据: + +```bash +127.0.0.1:6379> setbit 20210308 1 1 +(integer) 0 +127.0.0.1:6379> setbit 20210308 2 1 +(integer) 0 +127.0.0.1:6379> setbit 20210309 1 1 +(integer) 0 +``` + +统计 20210308~20210309 总活跃用户数: 1 + +```bash +127.0.0.1:6379> bitop and desk1 20210308 20210309 +(integer) 1 +127.0.0.1:6379> bitcount desk1 +(integer) 1 +``` + +统计 20210308~20210309 在线活跃用户数: 2 + +```bash +127.0.0.1:6379> bitop or desk2 20210308 20210309 +(integer) 1 +127.0.0.1:6379> bitcount desk2 +(integer) 2 +``` + +**使用场景三:用户在线状态** + +对于获取或者统计用户在线状态,使用 bitmap 是一个节约空间且效率又高的一种方法。 + +只需要一个 key,然后用户 ID 为 offset,如果在线就设置为 1,不在线就设置为 0。 + +### Redis 单线程模型详解 + +**Redis 基于 Reactor 模式来设计开发了自己的一套高效的事件处理模型** (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。 + +**既然是单线程,那怎么监听大量的客户端连接呢?** + +Redis 通过**IO 多路复用程序** 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。 + +这样的好处非常明显: **I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗**(和 NIO 中的 `Selector` 组件很像)。 + +另外, Redis 服务器是一个事件驱动程序,服务器需要处理两类事件:1. 文件事件; 2. 时间事件。 + +时间事件不需要多花时间了解,我们接触最多的还是 **文件事件**(客户端进行读取写入等操作,涉及一系列网络通信)。 + +《Redis 设计与实现》有一段话是如是介绍文件事件的,我觉得写得挺不错。 + +> Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。 +> +> 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。 +> +> **虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字**,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。 + +可以看出,文件事件处理器(file event handler)主要是包含 4 个部分: + +- 多个 socket(客户端连接) +- IO 多路复用程序(支持多个客户端连接的关键) +- 文件事件分派器(将 socket 关联到相应的事件处理器) +- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) + +![](images/redis-all/redis事件处理器.png) + +

《Redis设计与实现:12章》

+ +### Redis 没有使用多线程?为什么不使用多线程? + +虽然说 Redis 是单线程模型,但是,实际上,**Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。** + +![redis4.0 more thread](images/redis-all/redis4.0-more-thread.png) + +不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主处理之外的其他线程来“异步处理”。 + +大体上来说,**Redis 6.0 之前主要还是单线程处理。** + +**那,Redis6.0 之前 为什么不使用多线程?** + +我觉得主要原因有下面 3 个: + +1. 单线程编程容易并且更容易维护; +2. Redis 的性能瓶颈不在 CPU ,主要在内存和网络; +3. 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。 + +### Redis6.0 之后为何引入了多线程? + +**Redis6.0 引入多线程主要是为了提高网络 IO 读写性能**,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。 + +虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。 + +Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : + +```bash +io-threads-do-reads yes +``` + +开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis 配置文件 `redis.conf` : + +```bash +io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 +``` + +推荐阅读: + +1. [Redis 6.0 新特性-多线程连环 13 问!](https://mp.weixin.qq.com/s/FZu3acwK6zrCBZQ_3HoUgw) +2. [为什么 Redis 选择单线程模型](https://draveness.me/whys-the-design-redis-single-thread/) + +### Redis 给缓存数据设置过期时间有啥用? + +一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢? + +因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。 + +Redis 自带了给缓存数据设置过期时间的功能,比如: + +```bash +127.0.0.1:6379> exp key 60 # 数据在 60s 后过期 +(integer) 1 +127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire) +OK +127.0.0.1:6379> ttl key # 查看数据还有多久过期 +(integer) 56 +``` + +注意:**Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外, `persist` 命令可以移除一个键的过期时间。 ** + +**过期时间除了有助于缓解内存的消耗,还有什么其他用么?** + +很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 token 可能只在 1 天内有效。 + +如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。 + +### Redis 是如何判断数据是否过期的呢? + +Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。 + +![redis过期字典](images/redis-all/redis过期时间.png) + +过期字典是存储在 redisDb 这个结构里的: + +```c +typedef struct redisDb { + ... + + dict *dict; //数据库键空间,保存着数据库中所有键值对 + dict *expires // 过期字典,保存着键的过期时间 + ... +} redisDb; +``` + +### 过期的数据的删除策略了解么? + +如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢? + +常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西): + +1. **惰性删除** :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。 +2. **定期删除** : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。 + +定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 **定期删除+惰性/懒汉式删除** 。 + +但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。 + +怎么解决这个问题呢?答案就是:**Redis 内存淘汰机制。** + +### Redis 内存淘汰机制了解么? + +> 相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据? + +Redis 提供 6 种数据淘汰策略: + +1. **volatile-lru(least recently used)**:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 +2. **volatile-ttl**:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 +3. **volatile-random**:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 +4. **allkeys-lru(least recently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的) +5. **allkeys-random**:从数据集(server.db[i].dict)中任意选择数据淘汰 +6. **no-eviction**:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧! + +4.0 版本后增加以下两种: + +7. **volatile-lfu(least frequently used)**:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰 +8. **allkeys-lfu(least frequently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key + +### Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复) + +很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。 + +Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持两种不同的持久化操作。**Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)**。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。 + +**快照(snapshotting)持久化(RDB)** + +Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。 + +快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置: + +```conf +save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 + +save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 + +save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 +``` + +**AOF(append-only file)持久化** + +与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启: + +```conf +appendonly yes +``` + +开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 `server.aof_buf` 中,然后再根据 `appendfsync` 配置来决定何时将其同步到硬盘中的 AOF 文件。 + +AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 `appendonly.aof`。 + +在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是: + +```conf +appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 +appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘 +appendfsync no #让操作系统决定何时进行同步 +``` + +为了兼顾数据和写入性能,用户可以考虑 `appendfsync everysec` 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。 + +**相关 issue** :[783:Redis 的 AOF 方式](https://github.com/Snailclimb/JavaGuide/issues/783) + +**拓展:Redis 4.0 对于持久化机制的优化** + +Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。 + +如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。 + +官方文档地址:https://redis.io/topics/persistence + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/image-host-github-stars-01@main/webfunny_monitor/image-20210807145107290.png) + +**补充内容:AOF 重写** + +AOF 重写可以产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。 + +AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。 + +在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。 + +### Redis 事务 + +Redis 可以通过 **`MULTI`,`EXEC`,`DISCARD` 和 `WATCH`** 等命令来实现事务(transaction)功能。 + +```bash +> MULTI +OK +> SET USER "Guide哥" +QUEUED +> GET USER +QUEUED +> EXEC +1) OK +2) "Guide哥" +``` + +使用 [`MULTI`](https://redis.io/commands/multi) 命令后可以输入多个命令。Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 [`EXEC`](https://redis.io/commands/exec) 命令将执行所有命令。 + +这个过程是这样的: + +1. 开始事务(`MULTI`)。 +2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行)。 +3. 执行事务(`EXEC`)。 + +你也可以通过 [`DISCARD`](https://redis.io/commands/discard) 命令取消一个事务,它会清空事务队列中保存的所有命令。 + +```bash +> MULTI +OK +> SET USER "Guide哥" +QUEUED +> GET USER +QUEUED +> DISCARD +OK +``` + +[`WATCH`](https://redis.io/commands/watch) 命令用于监听指定的键,当调用 `EXEC` 命令执行事务时,如果一个被 `WATCH` 命令监视的键被修改的话,整个事务都不会执行,直接返回失败。 + +```bash +> WATCH USER +OK +> MULTI +> SET USER "Guide哥" +OK +> GET USER +Guide哥 +> EXEC +ERR EXEC without MULTI +``` + +Redis 官网相关介绍 [https://redis.io/topics/transactions](https://redis.io/topics/transactions) 如下: + +![redis事务](images/redis-all/redis事务.png) + +但是,Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性: **1. 原子性**,**2. 隔离性**,**3. 持久性**,**4. 一致性**。 + +1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; +2. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; +3. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 +4. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的; + +**Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。** + +Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。 + +![redis roll back](images/redis-all/redis-rollBack.png) + +你可以将 Redis 中的事务就理解为 :**Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。** + +**相关 issue** : + +- [issue452: 关于 Redis 事务不满足原子性的问题](https://github.com/Snailclimb/JavaGuide/issues/452) 。 +- [Issue491:关于 redis 没有事务回滚?](https://github.com/Snailclimb/JavaGuide/issues/491) + +### 缓存穿透 + +#### 什么是缓存穿透? + +缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。 + +#### 缓存穿透情况的处理流程是怎样的? + +如下图所示,用户的请求最终都要跑到数据库中查询一遍。 + +![缓存穿透情况](./images/redis-all/缓存穿透情况.png) + +#### 有哪些解决办法? + +最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。 + +**1)缓存无效 key** + +如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: `SET key value EX 10086` 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。 + +另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值` 。 + +如果用 Java 代码展示的话,差不多是下面这样的: + +```java +public Object getObjectInclNullById(Integer id) { + // 从缓存中获取数据 + Object cacheValue = cache.get(id); + // 缓存为空 + if (cacheValue == null) { + // 从数据库中获取 + Object storageValue = storage.get(key); + // 缓存空对象 + cache.set(key, storageValue); + // 如果存储数据为空,需要设置一个过期时间(300秒) + if (storageValue == null) { + // 必须设置过期时间,否则有被攻击的风险 + cache.expire(key, 60 * 5); + } + return storageValue; + } + return cacheValue; +} +``` + +**2)布隆过滤器** + +布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。 + +具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。 + +加入布隆过滤器之后的缓存处理流程图如下。 + +![image](images/redis-all/加入布隆过滤器后的缓存处理流程.png) + +但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: **布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。** + +_为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!_ + +我们先来看一下,**当一个元素加入布隆过滤器中的时候,会进行哪些操作:** + +1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。 +2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。 + +我们再来看一下,**当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:** + +1. 对给定元素再次进行相同的哈希计算; +2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 + +然后,一定会出现这样一种情况:**不同的字符串可能哈希出来的位置相同。** (可以适当增加位数组大小或者调整我们的哈希函数来降低概率) + +更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://github.com/Snailclimb/JavaGuide/blob/master/docs/cs-basics/data-structure/bloom-filter.md) ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。 + +### 缓存雪崩 + +#### 什么是缓存雪崩? + +我发现缓存雪崩这名字起的有点意思,哈哈。 + +实际上,缓存雪崩描述的就是这样一个简单的场景:**缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。** 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。 + +举个例子:系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。 + +还有一种缓存雪崩的场景是:**有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。** 这样的情况,有下面几种解决办法: + +举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。 + +#### 有哪些解决办法? + +**针对 Redis 服务不可用的情况:** + +1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。 +2. 限流,避免同时处理大量的请求。 + +**针对热点缓存失效的情况:** + +1. 设置不同的失效时间比如随机设置缓存的失效时间。 +2. 缓存永不失效。 + +### 如何保证缓存和数据库数据的一致性? + +细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。 + +下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。 + +Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。 + +如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案: + +1. **缓存失效时间变短(不推荐,治标不治本)** :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 +2. **增加 cache 更新重试机制(常用)**: 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。 + +### 参考 + +- 《Redis 开发与运维》 +- 《Redis 设计与实现》 +- Redis 命令总结:http://Redisdoc.com/string/set.html +- 通俗易懂的 Redis 数据结构基础教程:[https://juejin.im/post/5b53ee7e5188251aaa2d2e16](https://juejin.im/post/5b53ee7e5188251aaa2d2e16) +- WHY Redis choose single thread (vs multi threads): [https://medium.com/@jychen7/sharing-redis-single-thread-vs-multi-threads-5870bd44d153](https://medium.com/@jychen7/sharing-redis-single-thread-vs-multi-threads-5870bd44d153) diff --git "a/docs/database/\345\255\227\347\254\246\351\233\206.md" "b/docs/database/\345\255\227\347\254\246\351\233\206.md" new file mode 100644 index 00000000000..1cca2575e6e --- /dev/null +++ "b/docs/database/\345\255\227\347\254\246\351\233\206.md" @@ -0,0 +1,160 @@ +--- +title: 字符集 +category: 数据库 +tag: + - 数据库基础 +--- + + +MySQL 字符编码集中有两套 UTF-8 编码实现:**`utf8`** 和 **`utf8mb4`**。 + +如果使用 **`utf8`** 的话,存储emoji 符号和一些比较复杂的汉字、繁体字就会出错。 + +为什么会这样呢?这篇文章可以从源头给你解答。 + +## 何为字符集? + +字符是各种文字和符号的统称,包括各个国家文字、标点符号、表情、数字等等。 **字符集** 就是一系列字符的集合。字符集的种类较多,每个字符集可以表示的字符范围通常不同,就比如说有些字符集是无法表示汉字的。 + +**计算机只能存储二进制的数据,那英文、汉字、表情等字符应该如何存储呢?** + +我们要将这些字符和二级制的数据一一对应起来,比如说字符“a”对应“01100001”,反之,“01100001”对应 “a”。我们将字符对应二进制数据的过程称为"**字符编码**",反之,二进制数据解析成字符的过程称为“**字符解码**”。 + +## 有哪些常见的字符集? + +常见的字符集有 ASCII、GB2312、GBK、UTF-8......。 + +不同的字符集的主要区别在于: + +- 可以表示的字符范围 +- 编码方式 + +### ASCII + +**ASCII** (**A**merican **S**tandard **C**ode for **I**nformation **I**nterchange,美国信息交换标准代码) 是一套主要用于现代美国英语的字符集(这也是 ASCII 字符集的局限性所在)。 + +**为什么 ASCII 字符集没有考虑到中文等其他字符呢?** 因为计算机是美国人发明的,当时,计算机的发展还处于比较雏形的时代,还未在其他国家大规模使用。因此,美国发布 ASCII 字符集的时候没有考虑兼容其他国家的语言。 + +ASCII 字符集至今为止共定义了 128 个字符,其中有 33 个控制字符(比如回车、删除)无法显示。 + +一个 ASCII 码长度是一个字节也就是 8 个 bit,比如“a”对应的 ASCII 码是“01100001”。不过,最高位是 0 仅仅作为校验位,其余 7 位使用 0 和 1 进行组合,所以,ASCII 字符集可以定义 128(2^7)个字符。 + +由于,ASCII 码可以表示的字符实在是太少了。后来,人们对其进行了扩展得到了 **ASCII 扩展字符集** 。ASCII 扩展字符集使用 8 位(bits)表示一个字符,所以,ASCII 扩展字符集可以定义 256(2^8)个字符。 + +![ASCII字符编码](https://img-blog.csdnimg.cn/img_convert/c1c6375d08ca268690cef2b13591a5b4.png) + +### GB2312 + +我们上面说了,ASCII 字符集是一种现代美国英语适用的字符集。因此,很多国家都捣鼓了一个适合自己国家语言的字符集。 + +GB2312 字符集是一种对汉字比较友好的字符集,共收录 6700 多个汉字,基本涵盖了绝大部分常用汉字。不过,GB2312 字符集不支持绝大部分的生僻字和繁体字。 + +对于英语字符,GB2312 编码和 ASCII 码是相同的,1 字节编码即可。对于非英字符,需要 2 字节编码。 + +### GBK + +GBK 字符集可以看作是 GB2312 字符集的扩展,兼容 GB2312 字符集,共收录了 20000 多个汉字。 + +GBK 中 K 是汉语拼音 Kuo Zhan(扩展)中的“Kuo”的首字母。 + +### GB18030 + +GB18030 完全兼容 GB2312 和 GBK 字符集,纳入中国国内少数民族的文字,且收录了日韩汉字,是目前为止最全面的汉字字符集,共收录汉字 70000 多个。 + +### BIG5 + +BIG5 主要针对的是繁体中文,收录了 13000 多个汉字。 + +### Unicode & UTF-8编码 + +为了更加适合本国语言,诞生了很多种字符集。 + +我们上面也说了不同的字符集可以表示的字符范围以及编码规则存在差异。这就导致了一个非常严重的问题:**使用错误的编码方式查看一个包含字符的文件就会产生乱码现象。** + +就比如说你使用 UTF-8 编码方式打开 GB2312 编码格式的文件就会出现乱码。示例:“牛”这个汉字 GB2312 编码后的十六进制数值为 “C5A3”,而 “C5A3” 用 UTF-8 解码之后得到的却是 “ţ”。 + +你可以通过这个网站在线进行编码和解码:https://www.haomeili.net/HanZi/ZiFuBianMaZhuanHuan + +![](https://img-blog.csdnimg.cn/836c49b117ee4408871b0020b74c991d.png) + +这样我们就搞懂了乱码的本质: **编码和解码时用了不同或者不兼容的字符集** 。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/a8808cbabeea49caa3af27d314fa3c02-1.jpg) + +为了解决这个问题,人们就想:“如果我们能够有一种字符集将世界上所有的字符都纳入其中就好了!”。 + +然后,**Unicode** 带着这个使命诞生了。 + +Unicode 字符集中包含了世界上几乎所有已知的字符。不过,Unicode 字符集并没有规定如何存储这些字符(也就是如何使用二进制数据表示这些字符)。 + +然后,就有了 **UTF-8**(**8**-bit **U**nicode **T**ransformation **F**ormat)。类似的还有 UTF-16、 UTF-32。 + +UTF-8 使用 1 到 4 个字节为每个字符编码, UTF-16 使用 2 或 4 个字节为每个字符编码,UTF-32 固定位 4 个字节为每个字符编码。 + +UTF-8 可以根据不同的符号自动选择编码的长短,像英文字符只需要 1 个字节就够了,这一点 ASCII 字符集一样 。因此,对于英语字符,UTF-8 编码和 ASCII 码是相同的。 + +UTF-32 的规则最简单,不过缺陷也比较明显,对于英文字母这类字符消耗的空间是 UTF-8 的 4 倍之多。 + +**UTF-8** 是目前使用最广的一种字符编码,。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/1280px-Utf8webgrowth.svg.png) + +## MySQL 字符集 + +MySQL 支持很多种字符编码的方式,比如 UTF-8、GB2312、GBK、BIG5。 + +你可以通过 `SHOW CHARSET` 命令来查看。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/image-20211008164229671.png) + +通常情况下,我们建议使用 UTF-8 作为默认的字符编码方式。 + +不过,这里有一个小坑。 + +MySQL 字符编码集中有两套 UTF-8 编码实现: + +- **`utf8`** : `utf8`编码只支持`1-3`个字节 。 在 `utf8` 编码中,中文是占 3 个字节,其他数字、英文、符号占一个字节。但 emoji 符号占 4 个字节,一些较复杂的文字、繁体字也是 4 个字节。 +- **`utf8mb4`** : UTF-8 的完整实现,正版!最多支持使用 4 个字节表示字符,因此,可以用来存储 emoji 符号。 + +**为什么有两套 UTF-8 编码实现呢?** 原因如下: + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/image-20211008164542347.png) + +因此,如果你需要存储`emoji`类型的数据或者一些比较复杂的文字、繁体字到 MySQL 数据库的话,数据库的编码一定要指定为`utf8mb4` 而不是`utf8` ,要不然存储的时候就会报错了。 + +演示一下吧!(环境:MySQL 5.7+) + +建表语句如下,我们指定数据库 CHARSET 为 `utf8` 。 + +```sql +CREATE TABLE `user` ( + `id` varchar(66) CHARACTER SET utf8mb4 NOT NULL, + `name` varchar(33) CHARACTER SET utf8mb4 NOT NULL, + `phone` varchar(33) CHARACTER SET utf8mb4 DEFAULT NULL, + `password` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` + +当我们执行下面的 insert 语句插入数据到数据库时,果然报错! + +```sql +INSERT INTO `user` (`id`, `name`, `phone`, `password`) +VALUES + ('A00003', 'guide哥😘😘😘', '181631312312', '123456'); + +``` + +报错信息如下: + +``` +Incorrect string value: '\xF0\x9F\x98\x98\xF0\x9F...' for column 'name' at row 1 +``` + +## 参考 + +- 字符集和字符编码(Charset & Encoding): https://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html +- 十分钟搞清字符集和字符编码:http://cenalulu.github.io/linux/character-encoding/ +- Unicode-维基百科:https://zh.wikipedia.org/wiki/Unicode +- GB2312-维基百科:https://zh.wikipedia.org/wiki/GB_2312 +- UTF-8-维基百科:https://zh.wikipedia.org/wiki/UTF-8 +- GB18030-维基百科: https://zh.wikipedia.org/wiki/GB_18030 \ No newline at end of file diff --git "a/docs/database/\346\225\260\346\215\256\345\272\223\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/database/\346\225\260\346\215\256\345\272\223\345\237\272\347\241\200\347\237\245\350\257\206.md" new file mode 100644 index 00000000000..92d30998a71 --- /dev/null +++ "b/docs/database/\346\225\260\346\215\256\345\272\223\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -0,0 +1,150 @@ +--- +title: 数据库基础知识 +category: 数据库 +tag: + - 数据库基础 +--- + +数据库知识基础,这部分内容一定要理解记忆。虽然这部分内容只是理论知识,但是非常重要,这是后面学习 MySQL 数据库的基础。PS: 这部分内容由于涉及太多概念性内容,所以参考了维基百科和百度百科相应的介绍。 + +## 什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员? + +* **数据库** : 数据库(DataBase 简称 DB)就是信息的集合或者说数据库是由数据库管理系统管理的数据的集合。 +* **数据库管理系统** : 数据库管理系统(Database Management System 简称 DBMS)是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。 +* **数据库系统** : 数据库系统(Data Base System,简称 DBS)通常由软件、数据库和数据管理员(DBA)组成。 +* **数据库管理员** : 数据库管理员(Database Administrator, 简称 DBA)负责全面管理和控制数据库系统。 + +数据库系统基本构成如下图所示: + +![数据库系统基本构成](https://img-blog.csdnimg.cn/img_convert/e21120184e63406526a4e873cacd23f2.png) + +## 什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性? + +* **元组** : 元组(tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。 +* **码** :码就是能唯一标识实体的属性,对应表中的列。 +* **候选码** : 若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何、子集都不能再标识,则称该属性组为候选码。例如:在学生实体中,“学号”是能唯一的区分学生实体的,同时又假设“姓名”、“班级”的属性组合足以区分学生实体,那么{学号}和{姓名,班级}都是候选码。 +* **主码** : 主码也叫主键。主码是从候选码中选出来的。 一个实体集中只能有一个主码,但可以有多个候选码。 +* **外码** : 外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。 +* **主属性** : 候选码中出现过的属性称为主属性。比如关系 工人(工号,身份证号,姓名,性别,部门). 显然工号和身份证号都能够唯一标示这个关系,所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组,那么属性组中的属性都是主属性。 +* **非主属性:** 不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生(学号,姓名,年龄,性别,班级)中,主码是“学号”,那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。 + +## 主键和外键有什么区别? + +* **主键(主码)** :主键用于唯一标识一个元组,不能有重复,不允许为空。一个表只能有一个主键。 +* **外键(外码)** :外键用来和其他表建立联系用,外键是另一表的主键,外键是可以有重复的,可以是空值。一个表可以有多个外键。 + +## 为什么不推荐使用外键与级联? + +对于外键和级联,阿里巴巴开发手册这样说到: + +> 【强制】不得使用外键与级联,一切外键概念必须在应用层解决。 +> +> 说明: 以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群; 级联更新是强阻塞,存在数据库更新风暴的风 险; 外键影响数据库的插入速度 + +为什么不要用外键呢?大部分人可能会这样回答: + +> 1. **增加了复杂性:** a. 每次做DELETE 或者UPDATE都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如那天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。 +> 2. **增加了额外工作**: 数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗资源;(个人觉得这个不是不用外键的原因,因为即使你不使用外键,你在应用层面也还是要保证的。所以,我觉得这个影响可以忽略不计。) +> 3. 外键还会因为需要请求对其他表内部加锁而容易出现死锁情况; +> 4. **对分库分表不友好** :因为分库分表下外键是无法生效的。 +> 5. ...... + +我个人觉得上面这种回答不是特别的全面,只是说了外键存在的一个常见的问题。实际上,我们知道外键也是有很多好处的,比如: + +1. 保证了数据库数据的一致性和完整性; +2. 级联操作方便,减轻了程序代码量; +3. ...... + +所以说,不要一股脑的就抛弃了外键这个概念,既然它存在就有它存在的道理,如果系统不涉及分库分表,并发量不是很高的情况还是可以考虑使用外键的。 + + +## 什么是 ER 图? + +> 我们做一个项目的时候一定要试着画 ER 图来捋清数据库设计,这个也是面试官问你项目的时候经常会被问道的。 + +**E-R 图** 也称实体-联系图(Entity Relationship Diagram),提供了表示实体类型、属性和联系的方法,用来描述现实世界的概念模型。 它是描述现实世界关系概念模型的有效方法。 是表示概念关系模型的一种方式。 + +下图是一个学生选课的 ER 图,每个学生可以选若干门课程,同一门课程也可以被若干人选择,所以它们之间的关系是多对多(M: N)。另外,还有其他两种关系是:1 对 1(1:1)、1 对多(1: N)。 + +![ER图示例](https://img-blog.csdnimg.cn/img_convert/4717673e36966e0e4b33fccfd753f6ea.png) + +我们试着将上面的 ER 图转换成数据库实际的关系模型(实际设计中,我们通常会将任课教师也作为一个实体来处理): + +![关系模型](https://img-blog.csdnimg.cn/img_convert/5897753dfb301dfa3a814ab06e718a5e.png) + +## 数据库范式了解吗? + +**1NF(第一范式)** + +属性(对应于表中的字段)不能再被分割,也就是这个字段只能是一个值,不能再分为多个其他的字段了。**1NF 是所有关系型数据库的最基本要求** ,也就是说关系型数据库中创建的表一定满足第一范式。 + +**2NF(第二范式)** + +2NF 在 1NF 的基础之上,消除了非主属性对于码的部分函数依赖。如下图所示,展示了第一范式到第二范式的过渡。第二范式在第一范式的基础上增加了一个列,这个列称为主键,非主属性都依赖于主键。 + +![第二范式](https://img-blog.csdnimg.cn/img_convert/bd1d31be3779342427fc9e462bf7f05c.png) + +一些重要的概念: + +* **函数依赖(functional dependency)** :若在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。 +* **部分函数依赖(partial functional dependency)** :如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中(学号,身份证号,姓名)当然学号属性取值是唯一的,在 R 关系中,(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖与(学号,身份证号); +* **完全函数依赖(Full functional dependency)** :在一个关系中,若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。比如学生基本信息表 R(学号,班级,姓名)假设不同的班级学号有相同的,班级内学号不能相同,在 R 关系中,(学号,班级)->(姓名),但是(学号)->(姓名)不成立,(班级)->(姓名)不成立,所以姓名完全函数依赖与(学号,班级); +* **传递函数依赖** : 在关系模式 R(U)中,设 X,Y,Z 是 U 的不同的属性子集,如果 X 确定 Y、Y 确定 Z,且有 X 不包含 Y,Y 不确定 X,(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X。传递函数依赖会导致数据冗余和异常。传递函数依赖的 Y 和 Z 子集往往同属于某一个事物,因此可将其合并放到一个表中。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。。 + +**3NF(第三范式)** + +3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。符合 3NF 要求的数据库设计,**基本**上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖,所以该表的设计,不符合 3NF 的要求。 + +**总结** + +* 1NF:属性不可再分。 +* 2NF:1NF 的基础之上,消除了非主属性对于码的部分函数依赖。 +* 3NF:3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。 + +## 什么是存储过程? + +我们可以把存储过程看成是一些 SQL 语句的集合,中间加了点逻辑控制语句。存储过程在业务比较复杂的时候是非常实用的,比如很多时候我们完成一个操作可能需要写一大串 SQL 语句,这时候我们就可以写有一个存储过程,这样也方便了我们下一次的调用。存储过程一旦调试完成通过后就能稳定运行,另外,使用存储过程比单纯 SQL 语句执行要快,因为存储过程是预编译过的。 + +存储过程在互联网公司应用不多,因为存储过程难以调试和扩展,而且没有移植性,还会消耗数据库资源。 + +阿里巴巴 Java 开发手册里要求禁止使用存储过程。 + +![阿里巴巴Java开发手册: 禁止存储过程](https://img-blog.csdnimg.cn/img_convert/0fa082bc4d4f919065767476a41b2156.png) + +## drop、delete 与 truncate 区别? + +### 用法不同 + +* drop(丢弃数据): `drop table 表名` ,直接将表都删除掉,在删除表的时候使用。 +* truncate (清空数据) : `truncate table 表名` ,只删除表中的数据,再插入数据的时候自增长 id 又从 1 开始,在清空表中数据的时候使用。 +* delete(删除数据) : `delete from 表名 where 列名=值`,删除某一列的数据,如果不加 where 子句和`truncate table 表名`作用类似。 + +truncate 和不带 where 子句的 delete、以及 drop 都会删除表内的数据,但是 **truncate 和 delete 只删除数据不删除表的结构(定义),执行 drop 语句,此表的结构也会删除,也就是执行 drop 之后对应的表不复存在。** + +### 属于不同的数据库语言 + +truncate 和 drop 属于 DDL(数据定义语言)语句,操作立即生效,原数据不放到 rollback segment 中,不能回滚,操作不触发 trigger。而 delete 语句是 DML (数据库操作语言)语句,这个操作会放到 rollback segement 中,事务提交之后才生效。 + +**DML 语句和 DDL 语句区别:** + +* DML 是数据库操作语言(Data Manipulation Language)的缩写,是指对数据库中表记录的操作,主要包括表记录的插入(insert)、更新(update)、删除(delete)和查询(select),是开发人员日常使用最频繁的操作。 +* DDL (Data Definition Language)是数据定义语言的缩写,简单来说,就是对数据库内部的对象进行创建、删除、修改的操作语言。它和 DML 语言的最大区别是 DML 只是对表内部数据的操作,而不涉及到表的定义、结构的修改,更不会涉及到其他对象。DDL 语句更多的被数据库管理员(DBA)所使用,一般的开发人员很少使用。 + +### 执行速度不同 + +一般来说:drop>truncate>delete(这个我没有设计测试过)。 + +## 数据库设计通常分为哪几步? + +1. **需求分析** : 分析用户的需求,包括数据、功能和性能需求。 +2. **概念结构设计** : 主要采用 E-R 模型进行设计,包括画 E-R 图。 +3. **逻辑结构设计** : 通过将 E-R 图转换成表,实现从 E-R 模型到关系模型的转换。 +4. **物理结构设计** : 主要是为所设计的数据库选择合适的存储结构和存取路径。 +5. **数据库实施** : 包括编程、测试和试运行 +6. **数据库的运行和维护** : 系统的运行与数据库的日常维护。 + +## 参考 + +* +* +* diff --git "a/docs/database/\346\225\260\346\215\256\345\272\223\347\264\242\345\274\225.md" "b/docs/database/\346\225\260\346\215\256\345\272\223\347\264\242\345\274\225.md" deleted file mode 100644 index 568ba833fab..00000000000 --- "a/docs/database/\346\225\260\346\215\256\345\272\223\347\264\242\345\274\225.md" +++ /dev/null @@ -1,225 +0,0 @@ -## 什么是索引? -**索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B树, B+树和Hash。** - -索引的作用就相当于目录的作用。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。 - -## 为什么要用索引?索引的优缺点分析 - -### 索引的优点 -**可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。毕竟大部分系统的读请求总是大于写请求的。 ** 另外,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 - -### 索引的缺点 -1. **创建索引和维护索引需要耗费许多时间**:当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低SQL执行效率。 -2. **占用物理存储空间** :索引需要使用物理文件存储,也会耗费一定空间。 - -## B树和B+树区别 - -* B树的所有节点既存放 键(key) 也存放 数据(data);而B+树只有叶子节点存放 key 和 data,其他内节点只存放key。 -* B树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。 -* B树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。 - -![B+树](../../media/pictures/database/B+树.png) - -## Hash索引和 B+树索引优劣分析 - -**Hash索引定位快** - -Hash索引指的就是Hash表,最大的优点就是能够在很短的时间内,根据Hash函数定位到数据所在的位置,这是B+树所不能比的。 - -**Hash冲突问题** - -知道HashMap或HashTable的同学,相信都知道它们最大的缺点就是Hash冲突了。不过对于数据库来说这还不算最大的缺点。 - -**Hash索引不支持顺序和范围查询(Hash索引不支持顺序和范围查询是它最大的缺点。** - -试想一种情况: - -````text -SELECT * FROM tb1 WHERE id < 500; -```` - -B+树是有序的,在这种范围查询中,优势非常大,直接遍历比500小的叶子节点就够了。而Hash索引是根据hash算法来定位的,难不成还要把 1 - 499的数据,每个都进行一次hash计算来定位吗?这就是Hash最大的缺点了。 - ---- - -## 索引类型 - -### 主键索引(Primary Key) -**数据表的主键列使用的就是主键索引。** - -**一张数据表有只能有一个主键,并且主键不能为null,不能重复。** - -**在mysql的InnoDB的表中,当没有显示的指定表的主键时,InnoDB会自动先检查表中是否有唯一索引的字段,如果有,则选择该字段为默认的主键,否则InnoDB将会自动创建一个6Byte的自增主键。** - -### 二级索引(辅助索引) -**二级索引又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。** - -唯一索引,普通索引,前缀索引等索引属于二级索引。 - -**PS:不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。** - -1. **唯一索引(Unique Key)** :唯一索引也是一种约束。**唯一索引的属性列不能出现重复的数据,但是允许数据为NULL,一张表允许创建多个唯一索引。**建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。 -2. **普通索引(Index)** :**普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和NULL。** -3. **前缀索引(Prefix)** :前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小, - 因为只取前几个字符。 -4. **全文索引(Full Text)** :全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6之前只有MYISAM引擎支持全文索引,5.6之后InnoDB也支持了全文索引。 - -二级索引: -![B+树](../../media/pictures/database/B+树二级索引(辅助索引).png) - -## 聚集索引与非聚集索引 - -### 聚集索引 -**聚集索引即索引结构和数据一起存放的索引。主键索引属于聚集索引。** - -在 Mysql 中,InnoDB引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。 - -#### 聚集索引的优点 -聚集索引的查询速度非常的快,因为整个B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。 - -#### 聚集索引的缺点 -1. **依赖于有序的数据** :因为B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或UUID这种又长又难比较的数据,插入或查找的速度肯定比较慢。 -2. **更新代价大** : 如果对索引列的数据被修改时,那么对应的索引也将会被修改, - 而且况聚集索引的叶子节点还存放着数据,修改代价肯定是较大的, - 所以对于主键索引来说,主键一般都是不可被修改的。 - -### 非聚集索引 - -**非聚集索引即索引结构和数据分开存放的索引。** - -**二级索引属于非聚集索引。** - ->MYISAM引擎的表的.MYI文件包含了表的索引, ->该表的索引(B+树)的每个叶子非叶子节点存储索引, ->叶子节点存储索引和索引对应数据的指针,指向.MYD文件的数据。 -> -**非聚集索引的叶子节点并不一定存放数据的指针, -因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。** - -#### 非聚集索引的优点 -**更新代价比聚集索引要小** 。非聚集索引的更新代价就没有聚集索引那么大了,非聚集索引的叶子节点是不存放数据的 - -#### 非聚集索引的缺点 -1. 跟聚集索引一样,非聚集索引也依赖于有序的数据 -2. **可能会二次查询(回表)** :这应该是非聚集索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。 - -这是Mysql的表的文件截图: - -![Mysql表文件截图](../../media/pictures/database/Mysql索引文件截图.png) - -聚集索引和非聚集索引: - -![B+树](../../media/pictures/database/B+树索引.png) - -### 非聚集索引一定回表查询吗(覆盖索引)? -**非聚集索引不一定回表查询。** - ->试想一种情况,用户准备使用SQL查询用户名,而用户名字段正好建立了索引。 - -````text - SELECT name FROM table WHERE username='guang19'; -```` - ->那么这个索引的key本身就是name,查到对应的name直接返回就行了,无需回表查询。 - -**即使是MYISAM也是这样,虽然MYISAM的主键索引确实需要回表, -因为它的主键索引的叶子节点存放的是指针。但是如果SQL查的就是主键呢?** - -```text -SELECT id FROM table WHERE id=1; -``` - -主键索引本身的key就是主键,查到返回就行了。这种情况就称之为覆盖索引了。 - -## 覆盖索引 - -如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。我们知道在InnoDB存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! - -**覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了, -而无需回表查询。** - ->如主键索引,如果一条SQL需要查询主键,那么正好根据主键索引就可以查到主键。 -> ->再如普通索引,如果一条SQL需要查询name,name字段正好有索引, ->那么直接根据这个索引就可以查到数据,也无需回表。 - -覆盖索引: -![B+树覆盖索引](../../media/pictures/database/B+树覆盖索引.png) - ---- - -## 索引创建原则 - -### 单列索引 -单列索引即由一列属性组成的索引。 - -### 联合索引(多列索引) -联合索引即由多列属性组成索引。 - -### 最左前缀原则 - -假设创建的联合索引由三个字段组成: - -```text -ALTER TABLE table ADD INDEX index_name (num,name,age) -``` - -那么当查询的条件有为:num / (num AND name) / (num AND name AND age)时,索引才生效。所以在创建联合索引时,尽量把查询最频繁的那个字段作为最左(第一个)字段。查询的时候也尽量以这个字段为第一条件。 - -> 但可能由于版本原因(我的mysql版本为8.0.x),我创建的联合索引,相当于在联合索引的每个字段上都创建了相同的索引: - -![联合索引(多列索引)](../../media/pictures/database/联合索引(多列索引).png) - -无论是否符合最左前缀原则,每个字段的索引都生效: - -![联合索引生效](../../media/pictures/database/联合索引之查询条件生效.png) - -## 索引创建注意点 - -### 最左前缀原则 - -虽然我目前的Mysql版本较高,好像不遵守最左前缀原则,索引也会生效。 -但是我们仍应遵守最左前缀原则,以免版本更迭带来的麻烦。 - -### 选择合适的字段 - -#### 1.不为NULL的字段 - -索引字段的数据应该尽量不为NULL,因为对于数据为NULL的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为NULL,建议使用0,1,true,false这样语义较为清晰的短值或短字符作为替代。 - -#### 2.被频繁查询的字段 - -我们创建索引的字段应该是查询操作非常频繁的字段。 - -#### 3.被作为条件查询的字段 - -被作为WHERE条件查询的字段,应该被考虑建立索引。 - -#### 4.被经常频繁用于连接的字段 - -经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。 - -### 不合适创建索引的字段 - -#### 1.被频繁更新的字段应该慎重建立索引 - -虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。 -如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。 - -#### 2.不被经常查询的字段没有必要建立索引 - -#### 3.尽可能的考虑建立联合索引而不是单列索引 - -因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。 - -#### 4.注意避免冗余索引 - -冗余索引指的是索引的功能相同,能够命中 就肯定能命中 ,那么 就是冗余索引如(name,city )和(name )这两个索引就是冗余索引,能够命中后者的查询肯定是能够命中前者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。 - -#### 5.考虑在字符串类型的字段上使用前缀索引代替普通索引 - -前缀索引仅限于字符串类型,较普通索引会占用更小的空间,所以可以考虑使用前缀索引带替普通索引。 - -### 使用索引一定能提高查询性能吗? - -大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。 \ No newline at end of file diff --git "a/docs/database/\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245\346\261\240.md" "b/docs/database/\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245\346\261\240.md" deleted file mode 100644 index 3e84dfc8efd..00000000000 --- "a/docs/database/\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245\346\261\240.md" +++ /dev/null @@ -1,21 +0,0 @@ -- 公众号和Github待发文章:[数据库:数据库连接池原理详解与自定义连接池实现](https://www.fangzhipeng.com/javainterview/2019/07/15/mysql-connector-pool.html) -- [基于JDBC的数据库连接池技术研究与应用](http://blog.itpub.net/9403012/viewspace-111794/) -- [数据库连接池技术详解](https://juejin.im/post/5b7944c6e51d4538c86cf195) - -数据库连接本质就是一个 socket 的连接。数据库服务端还要维护一些缓存和用户权限信息之类的 所以占用了一些内存 - -连接池是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序的请求,既昂贵又浪费资源。**在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中。**连接池还减少了用户必须等待建立与数据库的连接的时间。 - -操作过数据库的朋友应该都知道数据库连接池这个概念,它几乎每天都在和我们打交道,但是你真的了解 **数据库连接池** 吗? - -### 没有数据库连接池之前 - -我相信你一定听过这样一句话:**Java语言中,JDBC(Java DataBase Connection)是应用程序与数据库沟通的桥梁**。 - - - - - - - - diff --git "a/docs/database/\351\230\277\351\207\214\345\267\264\345\267\264\345\274\200\345\217\221\346\211\213\345\206\214\346\225\260\346\215\256\345\272\223\351\203\250\345\210\206\347\232\204\344\270\200\344\272\233\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/docs/database/\351\230\277\351\207\214\345\267\264\345\267\264\345\274\200\345\217\221\346\211\213\345\206\214\346\225\260\346\215\256\345\272\223\351\203\250\345\210\206\347\232\204\344\270\200\344\272\233\346\234\200\344\275\263\345\256\236\350\267\265.md" deleted file mode 100644 index 7eb84001c00..00000000000 --- "a/docs/database/\351\230\277\351\207\214\345\267\264\345\267\264\345\274\200\345\217\221\346\211\213\345\206\214\346\225\260\346\215\256\345\272\223\351\203\250\345\210\206\347\232\204\344\270\200\344\272\233\346\234\200\344\275\263\345\256\236\350\267\265.md" +++ /dev/null @@ -1,41 +0,0 @@ -# 阿里巴巴Java开发手册数据库部分的一些最佳实践总结 - -## 模糊查询 - -对于模糊查询阿里巴巴开发手册这样说到: - -> 【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。 -> -> 说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。 - -## 外键和级联 - -对于外键和级联,阿里巴巴开发手册这样说到: - ->【强制】不得使用外键与级联,一切外键概念必须在应用层解决。 -> ->说明:以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风 险;外键影响数据库的插入速度 - -为什么不要用外键呢?大部分人可能会这样回答: - -> 1. **增加了复杂性:** a.每次做DELETE 或者UPDATE都必须考虑外键约束,会导致开发的时候很痛苦,测试数据极为不方便;b.外键的主从关系是定的,假如那天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。 -> 2. **增加了额外工作**: 数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗资源;(个人觉得这个不是不用外键的原因,因为即使你不使用外键,你在应用层面也还是要保证的。所以,我觉得这个影响可以忽略不计。) -> 3. 外键还会因为需要请求对其他表内部加锁而容易出现死锁情况; -> 4. **对分不分表不友好** :因为分库分表下外键是无法生效的。 -> 5. ...... - -我个人觉得上面这种回答不是特别的全面,只是说了外键存在的一个常见的问题。实际上,我们知道外键也是有很多好处的,比如: - -1. 保证了数据库数据的一致性和完整性; -2. 级联操作方便,减轻了程序代码量; -3. ...... - -所以说,不要一股脑的就抛弃了外键这个概念,既然它存在就有它存在的道理,如果系统不涉及分不分表,并发量不是很高的情况还是可以考虑使用外键的。 - -我个人是不太喜欢外键约束,比较喜欢在应用层去进行相关操作。 - -## 关于@Transactional注解 - -对于`@Transactional`事务注解,阿里巴巴开发手册这样说到: - ->【参考】@Transactional事务不要滥用。事务会影响数据库的QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。 diff --git a/docs/distributed-system/api-gateway.md b/docs/distributed-system/api-gateway.md new file mode 100644 index 00000000000..11bab874ff1 --- /dev/null +++ b/docs/distributed-system/api-gateway.md @@ -0,0 +1,107 @@ + +# 网关 + +## 何为网关?为什么要网关? + +![微服务-网关](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/%E5%BE%AE%E6%9C%8D%E5%8A%A1-%E7%BD%91%E5%85%B3.png) + +微服务背景下,一个系统被拆分为多个服务,但是像安全认证,流量控制,日志,监控等功能是每个服务都需要的,没有网关的话,我们就需要在每个服务中单独实现,这使得我们做了很多重复的事情并且没有一个全局的视图来统一管理这些功能。 + +综上:**一般情况下,网关都会提供请求转发、安全认证(身份/权限认证)、流量控制、负载均衡、容灾、日志、监控这些功能。** + +上面介绍了这么多功能,实际上,网关主要做了一件事情:**请求过滤** 。 + +## 有哪些常见的网关系统? + +### Netflix Zuul + +Zuul 是 Netflix 开发的一款提供动态路由、监控、弹性、安全的网关服务。 + +Zuul 主要通过过滤器(类似于 AOP)来过滤请求,从而实现网关必备的各种功能。 + +![Zuul架构](https://img-blog.csdnimg.cn/img_convert/865991e34f69f8cb345b4aff918e946e.png) + +我们可以自定义过滤器来处理请求,并且,Zuul 生态本身就有很多现成的过滤器供我们使用。就比如限流可以直接用国外朋友写的 [spring-cloud-zuul-ratelimit](https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit) (这里只是举例说明,一般是配合 hystrix 来做限流): + +```xml + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + + + com.marcosbarbero.cloud + spring-cloud-zuul-ratelimit + 2.2.0.RELEASE + +``` + +Zuul 1.x 基于同步 IO,性能较差。Zuul 2.x 基于 Netty 实现了异步 IO,性能得到了大幅改进。 + +- Github 地址 : https://github.com/Netflix/zuul +- 官方 Wiki : https://github.com/Netflix/zuul/wiki + +### Spring Cloud Gateway + +SpringCloud Gateway 属于 Spring Cloud 生态系统中的网关,其诞生的目标是为了替代老牌网关 **Zuul **。准确点来说,应该是 Zuul 1.x。SpringCloud Gateway 起步要比 Zuul 2.x 更早。 + +为了提升网关的性能,SpringCloud Gateway 基于 Spring WebFlux 。Spring WebFlux 使用 Reactor 库来实现响应式编程模型,底层基于 Netty 实现异步 IO。 + +Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。 + +Spring Cloud Gateway 和 Zuul 2.x 的差别不大,也是通过过滤器来处理请求。不过,目前更加推荐使用 Spring Cloud Gateway 而非 Zuul,Spring Cloud 生态对其支持更加友好。 + +- Github 地址 : https://github.com/spring-cloud/spring-cloud-gateway +- 官网 : https://spring.io/projects/spring-cloud-gateway + +### Kong + +Kong 是一款基于 [OpenResty](https://github.com/openresty/) 的高性能、云原生、可扩展的网关系统。 + +> OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。 + +Kong 提供了插件机制来扩展其功能。比如、在服务上启用 Zipkin 插件 + +```shell +$ curl -X POST http://kong:8001/services/{service}/plugins \ + --data "name=zipkin" \ + --data "config.http_endpoint=http://your.zipkin.collector:9411/api/v2/spans" \ + --data "config.sample_ratio=0.001" +``` + +- Github 地址: https://github.com/Kong/kong +- 官网地址 : https://konghq.com/kong + +### APISIX + +APISIX 是一款基于 Nginx 和 etcd 的高性能、云原生、可扩展的网关系统。 + +> *etcd*是使用 Go 语言开发的一个开源的、高可用的分布式 key-value 存储系统,使用 Raft 协议做分布式共识。 + +与传统 API 网关相比,APISIX 具有动态路由和插件热加载,特别适合微服务系统下的 API 管理。并且,APISIX 与 SkyWalking(分布式链路追踪系统)、Zipkin(分布式链路追踪系统)、Prometheus(监控系统) 等 DevOps 生态工具对接都十分方便。 + +![apisix架构图](https://img-blog.csdnimg.cn/img_convert/727732fad2e943bdd2c502b83ddb1b89.png) + +作为 NGINX 和 Kong 的替代项目,APISIX 目前已经是 Apache 顶级开源项目,并且是最快毕业的国产开源项目。国内目前已经有很多知名企业(比如金山、有赞、爱奇艺、腾讯、贝壳)使用 APISIX 处理核心的业务流量。 + +根据官网介绍:“APISIX 已经生产可用,功能、性能、架构全面优于 Kong”。 + +- Github 地址 :https://github.com/apache/apisix +- 官网地址: https://apisix.apache.org/zh/ + +相关阅读: + +- [有了 NGINX 和 Kong,为什么还需要 Apache APISIX](https://www.apiseven.com/zh/blog/why-we-need-Apache-APISIX) +- [APISIX 技术博客](https://www.apiseven.com/zh/blog) +- [APISIX 用户案例](https://www.apiseven.com/zh/usercases) + +### Shenyu + +Shenyu 是一款基于 WebFlux 的可扩展、高性能、响应式网关,Apache 顶级开源项目。 + +![Shenyu架构](https://img-blog.csdnimg.cn/1104eb413cba468cba4dce119165e84e.png) + +Shenyu 通过插件扩展功能,插件是 ShenYu 的灵魂,并且插件也是可扩展和热插拔的。不同的插件实现不同的功能。Shenyu 自带了诸如限流、熔断、转发 、重写、重定向、和路由监控等插件。 + +- Github 地址: https://github.com/apache/incubator-shenyu +- 官网地址 : https://shenyu.apache.org/ + diff --git a/docs/distributed-system/distributed-id.md b/docs/distributed-system/distributed-id.md new file mode 100644 index 00000000000..0ec45819e55 --- /dev/null +++ b/docs/distributed-system/distributed-id.md @@ -0,0 +1,357 @@ +# 分布式 ID + +## 分布式 ID 介绍 + +### 何为 ID? + +日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/up-79beb853b8319f850638c9708f83039dfda.png) + +我们现实生活中也有各种 ID,比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应 + +简单来说,**ID 就是数据的唯一标识**。 + +### 何为分布式 ID? + +分布式 ID 是分布式系统下的 ID。分布式 ID 不存在与现实生活中,属于计算机系统中的一个概念。 + +我简单举一个分库分表的例子。 + +我司的一个项目,使用的是单机 MySQL 。但是,没想到的是,项目上线一个月之后,随着使用人数越来越多,整个系统的数据量将越来越大。 + +单机 MySQL 已经没办法支撑了,需要进行分库分表(推荐 Sharding-JDBC)。 + +在分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。**我们如何为不同的数据节点生成全局唯一主键呢?** + +![](https://oscimg.oschina.net/oscnet/up-d78d9d5362c71f4713a090baf7ec65d2b6d.png) + +这个时候就需要生成**分布式 ID**了。 + +### 分布式 ID 需要满足哪些要求? + +![](https://img-blog.csdnimg.cn/20210610082309988.png) + +分布式 ID 作为分布式系统中必不可少的一环,很多地方都要用到分布式 ID。 + +一个最基本的分布式 ID 需要满足下面这些要求: + +- **全局唯一** :ID 的全局唯一性肯定是首先要满足的! +- **高性能** : 分布式 ID 的生成速度要快,对本地资源消耗要小。 +- **高可用** :生成分布式 ID 的服务要保证可用性无限接近于 100%。 +- **方便易用** :拿来即用,使用方便,快速接入! + +除了这些之外,一个比较好的分布式 ID 还应保证: + +- **安全** :ID 中不包含敏感信息。 +- **有序递增** :如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候 ,我们还很有可能会直接通过 ID 来进行排序。 +- **有具体的业务含义** :生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。 +- **独立部署** :也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。 + +## 分布式 ID 常见解决方案 + +### 数据库 + +#### 数据库主键自增 + +这种方式就比较简单直白了,就是通过关系型数据库的自增主键产生来唯一的 ID。 + +![](https://img-blog.csdnimg.cn/20210610081957287.png) + +以 MySQL 举例,我们通过下面的方式即可。 + +**1.创建一个数据库表。** + +```sql +CREATE TABLE `sequence_id` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `stub` char(10) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + UNIQUE KEY `stub` (`stub`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +`stub` 字段无意义,只是为了占位,便于我们插入或者修改数据。并且,给 `stub` 字段创建了唯一索引,保证其唯一性。 + +**2.通过 `replace into` 来插入数据。** + +```java +BEGIN; +REPLACE INTO sequence_id (stub) VALUES ('stub'); +SELECT LAST_INSERT_ID(); +COMMIT; +``` + +插入数据这里,我们没有使用 `insert into` 而是使用 `replace into` 来插入数据,具体步骤是这样的: + +1)第一步: 尝试把数据插入到表中。 + +2)第二步: 如果主键或唯一索引字段出现重复数据错误而插入失败时,先从表中删除含有重复关键字值的冲突行,然后再次尝试把数据插入到表中。 + +这种方式的优缺点也比较明显: + +- **优点** :实现起来比较简单、ID 有序递增、存储消耗空间小 +- **缺点** : 支持的并发量不大、存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )、每次获取 ID 都要访问一次数据库(增加了对数据库的压力,获取速度也慢) + +#### 数据库号段模式 + +数据库主键自增这种模式,每次获取 ID 都要访问一次数据库,ID 需求比较大的时候,肯定是不行的。 + +如果我们可以批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面拿就舒服了!这也就是我们说的 **基于数据库的号段模式来生成分布式 ID。** + +数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的[Tinyid](https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D) 就是基于这种方式来做的。不过,TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化。 + +以 MySQL 举例,我们通过下面的方式即可。 + +**1.创建一个数据库表。** + +```sql +CREATE TABLE `sequence_id_generator` ( + `id` int(10) NOT NULL, + `current_max_id` bigint(20) NOT NULL COMMENT '当前最大id', + `step` int(10) NOT NULL COMMENT '号段的长度', + `version` int(20) NOT NULL COMMENT '版本号', + `biz_type` int(20) NOT NULL COMMENT '业务类型', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +`current_max_id` 字段和`step`字段主要用于获取批量 ID,获取的批量 id 为: `current_max_id ~ current_max_id+step`。 + +![](https://img-blog.csdnimg.cn/20210610081149228.png) + +`version` 字段主要用于解决并发问题(乐观锁),`biz_type` 主要用于表示业余类型。 + +**2.先插入一行数据。** + +```sql +INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`) +VALUES + (1, 0, 100, 0, 101); +``` + +**3.通过 SELECT 获取指定业务下的批量唯一 ID** + +```sql +SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101 +``` + +结果: + +``` +id current_max_id step version biz_type +1 0 100 1 101 +``` + +**4.不够用的话,更新之后重新 SELECT 即可。** + +```sql +UPDATE sequence_id_generator SET current_max_id = 0+100, version=version+1 WHERE version = 0 AND `biz_type` = 101 +SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101 +``` + +结果: + +``` +id current_max_id step version biz_type +1 100 100 1 101 +``` + +相比于数据库主键自增的方式,**数据库的号段模式对于数据库的访问次数更少,数据库压力更小。** + +另外,为了避免单点问题,你可以从使用主从模式来提高可用性。 + +**数据库号段模式的优缺点:** + +- **优点** :ID 有序递增、存储消耗空间小 +- **缺点** :存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! ) + +#### NoSQL + +![](https://img-blog.csdnimg.cn/2021061008245858.png) + +一般情况下,NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 `incr` 命令即可实现对 id 原子顺序递增。 + +```bash +127.0.0.1:6379> set sequence_id_biz_type 1 +OK +127.0.0.1:6379> incr sequence_id_biz_type +(integer) 2 +127.0.0.1:6379> get sequence_id_biz_type +"2" +``` + +为了提高可用性和并发,我们可以使用 Redis Cluser。Redis Cluser 是 Redis 官方提供的 Redis 集群解决方案(3.0+版本)。 + +除了 Redis Cluser 之外,你也可以使用开源的 Redis 集群方案[Codis](https://github.com/CodisLabs/codis) (大规模集群比如上百个节点的时候比较推荐)。 + +除了高可用和并发之外,我们知道 Redis 基于内存,我们需要持久化数据,避免重启机器或者机器故障后数据丢失。Redis 支持两种不同的持久化方式:**快照(snapshotting,RDB)**、**只追加文件(append-only file, AOF)**。 并且,Redis 4.0 开始支持 **RDB 和 AOF 的混合持久化**(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。 + +关于 Redis 持久化,我这里就不过多介绍。不了解这部分内容的小伙伴,可以看看 [JavaGuide 对于 Redis 知识点的总结](https://snailclimb.gitee.io/javaguide/#/docs/database/Redis/redis-all)。 + +**Redis 方案的优缺点:** + +- **优点** : 性能不错并且生成的 ID 是有序递增的 +- **缺点** : 和数据库主键自增方案的缺点类似 + +除了 Redis 之外,MongoDB ObjectId 经常也会被拿来当做分布式 ID 的解决方案。 + +![](https://img-blog.csdnimg.cn/20210207103320582.png) + +MongoDB ObjectId 一共需要 12 个字节存储: + +- 0~3:时间戳 +- 3~6: 代表机器 ID +- 7~8:机器进程 ID +- 9~11 :自增值 + +**MongoDB 方案的优缺点:** + +- **优点** : 性能不错并且生成的 ID 是有序递增的 +- **缺点** : 需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID) 、有安全性问题(ID 生成有规律性) + +### 算法 + +#### UUID + +UUID 是 Universally Unique Identifier(通用唯一标识符) 的缩写。UUID 包含 32 个 16 进制数字(8-4-4-4-12)。 + +JDK 就提供了现成的生成 UUID 的方法,一行代码就行了。 + +```java +//输出示例:cb4a9ede-fa5e-4585-b9bb-d60bce986eaa +UUID.randomUUID() +``` + +[RFC 4122](https://tools.ietf.org/html/rfc4122) 中关于 UUID 的示例是这样的: + +![](https://img-blog.csdnimg.cn/20210202110824430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70) + +我们这里重点关注一下这个 Version(版本),不同的版本对应的 UUID 的生成规则是不同的。 + +5 种不同的 Version(版本)值分别对应的含义(参考[维基百科对于 UUID 的介绍](https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%94%AF%E4%B8%80%E8%AF%86%E5%88%AB%E7%A0%81)): + +- **版本 1** : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成; +- **版本 2** : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成; +- **版本 3、版本 5** : 版本 5 - 确定性 UUID 通过散列(hashing)名字空间(namespace)标识符和名称生成; +- **版本 4** : UUID 使用[随机性](https://zh.wikipedia.org/wiki/随机性)或[伪随机性](https://zh.wikipedia.org/wiki/伪随机性)生成。 + +下面是 Version 1 版本下生成的 UUID 的示例: + +![](https://img-blog.csdnimg.cn/20210202113013477.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70) + +JDK 中通过 `UUID` 的 `randomUUID()` 方法生成的 UUID 的版本默认为 4。 + +```java +UUID uuid = UUID.randomUUID(); +int version = uuid.version();// 4 +``` + +另外,Variant(变体)也有 4 种不同的值,这种值分别对应不同的含义。这里就不介绍了,貌似平时也不怎么需要关注。 + +需要用到的时候,去看看维基百科对于 UUID 的 Variant(变体) 相关的介绍即可。 + +从上面的介绍中可以看出,UUID 可以保证唯一性,因为其生成规则包括 MAC 地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,计算机基于这些规则生成的 UUID 是肯定不会重复的。 + +虽然,UUID 可以做到全局唯一性,但是,我们一般很少会使用它。 + +比如使用 UUID 作为 MySQL 数据库主键的时候就非常不合适: + +- 数据库主键要尽量越短越好,而 UUID 的消耗的存储空间比较大(32 个字符串,128 位)。 +- UUID 是无顺序的,InnoDB 引擎下,数据库主键的无序性会严重影响数据库性能。 + +最后,我们再简单分析一下 **UUID 的优缺点** (面试的时候可能会被问到的哦!) : + +- **优点** :生成速度比较快、简单易用 +- **缺点** : 存储消耗空间大(32 个字符串,128 位) 、 不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含义、需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID) + +#### Snowflake(雪花算法) + +Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几部分,每一部分存储的数据都有特定的含义: + +- **第 0 位**: 符号位(标识正负),始终为 0,没有用,不用管。 +- **第 1~41 位** :一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年) +- **第 42~52 位** :一共 10 位,一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(实际项目中可以根据实际情况调整)。这样就可以区分不同集群/机房的节点。 +- **第 53~64 位** :一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID。 + +![](https://oscimg.oschina.net/oscnet/up-a7e54a77b5ab1d9fa16d5ae3a3c50c5aee9.png) + +如果你想要使用 Snowflake 算法的话,一般不需要你自己再造轮子。有很多基于 Snowflake 算法的开源实现比如美团 的 Leaf、百度的 UidGenerator,并且这些开源实现对原有的 Snowflake 算法进行了优化。 + +另外,在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息。 + +我们再来看看 Snowflake 算法的优缺点 : + +- **优点** :生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID) +- **缺点** : 需要解决重复 ID 问题(依赖时间,当机器时间不对的情况下,可能导致会产生重复 ID)。 + +### 开源框架 + +#### UidGenerator(百度) + +[UidGenerator](https://github.com/baidu/uid-generator) 是百度开源的一款基于 Snowflake(雪花算法)的唯一 ID 生成器。 + +不过,UidGenerator 对 Snowflake(雪花算法)进行了改进,生成的唯一 ID 组成如下。 + +![](https://oscimg.oschina.net/oscnet/up-ad5b9dd0077a949db923611b2450277e406.png) + +可以看出,和原始 Snowflake(雪花算法)生成的唯一 ID 的组成不太一样。并且,上面这些参数我们都可以自定义。 + +UidGenerator 官方文档中的介绍如下: + +![](https://oscimg.oschina.net/oscnet/up-358b1a4cddb3675018b8595f66ece9cae88.png) + +自 18 年后,UidGenerator 就基本没有再维护了,我这里也不过多介绍。想要进一步了解的朋友,可以看看 [UidGenerator 的官方介绍](https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md)。 + +#### Leaf(美团) + +**[Leaf](https://github.com/Meituan-Dianping/Leaf)** 是美团开源的一个分布式 ID 解决方案 。这个项目的名字 Leaf(树叶) 起源于德国哲学家、数学家莱布尼茨的一句话: “There are no two identical leaves in the world”(世界上没有两片相同的树叶) 。这名字起得真心挺不错的,有点文艺青年那味了! + +![](https://img-blog.csdnimg.cn/20210422145229617.png) + +Leaf 提供了 **号段模式** 和 **Snowflake(雪花算法)** 这两种模式来生成分布式 ID。并且,它支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper 。 + +Leaf 的诞生主要是为了解决美团各个业务线生成分布式 ID 的方法多种多样以及不可靠的问题。 + +Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避免获取 DB 在获取号段的时候阻塞请求获取 ID 的线程。简单来说,就是我一个号段还没用完之前,我自己就主动提前去获取下一个号段(图片来自于美团官方文章:[《Leaf——美团点评分布式 ID 生成系统》](https://tech.meituan.com/2017/04/21/mt-leaf.html))。 + +![](https://img-blog.csdnimg.cn/20210422144846724.png) + +根据项目 README 介绍,在 4C8G VM 基础上,通过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1ms。 + +#### Tinyid(滴滴) + +[Tinyid](https://github.com/didi/tinyid) 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。 + +数据库号段模式的原理我们在上面已经介绍过了。**Tinyid 有哪些亮点呢?** + +为了搞清楚这个问题,我们先来看看基于数据库号段模式的简单架构方案。(图片来自于 Tinyid 的官方 wiki:[《Tinyid 原理介绍》](https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D)) + +![](https://oscimg.oschina.net/oscnet/up-4afc0e45c0c86ba5ad645d023dce11e53c2.png) + +在这种架构模式下,我们通过 HTTP 请求向发号器服务申请唯一 ID。负载均衡 router 会把我们的请求送往其中的一台 tinyid-server。 + +这种方案有什么问题呢?在我看来(Tinyid 官方 wiki 也有介绍到),主要由下面这 2 个问题: + +- 获取新号段的情况下,程序获取唯一 ID 的速度比较慢。 +- 需要保证 DB 高可用,这个是比较麻烦且耗费资源的。 + +除此之外,HTTP 调用也存在网络开销。 + +Tinyid 的原理比较简单,其架构如下图所示: + +![](https://oscimg.oschina.net/oscnet/up-53f74cd615178046d6c04fe50513fee74ce.png) + +相比于基于数据库号段模式的简单架构方案,Tinyid 方案主要做了下面这些优化: + +- **双号段缓存** :为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。 Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。 +- **增加多 db 支持** :支持多个 DB,并且,每个 DB 都能生成唯一 ID,提高了可用性。 +- **增加 tinyid-client** :纯本地操作,无 HTTP 请求消耗,性能和可用性都有很大提升。 + +Tinyid 的优缺点这里就不分析了,结合数据库号段模式的优缺点和 Tinyid 的原理就能知道。 + +## 分布式 ID 生成方案总结 + +这篇文章中,我基本上已经把最常见的分布式 ID 生成方案都总结了一波。 + +除了上面介绍的方式之外,像 ZooKeeper 这类中间件也可以帮助我们生成唯一 ID。**没有银弹,一定要结合实际项目来选择最适合自己的方案。** \ No newline at end of file diff --git a/docs/distributed-system/distributed-transaction.md b/docs/distributed-system/distributed-transaction.md new file mode 100644 index 00000000000..985fefbee09 --- /dev/null +++ b/docs/distributed-system/distributed-transaction.md @@ -0,0 +1,7 @@ +# 分布式事务 + +这部分内容为我的星球专属,已经整理到了[《Java面试进阶指北 打造个人的技术竞争力》](https://www.yuque.com/docs/share/f37fc804-bfe6-4b0d-b373-9c462188fec7?# )中。 + +欢迎加入我的星球,[一个纯 Java 面试交流圈子 !Ready!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=100015911&idx=1&sn=2e8a0f5acb749ecbcbb417aa8a4e18cc&chksm=4ea1b0ec79d639fae37df1b86f196e8ce397accfd1dd2004bcadb66b4df5f582d90ae0d62448#rd) (点击链接了解星球详细信息,还有专属优惠款可以领取)。 + +![](https://img-blog.csdnimg.cn/57cedfa4d3d1425a8e4c6a6807d8f732.png) diff --git a/docs/distributed-system/rpc/dubbo.md b/docs/distributed-system/rpc/dubbo.md new file mode 100644 index 00000000000..870cc45dc5f --- /dev/null +++ b/docs/distributed-system/rpc/dubbo.md @@ -0,0 +1,501 @@ +# Dubbo知识点&面试题总结 + +这篇文章是我根据官方文档以及自己平时的使用情况,对 Dubbo 所做的一个总结。欢迎补充! + +## RPC基础 + +### 何为 RPC? + +**RPC(Remote Procedure Call)** 即远程过程调用,通过名字我们就能看出 RPC 关注的是远程调用而非本地调用。 + +**为什么要 RPC ?** 因为,两个不同的服务器上的服务提供的方法不在一个内存空间,所以,需要通过网络编程才能传递方法调用所需要的参数。并且,方法调用的结果也需要通过网络编程来接收。但是,如果我们自己手动网络编程来实现这个调用过程的话工作量是非常大的,因为,我们需要考虑底层传输方式(TCP还是UDP)、序列化方式等等方面。 + + +**RPC 能帮助我们做什么呢?** 简单来说,通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。并且!我们不需要了解底层网络编程的具体细节。 + + +举个例子:两个不同的服务 A、B 部署在两台不同的机器上,服务 A 如果想要调用服务 B 中的某个方法的话就可以通过 RPC 来做。 + +一言蔽之:**RPC 的出现就是为了让你调用远程方法像调用本地方法一样简单。** + +### RPC 的原理是什么? + +为了能够帮助小伙伴们理解 RPC 原理,我们可以将整个 RPC的 核心功能看作是下面👇 6 个部分实现的: + + +1. **客户端(服务消费端)** :调用远程方法的一端。 +1. **客户端 Stub(桩)** : 这其实就是一代理类。代理类主要做的事情很简单,就是把你调用方法、类、方法参数等信息传递到服务端。 +1. **网络传输** : 网络传输就是你要把你调用的方法的信息比如说参数啊这些东西传输到服务端,然后服务端执行完之后再把返回结果通过网络传输给你传输回来。网络传输的实现方式有很多种比如最近基本的 Socket或者性能以及封装更加优秀的 Netty(推荐)。 +1. **服务端 Stub(桩)** :这个桩就不是代理类了。我觉得理解为桩实际不太好,大家注意一下就好。这里的服务端 Stub 实际指的就是接收到客户端执行方法的请求后,去指定对应的方法然后返回结果给客户端的类。 +1. **服务端(服务提供端)** :提供远程方法的一端。 + +具体原理图如下,后面我会串起来将整个RPC的过程给大家说一下。 + + +![RPC原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/37345851.jpg) + +1. 服务消费端(client)以本地调用的方式调用远程服务; +1. 客户端 Stub(client stub) 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):`RpcRequest`; +1. 客户端 Stub(client stub) 找到远程服务的地址,并将消息发送到服务提供端; +1. 服务端 Stub(桩)收到消息将消息反序列化为Java对象: `RpcRequest`; +1. 服务端 Stub(桩)根据`RpcRequest`中的类、方法、方法参数等信息调用本地的方法; +1. 服务端 Stub(桩)得到方法执行结果并将组装成能够进行网络传输的消息体:`RpcResponse`(序列化)发送至消费方; +1. 客户端 Stub(client stub)接收到消息并将消息反序列化为Java对象:`RpcResponse` ,这样也就得到了最终结果。over! + +相信小伙伴们看完上面的讲解之后,已经了解了 RPC 的原理。 + +虽然篇幅不多,但是基本把 RPC 框架的核心原理讲清楚了!另外,对于上面的技术细节,我会在后面的章节介绍到。 + +**最后,对于 RPC 的原理,希望小伙伴不单单要理解,还要能够自己画出来并且能够给别人讲出来。因为,在面试中这个问题在面试官问到 RPC 相关内容的时候基本都会碰到。** + +## Dubbo基础 + +### 什么是 Dubbo? + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/427f2168-1930-4c14-8760-415fac8db1d0-20200802184737978.png) + +[Apache Dubbo](https://github.com/apache/dubbo) |ˈdʌbəʊ| 是一款高性能、轻量级的开源 Java RPC 框架。 + +根据 [Dubbo 官方文档](https://dubbo.apache.org/zh/)的介绍,Dubbo 提供了六大核心能力 + +1. 面向接口代理的高性能RPC调用。 +2. 智能容错和负载均衡。 +3. 服务自动注册和发现。 +4. 高度可扩展能力。 +5. 运行期流量调度。 +6. 可视化的服务治理与运维。 + +![Dubbo提供的六大核心能力](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/%E6%BA%90%E7%A0%81/dubbo/dubbo%E6%8F%90%E4%BE%9B%E7%9A%84%E5%85%AD%E5%A4%A7%E6%A0%B8%E5%BF%83%E8%83%BD%E5%8A%9B.png) + +简单来说就是: **Dubbo 不光可以帮助我们调用远程服务,还提供了一些其他开箱即用的功能比如智能负载均衡。** + +Dubbo 目前已经有接近 34.4 k 的 Star 。 + +在 **2020 年度 OSC 中国开源项目** 评选活动中,Dubbo 位列开发框架和基础组件类项目的第7名。想比几年前来说,热度和排名有所下降。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/%E6%BA%90%E7%A0%81/dubbo/image-20210107153159545.png) + +Dubbo 是由阿里开源,后来加入了 Apache 。正式由于 Dubbo 的出现,才使得越来越多的公司开始使用以及接受分布式架构。 + +### 为什么要用 Dubbo? + +随着互联网的发展,网站的规模越来越大,用户数量越来越多。单一应用架构 、垂直应用架构无法满足我们的需求,这个时候分布式服务架构就诞生了。 + +分布式服务架构下,系统被拆分成不同的服务比如短信服务、安全服务,每个服务独立提供系统的某个核心服务。 + +我们可以使用 Java RMI(Java Remote Method Invocation)、Hessian这种支持远程调用的框架来简单地暴露和引用远程服务。但是!当服务越来越多之后,服务调用关系越来越复杂。当应用访问压力越来越大后,负载均衡以及服务监控的需求也迫在眉睫。我们可以用 F5 这类硬件来做负载均衡,但这样增加了成本,并且存在单点故障的风险。 + +不过,Dubbo 的出现让上述问题得到了解决。**Dubbo 帮助我们解决了什么问题呢?** + +1. **负载均衡** : 同一个服务部署在不同的机器时该调用哪一台机器上的服务。 +2. **服务调用链路生成** : 随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。 +3. **服务访问压力以及时长统计、资源调度和治理** :基于访问压力实时管理集群容量,提高集群利用率。 +4. ...... + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-26/43050183.jpg) + +另外,Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。不过,由于 Spring Cloud 在微服务中应用更加广泛,所以,我觉得一般我们提 Dubbo 的话,大部分是分布式系统的情况。 + +**我们刚刚提到了分布式这个概念,下面再给大家介绍一下什么是分布式?为什么要分布式?** + +## 分布式基础 + +### 什么是分布式? + +分布式或者说 SOA 分布式重要的就是面向服务,说简单的分布式就是我们把整个系统拆分成不同的服务然后将这些服务放在不同的服务器上减轻单体服务的压力提高并发量和性能。比如电商系统可以简单地拆分成订单系统、商品系统、登录系统等等,拆分之后的每个服务可以部署在不同的机器上,如果某一个服务的访问量比较大的话也可以将这个服务同时部署在多台机器上。 + +![分布式事务示意图](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E7%A4%BA%E6%84%8F%E5%9B%BE.png) + +### 为什么要分布式? + +从开发角度来讲单体应用的代码都集中在一起,而分布式系统的代码根据业务被拆分。所以,每个团队可以负责一个服务的开发,这样提升了开发效率。另外,代码根据业务拆分之后更加便于维护和扩展。 + +另外,我觉得将系统拆分成分布式之后不光便于系统扩展和维护,更能提高整个系统的性能。你想一想嘛?把整个系统拆分成不同的服务/系统,然后每个服务/系统 单独部署在一台服务器上,是不是很大程度上提高了系统性能呢? + +## Dubbo 架构 + +### Dubbo 架构中的核心角色有哪些? + +[官方文档中的框架设计章节](https://dubbo.apache.org/zh/docs/v2.7/dev/design/) 已经介绍的非常详细了,我这里把一些比较重要的点再提一下。 + +![dubbo-relation](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/%E6%BA%90%E7%A0%81/dubbo/dubbo-relation.jpg) + +上述节点简单介绍以及他们之间的关系: + +- **Container:** 服务运行容器,负责加载、运行服务提供者。必须。 +- **Provider:** 暴露服务的服务提供方,会向注册中心注册自己提供的服务。必须。 +- **Consumer:** 调用远程服务的服务消费方,会向注册中心订阅自己所需的服务。必须。 +- **Registry:** 服务注册与发现的注册中心。注册中心会返回服务提供者地址列表给消费者。非必须。 +- **Monitor:** 统计服务的调用次数和调用时间的监控中心。服务消费者和提供者会定时发送统计数据到监控中心。 非必须。 + +### Dubbo 中的 Invoker 概念了解么? + +`Invoker` 是 Dubbo 领域模型中非常重要的一个概念,你如果阅读过 Dubbo 源码的话,你会无数次看到这玩意。就比如下面我要说的负载均衡这块的源码中就有大量 `Invoker` 的身影。 + +简单来说,`Invoker` 就是 Dubbo 对远程调用的抽象。 + +![dubbo_rpc_invoke.jpg](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/dubbo_rpc_invoke.jpg) + +按照 Dubbo 官方的话来说,`Invoker` 分为 + +- 服务提供 `Invoker` +- 服务消费 `Invoker` + +假如我们需要调用一个远程方法,我们需要动态代理来屏蔽远程调用的细节吧!我们屏蔽掉的这些细节就依赖对应的 `Invoker` 实现, `Invoker` 实现了真正的远程服务调用。 + +### Dubbo 的工作原理了解么? + +下图是 Dubbo 的整体设计,从下至上分为十层,各层均为单向依赖。 + +> 左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。 + +![dubbo-framework](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/source-code/dubbo/dubbo-framework.jpg) + +- **config 配置层**:Dubbo相关的配置。支持代码配置,同时也支持基于 Spring 来做配置,以 `ServiceConfig`, `ReferenceConfig` 为中心 +- **proxy 服务代理层**:调用远程方法像调用本地的方法一样简单的一个关键,真实调用过程依赖代理类,以 `ServiceProxy` 为中心。 +- **registry 注册中心层**:封装服务地址的注册与发现。 +- **cluster 路由层**:封装多个提供者的路由及负载均衡,并桥接注册中心,以 `Invoker` 为中心。 +- **monitor 监控层**:RPC 调用次数和调用时间监控,以 `Statistics` 为中心。 +- **protocol 远程调用层**:封装 RPC 调用,以 `Invocation`, `Result` 为中心。 +- **exchange 信息交换层**:封装请求响应模式,同步转异步,以 `Request`, `Response` 为中心。 +- **transport 网络传输层**:抽象 mina 和 netty 为统一接口,以 `Message` 为中心。 +- **serialize 数据序列化层** :对需要在网络传输的数据进行序列化。 + +### Dubbo 的 SPI 机制了解么? 如何扩展 Dubbo 中的默认实现? + +SPI(Service Provider Interface) 机制被大量用在开源项目中,它可以帮助我们动态寻找服务/功能(比如负载均衡策略)的实现。 + +SPI 的具体原理是这样的:我们将接口的实现类放在配置文件中,我们在程序运行过程中读取配置文件,通过反射加载实现类。这样,我们可以在运行的时候,动态替换接口的实现类。和 IoC 的解耦思想是类似的。 + +Java 本身就提供了 SPI 机制的实现。不过,Dubbo 没有直接用,而是对 Java原生的 SPI机制进行了增强,以便更好满足自己的需求。 + +**那我们如何扩展 Dubbo 中的默认实现呢?** + +比如说我们想要实现自己的负载均衡策略,我们创建对应的实现类 `XxxLoadBalance` 实现 `LoadBalance` 接口或者 `AbstractLoadBalance` 类。 + +```java +package com.xxx; + +import org.apache.dubbo.rpc.cluster.LoadBalance; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.RpcException; + +public class XxxLoadBalance implements LoadBalance { + public Invoker select(List> invokers, Invocation invocation) throws RpcException { + // ... + } +} +``` + +我们将这个实现类的路径写入到`resources` 目录下的 `META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance`文件中即可。 + +```java +src + |-main + |-java + |-com + |-xxx + |-XxxLoadBalance.java (实现LoadBalance接口) + |-resources + |-META-INF + |-dubbo + |-org.apache.dubbo.rpc.cluster.LoadBalance (纯文本文件,内容为:xxx=com.xxx.XxxLoadBalance) +``` + +`org.apache.dubbo.rpc.cluster.LoadBalance` + +``` +xxx=com.xxx.XxxLoadBalance +``` + +其他还有很多可供扩展的选择,你可以在[官方文档@SPI扩展实现](https://dubbo.apache.org/zh/docs/v2.7/dev/impls/)这里找到。 + +![](https://img-blog.csdnimg.cn/20210328091015555.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70) + +### Dubbo 的微内核架构了解吗? + +Dubbo 采用 微内核(Microkernel) + 插件(Plugin) 模式,简单来说就是微内核架构。微内核只负责组装插件。 + +**何为微内核架构呢?** 《软件架构模式》 这本书是这样介绍的: + +> 微内核架构模式(有时被称为插件架构模式)是实现基于产品应用程序的一种自然模式。基于产品的应用程序是已经打包好并且拥有不同版本,可作为第三方插件下载的。然后,很多公司也在开发、发布自己内部商业应用像有版本号、说明及可加载插件式的应用软件(这也是这种模式的特征)。微内核系统可让用户添加额外的应用如插件,到核心应用,继而提供了可扩展性和功能分离的用法。 + +微内核架构包含两类组件:**核心系统(core system)** 和 **插件模块(plug-in modules)**。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/source-code/dubbo/%E5%BE%AE%E5%86%85%E6%A0%B8%E6%9E%B6%E6%9E%84%E7%A4%BA%E6%84%8F%E5%9B%BE.png) + +核心系统提供系统所需核心能力,插件模块可以扩展系统的功能。因此, 基于微内核架构的系统,非常易于扩展功能。 + +我们常见的一些IDE,都可以看作是基于微内核架构设计的。绝大多数 IDE比如IDEA、VSCode都提供了插件来丰富自己的功能。 + +正是因为Dubbo基于微内核架构,才使得我们可以随心所欲替换Dubbo的功能点。比如你觉得Dubbo 的序列化模块实现的不满足自己要求,没关系啊!你自己实现一个序列化模块就好了啊! + +通常情况下,微核心都会采用 Factory、IoC、OSGi 等方式管理插件生命周期。Dubbo 不想依赖 Spring 等 IoC 容器,也不想自己造一个小的 IoC 容器(过度设计),因此采用了一种最简单的 Factory 方式管理插件 :**JDK 标准的 SPI 扩展机制** (`java.util.ServiceLoader`)。 + +### 关于Dubbo架构的一些自测小问题 + +#### 注册中心的作用了解么? + +注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互。 + +#### 服务提供者宕机后,注册中心会做什么? + +注册中心会立即推送事件通知消费者。 + +#### 监控中心的作用呢? + +监控中心负责统计各服务调用次数,调用时间等。 + +#### 注册中心和监控中心都宕机的话,服务都会挂掉吗? + +不会。两者都宕机也不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表。注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。 + + +## Dubbo 的负载均衡策略 + +### 什么是负载均衡? + +先来看一下稍微官方点的解释。下面这段话摘自维基百科对负载均衡的定义: + +> 负载均衡改善了跨多个计算资源(例如计算机,计算机集群,网络链接,中央处理单元或磁盘驱动)的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间,并避免任何单个资源的过载。使用具有负载平衡而不是单个组件的多个组件可以通过冗余提高可靠性和可用性。负载平衡通常涉及专用软件或硬件。 + +**上面讲的大家可能不太好理解,再用通俗的话给大家说一下。** + +我们的系统中的某个服务的访问量特别大,我们将这个服务部署在了多台服务器上,当客户端发起请求的时候,多台服务器都可以处理这个请求。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。 + +### Dubbo 提供的负载均衡策略有哪些? + +在集群负载均衡时,Dubbo 提供了多种均衡策略,默认为 `random` 随机调用。我们还可以自行扩展负载均衡策略(参考Dubbo SPI机制)。 + +在 Dubbo 中,所有负载均衡实现类均继承自 `AbstractLoadBalance`,该类实现了 `LoadBalance` 接口,并封装了一些公共的逻辑。 + +```java +public abstract class AbstractLoadBalance implements LoadBalance { + + static int calculateWarmupWeight(int uptime, int warmup, int weight) { + } + + @Override + public Invoker select(List> invokers, URL url, Invocation invocation) { + } + + protected abstract Invoker doSelect(List> invokers, URL url, Invocation invocation); + + + int getWeight(Invoker invoker, Invocation invocation) { + + } +} +``` + +`AbstractLoadBalance` 的实现类有下面这些: + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/image-20210326105257812.png) + +官方文档对负载均衡这部分的介绍非常详细,推荐小伙伴们看看,地址:[https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/#m-zhdocsv27devsourceloadbalance](https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/#m-zhdocsv27devsourceloadbalance ) 。 + +#### RandomLoadBalance + +根据权重随机选择(对加权随机算法的实现)。这是Dubbo默认采用的一种负载均衡策略。 + +` RandomLoadBalance` 具体的实现原理非常简单,假如有两个提供相同服务的服务器 S1,S2,S1的权重为7,S2的权重为3。 + +我们把这些权重值分布在坐标区间会得到:S1->[0, 7) ,S2->[7, 10)。我们生成[0, 10) 之间的随机数,随机数落到对应的区间,我们就选择对应的服务器来处理请求。 + +![RandomLoadBalance](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/%20RandomLoadBalance.png) + +`RandomLoadBalance` 的源码非常简单,简单花几分钟时间看一下。 + +> 以下源码来自 Dubbo master 分支上的最新的版本 2.7.9。 + +```java +public class RandomLoadBalance extends AbstractLoadBalance { + + public static final String NAME = "random"; + + @Override + protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { + + int length = invokers.size(); + boolean sameWeight = true; + int[] weights = new int[length]; + int totalWeight = 0; + // 下面这个for循环的主要作用就是计算所有该服务的提供者的权重之和 totalWeight(), + // 除此之外,还会检测每个服务提供者的权重是否相同 + for (int i = 0; i < length; i++) { + int weight = getWeight(invokers.get(i), invocation); + totalWeight += weight; + weights[i] = totalWeight; + if (sameWeight && totalWeight != weight * (i + 1)) { + sameWeight = false; + } + } + if (totalWeight > 0 && !sameWeight) { + // 随机生成一个 [0, totalWeight) 区间内的数字 + int offset = ThreadLocalRandom.current().nextInt(totalWeight); + // 判断会落在哪个服务提供者的区间 + for (int i = 0; i < length; i++) { + if (offset < weights[i]) { + return invokers.get(i); + } + } + + return invokers.get(ThreadLocalRandom.current().nextInt(length)); + } + +} + +``` + +#### LeastActiveLoadBalance + +`LeastActiveLoadBalance` 直译过来就是**最小活跃数负载均衡**。 + +这个名字起得有点不直观,不仔细看官方对活跃数的定义,你压根不知道这玩意是干嘛的。 + +我这么说吧!初始状态下所有服务提供者的活跃数均为 0(每个服务提供者的中特定方法都对应一个活跃数,我在后面的源码中会提到),每收到一个请求后,对应的服务提供者的活跃数 +1,当这个请求处理完之后,活跃数 -1。 + +因此,**Dubbo 就认为谁的活跃数越少,谁的处理速度就越快,性能也越好,这样的话,我就优先把请求给活跃数少的服务提供者处理。** + +**如果有多个服务提供者的活跃数相等怎么办?** + +很简单,那就再走一遍 `RandomLoadBalance` 。 + +```java +public class LeastActiveLoadBalance extends AbstractLoadBalance { + + public static final String NAME = "leastactive"; + + @Override + protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { + int length = invokers.size(); + int leastActive = -1; + int leastCount = 0; + int[] leastIndexes = new int[length]; + int[] weights = new int[length]; + int totalWeight = 0; + int firstWeight = 0; + boolean sameWeight = true; + // 这个 for 循环的主要作用是遍历 invokers 列表,找出活跃数最小的 Invoker + // 如果有多个 Invoker 具有相同的最小活跃数,还会记录下这些 Invoker 在 invokers 集合中的下标,并累加它们的权重,比较它们的权重值是否相等 + for (int i = 0; i < length; i++) { + Invoker invoker = invokers.get(i); + // 获取 invoker 对应的活跃(active)数 + int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); + int afterWarmup = getWeight(invoker, invocation); + weights[i] = afterWarmup; + if (leastActive == -1 || active < leastActive) { + leastActive = active; + leastCount = 1; + leastIndexes[0] = i; + totalWeight = afterWarmup; + firstWeight = afterWarmup; + sameWeight = true; + } else if (active == leastActive) { + leastIndexes[leastCount++] = i; + totalWeight += afterWarmup; + if (sameWeight && afterWarmup != firstWeight) { + sameWeight = false; + } + } + } + // 如果只有一个 Invoker 具有最小的活跃数,此时直接返回该 Invoker 即可 + if (leastCount == 1) { + return invokers.get(leastIndexes[0]); + } + // 如果有多个 Invoker 具有相同的最小活跃数,但它们之间的权重不同 + // 这里的处理方式就和 RandomLoadBalance 一致了 + if (!sameWeight && totalWeight > 0) { + int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight); + for (int i = 0; i < leastCount; i++) { + int leastIndex = leastIndexes[i]; + offsetWeight -= weights[leastIndex]; + if (offsetWeight < 0) { + return invokers.get(leastIndex); + } + } + } + return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]); + } +} + +``` + +活跃数是通过 `RpcStatus` 中的一个 `ConcurrentMap` 保存的,根据 URL 以及服务提供者被调用的方法的名称,我们便可以获取到对应的活跃数。也就是说服务提供者中的每一个方法的活跃数都是互相独立的。 + +```java +public class RpcStatus { + + private static final ConcurrentMap> METHOD_STATISTICS = + new ConcurrentHashMap>(); + + public static RpcStatus getStatus(URL url, String methodName) { + String uri = url.toIdentityString(); + ConcurrentMap map = METHOD_STATISTICS.computeIfAbsent(uri, k -> new ConcurrentHashMap<>()); + return map.computeIfAbsent(methodName, k -> new RpcStatus()); + } + public int getActive() { + return active.get(); + } + +} +``` + +#### ConsistentHashLoadBalance + +`ConsistentHashLoadBalance` 小伙伴们应该也不会陌生,在分库分表、各种集群中就经常使用这个负载均衡策略。 + +`ConsistentHashLoadBalance` 即**一致性Hash负载均衡策略**。 `ConsistentHashLoadBalance` 中没有权重的概念,具体是哪个服务提供者处理请求是由你的请求的参数决定的,也就是说相同参数的请求总是发到同一个服务提供者。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/consistent-hash-data-incline.jpg) + +另外,Dubbo 为了避免数据倾斜问题(节点不够分散,大量请求落到同一节点),还引入了虚拟节点的概念。通过虚拟节点可以让节点更加分散,有效均衡各个节点的请求量。 + + + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/consistent-hash-invoker.jpg) + +官方有详细的源码分析:[https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/#23-consistenthashloadbalance](https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/#23-consistenthashloadbalance) 。这里还有一个相关的 [PR#5440](https://github.com/apache/dubbo/pull/5440) 来修复老版本中 ConsistentHashLoadBalance 存在的一些Bug。感兴趣的小伙伴,可以多花点时间研究一下。我这里不多分析了,这个作业留给你们! + +#### RoundRobinLoadBalance + +加权轮询负载均衡。 + +轮询就是把请求依次分配给每个服务提供者。加权轮询就是在轮询的基础上,让更多的请求落到权重更大的服务提供者上。比如假如有两个提供相同服务的服务器 S1,S2,S1的权重为7,S2的权重为3。 + +如果我们有 10 次请求,那么 7 次会被 S1处理,3次被 S2处理。 + +但是,如果是 `RandomLoadBalance` 的话,很可能存在10次请求有9次都被 S1 处理的情况(概率性问题)。 + +Dubbo 中的 `RoundRobinLoadBalance` 的代码实现被修改重建了好几次,Dubbo-2.6.5 版本的 `RoundRobinLoadBalance` 为平滑加权轮询算法。 + +## Dubbo序列化协议 + +### Dubbo 支持哪些序列化方式呢? + +![](https://img-blog.csdnimg.cn/20210328092219640.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70) + +Dubbo 支持多种序列化方式:JDK自带的序列化、hessian2、JSON、Kryo、FST、Protostuff,ProtoBuf等等。 + +Dubbo 默认使用的序列化方式是 hession2。 + +### 谈谈你对这些序列化协议了解? + +一般我们不会直接使用 JDK 自带的序列化方式。主要原因有两个: + +1. **不支持跨语言调用** : 如果调用的是其他语言开发的服务的时候就不支持了。 +2. **性能差** :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。 + +JSON 序列化由于性能问题,我们一般也不会考虑使用。 + +像 Protostuff,ProtoBuf、hessian2这些都是跨语言的序列化方式,如果有跨语言需求的话可以考虑使用。 + +Kryo和FST这两种序列化方式是 Dubbo 后来才引入的,性能非常好。不过,这两者都是专门针对 Java 语言的。Dubbo 官网的一篇文章中提到说推荐使用 Kryo 作为生产环境的序列化方式。(文章地址:[https://dubbo.apache.org/zh/docs/v2.7/user/references/protocol/rest/](https://dubbo.apache.org/zh/docs/v2.7/user/references/protocol/rest/)) + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/569e541a-22b2-4846-aa07-0ad479f07440.png) + +Dubbo 官方文档中还有一个关于这些[序列化协议的性能对比图](https://dubbo.apache.org/zh/docs/v2.7/user/serialization/#m-zhdocsv27userserialization)可供参考。 + +![](https://img-blog.csdnimg.cn/20210328093219609.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70) + diff --git a/docs/system-design/data-communication/why-use-rpc.md b/docs/distributed-system/rpc/why-use-rpc.md similarity index 65% rename from docs/system-design/data-communication/why-use-rpc.md rename to docs/distributed-system/rpc/why-use-rpc.md index f295e13beed..a2fe5dbefa2 100644 --- a/docs/system-design/data-communication/why-use-rpc.md +++ b/docs/distributed-system/rpc/why-use-rpc.md @@ -1,3 +1,5 @@ +# 服务之间的调用为啥不直接用 HTTP 而用 RPC? + ## 什么是 RPC?RPC原理是什么? ### **什么是 RPC?** @@ -6,25 +8,19 @@ RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络 ### **RPC原理是什么?** -我这里这是简单的提一下,详细内容可以查看下面这篇文章: - -http://www.importnew.com/22003.html - -![RPC原理图](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/37345851.jpg) +![RPC原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/37345851.jpg) -1. 服务消费方(client)调用以本地调用方式调用服务; -2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体; -3. client stub找到服务地址,并将消息发送到服务端; -4. server stub收到消息后进行解码; -5. server stub根据解码结果调用本地的服务; -6. 本地服务执行并将结果返回给server stub; -7. server stub将返回结果打包成消息并发送至消费方; -8. client stub接收到消息,并进行解码; -9. 服务消费方得到最终结果。 +1. 服务消费端(client)以本地调用的方式调用远程服务; +2. 客户端 Stub(client stub) 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):`RpcRequest`; +3. 客户端 Stub(client stub) 找到远程服务的地址,并将消息发送到服务提供端; +4. 服务端 Stub(桩)收到消息将消息反序列化为Java对象: `RpcRequest`; +5. 服务端 Stub(桩)根据`RpcRequest`中的类、方法、方法参数等信息调用本地的方法; +6. 服务端 Stub(桩)得到方法执行结果并将组装成能够进行网络传输的消息体:`RpcResponse`(序列化)发送至消费方; +7. 客户端 Stub(client stub)接收到消息并将消息反序列化为Java对象:`RpcResponse` ,这样也就得到了最终结果。 -下面再贴一个网上的时序图: +下面再贴一个网上的时序图,辅助理解: -![RPC原理时序图](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/32527396.jpg) +![RPC原理时序图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-6/32527396.jpg) ### RPC 解决了什么问题? @@ -35,13 +31,16 @@ http://www.importnew.com/22003.html - **RMI(JDK自带):** JDK自带的RPC,有很多局限性,不推荐使用。 - **Dubbo:** Dubbo是 阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。目前 Dubbo 已经成为 Spring Cloud Alibaba 中的官方组件。 - **gRPC** :gRPC是可以在任何环境中运行的现代开源高性能RPC框架。它可以通过可插拔的支持来有效地连接数据中心内和跨数据中心的服务,以实现负载平衡,跟踪,运行状况检查和身份验证。它也适用于分布式计算的最后一英里,以将设备,移动应用程序和浏览器连接到后端服务。 - -- **Hessian:** Hessian是一个轻量级的remotingonhttp工具,使用简单的方法提供了RMI的功能。 相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。 +- **Hessian:** Hessian是一个轻量级的remoting on http工具,使用简单的方法提供了RMI的功能。 相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。 - **Thrift:** Apache Thrift是Facebook开源的跨语言的RPC通信框架,目前已经捐献给Apache基金会管理,由于其跨语言特性和出色的性能,在很多互联网公司得到应用,有能力的公司甚至会基于thrift研发一套分布式服务框架,增加诸如服务注册、服务发现等功能。 +### RPC学习材料 + +- [跟着 Guide 哥造轮子](https://github.com/Snailclimb/guide-rpc-framework) + ## 既有 HTTP ,为啥用 RPC 进行服务调用? -###RPC 只是一种设计而已 +### RPC 只是一种设计而已 RPC 只是一种概念、一种设计,就是为了解决 **不同服务之间的调用问题**, 它一般会包含有 **传输协议** 和 **序列化协议** 这两个。 @@ -53,9 +52,9 @@ RPC 只是一种概念、一种设计,就是为了解决 **不同服务之间 > 我们通常谈计算机网络的五层协议的体系结构是指:应用层、传输层、网络层、数据链路层、物理层。 > -> **应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。**HTTP 属于应用层协议,它会基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过 URL 向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。HTTP协议建立在 TCP 协议之上。 +> **应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。** HTTP 属于应用层协议,它会基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过 URL 向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。HTTP协议建立在 TCP 协议之上。 > -> **运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务**。TCP是传输层协议,主要解决数据如何在网络中传输。相比于UDP,**TCP** 提供的是**面向连接**的,**可靠的**数据传输服务。 +> **传输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务**。TCP是传输层协议,主要解决数据如何在网络中传输。相比于UDP,**TCP** 提供的是**面向连接**的,**可靠的**数据传输服务。 ### RPC框架功能更齐全 @@ -68,7 +67,7 @@ RPC 进行服务注册和发现的一方面原因吧! ### 一个常见的错误观点 -很多文章中还会提到说 HTTP 协议相较于自定义 TCP 报文协议,增加的开销在于连接的建立与断开,但是这个观点已经被否认,下面截取自知乎中一个回答,原回答地址:https://www.zhihu.com/question/41609070/answer/191965937。 +很多文章中还会提到说 HTTP 协议相较于自定义 TCP 报文协议,增加的开销在于连接的建立与断开,但是这个观点已经被否认,下面截取自知乎中一个回答,原回答地址:https://www.zhihu.com/question/41609070/answer/191965937 。 >首先要否认一点 HTTP 协议相较于自定义 TCP 报文协议,增加的开销在于连接的建立与断开。HTTP 协议是支持连接池复用的,也就是建立一定数量的连接不断开,并不会频繁的创建和销毁连接。二一要说的是 HTTP 也可以使用 Protobuf 这种二进制编码协议对内容进行编码,因此二者最大的区别还是在传输协议上。 diff --git "a/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/curator.png" "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/curator.png" new file mode 100644 index 00000000000..28da0247ec3 Binary files /dev/null and "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/curator.png" differ diff --git "a/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/watche\346\234\272\345\210\266.png" "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/watche\346\234\272\345\210\266.png" new file mode 100644 index 00000000000..68144db1e54 Binary files /dev/null and "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/watche\346\234\272\345\210\266.png" differ diff --git "a/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/znode-structure.png" "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/znode-structure.png" new file mode 100644 index 00000000000..746c3f6e3a4 Binary files /dev/null and "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/znode-structure.png" differ diff --git "a/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/zookeeper\351\233\206\347\276\244.png" "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/zookeeper\351\233\206\347\276\244.png" new file mode 100644 index 00000000000..a3067cda18e Binary files /dev/null and "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/zookeeper\351\233\206\347\276\244.png" differ diff --git "a/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/zookeeper\351\233\206\347\276\244\344\270\255\347\232\204\350\247\222\350\211\262.png" "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/zookeeper\351\233\206\347\276\244\344\270\255\347\232\204\350\247\222\350\211\262.png" new file mode 100644 index 00000000000..6b118fe08e1 Binary files /dev/null and "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/zookeeper\351\233\206\347\276\244\344\270\255\347\232\204\350\247\222\350\211\262.png" differ diff --git "a/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/\350\277\236\346\216\245ZooKeeper\346\234\215\345\212\241.png" "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/\350\277\236\346\216\245ZooKeeper\346\234\215\345\212\241.png" new file mode 100644 index 00000000000..d391329949d Binary files /dev/null and "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/images/\350\277\236\346\216\245ZooKeeper\346\234\215\345\212\241.png" differ diff --git "a/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/zookeeper-in-action.md" "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/zookeeper-in-action.md" new file mode 100644 index 00000000000..71dad09a4a2 --- /dev/null +++ "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/zookeeper-in-action.md" @@ -0,0 +1,296 @@ +# ZooKeeper 实战 + +## 1. 前言 + +这篇文章简单给演示一下 ZooKeeper 常见命令的使用以及 ZooKeeper Java客户端 Curator 的基本使用。介绍到的内容都是最基本的操作,能满足日常工作的基本需要。 + +如果文章有任何需要改善和完善的地方,欢迎在评论区指出,共同进步! + +## 2. ZooKeeper 安装和使用 + +### 2.1. 使用Docker 安装 zookeeper + +**a.使用 Docker 下载 ZooKeeper** + +```shell +docker pull zookeeper:3.5.8 +``` + +**b.运行 ZooKeeper** + +```shell +docker run -d --name zookeeper -p 2181:2181 zookeeper:3.5.8 +``` + +### 2.2. 连接 ZooKeeper 服务 + +**a.进入ZooKeeper容器中** + +先使用 `docker ps` 查看 ZooKeeper 的 ContainerID,然后使用 `docker exec -it ContainerID /bin/bash` 命令进入容器中。 + +**b.先进入 bin 目录,然后通过 `./zkCli.sh -server 127.0.0.1:2181`命令连接ZooKeeper 服务** + +```bash +root@eaf70fc620cb:/apache-zookeeper-3.5.8-bin# cd bin +``` + +如果你看到控制台成功打印出如下信息的话,说明你已经成功连接 ZooKeeper 服务。 + +![](images/连接ZooKeeper服务.png) + +### 2.3. 常用命令演示 + +#### 2.3.1. 查看常用命令(help 命令) + +通过 `help` 命令查看 ZooKeeper 常用命令 + +#### 2.3.2. 创建节点(create 命令) + +通过 `create` 命令在根目录创建了 node1 节点,与它关联的字符串是"node1" + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 34] create /node1 “node1” +``` + +通过 `create` 命令在根目录创建了 node1 节点,与它关联的内容是数字 123 + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 1] create /node1/node1.1 123 +Created /node1/node1.1 +``` + +#### 2.3.3. 更新节点数据内容(set 命令) + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 11] set /node1 "set node1" +``` + +#### 2.3.4. 获取节点的数据(get 命令) + +`get` 命令可以获取指定节点的数据内容和节点的状态,可以看出我们通过 `set` 命令已经将节点数据内容改为 "set node1"。 + +```shell +set node1 +cZxid = 0x47 +ctime = Sun Jan 20 10:22:59 CST 2019 +mZxid = 0x4b +mtime = Sun Jan 20 10:41:10 CST 2019 +pZxid = 0x4a +cversion = 1 +dataVersion = 1 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 9 +numChildren = 1 + +``` + +#### 2.3.5. 查看某个目录下的子节点(ls 命令) + +通过 `ls` 命令查看根目录下的节点 + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 37] ls / +[dubbo, ZooKeeper, node1] +``` + +通过 `ls` 命令查看 node1 目录下的节点 + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 5] ls /node1 +[node1.1] +``` + +ZooKeeper 中的 ls 命令和 linux 命令中的 ls 类似, 这个命令将列出绝对路径 path 下的所有子节点信息(列出 1 级,并不递归) + +#### 2.3.6. 查看节点状态(stat 命令) + +通过 `stat` 命令查看节点状态 + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 10] stat /node1 +cZxid = 0x47 +ctime = Sun Jan 20 10:22:59 CST 2019 +mZxid = 0x47 +mtime = Sun Jan 20 10:22:59 CST 2019 +pZxid = 0x4a +cversion = 1 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 11 +numChildren = 1 +``` + +上面显示的一些信息比如 cversion、aclVersion、numChildren 等等,我在上面 “znode(数据节点)的结构” 这部分已经介绍到。 + +#### 2.3.7. 查看节点信息和状态(ls2 命令) + +`ls2` 命令更像是 `ls` 命令和 `stat` 命令的结合。 `ls2` 命令返回的信息包括 2 部分: + +1. 子节点列表 +2. 当前节点的 stat 信息。 + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 7] ls2 /node1 +[node1.1] +cZxid = 0x47 +ctime = Sun Jan 20 10:22:59 CST 2019 +mZxid = 0x47 +mtime = Sun Jan 20 10:22:59 CST 2019 +pZxid = 0x4a +cversion = 1 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 11 +numChildren = 1 + +``` + +#### 2.3.8. 删除节点(delete 命令) + +这个命令很简单,但是需要注意的一点是如果你要删除某一个节点,那么这个节点必须无子节点才行。 + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 3] delete /node1/node1.1 +``` + +在后面我会介绍到 Java 客户端 API 的使用以及开源 ZooKeeper 客户端 ZkClient 和 Curator 的使用。 + +## 3. ZooKeeper Java客户端 Curator简单使用 + +Curator 是Netflix公司开源的一套 ZooKeeper Java客户端框架,相比于 Zookeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。 + +![](images/curator.png) + +下面我们就来简单地演示一下 Curator 的使用吧! + +Curator4.0+版本对ZooKeeper 3.5.x支持比较好。开始之前,请先将下面的依赖添加进你的项目。 + +```xml + + org.apache.curator + curator-framework + 4.2.0 + + + org.apache.curator + curator-recipes + 4.2.0 + +``` + +### 3.1. 连接 ZooKeeper 客户端 + +通过 `CuratorFrameworkFactory` 创建 `CuratorFramework` 对象,然后再调用 `CuratorFramework` 对象的 `start()` 方法即可! + +```java +private static final int BASE_SLEEP_TIME = 1000; +private static final int MAX_RETRIES = 3; + +// Retry strategy. Retry 3 times, and will increase the sleep time between retries. +RetryPolicy retryPolicy = new ExponentialBackoffRetry(BASE_SLEEP_TIME, MAX_RETRIES); +CuratorFramework zkClient = CuratorFrameworkFactory.builder() + // the server to connect to (can be a server list) + .connectString("127.0.0.1:2181") + .retryPolicy(retryPolicy) + .build(); +zkClient.start(); +``` + +对于一些基本参数的说明: + +- `baseSleepTimeMs`:重试之间等待的初始时间 +- `maxRetries` :最大重试次数 +- `connectString` :要连接的服务器列表 +- `retryPolicy` :重试策略 + +### 3.2. 数据节点的增删改查 + +#### 3.2.1. 创建节点 + +我们在 [ZooKeeper常见概念解读](./zookeeper-intro.md) 中介绍到,我们通常是将 znode 分为 4 大类: + +- **持久(PERSISTENT)节点** :一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。 +- **临时(EPHEMERAL)节点** :临时节点的生命周期是与 **客户端会话(session)** 绑定的,**会话消失则节点消失** 。并且,临时节点 **只能做叶子节点** ,不能创建子节点。 +- **持久顺序(PERSISTENT_SEQUENTIAL)节点** :除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 `/node1/app0000000001` 、`/node1/app0000000002` 。 +- **临时顺序(EPHEMERAL_SEQUENTIAL)节点** :除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。 + +你在使用的ZooKeeper 的时候,会发现 `CreateMode` 类中实际有 7种 znode 类型 ,但是用的最多的还是上面介绍的 4 种。 + +**a.创建持久化节点** + +你可以通过下面两种方式创建持久化的节点。 + +```java +//注意:下面的代码会报错,下文说了具体原因 +zkClient.create().forPath("/node1/00001"); +zkClient.create().withMode(CreateMode.PERSISTENT).forPath("/node1/00002"); +``` + +但是,你运行上面的代码会报错,这是因为的父节点`node1`还未创建。 + +你可以先创建父节点 `node1` ,然后再执行上面的代码就不会报错了。 + +```java +zkClient.create().forPath("/node1"); +``` + +更推荐的方式是通过下面这行代码, **`creatingParentsIfNeeded()` 可以保证父节点不存在的时候自动创建父节点,这是非常有用的。** + +```java +zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath("/node1/00001"); +``` + +**b.创建临时节点** + +```java +zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001"); +``` + +**c.创建节点并指定数据内容** + +```java +zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001","java".getBytes()); +zkClient.getData().forPath("/node1/00001");//获取节点的数据内容,获取到的是 byte数组 +``` + +**d.检测节点是否创建成功** + +```java +zkClient.checkExists().forPath("/node1/00001");//不为null的话,说明节点创建成功 +``` + +#### 3.2.2. 删除节点 + +**a.删除一个子节点** + +```java +zkClient.delete().forPath("/node1/00001"); +``` + +**b.删除一个节点以及其下的所有子节点** + +```java +zkClient.delete().deletingChildrenIfNeeded().forPath("/node1"); +``` + +#### 3.2.3. 获取/更新节点数据内容 + +```java +zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/node1/00001","java".getBytes()); +zkClient.getData().forPath("/node1/00001");//获取节点的数据内容 +zkClient.setData().forPath("/node1/00001","c++".getBytes());//更新节点数据内容 +``` + +#### 3.2.4. 获取某个节点的所有子节点路径 + +```java +List childrenPaths = zkClient.getChildren().forPath("/node1"); +``` + + + + + diff --git "a/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/zookeeper-intro.md" "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/zookeeper-intro.md" new file mode 100644 index 00000000000..3ead663c4b3 --- /dev/null +++ "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/zookeeper-intro.md" @@ -0,0 +1,280 @@ +# ZooKeeper 相关概念总结(入门) + +## 1. 前言 + +相信大家对 ZooKeeper 应该不算陌生。但是你真的了解 ZooKeeper 到底有啥用不?如果别人/面试官让你给他讲讲对于 ZooKeeper 的认识,你能回答到什么地步呢? + +拿我自己来说吧!我本人曾经使用 Dubbo 来做分布式项目的时候,使用了 ZooKeeper 作为注册中心。为了保证分布式系统能够同步访问某个资源,我还使用 ZooKeeper 做过分布式锁。另外,我在学习 Kafka 的时候,知道 Kafka 很多功能的实现依赖了 ZooKeeper。 + +前几天,总结项目经验的时候,我突然问自己 ZooKeeper 到底是个什么东西?想了半天,脑海中只是简单的能浮现出几句话: + +1. ZooKeeper 可以被用作注册中心、分布式锁; +2. ZooKeeper 是 Hadoop 生态系统的一员; +3. 构建 ZooKeeper 集群的时候,使用的服务器最好是奇数台。 + +由此可见,我对于 ZooKeeper 的理解仅仅是停留在了表面。 + +所以,通过本文,希望带大家稍微详细的了解一下 ZooKeeper 。如果没有学过 ZooKeeper ,那么本文将会是你进入 ZooKeeper 大门的垫脚砖。如果你已经接触过 ZooKeeper ,那么本文将带你回顾一下 ZooKeeper 的一些基础概念。 + +另外,本文不光会涉及到 ZooKeeper 的一些概念,后面的文章会介绍到 ZooKeeper 常见命令的使用以及使用 Apache Curator 作为 ZooKeeper 的客户端。 + +_如果文章有任何需要改善和完善的地方,欢迎在评论区指出,共同进步!_ + +## 2. ZooKeeper 介绍 + +### 2.1. ZooKeeper 由来 + +正式介绍 ZooKeeper 之前,我们先来看看 ZooKeeper 的由来,还挺有意思的。 + +下面这段内容摘自《从 Paxos 到 ZooKeeper 》第四章第一节,推荐大家阅读一下: + +> ZooKeeper 最早起源于雅虎研究院的一个研究小组。在当时,研究人员发现,在雅虎内部很多大型系统基本都需要依赖一个类似的系统来进行分布式协调,但是这些系统往往都存在分布式单点问题。所以,雅虎的开发人员就试图开发一个通用的无单点问题的分布式协调框架,以便让开发人员将精力集中在处理业务逻辑上。 +> +> 关于“ZooKeeper”这个项目的名字,其实也有一段趣闻。在立项初期,考虑到之前内部很多项目都是使用动物的名字来命名的(例如著名的 Pig 项目),雅虎的工程师希望给这个项目也取一个动物的名字。时任研究院的首席科学家 RaghuRamakrishnan 开玩笑地说:“在这样下去,我们这儿就变成动物园了!”此话一出,大家纷纷表示就叫动物园管理员吧一一一因为各个以动物命名的分布式组件放在一起,雅虎的整个分布式系统看上去就像一个大型的动物园了,而 ZooKeeper 正好要用来进行分布式环境的协调一一于是,ZooKeeper 的名字也就由此诞生了。 + +### 2.2. ZooKeeper 概览 + +ZooKeeper 是一个开源的**分布式协调服务**,它的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。 + +> **原语:** 操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程。具有不可分割性·即原语的执行必须是连续的,在执行过程中不允许被中断。 + +**ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案,通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。** + +另外,**ZooKeeper 将数据保存在内存中,性能是非常棒的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景)。** + +### 2.3. ZooKeeper 特点 + +- **顺序一致性:** 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。 +- **原子性:** 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。 +- **单一系统映像 :** 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。 +- **可靠性:** 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。 + +### 2.4. ZooKeeper 典型应用场景 + +ZooKeeper 概览中,我们介绍到使用其通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。 + +下面选 3 个典型的应用场景来专门说说: + +1. **分布式锁** : 通过创建唯一节点获得分布式锁,当获得锁的一方执行完相关代码或者是挂掉之后就释放锁。 +2. **命名服务** :可以通过 ZooKeeper 的顺序节点生成全局唯一 ID +3. **数据发布/订阅** :通过 **Watcher 机制** 可以很方便地实现数据发布/订阅。当你将数据发布到 ZooKeeper 被监听的节点上,其他机器可通过监听 ZooKeeper 上节点的变化来实现配置的动态更新。 + +实际上,这些功能的实现基本都得益于 ZooKeeper 可以保存数据的功能,但是 ZooKeeper 不适合保存大量数据,这一点需要注意。 + +### 2.5. 有哪些著名的开源项目用到了 ZooKeeper? + +1. **Kafka** : ZooKeeper 主要为 Kafka 提供 Broker 和 Topic 的注册以及多个 Partition 的负载均衡等功能。 +2. **Hbase** : ZooKeeper 为 Hbase 提供确保整个集群只有一个 Master 以及保存和提供 regionserver 状态信息(是否在线)等功能。 +3. **Hadoop** : ZooKeeper 为 Namenode 提供高可用支持。 + +## 3. ZooKeeper 重要概念解读 + +_破音:拿出小本本,下面的内容非常重要哦!_ + +### 3.1. Data model(数据模型) + +ZooKeeper 数据模型采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二级制序列。并且。每个节点还可以拥有 N 个子节点,最上层是根节点以“/”来代表。每个数据节点在 ZooKeeper 中被称为 **znode**,它是 ZooKeeper 中数据的最小单元。并且,每个 znode 都一个唯一的路径标识。 + +强调一句:**ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在 znode 上,ZooKeeper 给出的上限是每个结点的数据大小最大是 1M。** + +从下图可以更直观地看出:ZooKeeper 节点路径标识方式和 Unix 文件系统路径非常相似,都是由一系列使用斜杠"/"进行分割的路径表示,开发人员可以向这个节点中写入数据,也可以在节点下面创建子节点。这些操作我们后面都会介绍到。 + +![ZooKeeper 数据模型](images/znode-structure.png) + +### 3.2. znode(数据节点) + +介绍了 ZooKeeper 树形数据模型之后,我们知道每个数据节点在 ZooKeeper 中被称为 **znode**,它是 ZooKeeper 中数据的最小单元。你要存放的数据就放在上面,是你使用 ZooKeeper 过程中经常需要接触到的一个概念。 + +#### 3.2.1. znode 4 种类型 + +我们通常是将 znode 分为 4 大类: + +- **持久(PERSISTENT)节点** :一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。 +- **临时(EPHEMERAL)节点** :临时节点的生命周期是与 **客户端会话(session)** 绑定的,**会话消失则节点消失** 。并且,**临时节点只能做叶子节点** ,不能创建子节点。 +- **持久顺序(PERSISTENT_SEQUENTIAL)节点** :除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 `/node1/app0000000001` 、`/node1/app0000000002` 。 +- **临时顺序(EPHEMERAL_SEQUENTIAL)节点** :除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。 + +#### 3.2.2. znode 数据结构 + +每个 znode 由 2 部分组成: + +- **stat** :状态信息 +- **data** : 节点存放的数据的具体内容 + +如下所示,我通过 get 命令来获取 根目录下的 dubbo 节点的内容。(get 命令在下面会介绍到)。 + +```shell +[zk: 127.0.0.1:2181(CONNECTED) 6] get /dubbo +# 该数据节点关联的数据内容为空 +null +# 下面是该数据节点的一些状态信息,其实就是 Stat 对象的格式化输出 +cZxid = 0x2 +ctime = Tue Nov 27 11:05:34 CST 2018 +mZxid = 0x2 +mtime = Tue Nov 27 11:05:34 CST 2018 +pZxid = 0x3 +cversion = 1 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 0 +numChildren = 1 +``` + +Stat 类中包含了一个数据节点的所有状态信息的字段,包括事务 ID-cZxid、节点创建时间-ctime 和子节点个数-numChildren 等等。 + +下面我们来看一下每个 znode 状态信息究竟代表的是什么吧!(下面的内容来源于《从 Paxos 到 ZooKeeper 分布式一致性原理与实践》,因为 Guide 确实也不是特别清楚,要学会参考资料的嘛! ) : + +| znode 状态信息 | 解释 | +| -------------- | ------------------------------------------------------------ | +| cZxid | create ZXID,即该数据节点被创建时的事务 id | +| ctime | create time,即该节点的创建时间 | +| mZxid | modified ZXID,即该节点最终一次更新时的事务 id | +| mtime | modified time,即该节点最后一次的更新时间 | +| pZxid | 该节点的子节点列表最后一次修改时的事务 id,只有子节点列表变更才会更新 pZxid,子节点内容变更不会更新 | +| cversion | 子节点版本号,当前节点的子节点每次变化时值增加 1 | +| dataVersion | 数据节点内容版本号,节点创建时为 0,每更新一次节点内容(不管内容有无变化)该版本号的值增加 1 | +| aclVersion | 节点的 ACL 版本号,表示该节点 ACL 信息变更次数 | +| ephemeralOwner | 创建该临时节点的会话的 sessionId;如果当前节点为持久节点,则 ephemeralOwner=0 | +| dataLength | 数据节点内容长度 | +| numChildren | 当前节点的子节点个数 | + +### 3.3. 版本(version) + +在前面我们已经提到,对应于每个 znode,ZooKeeper 都会为其维护一个叫作 **Stat** 的数据结构,Stat 中记录了这个 znode 的三个相关的版本: + +- **dataVersion** :当前 znode 节点的版本号 +- **cversion** : 当前 znode 子节点的版本 +- **aclVersion** : 当前 znode 的 ACL 的版本。 + +### 3.4. ACL(权限控制) + +ZooKeeper 采用 ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。 + +对于 znode 操作的权限,ZooKeeper 提供了以下 5 种: + +- **CREATE** : 能创建子节点 +- **READ** :能获取节点数据和列出其子节点 +- **WRITE** : 能设置/更新节点数据 +- **DELETE** : 能删除子节点 +- **ADMIN** : 能设置节点 ACL 的权限 + +其中尤其需要注意的是,**CREATE** 和 **DELETE** 这两种权限都是针对 **子节点** 的权限控制。 + +对于身份认证,提供了以下几种方式: + +- **world** : 默认方式,所有用户都可无条件访问。 +- **auth** :不使用任何 id,代表任何已认证的用户。 +- **digest** :用户名:密码认证方式: _username:password_ 。 +- **ip** : 对指定 ip 进行限制。 + +### 3.5. Watcher(事件监听器) + +Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。 + +![watcher机制](images/watche机制.png) + +_破音:非常有用的一个特性,都能出小本本记好了,后面用到 ZooKeeper 基本离不开 Watcher(事件监听器)机制。_ + +### 3.6. 会话(Session) + +Session 可以看作是 ZooKeeper 服务器与客户端的之间的一个 TCP 长连接,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watcher 事件通知。 + +Session 有一个属性叫做:`sessionTimeout` ,`sessionTimeout` 代表会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在`sessionTimeout`规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。 + +另外,在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 `sessionID`。由于 `sessionID`是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 `sessionID` 的,因此,无论是哪台服务器为客户端分配的 `sessionID`,都务必保证全局唯一。 + +## 4. ZooKeeper 集群 + +为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。通常 3 台服务器就可以构成一个 ZooKeeper 集群了。ZooKeeper 官方提供的架构图就是一个 ZooKeeper 集群整体对外提供服务。 + +![](images/zookeeper集群.png) + +上图中每一个 Server 代表一个安装 ZooKeeper 服务的服务器。组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过 ZAB 协议(ZooKeeper Atomic Broadcast)来保持数据的一致性。 + +**最典型集群模式: Master/Slave 模式(主备模式)**。在这种模式中,通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。 + +### 4.1. ZooKeeper 集群角色 + +但是,在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了 Leader、Follower 和 Observer 三种角色。如下图所示 + +![](images/zookeeper集群中的角色.png) + +ZooKeeper 集群中的所有机器通过一个 **Leader 选举过程** 来选定一台称为 “**Leader**” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,**Follower** 和 **Observer** 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。 + +| 角色 | 说明 | +| -------- | ------------------------------------------------------------ | +| Leader | 为客户端提供读和写的服务,负责投票的发起和决议,更新系统状态。 | +| Follower | 为客户端提供读服务,如果是写服务则转发给 Leader。参与选举过程中的投票。 | +| Observer | 为客户端提供读服务,如果是写服务则转发给 Leader。不参与选举过程中的投票,也不参与“过半写成功”策略。在不影响写性能的情况下提升集群的读性能。此角色于 ZooKeeper3.3 系列新增的角色。 | + +当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,就会进入 Leader 选举过程,这个过程会选举产生新的 Leader 服务器。 + +这个过程大致是这样的: + +1. **Leader election(选举阶段)**:节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。 +2. **Discovery(发现阶段)** :在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。 +3. **Synchronization(同步阶段)** :同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后 + 准 leader 才会成为真正的 leader。 +4. **Broadcast(广播阶段)** :到了这个阶段,ZooKeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。 + +### 4.2. ZooKeeper 集群中的服务器状态 + +- **LOOKING** :寻找 Leader。 +- **LEADING** :Leader 状态,对应的节点为 Leader。 +- **FOLLOWING** :Follower 状态,对应的节点为 Follower。 +- **OBSERVING** :Observer 状态,对应节点为 Observer,该节点不参与 Leader 选举。 + +### 4.3. ZooKeeper 集群为啥最好奇数台? + +ZooKeeper 集群在宕掉几个 ZooKeeper 服务器之后,如果剩下的 ZooKeeper 服务器个数大于宕掉的个数的话整个 ZooKeeper 才依然可用。假如我们的集群中有 n 台 ZooKeeper 服务器,那么也就是剩下的服务数必须大于 n/2。先说一下结论,2n 和 2n-1 的容忍度是一样的,都是 n-1,大家可以先自己仔细想一想,这应该是一个很简单的数学问题了。 + +比如假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的的时候也同样只允许宕掉 1 台。 +假如我们有 5 台,那么最大允许宕掉 2 台 ZooKeeper 服务器,如果我们有 6 台的的时候也同样只允许宕掉 2 台。 + +综上,何必增加那一个不必要的 ZooKeeper 呢? + +### 4.4. ZooKeeper 选举的过半机制防止脑裂 + +**何为集群脑裂?** + +对于一个集群,通常多台机器会部署在不同机房,来提高这个集群的可用性。保证可用性的同时,会发生一种机房间网络线路故障,导致机房间网络不通,而集群被割裂成几个小集群。这时候子集群各自选主导致“脑裂”的情况。 + +举例说明:比如现在有一个由 6 台服务器所组成的一个集群,部署在了 2 个机房,每个机房 3 台。正常情况下只有 1 个 leader,但是当两个机房中间网络断开的时候,每个机房的 3 台服务器都会认为另一个机房的 3 台服务器下线,而选出自己的 leader 并对外提供服务。若没有过半机制,当网络恢复的时候会发现有 2 个 leader。仿佛是 1 个大脑(leader)分散成了 2 个大脑,这就发生了脑裂现象。脑裂期间 2 个大脑都可能对外提供了服务,这将会带来数据一致性等问题。 + +**过半机制是如何防止脑裂现象产生的?** + +ZooKeeper 的过半机制导致不可能产生 2 个 leader,因为少于等于一半是不可能产生 leader 的,这就使得不论机房的机器如何分配都不可能发生脑裂。 + +## 5. ZAB 协议和 Paxos 算法 + +Paxos 算法应该可以说是 ZooKeeper 的灵魂了。但是,ZooKeeper 并没有完全采用 Paxos 算法 ,而是使用 ZAB 协议作为其保证数据一致性的核心算法。另外,在 ZooKeeper 的官方文档中也指出,ZAB 协议并不像 Paxos 算法那样,是一种通用的分布式一致性算法,它是一种特别为 Zookeeper 设计的崩溃可恢复的原子消息广播算法。 + +### 5.1. ZAB 协议介绍 + +ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。 + +### 5.2. ZAB 协议两种基本的模式:崩溃恢复和消息广播 + +ZAB 协议包括两种基本的模式,分别是 + +- **崩溃恢复** :当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。其中,**所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致**。 +- **消息广播** :**当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。** 当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。 + +关于 **ZAB 协议&Paxos 算法** 需要讲和理解的东西太多了,具体可以看下面这两篇文章: + +- [图解 Paxos 一致性协议](http://codemacro.com/2014/10/15/explain-poxos/) +- [Zookeeper ZAB 协议分析](https://dbaplus.cn/news-141-1875-1.html) + +## 6. 总结 + +1. ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。 +2. 为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。 +3. ZooKeeper 将数据保存在内存中,这也就保证了 高吞吐量和低延迟(但是内存限制了能够存储的容量不太大,此限制也是保持 znode 中存储的数据量较小的进一步原因)。 +4. ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地明显,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。) +5. ZooKeeper 有临时节点的概念。 当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。而当会话终结时,瞬时节点被删除。持久节点是指一旦这个 znode 被创建了,除非主动进行 znode 的移除操作,否则这个 znode 将一直保存在 ZooKeeper 上。 +6. ZooKeeper 底层其实只提供了两个功能:① 管理(存储、读取)用户程序提交的数据;② 为用户程序提供数据节点监听服务。 + +## 7. 参考 + +1. 《从 Paxos 到 ZooKeeper 分布式一致性原理与实践》 diff --git a/docs/system-design/framework/ZooKeeper-plus.md "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/zookeeper-plus.md" similarity index 91% rename from docs/system-design/framework/ZooKeeper-plus.md rename to "docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/zookeeper-plus.md" index d68a4c47324..11f2ac5a1a8 100644 --- a/docs/system-design/framework/ZooKeeper-plus.md +++ "b/docs/distributed-system/\345\210\206\345\270\203\345\274\217\345\215\217\350\260\203/zookeeper/zookeeper-plus.md" @@ -1,8 +1,8 @@ -[FrancisQ](https://juejin.im/user/5c33853851882525ea106810) 投稿。 +# ZooKeeper 相关概念总结(进阶) -# ZooKeeper +> [FrancisQ](https://juejin.im/user/5c33853851882525ea106810) 投稿。 -## 好久不见 +## 1. 好久不见 离上一篇文章的发布也快一个月了,想想已经快一个月没写东西了,其中可能有期末考试、课程设计和驾照考试,但这都不是借口! @@ -10,11 +10,11 @@ > 文章很长,先赞后看,养成习惯。❤️ 🧡 💛 💚 💙 💜 -## 什么是ZooKeeper +## 2. 什么是ZooKeeper `ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理、分布式事务等。 -![zookeeper](http://img.francisqiang.top/img/Zookeeper.jpg) +![zookeeper](https://img-blog.csdnimg.cn/img_convert/7c349891b773671514a68f057b2e14f8.png) 简单来说, `ZooKeeper` 是一个 **分布式协调服务框架** 。分布式?协调服务?这啥玩意?🤔🤔 @@ -22,41 +22,41 @@ 比如,我现在有一个秒杀服务,并发量太大单机系统承受不住,那我加几台服务器也 **一样** 提供秒杀服务,这个时候就是 **`Cluster` 集群** 。 -![cluster](http://img.francisqiang.top/img/cluster.jpg) +![cluster](https://img-blog.csdnimg.cn/img_convert/ffcb080eb66f242ffcd8d2047a7f46aa.png) 但是,我现在换一种方式,我将一个秒杀服务 **拆分成多个子服务** ,比如创建订单服务,增加积分服务,扣优惠券服务等等,**然后我将这些子服务都部署在不同的服务器上** ,这个时候就是 **`Distributed` 分布式** 。 -![distributed](http://img.francisqiang.top/img/distributed.jpg) +![distributed](https://img-blog.csdnimg.cn/img_convert/07191f38aa947b0075e5c0a6a019a11d.png) 而我为什么反驳同学所说的分布式就是加机器呢?因为我认为加机器更加适用于构建集群,因为它真是只有加机器。而对于分布式来说,你首先需要将业务进行拆分,然后再加机器(不仅仅是加机器那么简单),同时你还要去解决分布式带来的一系列问题。 -![](http://img.francisqiang.top/img/miao.jpg) +![](https://img-blog.csdnimg.cn/img_convert/2b2fbc21abfb3f6547a2121f28c6d00f.png) 比如各个分布式组件如何协调起来,如何减少各个系统之间的耦合度,分布式事务的处理,如何去配置整个分布式系统等等。`ZooKeeper` 主要就是解决这些问题的。 -## 一致性问题 +## 3. 一致性问题 设计一个分布式系统必定会遇到一个问题—— **因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡** 。这就是著名的 `CAP` 定理。 理解起来其实很简单,比如说把一个班级作为整个系统,而学生是系统中的一个个独立的子系统。这个时候班里的小红小明偷偷谈恋爱被班里的大嘴巴小花发现了,小花欣喜若狂告诉了周围的人,然后小红小明谈恋爱的消息在班级里传播起来了。当在消息的传播(散布)过程中,你抓到一个同学问他们的情况,如果回答你不知道,那么说明整个班级系统出现了数据不一致的问题(因为小花已经知道这个消息了)。而如果他直接不回答你,因为整个班级有消息在进行传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。 -![](http://img.francisqiang.top/img/垃圾例子.jpg) +![](https://img-blog.csdnimg.cn/img_convert/34ffff41f6ca4f221ca9d9ad6f0b5470.png) -而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要将的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。 +而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要讲的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。 -## 一致性协议和算法 +## 4. 一致性协议和算法 而为了解决数据一致性问题,在科学家和程序员的不断探索中,就出现了很多的一致性协议和算法。比如 2PC(两阶段提交),3PC(三阶段提交),Paxos算法等等。 这时候请你思考一个问题,同学之间如果采用传纸条的方式去传播消息,那么就会出现一个问题——我咋知道我的小纸条有没有传到我想要传递的那个人手中呢?万一被哪个小家伙给劫持篡改了呢,对吧? -![](http://img.francisqiang.top/img/neigui.jpg) +![](https://img-blog.csdnimg.cn/img_convert/b0e01fe3213dcc1535c31298ba9bdfbc.png) 这个时候就引申出一个概念—— **拜占庭将军问题** 。它意指 **在不可靠信道上试图通过消息传递的方式达到一致性是不可能的**, 所以所有的一致性算法的 **必要前提** 就是安全可靠的消息通道。 而为什么要去解决数据一致性的问题?你想想,如果一个秒杀系统将服务拆分成了下订单和加积分服务,这两个服务部署在不同的机器上了,万一在消息的传播过程中积分系统宕机了,总不能你这边下了订单却没加积分吧?你总得保证两边的数据需要一致吧? -### 2PC(两阶段提交) +### 4.1. 2PC(两阶段提交) 两阶段提交是一种保证分布式系统数据一致性的协议,现在很多数据库都是采用的两阶段提交协议来完成 **分布式事务** 的处理。 @@ -76,17 +76,17 @@ 而如果在第一阶段并不是所有参与者都返回了准备好了的消息,那么此时协调者将会给所有参与者发送 **回滚事务的 `rollback` 请求**,参与者收到之后将会 **回滚它在第一阶段所做的事务处理** ,然后再将处理情况返回给协调者,最终协调者收到响应后便给事务发起者返回处理失败的结果。 -![2PC流程](http://img.francisqiang.top/img/2PC.jpg) +![2PC流程](https://img-blog.csdnimg.cn/img_convert/7ce4e40b68d625676bb42c29efce046a.png) 个人觉得 2PC 实现得还是比较鸡肋的,因为事实上它只解决了各个事务的原子性问题,随之也带来了很多的问题。 -![](http://img.francisqiang.top/img/laji.jpg) +![](https://img-blog.csdnimg.cn/img_convert/9af1ad68517561a8e9f5d3455a32132d.png) * **单点故障问题**,如果协调者挂了那么整个系统都处于不可用的状态了。 * **阻塞问题**,即当协调者发送 `prepare` 请求,参与者收到之后如果能处理那么它将会进行事务的处理但并不提交,这个时候会一直占用着资源不释放,如果此时协调者挂了,那么这些资源都不会再释放了,这会极大影响性能。 * **数据不一致问题**,比如当第二阶段,协调者只发送了一部分的 `commit` 请求就挂了,那么也就意味着,收到消息的参与者会进行事务的提交,而后面没收到的则不会进行事务提交,那么这时候就会产生数据不一致性问题。 -### 3PC(三阶段提交) +### 4.2. 3PC(三阶段提交) 因为2PC存在的一系列问题,比如单点,容错机制缺陷等等,从而产生了 **3PC(三阶段提交)** 。那么这三阶段又分别是什么呢? @@ -96,7 +96,7 @@ 2. **PreCommit阶段**:协调者根据参与者返回的响应来决定是否可以进行下面的 `PreCommit` 操作。如果上面参与者返回的都是 YES,那么协调者将向所有参与者发送 `PreCommit` 预提交请求,**参与者收到预提交请求后,会进行事务的执行操作,并将 `Undo` 和 `Redo` 信息写入事务日志中** ,最后如果参与者顺利执行了事务则给协调者返回成功的响应。如果在第一阶段协调者收到了 **任何一个 NO** 的信息,或者 **在一定时间内** 并没有收到全部的参与者的响应,那么就会中断事务,它会向所有参与者发送中断请求(abort),参与者收到中断请求之后会立即中断事务,或者在一定时间内没有收到协调者的请求,它也会中断事务。 3. **DoCommit阶段**:这个阶段其实和 `2PC` 的第二阶段差不多,如果协调者收到了所有参与者在 `PreCommit` 阶段的 YES 响应,那么协调者将会给所有参与者发送 `DoCommit` 请求,**参与者收到 `DoCommit` 请求后则会进行事务的提交工作**,完成后则会给协调者返回响应,协调者收到所有参与者返回的事务提交成功的响应之后则完成事务。若协调者在 `PreCommit` 阶段 **收到了任何一个 NO 或者在一定时间内没有收到所有参与者的响应** ,那么就会进行中断请求的发送,参与者收到中断请求后则会 **通过上面记录的回滚日志** 来进行事务的回滚操作,并向协调者反馈回滚状况,协调者收到参与者返回的消息后,中断事务。 -![3PC流程](http://img.francisqiang.top/img/3PC.jpg) +![3PC流程](https://img-blog.csdnimg.cn/img_convert/d0b44361c746593f70a6e42c298b413a.png) > 这里是 `3PC` 在成功的环境下的流程图,你可以看到 `3PC` 在很多地方进行了超时中断的处理,比如协调者在指定时间内为收到全部的确认消息则进行事务中断的处理,这样能 **减少同步阻塞的时间** 。还有需要注意的是,**`3PC` 在 `DoCommit` 阶段参与者如未收到协调者发送的提交事务的请求,它会在一定时间内进行事务的提交**。为什么这么做呢?是因为这个时候我们肯定**保证了在第一阶段所有的协调者全部返回了可以执行事务的响应**,这个时候我们有理由**相信其他系统都能进行事务的执行和提交**,所以**不管**协调者有没有发消息给参与者,进入第三阶段参与者都会进行事务的提交操作。 @@ -104,38 +104,38 @@ 所以,要解决一致性问题还需要靠 `Paxos` 算法⭐️ ⭐️ ⭐️ 。 -### `Paxos` 算法 +### 4.3. `Paxos` 算法 `Paxos` 算法是基于**消息传递且具有高度容错特性的一致性算法**,是目前公认的解决分布式一致性问题最有效的算法之一,**其解决的问题就是在分布式系统中如何就某个值(决议)达成一致** 。 在 `Paxos` 中主要有三个角色,分别为 `Proposer提案者`、`Acceptor表决者`、`Learner学习者`。`Paxos` 算法和 `2PC` 一样,也有两个阶段,分别为 `Prepare` 和 `accept` 阶段。 -#### prepare 阶段 +#### 4.3.1. prepare 阶段 * `Proposer提案者`:负责提出 `proposal`,每个提案者在提出提案时都会首先获取到一个 **具有全局唯一性的、递增的提案编号N**,即在整个集群中是唯一的编号 N,然后将该编号赋予其要提出的提案,在**第一阶段是只将提案编号发送给所有的表决者**。 * `Acceptor表决者`:每个表决者在 `accept` 某提案后,会将该提案编号N记录在本地,这样每个表决者中保存的已经被 accept 的提案中会存在一个**编号最大的提案**,其编号假设为 `maxN`。每个表决者仅会 `accept` 编号大于自己本地 `maxN` 的提案,在批准提案时表决者会将以前接受过的最大编号的提案作为响应反馈给 `Proposer` 。 > 下面是 `prepare` 阶段的流程图,你可以对照着参考一下。 -![paxos第一阶段](http://img.francisqiang.top/img/paxos1.jpg) +![paxos第一阶段](https://img-blog.csdnimg.cn/img_convert/22e8d512d954676bdf0cc92d200af8ef.png) -#### accept 阶段 +#### 4.3.2. accept 阶段 当一个提案被 `Proposer` 提出后,如果 `Proposer` 收到了超过半数的 `Acceptor` 的批准(`Proposer` 本身同意),那么此时 `Proposer` 会给所有的 `Acceptor` 发送真正的提案(你可以理解为第一阶段为试探),这个时候 `Proposer` 就会发送提案的内容和提案编号。 表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号 **大于等于** 已经批准过的最大提案编号,那么就 `accept` 该提案(此时执行提案内容但不提交),随后将情况返回给 `Proposer` 。如果不满足则不回应或者返回 NO 。 -![paxos第二阶段1](http://img.francisqiang.top/img/paxos2.jpg) +![paxos第二阶段1](https://img-blog.csdnimg.cn/img_convert/b82536f956f70a584c6a20c10113f225.png) 当 `Proposer` 收到超过半数的 `accept` ,那么它这个时候会向所有的 `acceptor` 发送提案的提交请求。需要注意的是,因为上述仅仅是超过半数的 `acceptor` 批准执行了该提案内容,其他没有批准的并没有执行该提案内容,所以这个时候需要**向未批准的 `acceptor` 发送提案内容和提案编号并让它无条件执行和提交**,而对于前面已经批准过该提案的 `acceptor` 来说 **仅仅需要发送该提案的编号** ,让 `acceptor` 执行提交就行了。 -![paxos第二阶段2](http://img.francisqiang.top/img/paxos3.jpg) +![paxos第二阶段2](https://img-blog.csdnimg.cn/img_convert/743889b97485fdfe2094e5ef0af6b141.png) 而如果 `Proposer` 如果没有收到超过半数的 `accept` 那么它将会将 **递增** 该 `Proposal` 的编号,然后 **重新进入 `Prepare` 阶段** 。 > 对于 `Learner` 来说如何去学习 `Acceptor` 批准的提案内容,这有很多方式,读者可以自己去了解一下,这里不做过多解释。 -#### `paxos` 算法的死循环问题 +#### 4.3.3. `paxos` 算法的死循环问题 其实就有点类似于两个人吵架,小明说我是对的,小红说我才是对的,两个人据理力争的谁也不让谁🤬🤬。 @@ -143,19 +143,19 @@ 就这样无休无止的永远提案下去,这就是 `paxos` 算法的死循环问题。 -![](http://img.francisqiang.top/img/chaojia.jpg) +![](https://img-blog.csdnimg.cn/img_convert/72ccf65cdc107346ff2a1a881d296a2b.png) 那么如何解决呢?很简单,人多了容易吵架,我现在 **就允许一个能提案** 就行了。 -## 引出 `ZAB` +## 5. 引出 `ZAB` -### `Zookeeper` 架构 +### 5.1. `Zookeeper` 架构 -作为一个优秀高效且可靠的分布式协调框架,`ZooKeeper` 在解决分布式数据一致性问题时并没有直接使用 `Paxos` ,而是专门定制了一致性协议叫做 `ZAB(ZooKeeper Automic Broadcast)` 原子广播协议,该协议能够很好地支持 **崩溃恢复** 。 +作为一个优秀高效且可靠的分布式协调框架,`ZooKeeper` 在解决分布式数据一致性问题时并没有直接使用 `Paxos` ,而是专门定制了一致性协议叫做 `ZAB(ZooKeeper Atomic Broadcast)` 原子广播协议,该协议能够很好地支持 **崩溃恢复** 。 -![Zookeeper架构](http://img.francisqiang.top/img/Zookeeper架构.jpg) +![Zookeeper架构](https://img-blog.csdnimg.cn/img_convert/0c38d08ea026e25bf3849cc7654a4e79.png) -### `ZAB` 中的三个角色 +### 5.2. `ZAB` 中的三个角色 和介绍 `Paxos` 一样,在介绍 `ZAB` 协议之前,我们首先来了解一下在 `ZAB` 中三个主要的角色,`Leader 领导者`、`Follower跟随者`、`Observer观察者` 。 @@ -165,17 +165,17 @@ 在 `ZAB` 协议中对 `zkServer`(即上面我们说的三个角色的总称) 还有两种模式的定义,分别是 **消息广播** 和 **崩溃恢复** 。 -### 消息广播模式 +### 5.3. 消息广播模式 说白了就是 `ZAB` 协议是如何处理写请求的,上面我们不是说只有 `Leader` 能处理写请求嘛?那么我们的 `Follower` 和 `Observer` 是不是也需要 **同步更新数据** 呢?总不能数据只在 `Leader` 中更新了,其他角色都没有得到更新吧? 不就是 **在整个集群中保持数据的一致性** 嘛?如果是你,你会怎么做呢? -![](http://img.francisqiang.top/img/zenmezhidao.jpg) +![](https://img-blog.csdnimg.cn/img_convert/e6064aea729dcc2d927d5d81c4797e74.png) 废话,第一步肯定需要 `Leader` 将写请求 **广播** 出去呀,让 `Leader` 问问 `Followers` 是否同意更新,如果超过半数以上的同意那么就进行 `Follower` 和 `Observer` 的更新(和 `Paxos` 一样)。当然这么说有点虚,画张图理解一下。 -![消息广播](http://img.francisqiang.top/img/消息广播1.jpg) +![消息广播](https://img-blog.csdnimg.cn/img_convert/08ccce48190fe4edcbcbb223d6231876.png) 嗯。。。看起来很简单,貌似懂了🤥🤥🤥。这两个 `Queue` 哪冒出来的?答案是 **`ZAB` 需要让 `Follower` 和 `Observer` 保证顺序性** 。何为顺序性,比如我现在有一个写请求A,此时 `Leader` 将请求A广播出去,因为只需要半数同意就行,所以可能这个时候有一个 `Follower` F1因为网络原因没有收到,而 `Leader` 又广播了一个请求B,因为网络原因,F1竟然先收到了请求B然后才收到了请求A,这个时候请求处理的顺序不同就会导致数据的不同,从而 **产生数据不一致问题** 。 @@ -185,7 +185,7 @@ 定义这个的原因也是为了顺序性,每个 `proposal` 在 `Leader` 中生成后需要 **通过其 `ZXID` 来进行排序** ,才能得到处理。 -### 崩溃恢复模式 +### 5.4. 崩溃恢复模式 说到崩溃恢复我们首先要提到 `ZAB` 中的 `Leader` 选举算法,当系统出现崩溃影响最大应该是 `Leader` 的崩溃,因为我们只有一个 `Leader` ,所以当 `Leader` 出现问题的时候我们势必需要重新选举 `Leader` 。 @@ -217,7 +217,7 @@ 假设 `Leader (server2)` 发送 `commit` 请求(忘了请看上面的消息广播模式),他发送给了 `server3`,然后要发给 `server1` 的时候突然挂了。这个时候重新选举的时候我们如果把 `server1` 作为 `Leader` 的话,那么肯定会产生数据不一致性,因为 `server3` 肯定会提交刚刚 `server2` 发送的 `commit` 请求的提案,而 `server1` 根本没收到所以会丢弃。 -![崩溃恢复](http://img.francisqiang.top/img/崩溃恢复1.jpg) +![崩溃恢复](https://img-blog.csdnimg.cn/img_convert/ffcb12c6fb2bad76ac7105696655e85c.png) 那怎么解决呢? @@ -227,19 +227,19 @@ 假设 `Leader (server2)` 此时同意了提案N1,自身提交了这个事务并且要发送给所有 `Follower` 要 `commit` 的请求,却在这个时候挂了,此时肯定要重新进行 `Leader` 的选举,比如说此时选 `server1` 为 `Leader` (这无所谓)。但是过了一会,这个 **挂掉的 `Leader` 又重新恢复了** ,此时它肯定会作为 `Follower` 的身份进入集群中,需要注意的是刚刚 `server2` 已经同意提交了提案N1,但其他 `server` 并没有收到它的 `commit` 信息,所以其他 `server` 不可能再提交这个提案N1了,这样就会出现数据不一致性问题了,所以 **该提案N1最终需要被抛弃掉** 。 -![崩溃恢复](http://img.francisqiang.top/img/崩溃恢复2.jpg) +![崩溃恢复](https://img-blog.csdnimg.cn/img_convert/abb6efc7d4df9c82b162cbecb129a6e3.png) -## Zookeeper的几个理论知识 +## 6. Zookeeper的几个理论知识 了解了 `ZAB` 协议还不够,它仅仅是 `Zookeeper` 内部实现的一种方式,而我们如何通过 `Zookeeper` 去做一些典型的应用场景呢?比如说集群管理,分布式锁,`Master` 选举等等。 这就涉及到如何使用 `Zookeeper` 了,但在使用之前我们还需要掌握几个概念。比如 `Zookeeper` 的 **数据模型** 、**会话机制**、**ACL**、**Watcher机制** 等等。 -### 数据模型 +### 6.1. 数据模型 `zookeeper` 数据存储结构与标准的 `Unix` 文件系统非常相似,都是在根节点下挂很多子节点(树型)。但是 `zookeeper` 中没有文件系统中目录与文件的概念,而是 **使用了 `znode` 作为数据节点** 。`znode` 是 `zookeeper` 中的最小数据单元,每个 `znode` 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。 -![zk数据模型](http://img.francisqiang.top/img/zk数据模型.jpg) +![zk数据模型](https://img-blog.csdnimg.cn/img_convert/8f35dba8c44c4a10d81e3395df971ce7.png) 每个 `znode` 都有自己所属的 **节点类型** 和 **节点状态**。 @@ -264,13 +264,13 @@ * `numChildre`:该节点的子节点个数,如果为临时节点为0。 * `pzxid`:该节点子节点列表最后一次被修改时的事务ID,注意是子节点的 **列表** ,不是内容。 -### 会话 +### 6.2. 会话 我想这个对于后端开发的朋友肯定不陌生,不就是 `session` 吗?只不过 `zk` 客户端和服务端是通过 **`TCP` 长连接** 维持的会话机制,其实对于会话来说你可以理解为 **保持连接状态** 。 在 `zookeeper` 中,会话还有对应的事件,比如 `CONNECTION_LOSS 连接丢失事件` 、`SESSION_MOVED 会话转移事件` 、`SESSION_EXPIRED 会话超时失效事件` 。 -### ACL +### 6.3. ACL `ACL` 为 `Access Control Lists` ,它是一种权限控制。在 `zookeeper` 中定义了5种权限,它们分别为: @@ -280,19 +280,19 @@ * `DELETE`:删除子节点的权限。 * `ADMIN`:设置节点 ACL 的权限。 -### Watcher机制 +### 6.4. Watcher机制 `Watcher` 为事件监听器,是 `zk` 非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端 **注册** 指定的 `watcher` ,当服务端符合了 `watcher` 的某些事件或要求则会 **向客户端发送事件通知** ,客户端收到通知后找到自己定义的 `Watcher` 然后 **执行相应的回调方法** 。 -![watcher机制](http://img.francisqiang.top/img/watcher机制.jpg) +![watcher机制](https://img-blog.csdnimg.cn/img_convert/241ab8cc37571034fa984322b753c7ba.png) -## Zookeeper的几个典型应用场景 +## 7. Zookeeper的几个典型应用场景 前面说了这么多的理论知识,你可能听得一头雾水,这些玩意有啥用?能干啥事?别急,听我慢慢道来。 -![](http://img.francisqiang.top/img/feijie.jpg) +![](https://img-blog.csdnimg.cn/img_convert/9c9bd2a892e23e0b7582370c50117d8c.png) -### 选主 +### 7.1. 选主 还记得上面我们的所说的临时节点吗?因为 `Zookeeper` 的强一致性,能够很好地在保证 **在高并发的情况下保证节点创建的全局唯一性** (即无法重复创建同样的节点)。 @@ -302,11 +302,11 @@ 你想想为什么我们要创建临时节点?还记得临时节点的生命周期吗?`master` 挂了是不是代表会话断了?会话断了是不是意味着这个节点没了?还记得 `watcher` 吗?我们是不是可以 **让其他不是 `master` 的节点监听节点的状态** ,比如说我们监听这个临时节点的父节点,如果子节点个数变了就代表 `master` 挂了,这个时候我们 **触发回调函数进行重新选举** ,或者我们直接监听节点的状态,我们可以通过节点是否已经失去连接来判断 `master` 是否挂了等等。 -![选主](http://img.francisqiang.top/img/选主.jpg) +![选主](https://img-blog.csdnimg.cn/img_convert/a94707028c5581c815f72fba0f50f43a.png) 总的来说,我们可以完全 **利用 临时节点、节点状态 和 `watcher` 来实现选主的功能**,临时节点主要用来选举,节点状态和`watcher` 可以用来判断 `master` 的活性和进行重新选举。 -### 分布式锁 +### 7.2. 分布式锁 分布式锁的实现方式有很多种,比如 `Redis` 、数据库 、`zookeeper` 等。个人认为 `zookeeper` 在实现分布式锁这方面是非常非常简单的。 @@ -330,35 +330,35 @@ 具体怎么做呢?其实也很简单,你可以让 **读请求监听比自己小的最后一个写请求节点,写请求只监听比自己小的最后一个节点** ,感兴趣的小伙伴可以自己去研究一下。 -### 命名服务 +### 7.3. 命名服务 如何给一个对象设置ID,大家可能都会想到 `UUID`,但是 `UUID` 最大的问题就在于它太长了。。。(太长不一定是好事,嘿嘿嘿)。那么在条件允许的情况下,我们能不能使用 `zookeeper` 来实现呢? 我们之前提到过 `zookeeper` 是通过 **树形结构** 来存储数据节点的,那也就是说,对于每个节点的 **全路径**,它必定是唯一的,我们可以使用节点的全路径作为命名方式了。而且更重要的是,路径是我们可以自己定义的,这对于我们对有些有语意的对象的ID设置可以更加便于理解。 -### 集群管理和注册中心 +### 7.4. 集群管理和注册中心 看到这里是不是觉得 `zookeeper` 实在是太强大了,它怎么能这么能干! -别急,它能干的事情还很多呢。可能我们会有这样的需求,我们需要了解整个集群中有多少机器在工作,我们想对及群众的每台机器的运行时状态进行数据采集,对集群中机器进行上下线操作等等。 +别急,它能干的事情还很多呢。可能我们会有这样的需求,我们需要了解整个集群中有多少机器在工作,我们想对集群中的每台机器的运行时状态进行数据采集,对集群中机器进行上下线操作等等。 而 `zookeeper` 天然支持的 `watcher` 和 临时节点能很好的实现这些需求。我们可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的 `watcher` 进行状态监控和回调。 -![集群管理](http://img.francisqiang.top/img/集群管理.jpg) +![集群管理](https://img-blog.csdnimg.cn/img_convert/6115820219c35c68bcb2c9a855ebace3.png) 至于注册中心也很简单,我们同样也是让 **服务提供者** 在 `zookeeper` 中创建一个临时节点并且将自己的 `ip、port、调用方式` 写入节点,当 **服务消费者** 需要进行调用的时候会 **通过注册中心找到相应的服务的地址列表(IP端口什么的)** ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。 当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得 `Eureka` 会先试错,然后再更新)。 -![注册中心](http://img.francisqiang.top/img/注册中心.jpg) +![注册中心](https://img-blog.csdnimg.cn/img_convert/0b5b3911a7c2dae23391d17c91416b29.png) -## 总结 +## 8. 总结 看到这里的同学实在是太有耐心了👍👍👍,如果觉得我写得不错的话点个赞哈。 不知道大家是否还记得我讲了什么😒。 -![](http://img.francisqiang.top/img/masmmei.jpg) +![](https://img-blog.csdnimg.cn/img_convert/b0bde9f3979b8f19187e6c374ad98993.png) 这篇文章中我带大家入门了 `zookeeper` 这个强大的分布式协调框架。现在我们来简单梳理一下整篇文章的内容。 @@ -372,4 +372,4 @@ * `zookeeper` 的典型应用场景,比如选主,注册中心等等。 - 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出🤝🤝🤝。 \ No newline at end of file + 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出🤝🤝🤝。 diff --git "a/docs/distributed-system/\347\220\206\350\256\272&\347\256\227\346\263\225/cap&base\347\220\206\350\256\272.md" "b/docs/distributed-system/\347\220\206\350\256\272&\347\256\227\346\263\225/cap&base\347\220\206\350\256\272.md" new file mode 100644 index 00000000000..3340409a799 --- /dev/null +++ "b/docs/distributed-system/\347\220\206\350\256\272&\347\256\227\346\263\225/cap&base\347\220\206\350\256\272.md" @@ -0,0 +1,157 @@ + +# CAP & BASE理论 + +经历过技术面试的小伙伴想必对这个两个概念已经再熟悉不过了! + +Guide哥当年参加面试的时候,不夸张地说,只要问到分布式相关的内容,面试官几乎是必定会问这两个分布式相关的理论。 + +并且,这两个理论也可以说是小伙伴们学习分布式相关内容的基础了! + +因此,小伙伴们非常非常有必要将这理论搞懂,并且能够用自己的理解给别人讲出来。 + +这篇文章我会站在自己的角度对这两个概念进行解读! + +*个人能力有限。如果文章有任何需要改善和完善的地方,欢迎在评论区指出,共同进步!——爱你们的Guide哥* + +## CAP理论 + +[CAP 理论/定理](https://zh.wikipedia.org/wiki/CAP%E5%AE%9A%E7%90%86)起源于 2000年,由加州大学伯克利分校的Eric Brewer教授在分布式计算原理研讨会(PODC)上提出,因此 CAP定理又被称作 **布鲁尔定理(Brewer’s theorem)** + +2年后,麻省理工学院的Seth Gilbert和Nancy Lynch 发表了布鲁尔猜想的证明,CAP理论正式成为分布式领域的定理。 + +### 简介 + +**CAP** 也就是 **Consistency(一致性)**、**Availability(可用性)**、**Partition Tolerance(分区容错性)** 这三个单词首字母组合。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/cap.png) + +CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细定义 **Consistency**、**Availability**、**Partition Tolerance** 三个单词的明确定义。 + +因此,对于 CAP 的民间解读有很多,一般比较被大家推荐的是下面 👇 这种版本的解读。 + +在理论计算机科学中,CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个: + +- **一致性(Consistency)** : 所有节点访问同一份最新的数据副本 +- **可用性(Availability)**: 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。 +- **分区容错性(Partition tolerance)** : 分布式系统出现网络分区的时候,仍然能够对外提供服务。 + +**什么是网络分区?** + +> 分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫网络分区。 + +![partition-tolerance](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/partition-tolerance.png) + +### 不是所谓的“3 选 2” + +大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法,而且在 CAP 理论诞生 12 年之后,CAP 之父也在 2012 年重写了之前的论文。 + +> **当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。** +> +> 简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。 + +因此,**分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。** 比如 ZooKeeper、HBase 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。 + +**为啥不可能选择 CA 架构呢?** 举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。 + +**选择 CP 还是 AP 的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP 。** + +另外,需要补充说明的一点是: **如果网络分区正常的话(系统在绝大部分时候所处的状态),也就说不需要保证 P 的时候,C 和 A 能够同时保证。** + +### CAP 实际应用案例 + +我这里以注册中心来探讨一下 CAP 的实际应用。考虑到很多小伙伴不知道注册中心是干嘛的,这里简单以 Dubbo 为例说一说。 + +下图是 Dubbo 的架构图。**注册中心 Registry 在其中扮演了什么角色呢?提供了什么服务呢?** + +注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/dubbo-architecture.png) + +常见的可以作为注册中心的组件有:ZooKeeper、Eureka、Nacos...。 + +1. **ZooKeeper 保证的是 CP。** 任何时刻对 ZooKeeper 的读请求都能得到一致性的结果,但是, ZooKeeper 不保证每次请求的可用性比如在 Leader 选举过程中或者半数以上的机器不可用的时候服务就是不可用的。 +2. **Eureka 保证的则是 AP。** Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。 +3. **Nacos 不仅支持 CP 也支持 AP。** + +### 总结 + +在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等 + +在系统发生“分区”的情况下,CAP 理论只能满足 CP 或者 AP。要注意的是,这里的前提是系统发生了“分区” + +如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。 + +总结:**如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。** + +### 推荐阅读 + +1. [CAP 定理简化](https://medium.com/@ravindraprasad/cap-theorem-simplified-28499a67eab4) (英文,有趣的案例) +2. [神一样的 CAP 理论被应用在何方](https://juejin.im/post/6844903936718012430) (中文,列举了很多实际的例子) +3. [请停止呼叫数据库 CP 或 AP ](https://martin.kleppmann.com/2015/05/11/please-stop-calling-databases-cp-or-ap.html) (英文,带给你不一样的思考) + +## BASE 理论 + +[BASE 理论](https://dl.acm.org/doi/10.1145/1394127.1394128)起源于 2008 年, 由eBay的架构师Dan Pritchett在ACM上发表。 + +### 简介 + +**BASE** 是 **Basically Available(基本可用)** 、**Soft-state(软状态)** 和 **Eventually Consistent(最终一致性)** 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。 + +### BASE 理论的核心思想 + +即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +> 也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。 + +**BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。** + +**为什么这样说呢?** + +CAP 理论这节我们也说过了: + +> 如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。因此,**如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。** + +因此,AP 方案只是在系统发生分区的时候放弃一致性,而不是永远放弃一致性。在分区故障恢复后,系统应该达到最终一致性。这一点其实就是 BASE 理论延伸的地方。 + +### BASE 理论三要素 + +![BASE理论三要素](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC81LzI0LzE2MzkxNDgwNmQ5ZTE1YzY?x-oss-process=image/format,png) + +#### 1. 基本可用 + +基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。 + +**什么叫允许损失部分可用性呢?** + +- **响应时间上的损失**: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。 +- **系统功能上的损失**:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。 + +#### 2. 软状态 + +软状态指允许系统中的数据存在中间状态(**CAP 理论中的数据不一致**),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。 + +#### 3. 最终一致性 + +最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。 + +> 分布式一致性的 3 种级别: +> +> 1. **强一致性** :系统写入了什么,读出来的就是什么。 +> +> 2. **弱一致性** :不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。 +> +> 3. **最终一致性** :弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。 +> +> **业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。** + +那实现最终一致性的具体方式是什么呢? [《分布式协议与算法实战》](http://gk.link/a/10rZM) 中是这样介绍: + +> - **读时修复** : 在读取数据时,检测数据的不一致,进行修复。比如 Cassandra 的 Read Repair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点 的副本数据不一致,系统就自动修复数据。 +> - **写时修复** : 在写入数据,检测数据的不一致时,进行修复。比如 Cassandra 的 Hinted Handoff 实现。具体来说,Cassandra 集群的节点之间远程写数据的时候,如果写失败 就将数据缓存下来,然后定时重传,修复数据的不一致性。 +> - **异步修复** : 这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。 + +比较推荐 **写时修复**,这种方式对性能消耗比较低。 + +### 总结 + +**ACID 是数据库事务完整性的理论,CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。** diff --git "a/docs/distributed-system/\347\220\206\350\256\272&\347\256\227\346\263\225/paxos&raft\347\256\227\346\263\225.md" "b/docs/distributed-system/\347\220\206\350\256\272&\347\256\227\346\263\225/paxos&raft\347\256\227\346\263\225.md" new file mode 100644 index 00000000000..36bd77241dd --- /dev/null +++ "b/docs/distributed-system/\347\220\206\350\256\272&\347\256\227\346\263\225/paxos&raft\347\256\227\346\263\225.md" @@ -0,0 +1,4 @@ +# Paxos 算法和 Raft 算法 + +Paxos 算法诞生于 1990 年,这是一种解决分布式系统一致性的经典算法 。但是,由于 Paxos 算法非常难以理解和实现,不断有人尝试简化这一算法。到了2013 年才诞生了一个比 Paxos 算法更易理解和实现的分布式一致性算法—Raft 算法。 + diff --git a/docs/essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md b/docs/essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md deleted file mode 100644 index 183a1852a90..00000000000 --- a/docs/essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md +++ /dev/null @@ -1,294 +0,0 @@ -作者: rhwayfun,原文地址:https://mp.weixin.qq.com/s/msYty4vjjC0PvrwasRH5Bw ,JavaGuide 已经获得作者授权并对原文进行了重新排版。 - - -- [写在2019年后的蚂蚁、头条、拼多多的面试总结](#写在2019年后的蚂蚁头条拼多多的面试总结) - - [准备过程](#准备过程) - - [蚂蚁金服](#蚂蚁金服) - - [一面](#一面) - - [二面](#二面) - - [三面](#三面) - - [四面](#四面) - - [五面](#五面) - - [小结](#小结) - - [拼多多](#拼多多) - - [面试前](#面试前) - - [一面](#一面-1) - - [二面](#二面-1) - - [三面](#三面-1) - - [小结](#小结-1) - - [字节跳动](#字节跳动) - - [面试前](#面试前-1) - - [一面](#一面-2) - - [二面](#二面-2) - - [小结](#小结-2) - - [总结](#总结) - - - -# 2019年蚂蚁金服、头条、拼多多的面试总结 - -文章有点长,请耐心看完,绝对有收获!不想听我BB直接进入面试分享: - -- 准备过程 -- 蚂蚁金服面试分享 -- 拼多多面试分享 -- 字节跳动面试分享 -- 总结 - -说起来开始进行面试是年前倒数第二周,上午9点,我还在去公司的公交上,突然收到蚂蚁的面试电话,其实算不上真正的面试。面试官只是和我聊了下他们在做的事情(主要是做双十一这里大促的稳定性保障,偏中间件吧),说的很详细,然后和我沟通了下是否有兴趣,我表示有兴趣,后面就收到正式面试的通知,最后没选择去蚂蚁表示抱歉。 - -当时我自己也准备出去看看机会,顺便看看自己的实力。当时我其实挺纠结的,一方面现在部门也正需要我,还是可以有一番作为的,另一方面觉得近一年来进步缓慢,没有以前飞速进步的成就感了,而且业务和技术偏于稳定,加上自己也属于那种比较懒散的人,骨子里还是希望能够突破现状,持续在技术上有所精进。 - -在开始正式的总结之前,还是希望各位同仁能否听我继续发泄一会,抱拳! - -我翻开自己2018年初立的flag,觉得甚是惭愧。其中就有一条是保持一周写一篇博客,奈何中间因为各种原因没能坚持下去。细细想来,主要是自己没能真正静下来心认真投入到技术的研究和学习,那么为什么会这样?说白了还是因为没有确定目标或者目标不明确,没有目标或者目标不明确都可能导致行动的失败。 - -那么问题来了,目标是啥?就我而言,短期目标是深入研究某一项技术,比如最近在研究mysql,那么深入研究一定要动手实践并且有所产出,这就够了么?还需要我们能够举一反三,结合实际开发场景想一想日常开发要注意什么,这中间有没有什么坑?可以看出,要进步真的不是一件简单的事,这种反人类的行为需要我们克服自我的弱点,逐渐形成习惯。真正牛逼的人,从不觉得认真学习是一件多么难的事,因为这已经形成了他的习惯,就喝早上起床刷牙洗脸那么自然简单。 - -扯了那么多,开始进入正题,先后进行了蚂蚁、拼多多和字节跳动的面试。 - -## 准备过程 - -先说说我自己的情况,我2016先在蚂蚁实习了将近三个月,然后去了我现在的老东家,2.5年工作经验,可以说毕业后就一直老老实实在老东家打怪升级,虽说有蚂蚁的实习经历,但是因为时间太短,还是有点虚的。所以面试官看到我简历第一个问题绝对是这样的。 - -“哇,你在蚂蚁待过,不错啊”,面试官笑嘻嘻地问到。“是的,还好”,我说。“为啥才三个月?”,面试官脸色一沉问到。“哗啦啦解释一通。。。”,我解释道。“哦,原来如此,那我们开始面试吧”,面试官一本正经说到。 - -尼玛,早知道不写蚂蚁的实习经历了,后面仔细一想,当初写上蚂蚁不就给简历加点料嘛。 - -言归正传,准备过程其实很早开始了(当然这不是说我工作时老想着跳槽,因为我明白现在的老东家并不是终点,我还需要不断提升),具体可追溯到从蚂蚁离职的时候,当时出来也面了很多公司,没啥大公司,面了大概5家公司,都拿到offer了。 - -工作之余常常会去额外研究自己感兴趣的技术以及工作用到的技术,力求把原理搞明白,并且会自己实践一把。此外,买了N多书,基本有时间就会去看,补补基础,什么操作系统、数据结构与算法、MySQL、JDK之类的源码,基本都好好温习了(文末会列一下自己看过的书和一些好的资料)。**我深知基础就像“木桶效应”的短板,决定了能装多少水。** - -此外,在正式决定看机会之前,我给自己列了一个提纲,主要包括Java要掌握的核心要点,有不懂的就查资料搞懂。我给自己定位还是Java工程师,所以Java体系是一定要做到心中有数的,很多东西没有常年的积累面试的时候很容易露馅,学习要对得起自己,不要骗人。 - -剩下的就是找平台和内推了,除了蚂蚁,头条和拼多多都是找人内推的,感谢蚂蚁面试官对我的欣赏,以后说不定会去蚂蚁咯😄。 - -平台:脉脉、GitHub、v2 - -## 蚂蚁金服 - -![img](https://mmbiz.qpic.cn/mmbiz_jpg/zsXjkGNcic53JMPc0FUw1lBXl5iaibrEXvt9qal7lJSgfGJ8mq00yE1J4UQ9H1oo9t6RAL4T3whhx17TYlj1mjlXA/?wx_fmt=jpeg) - -- 一面 -- 二面 -- 三面 -- 四面 -- 五面 -- 小结 - -### 一面 - -一面就做了一道算法题,要求两小时内完成,给了长度为N的有重复元素的数组,要求输出第10大的数。典型的TopK问题,快排算法搞定。 - -算法题要注意的是合法性校验、边界条件以及异常的处理。另外,如果要写测试用例,一定要保证测试覆盖场景尽可能全。加上平时刷刷算法题,这种考核应该没问题的。 - -### 二面 - -- 自我介绍下呗 -- 开源项目贡献过代码么?(Dubbo提过一个打印accesslog的bug算么) -- 目前在部门做什么,业务简单介绍下,内部有哪些系统,作用和交互过程说下 -- Dubbo踩过哪些坑,分别是怎么解决的?(说了异常处理时业务异常捕获的问题,自定义了一个异常拦截器) -- 开始进入正题,说下你对线程安全的理解(多线程访问同一个对象,如果不需要考虑额外的同步,调用对象的行为就可以获得正确的结果就是线程安全) -- 事务有哪些特性?(ACID) -- 怎么理解原子性?(同一个事务下,多个操作要么成功要么失败,不存在部分成功或者部分失败的情况) -- 乐观锁和悲观锁的区别?(悲观锁假定会发生冲突,访问的时候都要先获得锁,保证同一个时刻只有线程获得锁,读读也会阻塞;乐观锁假设不会发生冲突,只有在提交操作的时候检查是否有冲突)这两种锁在Java和MySQL分别是怎么实现的?(Java乐观锁通过CAS实现,悲观锁通过synchronize实现。mysql乐观锁通过MVCC,也就是版本实现,悲观锁可以通过select... for update加上排它锁) -- HashMap为什么不是线程安全的?(多线程操作无并发控制,顺便说了在扩容的时候多线程访问时会造成死锁,会形成一个环,不过扩容时多线程操作形成环的问题再JDK1.8已经解决,但多线程下使用HashMap还会有一些其他问题比如数据丢失,所以多线程下不应该使用HashMap,而应该使用ConcurrentHashMap)怎么让HashMap变得线程安全?(Collections的synchronize方法包装一个线程安全的Map,或者直接用ConcurrentHashMap)两者的区别是什么?(前者直接在put和get方法加了synchronize同步,后者采用了分段锁以及CAS支持更高的并发) -- jdk1.8对ConcurrentHashMap做了哪些优化?(插入的时候如果数组元素使用了红黑树,取消了分段锁设计,synchronize替代了Lock锁)为什么这样优化?(避免冲突严重时链表多长,提高查询效率,时间复杂度从O(N)提高到O(logN)) -- redis主从机制了解么?怎么实现的? -- 有过GC调优的经历么?(有点虚,答得不是很好) -- 有什么想问的么? - -### 三面 - -- 简单自我介绍下 -- 监控系统怎么做的,分为哪些模块,模块之间怎么交互的?用的什么数据库?(MySQL)使用什么存储引擎,为什么使用InnnoDB?(支持事务、聚簇索引、MVCC) -- 订单表有做拆分么,怎么拆的?(垂直拆分和水平拆分) -- 水平拆分后查询过程描述下 -- 如果落到某个分片的数据很大怎么办?(按照某种规则,比如哈希取模、range,将单张表拆分为多张表) -- 哈希取模会有什么问题么?(有的,数据分布不均,扩容缩容相对复杂 ) -- 分库分表后怎么解决读写压力?(一主多从、多主多从) -- 拆分后主键怎么保证惟一?(UUID、Snowflake算法) -- Snowflake生成的ID是全局递增唯一么?(不是,只是全局唯一,单机递增) -- 怎么实现全局递增的唯一ID?(讲了TDDL的一次取一批ID,然后再本地慢慢分配的做法) -- Mysql的索引结构说下(说了B+树,B+树可以对叶子结点顺序查找,因为叶子结点存放了数据结点且有序) -- 主键索引和普通索引的区别(主键索引的叶子结点存放了整行记录,普通索引的叶子结点存放了主键ID,查询的时候需要做一次回表查询)一定要回表查询么?(不一定,当查询的字段刚好是索引的字段或者索引的一部分,就可以不用回表,这也是索引覆盖的原理) -- 你们系统目前的瓶颈在哪里? -- 你打算怎么优化?简要说下你的优化思路 -- 有什么想问我么? - -### 四面 - -- 介绍下自己 -- 为什么要做逆向? -- 怎么理解微服务? -- 服务治理怎么实现的?(说了限流、压测、监控等模块的实现) -- 这个不是中间件做的事么,为什么你们部门做?(当时没有单独的中间件团队,微服务刚搞不久,需要进行监控和性能优化) -- 说说Spring的生命周期吧 -- 说说GC的过程(说了young gc和full gc的触发条件和回收过程以及对象创建的过程) -- CMS GC有什么问题?(并发清除算法,浮动垃圾,短暂停顿) -- 怎么避免产生浮动垃圾?(记得有个VM参数设置可以让扫描新生代之前进行一次young gc,但是因为gc是虚拟机自动调度的,所以不保证一定执行。但是还有参数可以让虚拟机强制执行一次young gc) -- 强制young gc会有什么问题?(STW停顿时间变长) -- 知道G1么?(了解一点 ) -- 回收过程是怎么样的?(young gc、并发阶段、混合阶段、full gc,说了Remember Set) -- 你提到的Remember Set底层是怎么实现的? -- 有什么想问的么? - -### 五面 - -五面是HRBP面的,和我提前预约了时间,主要聊了之前在蚂蚁的实习经历、部门在做的事情、职业发展、福利待遇等。阿里面试官确实是具有一票否决权的,很看重你的价值观是否match,一般都比较喜欢皮实的候选人。HR面一定要诚实,不要说谎,只要你说谎HR都会去证实,直接cut了。 - -- 之前蚂蚁实习三个月怎么不留下来? -- 实习的时候主管是谁? -- 实习做了哪些事情?(尼玛这种也问?) -- 你对技术怎么看?平时使用什么技术栈?(阿里HR真的是既当爹又当妈,😂) -- 最近有在研究什么东西么 -- 你对SRE怎么看 -- 对待遇有什么预期么 - -最后HR还对我说目前稳定性保障部挺缺人的,希望我尽快回复。 - -### 小结 - -蚂蚁面试比较重视基础,所以Java那些基本功一定要扎实。蚂蚁的工作环境还是挺赞的,因为我面的是稳定性保障部门,还有许多单独的小组,什么三年1班,很有青春的感觉。面试官基本水平都比较高,基本都P7以上,除了基础还问了不少架构设计方面的问题,收获还是挺大的。 - -## 拼多多 - -![img](https://mmbiz.qpic.cn/mmbiz_jpg/zsXjkGNcic53JMPc0FUw1lBXl5iaibrEXvtsmoh9TdJcV0hwnrjtbWPdOacyj2uYe2qaI5jvlGIQHwYtknwnGTibbQ/?wx_fmt=jpeg) - -- 面试前 -- 一面 -- 二面 -- 三面 -- 小结 - -### 面试前 - -面完蚂蚁后,早就听闻拼多多这个独角兽,决定也去面一把。首先我在脉脉找了一个拼多多的HR,加了微信聊了下,发了简历便开始我的拼多多面试之旅。这里要非常感谢拼多多HR小姐姐,从面试内推到offer确认一直都在帮我,人真的很nice。 - -### 一面 - -- 为啥蚂蚁只待了三个月?没转正?(转正了,解释了一通。。。) -- Java中的HashMap、TreeMap解释下?(TreeMap红黑树,有序,HashMap无序,数组+链表) -- TreeMap查询写入的时间复杂度多少?(O(logN)) -- HashMap多线程有什么问题?(线程安全,死锁)怎么解决?( jdk1.8用了synchronize + CAS,扩容的时候通过CAS检查是否有修改,是则重试)重试会有什么问题么?(CAS(Compare And Swap)是比较和交换,不会导致线程阻塞,但是因为重试是通过自旋实现的,所以仍然会占用CPU时间,还有ABA的问题)怎么解决?(超时,限定自旋的次数,ABA可以通过原理变量AtomicStampedReference解决,原理利用版本号进行比较)超过重试次数如果仍然失败怎么办?(synchronize互斥锁) -- CAS和synchronize有什么区别?都用synchronize不行么?(CAS是乐观锁,不需要阻塞,硬件级别实现的原子性;synchronize会阻塞,JVM级别实现的原子性。使用场景不同,线程冲突严重时CAS会造成CPU压力过大,导致吞吐量下降,synchronize的原理是先自旋然后阻塞,线程冲突严重仍然有较高的吞吐量,因为线程都被阻塞了,不会占用CPU -) -- 如果要保证线程安全怎么办?(ConcurrentHashMap) -- ConcurrentHashMap怎么实现线程安全的?(分段锁) -- get需要加锁么,为什么?(不用,volatile关键字) -- volatile的作用是什么?(保证内存可见性) -- 底层怎么实现的?(说了主内存和工作内存,读写内存屏障,happen-before,并在纸上画了线程交互图) -- 在多核CPU下,可见性怎么保证?(思考了一会,总线嗅探技术) -- 聊项目,系统之间是怎么交互的? -- 系统并发多少,怎么优化? -- 给我一张纸,画了一个九方格,都填了数字,给一个M*N矩阵,从1开始逆时针打印这M*N个数,要求时间复杂度尽可能低(内心OS:之前貌似碰到过这题,最优解是怎么实现来着)思考中。。。 -- 可以先说下你的思路(想起来了,说了什么时候要变换方向的条件,向右、向下、向左、向上,依此循环) -- 有什么想问我的? - -### 二面 - -- 自我介绍下 -- 手上还有其他offer么?(拿了蚂蚁的offer) -- 部门组织结构是怎样的?(这轮不是技术面么,不过还是老老实实说了) -- 系统有哪些模块,每个模块用了哪些技术,数据怎么流转的?(面试官有点秃顶,一看级别就很高)给了我一张纸,我在上面简单画了下系统之间的流转情况 -- 链路追踪的信息是怎么传递的?(RpcContext的attachment,说了Span的结构:parentSpanId + curSpanId) -- SpanId怎么保证唯一性?(UUID,说了下内部的定制改动) -- RpcContext是在什么维度传递的?(线程) -- Dubbo的远程调用怎么实现的?(讲了读取配置、拼装url、创建Invoker、服务导出、服务注册以及消费者通过动态代理、filter、获取Invoker列表、负载均衡等过程(哗啦啦讲了10多分钟),我可以喝口水么) -- Spring的单例是怎么实现的?(单例注册表) -- 为什么要单独实现一个服务治理框架?(说了下内部刚搞微服务不久,主要对服务进行一些监控和性能优化) -- 谁主导的?内部还在使用么? -- 逆向有想过怎么做成通用么? -- 有什么想问的么? - -### 三面 - -二面老大面完后就直接HR面了,主要问了些职业发展、是否有其他offer、以及入职意向等问题,顺便说了下公司的福利待遇等,都比较常规啦。不过要说的是手上有其他offer或者大厂经历会有一定加分。 - -### 小结 - -拼多多的面试流程就简单许多,毕竟是一个成立三年多的公司。面试难度中规中矩,只要基础扎实应该不是问题。但不得不说工作强度很大,开始面试前HR就提前和我确认能否接受这样强度的工作,想来的老铁还是要做好准备 - -## 字节跳动 - -![img](https://mmbiz.qpic.cn/mmbiz_jpg/zsXjkGNcic53JMPc0FUw1lBXl5iaibrEXvtRoTSCMeUWramk7M4CekxE9ssH5DFGBxmDcw0x9hjzmbIGHVWenDK8w/?wx_fmt=jpeg) - -- 面试前 -- 一面 -- 二面 -- 小结 - -### 面试前 - -头条的面试是三家里最专业的,每次面试前有专门的HR和你约时间,确定OK后再进行面试。每次都是通过视频面试,因为都是之前都是电话面或现场面,所以视频面试还是有点不自然。也有人觉得视频面试体验很赞,当然萝卜青菜各有所爱。最坑的二面的时候对方面试官的网络老是掉线,最后很冤枉的挂了(当然有一些点答得不好也是原因之一)。所以还是有点遗憾的。 - -### 一面 - -- 先自我介绍下 -- 聊项目,逆向系统是什么意思 -- 聊项目,逆向系统用了哪些技术 -- 线程池的线程数怎么确定? -- 如果是IO操作为主怎么确定? -- 如果计算型操作又怎么确定? -- Redis熟悉么,了解哪些数据结构?(说了zset) zset底层怎么实现的?(跳表) -- 跳表的查询过程是怎么样的,查询和插入的时间复杂度?(说了先从第一层查找,不满足就下沉到第二层找,因为每一层都是有序的,写入和插入的时间复杂度都是O(logN)) -- 红黑树了解么,时间复杂度?(说了是N叉平衡树,O(logN)) -- 既然两个数据结构时间复杂度都是O(logN),zset为什么不用红黑树(跳表实现简单,踩坑成本低,红黑树每次插入都要通过旋转以维持平衡,实现复杂) -- 点了点头,说下Dubbo的原理?(说了服务注册与发布以及消费者调用的过程)踩过什么坑没有?(说了dubbo异常处理的和打印accesslog的问题) -- CAS了解么?(说了CAS的实现)还了解其他同步机制么?(说了synchronize以及两者的区别,一个乐观锁,一个悲观锁) -- 那我们做一道题吧,数组A,2*n个元素,n个奇数、n个偶数,设计一个算法,使得数组奇数下标位置放置的都是奇数,偶数下标位置放置的都是偶数 -- 先说下你的思路(从0下标开始遍历,如果是奇数下标判断该元素是否奇数,是则跳过,否则从该位置寻找下一个奇数) -- 下一个奇数?怎么找?(有点懵逼,思考中。。) -- 有思路么?(仍然是先遍历一次数组,并对下标进行判断,如果下标属性和该位置元素不匹配从当前下标的下一个遍历数组元素,然后替换) -- 你这样时间复杂度有点高,如果要求O(N)要怎么做(思考一会,答道“定义两个指针,分别从下标0和1开始遍历,遇见奇数位是是偶数和偶数位是奇数就停下,交换内容”) -- 时间差不多了,先到这吧。你有什么想问我的? - -### 二面 - -- 面试官和蔼很多,你先介绍下自己吧 -- 你对服务治理怎么理解的? -- 项目中的限流怎么实现的?(Guava ratelimiter,令牌桶算法) -- 具体怎么实现的?(要点是固定速率且令牌数有限) -- 如果突然很多线程同时请求令牌,有什么问题?(导致很多请求积压,线程阻塞) -- 怎么解决呢?(可以把积压的请求放到消息队列,然后异步处理) -- 如果不用消息队列怎么解决?(说了RateLimiter预消费的策略) -- 分布式追踪的上下文是怎么存储和传递的?(ThreadLocal + spanId,当前节点的spanId作为下个节点的父spanId) -- Dubbo的RpcContext是怎么传递的?(ThreadLocal)主线程的ThreadLocal怎么传递到线程池?(说了先在主线程通过ThreadLocal的get方法拿到上下文信息,在线程池创建新的ThreadLocal并把之前获取的上下文信息设置到ThreadLocal中。这里要注意的线程池创建的ThreadLocal要在finally中手动remove,不然会有内存泄漏的问题) -- 你说的内存泄漏具体是怎么产生的?(说了ThreadLocal的结构,主要分两种场景:主线程仍然对ThreadLocal有引用和主线程不存在对ThreadLocal的引用。第一种场景因为主线程仍然在运行,所以还是有对ThreadLocal的引用,那么ThreadLocal变量的引用和value是不会被回收的。第二种场景虽然主线程不存在对ThreadLocal的引用,且该引用是弱引用,所以会在gc的时候被回收,但是对用的value不是弱引用,不会被内存回收,仍然会造成内存泄漏) -- 线程池的线程是不是必须手动remove才可以回收value?(是的,因为线程池的核心线程是一直存在的,如果不清理,那么核心线程的threadLocals变量会一直持有ThreadLocal变量) -- 那你说的内存泄漏是指主线程还是线程池?(主线程 ) -- 可是主线程不是都退出了,引用的对象不应该会主动回收么?(面试官和内存泄漏杠上了),沉默了一会。。。 -- 那你说下SpringMVC不同用户登录的信息怎么保证线程安全的?(刚才解释的有点懵逼,一下没反应过来,居然回答成锁了。大脑有点晕了,此时已经一个小时过去了,感觉情况不妙。。。) -- 这个直接用ThreadLocal不就可以么,你见过SpringMVC有锁实现的代码么?(有点晕菜。。。) -- 我们聊聊mysql吧,说下索引结构(说了B+树) -- 为什么使用B+树?( 说了查询效率高,O(logN),可以充分利用磁盘预读的特性,多叉树,深度小,叶子结点有序且存储数据) -- 什么是索引覆盖?(忘记了。。。 ) -- Java为什么要设计双亲委派模型? -- 什么时候需要自定义类加载器? -- 我们做一道题吧,手写一个对象池 -- 有什么想问我的么?(感觉我很多点都没答好,是不是挂了(结果真的是) ) - -### 小结 - -头条的面试确实很专业,每次面试官会提前给你发一个视频链接,然后准点开始面试,而且考察的点都比较全。 - -面试官都有一个特点,会抓住一个值得深入的点或者你没说清楚的点深入下去直到你把这个点讲清楚,不然面试官会觉得你并没有真正理解。二面面试官给了我一点建议,研究技术的时候一定要去研究产生的背景,弄明白在什么场景解决什么特定的问题,其实很多技术内部都是相通的。很诚恳,还是很感谢这位面试官大大。 - -## 总结 - -从年前开始面试到头条面完大概一个多月的时间,真的有点身心俱疲的感觉。最后拿到了拼多多、蚂蚁的offer,还是蛮幸运的。头条的面试对我帮助很大,再次感谢面试官对我的诚恳建议,以及拼多多的HR对我的啰嗦的问题详细解答。 - -这里要说的是面试前要做好两件事:简历和自我介绍,简历要好好回顾下自己做的一些项目,然后挑几个亮点项目。自我介绍基本每轮面试都有,所以最好提前自己练习下,想好要讲哪些东西,分别怎么讲。此外,简历提到的技术一定是自己深入研究过的,没有深入研究也最好找点资料预热下,不打无准备的仗。 - -**这些年看过的书**: - -《Effective Java》、《现代操作系统》、《TCP/IP详解:卷一》、《代码整洁之道》、《重构》、《Java程序性能优化》、《Spring实战》、《Zookeeper》、《高性能MySQL》、《亿级网站架构核心技术》、《可伸缩服务架构》、《Java编程思想》 - -说实话这些书很多只看了一部分,我通常会带着问题看书,不然看着看着就睡着了,简直是催眠良药😅。 - - -最后,附一张自己面试前准备的脑图: - -链接:https://pan.baidu.com/s/1o2l1tuRakBEP0InKEh4Hzw 密码:300d - -全文完。 diff --git a/docs/essential-content-for-interview/BATJrealInterviewExperience/2020-zijietiaodong.md b/docs/essential-content-for-interview/BATJrealInterviewExperience/2020-zijietiaodong.md deleted file mode 100644 index 0e63008bc27..00000000000 --- a/docs/essential-content-for-interview/BATJrealInterviewExperience/2020-zijietiaodong.md +++ /dev/null @@ -1,61 +0,0 @@ - - -> 本文来自读者 Boyn 投稿!恭喜这位粉丝拿到了含金量极高的字节跳动实习 offer!赞! - -## 基本条件 - -本人是底层 211 本科,现在大三,无科研经历,但是有一些项目经历,在国内监控行业某头部企业做过一段时间的实习。想着投一下字节,可以积累一下面试经验和为春招做准备.投了简历之后,过了一段时间,HR 就打电话跟我约时间,在年后进行远程面。 - -说明一下,我投的是北京 office。 - -## 一面 - -面试官很和蔼,由于疫情的原因,大家都在家里面进行远程面试 - -开头没有自我介绍,直接开始问项目了,问了比如 - -- 常用的 Web 组件有哪些(回答了自己经常用到的 SpringBoot,Redis,Mysql 等等,字节这边基本没有用 Java 的后台,所以感觉面试官不大会问 Spring,Java 这些东西,反倒是对数据库和中间件比较感兴趣) -- Kafka 相关,如何保证不会重复消费,Kafka 消费组结构等等(这个只是凭着感觉和面试官说了,因为 Kafka 自己确实准备得不充分,但是心态稳住了) -- **Mysql 索引,B+树(必考嗷同学们)** - -还有一些项目中的细节,这些因人而异,就不放上来了,提示一点就是要在项目中介绍一些亮眼的地方,比如用了什么牛逼的数据结构,架构上有什么特点,并发量大小还有怎么去 hold 住并发量 - -后面就是算法题了,一共做了两道 - -1. 判断平衡二叉树(这道题总体来说并不难,但是面试官在中间穿插了垃圾回收的知识,这就很难受了,具体的就是大家要判断一下对象在什么时候会回收,可达性分析什么时候对这个对象来说是不可达的,还有在递归函数中内存如何变化,这个是让我们来对这个函数进行执行过程的建模,只看栈帧大小变化的话,应该有是两个峰值,中间会有抖动的情况) -2. 二分查找法的变种题,给定`target`和一个升序的数组,寻找下一个比数组大的数.这道题也不难,靠大家对二分查找法的熟悉程度,当然,这边还有一个优化的点,可以看看[我的博客](https://boyn.top/2019/11/09/%E7%AE%97%E6%B3%95%E4%B8%8E%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%B3%95/)找找灵感 - -完成了之后,面试官让我等一会有二面,大概 10 分钟左右吧,休息了一会就继续了 - -## 二面 - -二面一上来就是先让我自我介绍,当然还是同样的套路,同样的香脆 - -然后问了我一些关于 Redis 的问题,比如 **zset 的实现(跳表,这个高频)** ,键的过期策略,持久化等等,这些在大多数 Redis 的介绍中都可以找到,就不细说了 - -还有一些数据结构的问题,比如说问了哈希表是什么,给面试官详细说了一下`java.util.HashMap`是怎么实现(当然里面就穿插着红黑树了,多看看红黑树是有什么特点之类的)的,包括说为什么要用链地址法来避免冲突,探测法有哪些,链地址法和探测法的优劣对比 - -后面还跟我讨论了很久的项目,所以说大家的项目一定要做好,要有亮点的地方,在这里跟面试官讨论了很多项目优化的地方,还有什么不足,还有什么地方可以新增功能等等,同样不细说了 - -一边讨论的时候劈里啪啦敲了很多,应该是对个人的面试评价一类的 - -后面就是字节的传统艺能手撕算法了,一共做了三道 - -- 一二道是连在一起的.给定一个规则`S_0 = {1} S_1={1,2,1} S_2 = {1,2,1,3,1,2,1} S_n = {S_n-1 , n + 1, S_n-1}`.第一个问题是他们的个数有什么关系(1 3 7 15... 2 的 n 次方-1,用位运算解决).第二个问题是给定数组个数下标 n 和索引 k,让我们求出 S_n(k)所指的数,假如`S_2(2) = 1`,我在做的时候没有什么好的思路,如果有的话大家可以分享一下 -- 第三道是下一个排列:[https://leetcode-cn.com/problems/next-permutation](https://leetcode-cn.com/problems/next-permutation) 的题型,不过做了一些修改,数组大小`10000 作者:ppxyn。本文来自读者投稿,同时也欢迎各位投稿,**对于不错的原创文章我根据你的选择给予现金(50-200)、付费专栏或者任选书籍进行奖励!所以,快提 pr 或者邮件的方式(邮件地址在主页)给我投稿吧!** 当然,我觉得奖励是次要的,最重要的是你可以从自己整理知识点的过程中学习到很多知识。 - -**目录** - - - -- [前言](#前言) -- [一面\(技术面\)](#一面技术面) -- [二面\(技术面\)](#二面技术面) -- [三面\(技术面\)](#三面技术面) -- [四面\(半个技术面\)](#四面半个技术面) -- [五面\(HR面\)](#五面hr面) -- [总结](#总结) - - - -### 前言 - -在接触 Java 之前我接触的比较多的是硬件方面,用的比较多的语言就是C和C++。到了大三我才正式选择 Java 方向,到目前为止使用Java到现在大概有一年多的时间,所以Java算不上很好。刚开始投递的时候,实习刚辞职,也没准备笔试面试,很多东西都忘记了。所以,刚开始我并没有直接就投递阿里,毕竟心里还是有一点点小害怕的。于是,我就先投递了几个不算大的公司来练手,就是想着刷刷经验而已或者说是练练手(ps:还是挺对不起那些公司的)。面了一个月其他公司后,我找了我实验室的学长内推我,后面就有了这5次面试。 - -下面简单的说一下我的这5次面试:4次技术面+1次HR面,希望我的经历能对你有所帮助。 - -### 一面(技术面) - -1. 自我介绍(主要讲自己会的技术细节,项目经验,经历那些就一语带过,后面面试官会问你的)。 -2. 聊聊项目(就是一个很普通的分布式商城,自己做了一些改进),让我画了整个项目的架构图,然后针对项目抛了一系列的提高性能的问题,还问了我做项目的过程中遇到了那些问题,如何解决的,差不读就这些吧。 -3. 可能是我前面说了我会数据库优化,然后面试官就开始问索引、事务隔离级别、悲观锁和乐观锁、索引、ACID、MVVC这些问题。 -4. 浏览器输入URL发生了什么? TCP和UDP区别? TCP如何保证传输可靠性? -5. 讲下跳表怎么实现的?哈夫曼编码是怎么回事?非递归且不用额外空间(不用栈),如何遍历二叉树 -6. 后面又问了很多JVM方面的问题,比如Java内存模型、常见的垃圾回收器、双亲委派模型这些 -7. 你有什么问题要问吗? - -### 二面(技术面) - -1. 自我介绍(主要讲自己会的技术细节,项目经验,经历那些就一语带过,后面面试官会问你的)。 -2. 操作系统的内存管理机制 -3. 进程和线程的区别 -4. 说下你对线程安全的理解 -5. volatile 有什么作用 ,sychronized和lock有什么区别 -6. ReentrantLock实现原理 -7. 用过CountDownLatch么?什么场景下用的? -8. AQS底层原理。 -9. 造成死锁的原因有哪些,如何预防? -10. 加锁会带来哪些性能问题。如何解决? -11. HashMap、ConcurrentHashMap源码。HashMap是线程安全的吗?Hashtable呢?ConcurrentHashMap有了解吗? -12. 是否可以实习? -13. 你有什么问题要问吗? - -### 三面(技术面) - -1. 有没有参加过 ACM 或者他竞赛,有没有拿过什么奖?( 我说我没参加过ACM,本科参加过数学建模竞赛,名次并不好,没拿过什么奖。面试官好像有点失望,然后我又赶紧补充说我和老师一起做过一个项目,目前已经投入使用。面试官还比较感兴趣,后面又和他聊了一下这个项目。) -2. 研究生期间,做过什么项目,发过论文吗?有什么成果吗? -3. 你觉得你有什么优点和缺点?你觉得你相比于那些比你更优秀的人欠缺什么? -4. 有读过什么源码吗?(我说我读过 Java 集合框架和 Netty 的,面试官说 Java 集合前几面一定问的差不多,就不问了,然后就问我 Netty的,我当时很慌啊!) -5. 介绍一下自己对 Netty 的认识,为什么要用。说说业务中,Netty 的使用场景。什么是TCP 粘包/拆包,解决办法。Netty线程模型。Dubbo 在使用 Netty 作为网络通讯时候是如何避免粘包与半包问题?讲讲Netty的零拷贝?巴拉巴拉问了好多,我记得有好几个我都没回答上来,心里想着凉凉了啊。 -6. 用到了那些开源技术、在开源领域做过贡献吗? -7. 常见的排序算法及其复杂度,现场写了快排。 -8. 红黑树,B树的一些问题。 -9. 讲讲算法及数据结构在实习项目中的用处。 -10. 自己的未来规划(就简单描述了一下自己未来的设想啊,说的还挺诚恳,面试官好像还挺满意的) -11. 你有什么问题要问吗? - -### 四面(半个技术面) - -三面面完当天,晚上9点接到面试电话,感觉像是部门或者项目主管。 这个和之前的面试不大相同,感觉面试官主要考察的是你解决问题的能力、学习能力和团队协作能力。 - -1. 让我讲一个自己觉得最不错的项目。然后就巴拉巴拉的聊,我记得主要是问了项目是如何进行协作的、遇到问题是如何解决的、与他人发生冲突是如何解决的这些。感觉聊了挺久。 -2. 出现 OOM 后你会怎么排查问题? -3. 自己平时是如何学习新技术的?除了 Java 还回去了解其他技术吗? -4. 上一段实习经历的收获。 -5. NginX如何做负载均衡、常见的负载均衡算法有哪些、一致性哈希的一致性是什么意思、一致性哈希是如何做哈希的 -6. 你有什么问题问我吗? -7. 还有一些其他的,想不起来了,感觉这一面不是偏向技术来问。 - -## 五面(HR面) - -1. 自我介绍(主要讲能突出自己的经历,会的编程技术一语带过)。 -2. 你觉得你有什么优点和缺点?如何克服这些缺点? -3. 说一件大学里你自己比较有成就感的一件事情,为此付出了那些努力。 -4. 你前面跟其他面试官讲过一些你做的项目吧?可以给我讲讲吗?你要考虑到我不是一个做技术的人,怎么让我也听得懂。项目中有什么问题,你怎么解决的?你最大的收获是什么? -5. 你目前有面试过其他公司吗?如果让你选,这些公司和阿里,你选哪个?(送分题,回答不好可能送命) -6. 你期望的工作地点是哪里? -7. 你有什么问题吗? - -### 总结 - -1. 可以看出面试官问我的很多问题都是比较常见的问题,所以记得一定要提前准备,还要深入准备,不要回答的太皮毛。很多时候一个问题可能会牵扯出很多问题,遇到不会的问题不要慌,冷静分析,如果你真的回答不上来,也不要担心自己是不是就要挂了,很可能这个问题本身就比较难。 -2. 表达能力和沟通能力太重要了,一定要提前练一下,我自身就是一个不太会说话的人,所以,面试前我对于自我介绍、项目介绍和一些常见问题都在脑子里练了好久,确保面试的时候能够很清晰和简洁的说出来。 -3. 等待面试的过程和面试的过程真的好熬人,那段时间我压力也比较大,好在我私下找到学长聊了很多,心情也好了很多。 -4. 面试之后及时总结,面的好的话,不要得意,尽快准备下一场面试吧! - -我觉得我还算是比较幸运的,最后也祝大家都能获得心仪的Offer。 - - - - diff --git a/docs/essential-content-for-interview/BATJrealInterviewExperience/bingo-interview.md b/docs/essential-content-for-interview/BATJrealInterviewExperience/bingo-interview.md deleted file mode 100644 index c43656a19d0..00000000000 --- a/docs/essential-content-for-interview/BATJrealInterviewExperience/bingo-interview.md +++ /dev/null @@ -1,125 +0,0 @@ -> 本文是鄙人薛某这位老哥的投稿,虽然面试最后挂了,但是老哥本身还是挺优秀的,而且通过这次面试学到了很多东西,我想这就足够了!加油!不要畏惧面试失败,好好修炼自己,多准备一下,后面一定会找到让自己满意的工作。 -> - -## 背景 - -前段时间家里出了点事,辞职回家待了一段时间,处理完老家的事情后就回到广州这边继续找工作,大概是国庆前几天我去面试了一家叫做Bigo(YY的子公司),面试的职位是面向3-5年的Java开发,最终自己倒在了第三轮的技术面上。虽然有些遗憾和泄气,但想着还是写篇博客来记录一下自己的面试过程好了,也算是对广大程序员同胞们的分享,希望对你们以后的学习和面试能有所帮助。 - -## 个人情况 - -先说下LZ的个人情况。 - -17年毕业,二本,目前位于广州,是一个非常普通的Java开发程序员,算起来有两年多的开发经验。 - -其实这个阶段有点尴尬,高不成低不就,比初级程序员稍微好点,但也达不到高级的程度。加上现如今IT行业接近饱和,很多岗位都是要求至少3-5年以上开发经验,所以对于两年左右开发经验的需求其实是比较小的,这点在LZ找工作的过程中深有体会。最可悲的是,今年的大环境不好,很多公司不断的在裁员,更别说招人了,残酷的形势对于求职者来说更是雪上加霜,相信很多求职的同学也有所体会。所以,不到万不得已的情况下,建议不要裸辞! - -## Bigo面试 - -面试岗位:Java后台开发 - -经验要求:3-5年 - -由于是国庆前去面试Bigo的,到现在也有一个多月的时间了,虽然仍有印象,但也有不少面试题忘了,所以我只能尽量按照自己的回忆来描述面试的过程,不明白之处还请见谅! - -### 一面(微信电话面) - -bigo的第一面是微信电话面试,本来是想直接电话面,但面试官说需要手写算法题,就改成微信电话面。 - -- 自我介绍 -- 先了解一下Java基础吧,什么是内存泄漏和内存溢出?(溢出是指创建太多对象导致内存空间不足,泄漏是无用对象没有回收) -- JVM怎么判断对象是无用对象?(根搜索算法,从GC Root出发,对象没有引用,就判定为无用对象) -- 根搜索算法中的根节点可以是哪些对象?(类对象,虚拟机栈的对象,常量引用的对象) -- 重载和重写的区别?(重载发生在同个类,方法名相同,参数列表不同;重写是父子类之间的行为,方法名好参数列表都相同,方法体内的程序不同) -- 重写有什么限制没有? -- Java有哪些同步工具?(synchronized和Lock) -- 这两者有什么区别? -- ArrayList和LinkedList的区别?(ArrayList基于数组,搜索快,增删元素慢,LinkedList基于链表,增删快,搜索因为要遍历元素所以效率低) -- 这两种集合哪个比较占内存?(看情况的,ArrayList如果有扩容并且元素没占满数组的话,浪费的内存空间也是比较多的,但一般情况下,LinkedList占用的内存会相对多点,因为每个元素都包含了指向前后节点的指针) -- 说一下HashMap的底层结构(数组 + 链表,链表过长变成红黑树) -- HashMap为什么线程不安全,1.7版本之前HashMap有什么问题(扩容时多线程操作可能会导致链表成环的出现,然后调用get方法会死循环) -- 了解ConcurrentHashMap吗?说一下它为什么能线程安全(用了分段锁) -- 哪些方法需要锁住整个集合的?(读取size的时候) -- 看你简历写着你了解RPC啊,那你说下RPC的整个过程?(从客户端发起请求,到socket传输,然后服务端处理消息,以及怎么序列化之类的都大概讲了一下) -- 服务端获取客户端要调用的接口信息后,怎么找到对应的实现类的?(反射 + 注解吧,这里也不是很懂) -- dubbo的负载均衡有几种算法?(随机,轮询,最少活跃请求数,一致性hash) -- 你说的最少活跃数算法是怎么回事?(服务提供者有一个计数器,记录当前同时请求个数,值越小说明该服务器负载越小,路由器会优先选择该服务器) -- 服务端怎么知道客户端要调用的算法的?(socket传递消息过来的时候会把算法策略传递给服务端) -- 你用过redis做分布式锁是吧,你们是自己写的工具类吗?(不是,我们用redission做分布式锁) -- 线程拿到key后是怎么保证不死锁的呢?(给这个key加上一个过期时间) -- 如果这个过期时间到了,但是业务程序还没处理完,该怎么办?(额......可以在业务逻辑上保证幂等性吧) -- 那如果多个业务都用到分布式锁的话,每个业务都要保证幂等性了,有没有更好的方法?(额......思考了下暂时没有头绪,面试官就说那先跳过吧。事后我了解到redission本身是有个看门狗的监控线程的,如果检测到key被持有的话就会再次重置过期时间) -- 你那边有纸和笔吧,写一道算法,用两个栈模拟一个队列的入队和出队。(因为之前复习的时候对这道题有印象,写的时候也比较快,大概是用了五分钟,然后就拍成图片发给了面试官,对方看完后表示没问题就结束了面试。) - -第一面问的不算难,问题也都是偏基础之类的,虽然答得不算完美,但过程还是比较顺利的。几天之后,Bigo的hr就邀请我去他们公司参加现场面试。 - -### 二面 - -到Bigo公司后,一位hr小姐姐招待我到了一个会议室,等了大概半个小时,一位中年男子走了进来,非常的客气,说不好意思让我等那么久了,并且介绍了自己是技术经理,然后就开始了我们的交谈。 - -- 依照惯例,让我简单做下自我介绍,这个过程他也在边看我的简历。 -- 说下你最熟悉的项目吧。(我就拿我上家公司最近做的一个电商项目开始介绍,从简单的项目描述,到项目的主要功能,以及我主要负责的功能模块,吧啦吧啦..............) -- 你对这个项目这么熟悉,那你根据你的理解画一下你的项目架构图,还有说下你具体参与了哪部分。(这个题目还是比较麻烦的,毕竟我当时离职的时间也挺长了,对这个项目的架构也是有些模糊。当然,最后还是硬着头皮还是画了个大概,从前端开始访问,然后通过nginx网关层,最后到具体的服务等等,并且把自己参与的服务模块也标示了出来) -- 你的项目用到了Spring Cloud GateWay,既然你已经有nginx做网关了,为什么还要用gateWay呢?(nginx是做负载均衡,还有针对客户端的访问做网关用的,gateWay是接入业务层做的网关,而且还整合了熔断器Hystrix) -- 熔断器Hystrix最主要的作用是什么?(防止服务调用失败导致的服务雪崩,能降级) -- 你的项目用到了redis,你们的redis是怎么部署的?(额。。。。好像是哨兵模式部署的吧。) -- 说一下你对哨兵模式的理解?(我对哨兵模式了解的不多,就大概说了下Sentinel监控之类的,还有类似ping命令的心跳机制,以及怎么判断一个master是下线之类。。。。。) -- 那你们为什么要用哨兵模式呢?怎么不用集群的方式部署呢?一开始get不到他的点,就说哨兵本身就是多实例部署的,他解释了一下,说的是redis-cluster的部署方案。(额......redis的环境搭建有专门的运维人员部署的,应该是优先考虑高可用吧..........开始有点心慌了,因为我也不知道为什么) -- 哦,那你是觉得集群没有办法实现高可用吗?(不....不是啊,只是觉得哨兵模式可能比较保证主从复制安全性吧........我也不知道自己在说什么) -- 集群也是能保证高可用的,你知道它又是怎么保证主从一致性的吗?(好吧,这里真的不知道了,只能跳过) -- 你肯定有微信吧,如果让你来设计微信朋友圈的话,你会怎么设计它的属性成员呢?(嗯......需要有用户表,朋友圈的表,好友表之类的吧) -- 嗯,好,你也知道微信用户有接近10亿之多,那肯定要涉及到分库分表,如果是你的话,怎么设计分库分表呢?(这个问题考察的点比较大,我答的其实一般,而且这个过程面试官还不断的进行连环炮发问,导致这个话题说了有将近20分钟,限于篇幅,这里就不再详述了) -- 这边差不多了,最后你写一道算法吧,有一组未排序的整形数组,你设计一个算法,对数组的元素两两配对,然后输出最大的绝对值差和最小的绝对值差的"对数"。(听到这道题,我第一想法就是用HashMap来保存,key是两个元素的绝对值差,value是配对的数量,如果有相同的就加1,没有就赋值为1,然后最后对map做排序,输出最大和最小的value值,写完后面试官说结果虽然是正确的,但是不够效率,因为遍历的时间复杂度成了O(n^2),然后提醒了我往排序这方面想。我灵机一动,可以先对数组做排序,然后首元素与第二个元素做绝对值差,记为num,然后首元素循环和后面的元素做计算,直到绝对值差不等于num位置,这样效率比起O(n^2)快多了。) - -面试完后,技术官就问我有什么要问他的,我就针对这个岗位的职责和项目所用的技术栈做了询问,然后就让我先等下,等他去通知三面的技术官。说实话,二面给我的感觉是最舒服的,因为面试官很亲切,面试的过程一直积极的引导我,而且在职业规划方面给了我很多的建议,让我受益匪浅,虽然面试时间有一个半小时,但却丝毫不觉得长,整个面试过程聊得挺舒服的,不过因为时间比较久了,很多问题我也记不清了。 - -### 三面 - -二面结束后半个小时,三面的技术面试官就开始进来了,从他的额头发量分布情况就能猜想是个大牛,人狠话不多,坐下后也没让我做自我介绍,直接开问,整个过程我答的也不好,而且面试官的问题表述有些不太清晰,经常需要跟他重复确认清楚。 - -- 对事务了解吗?说一下事务的隔离级别有哪些(我以比较了解的Spring来说,把Spring的四种事务隔离级别都叙述了一遍) - -- 你做过电商,那应该知道下单的时候需要减库存对吧,假设现在有两个服务A和B,分别操作订单和库存表,A保存订单后,调用B减库存的时候失败了,这个时候A也要回滚,这个事务要怎么设计?(B服务的减库存方法不抛异常,由调用方也就是A服务来抛异常) - -- 了解过读写分离吗?(额。。。大概了解一点,就是写的时候进主库,读的时候读从库) - -- 你说读的时候读从库,现在假设有一张表User做了读写分离,然后有个线程在**一个事务范围内**对User表先做了写的处理,然后又做了读的处理,这时候数据还没同步到从库,怎么保证读的时候能读到最新的数据呢?(听完顿时有点懵圈,一时间答不上来,后来面试官说想办法保证一个事务中读写都是同一个库才行) - -- 你的项目里用到了rabbitmq,那你说下mq的消费端是怎么处理的?(就是消费端接收到消息之后,会先把消息存到数据库中,然后再从数据库中定时跑消息) - -- 也就是说你的mq是先保存到数据库中,然后业务逻辑就是从mq中读取消息然后再处理的是吧?(是的) - -- 那你的消息是唯一的吗?(是的,用了唯一约束) - -- 你怎么保证消息一定能被消费?或者说怎么保证一定能存到数据库中?(这里开始慌了,因为mq接入那一块我只是看过部分逻辑,但没有亲自参与,凭着自己对mq的了解就答道,应该是靠rabbitmq的ack确认机制) - -- 好,那你整理一下你的消费端的整个处理逻辑流程,然后说说你的ack是在哪里返回的(听到这里我的心凉了一截,mq接入这部分我确实没有参与,硬着头皮按照自己的理解画了一下流程,但其实漏洞百出) - -- 按照你这样画的话,如果数据库突然宕机,你的消息该怎么确认已经接收?(额.....那发送消息的时候就存放消息可以吧.........回答的时候心里千万只草泥马路过........行了吧,没玩没了了。) - -- 那如果发送端的服务是多台部署呢?你保存消息的时候数据库就一直报唯一性的错误?(好吧,你赢了。。。最后硬是憋出了一句,您说的是,这样设计确实不好。。。。) - -- 算了,跳过吧,现在你来设计一个map,然后有两个线程对这个map进行操作,主线程高速增加和删除map的元素,然后有个异步线程定时去删除map中主线程5秒内没有删除的数据,你会怎么设计? - - (这道题我答得并不好,做了下简单的思考就说可以把map的key加上时间戳的标志,遍历的时候发现小于当前时间戳5秒前的元素就进行删除,面试官对这样的回答明显不太满意,说这样遍历会影响效率,ps:对这道题,大佬们如果有什么高见可以在评论区说下!) - -......还有其他问题,但我只记住了这么多,就这样吧。 - -面完最后一道题后,面试官就表示这次面试过程结束了,让我回去等消息。听到这里,我知道基本上算是宣告结果了。回想起来,自己这一轮面试确实表现的很一般,加上时间拖得很长,从当天的2点半一直面试到6点多,精神上也尽显疲态。果然,几天之后,hr微信通知了我,说我第三轮技术面试没有通过,这一次面试以失败告终。 - -## 总结 - -以上就是面试的大概过程,不得不说,大厂的面试还是非常有技术水平的,这个过程中我学到了很多,这里分享下个人的一些心得: - -1、**基础**!**基础**!**基础**!重要的事情说三遍,无论是什么阶段的程序员,基础都是最重要的。每个公司的面试一定会涉及到基础知识的提问,如果你的基础不扎实,往往第一面就可能被淘汰。 - -2、**简历需要适当的包装**。老实说,我的简历肯定是经过包装的,这也是我的工作年限不够,但却能获取Bigo面试机会的重要原因,所以适当的包装一下简历很有必要,不过切记一点,就是**不能脱离现实**,比如明明只有两年经验,却硬是写到三年。小厂还可能蒙混过关,但大厂基本很难,因为很多公司会在入职前做背景调查。 - -3、**要对简历上的技术点很熟悉**。简历包装可以,但一定要对简历上的技术点很熟悉,比如只是简单写过rabbitmq的demo的话,就不要写“熟悉”等字眼,因为很多的面试官会针对一个技能点问的很深入,像连环炮一样的深耕你对这个技能点的理解程度。 - -4、**简历上的项目要非常熟悉**。一般我们写简历都是需要对自己的项目做一定程序的包装和美化,项目写得好能给简历加很多分。但一定要对项目非常的熟悉,不熟悉的模块最好不要写上去。笔者这次就吃了大亏,我的简历上有个电商项目就写到了用rabbitmq处理下单,虽然稍微了解过那部分下单的处理逻辑,但由于没有亲自参与就没有做深入的了解,面试时在这一块内容上被Bigo三面的面试官逼得最后哑口无言。 - -5、**提升自己的架构思维**。对于初中级程序员来说,日常的工作就是基本的增删改查,把功能实现就完事了,这种思维不能说不好,只是想更上一层楼的话,业务时间需要提升下自己的架构思维能力,比如说如果让你接手一个项目的话,你会怎么考虑设计这个项目,从整体架构,到引入一些组件,再到设计具体的业务服务,这些都是设计一个项目必须要考虑的环节,对于提升我们的架构思维是一种很好的锻炼,这也是很多大厂面试高级程序员时的重要考察部分。 - -6、**不要裸辞**。这也是我最朴实的建议了,大环境不好,且行且珍惜吧,唉~~~~ - -总的来说,这次面试Bigo还是收获颇丰的,虽然有点遗憾,但也没什么后悔的,毕竟自己面试之前也是准备的很充分了,有些题目答得不好说明我还有很多技术盲区,不懂就是不懂,再这么吹也吹不出来。这也算是给我提了个醒,你还嫩着呢,好好修炼内功吧,毕竟菜可是原罪啊。 \ No newline at end of file diff --git "a/docs/essential-content-for-interview/BATJrealInterviewExperience/\350\232\202\350\232\201\351\207\221\346\234\215\345\256\236\344\271\240\347\224\237\351\235\242\347\273\217\346\200\273\347\273\223(\345\267\262\346\213\277\345\217\243\345\244\264offer).md" "b/docs/essential-content-for-interview/BATJrealInterviewExperience/\350\232\202\350\232\201\351\207\221\346\234\215\345\256\236\344\271\240\347\224\237\351\235\242\347\273\217\346\200\273\347\273\223(\345\267\262\346\213\277\345\217\243\345\244\264offer).md" deleted file mode 100644 index e098432520c..00000000000 --- "a/docs/essential-content-for-interview/BATJrealInterviewExperience/\350\232\202\350\232\201\351\207\221\346\234\215\345\256\236\344\271\240\347\224\237\351\235\242\347\273\217\346\200\273\347\273\223(\345\267\262\346\213\277\345\217\243\345\244\264offer).md" +++ /dev/null @@ -1,251 +0,0 @@ -本文来自 Anonymous 的投稿 ,Guide哥 对原文进行了重新排版和一点完善。 - - - -- [一面 (37 分钟左右)](#一面-37-分钟左右) -- [二面 (33 分钟左右)](#二面-33-分钟左右) -- [三面 (46 分钟)](#三面-46-分钟) -- [HR 面](#hr-面) - - - -### 一面 (37 分钟左右) - -一面是上海的小哥打来的,3.12 号中午确认的内推,下午就打来约时间了,也是唯一一个约时间的面试官。约的晚上八点。紧张的一比,人生第一次面试就献给了阿里。 - -幸运的是一面的小哥特温柔。好像是个海归?口语中夹杂着英文。废话不多说,上干货: - -**面试官:** 先自我介绍下吧! - -**我:** 巴拉巴拉...。 - -> 关于自我介绍:从 HR 面、技术面到高管面/部门主管面,面试官一般会让你先自我介绍一下,所以好好准备自己的自我介绍真的非常重要。网上一般建议的是准备好两份自我介绍:一份对 HR 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。 - -**面试官:** 我看你简历上写你做了个秒杀系统?我们就从这个项目开始吧,先介绍下你的项目。 - -> 关于项目介绍:如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑: -> -> 1. 对项目整体设计的一个感受(面试官可能会让你画系统的架构图) -> 2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -> 3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -> 4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用 redis 做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 - -**我:** 我说了我是如何考虑它的需求(秒杀地址隐藏,记录订单,减库存),一开始简单的用 synchronized 锁住方法,出现了问题,后来乐观锁改进,又有瓶颈,再上缓存,出现了缓存雪崩,于是缓存预热,错开缓存失效时间。最后,发现先记录订单再减库存会减少行级锁等待时间。 - -> 一面面试官很耐心地听,并给了我一些指导,问了我乐观锁是怎么实现的,我说是基于 sql 语句,在减库存操作的 where 条件里加剩余库存数>0,他说这应该不算是一种乐观锁,应该先查库存,在减库存的时候判断当前库存是否与读到的库存一样(可这样不是多一次查询操作吗?不是很理解,不过我没有反驳,只是说理解您的意思。事实证明千万别怼面试官,即使你觉得他说的不对) - -**面试官:** 我缓存雪崩什么情况下会发生?如何避免? - -**我:** 当多个商品缓存同时失效时会雪崩,导致大量查询数据库。还有就是秒杀刚开始的时候缓存里没有数据。解决方案:缓存预热,错开缓存失效时间 - -**面试官:** 问我更新数据库的同时为什么不马上更新缓存,而是删除缓存? - -**我:** 因为考虑到更新数据库后更新缓存可能会因为多线程下导致写入脏数据(比如线程 A 先更新数据库成功,接下来要取更新缓存,接着线程 B 更新数据库,但 B 又更新了缓存,接着 B 的时间片用完了,线程 A 更新了缓存) - -逼逼了将近 30 分钟,面试官居然用周杰伦的语气对我说: - -![not bad](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3not-bad.jpg) - -我突然受宠若惊,连忙说谢谢,也正是因为第一次面试得到了面试官的肯定,才让我信心大增,二三面稳定发挥。 - -**面试官又曰:** 我看你还懂数据库是吧,答:略懂略懂。。。那我问个简单的吧! - -**我:** 因为这个问题太简单了,所以我忘记它是什么了。 - -**面试官:** 你还会啥数据库知识? - -**我:** 我一听,问的这么随意的吗。。。都让我选题了,我就说我了解索引,慢查询优化,巴拉巴拉 - -**面试官:** 等等,你说索引是吧,那你能说下索引的存储数据结构吗? - -**我:** 我心想这简单啊,我就说 B+树,还说了为什么用 B+树 - -**面试官:** 你简历上写的这个 J.U.C 包是什么啊?(他居然不知道 JUC) - -**我:** 就是 java 多线程的那个包啊。。。 - -**面试官:** 那你都了解里面的哪些东西呢? - -**我:** 哈哈哈!这可是我的强项,从 ConcurrentHashMap,ConcurrentLinkedQueue 说到 CountDownLatch,CyclicBarrier,又说到线程池,分别说了底层实现和项目中的应用。 - -**面试官:** 我觉得差不多了,那我再问个与技术无关的问题哈,虽然这个问题可能不应该我问,就是你是如何考虑你的项目架构的呢? - -**我:** 先用最简单的方式实现它,再去发掘系统的问题和瓶颈,于是查资料改进架构。。。 - -**面试官:** 好,那我给你介绍下我这边的情况吧 - -![chat-end](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3chat-end.jpg) - -**总结:** 一面可能是简历面吧,问的比较简单,我在讲项目中说出了我做项目时的学习历程和思考,赢得了面试官的好感,感觉他应该给我的评价很好。 - -### 二面 (33 分钟左右) - -然而开心了没一会,内推人问我面的怎么样啊?看我流程已经到大大 boss 那了。我一听二面不是主管吗???怎么直接跳了一面。于是瞬间慌了,赶紧(下床)学习准备二面。 - -隔了一天,3.14 的早上 10:56 分,杭州的大大 boss 给我打来了电话,卧槽我当时在上毛概课,万恶的毛概课每节课都点名,我还在最后一排不敢跑出去。于是接起电话来怂怂地说不好意思我在上课,晚上可以面试吗?大大 boss 看来很忙啊,跟我说晚上没时间啊,再说吧! - -于是又隔了一天,3.16 中午我收到了北京的电话,当时心里小失望,我的大大 boss 呢???接起电话来,就是一番狂轰乱炸。。。 - -第一步还是先自我介绍,这个就不多说了,提前准备好要说的重点就没问题! - -**面试官:** 我们还是从你的项目开始吧,说说你的秒杀系统。 - -**我:** 一面时的套路。。。我考虑到秒杀地址在开始前不应暴露给用户。。。 - -**面试官:** 等下啊,为什么要这样呢?暴露给用户会怎么样? - -**我:** 用户提前知道秒杀地址就可以写脚本来抢购了,这样不公平 - -**面试官:** 那比如说啊,我现在是个黑客,我在秒杀开始时写好了脚本,运行一万个线程获取秒杀地址,这样是不是也不公平呢? - -**我:** 我考虑到了这方面,于是我自己写了个 LRU 缓存(划重点,这么多好用的缓存我为啥不用偏要自己写?就是为了让面试官上钩问我是怎么写的,这样我就可以逼逼准备好的内容了!),用这个缓存存储请求的 ip 和用户名,一个 ip 和用户名只能同时透过 3 个请求。 - -**面试官:** 那我可不可以创建一个 ip 代理池和很多用户来抢购呢?假设我有很多手机号的账户。 - -**我:** 这就是在为难我胖虎啊,我说这种情况跟真实用户操作太像了。。。我没法区别,不过我觉得可以通过地理位置信息或者机器学习算法来做吧。。。 - -**面试官:** 好的这个问题就到这吧,你接着说 - -**我:** 我把生成订单和减库存两条 sql 语句放在一个事务里,都操作成功了则认为秒杀成功。 - -**面试官:** 等等,你这个订单表和商品库存表是在一个数据库的吧,那如果在不同的数据库中呢? - -**我:** 这面试官好变态啊,我只是个本科生?!?!我觉得应该要用分布式锁来实现吧。。。 - -**面试官:** 有没有更轻量级的做法? - -**我:** 不知道了。后来查资料发现可以用消息队列来实现。使用消息队列主要能带来两个好处:(1) 通过异步处理提高系统性能(削峰、减少响应所需时间);(2) 降低系统耦合性。关于消息队列的更多内容可以查看这篇文章: - -后来发现消息队列作用好大,于是现在在学手写一个消息队列。 - -**面试官:** 好的你接着说项目吧。 - -**我:** 我考虑到了缓存雪崩问题,于是。。。 - -**面试官:** 等等,你有没有考虑到一种情况,假如说你的缓存刚刚失效,大量流量就来查缓存,你的数据库会不会炸? - -**我:** 我不知道数据库会不会炸,反正我快炸了。当时说没考虑这么高的并发量,后来发现也是可以用消息队列来解决,对流量削峰填谷。 - -**面试官:** 好项目聊(怼)完了,我们来说说别的,操作系统了解吧,你能说说 NIO 吗? - -**我:** NIO 是。。。 - -**面试官:** 那你知道 NIO 的系统调用有哪些吗,具体是怎么实现的? - -**我:** 当时复习 NIO 的时候就知道是咋回事,不知道咋实现。最近在补这方面的知识,可见 NIO 还是很重要的! - -**面试官:** 说说进程切换时操作系统都会发生什么? - -**我:** 不如杀了我,我最讨厌操作系统了。简单说了下,可能不对,需要答案自行百度。 - -**面试官:** 说说线程池? - -**答:** 卧槽这我熟啊,把 Java 并发编程的艺术里讲的都说出来了,说了得有十分钟,自夸一波,毕竟这本书我看了五遍😂 - -**面试官:** 好问问计网吧如果设计一个聊天系统,应该用 TCP 还是 UDP?为什么 - -**我:** 当然是 TCP!原因如下: - -![TCP VS UDP](https://user-gold-cdn.xitu.io/2018/4/19/162db5e97e9a9e01?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -**面试官:** 好的,你有什么要问我的吗? - -**我:** 我还有下一次面试吗? - -**面试官:** 应该。应该有的,一周内吧。还告诉我居然转正前要实习三个月?wtf,一个大三满课的本科生让我如何在八月底前实习三个月? - -**我:** 面试官再见 - -![saygoodbye-smile](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3saygoodbye-smile.jpg) - -### 三面 (46 分钟) - -3.18 号,三面来了,这次又是那个大大 boss! - -第一步还是先自我介绍,这个就不多说了,提前准备好要说的重点就没问题! - -**面试官:** 聊聊你的项目? - -**我:** 经过二面的教训,我迅速学习了一下分布式的理论知识,并应用到了我的项目(吹牛逼)中。 - -**面试官:** 看你用到了 Spring 的事务机制,你能说下 Spring 的事务传播吗? - -**我:** 完了这个问题好像没准备,虽然之前刷知乎看到过。。。我就只说出来一条,面试官说其实这个有很多机制的,比如事务嵌套,内事务回滚外事务回滚都会有不同情况,你可以回去看看。 - -**面试官:** 说说你的分布式事务解决方案? - -**我:** 我叭叭的照着资料查到的解决方案说了一通,面试官怎么好像没大听懂??? - -> 阿里巴巴之前开源了一个分布式 Fescar(一种易于使用,高性能,基于 Java 的开源分布式事务解决方案),后来,Ant Financial 加入 Fescar,使其成为一个更加中立和开放的分布式交易社区,Fescar 重命名为 Seata。Github 地址: - -**面试官:** 好,我们聊聊其他项目,说说你这个 MapReduce 项目?MapReduce 原理了解过吗? - -**我:** 我叭叭地说了一通,面试官好像觉得这个项目太简单了。要不是没项目,我会把我的实验写上吗??? - -**面试官:** 你这个手写 BP 神经网络是干了啥? - -**我:** 这是我选修机器学习课程时的一个作业,我又对它进行了扩展。 - -**面试官:** 你能说说为什么调整权值时要沿着梯度下降的方向? - -**我:** 老大,你太厉害了,怎么什么都懂。我压根没准备这个项目。。。没想到会问,做过去好几个月了,加上当时一紧张就忘了,后来想起来大概是....。 - -**面试官:** 好我们问问基础知识吧,说说什么叫 xisuo? - -**我:**???xisuo,您说什么,不好意思我没听清。(这面试官有点口音。。。)就是 xisuo 啊!xisuo 你不知道吗?。。。尴尬了十几秒后我终于意识到,他在说死锁!!! - -**面试官:** 假如 A 账户给 B 账户转钱,会发生 xisuo 吗?能具体说说吗? - -**我:** 当时答的不好,后来发现面试官又是想问分布式,具体答案参考这个: - -**面试官:** 为什么不考研? - -**我:** 不喜欢学术氛围,巴拉巴拉。 - -**面试官:** 你有什么问题吗? - -**我:** 我还有下一面吗。。。面试官说让我等,一周内答复。 - ------- - -等了十天,一度以为我凉了,内推人说我流程到 HR 了,让我等着吧可能 HR 太忙了,3.28 号 HR 打来了电话,当时在教室,我直接飞了出去。 - -### HR 面 - -**面试官:** 你好啊,先自我介绍下吧 - -**我:** 巴拉巴拉....HR 面的技术面试和技术面的还是有所区别的! - -面试官人特别好,一听就是很会说话的小姐姐!说我这里给你悄悄透露下,你的评级是 A 哦! - -![panghu-knowledge](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3panghu-knowledge.jpg) - -接下来就是几个经典 HR 面挂人的问题,什么难给我来什么,我看别人的 HR 面怎么都是聊聊天。。。 - -**面试官:** 你为什么选择支付宝呢,你怎么看待支付宝? - -**我:** 我从个人情怀,公司理念,环境氛围,市场价值,趋势导向分析了一波(说白了就是疯狂夸支付宝,不过说实话我说的那些一点都没撒谎,阿里确实做到了。比如我举了个雷军和格力打赌 5 年 2000 亿销售额,大部分企业家关注的是利益,而马云更关注的是真的为人类为世界做一些事情,利益不是第一位的。) - -**面试官:** 明白了解,那你的优点我们都很明了了,你能说说你的缺点吗? - -> 缺点肯定不能是目标岗位需要的关键能力!!! -> -> 总之,记住一点,面试官问你这个问题的话,你可以说一些不影响你这个职位工作需要的一些缺点。比如你面试后端工程师,面试官问你的缺点是什么的话,你可以这样说:自己比较内向,平时不太爱与人交流,但是考虑到以后可能要和客户沟通,自己正在努力改。 - -**我:** 据说这是 HR 面最难的一个问题。。。我当时翻了好几天的知乎才找到一个合适的,也符合我的答案:我有时候会表现的不太自信,比如阿里的内推二月份就开始了,其实我当时已经复习了很久了,但是老是觉得自己还不行,不敢投简历,于是又把书看了一遍才投的,当时也是舍友怂恿一波才投的,面了之后发现其实自己也没有很差。(划重点,一定要把自己的缺点圆回来)。 - -**面试官:** HR 好像不太满意我的答案,继续问我还有缺点吗? - -**我:** 我说比较容易紧张吧,举了自己大一面实验室因为紧张没进去的例子,后来不断调整心态,现在已经好很多了。 - -接下来又是个好难的问题。 - -**面试官:** BAT 都给你 offer 了,你怎么选? - -其实我当时好想说,BT 是什么?不好意思我只知道阿里。 - -**我 :** 哈哈哈哈开玩笑,就说了阿里的文化,支付宝给我们带来很多便利,想加入支付宝为人类做贡献! - -最后 HR 问了我实习时间,现在大几之类的问题,说肯定会给我发 offer 的,让我等着就好了,希望过两天能收到好的结果。 - -![mengbi](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3mengbi.jpg) diff --git a/docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md b/docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md deleted file mode 100644 index 835b6a54fa0..00000000000 --- a/docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md +++ /dev/null @@ -1,89 +0,0 @@ -昨天我整理了公众号历史所有和面试相关的我觉得还不错的文章:[整理了一些有助于你拿Offer的文章]() 。今天分享一下最近逛Github看到了一些我觉得对于Java面试以及学习有帮助的仓库,这些仓库涉及Java核心知识点整理、Java常见面试题、算法、基础知识点比如网络和操作系统等等。 - -## 知识点相关 - -### 1.JavaGuide - -- Github地址: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- star: 64.0k -- 介绍: 【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。 - -### 2.CS-Notes - -- Github 地址: -- Star: 68.3k -- 介绍: 技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。 - -### 3. advanced-java - -- Github地址:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- star: 23.4k -- 介绍: 互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务等领域知识,后端同学必看,前端同学也可学习。 - -### 4.JCSprout - -- Github地址:[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) -- star: 21.2k -- 介绍: Java Core Sprout:处于萌芽阶段的 Java 核心知识库。 - -### 5.toBeTopJavaer - -- Github地址:[https://github.com/hollischuang/toBeTopJavaer](https://github.com/hollischuang/toBeTopJavaer) -- star: 4.0 k -- 介绍: Java工程师成神之路。 - -### 6.architect-awesome - -- Github地址:[https://github.com/xingshaocheng/architect-awesome](https://github.com/xingshaocheng/architect-awesome) -- star: 34.4 k -- 介绍:后端架构师技术图谱。 - -### 7.technology-talk - -- Github地址: [https://github.com/aalansehaiyang/technology-talk](https://github.com/aalansehaiyang/technology-talk) -- star: 6.1k -- 介绍: 汇总java生态圈常用技术框架、开源中间件,系统架构、项目管理、经典架构案例、数据库、常用三方库、线上运维等知识。 - -### 8.fullstack-tutorial - -- Github地址: [https://github.com/frank-lam/fullstack-tutorial](https://github.com/frank-lam/fullstack-tutorial) -- star: 4.0k -- 介绍: fullstack tutorial 2019,后台技术栈/架构师之路/全栈开发社区,春招/秋招/校招/面试。 - -### 9.3y - -- Github地址:[https://github.com/ZhongFuCheng3y/3y](https://github.com/ZhongFuCheng3y/3y) -- star: 1.9 k -- 介绍: Java 知识整合。 - -### 10.java-bible - -- Github地址:[https://github.com/biezhi/java-bible](https://github.com/biezhi/java-bible) -- star: 2.3k -- 介绍: 这里记录了一些技术摘要,部分文章来自网络,本项目的目的力求分享精品技术干货,以Java为主。 - -### 11.interviews - -- Github地址: [https://github.com/kdn251/interviews/blob/master/README-zh-cn.md](https://github.com/kdn251/interviews/blob/master/README-zh-cn.md) -- star: 35.3k -- 介绍: 软件工程技术面试个人指南(国外的一个项目,虽然有翻译版,但是不太推荐,因为很多内容并不适用于国内)。 - -## 算法相关 - -### 1.LeetCodeAnimation - -- Github 地址: -- Star: 33.4k -- 介绍: Demonstrate all the questions on LeetCode in the form of animation.(用动画的形式呈现解LeetCode题目的思路)。 - -### 2.awesome-java-leetcode - -- Github地址:[https://github.com/Blankj/awesome-java-leetcode](https://github.com/Blankj/awesome-java-leetcode) -- star: 6.1k -- 介绍: LeetCode 上 Facebook 的面试题目。 - -### 3.leetcode - -- Github地址:[https://github.com/azl397985856/leetcode](https://github.com/azl397985856/leetcode) -- star: 12.0k -- 介绍: LeetCode Solutions: A Record of My Problem Solving Journey.( leetcode题解,记录自己的leetcode解题之路。) \ No newline at end of file diff --git a/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md b/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md deleted file mode 100644 index 06d3ffc9434..00000000000 --- a/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md +++ /dev/null @@ -1,101 +0,0 @@ -身边的朋友或者公众号的粉丝很多人都向我询问过:“我是双非/三本/专科学校的,我有机会进入大厂吗?”、“非计算机专业的学生能学好吗?”、“如何学习 Java?”、“Java 学习该学哪些东西?”、“我该如何准备 Java 面试?”......这些方面的问题。我会根据自己的一点经验对大部分人关心的这些问题进行答疑解惑。现在又刚好赶上考研结束,这篇文章也算是给考研结束准备往 Java 后端方向发展的朋友们指明一条学习之路。道理懂了如果没有实际行动,那这篇文章对你或许没有任何意义。 - - - -- [Question1:我是双非/三本/专科学校的,我有机会进入大厂吗?](#question1我是双非三本专科学校的我有机会进入大厂吗) -- [Question2:非计算机专业的学生能学好 Java 后台吗?我能进大厂吗?](#question2非计算机专业的学生能学好-java-后台吗我能进大厂吗) -- [Question3: 我没有实习经历的话找工作是不是特别艰难?](#question3-我没有实习经历的话找工作是不是特别艰难) -- [Question4: 我该如何准备面试呢?面试的注意事项有哪些呢?](#question4-我该如何准备面试呢面试的注意事项有哪些呢) -- [Question5: 我该自学还是报培训班呢?](#question5-我该自学还是报培训班呢) -- [Question6: 没有项目经历/博客/Github 开源项目怎么办?](#question6-没有项目经历博客github-开源项目怎么办) -- [Question7: 大厂青睐什么样的人?](#question7-大厂青睐什么样的人) - - - -### Question1:我是双非/三本/专科学校的,我有机会进入大厂吗? - -我自己也是非 985 非 211 学校的,结合自己的经历以及一些朋友的经历,我觉得让我回答这个问题再好不过。 - -首先,我觉得学校歧视很正常,真的太正常了,如果要抱怨的话,你只能抱怨自己没有进入名校。但是,千万不要动不动说自己学校差,动不动拿自己学校当做自己进不了大厂的借口,学历只是筛选简历的很多标准中的一个而已,如果你够优秀,简历够丰富,你也一样可以和名校同学一起同台竞争。 - -企业 HR 肯定是更喜欢高学历的人,毕竟 985、211 优秀人才比例肯定比普通学校高很多,HR 团队肯定会优先在这些学校里选。这就好比相亲,你是愿意在很多优秀的人中选一个优秀的,还是愿意在很多普通的人中选一个优秀的呢? - -双非本科甚至是二本、三本甚至是专科的同学也有很多进入大厂的,不过比率相比于名校的低很多而已。从大厂招聘的结果上看,高学历人才的数量占据大头,那些成功进入 BAT、美团,京东,网易等大厂的双非本科甚至是二本、三本甚至是专科的同学往往是因为具备丰富的项目经历或者在某个含金量比较高的竞赛比如 ACM 中取得了不错的成绩。**一部分学历不突出但能力出众的面试者能够进入大厂并不是说明学历不重要,而是学历的软肋能够通过其他的优势来弥补。** 所以,如果你的学校不够好而你自己又想去大厂的话,建议你可以从这几点来做:**① 尽量在面试前最好有一个可以拿的出手的项目;② 有实习条件的话,尽早出去实习,实习经历也会是你的简历的一个亮点(有能力在大厂实习最佳!);③ 参加一些含金量比较高的比赛,拿不拿得到名次没关系,重在锻炼。** - -### Question2:非计算机专业的学生能学好 Java 后台吗?我能进大厂吗? - -当然可以!现在非科班的程序员很多,很大一部分原因是互联网行业的工资比较高。我们学校外面的培训班里面 90%都是非科班,我觉得他们很多人学的都还不错。另外,我的一个朋友本科是机械专业,大一开始自学安卓,技术贼溜,在我看来他比大部分本科是计算机的同学学的还要好。参考 Question1 的回答,即使你是非科班程序员,如果你想进入大厂的话,你也可以通过自己的其他优势来弥补。 - -我觉得我们不应该因为自己的专业给自己划界限或者贴标签,说实话,很多科班的同学可能并不如你,你以为科班的同学就会认真听讲吗?还不是几乎全靠自己课下自学!不过如果你是非科班的话,你想要学好,那么注定就要舍弃自己本专业的一些学习时间,这是无可厚非的。 - -建议非科班的同学,首先要打好计算机基础知识基础:① 计算机网络、② 操作系统、③ 数据机构与算法,我个人觉得这 3 个对你最重要。这些东西就像是内功,对你以后的长远发展非常有用。当然,如果你想要进大厂的话,这些知识也是一定会被问到的。另外,“一定学好数据结构与算法!一定学好数据结构与算法!一定学好数据结构与算法!”,重要的东西说 3 遍。 - -### Question3: 我没有实习经历的话找工作是不是特别艰难? - -没有实习经历没关系,只要你有拿得出手的项目或者大赛经历的话,你依然有可能拿到大厂的 offer 。笔主当时找工作的时候就没有实习经历以及大赛获奖经历,单纯就是凭借自己的项目经验撑起了整个面试。 - -如果你既没有实习经历,又没有拿得出手的项目或者大赛经历的话,我觉得在简历关,除非你有其他特别的亮点,不然,你应该就会被刷。 - -### Question4: 我该如何准备面试呢?面试的注意事项有哪些呢? - -下面是我总结的一些准备面试的 Tips 以及面试必备的注意事项: - -1. **准备一份自己的自我介绍,面试的时候根据面试对象适当进行修改**(突出重点,突出自己的优势在哪里,切忌流水账); -2. **注意随身带上自己的成绩单和简历复印件;** (有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。) -3. **如果需要笔试就提前刷一些笔试题,大部分在线笔试的类型是选择题+编程题,有的还会有简答题。**(平时空闲时间多的可以刷一下笔试题目(牛客网上有很多),但是不要只刷面试题,不动手 code,程序员不是为了考试而存在的。)另外,注意抓重点,因为题目太多了,但是有很多题目几乎次次遇到,像这样的题目一定要搞定。 -4. **提前准备技术面试。** 搞清楚自己面试中可能涉及哪些知识点、哪些知识点是重点。面试中哪些问题会被经常问到、自己该如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!) -5. **面试之前做好定向复习。** 也就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你要面试的公司的面经。 -6. **准备好自己的项目介绍。** 如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:① 对项目整体设计的一个感受(面试官可能会让你画系统的架构图);② 在这个项目中你负责了什么、做了什么、担任了什么角色;③ 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用;④ 项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用 redis 做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 -7. **面试之后记得复盘。** 面试遭遇失败是很正常的事情,所以善于总结自己的失败原因才是最重要的。如果失败,不要灰心;如果通过,切勿狂喜。 - -**一些还算不错的 Java 面试/学习相关的仓库,相信对大家准备面试一定有帮助:**[盘点一下 Github 上开源的 Java 面试/学习相关的仓库,看完弄懂薪资至少增加 10k](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484817&idx=1&sn=12f0c254a240c40c2ccab8314653216b&chksm=fd9853f0caefdae6d191e6bf085d44ab9c73f165e3323aa0362d830e420ccbfad93aa5901021&token=766994974&lang=zh_CN#rd) - -### Question5: 我该自学还是报培训班呢? - -我本人更加赞同自学(你要知道去了公司可没人手把手教你了,而且几乎所有的公司都对培训班出生的有偏见。为什么有偏见,你学个东西还要去培训班,说明什么,同等水平下,你的自学能力以及自律能力一定是比不上自学的人的)。但是如果,你连每天在寝室坚持学上 8 个小时以上都坚持不了,或者总是容易半途而废的话,我还是推荐你去培训班。观望身边同学去培训班的,大多是非计算机专业或者是没有自律能力以及自学能力非常差的人。 - -另外,如果自律能力不行,你也可以通过结伴学习、参加老师的项目等方式来督促自己学习。 - -总结:去不去培训班主要还是看自己,如果自己能坚持自学就自学,坚持不下来就去培训班。 - -### Question6: 没有项目经历/博客/Github 开源项目怎么办? - -从现在开始做! - -网上有很多非常不错的项目视频,你就跟着一步一步做,不光要做,还要改进,改善。另外,如果你的老师有相关 Java 后台项目的话,你也可以主动申请参与进来。 - -如果有自己的博客,也算是简历上的一个亮点。建议可以在掘金、Segmentfault、CSDN 等技术交流社区写博客,当然,你也可以自己搭建一个博客(采用 Hexo+Githu Pages 搭建非常简单)。写一些什么?学习笔记、实战内容、读书笔记等等都可以。 - -多用 Github,用好 Github,上传自己不错的项目,写好 readme 文档,在其他技术社区做好宣传。相信你也会收获一个不错的开源项目! - -### Question7: 大厂青睐什么样的人? - -**先从已经有两年左右开发经验的工程师角度来看:** 我们来看一下阿里官网支付宝 Java 高级开发工程师的招聘要求,从下面的招聘信息可以看出,除去 Java 基础/集合/多线程这些,这些能力格外重要: - -1. **底层知识比如 jvm** :不只是懂理论更会实操; -2. 面**向对象编程能力** :我理解这个不仅包括“面向对象编程”,还有 SOLID 软件设计原则,相关阅读:[《写了这么多年代码,你真的了解 SOLID 吗?》](https://insights.thoughtworks.cn/do-you-really-know-solid/)(我司大佬的一篇文章) -3. **框架能力** :不只是使用那么简单,更要搞懂原理和机制!搞懂原理和机制的基础是要学会看源码。 -4. **分布式系统开发能力** :缓存、消息队列等等都要掌握,关键是还要能使用这些技术解决实际问题而不是纸上谈兵。 -5. **不错的 sense** :喜欢和尝试新技术、追求编写优雅的代码等等。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/支付宝-JAVA开发工程师-专家.jpg) - -**再从应届生的角度来看:** 我们还是看阿里巴巴的官网相关应届生 Java 工程师招聘岗位的相关要求。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/应届生-alibaba-java.png) - -结合阿里、腾讯等大厂招聘官网对于 Java 后端方向/后端方向的应届实习生的要求下面几点也提升你的个人竞争力: - -1. 参加过竞赛( 含金量超高的是 ACM ); -2. 对数据结构与算法非常熟练; -3. 参与过实际项目(比如学校网站) -4. 熟悉 Python、Shell、Perl 其中一门脚本语言; -5. 熟悉如何优化 Java 代码、有写出质量更高的代码的意识; -6. 熟悉 SOA 分布式相关的知识尤其是理论知识; -7. 熟悉自己所用框架的底层知识比如 Spring; -8. 有高并发开发经验; -9. 有大数据开发经验等等。 - -从来到大学之后,我的好多阅历非常深的老师经常就会告诫我们:“ 一定要有一门自己的特长,不管是技术还好还是其他能力 ” 。我觉得这句话真的非常有道理! - -刚刚也提到了要有一门特长,所以在这里再强调一点:公司不需要你什么都会,但是在某一方面你一定要有过于常人的优点。换言之就是我们不需要去掌握每一门技术(你也没精力去掌握这么多技术),而是需要去深入研究某一门技术,对于其他技术我们可以简单了解一下。 diff --git a/docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md b/docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md deleted file mode 100644 index 5a091e1c2ef..00000000000 --- a/docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md +++ /dev/null @@ -1,151 +0,0 @@ -不论是笔试还是面试都是有章可循的,但是,一定要不要想着如何去应付面试,糊弄面试官,这样做终究是欺骗自己。这篇文章的目的也主要想让大家知道自己应该从哪些方向去准备面试,有哪些可以提高的方向。 - -网上已经有很多面经了,但是我认为网上的各种面经仅仅只能作为参考,你的实际面试与之还是有一些区别的。另外如果要在网上看别人的面经的话,建议即要看别人成功的案例也要适当看看别人失败的案例。**看面经没问题,不论是你要找工作还是平时学习,这都是一种比较好地检验自己水平的一种方式。但是,一定不要过分寄希望于各种面经,试着去提高自己的综合能力。** - -“ 80% 的 offer 掌握在 20% 的人手 ” 中这句话也不是不无道理的。决定你面试能否成功的因素中实力固然占有很大一部分比例,但是如果你的心态或者说运气不好的话,依然无法拿到满意的 offer。 - -运气暂且不谈,就拿心态来说,千万不要因为面试失败而气馁或者说怀疑自己的能力,面试失败之后多总结一下失败的原因,后面你就会发现自己会越来越强大。 - -另外,笔主只是在这里分享一下自己对于 “ 如何备战大厂面试 ” 的一个看法,以下大部分理论/言辞都经过过反复推敲验证,如果有不对的地方或者和你想法不同的地方,请您敬请雅正、不舍赐教。 - - - -- [1 如何获取大厂面试机会?](#1-如何获取大厂面试机会) -- [2 面试前的准备](#2--面试前的准备) - - [2.1 准备自己的自我介绍](#21-准备自己的自我介绍) - - [2.2 搞清楚技术面可能会问哪些方向的问题](#22-搞清楚技术面可能会问哪些方向的问题) - - [2.2 休闲着装即可](#22-休闲着装即可) - - [2.3 随身带上自己的成绩单和简历](#23-随身带上自己的成绩单和简历) - - [2.4 如果需要笔试就提前刷一些笔试题](#24-如果需要笔试就提前刷一些笔试题) - - [2.5 花时间一些逻辑题](#25-花时间一些逻辑题) - - [2.6 准备好自己的项目介绍](#26-准备好自己的项目介绍) - - [2.7 提前准备技术面试](#27-提前准备技术面试) - - [2.7 面试之前做好定向复习](#27-面试之前做好定向复习) -- [3 面试之后复盘](#3-面试之后复盘) -- [4 如何学习?学会各种框架有必要吗?](#4-如何学习学会各种框架有必要吗) - - [4.1 我该如何学习?](#41-我该如何学习) - - [4.2 学会各种框架有必要吗?](#42-学会各种框架有必要吗) - - - -## 1 如何获取大厂面试机会? - -**在讲如何获取大厂面试机会之前,先来给大家科普/对比一下两个校招非常常见的概念——春招和秋招。** - -1. **招聘人数** :秋招多于春招 ; -2. **招聘时间** : 秋招一般7月左右开始,大概一直持续到10月底。但是大厂(如BAT)都会早开始早结束,所以一定要把握好时间。春招最佳时间为3月,次佳时间为4月,进入5月基本就不会再有春招了(金三银四)。 -3. **应聘难度** :秋招略大于春招; -4. **招聘公司:** 秋招数量多,而春招数量较少,一般为秋招的补充。 - -**综上,一般来说,秋招的含金量明显是高于春招的。** - -**下面我就说一下我自己知道的一些方法,不过应该也涵盖了大部分获取面试机会的方法。** - -1. **关注大厂官网,随时投递简历(走流程的网申);** -2. **线下参加宣讲会,直接投递简历;** -3. **找到师兄师姐/认识的人,帮忙内推(能够让你避开网申简历筛选,笔试筛选,还是挺不错的,不过也还是需要你的简历够棒);** -4. **博客发文被看中/Github优秀开源项目作者,大厂内部人员邀请你面试;** -5. **求职类网站投递简历(不是太推荐,适合海投);** - - -除了这些方法,我也遇到过这样的经历:有些大公司的一些部门可能暂时没招够人,然后如果你的亲戚或者朋友刚好在这个公司,而你正好又在寻求offer,那么面试机会基本上是有了,而且这种面试的难度好像一般还普遍比其他正规面试低很多。 - -## 2 面试前的准备 - -### 2.1 准备自己的自我介绍 - -自我介绍一般是你和面试官的第一次面对面正式交流,换位思考一下,假如你是面试官的话,你想听到被你面试的人如何介绍自己呢?一定不是客套地说说自己喜欢编程、平时花了很多时间来学习、自己的兴趣爱好是打球吧? - -我觉得一个好的自我介绍应该包含这几点要素: - -1. 用简单的话说清楚自己主要的技术栈于擅长的领域; -2. 把重点放在自己在行的地方以及自己的优势之处; -3. 重点突出自己的能力比如自己的定位的bug的能力特别厉害; - -从社招和校招两个角度来举例子吧!我下面的两个例子仅供参考,自我介绍并不需要死记硬背,记住要说的要点,面试的时候根据公司的情况临场发挥也是没问题的。另外,网上一般建议的是准备好两份自我介绍:一份对hr说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节和项目经验。 - -**社招:** - -> 面试官,您好!我叫独秀儿。我目前有1年半的工作经验,熟练使用Spring、MyBatis等框架、了解 Java 底层原理比如JVM调优并且有着丰富的分布式开发经验。离开上一家公司是因为我想在技术上得到更多的锻炼。在上一个公司我参与了一个分布式电子交易系统的开发,负责搭建了整个项目的基础架构并且通过分库分表解决了原始数据库以及一些相关表过于庞大的问题,目前这个网站最高支持 10 万人同时访问。工作之余,我利用自己的业余时间写了一个简单的 RPC 框架,这个框架用到了Netty进行网络通信, 目前我已经将这个项目开源,在 Github 上收获了 2k的 Star! 说到业余爱好的话,我比较喜欢通过博客整理分享自己所学知识,现在已经是多个博客平台的认证作者。 生活中我是一个比较积极乐观的人,一般会通过运动打球的方式来放松。我一直都非常想加入贵公司,我觉得贵公司的文化和技术氛围我都非常喜欢,期待能与你共事! - -**校招:** - -> 面试官,您好!我叫秀儿。大学时间我主要利用课外时间学习了 Java 以及 Spring、MyBatis等框架 。在校期间参与过一个考试系统的开发,这个系统的主要用了 Spring、MyBatis 和 shiro 这三种框架。我在其中主要担任后端开发,主要负责了权限管理功能模块的搭建。另外,我在大学的时候参加过一次软件编程大赛,我和我的团队做的在线订餐系统成功获得了第二名的成绩。我还利用自己的业余时间写了一个简单的 RPC 框架,这个框架用到了Netty进行网络通信, 目前我已经将这个项目开源,在 Github 上收获了 2k的 Star! 说到业余爱好的话,我比较喜欢通过博客整理分享自己所学知识,现在已经是多个博客平台的认证作者。 生活中我是一个比较积极乐观的人,一般会通过运动打球的方式来放松。我一直都非常想加入贵公司,我觉得贵公司的文化和技术氛围我都非常喜欢,期待能与你共事! - -### 2.2 搞清楚技术面可能会问哪些方向的问题 - -你准备面试的话首先要搞清技术面可能会被问哪些方向的问题吧! - -**我直接用思维导图的形式展示出来吧!这样更加直观形象一点,细化到某个知识点的话这张图没有介绍到,留个悬念,下篇文章会详细介绍。** - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Xnip2020-03-11_20-24-32.jpg) - -**上面思维导图大概涵盖了技术面试可能会设计的技术,但是你不需要把上面的每一个知识点都搞得很熟悉,要分清主次,对于自己不熟悉的技术不要写在简历上,对于自己简单了解的技术不要说自己熟练掌握!** - -### 2.2 休闲着装即可 - -穿西装、打领带、小皮鞋?NO!NO!NO!这是互联网公司面试又不是去走红毯,所以你只需要穿的简单大方就好,不需要太正式。 - -### 2.3 随身带上自己的成绩单和简历 - -有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。 - -### 2.4 如果需要笔试就提前刷一些笔试题 - -平时空闲时间多的可以刷一下笔试题目(牛客网上有很多)。但是不要只刷面试题,不动手code,程序员不是为了考试而存在的。 - -### 2.5 花时间一些逻辑题 - -面试中发现有些公司都有逻辑题测试环节,并且都把逻辑笔试成绩作为很重要的一个参考。 - -### 2.6 准备好自己的项目介绍 - -如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑: - -1. 对项目整体设计的一个感受(面试官可能会让你画系统的架构图) -2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 - -### 2.7 提前准备技术面试 - -搞清楚自己面试中可能涉及哪些知识点、哪些知识点是重点。面试中哪些问题会被经常问到、自己该如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!) - -### 2.7 面试之前做好定向复习 - -所谓定向复习就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你要面试的公司的面经。 - -举个栗子:在我面试 ThoughtWorks 的前几天我就在网上找了一些关于 ThoughtWorks 的技术面的一些文章。然后知道了 ThoughtWorks 的技术面会让我们在之前做的作业的基础上增加一个或两个功能,所以我提前一天就把我之前做的程序重新重构了一下。然后在技术面的时候,简单的改了几行代码之后写个测试就完事了。如果没有提前准备,我觉得 20 分钟我很大几率会完不成这项任务。 - -## 3 面试之后复盘 - -如果失败,不要灰心;如果通过,切勿狂喜。面试和工作实际上是两回事,可能很多面试未通过的人,工作能力比你强的多,反之亦然。我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油! - -## 4 如何学习?学会各种框架有必要吗? - -### 4.1 我该如何学习? - -![如何学习?](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/如何学习.jpg) - -最最最关键也是对自己最最最重要的就是学习!看看别人分享的面经,看看我写的这篇文章估计你只需要10分钟不到。但这些东西终究是空洞的理论,最主要的还是自己平时的学习! - -如何去学呢?我觉得学习每个知识点可以考虑这样去入手: - -1. **官网(大概率是英文,不推荐初学者看)**。 -2. **书籍(知识更加系统完全,推荐)**。 -3. **视频(比较容易理解,推荐,特别是初学的时候。慕课网和哔哩哔哩上面有挺多学习视频可以看,只直接在上面搜索关键词就可以了)**。 -4. **网上博客(解决某一知识点的问题的时候可以看看)**。 - -这里给各位一个建议,**看视频的过程中最好跟着一起练,要做笔记!!!** - -**最好可以边看视频边找一本书籍看,看视频没弄懂的知识点一定要尽快解决,如何解决?** - -首先百度/Google,通过搜索引擎解决不了的话就找身边的朋友或者认识的一些人。 - -#### 4.2 学会各种框架有必要吗? - -**一定要学会分配自己时间,要学的东西很多,真的很多,搞清楚哪些东西是重点,哪些东西仅仅了解就够了。一定不要把精力都花在了学各种框架上,算法、数据结构还有计算机网络真的很重要!** - -另外,**学习的过程中有一个可以参考的文档很重要,非常有助于自己的学习**。我当初弄 JavaGuide: https://github.com/Snailclimb/JavaGuide 的很大一部分目的就是因为这个。**客观来说,相比于博客,JavaGuide 里面的内容因为更多人的参与变得更加准确和完善。** - -如果大家觉得这篇文章不错的话,欢迎给我来个三连(评论+转发+在看)!我会在下一篇文章中介绍如何从技术面时的角度准备面试? \ No newline at end of file diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\345\272\224\345\261\212\347\224\237\351\235\242\350\257\225\346\234\200\347\210\261\351\227\256\347\232\204\345\207\240\351\201\223Java\345\237\272\347\241\200\351\227\256\351\242\230.md" "b/docs/essential-content-for-interview/PreparingForInterview/\345\272\224\345\261\212\347\224\237\351\235\242\350\257\225\346\234\200\347\210\261\351\227\256\347\232\204\345\207\240\351\201\223Java\345\237\272\347\241\200\351\227\256\351\242\230.md" deleted file mode 100644 index 2e93113d0b8..00000000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\345\272\224\345\261\212\347\224\237\351\235\242\350\257\225\346\234\200\347\210\261\351\227\256\347\232\204\345\207\240\351\201\223Java\345\237\272\347\241\200\351\227\256\351\242\230.md" +++ /dev/null @@ -1,743 +0,0 @@ - - -- [一 为什么 Java 中只有值传递?](#一-为什么-java-中只有值传递) -- [二 ==与 equals(重要)](#二-与-equals重要) -- [三 hashCode 与 equals(重要)](#三-hashcode-与-equals重要) - - [3.1 hashCode()介绍](#31-hashcode介绍) - - [3.2 为什么要有 hashCode](#32-为什么要有-hashcode) - - [3.3 hashCode()与 equals()的相关规定](#33-hashcode与-equals的相关规定) - - [3.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?](#34-为什么两个对象有相同的-hashcode-值它们也不一定是相等的) -- [四 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的?](#四-string-和-stringbufferstringbuilder-的区别是什么string-为什么是不可变的) - - [String 为什么是不可变的吗?](#string-为什么是不可变的吗) - - [String 真的是不可变的吗?](#string-真的是不可变的吗) -- [五 什么是反射机制?反射机制的应用场景有哪些?](#五-什么是反射机制反射机制的应用场景有哪些) - - [5.1 反射机制介绍](#51-反射机制介绍) - - [5.2 静态编译和动态编译](#52-静态编译和动态编译) - - [5.3 反射机制优缺点](#53-反射机制优缺点) - - [5.4 反射的应用场景](#54-反射的应用场景) -- [六 什么是 JDK?什么是 JRE?什么是 JVM?三者之间的联系与区别](#六-什么是-jdk什么是-jre什么是-jvm三者之间的联系与区别) - - [6.1 JVM](#61-jvm) - - [6.2 JDK 和 JRE](#62-jdk-和-jre) -- [七 什么是字节码?采用字节码的最大好处是什么?](#七-什么是字节码采用字节码的最大好处是什么) -- [八 接口和抽象类的区别是什么?](#八-接口和抽象类的区别是什么) -- [九 重载和重写的区别](#九-重载和重写的区别) - - [重载](#重载) - - [重写](#重写) -- [十. Java 面向对象编程三大特性: 封装 继承 多态](#十-java-面向对象编程三大特性-封装-继承-多态) - - [封装](#封装) - - [继承](#继承) - - [多态](#多态) -- [十一. 什么是线程和进程?](#十一-什么是线程和进程) - - [11.1 何为进程?](#111-何为进程) - - [11.2 何为线程?](#112-何为线程) -- [十二. 请简要描述线程与进程的关系,区别及优缺点?](#十二-请简要描述线程与进程的关系区别及优缺点) - - [12.1 图解进程和线程的关系](#121-图解进程和线程的关系) - - [12.2 程序计数器为什么是私有的?](#122-程序计数器为什么是私有的) - - [12.3 虚拟机栈和本地方法栈为什么是私有的?](#123-虚拟机栈和本地方法栈为什么是私有的) - - [12.4 一句话简单了解堆和方法区](#124-一句话简单了解堆和方法区) -- [十三. 说说并发与并行的区别?](#十三-说说并发与并行的区别) -- [十四. 什么是上下文切换?](#十四-什么是上下文切换) -- [十五. 什么是线程死锁?如何避免死锁?](#十五-什么是线程死锁如何避免死锁) - - [15.1. 认识线程死锁](#151-认识线程死锁) - - [15.2 如何避免线程死锁?](#152-如何避免线程死锁) -- [十六. 说说 sleep() 方法和 wait() 方法区别和共同点?](#十六-说说-sleep-方法和-wait-方法区别和共同点) -- [十七. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#十七-为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法) -- [参考](#参考) - - - -## 一 为什么 Java 中只有值传递? - -首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。**按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。** 它用来描述各种程序设计语言(不只是 Java)中方法参数传递方式。 - -**Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。** - -**下面通过 3 个例子来给大家说明** - -> **example 1** - -```java -public static void main(String[] args) { - int num1 = 10; - int num2 = 20; - - swap(num1, num2); - - System.out.println("num1 = " + num1); - System.out.println("num2 = " + num2); -} - -public static void swap(int a, int b) { - int temp = a; - a = b; - b = temp; - - System.out.println("a = " + a); - System.out.println("b = " + b); -} -``` - -**结果:** - -``` -a = 20 -b = 10 -num1 = 10 -num2 = 20 -``` - -**解析:** - -![example 1 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/22191348.jpg) - -在 swap 方法中,a、b 的值进行交换,并不会影响到 num1、num2。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。 - -**通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example2.** - -> **example 2** - -```java - public static void main(String[] args) { - int[] arr = { 1, 2, 3, 4, 5 }; - System.out.println(arr[0]); - change(arr); - System.out.println(arr[0]); - } - - public static void change(int[] array) { - // 将数组的第一个元素变为0 - array[0] = 0; - } -``` - -**结果:** - -``` -1 -0 -``` - -**解析:** - -![example 2](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/3825204.jpg) - -array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的是同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。 - -**通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。** - -**很多程序设计语言(特别是,C++和 Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。** - -> **example 3** - -```java -public class Test { - - public static void main(String[] args) { - // TODO Auto-generated method stub - Student s1 = new Student("小张"); - Student s2 = new Student("小李"); - Test.swap(s1, s2); - System.out.println("s1:" + s1.getName()); - System.out.println("s2:" + s2.getName()); - } - - public static void swap(Student x, Student y) { - Student temp = x; - x = y; - y = temp; - System.out.println("x:" + x.getName()); - System.out.println("y:" + y.getName()); - } -} -``` - -**结果:** - -``` -x:小李 -y:小张 -s1:小张 -s2:小李 -``` - -**解析:** - -交换之前: - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/88729818.jpg) - -交换之后: - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/34384414.jpg) - -通过上面两张图可以很清晰的看出: **方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝** - -> **总结** - -Java 程序设计语言对对象采用的不是引用调用,实际上,对象引用是按 -值传递的。 - -下面再总结一下 Java 中方法参数的使用情况: - -- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。 -- 一个方法可以改变一个对象参数的状态。 -- 一个方法不能让对象参数引用一个新的对象。 - -**参考:** - -《Java 核心技术卷 Ⅰ》基础知识第十版第四章 4.5 小节 - -## 二 ==与 equals(重要) - -**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址) - -**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: - -- 情况 1:类没有覆盖 equals()方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况 2:类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。 - -**举个例子:** - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -**说明:** - -- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -## 三 hashCode 与 equals(重要) - -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?” - -### 3.1 hashCode()介绍 - -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。 - -```java - /** - * Returns a hash code value for the object. This method is - * supported for the benefit of hash tables such as those provided by - * {@link java.util.HashMap}. - *

- * As much as is reasonably practical, the hashCode method defined by - * class {@code Object} does return distinct integers for distinct - * objects. (This is typically implemented by converting the internal - * address of the object into an integer, but this implementation - * technique is not required by the - * Java™ programming language.) - * - * @return a hash code value for this object. - * @see java.lang.Object#equals(java.lang.Object) - * @see java.lang.System#identityHashCode - */ - public native int hashCode(); -``` - -散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -### 3.2 为什么要有 hashCode - -**我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** - -当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -### 3.3 hashCode()与 equals()的相关规定 - -1. 如果两个对象相等,则 hashcode 一定也是相同的 -2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true -3. 两个对象有相同的 hashcode 值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) - -### 3.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的? - -在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。 - -因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。 - -我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。 - -## 四 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的? - -**可变性** - -简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。 - -StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。 - -AbstractStringBuilder.java - -```java -abstract class AbstractStringBuilder implements Appendable, CharSequence { - char[] value; - int count; - AbstractStringBuilder() { - } - AbstractStringBuilder(int capacity) { - value = new char[capacity]; - } -``` - -**线程安全性** - -String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。 - -**性能** - -每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -**对于三者使用的总结:** - -1. 操作少量的数据: 适用 String -2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder -3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer - -#### String 为什么是不可变的吗? - -简单来说就是 String 类利用了 final 修饰的 char 类型数组存储字符,源码如下图所以: - -```java - /** The value is used for character storage. */ - private final char value[]; -``` - -#### String 真的是不可变的吗? - -我觉得如果别人问这个问题的话,回答不可变就可以了。 -下面只是给大家看两个有代表性的例子: - -**1) String 不可变但不代表引用不可以变** - -```java - String str = "Hello"; - str = str + " World"; - System.out.println("str=" + str); -``` - -结果: - -``` -str=Hello World -``` - -解析: - -实际上,原来 String 的内容是不变的,只是 str 由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。 - -**2) 通过反射是可以修改所谓的“不可变”对象** - -```java - // 创建字符串"Hello World", 并赋给引用s - String s = "Hello World"; - - System.out.println("s = " + s); // Hello World - - // 获取String类中的value字段 - Field valueFieldOfString = String.class.getDeclaredField("value"); - - // 改变value属性的访问权限 - valueFieldOfString.setAccessible(true); - - // 获取s对象上的value属性的值 - char[] value = (char[]) valueFieldOfString.get(s); - - // 改变value所引用的数组中的第5个字符 - value[5] = '_'; - - System.out.println("s = " + s); // Hello_World -``` - -结果: - -``` -s = Hello World -s = Hello_World -``` - -解析: - -用反射可以访问私有成员, 然后反射出 String 对象中的 value 属性, 进而改变通过获得的 value 引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。 - -## 五 什么是反射机制?反射机制的应用场景有哪些? - -### 5.1 反射机制介绍 - -JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。 - -### 5.2 静态编译和动态编译 - -- **静态编译:**在编译时确定类型,绑定对象 -- **动态编译:**运行时确定类型,绑定对象 - -### 5.3 反射机制优缺点 - -- **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。 -- **缺点:** 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。 - -### 5.4 反射的应用场景 - -**反射是框架设计的灵魂。** - -在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。 - -举例:① 我们在使用 JDBC 连接数据库时使用 `Class.forName()`通过反射加载数据库的驱动程序;②Spring 框架也用到很多反射机制,最经典的就是 xml 的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; -2)Java 类里面解析 xml 或 properties 里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的 Class 实例; 4)动态配置实例的属性 - -**推荐阅读:** - -- [Reflection:Java 反射机制的应用场景](https://segmentfault.com/a/1190000010162647?utm_source=tuicool&utm_medium=referral "Reflection:Java反射机制的应用场景") -- [Java 基础之—反射(非常重要)](https://blog.csdn.net/sinat_38259539/article/details/71799078 "Java基础之—反射(非常重要)") - -## 六 什么是 JDK?什么是 JRE?什么是 JVM?三者之间的联系与区别 - -### 6.1 JVM - -Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 - -**什么是字节码?采用字节码的好处是什么?** - -> 在 Java 中,JVM 可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 - -**Java 程序从源代码到运行一般有下面 3 步:** - -![Java程序运行过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png) - -我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。 - -> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。 - -**总结:** - -Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 - -### 6.2 JDK 和 JRE - -JDK 是 Java Development Kit,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。 - -JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。 - -如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 - -## 七 什么是字节码?采用字节码的最大好处是什么? - -**先看下 java 中的编译器和解释器:** - -Java 中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在 Java 中,这种供虚拟机理解的代码叫做`字节码`(即扩展名为`.class`的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java 源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了 Java 的编译与解释并存的特点。 - -Java 源代码---->编译器---->jvm 可执行的 Java 字节码(即虚拟指令)---->jvm---->jvm 中解释器----->机器可执行的二进制机器码---->程序运行。 - -**采用字节码的好处:** - -Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同的计算机上运行。 - -## 八 接口和抽象类的区别是什么? - -1. 接口的方法默认是 public,所有方法在接口中不能有实现,抽象类可以有非抽象的方法 -2. 接口中的实例变量默认是 final 类型的,而抽象类中则不一定 -3. 一个类可以实现多个接口,但最多只能实现一个抽象类 -4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定 -5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 - -注意:Java8 后接口可以有默认实现( default )。 - -## 九 重载和重写的区别 - -### 重载 - -发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 - -下面是《Java 核心技术》对重载这个概念的介绍: - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg) - -### 重写 - -重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。** - -## 十. Java 面向对象编程三大特性: 封装 继承 多态 - -### 封装 - -封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 - -### 继承 - -继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 - -**关于继承如下 3 点请记住:** - -1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 -2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -### 多态 - -所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 - -在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 - -## 十一. 什么是线程和进程? - -### 11.1 何为进程? - -进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 - -在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。 - -如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。 - -![进程示例图片-Windows](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/进程示例图片-Windows.png) - -### 11.2 何为线程? - -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。 - -```java -public class MultiThread { - public static void main(String[] args) { - // 获取 Java 线程管理 MXBean - ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); - // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 - ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); - // 遍历线程信息,仅打印线程 ID 和线程名称信息 - for (ThreadInfo threadInfo : threadInfos) { - System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); - } - } -} -``` - -上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可): - -``` -[5] Attach Listener //添加事件 -[4] Signal Dispatcher // 分发处理给 JVM 信号的线程 -[3] Finalizer //调用对象 finalize 方法的线程 -[2] Reference Handler //清除 reference 线程 -[1] main //main 线程,程序入口 -``` - -从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。 - -## 十二. 请简要描述线程与进程的关系,区别及优缺点? - -**从 JVM 角度说进程和线程之间的关系** - -### 12.1 图解进程和线程的关系 - -下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md "《可能是把 Java 内存区域讲的最清楚的一篇文章》") - -

- -
- -从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 - -**总结:** 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反 - -下面是该知识点的扩展内容! - -下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? - -### 12.2 程序计数器为什么是私有的? - -程序计数器主要有下面两个作用: - -1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 - -需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。 - -所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 - -### 12.3 虚拟机栈和本地方法栈为什么是私有的? - -- **虚拟机栈:** 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 -- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 - -所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 - -### 12.4 一句话简单了解堆和方法区 - -堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 - -## 十三. 说说并发与并行的区别? - -- **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行); -- **并行:** 单位时间内,多个任务同时执行。 - -## 十四. 什么是上下文切换? - -多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 - -概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 - -上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 - -Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 - -## 十五. 什么是线程死锁?如何避免死锁? - -### 15.1. 认识线程死锁 - -多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 - -如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 - -![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4%E6%AD%BB%E9%94%811.png) - -下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》): - -```java -public class DeadLockDemo { - private static Object resource1 = new Object();//资源 1 - private static Object resource2 = new Object();//资源 2 - - public static void main(String[] args) { - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 1").start(); - - new Thread(() -> { - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource1"); - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - } - } - }, "线程 2").start(); - } -} -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 2,5,main]get resource2 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 2,5,main]waiting get resource1 -``` - -线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。 - -学过操作系统的朋友都知道产生死锁必须具备以下四个条件: - -1. 互斥条件:该资源任意一个时刻只由一个线程占用。 -2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 -3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 -4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 - -### 15.2 如何避免线程死锁? - -我们只要破坏产生死锁的四个条件中的其中一个就可以了。 - -**破坏互斥条件** - -这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 - -**破坏请求与保持条件** - -一次性申请所有的资源。 - -**破坏不剥夺条件** - -占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 - -**破坏循环等待条件** - -靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 - -我们对线程 2 的代码修改成下面这样就不会产生死锁了。 - -```java - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 2").start(); -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 1,5,main]get resource2 -Thread[线程 2,5,main]get resource1 -Thread[线程 2,5,main]waiting get resource2 -Thread[线程 2,5,main]get resource2 - -Process finished with exit code 0 -``` - -我们分析一下上面的代码为什么避免了死锁的发生? - -线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 - -## 十六. 说说 sleep() 方法和 wait() 方法区别和共同点? - -- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。 -- 两者都可以暂停线程的执行。 -- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 -- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。 - -## 十七. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? - -这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! - -new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 - -**总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。** - -## 参考 - -- [https://blog.csdn.net/zhzhao999/article/details/53449504](https://blog.csdn.net/zhzhao999/article/details/53449504 "https://blog.csdn.net/zhzhao999/article/details/53449504") -- [https://www.cnblogs.com/skywang12345/p/3324958.html](https://www.cnblogs.com/skywang12345/p/3324958.html "https://www.cnblogs.com/skywang12345/p/3324958.html") -- [https://www.cnblogs.com/Eason-S/p/5524837.html](https://www.cnblogs.com/Eason-S/p/5524837.html "https://www.cnblogs.com/Eason-S/p/5524837.html") diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\347\250\213\345\272\217\345\221\230\347\232\204\347\256\200\345\216\206\344\271\213\351\201\223.md" "b/docs/essential-content-for-interview/PreparingForInterview/\347\250\213\345\272\217\345\221\230\347\232\204\347\256\200\345\216\206\344\271\213\351\201\223.md" deleted file mode 100644 index a746892f0d0..00000000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\347\250\213\345\272\217\345\221\230\347\232\204\347\256\200\345\216\206\344\271\213\351\201\223.md" +++ /dev/null @@ -1,122 +0,0 @@ - - -- [程序员简历就该这样写](#程序员简历就该这样写) - - [为什么说简历很重要?](#为什么说简历很重要) - - [先从面试前来说](#先从面试前来说) - - [再从面试中来说](#再从面试中来说) - - [下面这几点你必须知道](#下面这几点你必须知道) - - [必须了解的两大法则](#必须了解的两大法则) - - [STAR法则(Situation Task Action Result)](#star法则situation-task-action-result) - - [FAB 法则(Feature Advantage Benefit)](#fab-法则feature-advantage-benefit) - - [项目经历怎么写?](#项目经历怎么写) - - [专业技能该怎么写?](#专业技能该怎么写) - - [排版注意事项](#排版注意事项) - - [其他的一些小tips](#其他的一些小tips) - - [推荐的工具/网站](#推荐的工具网站) - - - -# 程序员简历就该这样写 - -本篇文章除了教大家用Markdown如何写一份程序员专属的简历,后面还会给大家推荐一些不错的用来写Markdown简历的软件或者网站,以及如何优雅的将Markdown格式转变为PDF格式或者其他格式。 - -推荐大家使用Markdown语法写简历,然后再将Markdown格式转换为PDF格式后进行简历投递。 - -如果你对Markdown语法不太了解的话,可以花半个小时简单看一下Markdown语法说明: http://www.markdown.cn 。 - -## 为什么说简历很重要? - -一份好的简历可以在整个申请面试以及面试过程中起到非常好的作用。 在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。为什么说简历很重要呢? - -### 先从面试前来说 - -- 假如你是网申,你的简历必然会经过HR的筛选,一张简历HR可能也就花费10秒钟看一下,然后HR就会决定你这一关是Fail还是Pass。 -- 假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。 - -另外,就算你通过了筛选,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。 - -所以,简历就像是我们的一个门面一样,它在很大程度上决定了你能否进入到下一轮的面试中。 - -### 再从面试中来说 - -我发现大家比较喜欢看面经 ,这点无可厚非,但是大部分面经都没告诉你很多问题都是在特定条件下才问的。举个简单的例子:一般情况下你的简历上注明你会的东西才会被问到(Java、数据结构、网络、算法这些基础是每个人必问的),比如写了你会 redis,那面试官就很大概率会问你 redis 的一些问题。比如:redis的常见数据类型及应用场景、redis是单线程为什么还这么快、 redis 和 memcached 的区别、redis 内存淘汰机制等等。 - -所以,首先,你要明确的一点是:**你不会的东西就不要写在简历上**。另外,**你要考虑你该如何才能让你的亮点在简历中凸显出来**,比如:你在某某项目做了什么事情解决了什么问题(只要有项目就一定有要解决的问题)、你的某一个项目里使用了什么技术后整体性能和并发量提升了很多等等。 - -面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。 - -## 下面这几点你必须知道 - -1. 大部分公司的HR都说我们不看重学历(骗你的!),但是如果你的学校不出众的话,很难在一堆简历中脱颖而出,除非你的简历上有特别的亮点,比如:某某大厂的实习经历、获得了某某大赛的奖等等。 -2. **大部分应届生找工作的硬伤是没有工作经验或实习经历,所以如果你是应届生就不要错过秋招和春招。一旦错过,你后面就极大可能会面临社招,这个时候没有工作经验的你可能就会面临各种碰壁,导致找不到一个好的工作** -3. **写在简历上的东西一定要慎重,这是面试官大量提问的地方;** -4. **将自己的项目经历完美的展示出来非常重要。** - -## 必须了解的两大法则 - -### STAR法则(Situation Task Action Result) - -- **Situation:** 事情是在什么情况下发生; -- **Task::** 你是如何明确你的任务的; -- **Action:** 针对这样的情况分析,你采用了什么行动方式; -- **Result:** 结果怎样,在这样的情况下你学习到了什么。 - -简而言之,STAR法则,就是一种讲述自己故事的方式,或者说,是一个清晰、条理的作文模板。不管是什么,合理熟练运用此法则,可以轻松的对面试官描述事物的逻辑方式,表现出自己分析阐述问题的清晰性、条理性和逻辑性。 - -### FAB 法则(Feature Advantage Benefit) - -- **Feature:** 是什么; -- **Advantage:** 比别人好在哪些地方; -- **Benefit:** 如果雇佣你,招聘方会得到什么好处。 - -简单来说,这个法则主要是让你的面试官知道你的优势、招了你之后对公司有什么帮助。 - -## 项目经历怎么写? - -简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑从如下几点来写: - -1. 对项目整体设计的一个感受 -2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 - -## 专业技能该怎么写? - -先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。比如你可以这样写(下面这部分内容摘自我的简历,大家可以根据自己的情况做一些修改和完善): - -- 计算机网络、数据结构、算法、操作系统等课内基础知识:掌握 -- Java 基础知识:掌握 -- JVM 虚拟机(Java内存区域、虚拟机垃圾算法、虚拟垃圾收集器、JVM内存管理):掌握 -- 高并发、高可用、高性能系统开发:掌握 -- Struts2、Spring、Hibernate、Ajax、Mybatis、JQuery :掌握 -- SSH 整合、SSM 整合、 SOA 架构:掌握 -- Dubbo: 掌握 -- Zookeeper: 掌握 -- 常见消息队列: 掌握 -- Linux:掌握 -- MySQL常见优化手段:掌握 -- Spring Boot +Spring Cloud +Docker:了解 -- Hadoop 生态相关技术中的 HDFS、Storm、MapReduce、Hive、Hbase :了解 -- Python 基础、一些常见第三方库比如OpenCV、wxpy、wordcloud、matplotlib:熟悉 - -## 排版注意事项 - -1. 尽量简洁,不要太花里胡哨; -2. 一些技术名词不要弄错了大小写比如MySQL不要写成mysql,Java不要写成java。这个在我看来还是比较忌讳的,所以一定要注意这个细节; -3. 中文和数字英文之间加上空格的话看起来会舒服一点; - -## 其他的一些小tips - -1. 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。 -2. 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。 -3. 如果自己的Github比较活跃的话,写上去也会为你加分很多。 -4. 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容 -5. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。 -6. 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。 -7. 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。 - -## 推荐的工具/网站 - -- 冷熊简历(MarkDown在线简历工具,可在线预览、编辑和生成PDF): -- Typora+[Java程序员简历模板](https://github.com/geekcompany/ResumeSample/blob/master/java.md) -- Guide哥自己写的Markdown模板:[https://github.com/Snailclimb/typora-markdown-resume](https://github.com/Snailclimb/typora-markdown-resume) diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" "b/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" deleted file mode 100644 index 869416ce018..00000000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" +++ /dev/null @@ -1,925 +0,0 @@ - - -- [一 基础篇](#一-基础篇) - - [1. `System.out.println(3|9)`输出什么?](#1-systemoutprintln39输出什么) - - [2. 说一下转发(Forward)和重定向(Redirect)的区别](#2-说一下转发forward和重定向redirect的区别) - - [3. 在浏览器中输入 url 地址到显示主页的过程,整个过程会使用哪些协议](#3-在浏览器中输入-url-地址到显示主页的过程整个过程会使用哪些协议) - - [4. TCP 三次握手和四次挥手](#4-tcp-三次握手和四次挥手) - - [为什么要三次握手](#为什么要三次握手) - - [为什么要传回 SYN](#为什么要传回-syn) - - [传了 SYN,为啥还要传 ACK](#传了-syn为啥还要传-ack) - - [为什么要四次挥手](#为什么要四次挥手) - - [5. IP 地址与 MAC 地址的区别](#5-ip-地址与-mac-地址的区别) - - [6. HTTP 请求,响应报文格式](#6-http-请求响应报文格式) - - [7. 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注意事项?Mysql 索引主要使用的两种数据结构?什么是覆盖索引?](#7-为什么要使用索引索引这么多优点为什么不对表中的每一个列创建一个索引呢索引是如何提高查询速度的说一下使用索引的注意事项mysql-索引主要使用的两种数据结构什么是覆盖索引) - - [8. 进程与线程的区别是什么?进程间的几种通信方式说一下?线程间的几种通信方式知道不?](#8-进程与线程的区别是什么进程间的几种通信方式说一下线程间的几种通信方式知道不) - - [9. 为什么要用单例模式?手写几种线程安全的单例模式?](#9-为什么要用单例模式手写几种线程安全的单例模式) - - [10. 简单介绍一下 bean;知道 Spring 的 bean 的作用域与生命周期吗?](#10-简单介绍一下-bean知道-spring-的-bean-的作用域与生命周期吗) - - [11. Spring 中的事务传播行为了解吗?TransactionDefinition 接口中哪五个表示隔离级别的常量?](#11-spring-中的事务传播行为了解吗transactiondefinition-接口中哪五个表示隔离级别的常量) - - [事务传播行为](#事务传播行为) - - [隔离级别](#隔离级别) - - [12. SpringMVC 原理了解吗?](#12-springmvc-原理了解吗) - - [13. Spring AOP IOC 实现原理](#13-spring-aop-ioc-实现原理) -- [二 进阶篇](#二-进阶篇) - - [1 消息队列 MQ 的套路](#1-消息队列-mq-的套路) - - [1.1 介绍一下消息队列 MQ 的应用场景/使用消息队列的好处](#11-介绍一下消息队列-mq-的应用场景使用消息队列的好处) - - [1)通过异步处理提高系统性能](#1通过异步处理提高系统性能) - - [2)降低系统耦合性](#2降低系统耦合性) - - [1.2 那么使用消息队列会带来什么问题?考虑过这些问题吗?](#12-那么使用消息队列会带来什么问题考虑过这些问题吗) - - [1.3 介绍一下你知道哪几种消息队列,该如何选择呢?](#13-介绍一下你知道哪几种消息队列该如何选择呢) - - [1.4 关于消息队列其他一些常见的问题展望](#14-关于消息队列其他一些常见的问题展望) - - [2 谈谈 InnoDB 和 MyIsam 两者的区别](#2-谈谈-innodb-和-myisam-两者的区别) - - [2.1 两者的对比](#21-两者的对比) - - [2.2 关于两者的总结](#22-关于两者的总结) - - [3 聊聊 Java 中的集合吧!](#3-聊聊-java-中的集合吧) - - [3.1 Arraylist 与 LinkedList 有什么不同?(注意加上从数据结构分析的内容)](#31-arraylist-与-linkedlist-有什么不同注意加上从数据结构分析的内容) - - [3.2 HashMap 的底层实现](#32-hashmap-的底层实现) - - [1)JDK1.8 之前](#1jdk18-之前) - - [2)JDK1.8 之后](#2jdk18-之后) - - [3.3 既然谈到了红黑树,你给我手绘一个出来吧,然后简单讲一下自己对于红黑树的理解](#33-既然谈到了红黑树你给我手绘一个出来吧然后简单讲一下自己对于红黑树的理解) - - [3.4 红黑树这么优秀,为何不直接使用红黑树得了?](#34-红黑树这么优秀为何不直接使用红黑树得了) - - [3.5 HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别](#35-hashmap-和-hashtable-的区别hashset-和-hashmap-区别) -- [三 终结篇](#三-终结篇) - - [1. Object 类有哪些方法?](#1-object-类有哪些方法) - - [1.1 Object 类的常见方法总结](#11-object-类的常见方法总结) - - [1.2 hashCode 与 equals](#12-hashcode-与-equals) - - [1.2.1 hashCode()介绍](#121-hashcode介绍) - - [1.2.2 为什么要有 hashCode](#122-为什么要有-hashcode) - - [1.2.3 hashCode()与 equals()的相关规定](#123-hashcode与-equals的相关规定) - - [1.2.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?](#124-为什么两个对象有相同的-hashcode-值它们也不一定是相等的) - - [1.3 ==与 equals](#13-与-equals) - - [2 ConcurrentHashMap 相关问题](#2-concurrenthashmap-相关问题) - - [2.1 ConcurrentHashMap 和 Hashtable 的区别](#21-concurrenthashmap-和-hashtable-的区别) - - [2.2 ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](#22-concurrenthashmap-线程安全的具体实现方式底层具体实现) - - [JDK1.7(上面有示意图)](#jdk17上面有示意图) - - [JDK1.8(上面有示意图)](#jdk18上面有示意图) - - [3 谈谈 synchronized 和 ReentrantLock 的区别](#3-谈谈-synchronized-和-reentrantlock-的区别) - - [4 线程池了解吗?](#4-线程池了解吗) - - [4.1 为什么要用线程池?](#41-为什么要用线程池) - - [4.2 Java 提供了哪几种线程池?他们各自的使用场景是什么?](#42-java-提供了哪几种线程池他们各自的使用场景是什么) - - [Java 主要提供了下面 4 种线程池](#java-主要提供了下面-4-种线程池) - - [各种线程池的适用场景介绍](#各种线程池的适用场景介绍) - - [4.3 创建的线程池的方式](#43-创建的线程池的方式) - - [5 Nginx](#5-nginx) - - [5.1 简单介绍一下 Nginx](#51-简单介绍一下-nginx) - - [反向代理](#反向代理) - - [负载均衡](#负载均衡) - - [动静分离](#动静分离) - - [5.2 为什么要用 Nginx?](#52-为什么要用-nginx) - - [5.3 Nginx 的四个主要组成部分了解吗?](#53-nginx-的四个主要组成部分了解吗) - - - -这些问题是 2018 年去美团面试的同学被问到的一些常见的问题,希望对你有帮助! - -# 一 基础篇 - -## 1. `System.out.println(3|9)`输出什么? - -正确答案:11。 - -**考察知识点:&和&&;|和||** - -**&和&&:** - -共同点:两者都可做逻辑运算符。它们都表示运算符的两边都是 true 时,结果为 true; - -不同点: &也是位运算符。& 表示在运算时两边都会计算,然后再判断;&&表示先运算符号左边的东西,然后判断是否为 true,是 true 就继续运算右边的然后判断并输出,是 false 就停下来直接输出不会再运行后面的东西。 - -**|和||:** - -共同点:两者都可做逻辑运算符。它们都表示运算符的两边任意一边为 true,结果为 true,两边都不是 true,结果就为 false; - -不同点:|也是位运算符。| 表示两边都会运算,然后再判断结果;|| 表示先运算符号左边的东西,然后判断是否为 true,是 true 就停下来直接输出不会再运行后面的东西,是 false 就继续运算右边的然后判断并输出。 - -**回到本题:** - -3 | 9=0011(二进制) | 1001(二进制)=1011(二进制)=11(十进制) - -## 2. 说一下转发(Forward)和重定向(Redirect)的区别 - -**转发是服务器行为,重定向是客户端行为。** - -**转发(Forword)** 通过 RequestDispatcher 对象的`forward(HttpServletRequest request,HttpServletResponse response)`方法实现的。`RequestDispatcher` 可以通过`HttpServletRequest` 的 `getRequestDispatcher()`方法获得。例如下面的代码就是跳转到 login_success.jsp 页面。 - -```java -request.getRequestDispatcher("login_success.jsp").forward(request, response); -``` - -**重定向(Redirect)** 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过 HttpServletRequestResponse 的 setStatus(int status)方法设置状态码。如果服务器返回 301 或者 302,则浏览器会到新的网址重新请求该资源。 - -1. **从地址栏显示来说**:forward 是服务器请求资源,服务器直接访问目标地址的 URL,把那个 URL 的响应内容读取过来,然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址。redirect 是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址。所以地址栏显示的是新的 URL。 -2. **从数据共享来说**:forward:转发页面和转发到的页面可以共享 request 里面的数据。redirect:不能共享数据。 -3. **从运用地方来说**:forward:一般用于用户登陆的时候,根据角色转发到相应的模块。redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等。 -4. **从效率来说**:forward:高。redirect:低。 - -## 3. 在浏览器中输入 url 地址到显示主页的过程,整个过程会使用哪些协议 - -图片来源:《图解 HTTP》: - -![各种网络请求用到的协议](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/各种网络请求用到的协议.jpg) - -总体来说分为以下几个过程: - -1. DNS 解析 -2. TCP 连接 -3. 发送 HTTP 请求 -4. 服务器处理请求并返回 HTTP 报文 -5. 浏览器解析渲染页面 -6. 连接结束 - -具体可以参考下面这篇文章: - -- [https://segmentfault.com/a/1190000006879700](https://segmentfault.com/a/1190000006879700 "https://segmentfault.com/a/1190000006879700") - -> 修正 [issue-568](https://github.com/Snailclimb/JavaGuide/issues/568 "issue-568"):上图中 IP 数据包在路由器之间使用的协议为 OPSF 协议错误,应该为 OSPF 协议 。 -> -> IP 数据包在路由器之间传播大致分为 IGP 和 BGP 协议,而 IGP 目前主流为 OSPF 协议,思科,华为和 H3C 等主流厂商都有各自实现并使用;BGP 协议为不同 AS(自治系统号)间路由传输,也分为 I-BGP 和 E-BGP,详细资料请查看《TCP/IP 卷一》 - -## 4. TCP 三次握手和四次挥手 - -为了准确无误地把数据送达目标处,TCP 协议采用了三次握手策略。 - -**漫画图解:** - -图片来源:《图解 HTTP》 - - -**简单示意图:** - - -- 客户端–发送带有 SYN 标志的数据包–一次握手–服务端 -- 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端 -- 客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端 - -#### 为什么要三次握手 - -**三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。** - -第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常。 - -第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发送正常 - -第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常 - -所以三次握手就能确认双发收发功能都正常,缺一不可。 - -#### 为什么要传回 SYN - -接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。 - -> SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务器之间传递。 - -#### 传了 SYN,为啥还要传 ACK - -双方通信无误必须是两者互相发送信息都无误。传了 SYN,证明发送方(主动关闭方)到接收方(被动关闭方)的通道没有问题,但是接收方到发送方的通道还需要 ACK 信号来进行验证。 - -断开一个 TCP 连接则需要“四次挥手”: - -- 客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送 -- 服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加 1 。和 SYN 一样,一个 FIN 将占用一个序号 -- 服务器-关闭与客户端的连接,发送一个 FIN 给客户端 -- 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加 1 - -#### 为什么要四次挥手 - -任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。 - -举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B 回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。 - -上面讲的比较概括,推荐一篇讲的比较细致的文章:[https://blog.csdn.net/qzcsu/article/details/72861891](https://blog.csdn.net/qzcsu/article/details/72861891 "https://blog.csdn.net/qzcsu/article/details/72861891") - -## 5. IP 地址与 MAC 地址的区别 - -参考:[https://blog.csdn.net/guoweimelon/article/details/50858597](https://blog.csdn.net/guoweimelon/article/details/50858597 "https://blog.csdn.net/guoweimelon/article/details/50858597") - -IP 地址是指互联网协议地址(Internet Protocol Address)IP Address 的缩写。IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 - -MAC 地址又称为物理地址、硬件地址,用来定义网络设备的位置。网卡的物理地址通常是由网卡生产厂家写入网卡的,具有全球唯一性。MAC 地址用于在网络中唯一标示一个网卡,一台电脑会有一或多个网卡,每个网卡都需要有一个唯一的 MAC 地址。 - -## 6. HTTP 请求,响应报文格式 - -HTTP 请求报文主要由请求行、请求头部、请求正文 3 部分组成 - -HTTP 响应报文主要由状态行、响应头部、响应正文 3 部分组成 - -详细内容可以参考:[https://blog.csdn.net/a19881029/article/details/14002273](https://blog.csdn.net/a19881029/article/details/14002273 "https://blog.csdn.net/a19881029/article/details/14002273") - -## 7. 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注意事项?Mysql 索引主要使用的两种数据结构?什么是覆盖索引? - -**为什么要使用索引?** - -1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 -2. 可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。 -3. 帮助服务器避免排序和临时表 -4. 将随机 IO 变为顺序 IO -5. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。 - -**索引这么多优点,为什么不对表中的每一个列创建一个索引呢?** - -1. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。 -2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。 -3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。 - -**索引是如何提高查询速度的?** - -将无序的数据变成相对有序的数据(就像查目录一样) - -**说一下使用索引的注意事项** - -1. 避免 where 子句中对字段施加函数,这会造成无法命中索引。 -2. 在使用 InnoDB 时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。 -3. 将打算加索引的列建议设置为 NOT NULL ,因为 NULL 比空字符串需要更多的存储空间(不仅仅是索引列,普通的列如果业务允许都建议设置为 NOT NULL) -4. 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用 -5. 在使用 limit offset 查询缓慢时,可以借助索引来提高性能 - -**Mysql 索引主要使用的哪两种数据结构?** - -- 哈希索引:对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择 BTree 索引。 -- BTree 索引:Mysql 的 BTree 索引使用的是 B 树中的 B+Tree。但对于主要的两种存储引擎(MyISAM 和 InnoDB)的实现方式是不同的。 - -更多关于索引的内容可以查看我的这篇文章:[【思维导图-索引篇】搞定数据库索引就是这么简单](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484486&idx=1&sn=215450f11e042bca8a58eac9f4a97686&chksm=fd985227caefdb3117b8375f150676f5824aa20d1ebfdbcfb93ff06e23e26efbafae6cf6b48e&token=1990180468&lang=zh_CN#rd) - -**什么是覆盖索引?** - -如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称 -之为“覆盖索引”。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! - -## 8. 进程与线程的区别是什么?进程间的几种通信方式说一下?线程间的几种通信方式知道不? - -**进程与线程的区别是什么?** - -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。另外,也正是因为共享资源,所以线程中执行时一般都要进行同步和互斥。总的来说,进程和线程的主要差别在于它们是不同的操作系统资源管理方式。 - -**进程间的几种通信方式说一下?** - -1. **管道(pipe)**:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。管道分为 pipe(无名管道)和 fifo(命名管道)两种,有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信。 -2. **信号量(semophore)**:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 -3. **消息队列(message queue)**:消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。 -4. **信号(signal)**:信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。 -5. **共享内存(shared memory)**:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问,共享内存是最快的 IPC 方式,它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。 -6. **套接字(socket)**:socket,即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。 - -**线程间的几种通信方式知道不?** - -1、锁机制 - -- 互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。 -- 读写锁:允许多个线程同时读共享数据,而对写操作互斥。 -- 条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。 - -2、信号量机制:包括无名线程信号量与有名线程信号量 - -3、信号机制:类似于进程间的信号处理。 - -线程间通信的主要目的是用于线程同步,所以线程没有象进程通信中用于数据交换的通信机制。 - -## 9. 为什么要用单例模式?手写几种线程安全的单例模式? - -**简单来说使用单例模式可以带来下面几个好处:** - -- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销; -- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。 - -**懒汉式(双重检查加锁版本)** - -```java -public class Singleton { - - //volatile保证,当uniqueInstance变量被初始化成Singleton实例时,多个线程可以正确处理uniqueInstance变量 - private volatile static Singleton uniqueInstance; - private Singleton() { - } - public static Singleton getInstance() { - //检查实例,如果不存在,就进入同步代码块 - if (uniqueInstance == null) { - //只有第一次才彻底执行这里的代码 - synchronized(Singleton.class) { - //进入同步代码块后,再检查一次,如果仍是null,才创建实例 - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` - -**静态内部类方式** - -静态内部实现的单例是懒加载的且线程安全。 - -只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance(只有第一次使用这个单例的实例的时候才加载,同时不会有线程安全问题)。 - -```java -public class Singleton { - private static class SingletonHolder { - private static final Singleton INSTANCE = new Singleton(); - } - private Singleton (){} - public static final Singleton getInstance() { - return SingletonHolder.INSTANCE; - } -} -``` - -## 10. 简单介绍一下 bean;知道 Spring 的 bean 的作用域与生命周期吗? - -在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 bean。简单地讲,bean 就是由 IOC 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。而 bean 的定义以及 bean 相互间的依赖关系将通过配置元数据来描述。 - -Spring 中的 bean 默认都是单例的,这些单例 Bean 在多线程程序下如何保证线程安全呢? 例如对于 Web 应用来说,Web 容器对于每个用户请求都创建一个单独的 Sevlet 线程来处理请求,引入 Spring 框架之后,每个 Action 都是单例的,那么对于 Spring 托管的单例 Service Bean,如何保证其安全呢? Spring 的单例是基于 BeanFactory 也就是 Spring 容器的,单例 Bean 在此容器内只有一个,Java 的单例是基于 JVM,每个 JVM 内只有一个实例。 - -![pring的bean的作用域](https://user-gold-cdn.xitu.io/2018/11/10/166fd45773d5dd2e?w=563&h=299&f=webp&s=27930) - -Spring 的 bean 的生命周期以及更多内容可以查看:[一文轻松搞懂 Spring 中 bean 的作用域与生命周期](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484400&idx=2&sn=7201eb365102fce017f89cb3527fb0bc&chksm=fd985591caefdc872a2fac897288119f94c345e4e12150774f960bf5f816b79e4b9b46be3d7f&token=1990180468&lang=zh_CN#rd) - -## 11. Spring 中的事务传播行为了解吗?TransactionDefinition 接口中哪五个表示隔离级别的常量? - -#### 事务传播行为 - -事务传播行为(为了解决业务层方法之间互相调用的事务问题): -当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。在 TransactionDefinition 定义中包括了如下几个表示传播行为的常量: - -**支持当前事务的情况:** - -- TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 -- TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 -- TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性) - -**不支持当前事务的情况:** - -- TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 -- TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 -- TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 - -**其他情况:** - -- TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。 - -#### 隔离级别 - -TransactionDefinition 接口中定义了五个表示隔离级别的常量: - -- **TransactionDefinition.ISOLATION_DEFAULT:** 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别. -- **TransactionDefinition.ISOLATION_READ_UNCOMMITTED:** 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 -- **TransactionDefinition.ISOLATION_READ_COMMITTED:** 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 -- **TransactionDefinition.ISOLATION_REPEATABLE_READ:** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 -- **TransactionDefinition.ISOLATION_SERIALIZABLE:** 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 - -## 12. SpringMVC 原理了解吗? - -![SpringMVC 原理](https://user-gold-cdn.xitu.io/2018/11/10/166fd45787394192?w=1015&h=466&f=webp&s=35352) - -客户端发送请求-> 前端控制器 DispatcherServlet 接受客户端请求 -> 找到处理器映射 HandlerMapping 解析请求对应的 Handler-> HandlerAdapter 会根据 Handler 来调用真正的处理器处理请求,并处理相应的业务逻辑 -> 处理器返回一个模型视图 ModelAndView -> 视图解析器进行解析 -> 返回一个视图对象->前端控制器 DispatcherServlet 渲染数据(Model)->将得到视图对象返回给用户 - -关于 SpringMVC 原理更多内容可以查看我的这篇文章:[SpringMVC 工作原理详解](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484496&idx=1&sn=5472ffa687fe4a05f8900d8ee6726de4&chksm=fd985231caefdb27fc75b44ecf76b6f43e4617e0b01b3c040f8b8fab32e51dfa5118eed1d6ad&token=1990180468&lang=zh_CN#rd) - -## 13. Spring AOP IOC 实现原理 - -过了秋招挺长一段时间了,说实话我自己也忘了如何简要概括 Spring AOP IOC 实现原理,就在网上找了一个较为简洁的答案,下面分享给各位。 - -**IOC:** 控制反转也叫依赖注入。IOC 利用 java 反射机制,AOP 利用代理模式。IOC 概念看似很抽象,但是很容易理解。说简单点就是将对象交给容器管理,你只需要在 spring 配置文件中配置对应的 bean 以及设置相关的属性,让 spring 容器来生成类的实例对象以及管理对象。在 spring 容器启动的时候,spring 会把你在配置文件中配置的 bean 都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些 bean 分配给你需要调用这些 bean 的类。 - -**AOP:** 面向切面编程。(Aspect-Oriented Programming) 。AOP 可以说是对 OOP 的补充和完善。OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。实现 AOP 的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码,属于静态代理。 - -# 二 进阶篇 - -## 1 消息队列 MQ 的套路 - -消息队列/消息中间件应该是 Java 程序员必备的一个技能了,如果你之前没接触过消息队列的话,建议先去百度一下某某消息队列入门,然后花 2 个小时就差不多可以学会任何一种消息队列的使用了。如果说仅仅学会使用是万万不够的,在实际生产环境还要考虑消息丢失等等情况。关于消息队列面试相关的问题,推荐大家也可以看一下视频《Java 工程师面试突击第 1 季-中华石杉老师》,如果大家没有资源的话,可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可! - -### 1.1 介绍一下消息队列 MQ 的应用场景/使用消息队列的好处 - -面试官一般会先问你这个问题,预热一下,看你知道消息队列不,一般在第一面的时候面试官可能只会问消息队列 MQ 的应用场景/使用消息队列的好处、使用消息队列会带来什么问题、消息队列的技术选型这几个问题,不会太深究下去,在后面的第二轮/第三轮技术面试中可能会深入问一下。 - -**《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。** - -#### 1)通过异步处理提高系统性能 - -![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/通过异步处理提高系统性能.jpg) -如上图,**在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。** - -通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: -![合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击.jpg) -因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 - -#### 2)降低系统耦合性 - -我们知道模块分布式部署以后聚合方式通常有两种:1.**分布式消息队列**和 2.**分布式服务**。 - -> **先来简单说一下分布式服务:** - -目前使用比较多的用来构建**SOA(Service Oriented Architecture 面向服务体系结构)**的**分布式服务框架**是阿里巴巴开源的**Dubbo**。如果想深入了解 Dubbo 的可以看我写的关于 Dubbo 的这一篇文章:**《高性能优秀的服务框架-dubbo 介绍》**:[https://juejin.im/post/5acadeb1f265da2375072f9c](https://juejin.im/post/5acadeb1f265da2375072f9c "https://juejin.im/post/5acadeb1f265da2375072f9c") - -> **再来谈我们的分布式消息队列:** - -我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。 - -我们最常见的**事件驱动架构**类似生产者消费者模式,在大型网站中通常用利用消息队列实现事件驱动结构。如下图所示: - -![利用消息队列实现事件驱动结构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/利用消息队列实现事件驱动结构.jpg) -**消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 - -消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 - -**另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。** - -**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的,**比如在我们的 ActiveMQ 消息队列中还有点对点工作模式**,具体的会在后面的文章给大家详细介绍,这一篇文章主要还是让大家对消息队列有一个更透彻的了解。 - -> 这个问题一般会在上一个问题问完之后,紧接着被问到。“使用消息队列会带来什么问题?”这个问题要引起重视,一般我们都会考虑使用消息队列会带来的好处而忽略它带来的问题! - -### 1.2 那么使用消息队列会带来什么问题?考虑过这些问题吗? - -- **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了! -- **系统复杂性提高:** 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! -- **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! - -> 了解下面这个问题是为了我们更好的进行技术选型!该部分摘自:《Java 工程师面试突击第 1 季-中华石杉老师》,如果大家没有资源的话,可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可! - -### 1.3 介绍一下你知道哪几种消息队列,该如何选择呢? - -| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | -| :----------------------- | -----------------------------------------------------------: | -----------------------------------------------------------: | -----------------------------------------------------------: | -----------------------------------------------------------: | -| 单机吞吐量 | 万级,吞吐量比 RocketMQ 和 Kafka 要低了一个数量级 | 万级,吞吐量比 RocketMQ 和 Kafka 要低了一个数量级 | 10 万级,RocketMQ 也是可以支撑高吞吐的一种 MQ | 10 万级别,这是 kafka 最大的优点,就是吞吐量高。一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | -| topic 数量对吞吐量的影响 | | | topic 可以达到几百,几千个的级别,吞吐量会有较小幅度的下降这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka 尽量保证 topic 数量不要过多。如果要支撑大规模 topic,需要增加更多的机器资源 | -| 可用性 | 高,基于主从架构实现高可用性 | 高,基于主从架构实现高可用性 | 非常高,分布式架构 | 非常高,kafka 是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | -| 消息可靠性 | 有较低的概率丢失数据 | | 经过参数优化配置,可以做到 0 丢失 | 经过参数优化配置,消息可以做到 0 丢失 | -| 时效性 | ms 级 | 微秒级,这是 rabbitmq 的一大特点,延迟是最低的 | ms 级 | 延迟在 ms 级以内 | -| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 | -| 优劣势总结 | 非常成熟,功能强大,在业内大量的公司以及项目中都有应用。偶尔会有较低概率丢失消息,而且现在社区以及国内应用都越来越少,官方社区现在对 ActiveMQ 5.x 维护越来越少,几个月才发布一个版本而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用 | erlang 语言开发,性能极其好,延时很低;吞吐量到万级,MQ 功能比较完备而且开源提供的管理界面非常棒,用起来很好用。社区相对比较活跃,几乎每个月都发布几个版本分在国内一些互联网公司近几年用 rabbitmq 也比较多一些但是问题也是显而易见的,RabbitMQ 确实吞吐量会低一些,这是因为他做的实现机制比较重。而且 erlang 开发,国内有几个公司有实力做 erlang 源码级别的研究和定制?如果说你没这个实力的话,确实偶尔会有一些问题,你很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复 bug。而且 rabbitmq 集群动态扩展会很麻烦,不过这个我觉得还好。其实主要是 erlang 语言本身带来的问题。很难读源码,很难定制和掌控。 | 接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障。日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是 ok 的,还可以支撑大规模的 topic 数量,支持复杂 MQ 业务场景。而且一个很大的优势在于,阿里出品都是 java 系的,我们可以自己阅读源码,定制自己公司的 MQ,可以掌控。社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用 RocketMQ 挺好的 | kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。而且 kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 | - -> 这部分内容,我这里不给出答案,大家可以自行根据自己学习的消息队列查阅相关内容,我可能会在后面的文章中介绍到这部分内容。另外,下面这些问题在视频《Java 工程师面试突击第 1 季-中华石杉老师》中都有提到,如果大家没有资源的话,可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可! - -### 1.4 关于消息队列其他一些常见的问题展望 - -1. 引入消息队列之后如何保证高可用性? -2. 如何保证消息不被重复消费呢? -3. 如何保证消息的可靠性传输(如何处理消息丢失的问题)? -4. 我该怎么保证从消息队列里拿到的数据按顺序执行? -5. 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决? -6. 如果让你来开发一个消息队列中间件,你会怎么设计架构? - -## 2 谈谈 InnoDB 和 MyIsam 两者的区别 - -### 2.1 两者的对比 - -1. **count 运算上的区别:** 因为 MyISAM 缓存有表 meta-data(行数等),因此在做 COUNT(\*)时对于一个结构很好的查询是不需要消耗多少资源的。而对于 InnoDB 来说,则没有这种缓存 -2. **是否支持事务和崩溃后的安全恢复:** MyISAM 强调的是性能,每次查询具有原子性,其执行速度比 InnoDB 类型更快,但是不提供事务支持。但是 InnoDB 提供事务支持,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 -3. **是否支持外键:** MyISAM 不支持,而 InnoDB 支持。 - -### 2.2 关于两者的总结 - -MyISAM 更适合读密集的表,而 InnoDB 更适合写密集的表。 在数据库做主从分离的情况下,经常选择 MyISAM 作为主库的存储引擎。 - -一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM 的表锁的粒度太大,所以当该表写并发量较高时,要等待的查询就会很多了),InnoDB 是不错的选择。如果你的数据量很大(MyISAM 支持压缩特性可以减少磁盘的空间占用),而且不需要支持事务时,MyISAM 是最好的选择。 - -## 3 聊聊 Java 中的集合吧! - -### 3.1 Arraylist 与 LinkedList 有什么不同?(注意加上从数据结构分析的内容) - -- **1. 是否保证线程安全:** ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; -- **2. 底层数据结构:** Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是双向链表数据结构(注意双向链表和双向循环链表的区别:); -- **3. 插入和删除是否受元素位置的影响:** ① **ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e)`方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1) 而数组为近似 O(n) 。** -- **4. 是否支持快速随机访问:** LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 -- **5. 内存空间占用:** ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。 - -**补充内容:RandomAccess 接口** - -```java -public interface RandomAccess { -} -``` - -查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 - -在 binarySearch() 方法中,它要判断传入的 list 是否 RamdomAccess 的实例,如果是,调用 indexedBinarySearch() 方法,如果不是,那么调用 iteratorBinarySearch() 方法 - -```java - public static - int binarySearch(List> list, T key) { - if (list instanceof RandomAccess || list.size() Java 中的集合这类问题几乎是面试必问的,问到这类问题的时候,HashMap 又是几乎必问的问题,所以大家一定要引起重视! - -### 3.2 HashMap 的底层实现 - -#### 1)JDK1.8 之前 - -JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的时数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** - -**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** - -**JDK 1.8 HashMap 的 hash 方法源码:** - -JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 - -```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^ :按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } -``` - -对比一下 JDK1.7 的 HashMap 的 hash 方法源码. - -```java -static int hash(int h) { - // This function ensures that hashCodes that differ only by - // constant multiples at each bit position have a bounded - // number of collisions (approximately 8 at default load factor). - - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); -} -``` - -相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 - -所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 - -![jdk1.8之前的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/jdk1.8之前的内部结构-HashMap.jpg) - -#### 2)JDK1.8 之后 - -相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。 - -![jdk1.8之后的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8之后的HashMap底层数据结构.jpg) - -TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 - -> 问完 HashMap 的底层原理之后,面试官可能就会紧接着问你 HashMap 底层数据结构相关的问题! - -### 3.3 既然谈到了红黑树,你给我手绘一个出来吧,然后简单讲一下自己对于红黑树的理解 - -![红黑树](https://user-gold-cdn.xitu.io/2018/11/14/16711ac29c138cba?w=851&h=614&f=jpeg&s=34458) - -**红黑树特点:** - -1. 每个节点非红即黑; -2. 根节点总是黑色的; -3. 每个叶子节点都是黑色的空节点(NIL 节点); -4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); -5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度) - -**红黑树的应用:** - -TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。 - -**为什么要用红黑树** - -简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 - -### 3.4 红黑树这么优秀,为何不直接使用红黑树得了? - -说一下自己对于这个问题的看法:我们知道红黑树属于(自)平衡二叉树,但是为了保持“平衡”是需要付出代价的,红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,这费事啊。你说说我们引入红黑树就是为了查找数据快,如果链表长度很短的话,根本不需要引入红黑树的,你引入之后还要付出代价维持它的平衡。但是链表过长就不一样了。至于为什么选 8 这个值呢?通过概率统计所得,这个值是综合查询成本和新增元素成本得出的最好的一个值。 - -### 3.5 HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别 - -**HashMap 和 Hashtable 的区别** - -1. **线程是否安全:** HashMap 是非线程安全的,Hashtable 是线程安全的;Hashtable 内部的方法基本都经过 `synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); -2. **效率:** 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它; -3. **对 Null key 和 Null value 的支持:** HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 Hashtable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。 -4. **初始容量大小和每次扩充容量大小的不同 :** ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 -5. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 - -**HashSet 和 HashMap 区别** - -如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。) - -![HashSet 和 HashMap 区别](https://user-gold-cdn.xitu.io/2018/3/2/161e717d734f3b23?w=896&h=363&f=jpeg&s=205536) - -# 三 终结篇 - -## 1. Object 类有哪些方法? - -这个问题,面试中经常出现。我觉得不论是出于应付面试还是说更好地掌握 Java 这门编程语言,大家都要掌握! - -### 1.1 Object 类的常见方法总结 - -Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法: - -```java - -public final native Class getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。 - -public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。 -public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。 - -protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。 - -public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。 - -public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 - -public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。 - -public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。 - -public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。 - -public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念 - -protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作 - -``` - -> 问完上面这个问题之后,面试官很可能紧接着就会问你“hashCode 与 equals”相关的问题。 - -### 1.2 hashCode 与 equals - -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?” - -#### 1.2.1 hashCode()介绍 - -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。 - -```java - public native int hashCode(); -``` - -散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -#### 1.2.2 为什么要有 hashCode - -**我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** - -当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -#### 1.2.3 hashCode()与 equals()的相关规定 - -1. 如果两个对象相等,则 hashcode 一定也是相同的 -2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true -3. 两个对象有相同的 hashcode 值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) - -#### 1.2.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的? - -在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。 - -因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。 - -我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。 - -> ==与 equals 的对比也是比较常问的基础问题之一! - -### 1.3 ==与 equals - -**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址) - -**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: - -- 情况 1:类没有覆盖 equals()方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况 2:类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。 - -**举个例子:** - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -**说明:** - -- String 中的 equals()方法是被重写过的,因为 Object 的 equals()方法是比较的对象的内存地址,而 String 的 equals()方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -> 在[【备战春招/秋招系列 5】美团面经总结进阶篇 (附详解答案)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484625&idx=1&sn=9c4fa1f7d4291a5fbd7daa44bac2b012&chksm=fd9852b0caefdba6edcf9a827aa4a17ddc97bf6ad2e5ee6f7e1aa1b443b54444d05d2b76732b&token=723699735&lang=zh_CN#rd) 这篇文章中,我们已经提到了一下关于 HashMap 在面试中常见的问题:HashMap 的底层实现、简单讲一下自己对于红黑树的理解、红黑树这么优秀,为何不直接使用红黑树得了、HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别。HashMap 和 ConcurrentHashMap 这俩兄弟在一般只要面试中问到集合相关的问题就一定会被问到,所以各位务必引起重视! - -## 2 ConcurrentHashMap 相关问题 - -### 2.1 ConcurrentHashMap 和 Hashtable 的区别 - -ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 - -- **底层数据结构:** JDK1.7 的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; -- **实现线程安全的方式(重要):** ① **在 JDK1.7 的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配 16 个 Segment,比 Hashtable 效率提高 16 倍。) **到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)**:使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 - -**两者的对比图:** - -图片来源:http://www.cnblogs.com/chengxiao/p/6842045.html - -Hashtable: -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/50656681.jpg) - -JDK1.7 的 ConcurrentHashMap: -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/33120488.jpg) -JDK1.8 的 ConcurrentHashMap(TreeBin: 红黑二叉树节点 -Node: 链表节点): -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/97739220.jpg) - -### 2.2 ConcurrentHashMap 线程安全的具体实现方式/底层具体实现 - -#### JDK1.7(上面有示意图) - -首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 - -**ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成**。 - -Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。 - -```java -static class Segment extends ReentrantLock implements Serializable { -} -``` - -一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。 - -#### JDK1.8(上面有示意图) - -ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。 - -synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。 - -## 3 谈谈 synchronized 和 ReentrantLock 的区别 - -**① 两者都是可重入锁** - -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。 - -**② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API** - -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -**③ ReentrantLock 比 synchronized 增加了一些高级功能** - -相比 synchronized,ReentrantLock 增加了一些高级功能。主要来说主要有三点:**① 等待可中断;② 可实现公平锁;③ 可实现选择性通知(锁可以绑定多个条件)** - -- **ReentrantLock 提供了一种能够中断等待锁的线程的机制**,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **ReentrantLock 可以指定是公平锁还是非公平锁。而 synchronized 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReentrantLock 默认情况是非公平的,可以通过 ReentrantLock 类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 -- synchronized 关键字与 wait()和 notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock 类当然也可以实现,但是需要借助于 Condition 接口与 newCondition() 方法。Condition 是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个 Lock 对象中可以创建多个 Condition 实例(即对象监视器),**线程对象可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用 notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用 ReentrantLock 类结合 Condition 实例可以实现“选择性通知”** ,这个功能非常重要,而且是 Condition 接口默认提供的。而 synchronized 关键字就相当于整个 Lock 对象中只有一个 Condition 实例,所有的线程都注册在它一个身上。如果执行 notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而 Condition 实例的 signalAll()方法 只会唤醒注册在该 Condition 实例中的所有等待线程。 - -如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。 - -**④ 两者的性能已经相差无几** - -在 JDK1.6 之前,synchronized 的性能是比 ReentrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而 ReentrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReentrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReentrantLock 的文章都是错的!JDK1.6 之后,性能已经不是选择 synchronized 和 ReentrantLock 的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的 synchronized,所以还是提倡在 synchronized 能满足你的需求的情况下,优先考虑使用 synchronized 关键字来进行同步!优化后的 synchronized 和 ReentrantLock 一样,在很多地方都是用到了 CAS 操作。 - -## 4 线程池了解吗? - -### 4.1 为什么要用线程池? - -线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。 - -这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处: - -- **降低资源消耗。** 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度。** 当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -- **提高线程的可管理性。** 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - -### 4.2 Java 提供了哪几种线程池?他们各自的使用场景是什么? - -#### Java 主要提供了下面 4 种线程池 - -- **FixedThreadPool:** 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 -- **ScheduledThreadPoolExecutor:** 主要用来在给定的延迟后运行任务,或者定期执行任务。ScheduledThreadPoolExecutor 又分为:ScheduledThreadPoolExecutor(包含多个线程)和 SingleThreadScheduledExecutor (只包含一个线程)两种。 - -#### 各种线程池的适用场景介绍 - -- **FixedThreadPool:** 适用于为了满足资源管理需求,而需要限制当前线程数量的应用场景。它适用于负载比较重的服务器; -- **SingleThreadExecutor:** 适用于需要保证顺序地执行各个任务并且在任意时间点,不会有多个线程是活动的应用场景; -- **CachedThreadPool:** 适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器; -- **ScheduledThreadPoolExecutor:** 适用于需要多个后台执行周期任务,同时为了满足资源管理需求而需要限制后台线程的数量的应用场景; -- **SingleThreadScheduledExecutor:** 适用于需要单个后台线程执行周期任务,同时保证顺序地执行各个任务的应用场景。 - -### 4.3 创建的线程池的方式 - -**(1) 使用 Executors 创建** - -我们上面刚刚提到了 Java 提供的几种线程池,通过 Executors 工具类我们可以很轻松的创建我们上面说的几种线程池。但是实际上我们一般都不是直接使用 Java 提供好的线程池,另外在《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 - -```java -Executors 返回线程池对象的弊端如下: - -FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。 -CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。 - -``` - -**(2) ThreadPoolExecutor 的构造函数创建** - -我们可以自己直接调用 ThreadPoolExecutor 的构造函数来自己创建线程池。在创建的同时,给 BlockQueue 指定容量就可以了。示例如下: - -```java -private static ExecutorService executor = new ThreadPoolExecutor(13, 13, - 60L, TimeUnit.SECONDS, - new ArrayBlockingQueue(13)); -``` - -这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出 java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。 - -**(3) 使用开源类库** - -Hollis 大佬之前在他的文章中也提到了:“除了自己定义 ThreadPoolExecutor 外。还有其他方法。这个时候第一时间就应该想到开源类库,如 apache 和 guava 等。”他推荐使用 guava 提供的 ThreadFactoryBuilder 来创建线程池。下面是参考他的代码示例: - -```java -public class ExecutorsDemo { - - private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() - .setNameFormat("demo-pool-%d").build(); - - private static ExecutorService pool = new ThreadPoolExecutor(5, 200, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); - - public static void main(String[] args) { - - for (int i = 0; i < Integer.MAX_VALUE; i++) { - pool.execute(new SubThread()); - } - } -} -``` - -通过上述方式创建线程时,不仅可以避免 OOM 的问题,还可以自定义线程名称,更加方便的出错的时候溯源。 - -## 5 Nginx - -### 5.1 简单介绍一下 Nginx - -Nginx 是一款轻量级的 Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。 Nginx 主要提供反向代理、负载均衡、动静分离(静态资源服务)等服务。下面我简单地介绍一下这些名词。 - -#### 反向代理 - -谈到反向代理,就不得不提一下正向代理。无论是正向代理,还是反向代理,说到底,就是代理模式的衍生版本罢了 - -- **正向代理:**某些情况下,代理我们用户去访问服务器,需要用户手动的设置代理服务器的 ip 和端口号。正向代理比较常见的一个例子就是 VPN 了。 -- **反向代理:** 是用来代理服务器的,代理我们要访问的目标服务器。代理服务器接受请求,然后将请求转发给内部网络的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个服务器。 - -通过下面两幅图,大家应该更好理解(图源:http://blog.720ui.com/2016/nginx_action_05_proxy/): - -![正向代理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-15/60925795.jpg) - -![反向代理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-15/62563930.jpg) - -所以,简单的理解,就是正向代理是为客户端做代理,代替客户端去访问服务器,而反向代理是为服务器做代理,代替服务器接受客户端请求。 - -#### 负载均衡 - -在高并发情况下需要使用,其原理就是将并发请求分摊到多个服务器执行,减轻每台服务器的压力,多台服务器(集群)共同完成工作任务,从而提高了数据的吞吐量。 - -Nginx 支持的 weight 轮询(默认)、ip_hash、fair、url_hash 这四种负载均衡调度算法,感兴趣的可以自行查阅。 - -负载均衡相比于反向代理更侧重的是将请求分担到多台服务器上去,所以谈论负载均衡只有在提供某服务的服务器大于两台时才有意义。 - -#### 动静分离 - -动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。 - -### 5.2 为什么要用 Nginx? - -> 这部分内容参考极客时间—[Nginx 核心知识 100 讲的内容](https://time.geekbang.org/course/intro/138?code=AycjiiQk6uQRxnVJzBupFkrGkvZlmYELPRsZbWzaAHE= "Nginx核心知识100讲的内容")。 - -如果面试官问你这个问题,就一定想看你知道 Nginx 服务器的一些优点吗。 - -Nginx 有以下 5 个优点: - -1. 高并发、高性能(这是其他 web 服务器不具有的) -2. 可扩展性好(模块化设计,第三方插件生态圈丰富) -3. 高可靠性(可以在服务器行持续不间断的运行数年) -4. 热部署(这个功能对于 Nginx 来说特别重要,热部署指可以在不停止 Nginx 服务的情况下升级 Nginx) -5. BSD 许可证(意味着我们可以将源代码下载下来进行修改然后使用自己的版本) - -### 5.3 Nginx 的四个主要组成部分了解吗? - -> 这部分内容参考极客时间—[Nginx 核心知识 100 讲的内容](https://time.geekbang.org/course/intro/138?code=AycjiiQk6uQRxnVJzBupFkrGkvZlmYELPRsZbWzaAHE= "Nginx核心知识100讲的内容")。 - -- Nginx 二进制可执行文件:由各模块源码编译出一个文件 -- nginx.conf 配置文件:控制 Nginx 行为 -- acess.log 访问日志: 记录每一条 HTTP 请求信息 -- error.log 错误日志:定位问题 diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\351\235\242\350\257\225\345\256\230-\344\275\240\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221.md" "b/docs/essential-content-for-interview/PreparingForInterview/\351\235\242\350\257\225\345\256\230-\344\275\240\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221.md" deleted file mode 100644 index 7a55d539d11..00000000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\351\235\242\350\257\225\345\256\230-\344\275\240\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221.md" +++ /dev/null @@ -1,64 +0,0 @@ -我还记得当时我去参加面试的时候,几乎每一场面试,特别是HR面和高管面的时候,面试官总是会在结尾问我:“问了你这么多问题了,你有什么问题问我吗?”。这个时候很多人内心就会陷入短暂的纠结中:我该问吗?不问的话面试官会不会对我影响不好?问什么问题?问这个问题会不会让面试官对我的影响不好啊? - -![无奈](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/无奈.jpg) - -### 这个问题对最终面试结果的影响到底大不大? - -就技术面试而言,回答这个问题的时候,只要你不是触碰到你所面试的公司的雷区,那么我觉得这对你能不能拿到最终offer来说影响确实是不大的。我说这些并不代表你就可以直接对面试官说:“我没问题了。”,笔主当时面试的时候确实也说过挺多次“没问题要问了。”,最终也没有导致笔主被pass掉(可能是前面表现比较好,哈哈,自恋一下)。我现在回想起来,觉得自己当时做法其实挺不对的。面试本身就是一个双向选择的过程,你对这个问题的回答也会侧面反映出你对这次面试的上心程度,你的问题是否有价值,也影响了你最终的选择与公司是否选择你。 - -面试官在技术面试中主要考察的还是你这样个人到底有没有胜任这个工作的能力以及你是否适合公司未来的发展需要,很多公司还需要你认同它的文化,我觉得你只要不是太笨,应该不会栽在这里。除非你和另外一个人在能力上相同,但是只能在你们两个人中选一个,那么这个问题才对你能不能拿到offer至关重要。有准备总比没准备好,给面试官留一个好的影响总归是没错的。 - -但是,就非技术面试来说,我觉得好好回答这个问题对你最终的结果还是比较重要的。 - -总的来说不管是技术面试还是非技术面试,如果你想赢得公司的青睐和尊重,我觉得我们都应该重视这个问题。 - -### 真诚一点,不要问太 Low 的问题 - -回答这个问题很重要的一点就是你没有必要放低自己的姿态问一些很虚或者故意讨好面试官的问题,也不要把自己从面经上学到的东西照搬下来使用。面试官也不是傻子,特别是那种特别有经验的面试官,你是真心诚意的问问题,还是从别处照搬问题来讨好面试官,人家可能一听就听出来了。总的来说,还是要真诚。除此之外,不要问太 Low 的问题,会显得你整个人格局比较小或者说你根本没有准备(侧面反映你对这家公司不上心,既然你不上心,为什么要要你呢)。举例几个比较 Low 的问题,大家看看自己有没有问过其中的问题: - -- 贵公司的主要业务是什么?(面试之前自己不知道提前网上查一下吗?) -- 贵公司的男女比例如何?(考虑脱单?记住你是来工作的!) -- 贵公司一年搞几次外出旅游?(你是来工作的,这些娱乐活动先别放在心上!) -- ...... - -### 有哪些有价值的问题值得问? - -针对这个问题。笔主专门找了几个专门做HR工作的小哥哥小姐姐们询问并且查阅了挺多前辈们的回答,然后结合自己的实际经历,我概括了下面几个比较适合问的问题。 - -#### 面对HR或者其他Level比较低的面试官时 - -1. **能不能谈谈你作为一个公司老员工对公司的感受?** (这个问题比较容易回答,不会让面试官陷入无话可说的尴尬境地。另外,从面试官的回答中你可以加深对这个公司的了解,让你更加清楚这个公司到底是不是你想的那样或者说你是否能适应这个公司的文化。除此之外,这样的问题在某种程度上还可以拉进你与面试官的距离。) -2. **能不能问一下,你当时因为什么原因选择加入这家公司的呢或者说这家公司有哪些地方吸引你?有什么地方你觉得还不太好或者可以继续完善吗?** (类似第一个问题,都是问面试官个人对于公司的看法。) -3. **我觉得我这次表现的不是太好,你有什么建议或者评价给我吗?**(这个是我常问的。我觉得说自己表现不好只是这个语境需要这样来说,这样可以显的你比较谦虚好学上进。) -4. **接下来我会有一段空档期,有什么值得注意或者建议学习的吗?** (体现出你对工作比较上心,自助学习意识比较强。) -5. **这个岗位为什么还在招人?** (岗位真实性和价值咨询) -6. **大概什么时候能给我回复呢?** (终面的时候,如果面试官没有说的话,可以问一下) -7. ...... - - - -#### 面对部门领导 - -1. **部门的主要人员分配以及对应的主要工作能简单介绍一下吗?** -2. **未来如果我要加入这个团队,你对我的期望是什么?** (部门领导一般情况下是你的直属上级了,你以后和他打交道的机会应该是最多的。你问这个问题,会让他感觉你是一个对他的部门比较上心,比较有团体意识,并且愿意倾听的候选人。) -3. **公司对新入职的员工的培养机制是什么样的呢?** (正规的公司一般都有培养机制,提前问一下是对你自己的负责也会显的你比较上心) -4. **以您来看,这个岗位未来在公司内部的发展如何?** (在我看来,问这个问题也是对你自己的负责吧,谁不想发展前景更好的岗位呢?) -5. **团队现在面临的最大挑战是什么?** (这样的问题不会暴露你对公司的不了解,并且也能让你对未来工作的挑战或困难有一个提前的预期。) - - - -#### 面对Level比较高的(比如总裁,老板) - -1. **贵公司的发展目标和方向是什么?** (看下公司的发展是否满足自己的期望) -2. **与同行业的竞争者相比,贵公司的核心竞争优势在什么地方?** (充分了解自己的优势和劣势) -3. **公司现在面临的最大挑战是什么?** - -### 来个补充,顺便送个祝福给大家 - -薪酬待遇和相关福利问题一般在终面的时候(最好不要在前面几面的时候就问到这个问题),面试官会提出来或者在面试完之后以邮件的形式告知你。一般来说,如果面试官很愿意为你回答问题,对你的问题也比较上心的话,那他肯定是觉得你就是他们要招的人。 - -大家在面试的时候,可以根据自己对于公司或者岗位的了解程度,对上面提到的问题进行适当修饰或者修改。上面提到的一些问题只是给没有经验的朋友一个参考,如果你还有其他比较好的问题的话,那当然也更好啦! - -金三银四。过了二月就到了面试高峰期或者说是黄金期。几份惊喜几份愁,愿各位能始终不忘初心!每个人都有每个人的难处。引用一句《阿甘正传》里面的台词:“生活就像一盒巧克力,你永远不知道下一块是什么味道“。 - -![加油!彩虹就要来了](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/生活就像一盒巧克力你永远不知道下一块是什么味道.JPEG) \ No newline at end of file diff --git a/docs/essential-content-for-interview/real-interview-experience-analysis/alibaba-1.md b/docs/essential-content-for-interview/real-interview-experience-analysis/alibaba-1.md deleted file mode 100644 index a1f61011887..00000000000 --- a/docs/essential-content-for-interview/real-interview-experience-analysis/alibaba-1.md +++ /dev/null @@ -1,222 +0,0 @@ -本文的内容都是根据读者投稿的真实面试经历改编而来,首次尝试这种风格的文章,花了几天晚上才总算写完,希望对你有帮助。 - -本文主要涵盖下面的内容: - -1. 分布式商城系统:架构图讲解; -2. 消息队列相关:削峰和解耦; -3. Redis 相关:缓存穿透问题的解决; -4. 一些基础问题: - - 网络相关:1.浏览器输入 URL 发生了什么? 2.TCP 和 UDP 区别? 3.TCP 如何保证传输可靠性? - - Java 基础:1. 既然有了字节流,为什么还要有字符流? 2.深拷贝 和 浅拷贝有啥区别呢? - -下面是正文! - -面试开始,坐在我前面的就是这次我的面试官吗?这发量看着根本不像程序员啊?我心里正嘀咕着,只听见面试官说:“小伙,下午好,我今天就是你的面试官,咱们开始面试吧!”。 - -### 第一面开始 - -**面试官:** 我也不用多说了,你先自我介绍一下吧,简历上有的就不要再说了哈。 - -**我:** 内心 os:"果然如我所料,就知道会让我先自我介绍一下,还好我看了 [JavaGuide](https://github.com/Snailclimb/JavaGuide "JavaGuide") ,学到了一些套路。套路总结起来就是:**最好准备好两份自我介绍,一份对 hr 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。** 所以,我按照这个套路准备了一个还算通用的模板,毕竟我懒嘛!不想多准备一个自我介绍,整个通用的多好! - -> 面试官,您好!我叫小李子。大学时间我主要利用课外时间学习 Java 相关的知识。在校期间参与过一个某某系统的开发,主要负责数据库设计和后端系统开发.,期间解决了什么问题,巴拉巴拉。另外,我自己在学习过程中也参照网上的教程写过一个电商系统的网站,写这个电商网站主要是为了能让自己接触到分布式系统的开发。在学习之余,我比较喜欢通过博客整理分享自己所学知识。我现在已经是某社区的认证作者,写过一系列关于 线程池使用以及源码分析的文章深受好评。另外,我获得过省级编程比赛二等奖,我将这个获奖项目开源到 Github 还收获了 2k 的 Star 呢? - -**面试官:** 你刚刚说参考网上的教程做了一个电商系统?你能画画这个电商系统的架构图吗? - -**我:** 内心 os: "这可难不倒我!早知道写在简历上的项目要重视了,提前都把这个系统的架构图画了好多遍了呢!" - - - -做过分布式电商系统的一定很熟悉上面的架构图(目前比较流行的是微服务架构,但是如果你有分布式开发经验也是非常加分的!)。 - -**面试官:** 简单介绍一下你做的这个系统吧! - -**我:** 我一本正经的对着我刚刚画的商城架构图开始了满嘴造火箭的讲起来: - -> 本系统主要分为展示层、服务层和持久层这三层。表现层顾名思义主要就是为了用来展示,比如我们的后台管理系统的页面、商城首页的页面、搜索系统的页面等等,这一层都只是作为展示,并没有提供任何服务。 -> -> 展示层和服务层一般是部署在不同的机器上来提高并发量和扩展性,那么展示层和服务层怎样才能交互呢?在本系统中我们使用 Dubbo 来进行服务治理。Dubbo 是一款高性能、轻量级的开源 Java RPC 框架。Dubbo 在本系统的主要作用就是提供远程 RPC 调用。在本系统中服务层的信息通过 Dubbo 注册给 ZooKeeper,表现层通过 Dubbo 去 ZooKeeper 中获取服务的相关信息。Zookeeper 的作用仅仅是存放提供服务的服务器的地址和一些服务的相关信息,实现 RPC 远程调用功能的还是 Dubbo。如果需要引用到某个服务的时候,我们只需要在配置文件中配置相关信息就可以在代码中直接使用了,就像调用本地方法一样。假如说某个服务的使用量增加时,我们只用为这单个服务增加服务器,而不需要为整个系统添加服务。 -> -> 另外,本系统的数据库使用的是常用的 MySQL,并且用到了数据库中间件 MyCat。另外,本系统还用到 redis 内存数据库来作为缓存来提高系统的反应速度。假如用户第一次访问数据库中的某些数据,这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。 -> -> 系统还用到了 Elasticsearch 来提供搜索功能。使用 Elasticsearch 我们可以非常方便的为我们的商城系统添加必备的搜索功能,并且使用 Elasticsearch 还能提供其它非常实用的功能,并且很容易扩展。 - -**面试官:** 我看你的系统里面还用到了消息队列,能说说为什么要用它吗? - -**我:** - -> 使用消息队列主要是为了: -> -> 1. 减少响应所需时间和削峰。 -> 2. 降低系统耦合性(解耦/提升系统可扩展性)。 - -**面试官:** 你这说的太简单了!能不能稍微详细一点,最好能画图给我解释一下。 - -**我:** 内心 os:"都 2019 年了,大部分面试者都能对消息队列的为系统带来的这两个好处倒背如流了,如果你想走的更远就要别别人懂的更深一点!" - -> 当我们不使用消息队列的时候,所有的用户的请求会直接落到服务器,然后通过数据库或者缓存响应。假如在高并发的场景下,如果没有缓存或者数据库承受不了这么大的压力的话,就会造成响应速度缓慢,甚至造成数据库宕机。但是,在使用消息队列之后,用户的请求数据发送给了消息队列之后就可以立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库,不过要确保消息不被重复消费还要考虑到消息丢失问题。由于消息队列服务器处理速度快于数据库,因此响应速度得到大幅改善。 -> -> 文字 is too 空洞,直接上图吧!下图展示了使用消息前后系统处理用户请求的对比(ps:我自己都被我画的这个图美到了,如果你也觉得这张图好看的话麻烦来个素质三连!)。 -> -> ![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Asynchronous-message-queue.png) -> -> 通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: -> -> ![削峰](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/削峰-消息队列.png) -> -> 使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧: -> -> ![解耦](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/消息队列-解耦.png) -> -> 生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。 - -**面试官:** 你觉得它有什么缺点吗?或者说怎么考虑用不用消息队列? - -**我:** 内心 os: "面试官真鸡贼!这不是勾引我上钩么?还好我准备充分。" - -> 我觉得可以从下面几个方面来说: -> -> 1. **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了! -> 2. **系统复杂性提高:** 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! -> 3. **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! - -**面试官**:做项目的过程中遇到了什么问题吗?解决了吗?如果解决的话是如何解决的呢? - -**我** : 内心 os: "做的过程中好像也没有遇到什么问题啊!怎么办?怎么办?突然想到可以说我在使用 Redis 过程中遇到的问题,毕竟我对 Redis 还算熟悉嘛,**把面试官往这个方向吸引**,准没错。" - -> 我在使用 Redis 对常用数据进行缓冲的过程中出现了缓存穿透问题。然后,我通过谷歌搜索相关的解决方案来解决的。 - -**面试官:** 你还知道缓存穿透啊?不错啊!来说说什么是缓存穿透以及你最后的解决办法。 - -**我:** 我先来谈谈什么是缓存穿透吧! - -> 缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。 -> -> 总结一下就是: -> -> 1. 缓存层不命中。 -> 2. 存储层不命中,不将空结果写回缓存。 -> 3. 返回空结果给客户端。 -> -> 一般 MySQL 默认的最大连接数在 150 左右,这个可以通过 `show variables like '%max_connections%';`命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等物理条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 的并发请求就能打死大部分数据库了。 - -**面试官:** 小伙子不错啊!还准备问你:“为什么 3000 的并发能把支持最大连接数 4000 数据库压死?”想不到你自己就提前回答了!不错! - -**我:** 别夸了!别夸了!我再来说说我知道的一些解决办法以及我最后采用的方案吧!您帮忙看看有没有问题。 - -> 最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。 -> -> 参数校验通过的情况还是会出现缓存穿透,我们还可以通过以下几个方案来解决这个问题: -> -> **1)缓存无效 key** : 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如何黑客恶意攻击,每次构建的不同的请求 key,会导致 redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。 -> -> 另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值`。 -> -> **2)布隆过滤器:** 布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。 - -**面试官:** 不错不错!你还知道布隆过滤器啊!来给我谈一谈。 - -**我:** 内心 os:“如果你准备过海量数据处理的面试题,你一定对:“如何确定一个数字是否在于包含大量数字的数字集中(数字集很大,5 亿以上!)?”这个题目很了解了!解决这道题目就要用到布隆过滤器。” - -> 布隆过滤器在针对海量数据去重或者验证数据合法性的时候非常有用。**布隆过滤器的本质实际上是 “位(bit)数组”,也就是说每一个存入布隆过滤器的数据都只占一位。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。** -> -> **当一个元素加入布隆过滤器中的时候,会进行如下操作:** -> -> 1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。 -> 2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。 -> -> **当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:** -> -> 1. 对给定元素再次进行相同的哈希计算; -> 2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 -> -> 举个简单的例子: -> -> ![布隆过滤器hash计算](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-hash运算.png) -> -> 如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为 0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。 -> -> 如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 -> -> **不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。** -> -> 综上,我们可以得出:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。** - -**面试官:** 看来你对布隆过滤器了解的还挺不错的嘛!那你快说说你最后是怎么利用它来解决缓存穿透的。 - -**我:** 知道了布隆过滤器的原理就之后就很容易做了。我是利用 Redis 布隆过滤器来做的。我把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。总结一下就是下面这张图(这张图片不是我画的,为了省事直接在网上找的): - - - -更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md "《不了解布隆过滤器?一文给你整的明明白白!》") ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。 - -**面试官:** 好了好了。项目就暂时问到这里吧!下面有一些比较基础的问题我简单地问一下你。内心 os: 难不成这家伙满口高并发,连最基础的东西都不会吧! - -**我:** 好的好的!没问题! - -**面试官:** 浏览器输入 URL 发生了什么? - -**我:** 内心 os:“很常问的一个问题,建议拿小本本记好了!另外,百度好像最喜欢问这个问题,去百度面试可要提前备好这道题的功课哦!相似问题:打开一个网页,整个过程会使用哪些协议?”。 - -> 图解(图片来源:《图解 HTTP》): -> -> -> -> 总体来说分为以下几个过程: -> -> 1. DNS 解析 -> 2. TCP 连接 -> 3. 发送 HTTP 请求 -> 4. 服务器处理请求并返回 HTTP 报文 -> 5. 浏览器解析渲染页面 -> 6. 连接结束 -> -> 具体可以参考下面这篇文章: -> -> - [https://segmentfault.com/a/1190000006879700](https://segmentfault.com/a/1190000006879700 "https://segmentfault.com/a/1190000006879700") - -**面试官:** TCP 和 UDP 区别? - -**我:** - -> ![TCP、UDP协议的区别](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/tcp-vs-udp.jpg) -> -> UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等 -> -> TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP 的可靠体现在 TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。 - -**面试官:** TCP 如何保证传输可靠性? - -**我:** - -> 1. 应用数据被分割成 TCP 认为最适合发送的数据块。 -> 2. TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。 -> 3. **校验和:** TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。 -> 4. TCP 的接收端会丢弃重复的数据。 -> 5. **流量控制:** TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制) -> 6. **拥塞控制:** 当网络拥塞时,减少数据的发送。 -> 7. **ARQ 协议:** 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 -> 8. **超时重传:** 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。 - -**面试官:** 我再来问你一些 Java 基础的问题吧!小伙子。 - -**我:** 好的。(内心 os:“你尽管来!”) - -**面试官:** 既然有了字节流,为什么还要有字符流? - -我:内心 os :“问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?**” - -> 字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - -**面试官**:深拷贝 和 浅拷贝有啥区别呢? - -**我:** - -> 1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 -> 2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 -> -> ![deep and shallow copy](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/java-deep-and-shallow-copy.jpg) - -**面试官:** 好的!面试结束。小伙子可以的!回家等通知吧! - -**我:** 好的好的!辛苦您了! diff --git "a/docs/essential-content-for-interview/\346\211\213\346\212\212\346\211\213\346\225\231\344\275\240\347\224\250Markdown\345\206\231\344\270\200\344\273\275\351\253\230\350\264\250\351\207\217\347\232\204\347\256\200\345\216\206.md" "b/docs/essential-content-for-interview/\346\211\213\346\212\212\346\211\213\346\225\231\344\275\240\347\224\250Markdown\345\206\231\344\270\200\344\273\275\351\253\230\350\264\250\351\207\217\347\232\204\347\256\200\345\216\206.md" deleted file mode 100644 index 9cb2811b75f..00000000000 --- "a/docs/essential-content-for-interview/\346\211\213\346\212\212\346\211\213\346\225\231\344\275\240\347\224\250Markdown\345\206\231\344\270\200\344\273\275\351\253\230\350\264\250\351\207\217\347\232\204\347\256\200\345\216\206.md" +++ /dev/null @@ -1,93 +0,0 @@ -## Markdown 简历模板样式一览 -![](https://user-gold-cdn.xitu.io/2018/9/3/1659f91e4843bd67?w=800&h=1737&f=png&s=97357) -**可以看到我把联系方式放在第一位,因为公司一般会与你联系,所以把联系方式放在第一位也是为了方便联系考虑。** - -## 为什么要用 Markdown 写简历? - -Markdown 语法简单,易于上手。使用正确的 Markdown 语言写出来的简历不论是在排版还是格式上都比较干净,易于阅读。另外,使用 Markdown 写简历也会给面试官一种你比较专业的感觉。 - -除了这些,我觉得使用 Markdown 写简历可以很方便将其与PDF、HTML、PNG格式之间转换。后面我会介绍到转换方法,只需要一条命令你就可以实现 Markdown 到 PDF、HTML 与 PNG之间的无缝切换。 - -> 下面的一些内容我在之前的一篇文章中已经提到过,这里再说一遍,最后会分享如何实现Markdown 到 PDF、HTML、PNG格式之间转换。 - -## 为什么说简历很重要? - -假如你是网申,你的简历必然会经过HR的筛选,一张简历HR可能也就花费10秒钟看一下,然后HR就会决定你这一关是Fail还是Pass。 - -假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。 - -另外,就算你通过了筛选,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。 - -## 写简历的两大法则 - -目前写简历的方式有两种普遍被认可,一种是 STAR, 一种是 FAB。 - -**STAR法则(Situation Task Action Result):** - -- **Situation:** 事情是在什么情况下发生; -- **Task::** 你是如何明确你的任务的; -- **Action:** 针对这样的情况分析,你采用了什么行动方式; -- **Result:** 结果怎样,在这样的情况下你学习到了什么。 - -**FAB 法则(Feature Advantage Benefit):** - -- **Feature:** 是什么; -- **Advantage:** 比别人好在哪些地方; -- **Benefit:** 如果雇佣你,招聘方会得到什么好处。 - -## 项目经历怎么写? -简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑从如下几点来写: - -1. 对项目整体设计的一个感受 -2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的。 - -## 专业技能该怎么写? -先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。比如你可以这样写: - -- Dubbo:精通 -- Spring:精通 -- Docker:掌握 -- SOA分布式开发 :掌握 -- Spring Cloud:了解 - -## 简历模板分享 - -**开源程序员简历模板**: [https://github.com/geekcompany/ResumeSample](https://github.com/geekcompany/ResumeSample)(包括PHP程序员简历模板、iOS程序员简历模板、Android程序员简历模板、Web前端程序员简历模板、Java程序员简历模板、C/C++程序员简历模板、NodeJS程序员简历模板、架构师简历模板以及通用程序员简历模板) - -**上述简历模板的改进版本:** [https://github.com/Snailclimb/Java-Guide/blob/master/面试必备/简历模板.md](https://github.com/Snailclimb/Java-Guide/blob/master/面试必备/简历模板.md) - -## 其他的一些小tips - -1. 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。 -2. 注意排版(不需要花花绿绿的),尽量使用Markdown语法。 -3. 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。 -4. 如果自己的Github比较活跃的话,写上去也会为你加分很多。 -5. 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容 -6. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。 -7. 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。 -8. 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。 - - -> 我们刚刚讲了很多关于如何写简历的内容并且分享了一份 Markdown 格式的简历文档。下面我们来看看如何实现 Markdown 到 HTML格式、PNG格式之间转换。 -## Markdown 到 HTML格式、PNG格式之间转换 - -网上很难找到一个比较方便并且效果好的转换方法,最后我是通过 Visual Studio Code 的 Markdown PDF 插件完美解决了这个问题! - -### 安装 Markdown PDF 插件 - -**① 打开Visual Studio Code ,按快捷键 F1,选择安装扩展选项** - -![① 打开Visual Studio Code ,按快捷键 F1,选择安装扩展选项](https://user-gold-cdn.xitu.io/2018/9/3/1659f9a44103e551?w=1366&h=688&f=png&s=104435) - -**② 搜索 “Markdown PDF” 插件并安装 ,然后重启** - -![② 搜索 “Markdown PDF” 插件并安装 ,然后重启](https://user-gold-cdn.xitu.io/2018/9/3/1659f9dbef0d06fb?w=1280&h=420&f=png&s=70510) - -### 使用方法 - -随便打开一份 Markdown 文件 点击F1,然后输入export即可! - -![](https://user-gold-cdn.xitu.io/2018/9/3/1659fa0292906150?w=1289&h=468&f=png&s=72178) - diff --git "a/docs/essential-content-for-interview/\347\256\200\345\216\206\346\250\241\346\235\277.md" "b/docs/essential-content-for-interview/\347\256\200\345\216\206\346\250\241\346\235\277.md" deleted file mode 100644 index 2a7a0431f35..00000000000 --- "a/docs/essential-content-for-interview/\347\256\200\345\216\206\346\250\241\346\235\277.md" +++ /dev/null @@ -1,79 +0,0 @@ -# 联系方式 - -- 手机: -- Email: -- 微信: - -# 个人信息 - - - 姓名/性别/出生日期 - - 本科/xxx计算机系xxx专业/英语六级 - - 技术博客:[http://snailclimb.top/](http://snailclimb.top/) - - 荣誉奖励:获得了什么奖(获奖时间) - - Github:[https://github.com/Snailclimb ](https://github.com/Snailclimb) - - Github Resume: [http://resume.github.io/?Snailclimb](http://resume.github.io/?Snailclimb) - - 期望职位:Java 研发程序员/大数据工程师(Java后台开发为首选) - - 期望城市:xxx城市 - - -# 项目经历 - -## xxx项目 - -### 项目描述 - -介绍该项目是做什么的、使用到了什么技术以及你对项目整体设计的一个感受 - -### 责任描述 - -主要可以从下面三点来写: - -1. 在这个项目中你负责了什么、做了什么、担任了什么角色 -2. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -3. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的。 - -# 开源项目和技术文章 - -## 开源项目 - -- [Java-Guide](https://github.com/Snailclimb/Java-Guide) :一份涵盖大部分Java程序员所需要掌握的核心知识。Star:3.9K; Fork:0.9k。 - - -## 技术文章推荐 - -- [可能是把Java内存区域讲的最清楚的一篇文章](https://juejin.im/post/5b7d69e4e51d4538ca5730cb) -- [搞定JVM垃圾回收就是这么简单](https://juejin.im/post/5b85ea54e51d4538dd08f601) -- [前端&后端程序员必备的Linux基础知识](https://juejin.im/post/5b3b19856fb9a04fa42f8c71) -- [可能是把Docker的概念讲的最清楚的一篇文章](https://juejin.im/post/5b260ec26fb9a00e8e4b031a) - - -# 校园经历(可选) - -## 2016-2017 - -担任学校社团-致深社副会长,主要负责团队每周活动的组建以及每周例会的主持。 - -## 2017-2018 - 担任学校传媒组织:“长江大学在线信息传媒”的副站长以及安卓组成员。主要负责每周例会主持、活动策划以及学校校园通APP的研发工作。 - - -# 技能清单 - -以下均为我熟练使用的技能 - -- Web开发:PHP/Hack/Node -- Web框架:ThinkPHP/Yaf/Yii/Lavarel/LazyPHP -- 前端框架:Bootstrap/AngularJS/EmberJS/HTML5/Cocos2dJS/ionic -- 前端工具:Bower/Gulp/SaSS/LeSS/PhoneGap -- 数据库相关:MySQL/PgSQL/PDO/SQLite -- 版本管理、文档和自动化部署工具:Svn/Git/PHPDoc/Phing/Composer -- 单元测试:PHPUnit/SimpleTest/Qunit -- 云和开放平台:SAE/BAE/AWS/微博开放平台/微信应用开发 - -# 自我评价(可选) - -自我发挥。切记不要过度自夸!!! - - -### 感谢您花时间阅读我的简历,期待能有机会和您共事。 - diff --git "a/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" "b/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" deleted file mode 100644 index 00aaecd8c5b..00000000000 --- "a/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" +++ /dev/null @@ -1,115 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [何谓悲观锁与乐观锁](#何谓悲观锁与乐观锁) - - [悲观锁](#悲观锁) - - [乐观锁](#乐观锁) - - [两种锁的使用场景](#两种锁的使用场景) -- [乐观锁常见的两种实现方式](#乐观锁常见的两种实现方式) - - [1. 版本号机制](#1-版本号机制) - - [2. CAS算法](#2-cas算法) -- [乐观锁的缺点](#乐观锁的缺点) - - [1 ABA 问题](#1-aba-问题) - - [2 循环时间长开销大](#2-循环时间长开销大) - - [3 只能保证一个共享变量的原子操作](#3-只能保证一个共享变量的原子操作) -- [CAS与synchronized的使用情景](#cas与synchronized的使用情景) - - - -### 何谓悲观锁与乐观锁 - -> 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。 - -#### 悲观锁 - -总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。 - - -#### 乐观锁 - -总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。**乐观锁适用于多读的应用类型,这样可以提高吞吐量**,像数据库提供的类似于**write_condition机制**,其实都是提供的乐观锁。在Java中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式**CAS**实现的。 - -#### 两种锁的使用场景 - -从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像**乐观锁适用于写比较少的情况下(多读场景)**,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以**一般多写的场景下用悲观锁就比较合适。** - - -### 乐观锁常见的两种实现方式 - -> **乐观锁一般会使用版本号机制或CAS算法实现。** - -#### 1. 版本号机制 - -一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。 - -**举一个简单的例子:** -假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。 - -1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。 -2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。 -3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。 -4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 - -这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。 - -#### 2. CAS算法 - -即**compare and swap(比较与交换)**,是一种有名的**无锁算法**。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。**CAS算法**涉及到三个操作数 - -- 需要读写的内存值 V -- 进行比较的值 A -- 拟写入的新值 B - -当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个**自旋操作**,即**不断的重试**。 - -关于自旋锁,大家可以看一下这篇文章,非常不错:[《 -面试必备之深入理解自旋锁》](https://blog.csdn.net/qq_34337272/article/details/81252853) - -### 乐观锁的缺点 - -> ABA 问题是乐观锁一个常见的问题 - -#### 1 ABA 问题 - -如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 **"ABA"问题。** - -JDK 1.5 以后的 `AtomicStampedReference 类`就提供了此种能力,其中的 `compareAndSet 方法`就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 - -#### 2 循环时间长开销大 -**自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。** 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。 - -#### 3 只能保证一个共享变量的原子操作 - -CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了`AtomicReference类`来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference类`把多个共享变量合并成一个共享变量来操作。 - -### CAS与synchronized的使用情景 - -> **简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)** - -1. 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。 -2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。 - -补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 **“重量级锁”** 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 **偏向锁** 和 **轻量级锁** 以及其它**各种优化**之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 **Lock-Free** 的队列,基本思路是 **自旋后阻塞**,**竞争切换后继续竞争锁**,**稍微牺牲了公平性,但获得了高吞吐量**。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - - - - - - - - - - - - diff --git a/docs/github-trending/2018-12.md b/docs/github-trending/2018-12.md deleted file mode 100644 index 3637e93ea0c..00000000000 --- a/docs/github-trending/2018-12.md +++ /dev/null @@ -1,78 +0,0 @@ -本文数据统计于 1.1 号凌晨,由 SnailClimb 整理。 - -### 1. JavaGuide - -- **Github地址**: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- **star**: 18.2k -- **介绍**: 【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。 - -### 2. mall - -- **Github地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 3.3k -- **介绍**: mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 3. advanced-java - -- **Github地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **star**: 3.3k -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲 - -### 4. matrix - -- **Github地址**:[https://github.com/Tencent/matrix](https://github.com/Tencent/matrix) -- **star**: 2.5k -- **介绍**: Matrix 是一款微信研发并日常使用的 APM(Application Performance Manage),当前主要运行在 Android 平台上。 Matrix 的目标是建立统一的应用性能接入框架,通过各种性能监控方案,对性能监控项的异常数据进行采集和分析,输出相应的问题分析、定位与优化建议,从而帮助开发者开发出更高质量的应用。 - -### 5. miaosha - -- **Github地址**:[https://github.com/qiurunze123/miaosha](https://github.com/qiurunze123/miaosha) -- **star**: 2.4k -- **介绍**: 高并发大流量如何进行秒杀架构,我对这部分知识做了一个系统的整理,写了一套系统。 - -### 6. arthas - -- **Github地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 8.2k -- **介绍**: Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。 - -### 7 spring-boot - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 32.6k -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - - **关于Spring Boot官方的介绍:** - - > Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”…Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可) - -### 8. tutorials - -- **Github地址**:[https://github.com/eugenp/tutorials](https://github.com/eugenp/tutorials) -- **star**: 10k -- **介绍**: 该项目是一系列小而专注的教程 - 每个教程都涵盖Java生态系统中单一且定义明确的开发领域。 当然,它们的重点是Spring Framework - Spring,Spring Boot和Spring Securiyt。 除了Spring之外,还有以下技术:核心Java,Jackson,HttpClient,Guava。 - -### 9. qmq - -- **Github地址**:[https://github.com/qunarcorp/qmq](https://github.com/qunarcorp/qmq) -- **star**: 1.1k -- **介绍**: QMQ是去哪儿网内部广泛使用的消息中间件,自2012年诞生以来在去哪儿网所有业务场景中广泛的应用,包括跟交易息息相关的订单场景; 也包括报价搜索等高吞吐量场景。 - - -### 10. symphony - -- **Github地址**:[https://github.com/b3log/symphony](https://github.com/b3log/symphony) -- **star**: 9k -- **介绍**: 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台。 - -### 11. incubator-dubbo - -- **Github地址**:[https://github.com/apache/incubator-dubbo](https://github.com/apache/incubator-dubbo) -- **star**: 23.6k -- **介绍**: 阿里开源的一个基于Java的高性能开源RPC框架。 - -### 12. apollo - -- **Github地址**:[https://github.com/ctripcorp/apollo](https://github.com/ctripcorp/apollo) -- **star**: 10k -- **介绍**: Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。 diff --git a/docs/github-trending/2019-1.md b/docs/github-trending/2019-1.md deleted file mode 100644 index aa1de92f623..00000000000 --- a/docs/github-trending/2019-1.md +++ /dev/null @@ -1,76 +0,0 @@ -### 1. JavaGuide - -- **Github地址**: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- **star**: 22.8k -- **介绍**: 【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。 - -### 2. advanced-java - -- **Github地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **star**: 7.9k -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲 - -### 3. fescar - -- **Github地址**:[https://github.com/alibaba/fescar](https://github.com/alibaba/fescar) -- **star**: 4.6k -- **介绍**: 具有 **高性能** 和 **易用性** 的 **微服务架构** 的 **分布式事务** 的解决方案。(特点:高性能且易于使用,旨在实现简单并快速的事务提交与回滚。 - -### 4. mall - -- **Github地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 5.6 k -- **介绍**: mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 5. miaosha - -- **Github地址**:[https://github.com/qiurunze123/miaosha](https://github.com/qiurunze123/miaosha) -- **star**: 4.4k -- **介绍**: 高并发大流量如何进行秒杀架构,我对这部分知识做了一个系统的整理,写了一套系统。 - -### 6. flink - -- **Github地址**:[https://github.com/apache/flink](https://github.com/apache/flink) -- **star**: 7.1 k -- **介绍**: Apache Flink是一个开源流处理框架,具有强大的流和批处理功能。 - -### 7. cim - -- **Github地址**:[https://github.com/crossoverJie/cim](https://github.com/crossoverJie/cim) -- **star**: 1.8 k -- **介绍**: cim(cross IM) 适用于开发者的即时通讯系统。 - -### 8. symphony - -- **Github地址**:[https://github.com/b3log/symphony](https://github.com/b3log/symphony) -- **star**: 10k -- **介绍**: 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台。 - -### 9. spring-boot - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 32.6k -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - - **关于Spring Boot官方的介绍:** - - > Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”…Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可) - -### 10. arthas - -- **Github地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 9.5k -- **介绍**: Arthas 是Alibaba开源的Java诊断工具。 - -**概览:** - -当你遇到以下类似问题而束手无策时,`Arthas`可以帮助你解决: - -0. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception? -1. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了? -2. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗? -3. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现! -4. 是否有一个全局视角来查看系统的运行状况? -5. 有什么办法可以监控到JVM的实时运行状态? - -`Arthas`支持JDK 6+,支持Linux/Mac/Winodws,采用命令行交互模式,同时提供丰富的 `Tab` 自动补全功能,进一步方便进行问题的定位和诊断。 diff --git a/docs/github-trending/2019-12.md b/docs/github-trending/2019-12.md deleted file mode 100644 index 9a8f79ed6e8..00000000000 --- a/docs/github-trending/2019-12.md +++ /dev/null @@ -1,144 +0,0 @@ -# 年末将至,值得你关注的16个Java 开源项目! - -Star 的数量统计于 2019-12-29。 - -### 1.JavaGuide - -Guide 哥大三开始维护的,目前算是纯 Java 类型项目中 Star 数量最多的项目了。但是,本仓库的价值远远(+N次 )比不上像 Spring Boot、Elasticsearch 等等这样非常非常非常优秀的项目。希望以后我也有能力为这些项目贡献一些有价值的代码。 - -- **Github 地址**: -- **Star**: 66.3k -- **介绍**: 【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 - -### 2.java-design-patterns - -感觉还不错。根据官网介绍: - -> 设计模式是程序员在设计应用程序或系统时可以用来解决常见问题的最佳形式化实践。 设计模式可以通过提供经过测试的,经过验证的开发范例来加快开发过程。 重用设计模式有助于防止引起重大问题的细微问题,并且还可以提高熟悉模式的编码人员和架构师的代码可读性。 - -![java-design-patterns-website](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/java-design-patterns-website.jpg) - -- **Github 地址** : [https://github.com/iluwatar/java-design-patterns](https://github.com/iluwatar/java-design-patterns) -- **Star**: 53.8k -- **介绍**: 用 Java 实现的设计模式。[https://java-design-patterns.com](https://java-design-patterns.com/)。 - -### 3.elasticsearch - -搜索引擎界的扛把子,但不仅仅是搜素引擎那么简单。 - -- **Github 地址** : [https://github.com/elastic/elasticsearch](https://github.com/elastic/elasticsearch) -- **Star**: 46.2k -- **介绍**: 开源,分布式,RESTful 搜索引擎。 - -### 4.spring-boot - -必须好好学啊,一定要好好学!现在 Java 后端新项目有不用 Spring Boot 开发的有吗?如果有的话,请把这个人的联系方式告诉我,我有很多话想给他交流交流! - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 34.8k (1,073 stars this month) -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - -### 5.RxJava - -这个没怎么用过,不做太多评价。 - -- **Github 地址** : [https://github.com/ReactiveX/RxJava](https://github.com/ReactiveX/RxJava) -- **Star**: 41.5k -- **介绍**: `RxJava` 是一个 基于事件流、实现异步操作的库。 - -### 6.advanced-java - -本项目大部分内容来自中华石杉的一个课程,内容涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识,非常不错了! - -- **Github 地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**: 36.7k -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务等领域知识,后端同学必看,前端同学也可学习。 - -### 7.mall - -很牛逼的实战项目,还附有详细的文档,作为毕设或者练手项目都再好不过了。 - -- **Github地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 27.6k -- **介绍**: mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 8.okhttp - -给我感觉是安卓项目中用的居多。当然,Java 后端项目也会经常用,但是一般使用 Spring Boot 进行开发的时候,如果需要远程调用的话建议使用 Spring 封装的 `RestTemplate `。 - -- **Github地址**:[https://github.com/square/okhttp](https://github.com/square/okhttp) -- **star**: 35.4k -- **介绍**: 适用于Android,Kotlin和Java的HTTP客户端。https://square.github.io/okhttp/。 - -### 9.guava - -很厉害很厉害!提供了很多非常实用的工具类、更加实用的集合类、一些常用的数据结构比如布隆过滤器、缓存等等。 - -- **Github地址**:[https://github.com/google/guava](https://github.com/google/guava) -- **star**: 35.3k -- **介绍**: Guava是一组核心库,其中包括新的集合类型(例如 multimap 和 multiset),不可变集合,图形库以及用于并发,I / O,哈希,基元,字符串等的实用程序! - -### 10.Spark - -我木有用过,留下了没有技术的眼泪。 - -- **Github地址**:[https://github.com/apache/spark](https://github.com/apache/spark) -- **star**: 24.7k -- **介绍**: Spark 是一个快速、通用的大规模数据处理引擎,和Hadoop的MapReduce计算框架类似,但是相对于MapReduce,Spark凭借其可伸缩、基于内存计算等特点,以及可以直接读写Hadoop上任何格式数据的优势,进行批处理时更加高效,并有更低的延迟。 - -### 11.arthas - -虽然我自己没有亲身用过,但是身边用过的朋友评价都还挺好的。根据官网介绍,这家伙可以解决下面这些让人脑壳疼的问题: - -1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception? -2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了? -3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗? -4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现! -5. 是否有一个全局视角来查看系统的运行状况? -6. 有什么办法可以监控到JVM的实时运行状态? -7. 怎么快速定位应用的热点,生成火焰图? - -- **Github 地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 18.8 k -- **介绍**: Arthas 是 Alibaba 开源的 Java 诊断工具。 - -### 12.spring-boot-examples - -学习 Spring Boot 必备!配合上我的 **springboot-guide** :[https://github.com/Snailclimb/springboot-guide](https://github.com/Snailclimb/springboot-guide),效果杠杠滴! - -- **Github 地址**:[https://github.com/ityouknow/spring-boot-examples](https://github.com/ityouknow/spring-boot-examples) -- **star**: 20.2 k -- **介绍**: Spring Boot 教程、技术栈示例代码,快速简单上手教程。 - -### 13.lombok - -使用 Lombok 我们可以简化我们的 Java 代码,比如使用它之后我们通过注释就可以实现 getter/setter、equals等方法。 - -- **Github 地址**:[https://github.com/rzwitserloot/lombok](https://github.com/rzwitserloot/lombok) -- **star**: 20.2 k -- **介绍**: 对 Java 编程语言的非常刺激的补充。[https://projectlombok.org/](https://projectlombok.org/) 。 - -### 14.p3c - -与我而言,没有特别惊艳,但是一些提供的一些代码规范确实挺有用的! - -- **Github 地址**:[https://github.com/alibaba/p3c](https://github.com/alibaba/p3c) -- **star**: 19.8 k -- **介绍**: 阿里巴巴Java编码指南pmd实现和IDE插件。 - -### 15.spring-boot-demo - -- **Github 地址**:[https://github.com/xkcoding/spring-boot-demo](https://github.com/xkcoding/spring-boot-demo) -- **Star**: 8.8k -- **介绍**: spring boot demo 是一个用来深度学习并实战 spring boot 的项目。 - -### 16. awesome-java - -Guide 哥半个多月前开始维护的,虽然现在 Star 数量比较少,我相信后面一定会有更多人喜欢上这个项目,我也会继续认真维护下去。 - -- **Github 地址**:[https://github.com/Snailclimb/awesome-java](https://github.com/Snailclimb/awesome-java) -- **Star**: 0.3 k -- **介绍**: Github 上非常棒的 Java 开源项目集合。 - - - diff --git a/docs/github-trending/2019-2.md b/docs/github-trending/2019-2.md deleted file mode 100644 index 51d34b32f78..00000000000 --- a/docs/github-trending/2019-2.md +++ /dev/null @@ -1,64 +0,0 @@ -### 1. JavaGuide - -- **Github地址**: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- **Star**: 27.2k (4,437 stars this month) -- **介绍**: 【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。 - -### 2.DoraemonKit - -- **Github地址**: -- **Star**: 5.2k (3,786 stars this month) -- **介绍**: 简称 "DoKit" 。一款功能齐全的客户端( iOS 、Android )研发助手,你值得拥有。 - -### 3.advanced-java - -- **Github地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**:11.2k (3,042 stars this month) -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲。 - -### 4. spring-boot-examples - -- **Github地址**: -- **star**: 9.6 k (1,764 stars this month) -- **介绍**: Spring Boot 教程、技术栈示例代码,快速简单上手教程。 - -### 5. mall - -- **Github地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 7.4 k (1,736 stars this month) -- **介绍**: mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 6. fescar - -- **Github地址**:[https://github.com/alibaba/fescar](https://github.com/alibaba/fescar) -- **star**: 6.0 k (1,308 stars this month) -- **介绍**: 具有 **高性能** 和 **易用性** 的 **微服务架构** 的 **分布式事务** 的解决方案。(特点:高性能且易于使用,旨在实现简单并快速的事务提交与回滚。) - -### 7. h4cker - -- **Github地址**: -- **star**: 2.1 k (1,303 stars this month) -- **介绍**: 该仓库主要由Omar Santos维护,包括与道德黑客/渗透测试,数字取证和事件响应(DFIR),漏洞研究,漏洞利用开发,逆向工程等相关的资源。 - -### 8. spring-boot - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 34.8k (1,073 stars this month) -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - - **关于Spring Boot官方的介绍:** - - > Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”…Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可) - -### 9. arthas - -- **Github地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 10.5 k (970 stars this month) -- **介绍**: Arthas 是Alibaba开源的Java诊断工具。 - -### 10. tutorials - -- **Github地址**:[https://github.com/eugenp/tutorials](https://github.com/eugenp/tutorials) -- **star**: 12.1 k (789 stars this month) -- **介绍**: 该项目是一系列小而专注的教程 - 每个教程都涵盖Java生态系统中单一且定义明确的开发领域。 当然,它们的重点是Spring Framework - Spring,Spring Boot和Spring Securiyt。 除了Spring之外,还有以下技术:核心Java,Jackson,HttpClient,Guava。 - diff --git a/docs/github-trending/2019-3.md b/docs/github-trending/2019-3.md deleted file mode 100644 index eaed4a5d483..00000000000 --- a/docs/github-trending/2019-3.md +++ /dev/null @@ -1,60 +0,0 @@ -### 1. JavaGuide - -- **Github 地址**: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- **Star**: 32.9k (6,196 stars this month) -- **介绍**: 【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 - -### 2.advanced-java - -- **Github 地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**: 15.1k (4,012 stars this month) -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲。 - -### 3.spring-boot-examples - -- **Github 地址**:[https://github.com/ityouknow/spring-boot-examples](https://github.com/ityouknow/spring-boot-examples) -- **Star**: 12.8k (3,462 stars this month) -- **介绍**: Spring Boot 教程、技术栈示例代码,快速简单上手教程。 - -### 4. mall - -- **Github 地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 9.7 k (2,418 stars this month) -- **介绍**: mall 项目是一套电商系统,包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 5. seata - -- **Github 地址** : [https://github.com/seata/seata](https://github.com/seata/seata) -- **star**: 7.2 k (1359 stars this month) -- **介绍**: Seata 是一种易于使用,高性能,基于 Java 的开源分布式事务解决方案。 - -### 6. quarkus - -- **Github 地址**:[https://github.com/quarkusio/quarkus](https://github.com/quarkusio/quarkus) -- **star**: 12 k (1,224 stars this month) -- **介绍**: Quarkus 是为 GraalVM 和 HotSpot 量身定制的 Kubernetes Native Java 框架,由最佳的 Java 库和标准精心打造而成。Quarkus 的目标是使 Java 成为 Kubernetes 和无服务器环境中的领先平台,同时为开发人员提供统一的反应式和命令式编程模型,以优化地满足更广泛的分布式应用程序架构。 - -### 7. arthas - -- **Github 地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 11.6 k (1,199 stars this month) -- **介绍**: Arthas 是 Alibaba 开源的 Java 诊断工具。 - -### 8.DoraemonKit - -- **Github 地址**: -- **Star**: 6.2k (1,177 stars this month) -- **介绍**: 简称 "DoKit" 。一款功能齐全的客户端( iOS 、Android )研发助手,你值得拥有。 - -### 9.elasticsearch - -- **Github 地址** [https://github.com/elastic/elasticsearch](https://github.com/elastic/elasticsearch) -- **Star**: 39.7k (1,069 stars this month) -- **介绍**: 开源,分布式,RESTful 搜索引擎。 - -### 10. tutorials - -- **Github 地址**:[https://github.com/eugenp/tutorials](https://github.com/eugenp/tutorials) -- **star**: 13 k (998 stars this month) -- **介绍**: 该项目是一系列小而专注的教程 - 每个教程都涵盖 Java 生态系统中单一且定义明确的开发领域。 当然,它们的重点是 Spring Framework - Spring,Spring Boot 和 Spring Securiyt。 除了 Spring 之外,还有以下技术:核心 Java,Jackson,HttpClient,Guava。 - diff --git a/docs/github-trending/2019-4.md b/docs/github-trending/2019-4.md deleted file mode 100644 index 713a76da642..00000000000 --- a/docs/github-trending/2019-4.md +++ /dev/null @@ -1,98 +0,0 @@ -以下涉及到的数据统计与 2019 年 5 月 1 日 12 点,数据来源: 。 - -下面的内容从 Java 学习文档到最热门的框架再到热门的工具应有尽有,比如下面推荐到的开源项目 Hutool 就是近期比较热门的项目之一,它是 Java 工具包,能够帮助我们简化代码!我觉得下面这些项目对于学习 Java 的朋友还是很有帮助的! - - -### 1. JavaGuide - -- **Github 地址**: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- **Star**: 37.9k (5,660 stars this month) -- **介绍**: 【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 - -### 2. advanced-java - -- **Github 地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**: 15.1k (4,654 stars this month) -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲。 - -### 3. CS-Notes - -- **Github 地址**: -- **Star**: 59.2k (4,012 stars this month) -- **介绍**: 技术面试必备基础知识。 - -### 4. ghidra - -- **Github 地址**: -- **Star**: 15.0k (2,995 stars this month) -- **介绍**: Ghidra是一个软件逆向工程(SRE)框架。 - -### 5. mall - -- **Github 地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 11.6 k (2,100 stars this month) -- **介绍**: mall 项目是一套电商系统,包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 6. ZXBlog - -- **Github 地址**: -- **star**: 2.1 k (2,086 stars this month) -- **介绍**: 记录各种学习笔记(算法、Java、数据库、并发......)。 - -### 7.DoraemonKit - -- **Github地址**: -- **Star**: 7.6k (1,541 stars this month) -- **介绍**: 简称 "DoKit" 。一款功能齐全的客户端( iOS 、Android )研发助手,你值得拥有。 - -### 8. spring-boot - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 37.3k (1,489 stars this month) -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - -**Spring Boot官方的介绍:** - -> Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”…Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可) - -### 9. spring-boot-examples - -- **Github 地址**:[https://github.com/ityouknow/spring-boot-examples](https://github.com/ityouknow/spring-boot-examples) -- **Star**: 12.8k (1,453 stars this month) -- **介绍**: Spring Boot 教程、技术栈示例代码,快速简单上手教程。 - -### 10. seata - -- **Github 地址** : [https://github.com/seata/seata](https://github.com/seata/seata) -- **star**: 8.4 k (1441 stars this month) -- **介绍**: Seata 是一种易于使用,高性能,基于 Java 的开源分布式事务解决方案。 - -### 11. litemall - -- **Github 地址**:[https://github.com/ityouknow/spring-boot-examples](https://github.com/ityouknow/spring-boot-examples) -- **Star**: 6.0k (1,427 stars this month) -- **介绍**: 又一个小商城。litemall = Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端。 - -### 12. skywalking - -- **Github 地址**: -- **Star**: 8.0k (1,381 stars this month) -- **介绍**: 针对分布式系统的应用性能监控,尤其是针对微服务、云原生和面向容器的分布式系统架构。 - -### 13. elasticsearch - -- **Github 地址** [https://github.com/elastic/elasticsearch](https://github.com/elastic/elasticsearch) -- **Star**: 4.0k (1,068stars this month) -- **介绍**: 开源,分布式,RESTful 搜索引擎。 - -### 14. arthas - -- **Github地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 12.6 k (1,080 stars this month) -- **介绍**: Arthas 是Alibaba开源的Java诊断工具。 - -### 15. hutool - -- **Github地址**: -- **star**: 4.5 k (1,031 stars this month) -- **介绍**: Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。官网: 。 \ No newline at end of file diff --git a/docs/github-trending/2019-5.md b/docs/github-trending/2019-5.md deleted file mode 100644 index df327b791fe..00000000000 --- a/docs/github-trending/2019-5.md +++ /dev/null @@ -1,125 +0,0 @@ -以下涉及到的数据统计与 2019 年 6 月 1 日 18 点,数据来源: 。下面推荐的内容从 Java 学习文档到最热门的框架再到热门的工具应有尽有,建议收藏+在看! - -### 1.LeetCodeAnimation - -- **Github 地址**: -- **Star**: 29.0k (11,492 stars this month) -- **介绍**: Demonstrate all the questions on LeetCode in the form of animation.(用动画的形式呈现解LeetCode题目的思路)。 - -### 2.CS-Notes - -- **Github 地址**: -- **Star**: 64.4k (5513 stars this month) -- **介绍**: 技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。 - -### 3.JavaGuide - -- **Github 地址**: -- **Star**: 42.0k (4,442 stars this month) -- **介绍**: 【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 - -### 4.mall - -- **Github 地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 14.6 k (3,086 stars this month) -- **介绍**: mall 项目是一套电商系统,包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 5.advanced-java - -- **Github 地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**: 20.8k (2,394 stars this month) -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲。 - -### 6.spring-boot - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 38.5k (1,339 stars this month) -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - -**Spring Boot官方的介绍:** - -> Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”…Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可) - -### 7. Java - -- **Github 地址**: -- **Star**:14.3k (1,334 stars this month) -- **介绍**: All Algorithms implemented in Java。 - -### 8.server - -- **Github 地址**: -- **star**: 2.2 k (1,275 stars this month) -- **介绍**: 全开源即时通讯(IM)系统。 - -### 9.litemall - -- **Github 地址**: -- **Star**: 7.1k (1,114 stars this month) -- **介绍**: 又一个小商城。litemall = Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端。 - -### 10.Linkage-RecyclerView - -- **Github 地址**: -- **Star**: 10.0k (1,093 stars this month) -- **介绍**: 即使不用饿了么订餐,也请务必收藏好该库!🔥 一行代码即可接入,二级联动订餐列表 - Even if you don't order food by PrubHub, be sure to collect this library, please! 🔥 This secondary linkage list widget can be accessed by only one line of code. Supporting by RecyclerView & AndroidX. - -### 11.toBeTopJavaer - -- **Github 地址** : -- **Star**: 3.3k (1,007 stars this month) -- **介绍**: To Be Top Javaer - Java工程师成神之路 - -### 12.elasticsearch - -- **Github 地址** : [https://github.com/elastic/elasticsearch](https://github.com/elastic/elasticsearch) -- **Star**: 48.0k (968 stars this month) -- **介绍**: 开源,分布式,RESTful 搜索引擎。 - -### 13.java-design-patterns - -- **Github 地址** : -- **Star**: 41.5k (955 stars this month) -- **介绍**: Design patterns implemented in Java。 - -### 14.apollo - -- **Github 地址** : -- **Star**: 14.5k (927 stars this month) -- **介绍**: Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。 - -### 15.arthas - -- **Github地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 13.5 k (933 stars this month) -- **介绍**: Arthas 是Alibaba开源的Java诊断工具。 - -### 16.dubbo - -- **Github地址**: -- **star**: 26.9 k (769 stars this month) -- **介绍**: Apache Dubbo是一个基于Java的高性能开源RPC框架。 - -### 17.DoraemonKit - -- **Github地址**: -- **Star**: 8.5k (909 stars this month) -- **介绍**: 简称 "DoKit" 。一款功能齐全的客户端( iOS 、Android )研发助手,你值得拥有。 - -### 18.halo - -- **Github地址**: -- **Star**: 4.1k (829 stars this month) -- **介绍**: Halo 可能是最好的 Java 博客系统。 - -### 19.seata - -- **Github 地址** : [https://github.com/seata/seata](https://github.com/seata/seata) -- **star**: 9.2 k (776 stars this month) -- **介绍**: Seata 是一种易于使用,高性能,基于 Java 的开源分布式事务解决方案。 - -### 20.hutool - -- **Github地址**: -- **star**: 5,3 k (812 stars this month) -- **介绍**: Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。官网: 。 \ No newline at end of file diff --git a/docs/github-trending/2019-6.md b/docs/github-trending/2019-6.md deleted file mode 100644 index 2a395e160d6..00000000000 --- a/docs/github-trending/2019-6.md +++ /dev/null @@ -1,119 +0,0 @@ -### 1.CS-Notes - -- **Github 地址**:https://github.com/CyC2018/CS-Notes -- **Star**: 69.8k -- **介绍**: 技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。 - -### 2.toBeTopJavaer - -- **Github 地址:**[https://github.com/hollischuang/toBeTopJavaer](https://github.com/hollischuang/toBeTopJavaer) -- **Star**: 4.7k -- **介绍**: To Be Top Javaer - Java工程师成神之路。 - -### 3.p3c - -- **Github 地址:** [https://github.com/alibaba/p3c](https://github.com/alibaba/p3c) -- **Star**: 16.6k -- **介绍**: Alibaba Java Coding Guidelines pmd implements and IDE plugin。Eclipse 和 IDEA 上都有该插件,推荐使用! - -### 4.SpringCloudLearning - -- **Github 地址:** [https://github.com/forezp/SpringCloudLearning](https://github.com/forezp/SpringCloudLearning) -- **Star**: 8.7k -- **介绍**: 史上最简单的Spring Cloud教程源码。 - -### 5.dubbo - -- **Github地址**: -- **star**: 27.6 k -- **介绍**: Apache Dubbo是一个基于Java的高性能开源RPC框架。 - -### 6.jeecg-boot - -- **Github地址**: [https://github.com/zhangdaiscott/jeecg-boot](https://github.com/zhangdaiscott/jeecg-boot) -- **star**: 3.3 k -- **介绍**: 一款基于代码生成器的JAVA快速开发平台!全新架构前后端分离:SpringBoot 2.x,Ant Design&Vue,Mybatis,Shiro,JWT。强大的代码生成器让前后端代码一键生成,无需写任何代码,绝对是全栈开发福音!! JeecgBoot的宗旨是提高UI能力的同时,降低前后分离的开发成本,JeecgBoot还独创在线开发模式,No代码概念,一系列在线智能开发:在线配置表单、在线配置报表、在线设计流程等等。 - -### 7.advanced-java - -- **Github 地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**: 24.2k -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务等领域知识,后端同学必看,前端同学也可学习。 - -### 8.FEBS-Shiro - -- **Github 地址**:[https://github.com/wuyouzhuguli/FEBS-Shiro](https://github.com/wuyouzhuguli/FEBS-Shiro) -- **Star**: 2.6k -- **介绍**: Spring Boot 2.1.3,Shiro1.4.0 & Layui 2.5.4 权限管理系统。预览地址:http://49.234.20.223:8080/login。 - -### 9.SpringAll - -- **Github 地址**: [https://github.com/wuyouzhuguli/SpringAll](https://github.com/wuyouzhuguli/SpringAll) -- **Star**: 5.4k -- **介绍**: 循序渐进,学习Spring Boot、Spring Boot & Shiro、Spring Cloud、Spring Security & Spring Security OAuth2,博客Spring系列源码。 - -### 10.JavaGuide - -- **Github 地址**: -- **Star**: 47.2k -- **介绍**: 【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 - -### 11.vhr - -- **Github 地址**:[https://github.com/lenve/vhr](https://github.com/lenve/vhr) -- **Star**: 4.9k -- **介绍**: 微人事是一个前后端分离的人力资源管理系统,项目采用SpringBoot+Vue开发。 - -### 12. tutorials - -- **Github 地址**:[https://github.com/eugenp/tutorials](https://github.com/eugenp/tutorials) -- **star**: 15.4 k -- **介绍**: 该项目是一系列小而专注的教程 - 每个教程都涵盖 Java 生态系统中单一且定义明确的开发领域。 当然,它们的重点是 Spring Framework - Spring,Spring Boot 和 Spring Securiyt。 除了 Spring 之外,还有以下技术:核心 Java,Jackson,HttpClient,Guava。 - -### 13.EasyScheduler - -- **Github 地址**:[https://github.com/analysys/EasyScheduler](https://github.com/analysys/EasyScheduler) -- **star**: 1.1 k -- **介绍**: Easy Scheduler是一个分布式工作流任务调度系统,主要解决“复杂任务依赖但无法直接监控任务健康状态”的问题。Easy Scheduler以DAG方式组装任务,可以实时监控任务的运行状态。同时,它支持重试,重新运行等操作... 。https://analysys.github.io/easyscheduler_docs_cn/ - -### 14.thingsboard - -- **Github 地址**:[https://github.com/thingsboard/thingsboard](https://github.com/thingsboard/thingsboard) -- **star**: 3.7 k -- **介绍**: 开源物联网平台 - 设备管理,数据收集,处理和可视化。 [https://thingsboard.io](https://thingsboard.io/) - -### 15.mall-learning - -- **Github 地址**: [https://github.com/macrozheng/mall-learning](https://github.com/macrozheng/mall-learning) -- **star**: 0.6 k -- **介绍**: mall学习教程,架构、业务、技术要点全方位解析。mall项目(16k+star)是一套电商系统,使用现阶段主流技术实现。 涵盖了SpringBoot2.1.3、MyBatis3.4.6、Elasticsearch6.2.2、RabbitMQ3.7.15、Redis3.2、Mongodb3.2、Mysql5.7等技术,采用Docker容器化部署。 https://github.com/macrozheng/mall - -### 16. flink - -- **Github地址**:[https://github.com/apache/flink](https://github.com/apache/flink) -- **star**: 9.3 k -- **介绍**: Apache Flink是一个开源流处理框架,具有强大的流和批处理功能。 - -### 17.spring-cloud-kubernetes - -- **Github地址**:[https://github.com/spring-cloud/spring-cloud-kubernetes](https://github.com/spring-cloud/spring-cloud-kubernetes) -- **star**: 1.4 k -- **介绍**: Kubernetes 集成 Spring Cloud Discovery Client, Configuration, etc... - -### 18.springboot-learning-example - -- **Github地址**:[https://github.com/JeffLi1993/springboot-learning-example](https://github.com/JeffLi1993/springboot-learning-example) -- **star**: 10.0 k -- **介绍**: spring boot 实践学习案例,是 spring boot 初学者及核心技术巩固的最佳实践。 - -### 19.canal - -- **Github地址**:[https://github.com/alibaba/canal](https://github.com/alibaba/canal) -- **star**: 9.3 k -- **介绍**: 阿里巴巴 MySQL binlog 增量订阅&消费组件。 - -### 20.react-native-device-info - -- **Github地址**:[https://github.com/react-native-community/react-native-device-info](https://github.com/react-native-community/react-native-device-info) -- **star**: 4.0 k -- **介绍**: React Native iOS和Android的设备信息。 \ No newline at end of file diff --git a/docs/github-trending/JavaGithubTrending.md b/docs/github-trending/JavaGithubTrending.md deleted file mode 100644 index 91d544ed37e..00000000000 --- a/docs/github-trending/JavaGithubTrending.md +++ /dev/null @@ -1,8 +0,0 @@ -- [2018 年 12 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2018-12.md) -- [2019 年 1 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-1.md) -- [2019 年 2 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-2.md) -- [2019 年 3 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-3.md) -- [2019 年 4 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-4.md) -- [2019 年 5 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-5.md) -- [2019 年 6 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-6.md) - diff --git a/docs/high-availability/limit-request.md b/docs/high-availability/limit-request.md new file mode 100644 index 00000000000..1c611e55a41 --- /dev/null +++ b/docs/high-availability/limit-request.md @@ -0,0 +1,203 @@ +# 限流 + +## 何为限流?为什么要限流? + +针对软件系统来说,限流就是对请求的速率进行限制,避免瞬时的大量请求击垮软件系统。毕竟,软件系统的处理能力是有限的。如果说超过了其处理能力的范围,软件系统可能直接就挂掉了。 + +限流可能会导致用户的请求无法被正确处理,不过,这往往也是权衡了软件系统的稳定性之后得到的最优解。 + +现实生活中,处处都有限流的实际应用,就比如排队买票是为了避免大量用户涌入购票而导致售票员无法处理。 + +![排队示意图](https://img-blog.csdnimg.cn/f9f17071fc4d489d85d2a234fb298df1.png) + +## 常见限流算法 + +简单介绍 4 种非常好理解并且容易实现的限流算法! + +> 图片来源于 InfoQ 的一篇文章[《分布式服务限流实战,已经为你排好坑了》](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673)。 + +### 固定窗口计数器算法 + +固定窗口其实就是时间窗口。**固定窗口计数器算法** 规定了我们单位时间处理的请求数量。 + +假如我们规定系统中某个接口 1 分钟只能访问 33 次的话,使用固定窗口计数器算法的实现思路如下: + +- 给定一个变量 `counter` 来记录当前接口处理的请求数量,初始值为 0(代表接口当前 1 分钟内还未处理请求)。 +- 1 分钟之内每处理一个请求之后就将 `counter+1` ,当 `counter=33` 之后(也就是说在这 1 分钟内接口已经被访问 33 次的话),后续的请求就会被全部拒绝。 +- 等到 1 分钟结束后,将 `counter` 重置 0,重新开始计数。 + +**这种限流算法无法保证限流速率,因而无法保证突然激增的流量。** + +就比如说我们限制某个接口 1 分钟只能访问 1000 次,该接口的 QPS 为 500,前 55s 这个接口 1 个请求没有接收,后 1s 突然接收了 1000 个请求。然后,在当前场景下,这 1000 个请求在 1s 内是没办法被处理的,系统直接就被瞬时的大量请求给击垮了。 + +![固定窗口计数器算法](https://static001.infoq.cn/resource/image/8d/15/8ded7a2b90e1482093f92fff555b3615.png) + +### 滑动窗口计数器算法 + +**滑动窗口计数器算法** 算的上是固定窗口计数器算法的升级版。 + +滑动窗口计数器算法相比于固定窗口计数器算法的优化在于:**它把时间以一定比例分片** 。 + +例如我们的借口限流每分钟处理 60 个请求,我们可以把 1 分钟分为 60 个窗口。每隔 1 秒移动一次,每个窗口一秒只能处理 不大于 `60(请求数)/60(窗口数)` 的请求, 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。 + +很显然, **当滑动窗口的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。** + +![滑动窗口计数器算法](https://static001.infoq.cn/resource/image/ae/15/ae4d3cd14efb8dc7046d691c90264715.png) + +### 漏桶算法 + +我们可以把发请求的动作比作成注水到桶中,我们处理请求的过程可以比喻为漏桶漏水。我们往桶中以任意速率流入水,以一定速率流出水。当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。 + +如果想要实现这个算法的话也很简单,准备一个队列用来保存请求,然后我们定期从队列中拿请求来执行就好了(和消息队列削峰/限流的思想是一样的)。 + +![漏桶算法](https://static001.infoq.cn/resource/image/75/03/75938d1010138ce66e38c6ed0392f103.png) + +### 令牌桶算法 + +令牌桶算法也比较简单。和漏桶算法算法一样,我们的主角还是桶(这限流算法和桶过不去啊)。不过现在桶里装的是令牌了,请求在被处理之前需要拿到一个令牌,请求处理完毕之后将这个令牌丢弃(删除)。我们根据限流大小,按照一定的速率往桶里添加令牌。如果桶装满了,就不能继续往里面继续添加令牌了。 + +![令牌桶算法](https://static001.infoq.cn/resource/image/ec/93/eca0e5eaa35dac938c673fecf2ec9a93.png) + +## 单机限流 + +单机限流可以直接使用 Google Guava 自带的限流工具类 `RateLimiter` 。 `RateLimiter` 基于令牌桶算法,可以应对突发流量。 + +> Guava 地址:https://github.com/google/guava + +除了最基本的令牌桶算法(平滑突发限流)实现之外,Guava 的`RateLimiter`还提供了 **平滑预热限流** 的算法实现。 + +平滑突发限流就是按照指定的速率放令牌到桶里,而平滑预热限流会有一段预热时间,预热时间之内,速率会逐渐提升到配置的速率。 + +我们下面通过两个简单的小例子来详细了解吧! + +我们直接在项目中引入 Guava 相关的依赖即可使用。 + +```xml + + com.google.guava + guava + 31.0.1-jre + +``` + +下面是一个简单的 Guava 平滑突发限流的 Demo。 + +```java +import com.google.common.util.concurrent.RateLimiter; + +/** + * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * + * @author Guide哥 + * @date 2021/10/08 19:12 + **/ +public class RateLimiterDemo { + + public static void main(String[] args) { + // 1s 放 5 个令牌到桶里也就是 0.2s 放 1个令牌到桶里 + RateLimiter rateLimiter = RateLimiter.create(5); + for (int i = 0; i < 10; i++) { + double sleepingTime = rateLimiter.acquire(1); + System.out.printf("get 1 tokens: %ss%n", sleepingTime); + } + } +} + +``` + +输出: + +```bash +get 1 tokens: 0.0s +get 1 tokens: 0.188413s +get 1 tokens: 0.197811s +get 1 tokens: 0.198316s +get 1 tokens: 0.19864s +get 1 tokens: 0.199363s +get 1 tokens: 0.193997s +get 1 tokens: 0.199623s +get 1 tokens: 0.199357s +get 1 tokens: 0.195676s +``` + +下面是一个简单的 Guava 平滑预热限流的 Demo。 + +```java +import com.google.common.util.concurrent.RateLimiter; +import java.util.concurrent.TimeUnit; + +/** + * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * + * @author Guide哥 + * @date 2021/10/08 19:12 + **/ +public class RateLimiterDemo { + + public static void main(String[] args) { + // 1s 放 5 个令牌到桶里也就是 0.2s 放 1个令牌到桶里 + // 预热时间为3s,也就说刚开始的 3s 内发牌速率会逐渐提升到 0.2s 放 1 个令牌到桶里 + RateLimiter rateLimiter = RateLimiter.create(5, 3, TimeUnit.SECONDS); + for (int i = 0; i < 20; i++) { + double sleepingTime = rateLimiter.acquire(1); + System.out.printf("get 1 tokens: %sds%n", sleepingTime); + } + } +} +``` + +输出: + +```bash +get 1 tokens: 0.0s +get 1 tokens: 0.561919s +get 1 tokens: 0.516931s +get 1 tokens: 0.463798s +get 1 tokens: 0.41286s +get 1 tokens: 0.356172s +get 1 tokens: 0.300489s +get 1 tokens: 0.252545s +get 1 tokens: 0.203996s +get 1 tokens: 0.198359s +``` + +另外,**Bucket4j** 是一个非常不错的基于令牌/漏桶算法的限流库。 + +> Bucket4j 地址:https://github.com/vladimir-bukhtoyarov/bucket4j + +相对于,Guava 的限流工具类来说,Bucket4j 提供的限流功能更加全面。不仅支持单机限流和分布式限流,还可以集成监控,搭配 Prometheus 和 Grafana 使用。 + +不过,毕竟 Guava 也只是一个功能全面的工具类库,其提供的开箱即用的限流功能在很多单机场景下还是比较实用的。 + +Spring Cloud Gateway 中自带的单机限流的早期版本就是基于 Bucket4j 实现的。后来,替换成了 **Resilience4j**。 + +Resilience4j 是一个轻量级的容错组件,其灵感来自于 Hystrix。自[Netflix 宣布不再积极开发 Hystrix](https://github.com/Netflix/Hystrix/commit/a7df971cbaddd8c5e976b3cc5f14013fe6ad00e6) 之后,Spring 官方和 Netflix 都更推荐使用 Resilience4j 来做限流熔断。 + +> Resilience4j 地址: https://github.com/resilience4j/resilience4j + +一般情况下,为了保证系统的高可用,项目的限流和熔断都是要一起做的。 + +Resilience4j 不仅提供限流,还提供了熔断、负载保护、自动重试等保障系统高可用开箱即用的功能。并且,Resilience4j 的生态也更好,很多网关都使用 Resilience4j 来做限流熔断的。 + +因此,在绝大部分场景下 Resilience4j 或许会是更好的选择。如果是一些比较简单的限流场景的话,Guava 或者 Bucket4j 也是不错的选择。 + +## 分布式限流 + +分布式限流常见的方案: + +- **借助中间件架限流** :可以借助 Sentinel 或者使用 Redis 来自己实现对应的限流逻辑。 +- **网关层限流** :比较常用的一种方案,直接在网关层把限流给安排上了。不过,通常网关层限流通常也需要借助到中间件/框架。就比如 Spring Cloud Gateway 的分布式限流实现`RedisRateLimiter`就是基于 Redis+Lua 来实现的,再比如 Spring Cloud Gateway 还可以整合 Sentinel 来做限流。 + +如果你要基于 Redis 来手动实现限流逻辑的话,建议配合 Lua 脚本来做。 + +网上也有很多现成的脚本供你参考,就比如 Apache 网关项目 ShenYu 的 RateLimiter 限流插件就基于 Redis + Lua 实现了令牌桶算法/并发令牌桶算法、漏桶算法、滑动窗口算法。 + +> ShenYu 地址: https://github.com/apache/incubator-shenyu + +![](https://img-blog.csdnimg.cn/e1e2a75f489e4854990dabe3b6cec522.png) + +## 相关阅读 + +- 服务治理之轻量级熔断框架 Resilience4j :https://xie.infoq.cn/article/14786e571c1a4143ad1ef8f19 +- 超详细的 Guava RateLimiter 限流原理解析:https://cloud.tencent.com/developer/article/1408819 +- 实战 Spring Cloud Gateway 之限流篇 👍:https://www.aneasystone.com/archives/2020/08/spring-cloud-gateway-current-limiting.html diff --git "a/docs/high-availability/\346\200\247\350\203\275\346\265\213\350\257\225.md" "b/docs/high-availability/\346\200\247\350\203\275\346\265\213\350\257\225.md" new file mode 100644 index 00000000000..dc3ff9ba749 --- /dev/null +++ "b/docs/high-availability/\346\200\247\350\203\275\346\265\213\350\257\225.md" @@ -0,0 +1,150 @@ +# 性能测试入门 + +性能测试一般情况下都是由测试这个职位去做的,那还需要我们开发学这个干嘛呢?了解性能测试的指标、分类以及工具等知识有助于我们更好地去写出性能更好的程序,另外作为开发这个角色,如果你会性能测试的话,相信也会为你的履历加分不少。 + +这篇文章是我会结合自己的实际经历以及在测试这里取的经所得,除此之外,我还借鉴了一些优秀书籍,希望对你有帮助。 + +本文思维导图: + + + +## 一 不同角色看网站性能 + +### 1.1 用户 + +当用户打开一个网站的时候,最关注的是什么?当然是网站响应速度的快慢。比如我们点击了淘宝的主页,淘宝需要多久将首页的内容呈现在我的面前,我点击了提交订单按钮需要多久返回结果等等。 + +所以,用户在体验我们系统的时候往往根据你的响应速度的快慢来评判你的网站的性能。 + +### 1.2 开发人员 + +用户与开发人员都关注速度,这个速度实际上就是我们的系统**处理用户请求的速度**。 + +开发人员一般情况下很难直观的去评判自己网站的性能,我们往往会根据网站当前的架构以及基础设施情况给一个大概的值,比如: + +1. 项目架构是分布式的吗? +2. 用到了缓存和消息队列没有? +3. 高并发的业务有没有特殊处理? +4. 数据库设计是否合理? +5. 系统用到的算法是否还需要优化? +6. 系统是否存在内存泄露的问题? +7. 项目使用的 Redis 缓存多大?服务器性能如何?用的是机械硬盘还是固态硬盘? +8. ...... + +### 1.3 测试人员 + +测试人员一般会根据性能测试工具来测试,然后一般会做出一个表格。这个表格可能会涵盖下面这些重要的内容: + +1. 响应时间; +2. 请求成功率; +3. 吞吐量; +4. ...... + +### 1.4 运维人员 + +运维人员会倾向于根据基础设施和资源的利用率来判断网站的性能,比如我们的服务器资源使用是否合理、数据库资源是否存在滥用的情况、当然,这是传统的运维人员,现在 Devpos 火起来后,单纯干运维的很少了。我们这里暂且还保留有这个角色。 + +## 二 性能测试需要注意的点 + +几乎没有文章在讲性能测试的时候提到这个问题,大家都会讲如何去性能测试,有哪些性能测试指标这些东西。 + +### 2.1 了解系统的业务场景 + +**性能测试之前更需要你了解当前的系统的业务场景。** 对系统业务了解的不够深刻,我们很容易犯测试方向偏执的错误,从而导致我们忽略了对系统某些更需要性能测试的地方进行测试。比如我们的系统可以为用户提供发送邮件的功能,用户配置成功邮箱后只需输入相应的邮箱之后就能发送,系统每天大概能处理上万次发邮件的请求。很多人看到这个可能就直接开始使用相关工具测试邮箱发送接口,但是,发送邮件这个场景可能不是当前系统的性能瓶颈,这么多人用我们的系统发邮件, 还可能有很多人一起发邮件,单单这个场景就这么人用,那用户管理可能才是性能瓶颈吧! + +### 2.2 历史数据非常有用 + +当前系统所留下的历史数据非常重要,一般情况下,我们可以通过相应的些历史数据初步判定这个系统哪些接口调用的比较多、哪些 service 承受的压力最大,这样的话,我们就可以针对这些地方进行更细致的性能测试与分析。 + +另外,这些地方也就像这个系统的一个短板一样,优化好了这些地方会为我们的系统带来质的提升。 + +### 三 性能测试的指标 + +### 3.1 响应时间 + +**响应时间就是用户发出请求到用户收到系统处理结果所需要的时间。** 重要吗?实在太重要! + +比较出名的 2-5-8 原则是这样描述的:通常来说,2到5秒,页面体验会比较好,5到8秒还可以接受,8秒以上基本就很难接受了。另外,据统计当网站慢一秒就会流失十分之一的客户。 + +但是,在某些场景下我们也并不需要太看重 2-5-8 原则 ,比如我觉得系统导出导入大数据量这种就不需要,系统生成系统报告这种也不需要。 + +### 3.2 并发数 + +**并发数是系统能同时处理请求的数目即同时提交请求的用户数目。** + +不得不说,高并发是现在后端架构中非常非常火热的一个词了,这个与当前的互联网环境以及中国整体的互联网用户量都有很大关系。一般情况下,你的系统并发量越大,说明你的产品做的就越大。但是,并不是每个系统都需要达到像淘宝、12306 这种亿级并发量的。 + +### 3.3 吞吐量 + +吞吐量指的是系统单位时间内系统处理的请求数量。衡量吞吐量有几个重要的参数:QPS(TPS)、并发数、响应时间。 + +1. QPS(Query Per Second):服务器每秒可以执行的查询次数; +2. TPS(Transaction Per Second):服务器每秒处理的事务数(这里的一个事务可以理解为客户发出请求到收到服务器的过程); +3. 并发数;系统能同时处理请求的数目即同时提交请求的用户数目。 +4. 响应时间: 一般取多次请求的平均响应时间 + +理清他们的概念,就很容易搞清楚他们之间的关系了。 + +- **QPS(TPS)** = 并发数/平均响应时间 +- **并发数** = QPS\平均响应时间 + +书中是这样描述 QPS 和 TPS 的区别的。 + +> QPS vs TPS:QPS 基本类似于 TPS,但是不同的是,对于一个页面的一次访问,形成一个TPS;但一次页面请求,可能产生多次对服务器的请求,服务器对这些请求,就可计入“QPS”之中。如,访问一个页面会请求服务器2次,一次访问,产生一个“T”,产生2个“Q”。 + +### 3.4 性能计数器 + +**性能计数器是描述服务器或者操作系统的一些数据指标如内存使用、CPU使用、磁盘与网络I/O等情况。** + +### 四 几种常见的性能测试 + +### 性能测试 + +性能测试方法是通过测试工具模拟用户请求系统,目的主要是为了测试系统的性能是否满足要求。通俗地说,这种方法就是要在特定的运行条件下验证系统的能力状态。 + +性能测试是你在对系统性能已经有了解的前提之后进行的,并且有明确的性能指标。 + +### 负载测试 + +对被测试的系统继续加大请求压力,直到服务器的某个资源已经达到饱和了,比如系统的缓存已经不够用了或者系统的响应时间已经不满足要求了。 + +负载测试说白点就是测试系统的上线。 + +### 压力测试 + +不去管系统资源的使用情况,对系统继续加大请求压力,直到服务器崩溃无法再继续提供服务。 + +### 稳定性测试 + +模拟真实场景,给系统一定压力,看看业务是否能稳定运行。 + +## 五 常用性能测试工具 + +这里就不多扩展了,有时间的话会单独拎一个熟悉的说一下。 + +### 5.1 后端常用 + +没记错的话,除了 LoadRunner 其他几款性能测试工具都是开源免费的。 + +1. Jmeter :Apache JMeter 是 JAVA 开发的性能测试工具。 +2. LoadRunner:一款商业的性能测试工具。 +3. Galtling :一款基于Scala 开发的高性能服务器性能测试工具。 +4. ab :全称为 Apache Bench 。Apache 旗下的一款测试工具,非常实用。 + +### 5.2 前端常用 + +1. Fiddler:抓包工具,它可以修改请求的数据,甚至可以修改服务器返回的数据,功能非常强大,是Web 调试的利器。 +2. HttpWatch: 可用于录制HTTP请求信息的工具。 + +## 六 常见的性能优化策略 + +性能优化之前我们需要对请求经历的各个环节进行分析,排查出可能出现性能瓶颈的地方,定位问题。 + +下面是一些性能优化时,我经常拿来自问的一些问题: + +1. 系统是否需要缓存? +2. 系统架构本身是不是就有问题? +3. 系统是否存在死锁的地方? +4. 系统是否存在内存泄漏?(Java 的自动回收内存虽然很方便,但是,有时候代码写的不好真的会造成内存泄漏) +5. 数据库索引使用是否合理? +6. ...... \ No newline at end of file diff --git "a/docs/high-availability/\347\201\276\345\244\207\350\256\276\350\256\241\345\222\214\345\274\202\345\234\260\345\244\232\346\264\273.md" "b/docs/high-availability/\347\201\276\345\244\207\350\256\276\350\256\241\345\222\214\345\274\202\345\234\260\345\244\232\346\264\273.md" new file mode 100644 index 00000000000..18756b69127 --- /dev/null +++ "b/docs/high-availability/\347\201\276\345\244\207\350\256\276\350\256\241\345\222\214\345\274\202\345\234\260\345\244\232\346\264\273.md" @@ -0,0 +1,14 @@ +# 灾备设计&异地多活 + +**灾备** = 容灾+备份。 + +- **备份** : 将系统所产生的所有重要数据多备份几份。 +- **容灾** : 在异地建立两个完全相同的系统。当某个地方的系统突然挂掉,整个应用系统可以切换到另一个,这样系统就可以正常提供服务了。 + +**异地多活** 描述的是将服务部署在异地并且服务同时对外提供服务。和传统的灾备设计的最主要区别在于“多活”,即所有站点都是同时在对外提供服务的。异地多活是为了应对突发状况比如火灾、地震等自然或者人为灾害。 + +相关阅读: + +- [搞懂异地多活,看这篇就够了](https://mp.weixin.qq.com/s/T6mMDdtTfBuIiEowCpqu6Q) +- [四步构建异地多活](https://mp.weixin.qq.com/s/hMD-IS__4JE5_nQhYPYSTg) +- [《从零开始学架构》— 28 | 业务高可用的保障:异地多活架构](http://gk.link/a/10pKZ) \ No newline at end of file diff --git "a/docs/high-availability/\350\266\205\346\227\266\345\222\214\351\207\215\350\257\225\346\234\272\345\210\266.md" "b/docs/high-availability/\350\266\205\346\227\266\345\222\214\351\207\215\350\257\225\346\234\272\345\210\266.md" new file mode 100644 index 00000000000..ee4f90f2056 --- /dev/null +++ "b/docs/high-availability/\350\266\205\346\227\266\345\222\214\351\207\215\350\257\225\346\234\272\345\210\266.md" @@ -0,0 +1,5 @@ +# 超时&重试机制 + +**一旦用户的请求超过某个时间得不到响应就结束此次请求并抛出异常。** 如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法再处理请求。 + +另外,重试的次数一般设为 3 次,再多次的重试没有好处,反而会加重服务器压力(部分场景使用失败重试机制会不太适合)。 \ No newline at end of file diff --git "a/docs/high-availability/\351\231\215\347\272\247&\347\206\224\346\226\255.md" "b/docs/high-availability/\351\231\215\347\272\247&\347\206\224\346\226\255.md" new file mode 100644 index 00000000000..2ff7b922893 --- /dev/null +++ "b/docs/high-availability/\351\231\215\347\272\247&\347\206\224\346\226\255.md" @@ -0,0 +1,9 @@ +# 降级&熔断 + +降级是从系统功能优先级的角度考虑如何应对系统故障。 + +服务降级指的是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。 + +熔断和降级是两个比较容易混淆的概念,两者的含义并不相同。 + +降级的目的在于应对系统自身的故障,而熔断的目的在于应对当前系统依赖的外部系统或者第三方系统的故障。 \ No newline at end of file diff --git "a/docs/high-availability/\351\233\206\347\276\244.md" "b/docs/high-availability/\351\233\206\347\276\244.md" new file mode 100644 index 00000000000..5da34020f32 --- /dev/null +++ "b/docs/high-availability/\351\233\206\347\276\244.md" @@ -0,0 +1,3 @@ +# 集群 + +相同的服务部署多份,避免单点故障。 \ No newline at end of file diff --git "a/docs/system-design/website-architecture/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\357\274\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271\357\274\237.md" "b/docs/high-availability/\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241.md" similarity index 89% rename from "docs/system-design/website-architecture/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\357\274\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271\357\274\237.md" rename to "docs/high-availability/\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241.md" index cc24d0bd072..e336f676251 100644 --- "a/docs/system-design/website-architecture/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\357\274\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271\357\274\237.md" +++ "b/docs/high-availability/\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241.md" @@ -1,8 +1,10 @@ +# 高可用系统设计 + 一篇短小的文章,面试经常遇到的这个问题。本文主要包括下面这些内容: 1. 高可用的定义 2. 哪些情况可能会导致系统不可用? -3. 有些提高系统可用性的方法?只是简单的提一嘴,更具体内容在后续的文章中介绍,就拿限流来说,你需要搞懂:何为限流?如何限流?为什么要限流?如何做呢?说一下原理?。 +3. 有哪些提高系统可用性的方法?只是简单的提一嘴,更具体内容在后续的文章中介绍,就拿限流来说,你需要搞懂:何为限流?如何限流?为什么要限流?如何做呢?说一下原理?。 ## 什么是高可用?可用性的判断标准是啥? @@ -36,7 +38,7 @@ ### 2.使用集群,减少单点故障 -先拿常用的 Redis 举个例子!我们如何保证我们的 Redis 缓存高可用呢?答案就是使用集群,避免单点故障。当我们使用一个 Redis 实例作为缓存的时候,这个 Redis 实例挂了之后,整个缓存服务可能就挂了。使用了集群之后,即使一台 Redis 实例,不到一秒就会有另外一台 Redis 实例顶上。 +先拿常用的 Redis 举个例子!我们如何保证我们的 Redis 缓存高可用呢?答案就是使用集群,避免单点故障。当我们使用一个 Redis 实例作为缓存的时候,这个 Redis 实例挂了之后,整个缓存服务可能就挂了。使用了集群之后,即使一台 Redis 实例挂了,不到一秒就会有另外一台 Redis 实例顶上。 ### 3.限流 @@ -44,11 +46,11 @@ ### 4.超时和重试机制设置 -一旦用户请求超过某个时间的得不到响应,就抛出异常。这个是非常重要的,很多线上系统故障都是因为没有进行超时设置或者超时设置的方式不对导致的。我们在读取第三方服务的时候,尤其适合设置超时和重试机制。一般我们使用一些 RPC 框架的时候,这些框架都自带的超时重试的配置。如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法在处理请求。重试的次数一般设为 3 次,再多次的重试没有好处,反而会加重服务器压力(部分场景使用失败重试机制会不太适合)。 +一旦用户请求超过某个时间的得不到响应,就抛出异常。这个是非常重要的,很多线上系统故障都是因为没有进行超时设置或者超时设置的方式不对导致的。我们在读取第三方服务的时候,尤其适合设置超时和重试机制。一般我们使用一些 RPC 框架的时候,这些框架都自带的超时重试的配置。如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法再处理请求。重试的次数一般设为 3 次,再多次的重试没有好处,反而会加重服务器压力(部分场景使用失败重试机制会不太适合)。 ### 5.熔断机制 -超时和重试机制设置之外,熔断机制也是很重要的。 熔断机制说的是系统自动收集所依赖服务的资源使用情况和性能指标,当所依赖的服务恶化或者调用失败次数达到某个阈值的时候就迅速失败,让当前系统立即切换依赖其他备用服务。 比较常用的是流量控制和熔断降级框架是 Netflix 的 Hystrix 和 alibaba 的 Sentinel。 +超时和重试机制设置之外,熔断机制也是很重要的。 熔断机制说的是系统自动收集所依赖服务的资源使用情况和性能指标,当所依赖的服务恶化或者调用失败次数达到某个阈值的时候就迅速失败,让当前系统立即切换依赖其他备用服务。 比较常用的流量控制和熔断降级框架是 Netflix 的 Hystrix 和 alibaba 的 Sentinel。 ### 6.异步调用 @@ -66,7 +68,3 @@ 4. **灰度发布:** 将服务器集群分成若干部分,每天只发布一部分机器,观察运行稳定没有故障,第二天继续发布一部分机器,持续几天才把整个集群全部发布完毕,期间如果发现问题,只需要回滚已发布的一部分服务器即可 5. **定期检查/更换硬件:** 如果不是购买的云服务的话,定期还是需要对硬件进行一波检查的,对于一些需要更换或者升级的硬件,要及时更换或者升级。 6. .....(想起来再补充!也欢迎各位欢迎补充!) - -## 总结 - -![如何设计高可用系统?](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/如何设计高可用的系统?.png) \ No newline at end of file diff --git a/docs/system-design/data-communication/kafka-inverview.md "b/docs/high-performance/message-queue/kafka\347\237\245\350\257\206\347\202\271&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" similarity index 86% rename from docs/system-design/data-communication/kafka-inverview.md rename to "docs/high-performance/message-queue/kafka\347\237\245\350\257\206\347\202\271&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" index 3788106f490..8cea44f2225 100644 --- a/docs/system-design/data-communication/kafka-inverview.md +++ "b/docs/high-performance/message-queue/kafka\347\237\245\350\257\206\347\202\271&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" @@ -1,8 +1,5 @@ ------- - - -## Kafka面试题总结 +# Kafka知识点&面试题总结 ### Kafka 是什么?主要应用场景有哪些? @@ -11,7 +8,7 @@ Kafka 是一个分布式流式处理平台。这到底是什么意思呢? 流平台具有三个关键功能: 1. **消息队列**:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。 -2. **容错的持久方式存储记录消息流**: Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险·。 +2. **容错的持久方式存储记录消息流**: Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险。 3. **流式处理平台:** 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。 Kafka 主要有两大应用场景: @@ -21,7 +18,7 @@ Kafka 主要有两大应用场景: ### 和其他消息队列相比,Kafka的优势在哪里? -我们现在经常提到 Kafka 的时候就已经默认它是一个非常优秀的消息队列了,我们也会经常拿它给 RocketMQ、RabbitMQ 对比。我觉得 Kafka 相比其他消息队列主要的优势如下: +我们现在经常提到 Kafka 的时候就已经默认它是一个非常优秀的消息队列了,我们也会经常拿它跟 RocketMQ、RabbitMQ 对比。我觉得 Kafka 相比其他消息队列主要的优势如下: 1. **极致的性能** :基于 Scala 和 Java 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。 2. **生态系统兼容性无可匹敌** :Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域。 @@ -36,13 +33,13 @@ Kafka 主要有两大应用场景: #### 队列模型:早期的消息模型 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/队列模型23.png) +![队列模型](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/队列模型23.png) **使用队列(Queue)作为消息通信载体,满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。** 比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) **队列模型存在的问题:** -假如我们存在这样一种情况:我们需要将生产者产生的消息分发给多个消费者,并且每个消费者都能接收到完成的消息内容。 +假如我们存在这样一种情况:我们需要将生产者产生的消息分发给多个消费者,并且每个消费者都能接收到完整的消息内容。 这种情况,队列模型就不好解决了。很多比较杠精的人就说:我们可以为每个消费者创建一个单独的队列,让生产者发送多份。这是一种非常愚蠢的做法,浪费资源不说,还违背了使用消息队列的目的。 @@ -50,7 +47,7 @@ Kafka 主要有两大应用场景: 发布-订阅模型主要是为了解决队列模型存在的问题。 -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/广播模型21312.png) +![发布订阅模型](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85%E6%A8%A1%E5%9E%8B.png) 发布订阅模型(Pub-Sub) 使用**主题(Topic)** 作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者,**在一条消息广播之后才订阅的用户则是收不到该条消息的**。 @@ -104,20 +101,25 @@ ZooKeeper 主要为 Kafka 提供元数据的管理的功能。 从图中我们可以看出,Zookeeper 主要为 Kafka 做了下面这些事情: -1. **Broker 注册** :在 Zookeeper 上会有一个专门**用来进行 Broker 服务器列表记录**的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,即到/brokers/ids 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去 +1. **Broker 注册** :在 Zookeeper 上会有一个专门**用来进行 Broker 服务器列表记录**的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,即到 `/brokers/ids` 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去 2. **Topic 注册** : 在 Kafka 中,同一个**Topic 的消息会被分成多个分区**并将其分布在多个 Broker 上,**这些分区信息及与 Broker 的对应关系**也都是由 Zookeeper 在维护。比如我创建了一个名字为 my-topic 的主题并且它有两个分区,对应到 zookeeper 中会创建这些文件夹:`/brokers/topics/my-topic/Partitions/0`、`/brokers/topics/my-topic/Partitions/1` 3. **负载均衡** :上面也说过了 Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力。 对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。 4. ...... ### Kafka 如何保证消息的消费顺序? -我们在使用消息队列的过程中经常有业务场景需要严格保证消息的消费顺序,比如我们同时发了 2 个消息,这 2 个消息对应的操作分别对应的数据库操作是:更改用户会员等级、根据会员等级计算订单价格。假如这两条消息的消费顺序不一样造成的最终结果就会截然不同。 +我们在使用消息队列的过程中经常有业务场景需要严格保证消息的消费顺序,比如我们同时发了 2 个消息,这 2 个消息对应的操作分别对应的数据库操作是: + +1. 更改用户会员等级。 +2. 根据会员等级计算订单价格。 + +假如这两条消息的消费顺序不一样造成的最终结果就会截然不同。 我们知道 Kafka 中 Partition(分区)是真正保存消息的地方,我们发送的消息都被放在了这里。而我们的 Partition(分区) 又存在于 Topic(主题) 这个概念中,并且我们可以给特定 Topic 指定多个 Partition。 ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/KafkaTopicPartionsLayout.png) -每次添加消息到 Partition(分区) 的时候都会采用尾加法,如上图所示。Kafka 只能为我们保证 Partition(分区) 中的消息有序,而不能保证 Topic(主题) 中的 Partition(分区) 的有序。 +每次添加消息到 Partition(分区) 的时候都会采用尾加法,如上图所示。 **Kafka 只能为我们保证 Partition(分区) 中的消息有序。** > 消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。 @@ -138,7 +140,7 @@ Kafka 中发送 1 条消息的时候,可以指定 topic, partition, key,data 生产者(Producer) 调用`send`方法发送消息之后,消息可能因为网络问题并没有发送过去。 -所以,我们不能默认在调用`send`方法发送消息之后消息消息发送成功了。为了确定消息是发送成功,我们要判断消息发送的结果。但是要注意的是 Kafka 生产者(Producer) 使用 `send` 方法发送消息实际上是异步的操作,我们可以通过 `get()`方法获取调用结果,但是这样也让它变为了同步操作,示例代码如下: +所以,我们不能默认在调用`send`方法发送消息之后消息发送成功了。为了确定消息是发送成功,我们要判断消息发送的结果。但是要注意的是 Kafka 生产者(Producer) 使用 `send` 方法发送消息实际上是异步的操作,我们可以通过 `get()`方法获取调用结果,但是这样也让它变为了同步操作,示例代码如下: > **详细代码见我的这篇文章:[Kafka系列第三篇!10 分钟学会如何在 Spring Boot 程序中使用 Kafka 作为消息队列?](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486269&idx=2&sn=ec00417ad641dd8c3d145d74cafa09ce&chksm=cea244f6f9d5cde0c8eb233fcc4cf82e11acd06446719a7af55230649863a3ddd95f78d111de&token=1633957262&lang=zh_CN#rd)** @@ -170,7 +172,7 @@ if (sendResult.getRecordMetadata() != null) { 当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。 -**解决办法也比较粗暴,我们手动关闭闭自动提交 offset,每次在真正消费完消息之后之后再自己手动提交 offset 。** 但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两次。 +**解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手动提交 offset 。** 但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两次。 #### Kafka 弄丢了消息 @@ -192,7 +194,7 @@ acks 的默认值即为1,代表我们的消息被leader副本接收之后就 一般情况下我们还需要设置 **min.insync.replicas> 1** ,这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。**min.insync.replicas** 的默认值为 1 ,在实际生产中应尽量避免默认值 1。 -但是,为了保证整个 Kafka 服务的高可用性,你需要确保 **replication.factor > min.insync.replicas** 。为什么呢?设想一下加入两者相等的话,只要是有一个副本挂掉,整个分区就无法正常工作了。这明显违反高可用性!一般推荐设置成 **replication.factor = min.insync.replicas + 1**。 +但是,为了保证整个 Kafka 服务的高可用性,你需要确保 **replication.factor > min.insync.replicas** 。为什么呢?设想一下假如两者相等的话,只要是有一个副本挂掉,整个分区就无法正常工作了。这明显违反高可用性!一般推荐设置成 **replication.factor = min.insync.replicas + 1**。 **设置 unclean.leader.election.enable = false** @@ -202,7 +204,17 @@ acks 的默认值即为1,代表我们的消息被leader副本接收之后就 ### Kafka 如何保证消息不重复消费 -代办... +**kafka出现消息重复消费的原因:** + +- 服务端侧已经消费的数据没有成功提交 offset(根本原因)。 +- Kafka 侧 由于服务端处理业务时间长或者网络链接等等原因让 Kafka 认为服务假死,触发了分区 rebalance。 + +**解决方案:** + +- 消费消息服务做幂等校验,比如 Redis 的set、MySQL 的主键等天然的幂等功能。这种方法最有效。 +- 将 **`enable.auto.commit`** 参数设置为 false,关闭自动提交,开发者在代码中手动提交 offset。那么这里会有个问题:**什么时候提交offset合适?** + * 处理完消息再提交:依旧有消息重复消费的风险,和自动提交一样 + * 拉取到消息即提交:会有消息丢失的风险。允许消息延时的场景,一般会采用这种方式。然后,通过定时任务在业务不繁忙(比如凌晨)的时候做数据兜底。 ### Reference diff --git a/docs/high-performance/message-queue/message-queue.md b/docs/high-performance/message-queue/message-queue.md new file mode 100644 index 00000000000..161bd5a021d --- /dev/null +++ b/docs/high-performance/message-queue/message-queue.md @@ -0,0 +1,138 @@ +# 消息队列知识点&面试题总结 + +“RabbitMQ?”“Kafka?”“RocketMQ?”...在日常学习与开发过程中,我们常常听到消息队列这个关键词。我也在我的多篇文章中提到了这个概念。可能你是熟练使用消息队列的老手,又或者你是不懂消息队列的新手,不论你了不了解消息队列,本文都将带你搞懂消息队列的一些基本理论。如果你是老手,你可能从本文学到你之前不曾注意的一些关于消息队列的重要概念,如果你是新手,相信本文将是你打开消息队列大门的一板砖。 + +## 一 什么是消息队列 + +我们可以把消息队列看作是一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。 + +![Message queue](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/message-queue-small.png) + +消息队列是分布式系统中重要的组件之一。使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。 + +我们知道队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。 + +## 二 为什么要用消息队列 + +通常来说,使用消息队列能为我们的系统带来下面三点好处: + +1. **通过异步处理提高系统性能(减少响应所需时间)。** +2. **削峰/限流** +3. **降低系统耦合性。** + +如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。 + +《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。 + +### 2.1 通过异步处理提高系统性能(减少响应所需时间) + +![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Asynchronous-message-queue.png) + +将用户的请求数据存储到消息队列之后就立即返回结果。随后,系统再对消息进行消费。 + +因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此,**使用消息队列进行异步处理之后,需要适当修改业务流程进行配合**,比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 + +### 2.2 削峰/限流 + +**先将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。** + +举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: + +![削峰](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/削峰-消息队列.png) + +### 2.3 降低系统耦合性 + +使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧: + +![解耦](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/消息队列-解耦.png) + +生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合,这显然也提高了系统的扩展性。 + +**消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 + +消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 + +另外,为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。 + +**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的。除了发布-订阅模式,还有点对点订阅模式(一个消息只有一个消费者),我们比较常用的是发布-订阅模式。另外,这两种消息模型是 JMS 提供的,AMQP 协议还提供了 5 种消息模型。 + +## 三 使用消息队列带来的一些问题 + +- **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了! +- **系统复杂性提高:** 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! +- **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! + +## 四 JMS VS AMQP + +### 4.1 JMS + +#### 4.1.1 JMS 简介 + +JMS(JAVA Message Service,java 消息服务)是 java 的消息服务,JMS 的客户端之间可以通过 JMS 服务进行异步的消息传输。**JMS(JAVA Message Service,Java 消息服务)API 是一个消息服务的标准或者说是规范**,允许应用程序组件基于 JavaEE 平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 + +**ActiveMQ 就是基于 JMS 规范实现的。** + +#### 4.1.2 JMS 两种消息模型 + +**① 点到点(P2P)模型** + +![队列模型](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/队列模型23.png) + +使用**队列(Queue)**作为消息通信载体;满足**生产者与消费者模式**,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) + +**② 发布/订阅(Pub/Sub)模型** + +![发布订阅模型](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85%E6%A8%A1%E5%9E%8B.png) + +发布订阅模型(Pub/Sub) 使用**主题(Topic)**作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者,**在一条消息广播之后才订阅的用户则是收不到该条消息的**。 + +#### 4.1.3 JMS 五种不同的消息正文格式 + +JMS 定义了五种不同的消息正文格式,以及调用的消息类型,允许你发送并接收以一些不同形式的数据,提供现有消息格式的一些级别的兼容性。 + +- StreamMessage -- Java 原始值的数据流 +- MapMessage--一套名称-值对 +- TextMessage--一个字符串对象 +- ObjectMessage--一个序列化的 Java 对象 +- BytesMessage--一个字节的数据流 + +### 4.2 AMQP + +AMQP,即 Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 **高级消息队列协议**(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。 + +**RabbitMQ 就是基于 AMQP 协议实现的。** + +### 4.3 JMS vs AMQP + +| 对比方向 | JMS | AMQP | +| :----------- | :-------------------------------------- | :----------------------------------------------------------- | +| 定义 | Java API | 协议 | +| 跨语言 | 否 | 是 | +| 跨平台 | 否 | 是 | +| 支持消息类型 | 提供两种消息模型:①Peer-2-Peer;②Pub/sub | 提供了五种消息模型:①direct exchange;②fanout exchange;③topic change;④headers exchange;⑤system exchange。本质来讲,后四种和 JMS 的 pub/sub 模型没有太大差别,仅是在路由机制上做了更详细的划分; | +| 支持消息类型 | 支持多种消息类型 ,我们在上面提到过 | byte[](二进制) | + +**总结:** + +- AMQP 为消息定义了线路层(wire-level protocol)的协议,而 JMS 所定义的是 API 规范。在 Java 体系中,多个 client 均可以通过 JMS 进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而 AMQP 天然具有跨平台、跨语言特性。 +- JMS 支持 TextMessage、MapMessage 等复杂的消息类型;而 AMQP 仅支持 byte[] 消息类型(复杂的类型可序列化后发送)。 +- 由于 Exchange 提供的路由算法,AMQP 可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队列 和 主题/订阅 方式两种。 + +## 五 常见的消息队列对比 + +| 对比方向 | 概要 | +| -------- | ------------------------------------------------------------ | +| 吞吐量 | 万级的 ActiveMQ 和 RabbitMQ 的吞吐量(ActiveMQ 的性能最差)要比 十万级甚至是百万级的 RocketMQ 和 Kafka 低一个数量级。 | +| 可用性 | 都可以实现高可用。ActiveMQ 和 RabbitMQ 都是基于主从架构实现高可用性。RocketMQ 基于分布式架构。 kafka 也是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | +| 时效性 | RabbitMQ 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。其他三个都是 ms 级。 | +| 功能支持 | 除了 Kafka,其他三个功能都较为完备。 Kafka 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 | +| 消息丢失 | ActiveMQ 和 RabbitMQ 丢失的可能性非常低, RocketMQ 和 Kafka 理论上不会丢失。 | + +**总结:** + +- ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用。 +- RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做 erlang 源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 +- RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的 MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用 RocketMQ 挺好的 +- Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 + +参考:《Java 工程师面试突击第 1 季-中华石杉老师》 \ No newline at end of file diff --git a/docs/system-design/data-communication/rabbitmq.md b/docs/high-performance/message-queue/rabbitmq-intro.md similarity index 79% rename from docs/system-design/data-communication/rabbitmq.md rename to docs/high-performance/message-queue/rabbitmq-intro.md index 79f24fdcc30..d676114c8e6 100644 --- a/docs/system-design/data-communication/rabbitmq.md +++ b/docs/high-performance/message-queue/rabbitmq-intro.md @@ -1,25 +1,5 @@ - - -- [一文搞懂 RabbitMQ 的重要概念以及安装](#一文搞懂-rabbitmq-的重要概念以及安装) - - [一 RabbitMQ 介绍](#一-rabbitmq-介绍) - - [1.1 RabbitMQ 简介](#11-rabbitmq-简介) - - [1.2 RabbitMQ 核心概念](#12-rabbitmq-核心概念) - - [1.2.1 Producer(生产者) 和 Consumer(消费者)](#121-producer生产者-和-consumer消费者) - - [1.2.2 Exchange(交换器)](#122-exchange交换器) - - [1.2.3 Queue(消息队列)](#123-queue消息队列) - - [1.2.4 Broker(消息中间件的服务节点)](#124-broker消息中间件的服务节点) - - [1.2.5 Exchange Types(交换器类型)](#125-exchange-types交换器类型) - - [① fanout](#①-fanout) - - [② direct](#②-direct) - - [③ topic](#③-topic) - - [④ headers(不推荐)](#④-headers不推荐) - - [二 安装 RabbitMq](#二-安装-rabbitmq) - - [2.1 安装 erlang](#21-安装-erlang) - - [2.2 安装 RabbitMQ](#22-安装-rabbitmq) - - - -# 一文搞懂 RabbitMQ 的重要概念以及安装 + +# RabbitMQ 入门总结 ## 一 RabbitMQ 介绍 @@ -32,7 +12,7 @@ RabbitMQ 是采用 Erlang 语言实现 AMQP(Advanced Message Queuing Protocol, RabbitMQ 发展到今天,被越来越多的人认可,这和它在易用性、扩展性、可靠性和高可用性等方面的卓著表现是分不开的。RabbitMQ 的具体特点可以概括为以下几点: - **可靠性:** RabbitMQ使用一些机制来保证消息的可靠性,如持久化、传输确认及发布确认等。 -- **灵活的路由:** 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。这个后面会在我们将 RabbitMQ 核心概念的时候详细介绍到。 +- **灵活的路由:** 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。这个后面会在我们讲 RabbitMQ 核心概念的时候详细介绍到。 - **扩展性:** 多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。 - **高可用性:** 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。 - **支持多种协议:** RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP、MQTT 等多种消息中间件协议。 @@ -46,7 +26,7 @@ RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、 下面再来看看图1—— RabbitMQ 的整体模型架构。 -![图1-RabbitMQ 的整体模型架构](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/96388546.jpg) +![图1-RabbitMQ 的整体模型架构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/96388546.jpg) 下面我会一一介绍上图中的一些概念。 @@ -67,7 +47,7 @@ RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、 Exchange(交换器) 示意图如下: -![Exchange(交换器) 示意图](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/24007899.jpg) +![Exchange(交换器) 示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/24007899.jpg) 生产者将消息发给交换器的时候,一般会指定一个 **RoutingKey(路由键)**,用来指定这个消息的路由规则,而这个 **RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效**。 @@ -75,7 +55,7 @@ RabbitMQ 中通过 **Binding(绑定)** 将 **Exchange(交换器)** 与 **Queue( Binding(绑定) 示意图: -![Binding(绑定) 示意图](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/70553134.jpg) +![Binding(绑定) 示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/70553134.jpg) 生产者将消息发送给交换器时,需要一个RoutingKey,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如fanout类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。 @@ -85,7 +65,7 @@ Binding(绑定) 示意图: **RabbitMQ** 中消息只能存储在 **队列** 中,这一点和 **Kafka** 这种消息中间件相反。Kafka 将消息存储在 **topic(主题)** 这个逻辑层面,而相对应的队列逻辑只是topic实际存储文件中的位移标识。 RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。 -**多个消费者可以订阅同一个队列**,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,这样避免的消息被重复消费。 +**多个消费者可以订阅同一个队列**,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。 **RabbitMQ** 不支持队列层面的广播消费,如果有广播消费的需求,需要在其上进行二次开发,这样会很麻烦,不建议这样做。 @@ -95,7 +75,7 @@ Binding(绑定) 示意图: 下图展示了生产者将消息存入 RabbitMQ Broker,以及消费者从Broker中消费数据的整个流程。 -![消息队列的运转过程](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/67952922.jpg) +![消息队列的运转过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/67952922.jpg) 这样图1中的一些关于 RabbitMQ 的基本概念我们就介绍完毕了,下面再来介绍一下 **Exchange Types(交换器类型)** 。 @@ -111,7 +91,7 @@ fanout 类型的Exchange路由规则非常简单,它会把所有发送到该Ex direct 类型的Exchange路由规则也很简单,它会把消息路由到那些 Bindingkey 与 RoutingKey 完全匹配的 Queue 中。 -![direct 类型交换器](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/37008021.jpg) +![direct 类型交换器](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/37008021.jpg) 以上图为例,如果发送消息的时候设置路由键为“warning”,那么消息会路由到 Queue1 和 Queue2。如果在发送消息的时候设置路由键为"Info”或者"debug”,消息只会路由到Queue2。如果以其他的路由键发送消息,则消息不会路由到这两个队列中。 @@ -123,23 +103,23 @@ direct 类型常用在处理有优先级的任务,根据任务的优先级把 - RoutingKey 为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”; - BindingKey 和 RoutingKey 一样也是点号“.”分隔的字符串; -- BindingKey 中可以存在两种特殊字符串“*”和“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。 +- BindingKey 中可以存在两种特殊字符串“\*”和“#”,用于做模糊匹配,其中“\*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。 -![topic 类型交换器](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/73843.jpg) +![topic 类型交换器](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-16/73843.jpg) 以上图为例: -- 路由键为 “com.rabbitmq.client” 的消息会同时路由到 Queuel 和 Queue2; +- 路由键为 “com.rabbitmq.client” 的消息会同时路由到 Queue1 和 Queue2; - 路由键为 “com.hidden.client” 的消息只会路由到 Queue2 中; - 路由键为 “com.hidden.demo” 的消息只会路由到 Queue2 中; -- 路由键为 “java.rabbitmq.demo” 的消息只会路由到Queuel中; +- 路由键为 “java.rabbitmq.demo” 的消息只会路由到 Queue1 中; - 路由键为 “java.util.concurrent” 的消息将会被丢弃或者返回给生产者(需要设置 mandatory 参数),因为它没有匹配任何路由键。 ##### ④ headers(不推荐) -headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定队列和交换器时制定一组键值对,当发送消息到交换器时,RabbitMQ会获取到该消息的 headers(也是一个键值对的形式)'对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。 +headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定队列和交换器时指定一组键值对,当发送消息到交换器时,RabbitMQ会获取到该消息的 headers(也是一个键值对的形式),对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。 -## 二 安装 RabbitMq +## 二 安装 RabbitMQ 通过 Docker 安装非常方便,只需要几条命令就好了,我这里是只说一下常规安装方法。 @@ -156,10 +136,10 @@ headers 类型的交换器不依赖于路由键的匹配规则来路由消息, 在官网下载然后上传到 Linux 上或者直接使用下面的命令下载对应的版本。 ```shell -[root@SnailClimb local]#wget http://erlang.org/download/otp_src_19.3.tar.gz +[root@SnailClimb local]#wget https://erlang.org/download/otp_src_19.3.tar.gz ``` -erlang 官网下载:[http://www.erlang.org/downloads](http://www.erlang.org/downloads) +erlang 官网下载:[https://www.erlang.org/downloads](https://www.erlang.org/downloads) **2 解压 erlang 安装包** @@ -211,7 +191,7 @@ make && make install ```erlang io:format("hello world~n", []). ``` -![输出“hello world”](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-12/49570541.jpg) +![输出“hello world”](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-12/49570541.jpg) 大功告成,我们的 erlang 已经安装完成。 @@ -242,7 +222,7 @@ export ERL_HOME PATH [root@SnailClimb etc]# erl ``` -![输入 erl 查看 erlang 环境变量是否配置正确](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-12/62504246.jpg) +![输入 erl 查看 erlang 环境变量是否配置正确](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-12/62504246.jpg) ### 2.2 安装 RabbitMQ @@ -279,23 +259,23 @@ rabbitmq-plugins enable rabbitmq_management chkconfig rabbitmq-server on ``` -**4. 启动服务** +**5. 启动服务** ```shell service rabbitmq-server start ``` -**5. 查看服务状态** +**6. 查看服务状态** ```shell service rabbitmq-server status ``` -**6. 访问 RabbitMQ 控制台** +**7. 访问 RabbitMQ 控制台** 浏览器访问:http://你的ip地址:15672/ -默认用户名和密码: guest/guest;但是需要注意的是:guestuest用户只是被容许从localhost访问。官网文档描述如下: +默认用户名和密码:guest/guest; 但是需要注意的是:guest用户只是被容许从localhost访问。官网文档描述如下: ```shell “guest” user can only connect via localhost @@ -319,6 +299,6 @@ Setting permissions for user "root" in vhost "/" ... 再次访问:http://你的ip地址:15672/ ,输入用户名和密码:root root -![RabbitMQ控制台](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-12/45835332.jpg) +![RabbitMQ控制台](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-12/45835332.jpg) diff --git a/docs/system-design/data-communication/RocketMQ.md b/docs/high-performance/message-queue/rocketmq-intro.md similarity index 96% rename from docs/system-design/data-communication/RocketMQ.md rename to docs/high-performance/message-queue/rocketmq-intro.md index c74fab6feeb..2fbacfabec4 100644 --- a/docs/system-design/data-communication/RocketMQ.md +++ b/docs/high-performance/message-queue/rocketmq-intro.md @@ -1,10 +1,12 @@ +# RocketMQ入门总结 + > 文章很长,点赞再看,养成好习惯😋😋😋 > > [本文由 FrancisQ 老哥投稿!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485969&idx=1&sn=6bd53abde30d42a778d5a35ec104428c&chksm=cea245daf9d5cccce631f93115f0c2c4a7634e55f5bef9009fd03f5a0ffa55b745b5ef4f0530&token=294077121&lang=zh_CN#rd) ## 消息队列扫盲 -消息队列顾名思义就是存放消息的队列,队列我就不解释了,别告诉我你连队列都不知道似啥吧? +消息队列顾名思义就是存放消息的队列,队列我就不解释了,别告诉我你连队列都不知道是啥吧? 所以问题并不是消息队列是什么,而是 **消息队列为什么会出现?消息队列能用来干什么?用它来干这些事会带来什么好处?消息队列会带来副作用吗?** @@ -26,7 +28,7 @@ 我们省略中间的网络通信时间消耗,假如购票系统处理需要 150ms ,短信系统处理需要 200ms ,那么整个处理流程的时间消耗就是 150ms + 200ms = 350ms。 -当然,乍看没什么问题。可是仔细一想你就感觉有点问题,我用户购票在购票系统的时候其实就已经完成了购买,而我现在通过同步调用非要让整个请求拉长时间,而短息系统这玩意又不是很有必要,它仅仅是一个辅助功能增强用户体验感而已。我现在整个调用流程就有点 **头重脚轻** 的感觉了,购票是一个不太耗时的流程,而我现在因为同步调用,非要等待发送短信这个比较耗时的操作才返回结果。那我如果再加一个发送邮件呢? +当然,乍看没什么问题。可是仔细一想你就感觉有点问题,我用户购票在购票系统的时候其实就已经完成了购买,而我现在通过同步调用非要让整个请求拉长时间,而短信系统这玩意又不是很有必要,它仅仅是一个辅助功能增强用户体验感而已。我现在整个调用流程就有点 **头重脚轻** 的感觉了,购票是一个不太耗时的流程,而我现在因为同步调用,非要等待发送短信这个比较耗时的操作才返回结果。那我如果再加一个发送邮件呢? ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef380429cf373e.jpg) @@ -44,7 +46,7 @@ ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/006APoFYly1fvd9cwjlfrj30as0b03ym.jpg) -那后来,我们工作赚钱了有钱去饭店吃饭了,我们告诉服务员来一碗牛肉面加个荷包蛋 **(传达一个消息)** ,然后我们就可以在饭桌上安心的玩手机了 **(干自己其他事情)** ,等到我们的牛肉面上了我们就可以吃了。这其中我们也就传达了一个消息,然后我们又转过头干其他事情了。这其中虽然做面的时间没有变短,但是我们只需要传达一个消息就可以看其他事情了,这是一个 **异步** 的概念。 +那后来,我们工作赚钱了有钱去饭店吃饭了,我们告诉服务员来一碗牛肉面加个荷包蛋 **(传达一个消息)** ,然后我们就可以在饭桌上安心的玩手机了 **(干自己其他事情)** ,等到我们的牛肉面上了我们就可以吃了。这其中我们也就传达了一个消息,然后我们又转过头干其他事情了。这其中虽然做面的时间没有变短,但是我们只需要传达一个消息就可以干其他事情了,这是一个 **异步** 的概念。 所以,为了解决这一个问题,聪明的程序员在中间也加了个类似于服务员的中间件——消息队列。这个时候我们就可以把模型给改造了。 @@ -174,7 +176,7 @@ ### RocketMQ中的消息模型 -`RockerMQ` 中的消息模型就是按照 **主题模型** 所实现的。你可能会好奇这个 **主题** 到底是怎么实现的呢?你上面也没有讲到呀! +`RocketMQ` 中的消息模型就是按照 **主题模型** 所实现的。你可能会好奇这个 **主题** 到底是怎么实现的呢?你上面也没有讲到呀! 其实对于主题模型的实现来说每个消息中间件的底层设计都是不一样的,就比如 `Kafka` 中的 **分区** ,`RocketMQ` 中的 **队列** ,`RabbitMQ` 中的 `Exchange` 。我们可以理解为 **主题模型/发布订阅模型** 就是一个标准,那些中间件只不过照着这个标准去实现而已。 @@ -190,7 +192,7 @@ 你可以看到图中生产者组中的生产者会向主题发送消息,而 **主题中存在多个队列**,生产者每次生产消息之后是指定主题中的某个队列发送消息的。 -每个主题中都有多个队列(这里还不涉及到 `Broker`),集群消费模式下,一个消费者集群多台机器共同消费一个 `topic` 的多个队列,**一个队列只会被一个消费者消费**。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。就像上图中 `Consumer1` 和 `Consumer2` 分别对应着两个队列,而 `Consuer3` 是没有队列对应的,所以一般来讲要控制 **消费者组中的消费者个数和主题中队列个数相同** 。 +每个主题中都有多个队列(分布在不同的 `Broker`中,如果是集群的话,`Broker`又分布在不同的服务器中),集群消费模式下,一个消费者集群多台机器共同消费一个 `topic` 的多个队列,**一个队列只会被一个消费者消费**。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。就像上图中 `Consumer1` 和 `Consumer2` 分别对应着两个队列,而 `Consumer3` 是没有队列对应的,所以一般来讲要控制 **消费者组中的消费者个数和主题中队列个数相同** 。 当然也可以消费者个数小于队列个数,只不过不太建议。如下图。 @@ -393,7 +395,7 @@ emmm,就两个字—— **幂等** 。在编程中一个*幂等* 操作的特 而在 `RocketMQ` 中采用了 `Dledger` 解决这个问题。他要求在写入消息的时候,要求**至少消息复制到半数以上的节点之后**,才给客⼾端返回写⼊成功,并且它是⽀持通过选举来动态切换主节点的。这里我就不展开说明了,读者可以自己去了解。 -> 也不是说 `Dledger` 是个完美的方案,至少在 `Dledger` 选举过程中是无法提供服务的,而且他必须要使用三个节点或以上,如果多数节点同时挂掉他也是无法保证可用性的,而且要求消息复制板书以上节点的效率和直接异步复制还是有一定的差距的。 +> 也不是说 `Dledger` 是个完美的方案,至少在 `Dledger` 选举过程中是无法提供服务的,而且他必须要使用三个节点或以上,如果多数节点同时挂掉他也是无法保证可用性的,而且要求消息复制半数以上节点的效率和直接异步复制还是有一定的差距的。 ### 存储机制 @@ -425,7 +427,7 @@ emmm,是不是有一点复杂🤣,看英文图片和英文文档的时候就 首先,在最上面的那一块就是我刚刚讲的你现在可以直接 **把 `ConsumerQueue` 理解为 `Queue`**。 -在图中最左边说明了 红色方块 代表被写入的消息,虚线方块代表等待被写入的。左边的生产者发送消息会指定 `Topic` 、`QueueId` 和具体消息内容,而在 `Broker` 中管你是哪门子消息,他直接 **全部顺序存储到了 CommitLog **。而根据生产者指定的 `Topic` 和 `QueueId` 将这条消息本身在 `CommitLog` 的偏移(offset),消息本身大小,和tag的hash值存入对应的 `ConsumeQueue` 索引文件中。而在每个队列中都保存了 `ConsumeOffset` 即每个消费者组的消费位置(我在架构那里提到了,忘了的同学可以回去看一下),而消费者拉取消息进行消费的时候只需要根据 `ConsumeOffset` 获取下一个未被消费的消息就行了。 +在图中最左边说明了 红色方块 代表被写入的消息,虚线方块代表等待被写入的。左边的生产者发送消息会指定 `Topic` 、`QueueId` 和具体消息内容,而在 `Broker` 中管你是哪门子消息,他直接 **全部顺序存储到了 CommitLog**。而根据生产者指定的 `Topic` 和 `QueueId` 将这条消息本身在 `CommitLog` 的偏移(offset),消息本身大小,和tag的hash值存入对应的 `ConsumeQueue` 索引文件中。而在每个队列中都保存了 `ConsumeOffset` 即每个消费者组的消费位置(我在架构那里提到了,忘了的同学可以回去看一下),而消费者拉取消息进行消费的时候只需要根据 `ConsumeOffset` 获取下一个未被消费的消息就行了。 上述就是我对于整个消息存储架构的大概理解(这里不涉及到一些细节讨论,比如稀疏索引等等问题),希望对你有帮助。 @@ -451,4 +453,4 @@ emmm,是不是有一点复杂🤣,看英文图片和英文文档的时候就 等等。。。 -> 如果喜欢可以点赞哟👍👍👍。 \ No newline at end of file +> 如果喜欢可以点赞哟👍👍👍。 diff --git a/docs/system-design/data-communication/RocketMQ-Questions.md b/docs/high-performance/message-queue/rocketmq-questions.md similarity index 76% rename from docs/system-design/data-communication/RocketMQ-Questions.md rename to docs/high-performance/message-queue/rocketmq-questions.md index a41a4035b7b..68957689c11 100644 --- a/docs/system-design/data-communication/RocketMQ-Questions.md +++ b/docs/high-performance/message-queue/rocketmq-questions.md @@ -1,25 +1,8 @@ +# RocketMQ常见问题 + 本文来自读者 [PR](https://github.com/Snailclimb/JavaGuide/pull/291)。 - - -- [1 单机版消息中心](#1-%E5%8D%95%E6%9C%BA%E7%89%88%E6%B6%88%E6%81%AF%E4%B8%AD%E5%BF%83) -- [2 分布式消息中心](#2-%E5%88%86%E5%B8%83%E5%BC%8F%E6%B6%88%E6%81%AF%E4%B8%AD%E5%BF%83) - - [2.1 问题与解决](#21-%E9%97%AE%E9%A2%98%E4%B8%8E%E8%A7%A3%E5%86%B3) - - [2.1.1 消息丢失的问题](#211-%E6%B6%88%E6%81%AF%E4%B8%A2%E5%A4%B1%E7%9A%84%E9%97%AE%E9%A2%98) - - [2.1.2 同步落盘怎么才能快](#212-%E5%90%8C%E6%AD%A5%E8%90%BD%E7%9B%98%E6%80%8E%E4%B9%88%E6%89%8D%E8%83%BD%E5%BF%AB) - - [2.1.3 消息堆积的问题](#213-%E6%B6%88%E6%81%AF%E5%A0%86%E7%A7%AF%E7%9A%84%E9%97%AE%E9%A2%98) - - [2.1.4 定时消息的实现](#214-%E5%AE%9A%E6%97%B6%E6%B6%88%E6%81%AF%E7%9A%84%E5%AE%9E%E7%8E%B0) - - [2.1.5 顺序消息的实现](#215-%E9%A1%BA%E5%BA%8F%E6%B6%88%E6%81%AF%E7%9A%84%E5%AE%9E%E7%8E%B0) - - [2.1.6 分布式消息的实现](#216-%E5%88%86%E5%B8%83%E5%BC%8F%E6%B6%88%E6%81%AF%E7%9A%84%E5%AE%9E%E7%8E%B0) - - [2.1.7 消息的 push 实现](#217-%E6%B6%88%E6%81%AF%E7%9A%84-push-%E5%AE%9E%E7%8E%B0) - - [2.1.8 消息重复发送的避免](#218-%E6%B6%88%E6%81%AF%E9%87%8D%E5%A4%8D%E5%8F%91%E9%80%81%E7%9A%84%E9%81%BF%E5%85%8D) - - [2.1.9 广播消费与集群消费](#219-%E5%B9%BF%E6%92%AD%E6%B6%88%E8%B4%B9%E4%B8%8E%E9%9B%86%E7%BE%A4%E6%B6%88%E8%B4%B9) - - [2.1.10 RocketMQ 不使用 ZooKeeper 作为注册中心的原因,以及自制的 NameServer 优缺点?](#2110-rocketmq-%E4%B8%8D%E4%BD%BF%E7%94%A8-zookeeper-%E4%BD%9C%E4%B8%BA%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83%E7%9A%84%E5%8E%9F%E5%9B%A0%E4%BB%A5%E5%8F%8A%E8%87%AA%E5%88%B6%E7%9A%84-nameserver-%E4%BC%98%E7%BC%BA%E7%82%B9) - - [2.1.11 其它](#2111-%E5%85%B6%E5%AE%83) -- [3 参考](#3-%E5%8F%82%E8%80%83) - - - -# 1 单机版消息中心 + +## 1 单机版消息中心 一个消息中心,最基本的需要支持多生产者、多消费者,例如下: @@ -127,40 +110,40 @@ class Broker { 4. 没有使用多个队列(即多个 LinkedBlockingQueue),RocketMQ 的顺序消息是通过生产者和消费者同时使用同一个 MessageQueue 来实现,但是如果我们只有一个 MessageQueue,那我们天然就支持顺序消息 5. 没有使用 MappedByteBuffer 来实现文件映射从而使消息数据落盘非常的快(实际 RocketMQ 使用的是 FileChannel+DirectBuffer) -# 2 分布式消息中心 +## 2 分布式消息中心 -## 2.1 问题与解决 +### 2.1 问题与解决 -### 2.1.1 消息丢失的问题 +#### 2.1.1 消息丢失的问题 1. 当你系统需要保证百分百消息不丢失,你可以使用生产者每发送一个消息,Broker 同步返回一个消息发送成功的反馈消息 2. 即每发送一个消息,同步落盘后才返回生产者消息发送成功,这样只要生产者得到了消息发送生成的返回,事后除了硬盘损坏,都可以保证不会消息丢失 3. 但是这同时引入了一个问题,同步落盘怎么才能快? -### 2.1.2 同步落盘怎么才能快 +#### 2.1.2 同步落盘怎么才能快 1. 使用 FileChannel + DirectBuffer 池,使用堆外内存,加快内存拷贝 2. 使用数据和索引分离,当消息需要写入时,使用 commitlog 文件顺序写,当需要定位某个消息时,查询index 文件来定位,从而减少文件IO随机读写的性能损耗 -### 2.1.3 消息堆积的问题 +#### 2.1.3 消息堆积的问题 1. 后台定时任务每隔72小时,删除旧的没有使用过的消息信息 2. 根据不同的业务实现不同的丢弃任务,具体参考线程池的 AbortPolicy,例如FIFO/LRU等(RocketMQ没有此策略) 3. 消息定时转移,或者对某些重要的 TAG 型(支付型)消息真正落库 -### 2.1.4 定时消息的实现 +#### 2.1.4 定时消息的实现 1. 实际 RocketMQ 没有实现任意精度的定时消息,它只支持某些特定的时间精度的定时消息 2. 实现定时消息的原理是:创建特定时间精度的 MessageQueue,例如生产者需要定时1s之后被消费者消费,你只需要将此消息发送到特定的 Topic,例如:MessageQueue-1 表示这个 MessageQueue 里面的消息都会延迟一秒被消费,然后 Broker 会在 1s 后发送到消费者消费此消息,使用 newSingleThreadScheduledExecutor 实现 -### 2.1.5 顺序消息的实现 +#### 2.1.5 顺序消息的实现 1. 与定时消息同原理,生产者生产消息时指定特定的 MessageQueue ,消费者消费消息时,消费特定的 MessageQueue,其实单机版的消息中心在一个 MessageQueue 就天然支持了顺序消息 2. 注意:同一个 MessageQueue 保证里面的消息是顺序消费的前提是:消费者是串行的消费该 MessageQueue,因为就算 MessageQueue 是顺序的,但是当并行消费时,还是会有顺序问题,但是串行消费也同时引入了两个问题: >1. 引入锁来实现串行 >2. 前一个消费阻塞时后面都会被阻塞 -### 2.1.6 分布式消息的实现 +#### 2.1.6 分布式消息的实现 1. 需要前置知识:2PC 2. RocketMQ4.3 起支持,原理为2PC,即两阶段提交,prepared->commit/rollback @@ -168,29 +151,31 @@ class Broker { >注意,就算是事务消息最后回滚了也不会物理删除,只会逻辑删除该消息 -### 2.1.7 消息的 push 实现 +#### 2.1.7 消息的 push 实现 1. 注意,RocketMQ 已经说了自己会有低延迟问题,其中就包括这个消息的 push 延迟问题 2. 因为这并不是真正的将消息主动的推送到消费者,而是 Broker 定时任务每5s将消息推送到消费者 +3. pull模式需要我们手动调用consumer拉消息,而push模式则只需要我们提供一个listener即可实现对消息的监听,而实际上,RocketMQ的push模式是基于pull模式实现的,它没有实现真正的push。 +4. push方式里,consumer把轮询过程封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。 -### 2.1.8 消息重复发送的避免 +#### 2.1.8 消息重复发送的避免 1. RocketMQ 会出现消息重复发送的问题,因为在网络延迟的情况下,这种问题不可避免的发生,如果非要实现消息不可重复发送,那基本太难,因为网络环境无法预知,还会使程序复杂度加大,因此默认允许消息重复发送 2. RocketMQ 让使用者在消费者端去解决该问题,即需要消费者端在消费消息时支持幂等性的去消费消息 -3. 最简单的解决方案是每条消费记录有个消费状态字段,根据这个消费状态字段来是否消费或者使用一个集中式的表,来存储所有消息的消费状态,从而避免重复消费 +3. 最简单的解决方案是每条消费记录有个消费状态字段,根据这个消费状态字段来判断是否消费或者使用一个集中式的表,来存储所有消息的消费状态,从而避免重复消费 4. 具体实现可以查询关于消息幂等消费的解决方案 -### 2.1.9 广播消费与集群消费 +#### 2.1.9 广播消费与集群消费 1. 消息消费区别:广播消费,订阅该 Topic 的消息者们都会消费**每个**消息。集群消费,订阅该 Topic 的消息者们只会有一个去消费**某个**消息 2. 消息落盘区别:具体表现在消息消费进度的保存上。广播消费,由于每个消费者都独立的去消费每个消息,因此每个消费者各自保存自己的消息消费进度。而集群消费下,订阅了某个 Topic,而旗下又有多个 MessageQueue,每个消费者都可能会去消费不同的 MessageQueue,因此总体的消费进度保存在 Broker 上集中的管理 -### 2.1.10 RocketMQ 不使用 ZooKeeper 作为注册中心的原因,以及自制的 NameServer 优缺点? +#### 2.1.10 RocketMQ 不使用 ZooKeeper 作为注册中心的原因,以及自制的 NameServer 优缺点? 1. ZooKeeper 作为支持顺序一致性的中间件,在某些情况下,它为了满足一致性,会丢失一定时间内的可用性,RocketMQ 需要注册中心只是为了发现组件地址,在某些情况下,RocketMQ 的注册中心可以出现数据不一致性,这同时也是 NameServer 的缺点,因为 NameServer 集群间互不通信,它们之间的注册信息可能会不一致 -2. 另外,当有新的服务器加入时,NameServer 并不会立马通知到 Produer,而是由 Produer 定时去请求 NameServer 获取最新的 Broker/Consumer 信息(这种情况是通过 Producer 发送消息时,负载均衡解决) +2. 另外,当有新的服务器加入时,NameServer 并不会立马通知到 Producer,而是由 Producer 定时去请求 NameServer 获取最新的 Broker/Consumer 信息(这种情况是通过 Producer 发送消息时,负载均衡解决) -### 2.1.11 其它 +#### 2.1.11 其它 ![][1] @@ -199,13 +184,13 @@ class Broker { 2. 消息重试负载均衡策略(具体参考 Dubbo 负载均衡策略) 3. 消息过滤器(Producer 发送消息到 Broker,Broker 存储消息信息,Consumer 消费时请求 Broker 端从磁盘文件查询消息文件时,在 Broker 端就使用过滤服务器进行过滤) 4. Broker 同步双写和异步双写中 Master 和 Slave 的交互 -5. Broker 在 4.5.0 版本更新中引入了基于 Raft 协议的多副本选举,之前这是商业版才有的特性 [ISSUE-1046][2] +5. Broker 在 4.5.0 版本更新中引入了基于 Raft 协议的多副本选举,之前这是商业版才有的特性 [ISSUE-1046][2] -# 3 参考 +## 3 参考 1. 《RocketMQ技术内幕》:https://blog.csdn.net/prestigeding/article/details/85233529 2. 关于 RocketMQ 对 MappedByteBuffer 的一点优化:https://lishoubo.github.io/2017/09/27/MappedByteBuffer%E7%9A%84%E4%B8%80%E7%82%B9%E4%BC%98%E5%8C%96/ -3. 阿里中间件团队博客-十分钟入门RocketMQ:http://jm.taobao.org/2017/01/12/rocketmq-quick-start-in-10-minutes/ +3. 十分钟入门RocketMQ:https://developer.aliyun.com/article/66101 4. 分布式事务的种类以及 RocketMQ 支持的分布式消息:https://www.infoq.cn/article/2018/08/rocketmq-4.3-release 5. 滴滴出行基于RocketMQ构建企业级消息队列服务的实践:https://yq.aliyun.com/articles/664608 6. 基于《RocketMQ技术内幕》源码注释:https://github.com/LiWenGu/awesome-rocketmq diff --git "a/docs/high-performance/\350\257\273\345\206\231\345\210\206\347\246\273&\345\210\206\345\272\223\345\210\206\350\241\250.md" "b/docs/high-performance/\350\257\273\345\206\231\345\210\206\347\246\273&\345\210\206\345\272\223\345\210\206\350\241\250.md" new file mode 100644 index 00000000000..4937d9fd7f3 --- /dev/null +++ "b/docs/high-performance/\350\257\273\345\206\231\345\210\206\347\246\273&\345\210\206\345\272\223\345\210\206\350\241\250.md" @@ -0,0 +1,190 @@ +# 读写分离&分库分表 + +大家好呀!今天和小伙伴们聊聊读写分离以及分库分表。 + +相信很多小伙伴们对于这两个概念已经比较熟悉了,这篇文章全程都是大白话的形式,希望能够给你带来不一样的感受。 + +如果你之前不太了解这两个概念,那我建议你搞懂之后,可以把自己对于读写分离以及分库分表的理解讲给你的同事/朋友听听。 + +**原创不易,若有帮助,点赞/分享就是对我最大的鼓励!** + +_个人能力有限。如果文章有任何需要补充/完善/修改的地方,欢迎在评论区指出,共同进步!_ + +## 读写分离 + +### 何为读写分离? + +见名思意,根据读写分离的名字,我们就可以知道:**读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。** 这样的话,就能够小幅提升写性能,大幅提升读性能。 + +我简单画了一张图来帮助不太清楚读写分离的小伙伴理解。 + +![读写分离](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/9c624bc130d053860a5089cb9a53310a.png) + +一般情况下,我们都会选择一主多从,也就是一台主数据库负责写,其他的从数据库负责读。主库和从库之间会进行数据同步,以保证从库中数据的准确性。这样的架构实现起来比较简单,并且也符合系统的写少读多的特点。 + +### 读写分离会带来什么问题?如何解决? + +读写分离对于提升数据库的并发非常有效,但是,同时也会引来一个问题:主库和从库的数据存在延迟,比如你写完主库之后,主库的数据同步到从库是需要时间的,这个时间差就导致了主库和从库的数据不一致性问题。这也就是我们经常说的 **主从同步延迟** 。 + +主从同步延迟问题的解决,没有特别好的一种方案(可能是我太菜了,欢迎评论区补充)。你可以根据自己的业务场景,参考一下下面几种解决办法。 + +**1.强制将读请求路由到主库处理。** + +既然你从库的数据过期了,那我就直接从主库读取嘛!这种方案虽然会增加主库的压力,但是,实现起来比较简单,也是我了解到的使用最多的一种方式。 + +比如 `Sharding-JDBC` 就是采用的这种方案。通过使用 Sharding-JDBC 的 `HintManager` 分片键值管理器,我们可以强制使用主库。 + +```java +HintManager hintManager = HintManager.getInstance(); +hintManager.setMasterRouteOnly(); +// 继续JDBC操作 +``` + +对于这种方案,你可以将那些必须获取最新数据的读请求都交给主库处理。 + +**2.延迟读取。** + +还有一些朋友肯定会想既然主从同步存在延迟,那我就在延迟之后读取啊,比如主从同步延迟 0.5s,那我就 1s 之后再读取数据。这样多方便啊!方便是方便,但是也很扯淡。 + +不过,如果你是这样设计业务流程就会好很多:对于一些对数据比较敏感的场景,你可以在完成写请求之后,避免立即进行请求操作。比如你支付成功之后,跳转到一个支付成功的页面,当你点击返回之后才返回自己的账户。 + +另外,[《MySQL 实战 45 讲》](https://time.geekbang.org/column/intro/100020801?code=ieY8HeRSlDsFbuRtggbBQGxdTh-1jMASqEIeqzHAKrI%3D)这个专栏中的[《读写分离有哪些坑?》](https://time.geekbang.org/column/article/77636)这篇文章还介绍了很多其他比较实际的解决办法,感兴趣的小伙伴可以自行研究一下。 + +### 如何实现读写分离? + +不论是使用哪一种读写分离具体的实现方案,想要实现读写分离一般包含如下几步: + +1. 部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。 +2. 保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的**主从复制**。 +3. 系统将写请求交给主数据库处理,读请求交给从数据库处理。 + +落实到项目本身的话,常用的方式有两种: + +**1.代理方式** + +![读写分离-代理层](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/461112716e30db118f4c784adc6e2ff7.png) + +我们可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中。 + +提供类似功能的中间件有 **MySQL Router**(官方)、**Atlas**(基于 MySQL Proxy)、**Maxscale**、**MyCat**。 + +**2.组件方式** + +在这种方式中,我们可以通过引入第三方组件来帮助我们读写请求。 + +这也是我比较推荐的一种方式。这种方式目前在各种互联网公司中用的最多的,相关的实际的案例也非常多。如果你要采用这种方式的话,推荐使用 `sharding-jdbc` ,直接引入 jar 包即可使用,非常方便。同时,也节省了很多运维的成本。 + +你可以在 shardingsphere 官方找到[sharding-jdbc 关于读写分离的操作](https://shardingsphere.apache.org/document/legacy/3.x/document/cn/manual/sharding-jdbc/usage/read-write-splitting/)。 + +### 主从复制原理了解么? + +MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。因此,我们根据主库的 MySQL binlog 日志就能够将主库的数据同步到从库中。 + +更具体和详细的过程是这个样子的(图片来自于:[《MySQL Master-Slave Replication on the Same Machine》](https://www.toptal.com/mysql/mysql-master-slave-replication-tutorial)): + +![MySQL主从复制](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/78816271d3ab52424bfd5ad3086c1a0f.png) + +1. 主库将数据库中数据的变化写入到 binlog +2. 从库连接主库 +3. 从库会创建一个 I/O 线程向主库请求更新的 binlog +4. 主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收 +5. 从库的 I/O 线程将接收的 binlog 写入到 relay log 中。 +6. 从库的 SQL 线程读取 relay log 同步数据本地(也就是再执行一遍 SQL )。 + +怎么样?看了我对主从复制这个过程的讲解,你应该搞明白了吧! + +你一般看到 binlog 就要想到主从复制。当然,除了主从复制之外,binlog 还能帮助我们实现数据恢复。 + +🌈 拓展一下: + +不知道大家有没有使用过阿里开源的一个叫做 canal 的工具。这个工具可以帮助我们实现 MySQL 和其他数据源比如 Elasticsearch 或者另外一台 MySQL 数据库之间的数据同步。很显然,这个工具的底层原理肯定也是依赖 binlog。canal 的原理就是模拟 MySQL 主从复制的过程,解析 binlog 将数据同步到其他的数据源。 + +另外,像咱们常用的分布式缓存组件 Redis 也是通过主从复制实现的读写分离。 + +🌕 简单总结一下: + +**MySQL 主从复制是依赖于 binlog 。另外,常见的一些同步 MySQL 数据到其他数据源的工具(比如 canal)的底层一般也是依赖 binlog 。** + +## 分库分表 + +读写分离主要应对的是数据库读并发,没有解决数据库存储问题。试想一下:**如果 MySQL 一张表的数据量过大怎么办?** + +换言之,**我们该如何解决 MySQL 的存储压力呢?** + +答案之一就是 **分库分表**。 + +### 何为分库? + +**分库** 就是将数据库中的数据分散到不同的数据库上。 + +下面这些操作都涉及到了分库: + +- 你将数据库中的用户表和用户订单表分别放在两个不同的数据库。 +- 由于用户表数据量太大,你对用户表进行了水平切分,然后将切分后的 2 张用户表分别放在两个不同的数据库。 + +### 何为分表? + +**分表** 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。 + +**何为垂直拆分?** + +简单来说,垂直拆分是对数据表列的拆分,把一张列比较多的表拆分为多张表。 + +举个例子:我们可以将用户信息表中的一些列单独抽出来作为一个表。 + +**何为水平拆分?** + +简单来说,水平拆分是对数据表行的拆分,把一张行比较多的表拆分为多张表。 + +举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。 + +[《从零开始学架构》](https://time.geekbang.org/column/intro/100006601?code=i00Nq3pHUcUj04ZWy70NCRl%2FD2Lfj8GVzcGzZ3Wf5Ug%3D) 中的有一张图片对于垂直拆分和水平拆分的描述还挺直观的。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/662ea3bda90061d0b40177e3a46fefc3.jpg) + +### 什么情况下需要分库分表? + +遇到下面几种场景可以考虑分库分表: + +- 单表的数据达到千万级别以上,数据库读写速度比较缓慢(分表)。 +- 数据库中的数据占用的空间越来越大,备份时间越来越长(分库)。 +- 应用的并发量太大(分库)。 + +### 分库分表会带来什么问题呢? + +记住,你在公司做的任何技术决策,不光是要考虑这个技术能不能满足我们的要求,是否适合当前业务场景,还要重点考虑其带来的成本。 + +引入分库分表之后,会给系统带来什么挑战呢? + +- **join 操作** : 同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。 +- **事务问题** :同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。 +- **分布式 id** :分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 id 了。 +- ...... + +另外,引入分库分表之后,一般需要 DBA 的参与,同时还需要更多的数据库服务器,这些都属于成本。 + +### 分库分表有没有什么比较推荐的方案? + +ShardingSphere 项目(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar)是当当捐入 Apache 的,目前主要由京东数科的一些巨佬维护。 + +![](https://img-blog.csdnimg.cn/img_convert/60649996bfc69acb1953063dddf0c2e6.png) + +ShardingSphere 绝对可以说是当前分库分表的首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。 + +另外,ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。 + +艿艿之前写了一篇分库分表的实战文章,各位朋友可以看看:[《芋道 Spring Boot 分库分表入门》](https://mp.weixin.qq.com/s/A2MYOFT7SP-7kGOon8qJaw) 。 + +### 分库分表后,数据怎么迁移呢? + +分库分表之后,我们如何将老库(单库单表)的数据迁移到新库(分库分表后的数据库系统)呢? + +比较简单同时也是非常常用的方案就是**停机迁移**,写个脚本老库的数据写到新库中。比如你在凌晨 2 点,系统使用的人数非常少的时候,挂一个公告说系统要维护升级预计 1 小时。然后,你写一个脚本将老库的数据都同步到新库中。 + +如果你不想停机迁移数据的话,也可以考虑**双写方案**。双写方案是针对那种不能停机迁移的场景,实现起来要稍微麻烦一些。具体原理是这样的: + +- 我们对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。 这样就能保证,咱们新库里的数据是最新的。 +- 在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。 +- 重复上一步的操作,直到老库和新库的数据一致为止。 + +想要在项目中实施双写还是比较麻烦的,很容易会出现问题。我们可以借助上面提到的数据库同步工具 Canal 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。 diff --git "a/docs/high-performance/\350\264\237\350\275\275\345\235\207\350\241\241.md" "b/docs/high-performance/\350\264\237\350\275\275\345\235\207\350\241\241.md" new file mode 100644 index 00000000000..a9d98b2cea5 --- /dev/null +++ "b/docs/high-performance/\350\264\237\350\275\275\345\235\207\350\241\241.md" @@ -0,0 +1,13 @@ +# 负载均衡 + +负载均衡系统通常用于将任务比如用户请求处理分配到多个服务器处理以提高网站、应用或者数据库的性能和可靠性。 + +常见的负载均衡系统包括 3 种: + +1. **DNS 负载均衡** :一般用来实现地理级别的均衡。 +2. **硬件负载均衡** : 通过单独的硬件设备比如 F5 来实现负载均衡功能(硬件的价格一般很贵)。 +3. **软件负载均衡** :通过负载均衡软件比如 Nginx 来实现负载均衡功能。 + +## 推荐阅读 + +- [《凤凰架构》-负载均衡](http://icyfenix.cn/architect-perspective/general-architecture/diversion-system/load-balancing.html) diff --git a/docs/idea-tutorial/idea-plugins/camel-case.md b/docs/idea-tutorial/idea-plugins/camel-case.md new file mode 100644 index 00000000000..5456378f4b2 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/camel-case.md @@ -0,0 +1,27 @@ +--- +title: Camel Case:命名之间快速切换 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + +非常有用!这个插件可以实现包含6种常见命名格式之间的切换。并且,你还可以对转换格式进行相关配置(转换格式),如下图所示: + +![img](./pictures/camel-case/camel-case1.png) + +有了这个插件之后,你只需要使用快捷键 `shift+option+u(mac)` / `shift+alt+u` 对准你要修改的变量或者方法名字,就能实现在多种格式之间切换了,如下图所示: + +![](./pictures/camel-case/camel-case2.gif) + +如果你突然忘记快捷键的话,可以直接在IDEA的菜单栏的 Edit 部分找到。 + +![](./pictures/camel-case/camel-case3.png) + +使用这个插件对开发效率提升高吗?拿我之前项目组的情况举个例子: + +我之前有一个项目组的测试名字是驼峰这种形式: `ShouldReturnTicketWhenRobotSaveBagGiven1LockersWith2FreeSpace` 。但是,使用驼峰形式命名测试方法的名字不太明显,一般建议用下划线_的形式: `should_return_ticket_when_robot_save_bag_given_1_lockers_with_2_free_space` + +如果我们不用这个插件,而是手动去一个一个改的话,工作量想必会很大,而且正确率也会因为手工的原因降低。 + +> diff --git a/docs/idea-tutorial/idea-plugins/code-glance.md b/docs/idea-tutorial/idea-plugins/code-glance.md new file mode 100644 index 00000000000..9345ab427bb --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/code-glance.md @@ -0,0 +1,11 @@ +--- +title: CodeGlance:代码微型地图 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + +CodeGlance提供一个代码的微型地图,当你的类比较多的时候可以帮忙你快速定位到要去的位置。这个插件在我们日常做普通开发的时候用处不大,不过,在你阅读源码的时候还是很有用的,如下图所示: + +![](./pictures/code-glance.png) \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/code-statistic.md b/docs/idea-tutorial/idea-plugins/code-statistic.md new file mode 100644 index 00000000000..1d60c81bad6 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/code-statistic.md @@ -0,0 +1,39 @@ +--- +title: Statistic:项目代码统计 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + +编程是一个很奇妙的事情,大部分的我们把大部分时间实际都花在了复制粘贴,而后修改代码上面。 + +很多时候,我们并不关注代码质量,只要功能能实现,我才不管一个类的代码有多长、一个方法的代码有多长。 + +因此,我们经常会碰到让自己想要骂街的项目,不过,说真的,你自己写的代码也有极大可能被后者 DISS。 + +为了快速分析项目情况,判断这个项目是不是一个“垃圾”项目,有一个方法挺简单的。 + +那就是**对代码的总行数、单个文件的代码行数、注释行数等信息进行统计。** + +**怎么统计呢?** + +首先想到的是 Excel 。不过,显然太麻烦了。 + +**有没有专门用于代码统计的工具呢?** + +基于Perl语言开发的cloc(count lines of code)或许可以满足你的要求。 + +**有没有什么更简单的办法呢?** + +如果你使用的是 IDEA 进行开发的话,推荐你可以使用一下 **Statistic** 这个插件。 + +有了这个插件之后你可以非常直观地看到你的项目中所有类型的文件的信息比如数量、大小等等,可以帮助你更好地了解你们的项目。 + +![](./pictures/Statistic1.png) + +你还可以使用它看所有类的总行数、有效代码行数、注释行数、以及有效代码比重等等这些东西。 + +![](./pictures/Statistic2.png) + +如果,你担心插件过多影响IDEA速度的话,可以只在有代码统计需求的时候开启这个插件,其他时间禁用它就完事了! \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/git-commit-template.md b/docs/idea-tutorial/idea-plugins/git-commit-template.md new file mode 100644 index 00000000000..c75dae11c79 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/git-commit-template.md @@ -0,0 +1,19 @@ +--- +title: Git Commit Template:提交代码格式规范 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + +没有安装这个插件之前,我们使用IDEA提供的Commit功能提交代码是下面这样的: + +![](./pictures/git-commit-template/Git-Commit-Template1.png) + +使用了这个插件之后是下面这样的,提供了一个commit信息模板的输入框: + +![](./pictures/git-commit-template/Git-Commit-Template2.png) + +完成之后的效果是这样的: + +![](./pictures/git-commit-template/Git-Commit-Template3.png) \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/gson-format.md b/docs/idea-tutorial/idea-plugins/gson-format.md new file mode 100644 index 00000000000..56750e1cb05 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/gson-format.md @@ -0,0 +1,32 @@ +--- +title: GsonFormat:JSON转对象 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + +GsonFormat 这个插件可以根据Gson库使用的要求,将JSONObject格式的String 解析成实体类。 + +> 说明:2021.x 版本以上的 IDEA 可以使用:GsonFormatPlus + +这个插件使用起来非常简单,我们新建一个类,然后在类中使用快捷键 `option + s`(Mac)或`alt + s` (win)调出操作窗口(**必须在类中使用快捷键才有效**),如下图所示。 + +![](./pictures/GsonFormat2.gif) + +这个插件是一个国人几年前写的,不过已经很久没有更新了,可能会因为IDEA的版本问题有一些小Bug。而且,这个插件无法将JSON转换为Kotlin(这个其实无关痛痒,IDEA自带的就有Java转Kotlin的功能)。 + +![](./pictures/GsonFormat1.png) + +另外一个与之相似的插件是 **:RoboPOJOGenerator** ,这个插件的更新频率比较快。 + +`File-> new -> Generate POJO from JSON` + +![](./pictures/RoboPOJOGenerator1.png) + +然后将JSON格式的数据粘贴进去之后,配置相关属性之后选择“*Generate*” + +![](./pictures/RoboPOJOGenerator2.png) + + + diff --git a/docs/idea-tutorial/idea-plugins/idea-features-trainer.md b/docs/idea-tutorial/idea-plugins/idea-features-trainer.md new file mode 100644 index 00000000000..a5cb4960c4d --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/idea-features-trainer.md @@ -0,0 +1,17 @@ +--- +title: IDE Features Trainer:IDEA 交互式教程 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + +**有了这个插件之后,你可以在 IDE 中以交互方式学习IDEA最常用的快捷方式和最基本功能。** 非常非常非常方便!强烈建议大家安装一个,尤其是刚开始使用IDEA的朋友。 + +当我们安装了这个插件之后,你会发现我们的IDEA 编辑器的右边多了一个“**Learn**”的选项,我们点击这个选项就可以看到如下界面。 + +![](./pictures/IDE-Features-Trainer1.png?lastModify=1633856821) + +我们选择“Editor Basics”进行,然后就可以看到如下界面,这样你就可以按照指示来练习了!非常不错! + +![](./pictures/IDE-Features-Trainer2.png?lastModify=1633856821) \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/idea-themes.md b/docs/idea-tutorial/idea-plugins/idea-themes.md new file mode 100644 index 00000000000..ca38f19f51f --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/idea-themes.md @@ -0,0 +1,99 @@ +--- +title: IDEA主题推荐 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + +经常有小伙伴问我:“Guide哥,你的IDEA 主题怎么这么好看,能推荐一下不?”。就实在有点不耐烦了,才索性写了这篇文章。 + +在这篇文章中,我精选了几个比较是和 Java 编码的 IDEA 主题供小伙伴们选择。另外,我自己用的是 One Dark theme 这款。 + +**注意:以下主题按照使用人数降序排序。** + +## [Material Theme UI](https://plugins.jetbrains.com/plugin/8006-material-theme-ui) + +**推荐指数** :⭐⭐⭐⭐ + +这是 IDEA 中使用人数最多的一款主题。 + +当你安装完这个插件之后,你会发现这个主题本身又提供了多种相关的主题供你选择。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-3@main/11-20/image-20201119182935201.png) + + **Material Deep Ocean** 这款的效果图如下。默认的字体是真的小,小伙伴们需要自行调整一下。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-3@main/11-20/image-20201119183044068.png) + +## [One Dark theme](https://plugins.jetbrains.com/plugin/11938-one-dark-theme) + +**推荐指数** :⭐⭐⭐⭐⭐ + +我比较喜欢的一款(*黄色比较多?*)。 没有花里花哨,简单大气,看起来比较舒服。颜色搭配也很棒,适合编码! + +这款主题的效果图如下。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-3@main/11-20-11/image-20201119180300975.png) + +## [Gradianto](https://plugins.jetbrains.com/plugin/12334-gradianto) + +**推荐指数** :⭐⭐⭐⭐⭐ + +Gradianto这个主题的目标是在保持页面色彩比较层次分明的情况下,让我们因为代码而疲惫的双眼更加轻松。 + +Gradianto附带了自然界的渐变色,看着挺舒服的。另外,这个主题本身也提供了多种相关的主题供你选择。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-3@main/11-20/image-20201119183825753.png) + +**Gradianto Nature Green** 的效果图如下。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-3@main/11-20/image-20201119183920889.png) + +## [Dark Purple Theme](https://plugins.jetbrains.com/plugin/12100-dark-purple-theme) + +**推荐指数** :⭐⭐⭐⭐⭐ + +这是一款紫色色调的深色主题,喜欢紫色的小伙伴不要错过。 + +这个主题的效果图如下。个人觉得整体颜色搭配的是比较不错的,适合编码! + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-3@main/11-20-11/image-20201119184654132.png) + +## [Hiberbee Theme](https://plugins.jetbrains.com/plugin/12118-hiberbee-theme) + +**推荐指数** :⭐⭐⭐⭐⭐ + +一款受到了 Monokai Pro 和 MacOS Mojave启发的主题,是一款色彩层次分明的深色主题。 + +这个主题的效果图如下。看着也是非常赞!适合编码! + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-3@main/11-20-11/image-20201119191441248.png) + +上面推荐的都是偏暗色系的主题,这里我再推荐两款浅色系的主题。 + +## [Gray Theme](https://plugins.jetbrains.com/plugin/12103-gray-theme) + +**推荐指数** :⭐⭐⭐ + +这是一款对比度比较低的一款浅色主题,不太适合代码阅读,毕竟这款主题是专门为在IntelliJ IDE中使用Markdown而设计的。 + +这个主题的效果图如下。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-3@main/11-20-11/image-20201119194512164.png) + +## [Roboticket Light Theme](https://plugins.jetbrains.com/plugin/12191-roboticket-light-theme) + +**推荐指数** :⭐⭐⭐ + +这是一款对比度比较低的浅色主题,不太适合代码阅读。 + +这个主题的效果图如下。 + +![](https://img-blog.csdnimg.cn/img_convert/194581c7b72d49b512b12814340f00c8.png) + +## 后记 + +我个人还是比较偏爱深色系的主题。 + +小伙伴们比较喜欢哪款主题呢?可以在评论区简单聊聊不?如果你还有其他比较喜欢的主题也可以在评论区说出来供大家参考哦! \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/improve-code.md b/docs/idea-tutorial/idea-plugins/improve-code.md new file mode 100644 index 00000000000..91b31b4e232 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/improve-code.md @@ -0,0 +1,153 @@ +--- +title: IDEA 代码优化插件推荐 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 + - 代码优化 +--- + +## Lombok:帮你简化代码 + +之前没有推荐这个插件的原因是觉得已经是人手必备的了。如果你要使用 Lombok 的话,不光是要安装这个插件,你的项目也要引入相关的依赖。 + +```xml + + org.projectlombok + lombok + true + +``` + +使用 Lombok 能够帮助我们少写很多代码比如 Getter/Setter、Constructor等等。 + +关于Lombok的使用,可以查看这篇文章:[《十分钟搞懂Java效率工具Lombok使用与原理》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485385&idx=2&sn=a7c3fb4485ffd8c019e5541e9b1580cd&chksm=cea24802f9d5c1144eee0da52cfc0cc5e8ee3590990de3bb642df4d4b2a8cd07f12dd54947b9&token=1667678311&lang=zh_CN#rd)。 + + +## Codota:代码智能提示 + +我一直在用的一个插件,可以说非常好用了(*我身边的很多大佬平时写代码也会用这个插件*)。 + +Codota 这个插件用于智能代码补全,它基于数百万Java程序,能够根据程序上下文提示补全代码。相比于IDEA自带的智能提示来说,Codota 的提示更加全面一些。 + +如果你觉得 IDEA 插件安装的太多比较卡顿的话,不用担心!Codota 插件还有一个对应的在线网站([https://www.codota.com/code](https://www.codota.com/code)),在这个网站上你可以根据代码关键字搜索相关代码示例,非常不错! + +我在工作中经常会用到,说实话确实给我带来了很大便利,比如我们搜索 `Files.readAllLines`相关的代码,搜索出来的结果如下图所示: + +![](./pictures/Codota3.png) + +另外,Codota 插件的基础功能都是免费的。你的代码也不会被泄露,这点你不用担心。 + +简单来看看 Codota 插件的骚操作吧! + +### 代码智能补全 + +我们使用`HttpUrlConnection ` 建立一个网络连接是真的样的: + +![](./pictures/Codota1.gif) + +我们创建线程池现在变成下面这样: + +![](./pictures/Codota4.gif) + +上面只是为了演示这个插件的强大,实际上创建线程池不推荐使用这种方式, 推荐使用 `ThreadPoolExecutor` 构造函数创建线程池。我下面要介绍的一个阿里巴巴的插件-**Alibaba Java Code Guidelines** 就检测出来了这个问题,所以,`Executors`下面用波浪线标记了出来。 + +### 代码智能搜索 + +除了,在写代码的时候智能提示之外。你还可以直接选中代码然后搜索相关代码示例。 + +![](./pictures/Codota2.png) + +## Alibaba Java Code Guidelines:阿里巴巴 Java 代码规范 + +阿里巴巴 Java 代码规范,对应的Github地址为:[https://github.com/alibaba/p3c](https://github.com/alibaba/p3c ) 。非常推荐安装! + +安装完成之后建议将与语言替换成中文,提示更加友好一点。 + +![](./pictures/p3c/Alibaba-Java-Code-Guidelines2.png) + +根据官方描述: + +> 目前这个插件实现了开发手册中的的53条规则,大部分基于PMD实现,其中有4条规则基于IDEA实现,并且基于IDEA [Inspection](https://www.jetbrains.com/help/idea/code-inspection.html)实现了实时检测功能。部分规则实现了Quick Fix功能,对于可以提供Quick Fix但没有提供的,我们会尽快实现,也欢迎有兴趣的同学加入进来一起努力。 目前插件检测有两种模式:实时检测、手动触发。 + +上述提到的开发手册也就是在Java开发领域赫赫有名的《阿里巴巴Java开发手册》。 + +### 手动配置检测规则 + +你还可以手动配置相关 inspection规则: + +![](./pictures/p3c/Alibaba-Java-Code-Guidelines3.png) + +### 使用效果 + +这个插件会实时检测出我们的代码不匹配它的规则的地方,并且会给出修改建议。比如我们按照下面的方式去创建线程池的话,这个插件就会帮我们检测出来,如下图所示。 + +![](./pictures/p3c/Alibaba-Java-Code-Guidelines1.png) + +这个可以对应上 《阿里巴巴Java开发手册》 这本书关于创建线程池的方式说明。 + +![](./pictures/p3c/阿里巴巴开发手册-线程池创建.png) + +## CheckStyle: Java代码格式规范 + +### 为何需要CheckStyle插件? + +**CheckStyle 几乎是 Java 项目开发必备的一个插件了,它会帮助我们检查 Java 代码的格式是否有问题比如变量命名格式是否有问题、某一行代码的长度是否过长等等。** + +在项目上,**通过项目开发人员自我约束来规范代码格式必然是不靠谱的!** 因此,我们非常需要这样一款工具来帮助我们规范代码格式。 + +如果你看过我写的轮子的话,可以发现我为每一个项目都集成了 CheckStyle,并且设置了 **Git Commit 钩子**,保证在提交代码之前代码格式没有问题。 + +> **Guide哥造的轮子**(*代码简洁,结构清晰,欢迎学习,欢迎一起完善*): +> +> 1. [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) :A custom RPC framework implemented by Netty+Kyro+Zookeeper.(一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架-附详细实现过程和相关教程) +> 2. [jsoncat](https://github.com/Snailclimb/jsoncat) :仿 Spring Boot 但不同于 Spring Boot 的一个轻量级的 HTTP 框架 +> +> **Git 钩子**: Git 能在特定的重要动作比如commit、push发生时触发自定义脚本。 钩子都被存储在 Git 目录下的 `hooks` 子目录中。 也即绝大部分项目中的 `.git/hooks` 。 + +### 如何在Maven/Gradle项目中集成 Checksytle? + +一般情况下,我们都是将其集成在项目中,并设置相应的 Git 钩子。网上有相应的介绍文章,这里就不多提了。 + +如果你觉得网上的文章不直观的话,可以参考我上面提到了两个轮子: + +1. [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) :Maven项目集成 Checksytle。 +2. [jsoncat](https://github.com/Snailclimb/jsoncat) :Gradle项目集成 Checksytle。 + +如果你在项目中集成了 Checksytle 的话,每次检测会生成一个 HTML格式的文件告诉你哪里的代码格式不对,这样看着非常不直观。通过 Checksytle插件的话可以非常直观的将项目中存在格式问题的地方显示出来。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/IDEA%E6%8F%92%E4%BB%B6/image-20201013135044410.png) + +如果你只是自己在本地使用,并不想在项目中集成 Checksytle 的话也可以,只需要下载一个 Checksytle插件就足够了。 + +### 如何安装? + +我们直接在 IDEA 的插件市场即可找到这个插件。我这里已经安装好了。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/IDEA%E6%8F%92%E4%BB%B6/image-20201013103610557.png) + +安装插件之后重启 IDEA,你会发现就可以在底部菜单栏找到 CheckStyle 了。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/IDEA%E6%8F%92%E4%BB%B6/image-20201013134644991.png) + +### 如何自定义检测规则? + +如果你需要自定义代码格式检测规则的话,可以按照如下方式进行配置(你可以导入用于自定义检测规则的`CheckStyle.xml`文件)。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/IDEA%E6%8F%92%E4%BB%B6/setting-check-style.png) + +### 使用效果 + +配置完成之后,按照如下方式使用这个插件! + +![run-check-style](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/IDEA%E6%8F%92%E4%BB%B6/run-check-style.png) + +可以非常清晰的看到:CheckStyle 插件已经根据我们自定义的规则将项目中的代码存在格式问题的地方都检测了出来。 + +## SonarLint:帮你优化代码 + +SonarLint 帮助你发现代码的错误和漏洞,就像是代码拼写检查器一样,SonarLint 可以实时显示出代码的问题,并提供清晰的修复指导,以便你提交代码之前就可以解决它们。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-2@main/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/image-20201019222817359.png) + +并且,很多项目都集成了 SonarQube,SonarLint 可以很方便地与 SonarQube 集成。 \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/interface-beautification.md b/docs/idea-tutorial/idea-plugins/interface-beautification.md new file mode 100644 index 00000000000..3f3f62fb679 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/interface-beautification.md @@ -0,0 +1,67 @@ +--- +title: IDEA 界面美化插件推荐 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 + - 代码优化 +--- + + +## Background Image Plus:背景图片 + +我这里推荐使用国人 Jack Chu 基于 Background Image Plus 开发的最新版本,适用于 2021.x 版本的 IDEA。 + +前面几个下载量比较高的,目前都还未支持 2021.x 版本的 IDEA。 + +![Background Image Plus](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/image-20211010174138279.png) + +通过这个插件,你可以将 IDEA 背景设置为指定的图片,支持随机背景。 + +效果图如下: + +![Background Image Plus 设置背景效果图](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/image-20211010173730828.png) + +如果你想要设置随机背景的话,可以通过 IDEA 设置页 **Settings** -> **Appearance & Behaviour** -> **Background Image Plus** 自定义设置项,随机显示目录下的图片为背景图。 + +## Power Mode II : 代码特效 + +使用了这个插件之后,写代码会自带特效,适用于 2021.x 版本的 IDEA。 2021.x 版本之前,可以使用 **activate-power-mode** 。 + +![Power Mode II 效果图](./pictures/power-mode/Power-Mode-II.gif) + +你可以通过 IDEA 设置页 **Settings** -> **Appearance & Behaviour** -> **Power Mode II** 自定义设置项。 + +![Power Mode II](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/image-20211010175304108.png) + +## Nyan Progress Bar : 进度条美化 + +可以让你拥有更加漂亮的进度条。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/image-20211010175434133.png) + +## Grep Console:控制台输出处理 + +可以说是必备的一个 IDEA 插件,非常实用! + +这个插件主要的功能有两个: + +**1. 自定义设置控制台输出颜色** + +我们可以在设置中进行相关的配置: + +![](./pictures/grep-console/grep-console2.png) + +配置完成之后的 log warn 的效果对比图如下: + +![](./pictures/grep-console/grep-console3.png) + +**2. 过滤控制台输出** + +![](./pictures/grep-console/grep-console.gif) + +## Rainbow Brackets : 彩虹括号 + +使用各种鲜明的颜色来展示你的括号,效果图如下。可以看出代码层级变得更加清晰了,可以说非常实用友好了! + +![](./pictures/rainbow-brackets.png) \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/jclasslib.md b/docs/idea-tutorial/idea-plugins/jclasslib.md new file mode 100644 index 00000000000..c5f29d2b657 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/jclasslib.md @@ -0,0 +1,93 @@ +--- +title: jclasslib :一款IDEA字节码查看神器 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 + - 字节码 +--- + +由于后面要分享的一篇文章中用到了这篇文章要推荐的一个插件,所以这里分享一下。非常实用!你会爱上它的! + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/%E8%AE%A1%E7%AE%97%E6%9C%BA%E4%B8%93%E4%B8%9A%E5%AD%A6%E4%B9%A0%E6%8C%87%E5%8D%97/image-20201013084919965.png) + +**开始推荐 IDEA 字节码查看神器之前,先来回顾一下 Java 字节码是啥。** + +## 何为 Java 字节码? + +Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 + +**什么是字节码?采用字节码的好处是什么?** + +> 在 Java 中,JVM 可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 + +**Java 程序从源代码到运行一般有下面 3 步:** + +![Java程序运行过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png) + +## 为什么要查看 Java 字节码? + +我们在平时学习的时候,经常需要查看某个 java 类的字节码文件。查看字节码文件更容易让我们搞懂 java 代码背后的原理比如搞懂 java 中的各种语法糖的本质。 + +## 如何查看 Java 字节码? + +如果我们通过命令行来查看某个 class 的字节码文件的话,可以直接通过 `javap` 命令,不过这种方式太原始了,效率十分低,并且看起来不直观。 + +下面介绍两种使用 IDEA 查看类对应字节码文件的方式(_`javap`这种方式就不提了_)。 + +我们以这段代码作为案例: + +```java +public class Main { + public static void main(String[] args) { + Integer i = null; + Boolean flag = false; + System.out.println(flag ? 0 : i); + } +} +``` + +上面这段代码由于使用三目运算符不当导致诡异了 NPE 异常。为了搞清楚事情的原因,我们来看其对应的字节码。 + +### 使用 IDEA 自带功能 + +我们点击 `View -> Show Bytecode` 即可通过 IDEA 查看某个类对应的字节码文件。 + +> 需要注意的是:**查看某个类对应的字节码文件之前确保它已经被编译过。** + +![使用IDEA自带功能查看Java字节码](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/IDEA%E6%8F%92%E4%BB%B6/image-20201012143530226.png) + +稍等几秒钟之后,你就可以直观到看到对应的类的字节码内容了。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-10/image-20201012145127465.png) + +从字节码中可以看出,我圈出来的位置发生了 **拆箱操作** 。 + +> - **装箱**:将基本类型用它们对应的引用类型包装起来; +> - **拆箱**:将包装类型转换为基本数据类型; + +详细解释下就是:`flag ? 0 : i` 这行代码中,0 是基本数据类型 int,返回数据的时候 i 会被强制拆箱成 int 类型,由于 i 的值是 null,因此就抛出了 NPE 异常。 + +```java +Integer i = null; +Boolean flag = false; +System.out.println(flag ? 0 : i); +``` + +如果,我们把代码中 `flag` 变量的值修改为 true 的话,就不会存在 NPE 问题了,因为会直接返回 0,不会进行拆箱操作。 + +### 使用 IDEA 插件 jclasslib(推荐) + +相比于 IDEA 自带的查看类字节的功能,我更推荐 `jclasslib` 这个插件,非常棒! + +**使用 `jclasslib` 不光可以直观地查看某个类对应的字节码文件,还可以查看类的基本信息、常量池、接口、属性、函数等信息。** + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-10/image-20201012145646086.png) + +我们直接在 IDEA 的插件市场即可找到这个插件。我这里已经安装好了。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-10-12/image-20201012145900911.png) + +安装完成之后,重启 IDEA。点击`View -> Show Bytecode With jclasslib` 即可通过`jclasslib` 查看某个类对应的字节码文件。 + +![使用IDEA插件jclasslib查看类的字节码](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/%E8%AE%A1%E7%AE%97%E6%9C%BA%E4%B8%93%E4%B8%9A%E5%AD%A6%E4%B9%A0%E6%8C%87%E5%8D%97/image-20201012150252106.png) \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/maven-helper.md b/docs/idea-tutorial/idea-plugins/maven-helper.md new file mode 100644 index 00000000000..d2b064a9934 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/maven-helper.md @@ -0,0 +1,19 @@ +--- +title: Maven Helper:解决 Maven 依赖冲突问题 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 + - Maven +--- + + +**Maven Helper** 主要用来分析 Maven 项目的相关依赖,可以帮助我们解决 Maven 依赖冲突问题。 + +![](./pictures/maver-helper.png) + +**何为依赖冲突?** + +说白了就是你的项目使用的 2 个 jar 包引用了同一个依赖 h,并且 h 的版本还不一样,这个时候你的项目就存在两个不同版本的 h。这时 Maven 会依据依赖路径最短优先原则,来决定使用哪个版本的 Jar 包,而另一个无用的 Jar 包则未被使用,这就是所谓的依赖冲突。 + +大部分情况下,依赖冲突可能并不会对系统造成什么异常,因为 Maven 始终选择了一个 Jar 包来使用。但是,不排除在某些特定条件下,会出现类似找不到类的异常,所以,只要存在依赖冲突,在我看来,最好还是解决掉,不要给系统留下隐患。 diff --git a/docs/idea-tutorial/idea-plugins/others.md b/docs/idea-tutorial/idea-plugins/others.md new file mode 100644 index 00000000000..da505ff8dfe --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/others.md @@ -0,0 +1,21 @@ +--- +title: 其他 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + + +1. **leetcode editor** :提供在线 Leetcode 刷题功能,比较方便我们刷题,不过我试用之后发现有一些小 bug,个人感觉还是直接在网站找题目刷来的痛快一些。 +2. **​A Search with Github** :直接通过 Github搜索相关代码。 +3. **stackoverflow** : 选中相关内容后单击右键即可快速跳转到 stackoverflow 。 +4. **CodeStream** :让code review变得更加容易。 +5. **Code screenshots** :代码片段保存为图片。 +6. **GitToolBox** :Git工具箱 +7. **OK,​ Gradle!** :搜索Java库用于Gradle项目 +8. **Java Stream Debugger** : Java8 Stream调试器 +9. **EasyCode** : Easycode 可以直接对数据的表生成entity、controller、service、dao、mapper无需任何编码,简单而强大。更多内容可以查看这篇文章:[《懒人 IDEA 插件插件:EasyCode 一键帮你生成所需代码~》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486205&idx=1&sn=0ff2f87f0d82a1bd9c0c44328ef69435&chksm=cea24536f9d5cc20c6cc7669f0d4167d747fe8b8c05a64546c0162d694aa96044a2862e24b57&token=1862674725&lang=zh_CN#rd) +10. **JFormDesigner** :Swing GUI 在线编辑器。 +11. **VisualVM Launcher** : Java性能分析神器。 +12. ...... diff --git a/docs/idea-tutorial/idea-plugins/pictures/Codota1.gif b/docs/idea-tutorial/idea-plugins/pictures/Codota1.gif new file mode 100644 index 00000000000..7b2947fe3a0 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/Codota1.gif differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/Codota2.png b/docs/idea-tutorial/idea-plugins/pictures/Codota2.png new file mode 100644 index 00000000000..0fe37e36047 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/Codota2.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/Codota3.png b/docs/idea-tutorial/idea-plugins/pictures/Codota3.png new file mode 100644 index 00000000000..44d1093e492 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/Codota3.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/Codota4.gif b/docs/idea-tutorial/idea-plugins/pictures/Codota4.gif new file mode 100644 index 00000000000..1322b60f5e0 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/Codota4.gif differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/GsonFormat1.png b/docs/idea-tutorial/idea-plugins/pictures/GsonFormat1.png new file mode 100644 index 00000000000..c8e678acb68 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/GsonFormat1.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/GsonFormat2.gif b/docs/idea-tutorial/idea-plugins/pictures/GsonFormat2.gif new file mode 100644 index 00000000000..7c371162d9e Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/GsonFormat2.gif differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/IDE-Features-Trainer1.png b/docs/idea-tutorial/idea-plugins/pictures/IDE-Features-Trainer1.png new file mode 100644 index 00000000000..27f888a9499 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/IDE-Features-Trainer1.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/IDE-Features-Trainer2.png b/docs/idea-tutorial/idea-plugins/pictures/IDE-Features-Trainer2.png new file mode 100644 index 00000000000..6d59082c281 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/IDE-Features-Trainer2.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/JavaStreamDebugger.gif b/docs/idea-tutorial/idea-plugins/pictures/JavaStreamDebugger.gif new file mode 100644 index 00000000000..6e910e72ed5 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/JavaStreamDebugger.gif differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/Presentation-Assistant.gif b/docs/idea-tutorial/idea-plugins/pictures/Presentation-Assistant.gif new file mode 100644 index 00000000000..335523ea5f9 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/Presentation-Assistant.gif differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit1.png b/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit1.png new file mode 100644 index 00000000000..5a69bc0595a Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit1.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit2.png b/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit2.png new file mode 100644 index 00000000000..6c8aefd7638 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit2.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit3.png b/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit3.png new file mode 100644 index 00000000000..b6cf628e76a Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit3.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit4.png b/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit4.png new file mode 100644 index 00000000000..be15f46bdd2 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/RestfulToolkit4.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/RoboPOJOGenerator1.png b/docs/idea-tutorial/idea-plugins/pictures/RoboPOJOGenerator1.png new file mode 100644 index 00000000000..c2d7704766b Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/RoboPOJOGenerator1.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/RoboPOJOGenerator2.png b/docs/idea-tutorial/idea-plugins/pictures/RoboPOJOGenerator2.png new file mode 100644 index 00000000000..4334b3db390 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/RoboPOJOGenerator2.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/Statistic1.png b/docs/idea-tutorial/idea-plugins/pictures/Statistic1.png new file mode 100644 index 00000000000..47521ee25dd Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/Statistic1.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/Statistic2.png b/docs/idea-tutorial/idea-plugins/pictures/Statistic2.png new file mode 100644 index 00000000000..f815aa1c722 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/Statistic2.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/camel-case/camel-case1.png b/docs/idea-tutorial/idea-plugins/pictures/camel-case/camel-case1.png new file mode 100644 index 00000000000..7fbbbba97e5 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/camel-case/camel-case1.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/camel-case/camel-case2.gif b/docs/idea-tutorial/idea-plugins/pictures/camel-case/camel-case2.gif new file mode 100644 index 00000000000..9565231e9d7 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/camel-case/camel-case2.gif differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/camel-case/camel-case3.png b/docs/idea-tutorial/idea-plugins/pictures/camel-case/camel-case3.png new file mode 100644 index 00000000000..d4b2fd27ab3 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/camel-case/camel-case3.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/check-style.png b/docs/idea-tutorial/idea-plugins/pictures/check-style.png new file mode 100644 index 00000000000..e0c17b64096 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/check-style.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/code-glance.png b/docs/idea-tutorial/idea-plugins/pictures/code-glance.png new file mode 100644 index 00000000000..afdf1a1bca0 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/code-glance.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/git-commit-template/Git-Commit-Template1.png b/docs/idea-tutorial/idea-plugins/pictures/git-commit-template/Git-Commit-Template1.png new file mode 100644 index 00000000000..26da6cd1b06 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/git-commit-template/Git-Commit-Template1.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/git-commit-template/Git-Commit-Template2.png b/docs/idea-tutorial/idea-plugins/pictures/git-commit-template/Git-Commit-Template2.png new file mode 100644 index 00000000000..c0e436432a5 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/git-commit-template/Git-Commit-Template2.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/git-commit-template/Git-Commit-Template3.png b/docs/idea-tutorial/idea-plugins/pictures/git-commit-template/Git-Commit-Template3.png new file mode 100644 index 00000000000..17f81a23469 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/git-commit-template/Git-Commit-Template3.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/grep-console/grep-console.gif b/docs/idea-tutorial/idea-plugins/pictures/grep-console/grep-console.gif new file mode 100644 index 00000000000..293c134207f Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/grep-console/grep-console.gif differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/grep-console/grep-console2.png b/docs/idea-tutorial/idea-plugins/pictures/grep-console/grep-console2.png new file mode 100644 index 00000000000..aa338d615ee Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/grep-console/grep-console2.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/grep-console/grep-console3.png b/docs/idea-tutorial/idea-plugins/pictures/grep-console/grep-console3.png new file mode 100644 index 00000000000..411128ed121 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/grep-console/grep-console3.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/maver-helper.png b/docs/idea-tutorial/idea-plugins/pictures/maver-helper.png new file mode 100644 index 00000000000..35a3f6e083f Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/maver-helper.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/p3c/Alibaba-Java-Code-Guidelines1.png b/docs/idea-tutorial/idea-plugins/pictures/p3c/Alibaba-Java-Code-Guidelines1.png new file mode 100644 index 00000000000..67c3571d836 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/p3c/Alibaba-Java-Code-Guidelines1.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/p3c/Alibaba-Java-Code-Guidelines2.png b/docs/idea-tutorial/idea-plugins/pictures/p3c/Alibaba-Java-Code-Guidelines2.png new file mode 100644 index 00000000000..e4b1dc8c9a8 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/p3c/Alibaba-Java-Code-Guidelines2.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/p3c/Alibaba-Java-Code-Guidelines3.png b/docs/idea-tutorial/idea-plugins/pictures/p3c/Alibaba-Java-Code-Guidelines3.png new file mode 100644 index 00000000000..5213aff02ca Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/p3c/Alibaba-Java-Code-Guidelines3.png differ diff --git "a/docs/idea-tutorial/idea-plugins/pictures/p3c/\351\230\277\351\207\214\345\267\264\345\267\264\345\274\200\345\217\221\346\211\213\345\206\214-\347\272\277\347\250\213\346\261\240\345\210\233\345\273\272.png" "b/docs/idea-tutorial/idea-plugins/pictures/p3c/\351\230\277\351\207\214\345\267\264\345\267\264\345\274\200\345\217\221\346\211\213\345\206\214-\347\272\277\347\250\213\346\261\240\345\210\233\345\273\272.png" new file mode 100644 index 00000000000..4d18c60b055 Binary files /dev/null and "b/docs/idea-tutorial/idea-plugins/pictures/p3c/\351\230\277\351\207\214\345\267\264\345\267\264\345\274\200\345\217\221\346\211\213\345\206\214-\347\272\277\347\250\213\346\261\240\345\210\233\345\273\272.png" differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/power-mode/Power-Mode-II.gif b/docs/idea-tutorial/idea-plugins/pictures/power-mode/Power-Mode-II.gif new file mode 100644 index 00000000000..026c32e947e Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/power-mode/Power-Mode-II.gif differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/rainbow-brackets.png b/docs/idea-tutorial/idea-plugins/pictures/rainbow-brackets.png new file mode 100644 index 00000000000..6529899a3ed Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/rainbow-brackets.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/save-actions/save-actions.png b/docs/idea-tutorial/idea-plugins/pictures/save-actions/save-actions.png new file mode 100644 index 00000000000..cf765cb6d0e Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/save-actions/save-actions.png differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/save-actions/save-actions2.gif b/docs/idea-tutorial/idea-plugins/pictures/save-actions/save-actions2.gif new file mode 100644 index 00000000000..93ae62cf6c2 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/save-actions/save-actions2.gif differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/translation/translation1.jpg b/docs/idea-tutorial/idea-plugins/pictures/translation/translation1.jpg new file mode 100644 index 00000000000..7b512c115b1 Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/translation/translation1.jpg differ diff --git a/docs/idea-tutorial/idea-plugins/pictures/translation/translation2.png b/docs/idea-tutorial/idea-plugins/pictures/translation/translation2.png new file mode 100644 index 00000000000..c92664718dd Binary files /dev/null and b/docs/idea-tutorial/idea-plugins/pictures/translation/translation2.png differ diff --git a/docs/idea-tutorial/idea-plugins/rest-devlop.md b/docs/idea-tutorial/idea-plugins/rest-devlop.md new file mode 100644 index 00000000000..329552b33b2 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/rest-devlop.md @@ -0,0 +1,96 @@ +--- +title: RestfulToolkit:RESTful Web 服务辅助开发工具 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + + +开始推荐这个 IDEA 插件之前,我觉得有必要花一小会时间简单聊聊 **REST** 这个我们经常打交道的概念。 + +## REST 相关概念解读 + +### 何为 REST? + +REST 即 **REpresentational State Transfer** 的缩写。这个词组的翻译过来就是"**表现层状态转化**"。 + +这样理解起来甚是晦涩,实际上 REST 的全称是 **Resource Representational State Transfer** ,直白地翻译过来就是 **“资源”在网络传输中以某种“表现形式”进行“状态转移”** 。 + +**有没有感觉很难理解?** + +没关系,看了我对 REST 涉及到的一些概念的解读之后你没准就能理解了! + +- **资源(Resource)** :我们可以把真实的对象数据称为资源。一个资源既可以是一个集合,也可以是单个个体。比如我们的班级 classes 是代表一个集合形式的资源,而特定的 class 代表单个个体资源。每一种资源都有特定的 URI(统一资源定位符)与之对应,如果我们需要获取这个资源,访问这个 URI 就可以了,比如获取特定的班级:`/class/12`。另外,资源也可以包含子资源,比如 `/classes/classId/teachers`:列出某个指定班级的所有老师的信息 +- **表现形式(Representational)**:"资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式比如 json,xml,image,txt 等等叫做它的"表现层/表现形式"。 +- **状态转移(State Transfer)** :大家第一眼看到这个词语一定会很懵逼?内心 BB:这尼玛是啥啊? **大白话来说 REST 中的状态转移更多地描述的服务器端资源的状态,比如你通过增删改查(通过 HTTP 动词实现)引起资源状态的改变。** (HTTP 协议是一个无状态的,所有的资源状态都保存在服务器端) + +### 何为 RESTful 架构? + +满足 REST 风格的架构设计就可以称为 RESTful 架构: + +1. 每一个 URI 代表一种资源; +2. 客户端和服务器之间,传递这种资源的某种表现形式比如 json,xml,image,txt 等等; +3. 客户端通过特定的 HTTP 动词,对服务器端资源进行操作,实现"表现层状态转化"。 + +### 何为 RESTful Web 服务? + +基于 REST 架构的 Web 服务就被称为 RESTful Web 服务。 + +## RESTful Web 服务辅助开发工具 + +### 安装 + +这个插件的名字叫做 “**RestfulToolkit**” 。我们直接在 IDEA 的插件市场即可找到这个插件。如下图所示。 + +> 如果你因为网络问题没办法使用 IDEA 自带的插件市场的话,也可以通过[IDEA 插件市场的官网](https://plugins.jetbrains.com/idea)手动下载安装。 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/41a9bfa18920403ba4db83e324f8621e~tplv-k3u1fbpfcp-zoom-1.image) + +### 简单使用 + +#### URL 跳转到对应方法 + +根据 URL 直接跳转到对应的方法定义 (Windows: `ctrl+\` or `ctrl+alt+n` Mac:`command+\` or `command+alt+n` )并且提供了一个服务的树形可视化显示窗口。 如下图所示。 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c26ea05ad472488fbf9eb21627964bee~tplv-k3u1fbpfcp-zoom-1.image) + +#### 作为 HTTP 请求工具 + +这个插件还可以作为一个简单的 http 请求工具来使用。如下图所示。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/RestfulToolkit2.png) + +#### 复制生成 URL、复制方法参数... + +这个插件还提供了生成 URL、查询参数、请求体(RequestBody)等功能。 + +举个例子。我们选中 `Controller` 中的某个请求对应的方法右击,你会发现多了几个可选项。当你选择`Generate & Copy Full URL`的话,就可以把整个请求的路径直接复制下来。eg:`http://localhost:9333/api/users?pageNum=1&pageSize=1` 。 + +![](pictures/RestfulToolkit3.png) + +#### 将 Java 类转换为对应的 JSON 格式 + +这个插件还为 Java 类上添加了 **Convert to JSON** 功能 。 + +我们选中的某个类对应的方法然后右击,你会发现多了几个可选项。 + +![](pictures/RestfulToolkit4.png) + +当我们选择`Convert to JSON`的话,你会得到如下 json 类型的数据: + +```json +{ + "username": "demoData", + "password": "demoData", + "rememberMe": true +} +``` + +## 后记 + +RESTFulToolkit 原作者不更新了,IDEA.201 及以上版本不再适配。 + +因此,国内就有一个大佬参考 RESTFulToolkit 开发了一款类似的插件——RestfulTool(功能较少一些,不过够用了)。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/image-20210830112030237.png) \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/save-actions.md b/docs/idea-tutorial/idea-plugins/save-actions.md new file mode 100644 index 00000000000..fc149ad301b --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/save-actions.md @@ -0,0 +1,23 @@ +--- +title: Save Actions:优化文件保存 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + + +真必备插件!可以帮助我们在保存文件的时候: + +1. 优化导入; +2. 格式化代码; +3. 执行一些quick fix +4. ...... + +这个插件是支持可配置的,我的配置如下: + +![](./pictures/save-actions/save-actions.png) + +实际使用效果如下: + +![](./pictures/save-actions/save-actions2.gif) \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/sequence-diagram.md b/docs/idea-tutorial/idea-plugins/sequence-diagram.md new file mode 100644 index 00000000000..050d6161163 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/sequence-diagram.md @@ -0,0 +1,91 @@ +--- +title: SequenceDiagram:一键可以生成时序图 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + + + +在平时的学习/工作中,我们会经常面临如下场景: + +1. 阅读别人的代码 +2. 阅读框架源码 +3. 阅读自己很久之前写的代码。 + +千万不要觉得工作就是单纯写代码,实际工作中,你会发现你的大部分时间实际都花在了阅读和理解已有代码上。 + +为了能够更快更清晰地搞清对象之间的调用关系,我经常需要用到序列图。手动画序列图还是很麻烦费时间的,不过 IDEA 提供了一个叫做**SequenceDiagram** 的插件帮助我们解决这个问题。通过 SequenceDiagram 这个插件,我们一键可以生成时序图。 + +## 何为序列图? + +网上对于序列图的定义有很多,我觉得都不太好理解,太抽象了。最神奇的是,大部分文章对于序列图的定义竟然都是一模一样,看来大家是充分发挥了写代码的“精髓”啊! + +我还是简单说一说我的理解吧!不过,说实话,我自己对于 Sequence Diagram 也不是很明朗。下面的描述如有问题和需要完善的地方,还请指出。 + +> **序列图**(Sequence Diagram),亦称为**循序图**,是一种[UML](https://zh.m.wikipedia.org/wiki/UML)行为图。表示系统执行某个方法/操作(如登录操作)时,对象之间的顺序调用关系。 +> +> 这个顺序调用关系可以这样理解:你需要执行系统中某个对象 a 提供的方法/操作 login(登录),但是这个对象又依赖了对象 b 提供的方法 getUser(获取用户)。因此,这里就有了 a -> b 调用关系之说。 + +再举两个例子来说一下! + +下图是微信支付的业务流程时序图。这个图描述了微信支付相关角色(顾客,商家...)在微信支付场景下,基础支付和支付的的顺序调用关系。 + +![](https://img-blog.csdnimg.cn/img_convert/3a48c8d17aea2064ff11b6d3fd1fb2cb.png) + +下图是我写的一个 HTTP 框架中的执行某个方法的序列图。这个图描述了我们在调用 `InterceptorFactory`类的 `loadInterceptors()` 方法的时候,所涉及到的类之间的调用关系。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MzM3Mjcy,size_16,color_FFFFFF,t_70.png) + +另外,国内一般更喜欢称呼序列图为"时序图"。 + +- 如果你按照纯翻译的角度来说, sequence 这个单词并无"时间"的意思,只有序列,顺序等意思,因此也有人说“时序图”的说法是不准确的。 +- 如果从定义角度来说,时序图这个描述是没问题的。因为 Sequence Diagram 中每条消息的触发时机确实是按照时间顺序执行的。 + +我觉得称呼 Sequence Diagram 为时序图或者序列图都是没问题的,不用太纠结。 + +## 哪些场景下需要查看类的时序图? + +我们在很多场景下都需要时序图,比如说: + +1. **阅读源码** :阅读源码的时候,你可能需要查看调用目标方法涉及的相关类的调用关系。特别是在代码的调用层级比较多的时候,对于我们理解源码非常有用。(_题外话:实际工作中,大部分时间实际我们都花在了阅读理解已有代码上。_) +2. **技术文档编写** :我们在写项目介绍文档的时候,为了让别人更容易理解你的代码,你需要根据核心方法为相关的类生成时序图来展示他们之间的调用关系。 +3. **梳理业务流程** :当我们的系统业务流程比较复杂的时候,我们可以通过序列图将系统中涉及的重要的角色和对象的之间关系可视化出来。 +4. ...... + +## 如何使用 IDEA 根据类中方法生成时序图? + +**通过 SequenceDiagram 这个插件,我们一键可以生成时序图。** + +并且,你还可以: + +1. 点击时序图中的类/方法即可跳转到对应的地方。 +2. 从时序图中删除对应的类或者方法。 +3. 将生成的时序图导出为 PNG 图片格式。 + +### 安装 + +我们直接在 IDEA 的插件市场即可找到这个插件。我这里已经安装好了。 + +> 如果你因为网络问题没办法使用 IDEA 自带的插件市场的话,也可以通过[IDEA 插件市场的官网](https://plugins.jetbrains.com/idea)手动下载安装。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20201021165654262.png) + +### 简单使用 + +1. 选中方法名(注意不要选类名),然后点击鼠标右键,选择 **Sequence Diagram** 选项即可! + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20201021170110697-20211010211819042.png) + +2. 配置生成的序列图的一些基本的参数比如调用深度之后,我们点击 ok 即可! + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/c5040f1105c762ddf8689892913bc02d-20211010211823985.png) + +你还可以通过生成的时序图来定位到相关的代码,这对于我们阅读源码的时候尤其有帮助! + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20201021171623809-20211010211828759.png) + +时序图生成完成之后,你还可以选择将其导出为图片。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20201021170228723-20211010211832965.png) \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/shortcut-key.md b/docs/idea-tutorial/idea-plugins/shortcut-key.md new file mode 100644 index 00000000000..c7e585290e7 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/shortcut-key.md @@ -0,0 +1,56 @@ +--- +title: IDEA 快捷键相关插件 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + + +相信我!下面这两个一定是IDEA必备的插件。 + +## Key Promoter X:快捷键提示 + +这个插件的功能主要是**在你本可以使用快捷键操作的地方提醒你用快捷键操作。** + +举个例子。我直接点击tab栏下的菜单打开 Version Control(版本控制) 的话,这个插件就会提示你可以用快捷键 `command+9`或者`shift+command+9`打开。如下图所示。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/source-code/dubbo/Key-Promoter-X1.png) + +除了这个很棒的功能之外,这个插件还有一个功能我觉得非常棒。 + +它可以展示出哪些快捷键你忘记使用的次数最多!这样的话,你可以给予你忘记次数最多的那些快捷键更多的关注。 + +我忘记最多的快捷键是debug的时候经常使用的 F8(Step Over)。如下图所示。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/source-code/dubbo/Key-Promoter-X2.png) + +关于快捷键,很多人不愿意去记,觉得单纯靠鼠标就完全够了。 + +让我来说的话!我觉得如果你偶尔使用一两次 IDEA 的话,你完全没有必要纠结快捷键。 + +但是,如果 IDEA 是你开发的主力,你经常需要使用的话,相信我,掌握常用的一些快捷键真的很重要! + +不说多的,**熟练掌握IDEA的一些最常见的快捷键,你的工作效率至少提升 30 %。** + +**除了工作效率的提升之外,使用快捷键会让我们显得更加专业。** + +你在使用快捷键进行操作的时候,是很帅,很酷啊!但是,当你用 IDEA 给别人演示一些操作的时候,你使用了快捷键的话,别人可能根本不知道你进行了什么快捷键操作。 + +**怎么解决这个问题呢?** + +很简单!这个时候就轮到 **Presentation Assistant** 这个插件上场了! + +## Presentation Assistant:快捷键展示 + +安装这个插件之后,你使用的快捷键操作都会被可视化地展示出来,非常适合自己在录制视频或者给别人展示代码的时候使用。 + +举个例子。我使用快捷键 `command+9`打开 Version Control ,使用了这个插件之后的效果如下图所示。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/source-code/dubbo/Presentation-Assistant.gif) + +从上图可以很清晰地看到,IDEA 的底部中间的位置将我刚刚所使用的快捷键给展示了出来。 + +并且,**这个插件会展示出 Mac 和 Win/Linux 两种不同的版本的快捷键。** + +因此,不论你的操作系统是 Mac 还是 Win/Linux ,这款插件都能满足你的需求。 \ No newline at end of file diff --git a/docs/idea-tutorial/idea-plugins/translation.md b/docs/idea-tutorial/idea-plugins/translation.md new file mode 100644 index 00000000000..7de2619a3b5 --- /dev/null +++ b/docs/idea-tutorial/idea-plugins/translation.md @@ -0,0 +1,28 @@ +--- +title: Translation:翻译 +category: IDEA指南 +tag: + - IDEA + - IDEA插件 +--- + + +有了这个插件之后,你再也不用在编码的时候打开浏览器查找某个单词怎么拼写、某句英文注释什么意思了。 + +并且,这个插件支持多种翻译源: + +1. Google 翻译 +2. Youdao 翻译 +3. Baidu 翻译 + +除了翻译功能之外还提供了语音朗读、单词本等实用功能。这个插件的Github地址是:[https://github.com/YiiGuxing/TranslationPlugin](https://github.com/YiiGuxing/TranslationPlugin) (貌似是国人开发的,很赞)。 + +**使用方法很简单!选中你要翻译的单词或者句子,使用快捷键 `command+ctrl+u(mac)` / `shift+ctrl+y(win/linux)`** (如果你忘记了快捷的话,鼠标右键操作即可!) + +![](./pictures/translation/translation1.jpg) + +**如果需要快速打开翻译框,使用快捷键`command+ctrl+i(mac)`/`ctrl + shift + o(win/linux)`** + +![](./pictures/translation/translation2.png) + +如果你需要将某个重要的单词添加到生词本的话,只需要点击单词旁边的收藏按钮即可! \ No newline at end of file diff --git a/docs/idea-tutorial/idea-tips/idea-plug-in-development-intro.md b/docs/idea-tutorial/idea-tips/idea-plug-in-development-intro.md new file mode 100644 index 00000000000..8f8822c5946 --- /dev/null +++ b/docs/idea-tutorial/idea-tips/idea-plug-in-development-intro.md @@ -0,0 +1,213 @@ +# IDEA 插件开发入门 + +我这个人没事就喜欢推荐一些好用的 [IDEA 插件](https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&album_id=1319419426898329600&__biz=Mzg2OTA0Njk0OA==#wechat_redirect)给大家。这些插件极大程度上提高了我们的生产效率以及编码舒适度。 + +**不知道大家有没有想过自己开发一款 IDEA 插件呢?** + +我自己想过,但是没去尝试过。刚好有一位读者想让我写一篇入门 IDEA 开发的文章,所以,我在周末就花了一会时间简单了解一下。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201118071711216.png) + +不过,**这篇文章只是简单带各位小伙伴入门一下 IDEA 插件开发**,个人精力有限,暂时不会深入探讨太多。如果你已经有 IDEA 插件开发的相关经验的话,这篇文章就可以不用看了,因为会浪费你 3 分钟的时间。 + +好的废话不多说!咱们直接开始! + +## 01 新建一个基于 Gradle 的插件项目 + +这里我们基于 Gradle 进行插件开发,这也是 IntelliJ 官方的推荐的插件开发解决方案。 + +**第一步,选择 Gradle 项目类型并勾选上相应的依赖。** + +![选择 Gradle 项目类型并勾选上相应的依赖](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/1.png) + +**第二步,填写项目相关的属性比如 GroupId、ArtifactId。** + +![填写项目相关的属性](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/2.png) + +**第三步,静静等待项目下载相关依赖。** + +第一次创建 IDEA 插件项目的话,这一步会比较慢。因为要下载 IDEA 插件开发所需的 SDK 。 + +## 02 插件项目结构概览 + +新建完成的项目结构如下图所示。 + +![插件项目结构概览](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/%E6%8F%92%E4%BB%B6%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84%E6%A6%82%E8%A7%88.png) + +这里需要额外注意的是下面这两个配置文件。 + +**`plugin.xml` :插件的核心配置文件。通过它可以配置插件名称、插件介绍、插件作者信息、Action 等信息。** + +```xml + + github.javaguide.my-first-idea-plugin + + Beauty + + JavaGuide + + + 这尼玛是什么垃圾插件!!! + ]]> + + + com.intellij.modules.platform + + + + + + + + + +``` + +**`build.gradle` :项目依赖配置文件。通过它可以配置项目第三方依赖、插件版本、插件版本更新记录等信息。** + +```groovy +plugins { + id 'java' + id 'org.jetbrains.intellij' version '0.6.3' +} + +group 'github.javaguide' +// 当前插件版本 +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +// 项目依赖 +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' +} + +// See https://github.com/JetBrains/gradle-intellij-plugin/ +// 当前开发该插件的 IDEA 版本 +intellij { + version '2020.1.2' +} +patchPluginXml { + // 版本更新记录 + changeNotes """ + Add change notes here.
+ most HTML tags may be used""" +} +``` + +没有开发过 IDEA 插件的小伙伴直接看这两个配置文件内容可能会有点蒙。所以,我专门找了一个 IDEA 插件市场提供的现成插件来说明一下。小伙伴们对照下面这张图来看下面的配置文件内容就非常非常清晰了。 + +![插件信息](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/iShot2020-11-13%2016.15.53.png) + +这就非常贴心了!如果这都不能让你点赞,我要这文章有何用! + +![](http://wx1.sinaimg.cn/large/006BkP2Hly1fsxxff7zd9g304g0480td.gif) + +## 03 手动创建 Action + +我们可以把 Action 看作是 IDEA 提高的事件响应处理器,通过 Action 我们可以自定义一些事件处理逻辑/动作。比如说你点击某个菜单的时候,我们进行一个展示对话框的操作。 + +**第一步,右键`java`目录并选择 new 一个 Action** + +![]() + +**第二步,配置 Action 相关信息比如展示名称。** + +![配置动作属性 (1)]() + +创建完成之后,我们的 `plugin.xml` 的 ``节点下会自动生成我们刚刚创建的 Action 信息: + +```xml + + + + + + +``` + +并且 `java` 目录下为生成一个叫做 `HelloAction` 的类。并且,这个类继承了 `AnAction` ,并覆盖了 `actionPerformed()` 方法。这个 `actionPerformed` 方法就好比 JS 中的 `onClick` 方法,会在你点击的时候被触发对应的动作。 + +我简单对`actionPerformed` 方法进行了修改,添加了一行代码。这行代码很简单,就是显示 1 个对话框并展示一些信息。 + +```java +public class HelloAction extends AnAction { + + @Override + public void actionPerformed(AnActionEvent e) { + //显示对话框并展示对应的信息 + Messages.showInfoMessage("素材不够,插件来凑!", "Hello"); + } +} + +``` + +另外,我们上面也说了,每个动作都会归属到一个 Group 中,这个 Group 可以简单看作 IDEA 中已经存在的菜单。 + +举个例子。我上面创建的 Action 的所属 Group 是 **ToolsMenu(Tools)** 。这样的话,我们创建的 Action 所在的位置就在 Tools 这个菜单下。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201113192255689.png) + +再举个例子。加入我上面创建的 Action 所属的 Group 是**MainMenu** (IDEA 最上方的主菜单栏)下的 **FileMenu(File)** 的话。 + +```xml + + + + + + +``` + +我们创建的 Action 所在的位置就在 File 这个菜单下。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201113201634643.png) + +## 04 验收成果 + +点击 `Gradle -> runIde` 就会启动一个默认了这个插件的 IDEA。然后,你可以在这个 IDEA 上实际使用这个插件了。 + +![点击 runIde 就会启动一个默认了这个插件的 IDEA](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201118075912490.png) + +效果如下: + +![点击 runIde 就会启动一个默认了这个插件的 IDEA](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201118080358764.png) + +我们点击自定义的 Hello Action 的话就会弹出一个对话框并展示出我们自定义的信息。 + +![IDEA插件HelloWorld](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/IDEA%E6%8F%92%E4%BB%B6HelloWorld.png) + +## 05 完善一下 + +想要弄点界面花里胡哨一下, 我们还可以通过 Swing 来写一个界面。 + +这里我们简单实现一个聊天机器人。代码的话,我是直接参考的我大二刚学 Java 那会写的一个小项目(_当时写的代码实在太烂了!就很菜!_)。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201114100213337.png) + +首先,你需要在[图灵机器人官网](http://www.tuling123.com/ "图灵机器人官网")申请一个机器人。(_其他机器人也一样,感觉这个图灵机器人没有原来好用了,并且免费调用次数也不多_) + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201118075453172.png) + +然后,简单写一个方法来请求调用机器人。由于代码比较简单,我这里就不放出来了,大家简单看一下效果就好。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201118075803163.png) + +## 06 深入学习 + +如果你想要深入学习的 IDEA 插件的话,可以看一下官网文档:[https://jetbrains.org/intellij/sdk/docs/basics/basics.html ](https://jetbrains.org/intellij/sdk/docs/basics/basics.html "https://jetbrains.org/intellij/sdk/docs/basics/basics.html") 。 + +这方面的资料还是比较少的。除了官方文档的话,你还可以简单看看下面这几篇文章: + +- [8 条经验轻松上手 IDEA 插件开发](https://developer.aliyun.com/article/777850?spm=a2c6h.12873581.0.dArticle777850.118d6446r096V4&groupCode=alitech "8 条经验轻松上手 IDEA 插件开发") +- [IDEA 插件开发入门教程](https://blog.xiaohansong.com/idea-plugin-development.html "IDEA 插件开发入门教程") + +## 07 后记 + +我们开发 IDEA 插件主要是为了让 IDEA 更加好用,比如有些框架使用之后可以减少重复代码的编写、有些主题类型的插件可以让你的 IDEA 更好看。 + +我这篇文章的这个案例说实话只是为了让大家简单入门一下 IDEA 开发,没有任何实际应用意义。**如果你想要开发一个不错的 IDEA 插件的话,还要充分发挥想象,利用 IDEA 插件平台的能力。** diff --git a/docs/idea-tutorial/idea-tips/idea-refractor-intro.md b/docs/idea-tutorial/idea-tips/idea-refractor-intro.md new file mode 100644 index 00000000000..7c3d4a66590 --- /dev/null +++ b/docs/idea-tutorial/idea-tips/idea-refractor-intro.md @@ -0,0 +1,75 @@ +# IDEA 重构入门 + +我们在使用 IDEA 进行重构之前,先介绍一个方便我们进行重构的快捷键:`ctrl+t(mac)/ctrl+shift+alt+t`(如果忘记快捷键的话,鼠标右键也能找到重构选项),使用这个快捷键可以快速调出常用重构的选项,如下图所示: + +![](./pictures/refractor-help.png) + +### 重命名(rename) + +快捷键:**Shift + F6(mac) / Shift + F6(windows/Linux):** 对类、变量或者方法名重命名。 + +![重命名](./pictures/rename.gif) + +### 提取相关重构手段 + +这部分的快捷键实际很好记忆,我是这样记忆的: + +前面两个键位是 `command + option(mac) / ctrl + alt (Windows/Linux)` 是固定的,只有后面一个键位会变比如Extract constant (提取变量)就是 c(constant)、Extract variable (提取变量)就是 v(variable)。 + +#### 提取常量(extract constant) + +1. **使用场景** :提取未经过定义就直接出现的常量。提取常量使得你的编码更易读,避免硬编码。 +2. **快捷键:** `command + option+ c(mac)/ ctrl + alt + c(Windows/Linux)` + +**示例:** + +![](./pictures/exact/extract-constant.gif) + +#### 提取参数(exact parameter) + +1. **使用场景** :提取参数到方法中。 +2. **快捷键:** `command + option+ p(mac)/ ctrl + alt + p(Windows/Linux)` + +![](./pictures/exact/exact-parameter.gif) + +#### 提取变量(exact variable) + +1. **使用场景** :提取多次出现的表达式。 +2. **快捷键:** `command + option+ v(mac) / ctrl + alt + v(Windows/Linux) ` + +**示例:** + +![](./pictures/exact/exact-variable.gif) + +#### 提取属性(exact field) + +1. **使用场景** :把当前表达式提取成为类的一个属性。 +2. **快捷键:** `command + option+ f(mac) / ctrl + alt + f(Windows/Linux) ` + +**示例:** + +![](./pictures/exact/exact-field.gif) + + +**示例:** + +![](./pictures/exact/exact-variable.gif) + +#### 提取方法(exact method) + +1. **使用场景** :1个或者多个表达式可以提取为一个方法。 提取方法也能使得你的编码更易读,更加语义化。 +2. **快捷键:** `command + option+ m(mac)/ ctrl + alt + m(Windows/Linux)` + +**示例:** + +![](./pictures/exact/exact-method.gif) + +#### 提取接口(exact interface) + +1. **使用场景** :想要把一个类中的1个或多个方法提取到一个接口中的时候。 +2. **快捷键:** `command + option+ m(mac)/ ctrl + alt + m(Windows/Linux)` + +**示例:** + +![](./pictures/exact/exact-interface.gif) + diff --git a/docs/idea-tutorial/idea-tips/idea-source-code-reading-skills.md b/docs/idea-tutorial/idea-tips/idea-source-code-reading-skills.md new file mode 100644 index 00000000000..0064598c423 --- /dev/null +++ b/docs/idea-tutorial/idea-tips/idea-source-code-reading-skills.md @@ -0,0 +1,189 @@ +# IDEA源码阅读技巧 + +项目有个新来了一个小伙伴,他看我查看项目源代码的时候,各种骚操作“花里胡哨”的。于是他向我请教,想让我分享一下我平时使用 IDEA 看源码的小技巧。 + +## 基本操作 + +这一部分的内容主要是一些我平时看源码的时候常用的快捷键/小技巧!非常好用! + +掌握这些快捷键/小技巧,看源码的效率提升一个等级! + +### 查看当前类的层次结构 + +| 使用频率 | 相关快捷键 | +| -------- | ---------- | +| ⭐⭐⭐⭐⭐ | `Ctrl + H` | + +平时,我们阅读源码的时候,经常需要查看类的层次结构。就比如我们遇到抽象类或者接口的时候,经常需要查看其被哪些类实现。 + +拿 Spring 源码为例,`BeanDefinition` 是一个关于 Bean 属性/定义的接口。 + +```java +public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { + ...... +} +``` + +如果我们需要查看 `BeanDefinition` 被哪些类实现的话,只需要把鼠标移动到 `BeanDefinition` 类名上,然后使用快捷键 `Ctrl + H` 即可。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210527135533686.png) + +同理,如果你想查看接口 `BeanDefinition` 继承的接口 `AttributeAccessor` 被哪些类实现的话,只需要把鼠标移动到 `AttributeAccessor` 类名上,然后使用快捷键 `Ctrl + H` 即可。 + +### 查看类结构 + +| 使用频率 | 相关快捷键 | +| -------- | ------------------------------------- | +| ⭐⭐⭐⭐ | `Alt + 7`(Win) / `Command +7` (Mac) | + +类结构可以让我们快速了解到当前类的方法、变量/常量,非常使用! + +我们在对应的类的任意位置使用快捷键 `Alt + 7`(Win) / `Command +7` (Mac)即可。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210527135552183.png) + +### 快速检索类 + +| 使用频率 | 相关快捷键 | +| -------- | ---------------------------------------- | +| ⭐⭐⭐⭐⭐ | `Ctrl + N` (Win) / `Command + O` (Mac) | + +使用快捷键 `Ctrl + N` (Win) / `Command + O` (Mac)可以快速检索类/文件。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210527135629367.png) + +### 关键字检索 + +| 使用频率 | 相关快捷键 | +| -------- | ---------- | +| ⭐⭐⭐⭐⭐ | 见下文 | + +- 当前文件下检索 : `Ctrl + F` (Win) / `Command + F` (Mac) +- 全局的文本检索 : `Ctrl + Shift + F` (Win) / `Command + Shift + F` (Mac) + +### 查看方法/类的实现类 + +| 使用频率 | 相关快捷键 | +| -------- | -------------------------------------------------- | +| ⭐⭐⭐⭐ | `Ctrl + Alt + B` (Win) / `Command + Alt + B` (Mac) | + +如果我们想直接跳转到某个方法/类的实现类,直接在方法名或者类名上使用快捷键 `Ctrl + Alt + B/鼠标左键` (Win) / `Command + Alt + B/鼠标左键` (Mac) 即可。 + +如果对应的方法/类只有一个实现类的话,会直接跳转到对应的实现类。 + +比如 `BeanDefinition` 接口的 `getBeanClassName()` 方法只被 `AbstractBeanDefinition` 抽象类实现,我们对这个方法使用快捷键就可以直接跳转到 `AbstractBeanDefinition` 抽象类中对应的实现方法。 + +```java +public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { + @Nullable + String getBeanClassName(); + ...... +} +``` + +如果对应的方法/类有多个实现类的话,IDEA 会弹出一个选择框让你选择。 + +比如 `BeanDefinition` 接口的 `getParentName()` 方法就有多个不同的实现。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210527135703164.png) + +### 查看方法被使用的情况 + +| 使用频率 | 相关快捷键 | +| -------- | ---------- | +| ⭐⭐⭐⭐ | `Alt + F7` | + +我们可以通过直接在方法名上使用快捷键 `Alt + F7` 来查看这个方法在哪些地方被调用过。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210527135712962.png) + +### 查看最近使用的文件 + +| 使用频率 | 相关快捷键 | +| -------- | -------------------------------------- | +| ⭐⭐⭐⭐⭐ | `Ctrl + E`(Win) / `Command +E` (Mac) | + +你可以通过快捷键 `Ctrl + E`(Win) / `Command +E` (Mac)来显示 IDEA 最近使用的一些文件。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210527135733816.png) + +### 查看图表形式的类继承链 + +| 使用频率 | 相关快捷键 | +| -------- | ------------------------ | +| ⭐⭐⭐⭐ | 相关快捷键较多,不建议记 | + +点击类名 **右键** ,选择 **Shw Diagrams** 即可查看图表形式的类继承链。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210527135745518.png) + +你还可以对图表进行一些操作。比如,你可以点击图表中具体的类 **右键**,然后选择显示它的实现类或者父类。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210527135757163.png) + +再比如你还可以选择是否显示类中的属性、方法、内部类等等信息。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210527135902827.png) + +如果你想跳转到对应类的源码的话,直接点击图表中具体的类 **右键** ,然后选择 **Jump to Source** 。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210527135807668.png) + +## 插件推荐 + +### 一键生成方法的序列图 + +**序列图**(Sequence Diagram),亦称为**循序图**,是一种 UML 行为图。表示系统执行某个方法/操作(如登录操作)时,对象之间的顺序调用关系。 + +这个顺序调用关系可以这样理解:你需要执行系统中某个对象 a 提供的方法/操作 login(登录),但是这个对象又依赖了对象 b 提供的方法 getUser(获取用户)。因此,这里就有了 a -> b 调用关系之说。 + +我们可以通过 **SequenceDiagram** 这个插件一键生成方法的序列图。 + +> 如果你因为网络问题没办法使用 IDEA 自带的插件市场的话,也可以通过 IDEA 插件市场的官网手动下载安装。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/2021052218304014.png) + +**如何使用呢?** + +1、选中方法名(注意不要选类名),然后点击鼠标右键,选择 **Sequence Diagram** 选项即可! + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20201021170110697.png) + +2、配置生成的序列图的一些基本的参数比如调用深度之后,我们点击 ok 即可! + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/c5040f1105c762ddf8689892913bc02d.png) + +3、你还可以通过生成的时序图来定位到相关的代码,这对于我们阅读源码的时候尤其有帮助! + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20201021171623809.png) + +4、时序图生成完成之后,你还可以选择将其导出为图片。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20201021170228723.png) + +相关阅读:[《安利一个 IDEA 骚操作:一键生成方法的序列图》](https://mp.weixin.qq.com/s/SG1twZczqdup_EQAOmNERg) 。 + +### 项目代码统计 + +为了快速分析项目情况,我们可以对项目的 **代码的总行数、单个文件的代码行数、注释行数等信息进行统计。** + +**Statistic** 这个插件来帮助我们实现这一需求。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210522183550110.png) + +有了这个插件之后你可以非常直观地看到你的项目中所有类型的文件的信息比如数量、大小等等,可以帮助你更好地了解你们的项目。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210522183616310.png) + +你还可以使用它看所有类的总行数、有效代码行数、注释行数、以及有效代码比重等等这些东西。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/idea/20210522183630459.png) + +如果,你担心插件过多影响 IDEA 速度的话,可以只在有代码统计需求的时候开启这个插件,其他时间禁用它就完事了! + +相关阅读:[快速识别烂项目!试试这款项目代码统计 IDEA 插件](https://mp.weixin.qq.com/s/fVEeMW6elhu79I-rTZB40A) + + + + + diff --git a/docs/idea-tutorial/idea-tips/pictures/exact/exact-field.gif b/docs/idea-tutorial/idea-tips/pictures/exact/exact-field.gif new file mode 100644 index 00000000000..770df36522d Binary files /dev/null and b/docs/idea-tutorial/idea-tips/pictures/exact/exact-field.gif differ diff --git a/docs/idea-tutorial/idea-tips/pictures/exact/exact-interface.gif b/docs/idea-tutorial/idea-tips/pictures/exact/exact-interface.gif new file mode 100644 index 00000000000..678b93de0a8 Binary files /dev/null and b/docs/idea-tutorial/idea-tips/pictures/exact/exact-interface.gif differ diff --git a/docs/idea-tutorial/idea-tips/pictures/exact/exact-method.gif b/docs/idea-tutorial/idea-tips/pictures/exact/exact-method.gif new file mode 100644 index 00000000000..3748903e21d Binary files /dev/null and b/docs/idea-tutorial/idea-tips/pictures/exact/exact-method.gif differ diff --git a/docs/idea-tutorial/idea-tips/pictures/exact/exact-parameter.gif b/docs/idea-tutorial/idea-tips/pictures/exact/exact-parameter.gif new file mode 100644 index 00000000000..578b5ccca83 Binary files /dev/null and b/docs/idea-tutorial/idea-tips/pictures/exact/exact-parameter.gif differ diff --git a/docs/idea-tutorial/idea-tips/pictures/exact/exact-variable.gif b/docs/idea-tutorial/idea-tips/pictures/exact/exact-variable.gif new file mode 100644 index 00000000000..7326761ef55 Binary files /dev/null and b/docs/idea-tutorial/idea-tips/pictures/exact/exact-variable.gif differ diff --git a/docs/idea-tutorial/idea-tips/pictures/exact/extract-constant.gif b/docs/idea-tutorial/idea-tips/pictures/exact/extract-constant.gif new file mode 100644 index 00000000000..6752a385e74 Binary files /dev/null and b/docs/idea-tutorial/idea-tips/pictures/exact/extract-constant.gif differ diff --git a/docs/idea-tutorial/idea-tips/pictures/refractor-help.png b/docs/idea-tutorial/idea-tips/pictures/refractor-help.png new file mode 100644 index 00000000000..032319487ae Binary files /dev/null and b/docs/idea-tutorial/idea-tips/pictures/refractor-help.png differ diff --git a/docs/idea-tutorial/idea-tips/pictures/rename.gif b/docs/idea-tutorial/idea-tips/pictures/rename.gif new file mode 100644 index 00000000000..c8a61b12863 Binary files /dev/null and b/docs/idea-tutorial/idea-tips/pictures/rename.gif differ diff --git a/docs/idea-tutorial/readme.md b/docs/idea-tutorial/readme.md new file mode 100644 index 00000000000..a5297b6bbed --- /dev/null +++ b/docs/idea-tutorial/readme.md @@ -0,0 +1,11 @@ +--- +icon: creative +category: IDEA指南 +--- + +# IntelliJ IDEA 使用指南 | 必备插件推荐 | 插件开发入门 | 重构小技巧 | 源码阅读技巧 + +分享一下自己使用 IDEA 的一些经验,希望对大家有帮助! + +- Github 地址:https://github.com/CodingDocs/awesome-idea-tutorial +- 码云地址:https://gitee.com/SnailClimb/awesome-idea-tutorial (Github 无法访问或者访问速度比较慢的小伙伴可以看码云上的对应内容) diff --git a/docs/java/BIO-NIO-AIO.md b/docs/java/BIO-NIO-AIO.md deleted file mode 100644 index caeeeb8940f..00000000000 --- a/docs/java/BIO-NIO-AIO.md +++ /dev/null @@ -1,347 +0,0 @@ -熟练掌握 BIO,NIO,AIO 的基本概念以及一些常见问题是你准备面试的过程中不可或缺的一部分,另外这些知识点也是你学习 Netty 的基础。 - - - -- [BIO,NIO,AIO 总结](#bionioaio-总结) - - [1. BIO \(Blocking I/O\)](#1-bio-blocking-io) - - [1.1 传统 BIO](#11-传统-bio) - - [1.2 伪异步 IO](#12-伪异步-io) - - [1.3 代码示例](#13-代码示例) - - [1.4 总结](#14-总结) - - [2. NIO \(New I/O\)](#2-nio-new-io) - - [2.1 NIO 简介](#21-nio-简介) - - [2.2 NIO的特性/NIO与IO区别](#22-nio的特性nio与io区别) - - [1)Non-blocking IO(非阻塞IO)](#1non-blocking-io(非阻塞io)) - - [2)Buffer\(缓冲区\)](#2buffer缓冲区) - - [3)Channel \(通道\)](#3channel-通道) - - [4)Selectors\(选择器\)](#4selectors选择器) - - [2.3 NIO 读数据和写数据方式](#23-nio-读数据和写数据方式) - - [2.4 NIO核心组件简单介绍](#24-nio核心组件简单介绍) - - [2.5 代码示例](#25-代码示例) - - [3. AIO \(Asynchronous I/O\)](#3-aio-asynchronous-io) - - [参考](#参考) - - - - -# BIO,NIO,AIO 总结 - - Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装。程序员在使用这些 API 的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。只需要使用Java的API就可以了。 - -在讲 BIO,NIO,AIO 之前先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。 - -关于同步和异步的概念解读困扰着很多程序员,大部分的解读都会带有自己的一点偏见。参考了 [Stackoverflow](https://stackoverflow.com/questions/748175/asynchronous-vs-synchronous-execution-what-does-it-really-mean)相关问题后对原有答案进行了进一步完善: - -> When you execute something synchronously, you wait for it to finish before moving on to another task. When you execute something asynchronously, you can move on to another task before it finishes. -> -> 当你同步执行某项任务时,你需要等待其完成才能继续执行其他任务。当你异步执行某些操作时,你可以在完成另一个任务之前继续进行。 - -- **同步** :两个同步任务相互依赖,并且一个任务必须以依赖于另一任务的某种方式执行。 比如在`A->B`事件模型中,你需要先完成 A 才能执行B。 再换句话说,同步调用种被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。 -- **异步**: 两个异步的任务完全独立的,一方的执行不需要等待另外一方的执行。再换句话说,异步调用种一调用就返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情, - -**阻塞和非阻塞** - -- **阻塞:** 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。 -- **非阻塞:** 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。 - -**如何区分 “同步/异步 ”和 “阻塞/非阻塞” 呢?** - -同步/异步是从行为角度描述事物的,而阻塞和非阻塞描述的当前事物的状态(等待调用结果时的状态)。 - -## 1. BIO (Blocking I/O) - -同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。 - -### 1.1 传统 BIO - -BIO通信(一请求一应答)模型图如下(图源网络,原出处不明): - -![传统BIO通信模型图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2.png) - -采用 **BIO 通信模型** 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在`while(true)` 循环中服务端会调用 `accept()` 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。 - -如果要让 **BIO 通信模型** 能够同时处理多个客户端请求,就必须使用多线程(主要原因是`socket.accept()`、`socket.read()`、`socket.write()` 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 **一请求一应答通信模型** 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 **线程池机制** 改善,线程池还可以让线程的创建和回收成本相对较低。使用`FixedThreadPool` 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M),下面一节"伪异步 BIO"中会详细介绍到。 - -**我们再设想一下当客户端并发访问量增加后这种模型会出现什么问题?** - -在 Java 虚拟机中,线程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。 - -### 1.2 伪异步 IO - -为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。 - -伪异步IO模型图(图源网络,原出处不明): - -![伪异步IO模型图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/3.png) - -采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。 - -伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。 - -### 1.3 代码示例 - -下面代码中演示了BIO通信(一请求一应答)模型。我们会在客户端创建多个线程依次连接服务端并向其发送"当前时间+:hello world",服务端会为每个客户端线程创建一个线程来处理。代码示例出自闪电侠的博客,原地址如下: - -[https://www.jianshu.com/p/a4e03835921a](https://www.jianshu.com/p/a4e03835921a) - -**客户端** - -```java -/** - * - * @author 闪电侠 - * @date 2018年10月14日 - * @Description:客户端 - */ -public class IOClient { - - public static void main(String[] args) { - // TODO 创建多个线程,模拟多个客户端连接服务端 - new Thread(() -> { - try { - Socket socket = new Socket("127.0.0.1", 3333); - while (true) { - try { - socket.getOutputStream().write((new Date() + ": hello world").getBytes()); - Thread.sleep(2000); - } catch (Exception e) { - } - } - } catch (IOException e) { - } - }).start(); - - } - -} - -``` - -**服务端** - -```java -/** - * @author 闪电侠 - * @date 2018年10月14日 - * @Description: 服务端 - */ -public class IOServer { - - public static void main(String[] args) throws IOException { - // TODO 服务端处理客户端连接请求 - ServerSocket serverSocket = new ServerSocket(3333); - - // 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理 - new Thread(() -> { - while (true) { - try { - // 阻塞方法获取新的连接 - Socket socket = serverSocket.accept(); - - // 每一个新的连接都创建一个线程,负责读取数据 - new Thread(() -> { - try { - int len; - byte[] data = new byte[1024]; - InputStream inputStream = socket.getInputStream(); - // 按字节流方式读取数据 - while ((len = inputStream.read(data)) != -1) { - System.out.println(new String(data, 0, len)); - } - } catch (IOException e) { - } - }).start(); - - } catch (IOException e) { - } - - } - }).start(); - - } - -} -``` - -### 1.4 总结 - -在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 - -## 2. NIO (New I/O) - -### 2.1 NIO 简介 - - NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。 - -NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。 - -### 2.2 NIO的特性/NIO与IO区别 - -如果是在面试中回答这个问题,我觉得首先肯定要从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 说起。然后,可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。如果,你把这些都回答上了我觉得你对于 NIO 就有了更为深入一点的认识,面试官问到你这个问题,你也能很轻松的回答上来了。 - -#### 1)Non-blocking IO(非阻塞IO) - -**IO流是阻塞的,NIO流是不阻塞的。** - -Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 - -Java IO的各种流是阻塞的。这意味着,当一个线程调用 `read()` 或 `write()` 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了 - -#### 2)Buffer(缓冲区) - -**IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。** - -Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。 - -在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。 - -最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。 - -#### 3)Channel (通道) - -NIO 通过Channel(通道) 进行读写。 - -通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。 - -#### 4)Selector (选择器) - -NIO有选择器,而IO没有。 - -选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。 - -![一个单线程中Selector维护3个Channel的示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/Slector.png) - -### 2.3 NIO 读数据和写数据方式 -通常来说NIO中的所有IO都是从 Channel(通道) 开始的。 - -- 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。 -- 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。 - -数据读取和写入操作图示: - -![NIO读写数据的方式](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/NIO读写数据的方式.png) - - -### 2.4 NIO核心组件简单介绍 - -NIO 包含下面几个核心的组件: - -- Channel(通道) -- Buffer(缓冲区) -- Selector(选择器) - -整个NIO体系包含的类远远不止这三个,只能说这三个是NIO体系的“核心API”。我们上面已经对这三个概念进行了基本的阐述,这里就不多做解释了。 - -### 2.5 代码示例 - -代码示例出自闪电侠的博客,原地址如下: - -[https://www.jianshu.com/p/a4e03835921a](https://www.jianshu.com/p/a4e03835921a) - -客户端 IOClient.java 的代码不变,我们对服务端使用 NIO 进行改造。以下代码较多而且逻辑比较复杂,大家看看就好。 - -```java -/** - * - * @author 闪电侠 - * @date 2019年2月21日 - * @Description: NIO 改造后的服务端 - */ -public class NIOServer { - public static void main(String[] args) throws IOException { - // 1. serverSelector负责轮询是否有新的连接,服务端监测到新的连接之后,不再创建一个新的线程, - // 而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等 - Selector serverSelector = Selector.open(); - // 2. clientSelector负责轮询连接是否有数据可读 - Selector clientSelector = Selector.open(); - - new Thread(() -> { - try { - // 对应IO编程中服务端启动 - ServerSocketChannel listenerChannel = ServerSocketChannel.open(); - listenerChannel.socket().bind(new InetSocketAddress(3333)); - listenerChannel.configureBlocking(false); - listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT); - - while (true) { - // 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms - if (serverSelector.select(1) > 0) { - Set set = serverSelector.selectedKeys(); - Iterator keyIterator = set.iterator(); - - while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - - if (key.isAcceptable()) { - try { - // (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector - SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(); - clientChannel.configureBlocking(false); - clientChannel.register(clientSelector, SelectionKey.OP_READ); - } finally { - keyIterator.remove(); - } - } - - } - } - } - } catch (IOException ignored) { - } - }).start(); - new Thread(() -> { - try { - while (true) { - // (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms - if (clientSelector.select(1) > 0) { - Set set = clientSelector.selectedKeys(); - Iterator keyIterator = set.iterator(); - - while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - - if (key.isReadable()) { - try { - SocketChannel clientChannel = (SocketChannel) key.channel(); - ByteBuffer byteBuffer = ByteBuffer.allocate(1024); - // (3) 面向 Buffer - clientChannel.read(byteBuffer); - byteBuffer.flip(); - System.out.println( - Charset.defaultCharset().newDecoder().decode(byteBuffer).toString()); - } finally { - keyIterator.remove(); - key.interestOps(SelectionKey.OP_READ); - } - } - - } - } - } - } catch (IOException ignored) { - } - }).start(); - - } -} -``` - -为什么大家都不愿意用 JDK 原生 NIO 进行开发呢?从上面的代码中大家都可以看出来,是真的难用!除了编程复杂、编程模型难之外,它还有以下让人诟病的问题: - -- JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100% -- 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高,上面这一坨代码我都不能保证没有 bug - -Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。 - -### 3. AIO (Asynchronous I/O) - -AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。 - -AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。(除了 AIO 其他的 IO 类型都是同步的,这一点可以从底层IO线程模型解释,推荐一篇文章:[《漫话:如何给女朋友解释什么是Linux的五种IO模型?》](https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41#wechat_redirect) ) - -查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 - -## 参考 - -- 《Netty 权威指南》第二版 -- https://zhuanlan.zhihu.com/p/23488863 (美团技术团队) diff --git "a/docs/java/Java IO\344\270\216NIO.md" "b/docs/java/Java IO\344\270\216NIO.md" deleted file mode 100644 index 74bd850e696..00000000000 --- "a/docs/java/Java IO\344\270\216NIO.md" +++ /dev/null @@ -1,200 +0,0 @@ - - -- [IO流学习总结](#io流学习总结) - - [一 Java IO,硬骨头也能变软](#一-java-io,硬骨头也能变软) - - [二 java IO体系的学习总结](#二-java-io体系的学习总结) - - [三 Java IO面试题](#三-java-io面试题) -- [NIO与AIO学习总结](#nio与aio学习总结) - - [一 Java NIO 概览](#一-java-nio-概览) - - [二 Java NIO 之 Buffer\(缓冲区\)](#二-java-nio-之-buffer缓冲区) - - [三 Java NIO 之 Channel(通道)](#三-java-nio-之-channel(通道)) - - [四 Java NIO之Selector(选择器)](#四-java-nio之selector(选择器)) - - [五 Java NIO之拥抱Path和Files](#五-java-nio之拥抱path和files) - - [六 NIO学习总结以及NIO新特性介绍](#六-nio学习总结以及nio新特性介绍) - - [七 Java NIO AsynchronousFileChannel异步文件通](#七-java-nio-asynchronousfilechannel异步文件通) - - [八 高并发Java(8):NIO和AIO](#八-高并发java(8):nio和aio) -- [推荐阅读](#推荐阅读) - - [在 Java 7 中体会 NIO.2 异步执行的快乐](#在-java-7-中体会-nio2-异步执行的快乐) - - [Java AIO总结与示例](#java-aio总结与示例) - - - - - -## IO流学习总结 - -### [一 Java IO,硬骨头也能变软](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483981&idx=1&sn=6e5c682d76972c8d2cf271a85dcf09e2&chksm=fd98542ccaefdd3a70428e9549bc33e8165836855edaa748928d16c1ebde9648579d3acaac10#rd) - -**(1) 按操作方式分类结构图:** - -![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png) - - -**(2)按操作对象分类结构图** - -![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png) - -### [二 java IO体系的学习总结](https://blog.csdn.net/nightcurtis/article/details/51324105) -1. **IO流的分类:** - - 按照流的流向分,可以分为输入流和输出流; - - 按照操作单元划分,可以划分为字节流和字符流; - - 按照流的角色划分为节点流和处理流。 -2. **流的原理浅析:** - - java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java Io流的40多个类都是从如下4个抽象类基类中派生出来的。 - - - **InputStream/Reader**: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 - - **OutputStream/Writer**: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 -3. **常用的io流的用法** - -### [三 Java IO面试题](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483985&idx=1&sn=38531c2cee7b87f125df7aef41637014&chksm=fd985430caefdd26b0506aa84fc26251877eccba24fac73169a4d6bd1eb5e3fbdf3c3b940261#rd) - -## NIO与AIO学习总结 - - -### [一 Java NIO 概览](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483956&idx=1&sn=57692bc5b7c2c6dfb812489baadc29c9&chksm=fd985455caefdd4331d828d8e89b22f19b304aa87d6da73c5d8c66fcef16e4c0b448b1a6f791#rd) - -1. **NIO简介**: - - Java NIO 是 java 1.4, 之后新出的一套IO接口NIO中的N可以理解为Non-blocking,不单纯是New。 - -2. **NIO的特性/NIO与IO区别:** - - 1)IO是面向流的,NIO是面向缓冲区的; - - 2)IO流是阻塞的,NIO流是不阻塞的; - - 3)NIO有选择器,而IO没有。 -3. **读数据和写数据方式:** - - 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。 - - - 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。 - -4. **NIO核心组件简单介绍** - - **Channels** - - **Buffers** - - **Selectors** - - -### [二 Java NIO 之 Buffer(缓冲区)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483961&idx=1&sn=f67bef4c279e78043ff649b6b03fdcbc&chksm=fd985458caefdd4e3317ccbdb2d0a5a70a5024d3255eebf38183919ed9c25ade536017c0a6ba#rd) - -1. **Buffer(缓冲区)介绍:** - - Java NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里,从Buffer把数据写入到Channels; - - Buffer本质上就是一块内存区; - - 一个Buffer有三个属性是必须掌握的,分别是:capacity容量、position位置、limit限制。 -2. **Buffer的常见方法** - - Buffer clear() - - Buffer flip() - - Buffer rewind() - - Buffer position(int newPosition) -3. **Buffer的使用方式/方法介绍:** - - 分配缓冲区(Allocating a Buffer): - ```java - ByteBuffer buf = ByteBuffer.allocate(28);//以ByteBuffer为例子 - ``` - - 写入数据到缓冲区(Writing Data to a Buffer) - - **写数据到Buffer有两种方法:** - - 1.从Channel中写数据到Buffer - ```java - int bytesRead = inChannel.read(buf); //read into buffer. - ``` - 2.通过put写数据: - ```java - buf.put(127); - ``` - -4. **Buffer常用方法测试** - - 说实话,NIO编程真的难,通过后面这个测试例子,你可能才能勉强理解前面说的Buffer方法的作用。 - - -### [三 Java NIO 之 Channel(通道)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483966&idx=1&sn=d5cf18c69f5f9ec2aff149270422731f&chksm=fd98545fcaefdd49296e2c78000ce5da277435b90ba3c03b92b7cf54c6ccc71d61d13efbce63#rd) - - -1. **Channel(通道)介绍** - - 通常来说NIO中的所有IO都是从 Channel(通道) 开始的。 - - NIO Channel通道和流的区别: -2. **FileChannel的使用** -3. **SocketChannel和ServerSocketChannel的使用** -4. **️DatagramChannel的使用** -5. **Scatter / Gather** - - Scatter: 从一个Channel读取的信息分散到N个缓冲区中(Buufer). - - Gather: 将N个Buffer里面内容按照顺序发送到一个Channel. -6. **通道之间的数据传输** - - 在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。 - - transferFrom() :transferFrom方法把数据从通道源传输到FileChannel - - transferTo() :transferTo方法把FileChannel数据传输到另一个channel - - -### [四 Java NIO之Selector(选择器)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483970&idx=1&sn=d5e2b133313b1d0f32872d54fbdf0aa7&chksm=fd985423caefdd354b587e57ce6cf5f5a7bec48b9ab7554f39a8d13af47660cae793956e0f46#rd) - - -1. **Selector(选择器)介绍** - - Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。 - - 使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。 -2. **Selector(选择器)的使用方法介绍** - - Selector的创建 - ```java - Selector selector = Selector.open(); - ``` - - 注册Channel到Selector(Channel必须是非阻塞的) - ```java - channel.configureBlocking(false); - SelectionKey key = channel.register(selector, Selectionkey.OP_READ); - ``` - - SelectionKey介绍 - - 一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。 - - 从Selector中选择channel(Selecting Channels via a Selector) - - 选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中. - - 停止选择的方法 - - wakeup()方法 和close()方法。 -3. **模板代码** - - 有了模板代码我们在编写程序时,大多数时间都是在模板代码中添加相应的业务代码。 -4. **客户端与服务端简单交互实例** - - - -### [五 Java NIO之拥抱Path和Files](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483976&idx=1&sn=2296c05fc1b840a64679e2ad7794c96d&chksm=fd985429caefdd3f48e2ee6fdd7b0f6fc419df90b3de46832b484d6d1ca4e74e7837689c8146&token=537240785&lang=zh_CN#rd) - -**一 文件I/O基石:Path:** -- 创建一个Path -- File和Path之间的转换,File和URI之间的转换 -- 获取Path的相关信息 -- 移除Path中的冗余项 - -**二 拥抱Files类:** -- Files.exists() 检测文件路径是否存在 -- Files.createFile() 创建文件 -- Files.createDirectories()和Files.createDirectory()创建文件夹 -- Files.delete()方法 可以删除一个文件或目录 -- Files.copy()方法可以吧一个文件从一个地址复制到另一个位置 -- 获取文件属性 -- 遍历一个文件夹 -- Files.walkFileTree()遍历整个目录 - -### [六 NIO学习总结以及NIO新特性介绍](https://blog.csdn.net/a953713428/article/details/64907250) - -- **内存映射:** - -这个功能主要是为了提高大文件的读写速度而设计的。内存映射文件(memory-mappedfile)能让你创建和修改那些大到无法读入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问了。将文件的一段区域映射到内存中,比传统的文件处理速度要快很多。内存映射文件它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。 - -### [七 Java NIO AsynchronousFileChannel异步文件通](http://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-asynchronousfilechannel.html) - -Java7中新增了AsynchronousFileChannel作为nio的一部分。AsynchronousFileChannel使得数据可以进行异步读写。 - -### [八 高并发Java(8):NIO和AIO](http://www.importnew.com/21341.html) - - - -## 推荐阅读 - -### [在 Java 7 中体会 NIO.2 异步执行的快乐](https://www.ibm.com/developerworks/cn/java/j-lo-nio2/index.html) - -### [Java AIO总结与示例](https://blog.csdn.net/x_i_y_u_e/article/details/52223406) -AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。 - - -**欢迎关注我的微信公众号:"Java面试通关手册"(一个有温度的微信公众号,期待与你共同进步~~~坚持原创,分享美文,分享各种Java学习资源):** diff --git "a/docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" deleted file mode 100644 index af0d2c9c550..00000000000 --- "a/docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ /dev/null @@ -1,602 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 - - -- [1. 面向对象和面向过程的区别](#1-面向对象和面向过程的区别) -- [2. Java 语言有哪些特点?](#2-java-语言有哪些特点) -- [3. 关于 JVM JDK 和 JRE 最详细通俗的解答](#3-关于-jvm-jdk-和-jre-最详细通俗的解答) - - [JVM](#jvm) - - [JDK 和 JRE](#jdk-和-jre) -- [4. Oracle JDK 和 OpenJDK 的对比](#4-oracle-jdk-和-openjdk-的对比) -- [5. Java 和 C++的区别?](#5-java-和-c的区别) -- [6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?](#6-什么是-java-程序的主类-应用程序和小程序的主类有何不同) -- [7. Java 应用程序与小程序之间有哪些差别?](#7-java-应用程序与小程序之间有哪些差别) -- [8. 字符型常量和字符串常量的区别?](#8-字符型常量和字符串常量的区别) -- [9. 构造器 Constructor 是否可被 override?](#9-构造器-constructor-是否可被-override) -- [10. 重载和重写的区别](#10-重载和重写的区别) - - [重载](#重载) - - [重写](#重写) -- [11. Java 面向对象编程三大特性: 封装 继承 多态](#11-java-面向对象编程三大特性-封装-继承-多态) - - [封装](#封装) - - [继承](#继承) - - [多态](#多态) -- [12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?](#12-string-stringbuffer-和-stringbuilder-的区别是什么-string-为什么是不可变的) -- [13. 自动装箱与拆箱](#13-自动装箱与拆箱) -- [14. 在一个静态方法内调用一个非静态成员为什么是非法的?](#14-在一个静态方法内调用一个非静态成员为什么是非法的) -- [15. 在 Java 中定义一个不做事且没有参数的构造方法的作用](#15-在-java-中定义一个不做事且没有参数的构造方法的作用) -- [16. import java 和 javax 有什么区别?](#16-import-java-和-javax-有什么区别) -- [17. 接口和抽象类的区别是什么?](#17-接口和抽象类的区别是什么) -- [18. 成员变量与局部变量的区别有哪些?](#18-成员变量与局部变量的区别有哪些) -- [19. 创建一个对象用什么运算符?对象实体与对象引用有何不同?](#19-创建一个对象用什么运算符对象实体与对象引用有何不同) -- [20. 什么是方法的返回值?返回值在类的方法里的作用是什么?](#20-什么是方法的返回值返回值在类的方法里的作用是什么) -- [21. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?](#21-一个类的构造方法的作用是什么-若一个类没有声明构造方法该程序能正确执行吗-为什么) -- [22. 构造方法有哪些特性?](#22-构造方法有哪些特性) -- [23. 静态方法和实例方法有何不同](#23-静态方法和实例方法有何不同) -- [24. 对象的相等与指向他们的引用相等,两者有什么不同?](#24-对象的相等与指向他们的引用相等两者有什么不同) -- [25. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?](#25-在调用子类构造方法之前会先调用父类没有参数的构造方法其目的是) -- [26. == 与 equals(重要)](#26--与-equals重要) -- [27. hashCode 与 equals (重要)](#27-hashcode-与-equals-重要) - - [hashCode()介绍](#hashcode介绍) - - [为什么要有 hashCode](#为什么要有-hashcode) - - [hashCode()与 equals()的相关规定](#hashcode与-equals的相关规定) -- [28. 为什么 Java 中只有值传递?](#28-为什么-java-中只有值传递) -- [29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?](#29-简述线程程序进程的基本概念以及他们之间关系是什么) -- [30. 线程有哪些基本状态?](#30-线程有哪些基本状态) -- [31 关于 final 关键字的一些总结](#31-关于-final-关键字的一些总结) -- [32 Java 中的异常处理](#32-java-中的异常处理) - - [Java 异常类层次结构图](#java-异常类层次结构图) - - [Throwable 类常用方法](#throwable-类常用方法) - - [异常处理总结](#异常处理总结) -- [33 Java 序列化中如果有些字段不想进行序列化,怎么办?](#33-java-序列化中如果有些字段不想进行序列化怎么办) -- [34 获取用键盘输入常用的两种方法](#34-获取用键盘输入常用的两种方法) -- [35 Java 中 IO 流](#35-java-中-io-流) - - [Java 中 IO 流分为几种?](#java-中-io-流分为几种) - - [既然有了字节流,为什么还要有字符流?](#既然有了字节流为什么还要有字符流) - - [BIO,NIO,AIO 有什么区别?](#bionioaio-有什么区别) -- [36. 常见关键字总结:static,final,this,super](#36-常见关键字总结staticfinalthissuper) -- [37. Collections 工具类和 Arrays 工具类常见方法总结](#37-collections-工具类和-arrays-工具类常见方法总结) -- [38. 深拷贝 vs 浅拷贝](#38-深拷贝-vs-浅拷贝) -- [参考](#参考) -- [公众号](#公众号) - - - - -## 1. 面向对象和面向过程的区别 - -- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。** -- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。 - -参见 issue : [面向过程 :面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) - -> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。 -> -> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。 - -## 2. Java 语言有哪些特点? - -1. 简单易学; -2. 面向对象(封装,继承,多态); -3. 平台无关性( Java 虚拟机实现平台无关性); -4. 可靠性; -5. 安全性; -6. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持); -7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便); -8. 编译与解释并存; - -> 修正(参见: [issue#544](https://github.com/Snailclimb/JavaGuide/issues/544)):C++11 开始(2011 年的时候),C++就引入了多线程库,在 windows、linux、macos 都可以使用`std::thread`和`std::async`来创建线程。参考链接:http://www.cplusplus.com/reference/thread/thread/?kw=thread - -## 3. 关于 JVM JDK 和 JRE 最详细通俗的解答 - -### JVM - -Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 - -**什么是字节码?采用字节码的好处是什么?** - -> 在 Java 中,JVM 可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 - -**Java 程序从源代码到运行一般有下面 3 步:** - -![Java程序运行过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png) - -我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。 - -> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。 - -**总结:** - -Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 - -### JDK 和 JRE - -JDK 是 Java Development Kit,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。 - -JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。 - -如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 - -## 4. Oracle JDK 和 OpenJDK 的对比 - -可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么 Oracle 和 OpenJDK 之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。 - -对于 Java 7,没什么关键的地方。OpenJDK 项目主要基于 Sun 捐赠的 HotSpot 源代码。此外,OpenJDK 被选为 Java 7 的参考实现,由 Oracle 工程师维护。关于 JVM,JDK,JRE 和 OpenJDK 之间的区别,Oracle 博客帖子在 2012 年有一个更详细的答案: - -> 问:OpenJDK 存储库中的源代码与用于构建 Oracle JDK 的代码之间有什么区别? -> -> 答:非常接近 - 我们的 Oracle JDK 版本构建过程基于 OpenJDK 7 构建,只添加了几个部分,例如部署代码,其中包括 Oracle 的 Java 插件和 Java WebStart 的实现,以及一些封闭的源代码派对组件,如图形光栅化器,一些开源的第三方组件,如 Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源 Oracle JDK 的所有部分,除了我们考虑商业功能的部分。 - -**总结:** - -1. Oracle JDK 大概每 6 个月发一次主要版本,而 OpenJDK 版本大概每三个月发布一次。但这不是固定的,我觉得了解这个没啥用处。详情参见:[https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence](https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence) 。 -2. OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是 OpenJDK 的一个实现,并不是完全开源的; -3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码几乎相同,但 Oracle JDK 有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择 Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用 OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题; -4. 在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能; -5. Oracle JDK 不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本; -6. Oracle JDK 根据二进制代码许可协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。 - -## 5. Java 和 C++的区别? - -我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过 C++,也要记下来! - -- 都是面向对象的语言,都支持封装、继承和多态 -- Java 不提供指针来直接访问内存,程序内存更加安全 -- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。 -- Java 有自动内存管理机制,不需要程序员手动释放无用内存 -- **在 C 语言中,字符串或字符数组最后都会有一个额外的字符‘\0’来表示结束。但是,Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189](https://blog.csdn.net/sszgg2006/article/details/49148189) - -## 6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同? - -一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main()方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。 - -## 7. Java 应用程序与小程序之间有哪些差别? - -简单说应用程序是从主线程启动(也就是 `main()` 方法)。applet 小程序没有 `main()` 方法,主要是嵌在浏览器页面上运行(调用`init()`或者`run()`来启动),嵌入浏览器这点跟 flash 的小游戏类似。 - -## 8. 字符型常量和字符串常量的区别? - -1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符 -2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置) -3. 占内存大小 字符常量只占 2 个字节; 字符串常量占若干个字节 (**注意: char 在 Java 中占两个字节**) - -> java 编程思想第四版:2.2.2 节 -> ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) - -## 9. 构造器 Constructor 是否可被 override? - -Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 - -## 10. 重载和重写的区别 - -> 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理 -> -> 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法 - -#### 重载 - -发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 - -下面是《Java 核心技术》对重载这个概念的介绍: - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg) - -**综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。** - -#### 重写 - -重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。 - -1. 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。 -2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。 -3. 构造方法无法被重写 - -**综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变** - -**暖心的 Guide 哥最后再来个图标总结一下!** - -| 区别点 | 重载方法 | 重写方法 | -| :--------- | :------- | :--------------------------------------------- | -| 发生范围 | 同一个类 | 子类 中 | -| 参数列表 | 必须修改 | 一定不能修改 | -| 返回类型 | 可修改 | 一定不能修改 | -| 异常 | 可修改 | 可以减少或删除,一定不能抛出新的或者更广的异常 | -| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) | -| 发生阶段 | 编译期 | 运行期 | - -## 11. Java 面向对象编程三大特性: 封装 继承 多态 - -### 封装 - -封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 - -### 继承 - -继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 - -**关于继承如下 3 点请记住:** - -1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 -2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -### 多态 - -所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 - -在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 - -## 12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? - -**可变性** - -简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。 - -> 补充(来自[issue 675](https://github.com/Snailclimb/JavaGuide/issues/675)):在 Java 9 之后,String 类的实现改用 byte 数组存储字符串 `private final byte[] value`; - -而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。 - -StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。 - -`AbstractStringBuilder.java` - -```java -abstract class AbstractStringBuilder implements Appendable, CharSequence { - /** - * The value is used for character storage. - */ - char[] value; - - /** - * The count is the number of characters used. - */ - int count; - - AbstractStringBuilder(int capacity) { - value = new char[capacity]; - } -``` - -**线程安全性** - -String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。 - -**性能** - -每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -**对于三者使用的总结:** - -1. 操作少量的数据: 适用 String -2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder -3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer - -## 13. 自动装箱与拆箱 - -- **装箱**:将基本类型用它们对应的引用类型包装起来; -- **拆箱**:将包装类型转换为基本数据类型; - -更多内容见:[深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html) - -## 14. 在一个静态方法内调用一个非静态成员为什么是非法的? - -由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 - -## 15. 在 Java 中定义一个不做事且没有参数的构造方法的作用 - -Java 程序在执行子类的构造方法之前,如果没有用 `super()`来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 `super()`来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。 - -## 16. import java 和 javax 有什么区别? - -刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。 - -所以,实际上 java 和 javax 没有区别。这都是一个名字。 - -## 17. 接口和抽象类的区别是什么? - -1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 -2. 接口中除了 static、final 变量,不能有其他变量,而抽象类中则不一定。 -3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。 -4. 接口方法默认修饰符是 public,抽象方法可以有 public、protected 和 default 这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰!)。 -5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。 - -> 备注: -> -> 1. 在 JDK8 中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。(详见 issue:[https://github.com/Snailclimb/JavaGuide/issues/146](https://github.com/Snailclimb/JavaGuide/issues/146)。 -> 2. jdk9 的接口被允许定义私有方法 。 - -总结一下 jdk7~jdk9 Java 中接口概念的变化([相关阅读](https://www.geeksforgeeks.org/private-methods-java-9-interfaces/)): - -1. 在 jdk 7 或更早版本中,接口里面只能有常量变量和抽象方法。这些接口方法必须由选择实现接口的类实现。 -2. jdk8 的时候接口可以有默认方法和静态方法功能。 -3. Jdk 9 在接口中引入了私有方法和私有静态方法。 - -## 18. 成员变量与局部变量的区别有哪些? - -1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。 -2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 -3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 -4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 - -## 19. 创建一个对象用什么运算符?对象实体与对象引用有何不同? - -new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。 - -## 20. 什么是方法的返回值?返回值在类的方法里的作用是什么? - -方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作! - -## 21. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么? - -主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。 - -## 22. 构造方法有哪些特性? - -1. 名字与类名相同。 -2. 没有返回值,但不能用 void 声明构造函数。 -3. 生成类的对象时自动执行,无需调用。 - -## 23. 静态方法和实例方法有何不同 - -1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 - -2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。 - -## 24. 对象的相等与指向他们的引用相等,两者有什么不同? - -对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。 - -## 25. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? - -帮助子类做初始化工作。 - -## 26. == 与 equals(重要) - -**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。 - -**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: - -- 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 - -**举个例子:** - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -**说明:** - -- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -## 27. hashCode 与 equals (重要) - -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?” - -### hashCode()介绍 - -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。 - -散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -### 为什么要有 hashCode - -**我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与该位置其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 `equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -通过我们可以看出:`hashCode()` 的作用就是**获取哈希码**,也称为散列码;它实际上是返回一个 int 整数。这个**哈希码的作用**是确定该对象在哈希表中的索引位置。**`hashCode()`在散列表中才有用,在其它情况下没用**。在散列表中 hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。 - -### hashCode()与 equals()的相关规定 - -1. 如果两个对象相等,则 hashcode 一定也是相同的 -2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true -3. 两个对象有相同的 hashcode 值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) - -推荐阅读:[Java hashCode() 和 equals()的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html) - -## 28. 为什么 Java 中只有值传递? - -[为什么 Java 中只有值传递?](https://juejin.im/post/5e18879e6fb9a02fc63602e2) - -## 29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么? - -**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。 - -**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 -线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。 - -## 30. 线程有哪些基本状态? - -Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。 - -![Java线程的状态](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81.png) - -线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节): - -![Java线程状态变迁](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%20%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81.png) - -由上图可以看出: - -线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 - -> 操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。 - -![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png) - -当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的`run()`方法之后将会进入到 **TERMINATED(终止)** 状态。 - -## 31 关于 final 关键字的一些总结 - -final 关键字主要用在三个地方:变量、方法、类。 - -1. 对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。 -2. 当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为 final 方法。 -3. 使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。 - -## 32 Java 中的异常处理 - -### Java 异常类层次结构图 - -![Java异常类层次结构图](./images/java-exception-handling-class-hierarchy-diagram.jpg) - -

图片来自:https://simplesnippets.tech/exception-handling-in-java-part-1/

- - -![Java异常类层次结构图](./images/exception-architechture-java.png) - -

图片来自:https://chercher.tech/java-programming/exceptions-java

- - -在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 **Throwable 类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。 - -**Error(错误):是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。 - -这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java 虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。 - -**Exception(异常):是程序本身可以处理的异常**。Exception 类有一个重要的子类 **RuntimeException**。RuntimeException 异常由 Java 虚拟机抛出。**NullPointerException**(要访问的变量没有引用任何对象时,抛出该异常)、**ArithmeticException**(算术运算异常,一个整数除以 0 时,抛出该异常)和 **ArrayIndexOutOfBoundsException** (下标越界异常)。 - -**注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。** - -### Throwable 类常用方法 - -- **public string getMessage()**:返回异常发生时的简要描述 -- **public string toString()**:返回异常发生时的详细信息 -- **public string getLocalizedMessage()**:返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同 -- **public void printStackTrace()**:在控制台上打印 Throwable 对象封装的异常信息 - -### 异常处理总结 - -- **try 块:** 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。 -- **catch 块:** 用于处理 try 捕获到的异常。 -- **finally 块:** 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return - 语句时,finally 语句块将在方法返回之前被执行。 - -**在以下 4 种特殊情况下,finally 块不会被执行:** - -1. 在 finally 语句块第一行发生了异常。 因为在其他行,finally 块还是会得到执行 -2. 在前面的代码中用了 System.exit(int)已退出程序。 exit 是带参函数 ;若该语句在异常语句之后,finally 会执行 -3. 程序所在的线程死亡。 -4. 关闭 CPU。 - -下面这部分内容来自 issue:。 - -**注意:** 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。如下: - -```java - public static int f(int value) { - try { - return value * value; - } finally { - if (value == 2) { - return 0; - } - } - } -``` - -如果调用 `f(2)`,返回值将是 0,因为 finally 语句的返回值覆盖了 try 语句块的返回值。 - -## 33 Java 序列化中如果有些字段不想进行序列化,怎么办? - -对于不想进行序列化的变量,使用 transient 关键字修饰。 - -transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。 - -## 34 获取用键盘输入常用的两种方法 - -方法 1:通过 Scanner - -```java -Scanner input = new Scanner(System.in); -String s = input.nextLine(); -input.close(); -``` - -方法 2:通过 BufferedReader - -```java -BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); -String s = input.readLine(); -``` - -## 35 Java 中 IO 流 - -### Java 中 IO 流分为几种? - -- 按照流的流向分,可以分为输入流和输出流; -- 按照操作单元划分,可以划分为字节流和字符流; -- 按照流的角色划分为节点流和处理流。 - -Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。 - -- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 -- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 - -按操作方式分类结构图: - -![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png) - -按操作对象分类结构图: - -![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png) - -### 既然有了字节流,为什么还要有字符流? - -问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** - -回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - -### BIO,NIO,AIO 有什么区别? - -- **BIO (Blocking I/O):** 同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 -- **NIO (Non-blocking/New I/O):** NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 -- **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 - -## 36. 常见关键字总结:static,final,this,super - -详见笔主的这篇文章: https://snailclimb.gitee.io/javaguide/#/docs/java/basic/final,static,this,super - -## 37. Collections 工具类和 Arrays 工具类常见方法总结 - -详见笔主的这篇文章: https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/basic/Arrays,CollectionsCommonMethods.md - -## 38. 深拷贝 vs 浅拷贝 - -1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 -2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 - -![deep and shallow copy](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/java-deep-and-shallow-copy.jpg) - -## 参考 - -- https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre -- https://www.educba.com/oracle-vs-openjdk/ -- https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk?answertab=active#tab-top - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java 面试突击"** 即可免费领取! - -**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/Java\347\226\221\351\232\276\347\202\271.md" "b/docs/java/Java\347\226\221\351\232\276\347\202\271.md" deleted file mode 100644 index 4232e66edee..00000000000 --- "a/docs/java/Java\347\226\221\351\232\276\347\202\271.md" +++ /dev/null @@ -1,373 +0,0 @@ - - -- [1. 基础](#_1-基础) - - [1.1. 正确使用 equals 方法](#_11-正确使用-equals-方法) - - [1.2. 整型包装类值的比较](#_12-整型包装类值的比较) - - [1.3. BigDecimal](#_13-bigdecimal) - - [1.3.1. BigDecimal 的用处](#_131-bigdecimal-的用处) - - [1.3.2. BigDecimal 的大小比较](#_132-bigdecimal-的大小比较) - - [1.3.3. BigDecimal 保留几位小数](#_133-bigdecimal-保留几位小数) - - [1.3.4. BigDecimal 的使用注意事项](#_134-bigdecimal-的使用注意事项) - - [1.3.5. 总结](#_135-总结) - - [1.4. 基本数据类型与包装数据类型的使用标准](#_14-基本数据类型与包装数据类型的使用标准) -- [2. 集合](#_2-集合) - - [2.1. Arrays.asList()使用指南](#_21-arraysaslist使用指南) - - [2.1.1. 简介](#_211-简介) - - [2.1.2. 《阿里巴巴Java 开发手册》对其的描述](#_212-阿里巴巴java-开发手册对其的描述) - - [2.1.3. 使用时的注意事项总结](#_213-使用时的注意事项总结) - - [2.1.4. 如何正确的将数组转换为ArrayList?](#_214-如何正确的将数组转换为arraylist) - - [2.2. Collection.toArray()方法使用的坑&如何反转数组](#_22-collectiontoarray方法使用的坑如何反转数组) - - [2.3. 不要在 foreach 循环里进行元素的 remove/add 操作](#_23-不要在-foreach-循环里进行元素的-removeadd-操作) - - - -# 1. 基础 - -## 1.1. 正确使用 equals 方法 - -Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。 - -举个例子: - -```java -// 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常 -String str = null; -if (str.equals("SnailClimb")) { - ... -} else { - .. -} -``` - -运行上面的程序会抛出空指针异常,但是我们把第二行的条件判断语句改为下面这样的话,就不会抛出空指针异常,else 语句块得到执行。: - -```java -"SnailClimb".equals(str);// false -``` -不过更推荐使用 `java.util.Objects#equals`(JDK7 引入的工具类)。 - -```java -Objects.equals(null,"SnailClimb");// false -``` -我们看一下`java.util.Objects#equals`的源码就知道原因了。 -```java -public static boolean equals(Object a, Object b) { - // 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。 - return (a == b) || (a != null && a.equals(b)); - } -``` - -**注意:** - -Reference:[Java中equals方法造成空指针异常的原因及解决方案](https://blog.csdn.net/tick_tock97/article/details/72824894) - -- 每种原始类型都有默认值一样,如int默认值为 0,boolean 的默认值为 false,null 是任何引用类型的默认值,不严格的说是所有 Object 类型的默认值。 -- 可以使用 == 或者 != 操作来比较null值,但是不能使用其他算法或者逻辑操作。在Java中`null == null`将返回true。 -- 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常 - -## 1.2. 整型包装类值的比较 - -所有整型包装类对象值的比较必须使用equals方法。 - -先看下面这个例子: - -```java -Integer x = 3; -Integer y = 3; -System.out.println(x == y);// true -Integer a = new Integer(3); -Integer b = new Integer(3); -System.out.println(a == b);//false -System.out.println(a.equals(b));//true -``` - -当使用自动装箱方式创建一个Integer对象时,当数值在-128 ~127时,会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。所以上述代码中,x和y引用的是相同的Integer对象。 - -**注意:**如果你的IDE(IDEA/Eclipse)上安装了阿里巴巴的p3c插件,这个插件如果检测到你用 ==的话会报错提示,推荐安装一个这个插件,很不错。 - -## 1.3. BigDecimal - -### 1.3.1. BigDecimal 的用处 - -《阿里巴巴Java开发手册》中提到:**浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。** 具体原理和浮点数的编码方式有关,这里就不多提了,我们下面直接上实例: - -```java -float a = 1.0f - 0.9f; -float b = 0.9f - 0.8f; -System.out.println(a);// 0.100000024 -System.out.println(b);// 0.099999964 -System.out.println(a == b);// false -``` -具有基本数学知识的我们很清楚的知道输出并不是我们想要的结果(**精度丢失**),我们如何解决这个问题呢?一种很常用的方法是:**使用使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作。** - -```java -BigDecimal a = new BigDecimal("1.0"); -BigDecimal b = new BigDecimal("0.9"); -BigDecimal c = new BigDecimal("0.8"); -BigDecimal x = a.subtract(b);// 0.1 -BigDecimal y = b.subtract(c);// 0.1 -System.out.println(x.equals(y));// true -``` - -### 1.3.2. BigDecimal 的大小比较 - -`a.compareTo(b)` : 返回 -1 表示小于,0 表示 等于, 1表示 大于。 - -```java -BigDecimal a = new BigDecimal("1.0"); -BigDecimal b = new BigDecimal("0.9"); -System.out.println(a.compareTo(b));// 1 -``` -### 1.3.3. BigDecimal 保留几位小数 - -通过 `setScale`方法设置保留几位小数以及保留规则。保留规则有挺多种,不需要记,IDEA会提示。 - -```java -BigDecimal m = new BigDecimal("1.255433"); -BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN); -System.out.println(n);// 1.255 -``` - -### 1.3.4. BigDecimal 的使用注意事项 - -注意:我们在使用BigDecimal时,为了防止精度丢失,推荐使用它的 **BigDecimal(String)** 构造方法来创建对象。《阿里巴巴Java开发手册》对这部分内容也有提到如下图所示。 - -![《阿里巴巴Java开发手册》对这部分BigDecimal的描述](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/BigDecimal.png) - -### 1.3.5. 总结 - -BigDecimal 主要用来操作(大)浮点数,BigInteger 主要用来操作大整数(超过 long 类型)。 - -BigDecimal 的实现利用到了 BigInteger, 所不同的是 BigDecimal 加入了小数位的概念 - -## 1.4. 基本数据类型与包装数据类型的使用标准 - -Reference:《阿里巴巴Java开发手册》 - -- 【强制】所有的 POJO 类属性必须使用包装数据类型。 -- 【强制】RPC 方法的返回值和参数必须使用包装数据类型。 -- 【推荐】所有的局部变量使用基本数据类型。 - -比如我们如果自定义了一个Student类,其中有一个属性是成绩score,如果用Integer而不用int定义,一次考试,学生可能没考,值是null,也可能考了,但考了0分,值是0,这两个表达的状态明显不一样. - -**说明** :POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。 - -**正例** : 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。 - -**反例** : 比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。 - -# 2. 集合 - -## 2.1. Arrays.asList()使用指南 - -最近使用`Arrays.asList()`遇到了一些坑,然后在网上看到这篇文章:[Java Array to List Examples](http://javadevnotes.com/java-array-to-list-examples) 感觉挺不错的,但是还不是特别全面。所以,自己对于这块小知识点进行了简单的总结。 - -### 2.1.1. 简介 - -`Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个List集合。 - -```java -String[] myArray = { "Apple", "Banana", "Orange" }; -List myList = Arrays.asList(myArray); -//上面两个语句等价于下面一条语句 -List myList = Arrays.asList("Apple","Banana", "Orange"); -``` - -JDK 源码对于这个方法的说明: - -```java -/** - *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。 - */ -public static List asList(T... a) { - return new ArrayList<>(a); -} -``` - -### 2.1.2. 《阿里巴巴Java 开发手册》对其的描述 - -`Arrays.asList()`将数组转换为集合后,底层其实还是数组,《阿里巴巴Java 开发手册》对于这个方法有如下描述: - -![阿里巴巴Java开发手-Arrays.asList()方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/阿里巴巴Java开发手-Arrays.asList()方法.png) - -### 2.1.3. 使用时的注意事项总结 - -**传递的数组必须是对象数组,而不是基本类型。** - -`Arrays.asList()`是泛型方法,传入的对象必须是对象数组。 - -```java -int[] myArray = { 1, 2, 3 }; -List myList = Arrays.asList(myArray); -System.out.println(myList.size());//1 -System.out.println(myList.get(0));//数组地址值 -System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException -int [] array=(int[]) myList.get(0); -System.out.println(array[0]);//1 -``` -当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时List 的唯一元素就是这个数组,这也就解释了上面的代码。 - -我们使用包装类型数组就可以解决这个问题。 - -```java -Integer[] myArray = { 1, 2, 3 }; -``` - -**使用集合的修改方法:`add()`、`remove()`、`clear()`会抛出异常。** - -```java -List myList = Arrays.asList(1, 2, 3); -myList.add(4);//运行时报错:UnsupportedOperationException -myList.remove(1);//运行时报错:UnsupportedOperationException -myList.clear();//运行时报错:UnsupportedOperationException -``` - -`Arrays.asList()` 方法返回的并不是 `java.util.ArrayList` ,而是 `java.util.Arrays` 的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。 - -```java -List myList = Arrays.asList(1, 2, 3); -System.out.println(myList.getClass());//class java.util.Arrays$ArrayList -``` - -下图是`java.util.Arrays$ArrayList`的简易源码,我们可以看到这个类重写的方法有哪些。 - -```java - private static class ArrayList extends AbstractList - implements RandomAccess, java.io.Serializable - { - ... - - @Override - public E get(int index) { - ... - } - - @Override - public E set(int index, E element) { - ... - } - - @Override - public int indexOf(Object o) { - ... - } - - @Override - public boolean contains(Object o) { - ... - } - - @Override - public void forEach(Consumer action) { - ... - } - - @Override - public void replaceAll(UnaryOperator operator) { - ... - } - - @Override - public void sort(Comparator c) { - ... - } - } -``` - -我们再看一下`java.util.AbstractList`的`remove()`方法,这样我们就明白为啥会抛出`UnsupportedOperationException`。 - -```java -public E remove(int index) { - throw new UnsupportedOperationException(); -} -``` - -### 2.1.4. 如何正确的将数组转换为ArrayList? - -stackoverflow:https://dwz.cn/vcBkTiTW - -**1. 自己动手实现(教育目的)** - -```java -//JDK1.5+ -static List arrayToList(final T[] array) { - final List l = new ArrayList(array.length); - - for (final T s : array) { - l.add(s); - } - return (l); -} -``` - -```java -Integer [] myArray = { 1, 2, 3 }; -System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList -``` - -**2. 最简便的方法(推荐)** - -```java -List list = new ArrayList<>(Arrays.asList("a", "b", "c")) -``` - -**3. 使用 Java8 的Stream(推荐)** - -```java -Integer [] myArray = { 1, 2, 3 }; -List myList = Arrays.stream(myArray).collect(Collectors.toList()); -//基本类型也可以实现转换(依赖boxed的装箱操作) -int [] myArray2 = { 1, 2, 3 }; -List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList()); -``` - -**4. 使用 Guava(推荐)** - -对于不可变集合,你可以使用[`ImmutableList`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java)类及其[`of()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L101)与[`copyOf()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L225)工厂方法:(参数不能为空) - -```java -List il = ImmutableList.of("string", "elements"); // from varargs -List il = ImmutableList.copyOf(aStringArray); // from array -``` -对于可变集合,你可以使用[`Lists`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java)类及其[`newArrayList()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java#L87)工厂方法: - -```java -List l1 = Lists.newArrayList(anotherListOrCollection); // from collection -List l2 = Lists.newArrayList(aStringArray); // from array -List l3 = Lists.newArrayList("or", "string", "elements"); // from varargs -``` - -**5. 使用 Apache Commons Collections** - -```java -List list = new ArrayList(); -CollectionUtils.addAll(list, str); -``` - -## 2.2. Collection.toArray()方法使用的坑&如何反转数组 - -该方法是一个泛型方法:` T[] toArray(T[] a);` 如果`toArray`方法中没有传递任何参数的话返回的是`Object`类型数组。 - -```java -String [] s= new String[]{ - "dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A" -}; -List list = Arrays.asList(s); -Collections.reverse(list); -s=list.toArray(new String[0]);//没有指定类型的话会报错 -``` - -由于JVM优化,`new String[0]`作为`Collection.toArray()`方法的参数现在使用更好,`new String[0]`就是起一个模板的作用,指定了返回数组的类型,0是为了节省空间,因为它只是为了说明返回的类型。详见: - -## 2.3. 不要在 foreach 循环里进行元素的 remove/add 操作 - -如果要进行`remove`操作,可以调用迭代器的 `remove `方法而不是集合类的 remove 方法。因为如果列表在任何时间从结构上修改创建迭代器之后,以任何方式除非通过迭代器自身`remove/add`方法,迭代器都将抛出一个`ConcurrentModificationException`,这就是单线程状态下产生的 **fail-fast 机制**。 - -> **fail-fast 机制** :多个线程对 fail-fast 集合进行修改的时,可能会抛出ConcurrentModificationException,单线程下也会出现这种情况,上面已经提到过。 - -`java.util`包下面的所有的集合类都是fail-fast的,而`java.util.concurrent`包下面的所有的类都是fail-safe的。 - -![不要在 foreach 循环里进行元素的 remove/add 操作](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/foreach-remove:add.png) - - - diff --git "a/docs/java/Java\347\274\226\347\250\213\350\247\204\350\214\203.md" "b/docs/java/Java\347\274\226\347\250\213\350\247\204\350\214\203.md" deleted file mode 100644 index 6b4731ef3c9..00000000000 --- "a/docs/java/Java\347\274\226\347\250\213\350\247\204\350\214\203.md" +++ /dev/null @@ -1,30 +0,0 @@ -讲真的,下面推荐的文章或者资源建议阅读 3 遍以上。 - -### 团队 - -- **阿里巴巴Java开发手册(详尽版)** -- **Google Java编程风格指南:** - -### 个人 - -- **程序员你为什么这么累:** - -### 如何写出优雅的 Java 代码 - -1. 使用 IntelliJ IDEA 作为您的集成开发环境 (IDE) -1. 使用 JDK 8 或更高版本 -1. 使用 Maven/Gradle -1. 使用 Lombok -1. 编写单元测试 -1. 重构:常见,但也很慢 -1. 注意代码规范 -1. 定期联络客户,以获取他们的反馈 - -上述建议的详细内容:[八点建议助您写出优雅的Java代码](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485140&idx=1&sn=ecaeace613474f1859aaeed0282ae680&chksm=cea2491ff9d5c00982ffaece847ce1aead89fdb3fe190752d9837c075c79fc95db5940992c56&token=1328169465&lang=zh_CN&scene=21#wechat_redirect)。 - -更多代码优化相关内容推荐: - -- [业务复杂=if else?刚来的大神竟然用策略+工厂彻底干掉了他们!](https://juejin.im/post/5dad23685188251d2c4ea2b6) -- [一些不错的 Java 实践!推荐阅读3遍以上!](http://lrwinx.github.io/2017/03/04/%E7%BB%86%E6%80%9D%E6%9E%81%E6%81%90-%E4%BD%A0%E7%9C%9F%E7%9A%84%E4%BC%9A%E5%86%99java%E5%90%97/) -- [[解锁新姿势] 兄dei,你代码需要优化了](https://juejin.im/post/5dafbc02e51d4524a0060bdd) -- [消灭 Java 代码的“坏味道”](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485599&idx=1&sn=d83ff4e6b1ee951a0a33508a10980ea3&chksm=cea24754f9d5ce426d18b435a8c373ddc580c06c7d6a45cc51377361729c31c7301f1bbc3b78&token=1328169465&lang=zh_CN#rd) \ No newline at end of file diff --git a/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md b/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md deleted file mode 100644 index 7a692cf192c..00000000000 --- a/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md +++ /dev/null @@ -1,940 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [Java 并发进阶常见面试题总结](#java-并发进阶常见面试题总结) - - [1. synchronized 关键字](#1-synchronized-关键字) - - [1.1. 说一说自己对于 synchronized 关键字的了解](#11-说一说自己对于-synchronized-关键字的了解) - - [1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗](#12-说说自己是怎么使用-synchronized-关键字在项目中用到了吗) - - [1.3. 讲一下 synchronized 关键字的底层原理](#13-讲一下-synchronized-关键字的底层原理) - - [1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗) - - [1.5. 谈谈 synchronized和ReentrantLock 的区别](#15-谈谈-synchronized和reentrantlock-的区别) - - [2. volatile关键字](#2-volatile关键字) - - [2.1. 讲一下Java内存模型](#21-讲一下java内存模型) - - [2.2. 说说 synchronized 关键字和 volatile 关键字的区别](#22-说说-synchronized-关键字和-volatile-关键字的区别) - - [3. ThreadLocal](#3-threadlocal) - - [3.1. ThreadLocal简介](#31-threadlocal简介) - - [3.2. ThreadLocal示例](#32-threadlocal示例) - - [3.3. ThreadLocal原理](#33-threadlocal原理) - - [3.4. ThreadLocal 内存泄露问题](#34-threadlocal-内存泄露问题) - - [4. 线程池](#4-线程池) - - [4.1. 为什么要用线程池?](#41-为什么要用线程池) - - [4.2. 实现Runnable接口和Callable接口的区别](#42-实现runnable接口和callable接口的区别) - - [4.3. 执行execute()方法和submit()方法的区别是什么呢?](#43-执行execute方法和submit方法的区别是什么呢) - - [4.4. 如何创建线程池](#44-如何创建线程池) - - [5. Atomic 原子类](#5-atomic-原子类) - - [5.1. 介绍一下Atomic 原子类](#51-介绍一下atomic-原子类) - - [5.2. JUC 包中的原子类是哪4类?](#52-juc-包中的原子类是哪4类) - - [5.3. 讲讲 AtomicInteger 的使用](#53-讲讲-atomicinteger-的使用) - - [5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理](#54-能不能给我简单介绍一下-atomicinteger-类的原理) - - [6. AQS](#6-aqs) - - [6.1. AQS 介绍](#61-aqs-介绍) - - [6.2. AQS 原理分析](#62-aqs-原理分析) - - [6.2.1. AQS 原理概览](#621-aqs-原理概览) - - [6.2.2. AQS 对资源的共享方式](#622-aqs-对资源的共享方式) - - [6.2.3. AQS底层使用了模板方法模式](#623-aqs底层使用了模板方法模式) - - [6.3. AQS 组件总结](#63-aqs-组件总结) - - [7 Reference](#7-reference) - - - -# Java 并发进阶常见面试题总结 - -## 1. synchronized 关键字 - -### 1.1. 说一说自己对于 synchronized 关键字的了解 - -synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 - -另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 - - -### 1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 - -**synchronized关键字最主要的三种使用方式:** - -- **修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 -- **修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。 -- **修饰代码块:** 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 - -**总结:** synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能! - -下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。 - -面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” - -**双重校验锁实现对象单例(线程安全)** - -```java -public class Singleton { - - private volatile static Singleton uniqueInstance; - - private Singleton() { - } - - public static Singleton getUniqueInstance() { - //先判断对象是否已经实例过,没有实例化过才进入加锁代码 - if (uniqueInstance == null) { - //类对象加锁 - synchronized (Singleton.class) { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` -另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。 - -uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: - -1. 为 uniqueInstance 分配内存空间 -2. 初始化 uniqueInstance -3. 将 uniqueInstance 指向分配的内存地址 - -但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 - -使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 - -### 1.3. 讲一下 synchronized 关键字的底层原理 - -**synchronized 关键字底层原理属于 JVM 层面。** - -**① synchronized 同步语句块的情况** - -```java -public class SynchronizedDemo { - public void method() { - synchronized (this) { - System.out.println("synchronized 代码块"); - } - } -} - -``` - -通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 - -![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理.png) - -从上面我们可以看出: - -**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 - -**② synchronized 修饰方法的的情况** - -```java -public class SynchronizedDemo2 { - public synchronized void method() { - System.out.println("synchronized 方法"); - } -} - -``` - -![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理2.png) - -synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 - - -### 1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗 - -JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 - -锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 - -关于这几种优化的详细信息可以查看笔主的这篇文章: - -### 1.5. 谈谈 synchronized和ReentrantLock 的区别 - - -**① 两者都是可重入锁** - -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 - -**② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API** - -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -**③ ReentrantLock 比 synchronized 增加了一些高级功能** - -相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)** - -- **ReentrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 -- synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。 - -如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。 - -**④ 性能已不是选择标准** - -## 2. volatile关键字 - -### 2.1. 讲一下Java内存模型 - - -在 JDK1.2 之前,Java的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。 - -![数据不一致](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数据不一致.png) - -要解决这个问题,就需要把变量声明为**volatile**,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。 - -说白了, **volatile** 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。 - -![volatile关键字的可见性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/volatile关键字的可见性.png) - -### 2.2 并发编程的三个重要特性 - -1. **原子性** : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。`synchronized ` 可以保证代码片段的原子性。 -2. **可见性** :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。`volatile` 关键字可以保证共享变量的可见性。 -3. **有序性** :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。`volatile` 关键字可以禁止指令进行重排序优化。 - -### 2.3. 说说 synchronized 关键字和 volatile 关键字的区别 - -`synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在: - -- **volatile关键字**是线程同步的**轻量级实现**,所以**volatile性能肯定比synchronized关键字要好**。但是**volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块**。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,**实际开发中使用 synchronized 关键字的场景还是更多一些**。 -- **多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞** -- **volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。** -- **volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。** - -## 3. ThreadLocal - -### 3.1. ThreadLocal简介 - -通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** - -**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** - -再举个简单的例子: - -比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来避免这两个线程竞争的。 - -### 3.2. ThreadLocal示例 - -相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 - -```java -import java.text.SimpleDateFormat; -import java.util.Random; - -public class ThreadLocalExample implements Runnable{ - - // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 - private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); - - public static void main(String[] args) throws InterruptedException { - ThreadLocalExample obj = new ThreadLocalExample(); - for(int i=0 ; i<10; i++){ - Thread t = new Thread(obj, ""+i); - Thread.sleep(new Random().nextInt(1000)); - t.start(); - } - } - - @Override - public void run() { - System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); - try { - Thread.sleep(new Random().nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - //formatter pattern is changed here by thread, but it won't reflect to other threads - formatter.set(new SimpleDateFormat()); - - System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); - } - -} - -``` - -Output: - -``` -Thread Name= 0 default Formatter = yyyyMMdd HHmm -Thread Name= 0 formatter = yy-M-d ah:mm -Thread Name= 1 default Formatter = yyyyMMdd HHmm -Thread Name= 2 default Formatter = yyyyMMdd HHmm -Thread Name= 1 formatter = yy-M-d ah:mm -Thread Name= 3 default Formatter = yyyyMMdd HHmm -Thread Name= 2 formatter = yy-M-d ah:mm -Thread Name= 4 default Formatter = yyyyMMdd HHmm -Thread Name= 3 formatter = yy-M-d ah:mm -Thread Name= 4 formatter = yy-M-d ah:mm -Thread Name= 5 default Formatter = yyyyMMdd HHmm -Thread Name= 5 formatter = yy-M-d ah:mm -Thread Name= 6 default Formatter = yyyyMMdd HHmm -Thread Name= 6 formatter = yy-M-d ah:mm -Thread Name= 7 default Formatter = yyyyMMdd HHmm -Thread Name= 7 formatter = yy-M-d ah:mm -Thread Name= 8 default Formatter = yyyyMMdd HHmm -Thread Name= 9 default Formatter = yyyyMMdd HHmm -Thread Name= 8 formatter = yy-M-d ah:mm -Thread Name= 9 formatter = yy-M-d ah:mm -``` - -从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。 - -上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。 - -```java - private static final ThreadLocal formatter = new ThreadLocal(){ - @Override - protected SimpleDateFormat initialValue() - { - return new SimpleDateFormat("yyyyMMdd HHmm"); - } - }; -``` - -### 3.3. ThreadLocal原理 - -从 `Thread`类源代码入手。 - -```java -public class Thread implements Runnable { - ...... -//与此线程有关的ThreadLocal值。由ThreadLocal类维护 -ThreadLocal.ThreadLocalMap threadLocals = null; - -//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 -ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - ...... -} -``` - -从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 - -`ThreadLocal`类的`set()`方法 - -```java - public void set(T value) { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) - map.set(this, value); - else - createMap(t, value); - } - ThreadLocalMap getMap(Thread t) { - return t.threadLocals; - } -``` - -通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。 - -**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为key ,Object 对象为 value的键值对。** - -```java -ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { - ...... -} -``` - -比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。 - -![ThreadLocal数据结构](https://upload-images.jianshu.io/upload_images/7432604-ad2ff581127ba8cc.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/806) - -`ThreadLocalMap`是`ThreadLocal`的静态内部类。 - -![ThreadLocal内部类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadLocal内部类.png) - -### 3.4. ThreadLocal 内存泄露问题 - -`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 - -```java - static class Entry extends WeakReference> { - /** The value associated with this ThreadLocal. */ - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } - } -``` - -**弱引用介绍:** - -> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 -> -> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 - -## 4. 线程池 - -### 4.1. 为什么要用线程池? - -> **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** - -**线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 - -这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: - -- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - -### 4.2. 实现Runnable接口和Callable接口的区别 - -`Runnable`自Java 1.0以来一直存在,但`Callable`仅在Java 1.5中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。 - -工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callable(Runnable task`)或 `Executors.callable(Runnable task,Object resule)`)。 - -`Runnable.java` - -```java -@FunctionalInterface -public interface Runnable { - /** - * 被线程执行,没有返回值也无法抛出异常 - */ - public abstract void run(); -} -``` - -`Callable.java` - -```java -@FunctionalInterface -public interface Callable { - /** - * 计算结果,或在无法这样做时抛出异常。 - * @return 计算得出的结果 - * @throws 如果无法计算结果,则抛出异常 - */ - V call() throws Exception; -} -``` - -### 4.3. 执行execute()方法和submit()方法的区别是什么呢? - -1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** -2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 - -我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码: - -```java - public Future submit(Runnable task) { - if (task == null) throw new NullPointerException(); - RunnableFuture ftask = newTaskFor(task, null); - execute(ftask); - return ftask; - } -``` - -上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。 - -```java - protected RunnableFuture newTaskFor(Runnable runnable, T value) { - return new FutureTask(runnable, value); - } -``` - -我们再来看看`execute()`方法: - -```java - public void execute(Runnable command) { - ... - } -``` - -### 4.4. 如何创建线程池 - -《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 - -> Executors 返回线程池对象的弊端如下: -> -> - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。 -> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。 - -**方式一:通过构造方法实现** -![ThreadPoolExecutor构造方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadPoolExecutor构造方法.png) -**方式二:通过Executor 框架的工具类Executors来实现** -我们可以创建三种类型的ThreadPoolExecutor: - -- **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 - -对应Executors工具类中的方法如图所示: -![Executor框架的工具类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Executor框架的工具类.png) - -### 4.5 ThreadPoolExecutor 类分析 - -`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。 - -```java - /** - * 用给定的初始参数创建一个新的ThreadPoolExecutor。 - */ - public ThreadPoolExecutor(int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - ThreadFactory threadFactory, - RejectedExecutionHandler handler) { - if (corePoolSize < 0 || - maximumPoolSize <= 0 || - maximumPoolSize < corePoolSize || - keepAliveTime < 0) - throw new IllegalArgumentException(); - if (workQueue == null || threadFactory == null || handler == null) - throw new NullPointerException(); - this.corePoolSize = corePoolSize; - this.maximumPoolSize = maximumPoolSize; - this.workQueue = workQueue; - this.keepAliveTime = unit.toNanos(keepAliveTime); - this.threadFactory = threadFactory; - this.handler = handler; - } -``` - -**下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。** - -#### 4.5.1 `ThreadPoolExecutor`构造函数重要参数分析 - -**`ThreadPoolExecutor` 3 个最重要的参数:** - -- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 -- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 - -`ThreadPoolExecutor`其他常见参数: - -1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; -2. **`unit`** : `keepAliveTime` 参数的时间单位。 -3. **`threadFactory`** :executor 创建新线程的时候会用到。 -4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。 - -#### 4.5.2 `ThreadPoolExecutor` 饱和策略 - -**`ThreadPoolExecutor` 饱和策略定义:** - -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略: - -- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 -- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 - -举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了) - -### 4.6 一个简单的线程池Demo:`Runnable`+`ThreadPoolExecutor` - -为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。 - -首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。) - -`MyRunnable.java` - -```java -import java.util.Date; - -/** - * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 - * @author shuang.kou - */ -public class MyRunnable implements Runnable { - - private String command; - - public MyRunnable(String s) { - this.command = s; - } - - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); - processCommand(); - System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); - } - - private void processCommand() { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - @Override - public String toString() { - return this.command; - } -} - -``` - -编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。 - -`ThreadPoolExecutorDemo.java` - -```java -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class ThreadPoolExecutorDemo { - - private static final int CORE_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = 10; - private static final int QUEUE_CAPACITY = 100; - private static final Long KEEP_ALIVE_TIME = 1L; - public static void main(String[] args) { - - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy()); - - for (int i = 0; i < 10; i++) { - //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) - Runnable worker = new MyRunnable("" + i); - //执行Runnable - executor.execute(worker); - } - //终止线程池 - executor.shutdown(); - while (!executor.isTerminated()) { - } - System.out.println("Finished all threads"); - } -} - -``` - -可以看到我们上面的代码指定了: - -1. `corePoolSize`: 核心线程数为 5。 -2. `maximumPoolSize` :最大线程数 10 -3. `keepAliveTime` : 等待时间为 1L。 -4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。 -5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100; -6. `handler`:饱和策略为 `CallerRunsPolicy`。 - -**Output:** - -``` -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 - -``` - -### 4.7 线程池原理分析 - -承接 4.6 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) - -现在,我们就分析上面的输出内容来简单分析一下线程池原理。 - -**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.6 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: - -```java - // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) - private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); - - private static int workerCountOf(int c) { - return c & CAPACITY; - } - - private final BlockingQueue workQueue; - - public void execute(Runnable command) { - // 如果任务为null,则抛出异常。 - if (command == null) - throw new NullPointerException(); - // ctl 中保存的线程池当前的一些状态信息 - int c = ctl.get(); - - // 下面会涉及到 3 步 操作 - // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize - // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - if (workerCountOf(c) < corePoolSize) { - if (addWorker(command, true)) - return; - c = ctl.get(); - } - // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里 - // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去 - if (isRunning(c) && workQueue.offer(command)) { - int recheck = ctl.get(); - // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 - if (!isRunning(recheck) && remove(command)) - reject(command); - // 如果当前线程池为空就新创建一个线程并执行。 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - } - //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 - else if (!addWorker(command, false)) - reject(command); - } -``` - -通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。 - -![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png) - -现在,让我们在回到 4.6 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢? - -没搞懂的话,也没关系,可以看看我的分析: - -> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。 - -## 5. Atomic 原子类 - -### 5.1. 介绍一下Atomic 原子类 - -Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 - -所以,所谓原子类说简单点就是具有原子/原子操作特征的类。 - - -并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。 - -![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) - -### 5.2. JUC 包中的原子类是哪4类? - -**基本类型** - -使用原子的方式更新基本类型 - -- AtomicInteger:整形原子类 -- AtomicLong:长整型原子类 -- AtomicBoolean:布尔型原子类 - -**数组类型** - -使用原子的方式更新数组里的某个元素 - - -- AtomicIntegerArray:整形数组原子类 -- AtomicLongArray:长整形数组原子类 -- AtomicReferenceArray:引用类型数组原子类 - -**引用类型** - -- AtomicReference:引用类型原子类 -- AtomicStampedReference:原子更新引用类型里的字段原子类 -- AtomicMarkableReference :原子更新带有标记位的引用类型 - -**对象的属性修改类型** - -- AtomicIntegerFieldUpdater:原子更新整形字段的更新器 -- AtomicLongFieldUpdater:原子更新长整形字段的更新器 -- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - - -### 5.3. 讲讲 AtomicInteger 的使用 - - **AtomicInteger 类常用方法** - -```java -public final int get() //获取当前的值 -public final int getAndSet(int newValue)//获取当前的值,并设置新的值 -public final int getAndIncrement()//获取当前的值,并自增 -public final int getAndDecrement() //获取当前的值,并自减 -public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 -boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) -public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` - - **AtomicInteger 类的使用示例** - -使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。 -```java -class AtomicIntegerTest { - private AtomicInteger count = new AtomicInteger(); - //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。 - public void increment() { - count.incrementAndGet(); - } - - public int getCount() { - return count.get(); - } -} - -``` - -### 5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理 - -AtomicInteger 线程安全原理简单分析 - -AtomicInteger 类的部分源码: - -```java - // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) - private static final Unsafe unsafe = Unsafe.getUnsafe(); - private static final long valueOffset; - - static { - try { - valueOffset = unsafe.objectFieldOffset - (AtomicInteger.class.getDeclaredField("value")); - } catch (Exception ex) { throw new Error(ex); } - } - - private volatile int value; -``` - -AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 - -CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 - -关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg) - -## 6. AQS - -### 6.1. AQS 介绍 - -AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。 - -![AQS类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS类.png) - -AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。 - -### 6.2. AQS 原理分析 - -AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。 - -> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 - -下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。 - -#### 6.2.1. AQS 原理概览 - -**AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。** - -> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 - -看个AQS(AbstractQueuedSynchronizer)原理图: - - -![AQS原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS原理图.png) - -AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。 - -```java -private volatile int state;//共享变量,使用volatile修饰保证线程可见性 -``` - -状态信息通过protected类型的getState,setState,compareAndSetState进行操作 - -```java - -//返回同步状态的当前值 -protected final int getState() { - return state; -} - // 设置同步状态的值 -protected final void setState(int newState) { - state = newState; -} -//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) -protected final boolean compareAndSetState(int expect, int update) { - return unsafe.compareAndSwapInt(this, stateOffset, expect, update); -} -``` - -#### 6.2.2. AQS 对资源的共享方式 - -**AQS定义两种资源共享方式** - -- **Exclusive**(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁: - - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 - - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 -- **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 - -ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。 - -不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。 - -#### 6.2.3. AQS底层使用了模板方法模式 - -同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): - -1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放) -2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 - -这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。 - -**AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:** - -```java -isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 -tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 -tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 -tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 -tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 - -``` - -默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 - -以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。 - -再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。 - -一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 - -推荐两篇 AQS 原理和相关源码分析的文章: - -- http://www.cnblogs.com/waterystone/p/4920797.html -- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html - -### 6.3. AQS 组件总结 - -- **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。 -- **CountDownLatch (倒计时器):** CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 -- **CyclicBarrier(循环栅栏):** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 - -## 7 Reference - -- 《深入理解 Java 虚拟机》 -- 《实战 Java 高并发程序设计》 -- 《Java并发编程的艺术》 -- http://www.cnblogs.com/waterystone/p/4920797.html -- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html -- - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/Multithread/ThreadLocal.md b/docs/java/Multithread/ThreadLocal.md deleted file mode 100644 index 06cdbea575e..00000000000 --- a/docs/java/Multithread/ThreadLocal.md +++ /dev/null @@ -1,170 +0,0 @@ -[ThreadLocal造成OOM内存溢出案例演示与原理分析](https://blog.csdn.net/xlgen157387/article/details/78298840) - -[深入理解 Java 之 ThreadLocal 工作原理]() - -## ThreadLocal - -### ThreadLocal简介 - -通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** - -**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** - -再举个简单的例子: - -比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来这两个线程竞争的。 - -### ThreadLocal示例 - -相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 - -```java -import java.text.SimpleDateFormat; -import java.util.Random; - -public class ThreadLocalExample implements Runnable{ - - // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 - private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); - - public static void main(String[] args) throws InterruptedException { - ThreadLocalExample obj = new ThreadLocalExample(); - for(int i=0 ; i<10; i++){ - Thread t = new Thread(obj, ""+i); - Thread.sleep(new Random().nextInt(1000)); - t.start(); - } - } - - @Override - public void run() { - System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); - try { - Thread.sleep(new Random().nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - //formatter pattern is changed here by thread, but it won't reflect to other threads - formatter.set(new SimpleDateFormat()); - - System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); - } - -} - -``` - -Output: - -``` -Thread Name= 0 default Formatter = yyyyMMdd HHmm -Thread Name= 0 formatter = yy-M-d ah:mm -Thread Name= 1 default Formatter = yyyyMMdd HHmm -Thread Name= 2 default Formatter = yyyyMMdd HHmm -Thread Name= 1 formatter = yy-M-d ah:mm -Thread Name= 3 default Formatter = yyyyMMdd HHmm -Thread Name= 2 formatter = yy-M-d ah:mm -Thread Name= 4 default Formatter = yyyyMMdd HHmm -Thread Name= 3 formatter = yy-M-d ah:mm -Thread Name= 4 formatter = yy-M-d ah:mm -Thread Name= 5 default Formatter = yyyyMMdd HHmm -Thread Name= 5 formatter = yy-M-d ah:mm -Thread Name= 6 default Formatter = yyyyMMdd HHmm -Thread Name= 6 formatter = yy-M-d ah:mm -Thread Name= 7 default Formatter = yyyyMMdd HHmm -Thread Name= 7 formatter = yy-M-d ah:mm -Thread Name= 8 default Formatter = yyyyMMdd HHmm -Thread Name= 9 default Formatter = yyyyMMdd HHmm -Thread Name= 8 formatter = yy-M-d ah:mm -Thread Name= 9 formatter = yy-M-d ah:mm -``` - -从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。 - -上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。 - -```java - private static final ThreadLocal formatter = new ThreadLocal(){ - @Override - protected SimpleDateFormat initialValue() - { - return new SimpleDateFormat("yyyyMMdd HHmm"); - } - }; -``` - -### ThreadLocal原理 - -从 `Thread`类源代码入手。 - -```java -public class Thread implements Runnable { - ...... -//与此线程有关的ThreadLocal值。由ThreadLocal类维护 -ThreadLocal.ThreadLocalMap threadLocals = null; - -//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 -ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - ...... -} -``` - -从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 - -`ThreadLocal`类的`set()`方法 - -```java - public void set(T value) { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) - map.set(this, value); - else - createMap(t, value); - } - ThreadLocalMap getMap(Thread t) { - return t.threadLocals; - } -``` - -通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。** - -**每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。`ThreadLocal` 是 map结构是为了让每个线程可以关联多个 `ThreadLocal`变量。这也就解释了ThreadLocal声明的变量为什么在每一个线程都有自己的专属本地变量。 - -```java -public class Thread implements Runnable { - ...... -//与此线程有关的ThreadLocal值。由ThreadLocal类维护 -ThreadLocal.ThreadLocalMap threadLocals = null; - -//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 -ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - ...... -} -``` - -`ThreadLocalMap`是`ThreadLocal`的静态内部类。 - -![ThreadLocal内部类](https://ws1.sinaimg.cn/large/006rNwoDgy1g2f47u9li2j30ka08cq43.jpg) - -### ThreadLocal 内存泄露问题 - -`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 - -```java - static class Entry extends WeakReference> { - /** The value associated with this ThreadLocal. */ - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } - } -``` - -**弱引用介绍:** - -> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 -> -> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 diff --git a/docs/java/Multithread/synchronized.md b/docs/java/Multithread/synchronized.md deleted file mode 100644 index 3c926654331..00000000000 --- a/docs/java/Multithread/synchronized.md +++ /dev/null @@ -1,169 +0,0 @@ - - -![Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/%E4%BA%8C%20%20Synchronized%20%E5%85%B3%E9%94%AE%E5%AD%97%E4%BD%BF%E7%94%A8%E3%80%81%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E3%80%81JDK1.6%20%E4%B9%8B%E5%90%8E%E7%9A%84%E5%BA%95%E5%B1%82%E4%BC%98%E5%8C%96%E4%BB%A5%E5%8F%8A%20%E5%92%8CReenTrantLock%20%E7%9A%84%E5%AF%B9%E6%AF%94.png) - -### synchronized关键字最主要的三种使用方式的总结 - -- **修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁** -- **修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁** 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。 -- **修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。** 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能! - -下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。 - -面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” - - - -**双重校验锁实现对象单例(线程安全)** - -```java -public class Singleton { - - private volatile static Singleton uniqueInstance; - - private Singleton() { - } - - public static Singleton getUniqueInstance() { - //先判断对象是否已经实例过,没有实例化过才进入加锁代码 - if (uniqueInstance == null) { - //类对象加锁 - synchronized (Singleton.class) { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` -另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。 - -uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: - -1. 为 uniqueInstance 分配内存空间 -2. 初始化 uniqueInstance -3. 将 uniqueInstance 指向分配的内存地址 - -但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 - -使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 - - -###synchronized 关键字底层原理总结 - - - -**synchronized 关键字底层原理属于 JVM 层面。** - -**① synchronized 同步语句块的情况** - -```java -public class SynchronizedDemo { - public void method() { - synchronized (this) { - System.out.println("synchronized 代码块"); - } - } -} - -``` - -通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 - -![synchronized 关键字原理](https://images.gitbook.cn/abc37c80-d21d-11e8-aab3-09d30029e0d5) - -从上面我们可以看出: - -**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 - -**② synchronized 修饰方法的的情况** - -```java -public class SynchronizedDemo2 { - public synchronized void method() { - System.out.println("synchronized 方法"); - } -} - -``` - -![synchronized 关键字原理](https://images.gitbook.cn/7d407bf0-d21e-11e8-b2d6-1188c7e0dd7e) - -synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 - - -在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 - - -### JDK1.6 之后的底层优化 - -JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 - -锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 - -**①偏向锁** - -**引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。 - -偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。 - -但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 - -**② 轻量级锁** - -倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。** 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。 - -**轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!** - -**③ 自旋锁和自适应自旋** - -轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。 - -互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。 - -**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。 - -百度百科对自旋锁的解释: - -> 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。 - -自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过`--XX:+UseSpinning`参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。**自旋次数的默认值是10次,用户可以修改`--XX:PreBlockSpin`来更改**。 - -另外,**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。 - -**④ 锁消除** - -锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。 - -**⑤ 锁粗化** - -原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。 - -大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。 - -### Synchronized 和 ReenTrantLock 的对比 - - -**① 两者都是可重入锁** - -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 - -**② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API** - -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -**③ ReenTrantLock 比 synchronized 增加了一些高级功能** - -相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)** - -- **ReenTrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 -- synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。 - -如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。 - -**④ 性能已不是选择标准** - -在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。**JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作**。 diff --git "a/docs/java/Multithread/\345\210\233\345\273\272\347\272\277\347\250\213\347\232\204\345\207\240\347\247\215\346\226\271\345\274\217\346\200\273\347\273\223.md" "b/docs/java/Multithread/\345\210\233\345\273\272\347\272\277\347\250\213\347\232\204\345\207\240\347\247\215\346\226\271\345\274\217\346\200\273\347\273\223.md" deleted file mode 100644 index 764a63a492a..00000000000 --- "a/docs/java/Multithread/\345\210\233\345\273\272\347\272\277\347\250\213\347\232\204\345\207\240\347\247\215\346\226\271\345\274\217\346\200\273\347\273\223.md" +++ /dev/null @@ -1,40 +0,0 @@ -## 面试官:“创建线程有哪几种常见的方式?” - -1. 继承 Thread 类 -2. 实现 Runnable 接口 -3. 使用 Executor 框架 -4. 使用 FutureTask - -## 最简单的两种方式 - -### 1.继承 Thread 类 - - - - - -### 2.实现 Runnable 接口 - -## 比较实用的两种方式 - -### 3.使用 Executor 框架 - -Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。 - -> 补充:this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。 - -Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。 - -为了能搞懂如何使用 Executor 框架创建 - -### Executor 框架结构(主要由三大部分组成) - -#### 1) 任务(`Runnable` /`Callable`) - -执行任务需要实现的 **`Runnable` 接口** 或 **`Callable`接口**。**`Runnable` 接口**或 **`Callable` 接口** 实现类都可以被 **`ThreadPoolExecutor`** 或 **`ScheduledThreadPoolExecutor`** 执行。 - -#### 2) 任务的执行(`Executor`) - -如下图所示,包括任务执行机制的核心接口 **`Executor`** ,以及继承自 `Executor` 接口的 **`ExecutorService` 接口。`ThreadPoolExecutor`** 和 **`ScheduledThreadPoolExecutor`** 这两个关键类实现了 **ExecutorService 接口**。 - -### 4.使用 FutureTask \ No newline at end of file diff --git "a/docs/java/Multithread/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227.md" "b/docs/java/Multithread/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227.md" deleted file mode 100644 index 06381d7c277..00000000000 --- "a/docs/java/Multithread/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227.md" +++ /dev/null @@ -1,158 +0,0 @@ -## 前言 - -这是我的第二篇专门介绍如何去学习某个知识点的文章,在上一篇[《写给 Java 程序员看的算法学习指南!》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486508&idx=1&sn=ce2faafcde166d5412d7166a01fdc1e9&chksm=cea243e7f9d5caf1dbf4d6ccf0438a1731bc0070310bba1ac481d485e4a6756349c20f02a6b1&token=211950660&lang=zh_CN#rd) 的文章中,我推荐了一些关于 **算法学习的书籍以及资源** 。 - -相比于写技术文章来说,写这种这种类型的文章实际花费的时间可能会稍微少一点。但是,这种学习指南形式的文章,我想对于 Java 初学者甚至是工作几年的 Java 工程师来说应该还是非常有帮助的! - -我们都知道多线程应该是大部分 Java 程序员最难啃的一块骨头之一,这部分内容的难度跨度大,难实践,并且市面上的参考资料的质量也层次不齐。 - -在这篇文章中,我会首先介绍一下 **Java 多线程学习** 中比较重要的一些问题,然后还会推荐一些比较不错的学习资源供大家参考。希望对你们学习多线程相关的知识能有帮助。以下介绍的很多知识点你都可以在这里找到:[https://snailclimb.gitee.io/javaguide/#/?id=并发](https://snailclimb.gitee.io/javaguide/#/?id=并发) - -![](https://imgkr.cn-bj.ufileos.com/49f0b564-224d-43d8-813e-0fe53196c1a9.png) - -**另外,我还将本文的内容同步到了 Github 上,点击阅读原文即可直达。如果你觉得有任何需要完善和修改的地方,都可以去 Github 给我提交 Issue 或者 PR(推荐)。** - -## 一.Java 多线程知识点总结 - -### 1.1.多线程基础 - -1. 什么是线程和进程? 线程与进程的关系,区别及优缺点? -2. 说说并发与并行的区别? -3. 为什么要使用多线程呢? -4. 使用多线程可能带来什么问题?(内存泄漏、死锁、线程不安全等等) -5. 创建线程有哪几种方式?(a.继承 Thread 类;b.实现 Runnable 接口;c. 使用 Executor 框架;d.使用 FutureTask) -6. 说说线程的生命周期和状态? -7. 什么是上下文切换? -8. 什么是线程死锁?如何避免死锁? -9. 说说 sleep() 方法和 wait() 方法区别和共同点? -10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? -11. ...... - -### 1.2.多线程知识进阶 - -#### volatile 关键字 - -1. Java 内存模型(**JMM**); -2. 重排序与 happens-before 原则了解吗? -3. volatile 关键字的作用; -4. 说说 synchronized 关键字和 volatile 关键字的区别; -5. ...... - -#### ThreadLocal - -1. 有啥用(解决了什么问题)?怎么用? -2. 原理了解吗? -3. 内存泄露问题了解吗? - -#### 线程池 - -1. 为什么要用线程池? -2. 你会使用线程池吗? -3. 如何创建线程池比较好? (推荐使用 `ThreadPoolExecutor` 构造函数创建线程池) -4. `ThreadPoolExecutor` 类的重要参数了解吗?`ThreadPoolExecutor` 饱和策略了解吗? -5. 线程池原理了解吗? -6. 几种常见的线程池了解吗?为什么不推荐使用`FixedThreadPool`? -7. 如何设置线程池的大小? -8. ...... - -#### AQS - -1. 简介 -2. 原理 -3. AQS 常用组件。 - - **Semaphore(信号量)**-允许多个线程同时访问 - - **CountDownLatch (倒计时器)**-CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。 - - **CyclicBarrier(循环栅栏)**-CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。 - - **ReentrantLock 和 ReentrantReadWriteLock** - - ...... - -#### 锁 - -锁的常见分类 - -1. 可重入锁和非可重入锁 -2. 公平锁与非公平锁 -3. 读写锁和排它锁 - -**synchronized 关键字** - -1. 说一说自己对于 synchronized 关键字的了解; -2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗; -3. 讲一下 synchronized 关键字的底层原理; -4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗; -5. 谈谈 synchronized 和 ReentrantLock 的区别; -6. ...... - -**ReentrantLock 和 ReentrantReadWriteLock** - -**ReadWriteLock** - -**StampedLock(JDK8)** - -#### **Atomic 与 CAS** - -**CAS:** - -1. 介绍 -2. 原理 - -**Atomic 原子类:** - -1. 介绍一下 Atomic 原子类; -2. JUC 包中的原子类是哪 4 类?; -3. 讲讲 AtomicInteger 的使用; -4. 能不能给我简单介绍一下 AtomicInteger 类的原理。 -5. ...... - -#### 并发容器 - -JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 - -- **ConcurrentHashMap:** 线程安全的 HashMap -- **CopyOnWriteArrayList:** 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector. -- **ConcurrentLinkedQueue:** 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。 -- **BlockingQueue:** 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。 -- **ConcurrentSkipListMap:** 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。 -- ...... - -#### Future 和 CompletableFuture - -## 二.书籍推荐 - -#### 《Java 并发编程之美》 - -![《Java 并发编程之美》](https://imgkr.cn-bj.ufileos.com/b4c03ec2-f907-47a4-ad19-731c969a499b.png) - -**我觉得这本书还是非常适合我们用来学习 Java 多线程的。这本书的讲解非常通俗易懂,作者从并发编程基础到实战都是信手拈来。** - -另外,这本书的作者加多自身也会经常在网上发布各种技术文章。我觉得这本书也是加多大佬这么多年在多线程领域的沉淀所得的结果吧!他书中的内容基本都是结合代码讲解,非常有说服力! - -#### 《实战 Java 高并发程序设计》 - -![《实战 Java 高并发程序设计》](https://imgkr.cn-bj.ufileos.com/0d6e5484-aea1-41cc-8417-4694c6028012.png) - -这个是我第二本要推荐的书籍,比较适合作为多线程入门/进阶书籍来看。这本书内容同样是理论结合实战,对于每个知识点的讲解也比较通俗易懂,整体结构也比较清。 - -#### 《深入浅出 Java 多线程》 - -![《深入浅出Java多线程》](https://imgkr.cn-bj.ufileos.com/7001a206-8ac0-432c-bf62-ca7130487c12.png) - -这本书是几位大厂(如阿里)的大佬开源的,Github 地址:[https://github.com/RedSpider1/concurrent](https://github.com/RedSpider1/concurrent) - -几位作者为了写好《深入浅出 Java 多线程》这本书阅读了大量的 Java 多线程方面的书籍和博客,然后再加上他们的经验总结、Demo 实例、源码解析,最终才形成了这本书。 - -这本书的质量也是非常过硬!给作者们点个赞!这本书有统一的排版规则和语言风格、清晰的表达方式和逻辑。并且每篇文章初稿写完后,作者们就会互相审校,合并到主分支时所有成员会再次审校,最后再通篇修订了三遍。 - -#### 《Java 并发编程的艺术》 - -![《Java 并发编程的艺术》](https://imgkr.cn-bj.ufileos.com/9ff63f79-f537-40df-a111-be5a11747b8f.png) - -这本书不是很适合作为 Java 多线程入门书籍,需要具备一定的 JVM 基础,有些东西讲的还是挺深入的。另外,就我自己阅读这本书的感觉来说,我觉得这本书的章节规划有点杂乱,但是,具体到某个知识点又很棒!这可能也和这本书由三名作者共同编写完成有关系吧! - -**综上:这本书并不是和 Java 多线程入门,你也不需要把这本书的每一章节都看一遍,建议挑选自己想要详细了解的知识点来看。** - -## 三.总结 - -在这篇文章中我主要总结了 Java 多线程方面的知识点,并且推荐了相关的书籍。并发这部分东西实战的话比较难,你可以尝试学会了某个知识点之后然后在自己写过的一些项目上实践。另外,leetcode 有一个练习多线程的类别: [https://leetcode-cn.com/problemset/concurrency](https://leetcode-cn.com/problemset/concurrency) 可以作为参考。 - -**为了这篇文章的内容更加完善,我还将本文的内容同步到了 Github 上,点击阅读原文即可直达。如果你觉得有任何需要完善和修改的地方,都可以去 Github 给我提交 Issue 或者 PR(推荐)。** diff --git "a/docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" "b/docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" deleted file mode 100644 index ed606342d2c..00000000000 --- "a/docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" +++ /dev/null @@ -1,229 +0,0 @@ -点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 - - - -- [一 JDK 提供的并发容器总结](#一-jdk-提供的并发容器总结 "一 JDK 提供的并发容器总结") -- [二 ConcurrentHashMap](#二-concurrenthashmap "二 ConcurrentHashMap") -- [三 CopyOnWriteArrayList](#三-copyonwritearraylist "三 CopyOnWriteArrayList") - - [3.1 CopyOnWriteArrayList 简介](#31-copyonwritearraylist-简介 "3.1 CopyOnWriteArrayList 简介") - - [3.2 CopyOnWriteArrayList 是如何做到的?](#32-copyonwritearraylist-是如何做到的? "3.2 CopyOnWriteArrayList 是如何做到的?") - - [3.3 CopyOnWriteArrayList 读取和写入源码简单分析](#33-copyonwritearraylist-读取和写入源码简单分析 "3.3 CopyOnWriteArrayList 读取和写入源码简单分析") - - [3.3.1 CopyOnWriteArrayList 读取操作的实现](#331-copyonwritearraylist-读取操作的实现 "3.3.1 CopyOnWriteArrayList 读取操作的实现") - - [3.3.2 CopyOnWriteArrayList 写入操作的实现](#332-copyonwritearraylist-写入操作的实现 "3.3.2 CopyOnWriteArrayList 写入操作的实现") -- [四 ConcurrentLinkedQueue](#四-concurrentlinkedqueue "四 ConcurrentLinkedQueue") -- [五 BlockingQueue](#五-blockingqueue "五 BlockingQueue") - - [5.1 BlockingQueue 简单介绍](#51-blockingqueue-简单介绍 "5.1 BlockingQueue 简单介绍") - - [5.2 ArrayBlockingQueue](#52-arrayblockingqueue "5.2 ArrayBlockingQueue") - - [5.3 LinkedBlockingQueue](#53-linkedblockingqueue "5.3 LinkedBlockingQueue") - - [5.4 PriorityBlockingQueue](#54-priorityblockingqueue "5.4 PriorityBlockingQueue") -- [六 ConcurrentSkipListMap](#六-concurrentskiplistmap "六 ConcurrentSkipListMap") -- [七 参考](#七-参考 "七 参考") - - - -## 一 JDK 提供的并发容器总结 - -JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 - -- **ConcurrentHashMap:** 线程安全的 HashMap -- **CopyOnWriteArrayList:** 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector. -- **ConcurrentLinkedQueue:** 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。 -- **BlockingQueue:** 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。 -- **ConcurrentSkipListMap:** 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。 - -## 二 ConcurrentHashMap - -我们知道 HashMap 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 `Collections.synchronizedMap()` 方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。 - -所以就有了 HashMap 的线程安全版本—— ConcurrentHashMap 的诞生。在 ConcurrentHashMap 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。 - -关于 ConcurrentHashMap 相关问题,我在 [Java 集合框架常见面试题](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md "Java集合框架常见面试题") 这篇文章中已经提到过。下面梳理一下关于 ConcurrentHashMap 比较重要的问题: - -- [ConcurrentHashMap 和 Hashtable 的区别](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md#concurrenthashmap-%E5%92%8C-hashtable-%E7%9A%84%E5%8C%BA%E5%88%AB "ConcurrentHashMap 和 Hashtable 的区别") -- [ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md#concurrenthashmap%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F%E5%BA%95%E5%B1%82%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0 "ConcurrentHashMap线程安全的具体实现方式/底层具体实现") - -## 三 CopyOnWriteArrayList - -### 3.1 CopyOnWriteArrayList 简介 - -```java -public class CopyOnWriteArrayList -extends Object -implements List, RandomAccess, Cloneable, Serializable -``` - -在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 List 的内部数据,毕竟读取操作是安全的。 - -这和我们之前在多线程章节讲过 `ReentrantReadWriteLock` 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 `CopyOnWriteArrayList` 类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,`CopyOnWriteArrayList` 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。**那它是怎么做的呢?** - -### 3.2 CopyOnWriteArrayList 是如何做到的? - -`CopyOnWriteArrayList` 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。 - -从 `CopyOnWriteArrayList` 的名字就能看出`CopyOnWriteArrayList` 是满足`CopyOnWrite` 的 ArrayList,所谓`CopyOnWrite` 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。 - -### 3.3 CopyOnWriteArrayList 读取和写入源码简单分析 - -#### 3.3.1 CopyOnWriteArrayList 读取操作的实现 - -读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。 - -```java - /** The array, accessed only via getArray/setArray. */ - private transient volatile Object[] array; - public E get(int index) { - return get(getArray(), index); - } - @SuppressWarnings("unchecked") - private E get(Object[] a, int index) { - return (E) a[index]; - } - final Object[] getArray() { - return array; - } - -``` - -#### 3.3.2 CopyOnWriteArrayList 写入操作的实现 - -CopyOnWriteArrayList 写入操作 add() 方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。 - -```java - /** - * Appends the specified element to the end of this list. - * - * @param e element to be appended to this list - * @return {@code true} (as specified by {@link Collection#add}) - */ - public boolean add(E e) { - final ReentrantLock lock = this.lock; - lock.lock();//加锁 - try { - Object[] elements = getArray(); - int len = elements.length; - Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组 - newElements[len] = e; - setArray(newElements); - return true; - } finally { - lock.unlock();//释放锁 - } - } -``` - -## 四 ConcurrentLinkedQueue - -Java 提供的线程安全的 Queue 可以分为**阻塞队列**和**非阻塞队列**,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 **阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。** - -从名字可以看出,`ConcurrentLinkedQueue`这个队列使用链表作为其数据结构.ConcurrentLinkedQueue 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。 - -ConcurrentLinkedQueue 内部代码我们就不分析了,大家知道 ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法来实现线程安全就好了。 - -ConcurrentLinkedQueue 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。 - -## 五 BlockingQueue - -### 5.1 BlockingQueue 简单介绍 - -上面我们己经提到了 ConcurrentLinkedQueue 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——BlockingQueue。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。 - -BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。下面是 BlockingQueue 的相关实现类: - -![BlockingQueue 的实现类](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/51622268.jpg) - -**下面主要介绍一下:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,这三个 BlockingQueue 的实现类。** - -### 5.2 ArrayBlockingQueue - -**ArrayBlockingQueue** 是 BlockingQueue 接口的有界队列实现类,底层采用**数组**来实现。ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。 - -ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码: - -```java -private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10,true); -``` - -### 5.3 LinkedBlockingQueue - -**LinkedBlockingQueue** 底层基于**单向链表**实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE。 - -**相关构造方法:** - -```java - /** - *某种意义上的无界队列 - * Creates a {@code LinkedBlockingQueue} with a capacity of - * {@link Integer#MAX_VALUE}. - */ - public LinkedBlockingQueue() { - this(Integer.MAX_VALUE); - } - - /** - *有界队列 - * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. - * - * @param capacity the capacity of this queue - * @throws IllegalArgumentException if {@code capacity} is not greater - * than zero - */ - public LinkedBlockingQueue(int capacity) { - if (capacity <= 0) throw new IllegalArgumentException(); - this.capacity = capacity; - last = head = new Node(null); - } -``` - -### 5.4 PriorityBlockingQueue - -**PriorityBlockingQueue** 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 `compareTo()` 方法来指定元素排序规则,或者初始化时通过构造器参数 `Comparator` 来指定排序规则。 - -PriorityBlockingQueue 并发控制采用的是 **ReentrantLock**,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,**如果空间不够的话会自动扩容**)。 - -简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。 - -**推荐文章:** - -《解读 Java 并发队列 BlockingQueue》 - -[https://javadoop.com/post/java-concurrent-queue](https://javadoop.com/post/java-concurrent-queue "https://javadoop.com/post/java-concurrent-queue") - -## 六 ConcurrentSkipListMap - -下面这部分内容参考了极客时间专栏[《数据结构与算法之美》](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster "《数据结构与算法之美》")以及《实战 Java 高并发程序设计》。 - -**为了引出 ConcurrentSkipListMap,先带着大家简单理解一下跳表。** - -对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 **O(logn)** 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。 - -跳表的本质是同时维护了多个链表,并且链表是分层的, - -![2级索引跳表](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/93666217.jpg) - -最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。 - -跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。 - -![在跳表中查找元素18](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/32005738.jpg) - -查找 18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。 - -从上面很容易看出,**跳表是一种利用空间换时间的算法。** - -使用跳表实现 Map 和使用哈希算法实现 Map 的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是 ConcurrentSkipListMap。 - -## 七 参考 - -- 《实战 Java 高并发程序设计》 -- https://javadoop.com/post/java-concurrent-queue -- https://juejin.im/post/5aeebd02518825672f19c546 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取! - -**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/What's New in JDK8/Java8foreach\346\214\207\345\215\227.md" "b/docs/java/What's New in JDK8/Java8foreach\346\214\207\345\215\227.md" deleted file mode 100644 index 48cda3abfd9..00000000000 --- "a/docs/java/What's New in JDK8/Java8foreach\346\214\207\345\215\227.md" +++ /dev/null @@ -1,139 +0,0 @@ -> 本文由 JavaGuide 翻译,原文地址:https://www.baeldung.com/foreach-java - -## 1 概述 - -在Java 8中引入的*forEach*循环为程序员提供了一种新的,简洁而有趣的迭代集合的方式。 - -在本文中,我们将看到如何将*forEach*与集合*一起*使用,它采用何种参数以及此循环与增强的*for*循环的不同之处。 - -## 2 基础知识 - -```Java -public interface Collection extends Iterable -``` - -Collection 接口实现了 Iterable 接口,而 Iterable 接口在 Java 8开始具有一个新的 API: - -```java -void forEach(Consumer action)//对 Iterable的每个元素执行给定的操作,直到所有元素都被处理或动作引发异常。 -``` - -使用*forEach*,我们可以迭代一个集合并对每个元素执行给定的操作,就像任何其他*迭代器一样。* - -例如,迭代和打印字符串集合*的*for循环版本: - -```java -for (String name : names) { - System.out.println(name); -} -``` - -我们可以使用*forEach*写这个 : - -```java -names.forEach(name -> { - System.out.println(name); -}); -``` - -## 3.使用forEach方法 - -### 3.1 匿名类 - -我们使用 *forEach*迭代集合并对每个元素执行特定操作。**要执行的操作包含在实现Consumer接口的类中,并作为参数传递给forEach 。** - -所述*消费者*接口是一个功能接口(具有单个抽象方法的接口)。它接受输入并且不返回任何结果。 - -Consumer 接口定义如下: - -```java -@FunctionalInterface -public interface Consumer { - void accept(T t); -} -``` -任何实现,例如,只是打印字符串的消费者: - -```java -Consumer printConsumer = new Consumer() { - public void accept(String name) { - System.out.println(name); - }; -}; -``` - -可以作为参数传递给*forEach*: - -```java -names.forEach(printConsumer); -``` - -但这不是通过消费者和使用*forEach* API 创建操作的唯一方法。让我们看看我们将使用*forEach*方法的另外2种最流行的方式: - -### 3.2 Lambda表达式 - -Java 8功能接口的主要优点是我们可以使用Lambda表达式来实例化它们,并避免使用庞大的匿名类实现。 - -由于 Consumer 接口属于函数式接口,我们可以通过以下形式在Lambda中表达它: - -```java -(argument) -> { body } -name -> System.out.println(name) -names.forEach(name -> System.out.println(name)); -``` - -### 3.3 方法参考 - -我们可以使用方法引用语法而不是普通的Lambda语法,其中已存在一个方法来对类执行操作: - -```java -names.forEach(System.out::println); -``` - -## 4.forEach在集合中的使用 - -### 4.1.迭代集合 - -**任何类型Collection的可迭代 - 列表,集合,队列 等都具有使用forEach的相同语法。** - -因此,正如我们已经看到的,迭代列表的元素: - -```java -List names = Arrays.asList("Larry", "Steve", "James"); - -names.forEach(System.out::println); -``` - -同样对于一组: - -```java -Set uniqueNames = new HashSet<>(Arrays.asList("Larry", "Steve", "James")); - -uniqueNames.forEach(System.out::println); -``` - -或者让我们说一个*队列*也是一个*集合*: - -```java -Queue namesQueue = new ArrayDeque<>(Arrays.asList("Larry", "Steve", "James")); - -namesQueue.forEach(System.out::println); -``` - -### 4.2.迭代Map - 使用Map的forEach - -Map没有实现Iterable接口,但它**提供了自己的forEach 变体,它接受BiConsumer**。* - -```java -Map namesMap = new HashMap<>(); -namesMap.put(1, "Larry"); -namesMap.put(2, "Steve"); -namesMap.put(3, "James"); -namesMap.forEach((key, value) -> System.out.println(key + " " + value)); -``` - -### 4.3.迭代一个Map - 通过迭代entrySet - -```java -namesMap.entrySet().forEach(entry -> System.out.println(entry.getKey() + " " + entry.getValue())); -``` \ No newline at end of file diff --git "a/docs/java/What's New in JDK8/Java8\346\225\231\347\250\213\346\216\250\350\215\220.md" "b/docs/java/What's New in JDK8/Java8\346\225\231\347\250\213\346\216\250\350\215\220.md" deleted file mode 100644 index 7de58352a5f..00000000000 --- "a/docs/java/What's New in JDK8/Java8\346\225\231\347\250\213\346\216\250\350\215\220.md" +++ /dev/null @@ -1,18 +0,0 @@ -### 书籍 - -- **《Java8 In Action》** -- **《写给大忙人看的Java SE 8》** - -上述书籍的PDF版本见 https://shimo.im/docs/CPB0PK05rP4CFmI2/ 中的 “Java 书籍推荐”。 - -### 开源文档 - -- **【译】Java 8 简明教程**: -- **30 seconds of java8:** - -### 视频 - -- **尚硅谷 Java 8 新特性** - -视频资源见: https://shimo.im/docs/CPB0PK05rP4CFmI2/ 。 - diff --git a/docs/java/basic/Arrays,CollectionsCommonMethods.md b/docs/java/basic/Arrays,CollectionsCommonMethods.md deleted file mode 100644 index 0710de44a95..00000000000 --- a/docs/java/basic/Arrays,CollectionsCommonMethods.md +++ /dev/null @@ -1,383 +0,0 @@ - - -- [Collections 工具类和 Arrays 工具类常见方法](#collections-工具类和-arrays-工具类常见方法) - - [Collections](#collections) - - [排序操作](#排序操作) - - [查找,替换操作](#查找替换操作) - - [同步控制](#同步控制) - - [Arrays类的常见操作](#arrays类的常见操作) - - [排序 : `sort()`](#排序--sort) - - [查找 : `binarySearch()`](#查找--binarysearch) - - [比较: `equals()`](#比较-equals) - - [填充 : `fill()`](#填充--fill) - - [转列表 `asList()`](#转列表-aslist) - - [转字符串 `toString()`](#转字符串-tostring) - - [复制 `copyOf()`](#复制-copyof) - - -# Collections 工具类和 Arrays 工具类常见方法 - -## Collections - -Collections 工具类常用方法: - -1. 排序 -2. 查找,替换操作 -3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合) - -### 排序操作 - -```java -void reverse(List list)//反转 -void shuffle(List list)//随机排序 -void sort(List list)//按自然排序的升序排序 -void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑 -void swap(List list, int i , int j)//交换两个索引位置的元素 -void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - System.out.println("原始数组:"); - System.out.println(arrayList); - // void reverse(List list):反转 - Collections.reverse(arrayList); - System.out.println("Collections.reverse(arrayList):"); - System.out.println(arrayList); - - - Collections.rotate(arrayList, 4); - System.out.println("Collections.rotate(arrayList, 4):"); - System.out.println(arrayList); - - // void sort(List list),按自然排序的升序排序 - Collections.sort(arrayList); - System.out.println("Collections.sort(arrayList):"); - System.out.println(arrayList); - - // void shuffle(List list),随机排序 - Collections.shuffle(arrayList); - System.out.println("Collections.shuffle(arrayList):"); - System.out.println(arrayList); - - // void swap(List list, int i , int j),交换两个索引位置的元素 - Collections.swap(arrayList, 2, 5); - System.out.println("Collections.swap(arrayList, 2, 5):"); - System.out.println(arrayList); - - // 定制排序的用法 - Collections.sort(arrayList, new Comparator() { - - @Override - public int compare(Integer o1, Integer o2) { - return o2.compareTo(o1); - } - }); - System.out.println("定制排序后:"); - System.out.println(arrayList); -``` - -### 查找,替换操作 - -```java -int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的 -int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll) -int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c) -void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。 -int frequency(Collection c, Object o)//统计元素出现次数 -int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target). -boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - ArrayList arrayList2 = new ArrayList(); - arrayList2.add(-3); - arrayList2.add(-5); - arrayList2.add(7); - System.out.println("原始数组:"); - System.out.println(arrayList); - - System.out.println("Collections.max(arrayList):"); - System.out.println(Collections.max(arrayList)); - - System.out.println("Collections.min(arrayList):"); - System.out.println(Collections.min(arrayList)); - - System.out.println("Collections.replaceAll(arrayList, 3, -3):"); - Collections.replaceAll(arrayList, 3, -3); - System.out.println(arrayList); - - System.out.println("Collections.frequency(arrayList, -3):"); - System.out.println(Collections.frequency(arrayList, -3)); - - System.out.println("Collections.indexOfSubList(arrayList, arrayList2):"); - System.out.println(Collections.indexOfSubList(arrayList, arrayList2)); - - System.out.println("Collections.binarySearch(arrayList, 7):"); - // 对List进行二分查找,返回索引,List必须是有序的 - Collections.sort(arrayList); - System.out.println(Collections.binarySearch(arrayList, 7)); -``` - -### 同步控制 - -Collections提供了多个`synchronizedXxx()`方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。 - -我们知道 HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。Collections提供了多个静态方法可以把他们包装成线程同步的集合。 - -**最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。** - -方法如下: - -```java -synchronizedCollection(Collection c) //返回指定 collection 支持的同步(线程安全的)collection。 -synchronizedList(List list)//返回指定列表支持的同步(线程安全的)List。 -synchronizedMap(Map m) //返回由指定映射支持的同步(线程安全的)Map。 -synchronizedSet(Set s) //返回指定 set 支持的同步(线程安全的)set。 -``` - -### Collections还可以设置不可变集合,提供了如下三类方法: - -```java -emptyXxx(): 返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是Set,还可以是Map。 -singletonXxx(): 返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是:List,Set,Map。 -unmodifiableXxx(): 返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map。 -上面三类方法的参数是原有的集合对象,返回值是该集合的”只读“版本。 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - HashSet integers1 = new HashSet<>(); - integers1.add(1); - integers1.add(3); - integers1.add(2); - Map scores = new HashMap(); - scores.put("语文" , 80); - scores.put("Java" , 82); - - //Collections.emptyXXX();创建一个空的、不可改变的XXX对象 - List list = Collections.emptyList(); - System.out.println(list);//[] - Set objects = Collections.emptySet(); - System.out.println(objects);//[] - Map objectObjectMap = Collections.emptyMap(); - System.out.println(objectObjectMap);//{} - - //Collections.singletonXXX(); - List> arrayLists = Collections.singletonList(arrayList); - System.out.println(arrayLists);//[[-1, 3, 3, -5, 7, 4, -9, -7]] - //创建一个只有一个元素,且不可改变的Set对象 - Set> singleton = Collections.singleton(arrayList); - System.out.println(singleton);//[[-1, 3, 3, -5, 7, 4, -9, -7]] - Map nihao = Collections.singletonMap("1", "nihao"); - System.out.println(nihao);//{1=nihao} - - //unmodifiableXXX();创建普通XXX对象对应的不可变版本 - List integers = Collections.unmodifiableList(arrayList); - System.out.println(integers);//[-1, 3, 3, -5, 7, 4, -9, -7] - Set integers2 = Collections.unmodifiableSet(integers1); - System.out.println(integers2);//[1, 2, 3] - Map objectObjectMap2 = Collections.unmodifiableMap(scores); - System.out.println(objectObjectMap2);//{Java=82, 语文=80} - - //添加出现异常:java.lang.UnsupportedOperationException -// list.add(1); -// arrayLists.add(arrayList); -// integers.add(1); -``` - -## Arrays类的常见操作 -1. 排序 : `sort()` -2. 查找 : `binarySearch()` -3. 比较: `equals()` -4. 填充 : `fill()` -5. 转列表: `asList()` -6. 转字符串 : `toString()` -7. 复制: `copyOf()` - - -### 排序 : `sort()` - -```java - // *************排序 sort**************** - int a[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - // sort(int[] a)方法按照数字顺序排列指定的数组。 - Arrays.sort(a); - System.out.println("Arrays.sort(a):"); - for (int i : a) { - System.out.print(i); - } - // 换行 - System.out.println(); - - // sort(int[] a,int fromIndex,int toIndex)按升序排列数组的指定范围 - int b[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - Arrays.sort(b, 2, 6); - System.out.println("Arrays.sort(b, 2, 6):"); - for (int i : b) { - System.out.print(i); - } - // 换行 - System.out.println(); - - int c[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - // parallelSort(int[] a) 按照数字顺序排列指定的数组(并行的)。同sort方法一样也有按范围的排序 - Arrays.parallelSort(c); - System.out.println("Arrays.parallelSort(c):"); - for (int i : c) { - System.out.print(i); - } - // 换行 - System.out.println(); - - // parallelSort给字符数组排序,sort也可以 - char d[] = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - Arrays.parallelSort(d); - System.out.println("Arrays.parallelSort(d):"); - for (char d2 : d) { - System.out.print(d2); - } - // 换行 - System.out.println(); - -``` - -在做算法面试题的时候,我们还可能会经常遇到对字符串排序的情况,`Arrays.sort()` 对每个字符串的特定位置进行比较,然后按照升序排序。 - -```java -String[] strs = { "abcdehg", "abcdefg", "abcdeag" }; -Arrays.sort(strs); -System.out.println(Arrays.toString(strs));//[abcdeag, abcdefg, abcdehg] -``` - -### 查找 : `binarySearch()` - -```java - // *************查找 binarySearch()**************** - char[] e = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - // 排序后再进行二分查找,否则找不到 - Arrays.sort(e); - System.out.println("Arrays.sort(e)" + Arrays.toString(e)); - System.out.println("Arrays.binarySearch(e, 'c'):"); - int s = Arrays.binarySearch(e, 'c'); - System.out.println("字符c在数组的位置:" + s); -``` - -### 比较: `equals()` - -```java - // *************比较 equals**************** - char[] e = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - char[] f = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - /* - * 元素数量相同,并且相同位置的元素相同。 另外,如果两个数组引用都是null,则它们被认为是相等的 。 - */ - // 输出true - System.out.println("Arrays.equals(e, f):" + Arrays.equals(e, f)); -``` - -### 填充 : `fill()` - -```java - // *************填充fill(批量初始化)**************** - int[] g = { 1, 2, 3, 3, 3, 3, 6, 6, 6 }; - // 数组中所有元素重新分配值 - Arrays.fill(g, 3); - System.out.println("Arrays.fill(g, 3):"); - // 输出结果:333333333 - for (int i : g) { - System.out.print(i); - } - // 换行 - System.out.println(); - - int[] h = { 1, 2, 3, 3, 3, 3, 6, 6, 6, }; - // 数组中指定范围元素重新分配值 - Arrays.fill(h, 0, 2, 9); - System.out.println("Arrays.fill(h, 0, 2, 9);:"); - // 输出结果:993333666 - for (int i : h) { - System.out.print(i); - } -``` - -### 转列表 `asList()` - -```java - // *************转列表 asList()**************** - /* - * 返回由指定数组支持的固定大小的列表。 - * (将返回的列表更改为“写入数组”。)该方法作为基于数组和基于集合的API之间的桥梁,与Collection.toArray()相结合 。 - * 返回的列表是可序列化的,并实现RandomAccess 。 - * 此方法还提供了一种方便的方式来创建一个初始化为包含几个元素的固定大小的列表如下: - */ - List stooges = Arrays.asList("Larry", "Moe", "Curly"); - System.out.println(stooges); -``` - -### 转字符串 `toString()` - -```java - // *************转字符串 toString()**************** - /* - * 返回指定数组的内容的字符串表示形式。 - */ - char[] k = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - System.out.println(Arrays.toString(k));// [a, f, b, c, e, A, C, B] -``` - -### 复制 `copyOf()` - -```java - // *************复制 copy**************** - // copyOf 方法实现数组复制,h为数组,6为复制的长度 - int[] h = { 1, 2, 3, 3, 3, 3, 6, 6, 6, }; - int i[] = Arrays.copyOf(h, 6); - System.out.println("Arrays.copyOf(h, 6);:"); - // 输出结果:123333 - for (int j : i) { - System.out.print(j); - } - // 换行 - System.out.println(); - // copyOfRange将指定数组的指定范围复制到新数组中 - int j[] = Arrays.copyOfRange(h, 6, 11); - System.out.println("Arrays.copyOfRange(h, 6, 11):"); - // 输出结果66600(h数组只有9个元素这里是从索引6到索引11复制所以不足的就为0) - for (int j2 : j) { - System.out.print(j2); - } - // 换行 - System.out.println(); -``` diff --git a/docs/java/basic/final,static,this,super.md b/docs/java/basic/final,static,this,super.md deleted file mode 100644 index e93e3975961..00000000000 --- a/docs/java/basic/final,static,this,super.md +++ /dev/null @@ -1,350 +0,0 @@ - - -- [final,static,this,super 关键字总结](#finalstaticthissuper-关键字总结) - - [final 关键字](#final-关键字) - - [static 关键字](#static-关键字) - - [this 关键字](#this-关键字) - - [super 关键字](#super-关键字) - - [参考](#参考) -- [static 关键字详解](#static-关键字详解) - - [static 关键字主要有以下四种使用场景](#static-关键字主要有以下四种使用场景) - - [修饰成员变量和成员方法\(常用\)](#修饰成员变量和成员方法常用) - - [静态代码块](#静态代码块) - - [静态内部类](#静态内部类) - - [静态导包](#静态导包) - - [补充内容](#补充内容) - - [静态方法与非静态方法](#静态方法与非静态方法) - - [static{}静态代码块与{}非静态代码块\(构造代码块\)](#static静态代码块与非静态代码块构造代码块) - - [参考](#参考-1) - - - -# final,static,this,super 关键字总结 - -## final 关键字 - -**final关键字主要用在三个地方:变量、方法、类。** - -1. **对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。** - -2. **当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。** - -3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 - -## static 关键字 - -**static 关键字主要有以下四种使用场景:** - -1. **修饰成员变量和成员方法:** 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:`类名.静态变量名` `类名.静态方法名()` -2. **静态代码块:** 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次. -3. **静态内部类(static修饰类的话只能修饰内部类):** 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。 -4. **静态导包(用来导入类中的静态资源,1.5之后的新特性):** 格式为:`import static` 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。 - -## this 关键字 - -this关键字用于引用类的当前实例。 例如: - -```java -class Manager { - Employees[] employees; - - void manageEmployees() { - int totalEmp = this.employees.length; - System.out.println("Total employees: " + totalEmp); - this.report(); - } - - void report() { } -} -``` - -在上面的示例中,this关键字用于两个地方: - -- this.employees.length:访问类Manager的当前实例的变量。 -- this.report():调用类Manager的当前实例的方法。 - -此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。 - -## super 关键字 - -super关键字用于从子类访问父类的变量和方法。 例如: - -```java -public class Super { - protected int number; - - protected showNumber() { - System.out.println("number = " + number); - } -} - -public class Sub extends Super { - void bar() { - super.number = 10; - super.showNumber(); - } -} -``` - -在上面的例子中,Sub 类访问父类成员变量 number 并调用其其父类 Super 的 `showNumber()` 方法。 - -**使用 this 和 super 要注意的问题:** - -- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 -- this、super不能用在static方法中。 - -**简单解释一下:** - -被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, **this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西**。 - -## 参考 - -- https://www.codejava.net/java-core/the-java-language/java-keywords -- https://blog.csdn.net/u013393958/article/details/79881037 - -# static 关键字详解 - -## static 关键字主要有以下四种使用场景 - -1. 修饰成员变量和成员方法 -2. 静态代码块 -3. 修饰类(只能修饰内部类) -4. 静态导包(用来导入类中的静态资源,1.5之后的新特性) - -### 修饰成员变量和成员方法(常用) - -被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。 - -方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。 - - HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。 - -调用格式: - -- `类名.静态变量名` -- `类名.静态方法名()` - -如果变量或者方法被 private 则代表该属性或者该方法只能在类的内部被访问而不能在类的外部被访问。 - -测试方法: - -```java -public class StaticBean { - - String name; - //静态变量 - static int age; - - public StaticBean(String name) { - this.name = name; - } - //静态方法 - static void SayHello() { - System.out.println("Hello i am java"); - } - @Override - public String toString() { - return "StaticBean{"+ - "name=" + name + ",age=" + age + - "}"; - } -} -``` - -```java -public class StaticDemo { - - public static void main(String[] args) { - StaticBean staticBean = new StaticBean("1"); - StaticBean staticBean2 = new StaticBean("2"); - StaticBean staticBean3 = new StaticBean("3"); - StaticBean staticBean4 = new StaticBean("4"); - StaticBean.age = 33; - System.out.println(staticBean + " " + staticBean2 + " " + staticBean3 + " " + staticBean4); - //StaticBean{name=1,age=33} StaticBean{name=2,age=33} StaticBean{name=3,age=33} StaticBean{name=4,age=33} - StaticBean.SayHello();//Hello i am java - } - -} -``` - - -### 静态代码块 - -静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次. - -静态代码块的格式是 - -``` -static { -语句体; -} -``` - - -一个类中的静态代码块可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。 - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-14/88531075.jpg) - -静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问. - - -### 静态内部类 - -静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着: - -1. 它的创建是不需要依赖外围类的创建。 -2. 它不能使用任何外围类的非static成员变量和方法。 - - -Example(静态内部类实现单例模式) - -```java -public class Singleton { - - //声明为 private 避免调用默认构造方法创建对象 - private Singleton() { - } - - // 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问 - private static class SingletonHolder { - private static final Singleton INSTANCE = new Singleton(); - } - - public static Singleton getUniqueInstance() { - return SingletonHolder.INSTANCE; - } -} -``` - -当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 `getUniqueInstance() `方法从而触发 `SingletonHolder.INSTANCE` 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。 - -这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。 - -### 静态导包 - -格式为:import static - -这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法 - -```java - - - //将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用 - //如果只想导入单一某个静态方法,只需要将换成对应的方法名即可 - -import static java.lang.Math.*;//换成import static java.lang.Math.max;具有一样的效果 - -public class Demo { - public static void main(String[] args) { - - int max = max(1,2); - System.out.println(max); - } -} - -``` - - -## 补充内容 - -### 静态方法与非静态方法 - -静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。 - -Example - -```java -class Foo { - int i; - public Foo(int i) { - this.i = i; - } - - public static String method1() { - return "An example string that doesn't depend on i (an instance variable)"; - - } - - public int method2() { - return this.i + 1; //Depends on i - } - -} -``` -你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行:`Foo bar = new Foo(1);bar.method2();` - -总结: - -- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 -- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 - -### `static{}`静态代码块与`{}`非静态代码块(构造代码块) - -相同点: 都是在JVM加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些static变量进行赋值。 - -不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。 - -> 修正 [issue #677](https://github.com/Snailclimb/JavaGuide/issues/677):静态代码块可能在第一次new的时候执行,但不一定只在第一次new的时候执行。比如通过 `Class.forName("ClassDemo")`创建 Class 对象的时候也会执行。 - -一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:Arrays类,Character类,String类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的. - -Example: - -```java -public class Test { - public Test() { - System.out.print("默认构造方法!--"); - } - - //非静态代码块 - { - System.out.print("非静态代码块!--"); - } - - //静态代码块 - static { - System.out.print("静态代码块!--"); - } - - private static void test() { - System.out.print("静态方法中的内容! --"); - { - System.out.print("静态方法中的代码块!--"); - } - - } - - public static void main(String[] args) { - Test test = new Test(); - Test.test();//静态代码块!--静态方法中的内容! --静态方法中的代码块!-- - } -} -``` - -上述代码输出: - -``` -静态代码块!--非静态代码块!--默认构造方法!--静态方法中的内容! --静态方法中的代码块!-- -``` - -当只执行 `Test.test();` 时输出: - -``` -静态代码块!--静态方法中的内容! --静态方法中的代码块!-- -``` - -当只执行 `Test test = new Test();` 时输出: - -``` -静态代码块!--非静态代码块!--默认构造方法!-- -``` - - -非静态代码块与构造函数的区别是: 非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。 - -### 参考 - -- httpsblog.csdn.netchen13579867831articledetails78995480 -- httpwww.cnblogs.comchenssyp3388487.html -- httpwww.cnblogs.comQian123p5713440.html diff --git a/docs/java/basic/reflection.md b/docs/java/basic/reflection.md deleted file mode 100644 index 6c8c6b8caeb..00000000000 --- a/docs/java/basic/reflection.md +++ /dev/null @@ -1,136 +0,0 @@ -### 反射机制介绍 - -JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。 - -### 获取 Class 对象的两种方式 - -如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了两种方式获取 Class 对象: - -1.知道具体类的情况下可以使用: - -```java -Class alunbarClass = TargetObject.class; -``` - -但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象 - -2.通过 `Class.forName()`传入类的路径获取: - -```java -Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject"); -``` - -### 代码实例 - -**简单用代码演示一下反射的一些操作!** - -1.创建一个我们要使用反射操作的类 `TargetObject`: - -```java -package cn.javaguide; - -public class TargetObject { - private String value; - - public TargetObject() { - value = "JavaGuide"; - } - - public void publicMethod(String s) { - System.out.println("I love " + s); - } - - private void privateMethod() { - System.out.println("value is " + value); - } -} -``` - -2.使用反射操作这个类的方法以及参数 - -```java -package cn.javaguide; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -public class Main { - public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException { - /** - * 获取TargetObject类的Class对象并且创建TargetObject类实例 - */ - Class tagetClass = Class.forName("cn.javaguide.TargetObject"); - TargetObject targetObject = (TargetObject) tagetClass.newInstance(); - /** - * 获取所有类中所有定义的方法 - */ - Method[] methods = tagetClass.getDeclaredMethods(); - for (Method method : methods) { - System.out.println(method.getName()); - } - /** - * 获取指定方法并调用 - */ - Method publicMethod = tagetClass.getDeclaredMethod("publicMethod", - String.class); - - publicMethod.invoke(targetObject, "JavaGuide"); - /** - * 获取指定参数并对参数进行修改 - */ - Field field = tagetClass.getDeclaredField("value"); - //为了对类中的参数进行修改我们取消安全检查 - field.setAccessible(true); - field.set(targetObject, "JavaGuide"); - /** - * 调用 private 方法 - */ - Method privateMethod = tagetClass.getDeclaredMethod("privateMethod"); - //为了调用private方法我们取消安全检查 - privateMethod.setAccessible(true); - privateMethod.invoke(targetObject); - } -} - -``` - -输出内容: - -``` -publicMethod -privateMethod -I love JavaGuide -value is JavaGuide -``` - -### 静态编译和动态编译 - -- **静态编译:**在编译时确定类型,绑定对象 -- **动态编译:**运行时确定类型,绑定对象 - -### 反射机制优缺点 - -- **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。 -- **缺点:** 1,性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。2,安全问题,让我们可以动态操作改变类的属性同时也增加了类的安全隐患。 - -### 反射的应用场景 - -**反射是框架设计的灵魂。** - -在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。 - -举例: - -1. 我们在使用 JDBC 连接数据库时使用 `Class.forName()`通过反射加载数据库的驱动程序; -2. Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系; -3. 动态配置实例的属性; -4. ...... - -**推荐阅读:** - -- [Java反射使用总结]( https://zhuanlan.zhihu.com/p/80519709) -- [Reflection:Java 反射机制的应用场景](https://segmentfault.com/a/1190000010162647?utm_source=tuicool&utm_medium=referral) -- [Java 基础之—反射(非常重要)](https://blog.csdn.net/sinat_38259539/article/details/71799078) - -## \ No newline at end of file diff --git "a/docs/java/basic/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225.md" "b/docs/java/basic/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225.md" deleted file mode 100644 index 20b02297758..00000000000 --- "a/docs/java/basic/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225.md" +++ /dev/null @@ -1,561 +0,0 @@ -> 最近重看 Java 枚举,看到这篇觉得还不错的文章,于是简单翻译和完善了一些内容,分享给大家,希望你们也能有所收获。另外,不要忘了文末还有补充哦! -> -> ps: 这里发一篇枚举的文章,也是因为后面要发一篇非常实用的关于 SpringBoot 全局异常处理的比较好的实践,里面就用到了枚举。 -> -> 这篇文章由 JavaGuide 翻译,公众号: JavaGuide,原文地址:https://www.baeldung.com/a-guide-to-java-enums 。 -> -> 转载请注明上面这段文字。 - -## 1.概览 - -在本文中,我们将看到什么是 Java 枚举,它们解决了哪些问题以及如何在实践中使用 Java 枚举实现一些设计模式。 - -enum关键字在 java5 中引入,表示一种特殊类型的类,其总是继承java.lang.Enum类,更多内容可以自行查看其[官方文档](https://docs.oracle.com/javase/6/docs/api/java/lang/Enum.html)。 - -枚举在很多时候会和常量拿来对比,可能因为本身我们大量实际使用枚举的地方就是为了替代常量。那么这种方式由什么优势呢? - -**以这种方式定义的常量使代码更具可读性,允许进行编译时检查,预先记录可接受值的列表,并避免由于传入无效值而引起的意外行为。** - -下面示例定义一个简单的枚举类型 pizza 订单的状态,共有三种 ORDERED, READY, DELIVERED状态: - -```java -package shuang.kou.enumdemo.enumtest; - -public enum PizzaStatus { - ORDERED, - READY, - DELIVERED; -} -``` - -**简单来说,我们通过上面的代码避免了定义常量,我们将所有和 pizza 订单的状态的常量都统一放到了一个枚举类型里面。** - -```java -System.out.println(PizzaStatus.ORDERED.name());//ORDERED -System.out.println(PizzaStatus.ORDERED);//ORDERED -System.out.println(PizzaStatus.ORDERED.name().getClass());//class java.lang.String -System.out.println(PizzaStatus.ORDERED.getClass());//class shuang.kou.enumdemo.enumtest.PizzaStatus -``` - -## 2.自定义枚举方法 - -现在我们对枚举是什么以及如何使用它们有了基本的了解,让我们通过在枚举上定义一些额外的API方法,将上一个示例提升到一个新的水平: - -```java -public class Pizza { - private PizzaStatus status; - public enum PizzaStatus { - ORDERED, - READY, - DELIVERED; - } - - public boolean isDeliverable() { - if (getStatus() == PizzaStatus.READY) { - return true; - } - return false; - } - - // Methods that set and get the status variable. -} -``` - -## 3.使用 == 比较枚举类型 - -由于枚举类型确保JVM中仅存在一个常量实例,因此我们可以安全地使用“ ==”运算符比较两个变量,如上例所示;此外,“ ==”运算符可提供编译时和运行时的安全性。 - -首先,让我们看一下以下代码段中的运行时安全性,其中“ ==”运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException: - -```java -if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); -if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED); -``` - -对于编译时安全性,我们看另一个示例,两个不同枚举类型进行比较,使用equal方法比较结果确定为true,因为getStatus方法的枚举值与另一个类型枚举值一致,但逻辑上应该为false。这个问题可以使用==操作符避免。因为编译器会表示类型不兼容错误: - -```java -if(testPz.getStatus().equals(TestColor.GREEN)); -if(testPz.getStatus() == TestColor.GREEN); -``` - -## 4.在 switch 语句中使用枚举类型 - -```java -public int getDeliveryTimeInDays() { - switch (status) { - case ORDERED: return 5; - case READY: return 2; - case DELIVERED: return 0; - } - return 0; -} -``` - -## 5.枚举类型的属性,方法和构造函数 - -> 文末有我(JavaGuide)的补充。 - -你可以通过在枚举类型中定义属性,方法和构造函数让它变得更加强大。 - -下面,让我们扩展上面的示例,实现从比萨的一个阶段到另一个阶段的过渡,并了解如何摆脱之前使用的if语句和switch语句: - -```java -public class Pizza { - - private PizzaStatus status; - public enum PizzaStatus { - ORDERED (5){ - @Override - public boolean isOrdered() { - return true; - } - }, - READY (2){ - @Override - public boolean isReady() { - return true; - } - }, - DELIVERED (0){ - @Override - public boolean isDelivered() { - return true; - } - }; - - private int timeToDelivery; - - public boolean isOrdered() {return false;} - - public boolean isReady() {return false;} - - public boolean isDelivered(){return false;} - - public int getTimeToDelivery() { - return timeToDelivery; - } - - PizzaStatus (int timeToDelivery) { - this.timeToDelivery = timeToDelivery; - } - } - - public boolean isDeliverable() { - return this.status.isReady(); - } - - public void printTimeToDeliver() { - System.out.println("Time to delivery is " + - this.getStatus().getTimeToDelivery()); - } - - // Methods that set and get the status variable. -} -``` - -下面这段代码展示它是如何 work 的: - -```java -@Test -public void givenPizaOrder_whenReady_thenDeliverable() { - Pizza testPz = new Pizza(); - testPz.setStatus(Pizza.PizzaStatus.READY); - assertTrue(testPz.isDeliverable()); -} -``` - -## 6.EnumSet and EnumMap - -### 6.1. EnumSet - -`EnumSet` 是一种专门为枚举类型所设计的 `Set` 类型。 - -与`HashSet`相比,由于使用了内部位向量表示,因此它是特定 `Enum` 常量集的非常有效且紧凑的表示形式。 - -它提供了类型安全的替代方法,以替代传统的基于int的“位标志”,使我们能够编写更易读和易于维护的简洁代码。 - -`EnumSet` 是抽象类,其有两个实现:`RegularEnumSet` 、`JumboEnumSet`,选择哪一个取决于实例化时枚举中常量的数量。 - -在很多场景中的枚举常量集合操作(如:取子集、增加、删除、`containsAll`和`removeAll`批操作)使用`EnumSet`非常合适;如果需要迭代所有可能的常量则使用`Enum.values()`。 - -```java -public class Pizza { - - private static EnumSet undeliveredPizzaStatuses = - EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY); - - private PizzaStatus status; - - public enum PizzaStatus { - ... - } - - public boolean isDeliverable() { - return this.status.isReady(); - } - - public void printTimeToDeliver() { - System.out.println("Time to delivery is " + - this.getStatus().getTimeToDelivery() + " days"); - } - - public static List getAllUndeliveredPizzas(List input) { - return input.stream().filter( - (s) -> undeliveredPizzaStatuses.contains(s.getStatus())) - .collect(Collectors.toList()); - } - - public void deliver() { - if (isDeliverable()) { - PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy() - .deliver(this); - this.setStatus(PizzaStatus.DELIVERED); - } - } - - // Methods that set and get the status variable. -} -``` - - 下面的测试演示了展示了 `EnumSet` 在某些场景下的强大功能: - -```java -@Test -public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() { - List pzList = new ArrayList<>(); - Pizza pz1 = new Pizza(); - pz1.setStatus(Pizza.PizzaStatus.DELIVERED); - - Pizza pz2 = new Pizza(); - pz2.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz3 = new Pizza(); - pz3.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz4 = new Pizza(); - pz4.setStatus(Pizza.PizzaStatus.READY); - - pzList.add(pz1); - pzList.add(pz2); - pzList.add(pz3); - pzList.add(pz4); - - List undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList); - assertTrue(undeliveredPzs.size() == 3); -} -``` - -### 6.2. EnumMap - -`EnumMap`是一个专门化的映射实现,用于将枚举常量用作键。与对应的 `HashMap` 相比,它是一个高效紧凑的实现,并且在内部表示为一个数组: - -```java -EnumMap map; -``` - -让我们快速看一个真实的示例,该示例演示如何在实践中使用它: - -```java -public static EnumMap> - groupPizzaByStatus(List pizzaList) { - EnumMap> pzByStatus = - new EnumMap>(PizzaStatus.class); - - for (Pizza pz : pizzaList) { - PizzaStatus status = pz.getStatus(); - if (pzByStatus.containsKey(status)) { - pzByStatus.get(status).add(pz); - } else { - List newPzList = new ArrayList(); - newPzList.add(pz); - pzByStatus.put(status, newPzList); - } - } - return pzByStatus; -} -``` - - 下面的测试演示了展示了 `EnumMap` 在某些场景下的强大功能: - -```java -@Test -public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() { - List pzList = new ArrayList<>(); - Pizza pz1 = new Pizza(); - pz1.setStatus(Pizza.PizzaStatus.DELIVERED); - - Pizza pz2 = new Pizza(); - pz2.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz3 = new Pizza(); - pz3.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz4 = new Pizza(); - pz4.setStatus(Pizza.PizzaStatus.READY); - - pzList.add(pz1); - pzList.add(pz2); - pzList.add(pz3); - pzList.add(pz4); - - EnumMap> map = Pizza.groupPizzaByStatus(pzList); - assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1); - assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2); - assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1); -} -``` - -## 7. 通过枚举实现一些设计模式 - -### 7.1 单例模式 - -通常,使用类实现 Singleton 模式并非易事,枚举提供了一种实现单例的简便方法。 - -《Effective Java 》和《Java与模式》都非常推荐这种方式,使用这种方式方式实现枚举可以有什么好处呢? - -《Effective Java》 - -> 这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。 —-《Effective Java 中文版 第二版》 - -《Java与模式》 - -> 《Java与模式》中,作者这样写道,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。 - -下面的代码段显示了如何使用枚举实现单例模式: - -```java -public enum PizzaDeliverySystemConfiguration { - INSTANCE; - PizzaDeliverySystemConfiguration() { - // Initialization configuration which involves - // overriding defaults like delivery strategy - } - - private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL; - - public static PizzaDeliverySystemConfiguration getInstance() { - return INSTANCE; - } - - public PizzaDeliveryStrategy getDeliveryStrategy() { - return deliveryStrategy; - } -} -``` - -如何使用呢?请看下面的代码: - -```java -PizzaDeliveryStrategy deliveryStrategy = PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy(); -``` - -通过 `PizzaDeliverySystemConfiguration.getInstance()` 获取的就是单例的 `PizzaDeliverySystemConfiguration` - -### 7.2 策略模式 - -通常,策略模式由不同类实现同一个接口来实现的。 - - 这也就意味着添加新策略意味着添加新的实现类。使用枚举,可以轻松完成此任务,添加新的实现意味着只定义具有某个实现的另一个实例。 - -下面的代码段显示了如何使用枚举实现策略模式: - -```java -public enum PizzaDeliveryStrategy { - EXPRESS { - @Override - public void deliver(Pizza pz) { - System.out.println("Pizza will be delivered in express mode"); - } - }, - NORMAL { - @Override - public void deliver(Pizza pz) { - System.out.println("Pizza will be delivered in normal mode"); - } - }; - - public abstract void deliver(Pizza pz); -} -``` - -给 `Pizza `增加下面的方法: - -```java -public void deliver() { - if (isDeliverable()) { - PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy() - .deliver(this); - this.setStatus(PizzaStatus.DELIVERED); - } -} -``` - -如何使用呢?请看下面的代码: - -```java -@Test -public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() { - Pizza pz = new Pizza(); - pz.setStatus(Pizza.PizzaStatus.READY); - pz.deliver(); - assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED); -} -``` - -## 8. Java 8 与枚举 - -Pizza 类可以用Java 8重写,您可以看到方法 lambda 和Stream API如何使 `getAllUndeliveredPizzas()`和`groupPizzaByStatus()`方法变得如此简洁: - -`getAllUndeliveredPizzas()`: - -```java -public static List getAllUndeliveredPizzas(List input) { - return input.stream().filter( - (s) -> !deliveredPizzaStatuses.contains(s.getStatus())) - .collect(Collectors.toList()); -} -``` - -`groupPizzaByStatus()` : - -```java -public static EnumMap> - groupPizzaByStatus(List pzList) { - EnumMap> map = pzList.stream().collect( - Collectors.groupingBy(Pizza::getStatus, - () -> new EnumMap<>(PizzaStatus.class), Collectors.toList())); - return map; -} -``` - -## 9. Enum 类型的 JSON 表现形式 - -使用Jackson库,可以将枚举类型的JSON表示为POJO。下面的代码段显示了可以用于同一目的的Jackson批注: - -```java -@JsonFormat(shape = JsonFormat.Shape.OBJECT) -public enum PizzaStatus { - ORDERED (5){ - @Override - public boolean isOrdered() { - return true; - } - }, - READY (2){ - @Override - public boolean isReady() { - return true; - } - }, - DELIVERED (0){ - @Override - public boolean isDelivered() { - return true; - } - }; - - private int timeToDelivery; - - public boolean isOrdered() {return false;} - - public boolean isReady() {return false;} - - public boolean isDelivered(){return false;} - - @JsonProperty("timeToDelivery") - public int getTimeToDelivery() { - return timeToDelivery; - } - - private PizzaStatus (int timeToDelivery) { - this.timeToDelivery = timeToDelivery; - } -} -``` - -我们可以按如下方式使用 `Pizza` 和 `PizzaStatus`: - -```java -Pizza pz = new Pizza(); -pz.setStatus(Pizza.PizzaStatus.READY); -System.out.println(Pizza.getJsonString(pz)); -``` - -生成 Pizza 状态以以下JSON展示: - -```json -{ - "status" : { - "timeToDelivery" : 2, - "ready" : true, - "ordered" : false, - "delivered" : false - }, - "deliverable" : true -} -``` - -有关枚举类型的JSON序列化/反序列化(包括自定义)的更多信息,请参阅[Jackson-将枚举序列化为JSON对象。](https://www.baeldung.com/jackson-serialize-enums) - -## 10.总结 - -本文我们讨论了Java枚举类型,从基础知识到高级应用以及实际应用场景,让我们感受到枚举的强大功能。 - -## 11. 补充 - -我们在上面讲到了,我们可以通过在枚举类型中定义属性,方法和构造函数让它变得更加强大。 - -下面我通过一个实际的例子展示一下,当我们调用短信验证码的时候可能有几种不同的用途,我们在下面这样定义: - -```java - -public enum PinType { - - REGISTER(100000, "注册使用"), - FORGET_PASSWORD(100001, "忘记密码使用"), - UPDATE_PHONE_NUMBER(100002, "更新手机号码使用"); - - private final int code; - private final String message; - - PinType(int code, String message) { - this.code = code; - this.message = message; - } - - public int getCode() { - return code; - } - - public String getMessage() { - return message; - } - - @Override - public String toString() { - return "PinType{" + - "code=" + code + - ", message='" + message + '\'' + - '}'; - } -} -``` - -实际使用: - - ```java -System.out.println(PinType.FORGET_PASSWORD.getCode()); -System.out.println(PinType.FORGET_PASSWORD.getMessage()); -System.out.println(PinType.FORGET_PASSWORD.toString()); - ``` - -Output: - -```java -100001 -忘记密码使用 -PinType{code=100001, message='忘记密码使用'} -``` - -这样的话,在实际使用起来就会非常灵活方便! \ No newline at end of file diff --git "a/docs/java/basis/BigDecimal\350\247\243\345\206\263\346\265\256\347\202\271\346\225\260\350\277\220\347\256\227\347\262\276\345\272\246\344\270\242\345\244\261\351\227\256\351\242\230.md" "b/docs/java/basis/BigDecimal\350\247\243\345\206\263\346\265\256\347\202\271\346\225\260\350\277\220\347\256\227\347\262\276\345\272\246\344\270\242\345\244\261\351\227\256\351\242\230.md" new file mode 100644 index 00000000000..5f788f087e8 --- /dev/null +++ "b/docs/java/basis/BigDecimal\350\247\243\345\206\263\346\265\256\347\202\271\346\225\260\350\277\220\347\256\227\347\262\276\345\272\246\344\270\242\345\244\261\351\227\256\351\242\230.md" @@ -0,0 +1,69 @@ +## BigDecimal 介绍 + +`BigDecimal` 可以实现对浮点数的运算,不会造成精度丢失。 + +那为什么浮点数 `float` 或 `double` 运算的时候会有精度丢失的风险呢? + +这是因为计算机是二进制的,浮点数没有办法用二进制精确表示。 + +## BigDecimal 的用处 + +《阿里巴巴Java开发手册》中提到:**浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。** 具体原理和浮点数的编码方式有关,这里就不多提了,我们下面直接上实例: + +```java +float a = 1.0f - 0.9f; +float b = 0.9f - 0.8f; +System.out.println(a);// 0.100000024 +System.out.println(b);// 0.099999964 +System.out.println(a == b);// false +``` +具有基本数学知识的我们很清楚的知道输出并不是我们想要的结果(**精度丢失**),我们如何解决这个问题呢?一种很常用的方法是:**使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作。** + +```java +BigDecimal a = new BigDecimal("1.0"); +BigDecimal b = new BigDecimal("0.9"); +BigDecimal c = new BigDecimal("0.8"); + +BigDecimal x = a.subtract(b); +BigDecimal y = b.subtract(c); + +System.out.println(x); /* 0.1 */ +System.out.println(y); /* 0.1 */ +System.out.println(Objects.equals(x, y)); /* true */ +``` + +## BigDecimal 常见方法 + +## 大小比较 + +`a.compareTo(b)` : 返回 -1 表示 `a` 小于 `b`,0 表示 `a` 等于 `b` , 1表示 `a` 大于 `b`。 + +```java +BigDecimal a = new BigDecimal("1.0"); +BigDecimal b = new BigDecimal("0.9"); +System.out.println(a.compareTo(b));// 1 +``` +### 保留几位小数 + +通过 `setScale`方法设置保留几位小数以及保留规则。保留规则有挺多种,不需要记,IDEA会提示。 + +```java +BigDecimal m = new BigDecimal("1.255433"); +BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN); +System.out.println(n);// 1.255 +``` + +## BigDecimal 的使用注意事项 + +注意:我们在使用BigDecimal时,为了防止精度丢失,推荐使用它的 **BigDecimal(String)** 构造方法来创建对象。《阿里巴巴Java开发手册》对这部分内容也有提到如下图所示。 + +![《阿里巴巴Java开发手册》对这部分BigDecimal的描述](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/BigDecimal.png) + +## 总结 + +BigDecimal 主要用来操作(大)浮点数,BigInteger 主要用来操作大整数(超过 long 类型)。 + +BigDecimal 的实现利用到了 BigInteger, 所不同的是 BigDecimal 加入了小数位的概念 + + + diff --git "a/docs/java/basis/io\346\250\241\345\236\213\350\257\246\350\247\243.md" "b/docs/java/basis/io\346\250\241\345\236\213\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..231da542c35 --- /dev/null +++ "b/docs/java/basis/io\346\250\241\345\236\213\350\257\246\350\247\243.md" @@ -0,0 +1,131 @@ +--- +title: IO模型详解 +category: Java +tag: + - Java基础 +--- + + +IO 模型这块确实挺难理解的,需要太多计算机底层知识。写这篇文章用了挺久,就非常希望能把我所知道的讲出来吧!希望朋友们能有收获!为了写这篇文章,还翻看了一下《UNIX 网络编程》这本书,太难了,我滴乖乖!心痛~ + +_个人能力有限。如果文章有任何需要补充/完善/修改的地方,欢迎在评论区指出,共同进步!_ + +## 前言 + +I/O 一直是很多小伙伴难以理解的一个知识点,这篇文章我会将我所理解的 I/O 讲给你听,希望可以对你有所帮助。 + +## I/O + +### 何为 I/O? + +I/O(**I**nput/**O**utpu) 即**输入/输出** 。 + +**我们先从计算机结构的角度来解读一下 I/O。** + +根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。 + +![冯诺依曼体系结构](https://img-blog.csdnimg.cn/20190624122126398.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9pcy1jbG91ZC5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) + +输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。 + +输入设备向计算机输入数据,输出设备接收计算机输出的数据。 + +**从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。** + +**我们再先从应用程序的角度来解读一下 I/O。** + +根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 **用户空间(User space)** 和 **内核空间(Kernel space )** 。 + +像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。 + +并且,用户空间的程序不能直接访问内核空间。 + +当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。 + +因此,用户进程想要执行 IO 操作的话,必须通过 **系统调用** 来间接访问内核空间 + +我们在平常开发过程中接触最多的就是 **磁盘 IO(读写文件)** 和 **网络 IO(网络请求和响应)**。 + +**从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。** + +当应用程序发起 I/O 调用后,会经历两个步骤: + +1. 内核等待 I/O 设备准备好数据 +2. 内核将数据从内核空间拷贝到用户空间。 + +### 有哪些常见的 IO 模型? + +UNIX 系统下, IO 模型一共有 5 种: **同步阻塞 I/O**、**同步非阻塞 I/O**、**I/O 多路复用**、**信号驱动 I/O** 和**异步 I/O**。 + +这也是我们经常提到的 5 种 IO 模型。 + +## Java 中 3 种常见 IO 模型 + +### BIO (Blocking I/O) + +**BIO 属于同步阻塞 IO 模型** 。 + +同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。 + +![图源:《深入拆解Tomcat & Jetty》](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6a9e704af49b4380bb686f0c96d33b81~tplv-k3u1fbpfcp-watermark.image) + +在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 + +### NIO (Non-blocking/New I/O) + +Java 中的 NIO 于 Java 1.4 中引入,对应 `java.nio` 包,提供了 `Channel` , `Selector`,`Buffer` 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。 + +Java 中的 NIO 可以看作是 **I/O 多路复用模型**。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。 + +跟着我的思路往下看看,相信你会得到答案! + +我们先来看看 **同步非阻塞 IO 模型**。 + +![图源:《深入拆解Tomcat & Jetty》](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb174e22dbe04bb79fe3fc126aed0c61~tplv-k3u1fbpfcp-watermark.image) + +同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。 + +相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。 + +但是,这种 IO 模型同样存在问题:**应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。** + +这个时候,**I/O 多路复用模型** 就上场了。 + +![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88ff862764024c3b8567367df11df6ab~tplv-k3u1fbpfcp-watermark.image) + +IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间->用户空间)还是阻塞的。 + +> 目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,是目前几乎在所有的操作系统上都有支持 +> +> - **select 调用** :内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。 +> - **epoll 调用** :linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。 + +**IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。** + +Java 中的 NIO ,有一个非常重要的**选择器 ( Selector )** 的概念,也可以被称为 **多路复用器**。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f483f2437ce4ecdb180134270a00144~tplv-k3u1fbpfcp-watermark.image) + +### AIO (Asynchronous I/O) + +AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。 + +异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。 + +![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3077e72a1af049559e81d18205b56fd7~tplv-k3u1fbpfcp-watermark.image) + +目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。 + +最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。 + +![](https://images.xiaozhuanlan.com/photo/2020/33b193457c928ae02217480f994814b6.png) + +## 参考 + +- 《深入拆解 Tomcat & Jetty》 +- 如何完成一次 IO:[https://llc687.top/post/如何完成一次-io/](https://llc687.top/post/如何完成一次-io/) +- 程序员应该这样理解 IO:[https://www.jianshu.com/p/fa7bdc4f3de7](https://www.jianshu.com/p/fa7bdc4f3de7) +- 10 分钟看懂, Java NIO 底层原理:https://www.cnblogs.com/crazymakercircle/p/10225159.html +- IO 模型知多少 | 理论篇:https://www.cnblogs.com/sheng-jie/p/how-much-you-know-about-io-models.html +- 《UNIX 网络编程 卷 1;套接字联网 API 》6.2 节 IO 模型 + diff --git "a/docs/java/basis/java\345\237\272\347\241\200\347\237\245\350\257\206\346\200\273\347\273\223.md" "b/docs/java/basis/java\345\237\272\347\241\200\347\237\245\350\257\206\346\200\273\347\273\223.md" new file mode 100644 index 00000000000..867bfb63867 --- /dev/null +++ "b/docs/java/basis/java\345\237\272\347\241\200\347\237\245\350\257\206\346\200\273\347\273\223.md" @@ -0,0 +1,1374 @@ +--- +title: Java基础知识&面试题总结 +category: Java +tag: + - Java基础 +--- + +## 基础概念与常识 + +### Java 语言有哪些特点? + +1. 简单易学; +2. 面向对象(封装,继承,多态); +3. 平台无关性( Java 虚拟机实现平台无关性); +4. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持); +5. 可靠性; +6. 安全性; +7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便); +8. 编译与解释并存; + +> **🐛 修正(参见: [issue#544](https://github.com/Snailclimb/JavaGuide/issues/544))** :C++11 开始(2011 年的时候),C++就引入了多线程库,在 windows、linux、macos 都可以使用`std::thread`和`std::async`来创建线程。参考链接:http://www.cplusplus.com/reference/thread/thread/?kw=thread + +### JVM vs JDK vs JRE + +#### JVM + +Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 + +**什么是字节码?采用字节码的好处是什么?** + +> 在 Java 中,JVM 可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 + +**Java 程序从源代码到运行一般有下面 3 步:** + +![Java程序运行过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png) + +我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。 + +> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。 + +**总结:** + +Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 + +#### JDK 和 JRE + +JDK 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。 + +JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。 + +如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 + +### 为什么说 Java 语言“编译与解释并存”? + +高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著,你可以找一个英文翻译人员帮助你阅读, +有两种选择方式,你可以先等翻译人员将全本的英文名著(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。 + +Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(`*.class` 文件),这种字节码必须由 Java 解释器来解释执行。因此,我们可以认为 Java 语言编译与解释并存。 + +### Oracle JDK 和 OpenJDK 的对比 + +可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么 Oracle JDK 和 OpenJDK 之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。 + +对于 Java 7,没什么关键的地方。OpenJDK 项目主要基于 Sun 捐赠的 HotSpot 源代码。此外,OpenJDK 被选为 Java 7 的参考实现,由 Oracle 工程师维护。关于 JVM,JDK,JRE 和 OpenJDK 之间的区别,Oracle 博客帖子在 2012 年有一个更详细的答案: + +> 问:OpenJDK 存储库中的源代码与用于构建 Oracle JDK 的代码之间有什么区别? +> +> 答:非常接近 - 我们的 Oracle JDK 版本构建过程基于 OpenJDK 7 构建,只添加了几个部分,例如部署代码,其中包括 Oracle 的 Java 插件和 Java WebStart 的实现,以及一些闭源的第三方组件,如图形光栅化器,一些开源的第三方组件,如 Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源 Oracle JDK 的所有部分,除了我们考虑商业功能的部分。 + +**总结:** + +1. Oracle JDK 大概每 6 个月发一次主要版本,而 OpenJDK 版本大概每三个月发布一次。但这不是固定的,我觉得了解这个没啥用处。详情参见:[https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence](https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence) 。 +2. OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是 OpenJDK 的一个实现,并不是完全开源的; +3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码几乎相同,但 Oracle JDK 有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择 Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用 OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题; +4. 在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能; +5. Oracle JDK 不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本; +6. Oracle JDK 使用 BCL/OTN 协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。 + +🌈 拓展一下: + +- BCL 协议(Oracle Binary Code License Agreement): 可以使用JDK(支持商用),但是不能进行修改。 +- OTN 协议(Oracle Technology Network License Agreement): 11 及之后新发布的JDK用的都是这个协议,可以自己私下用,但是商用需要付费。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/20210425151835918.png) + +相关阅读👍:[《Differences Between Oracle JDK and OpenJDK》](https://www.baeldung.com/oracle-jdk-vs-openjdk) + +### Java 和 C++的区别? + +我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过 C++,也要记下来! + +- 都是面向对象的语言,都支持封装、继承和多态 +- Java 不提供指针来直接访问内存,程序内存更加安全 +- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。 +- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。 +- C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。 +- ...... + +### import java 和 javax 有什么区别? + +刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。 + +所以,实际上 java 和 javax 没有区别。这都是一个名字。 + +## 基本语法 + +### 字符型常量和字符串常量的区别? + +1. **形式** : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符 +2. **含义** : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置) +3. **占内存大小** : 字符常量只占 2 个字节; 字符串常量占若干个字节 (**注意: char 在 Java 中占两个字节**), + + > 字符封装类 `Character` 有一个成员常量 `Character.SIZE` 值为 16,单位是`bits`,该值除以 8(`1byte=8bits`)后就可以得到 2 个字节 + +> java 编程思想第四版:2.2.2 节 +> ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) + +### 注释 + +Java 中的注释有三种: + +1. 单行注释 + +2. 多行注释 + +3. 文档注释。 + +在我们编写代码的时候,如果代码量比较少,我们自己或者团队其他成员还可以很轻易地看懂代码,但是当项目结构一旦复杂起来,我们就需要用到注释了。注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释),是我们程序员写给自己看的,注释是你的代码说明书,能够帮助看代码的人快速地理清代码之间的逻辑关系。因此,在写程序的时候随手加上注释是一个非常好的习惯。 + +《Clean Code》这本书明确指出: + +> **代码的注释不是越详细越好。实际上好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。** +> +> **若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。** +> +> 举个例子: +> +> 去掉下面复杂的注释,只需要创建一个与注释所言同一事物的函数即可 +> +> ```java +> // check to see if the employee is eligible for full benefits +> if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) +> ``` +> +> 应替换为 +> +> ```java +> if (employee.isEligibleForFullBenefits()) +> ``` + +### 标识符和关键字的区别是什么? + +在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了标识符,简单来说,标识符就是一个名字。但是有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这种特殊的标识符就是关键字。因此,关键字是被赋予特殊含义的标识符。比如,在我们的日常生活中 ,“警察局”这个名字已经被赋予了特殊的含义,所以如果你开一家店,店的名字不能叫“警察局”,“警察局”就是我们日常生活中的关键字。 + +### Java 中有哪些常见的关键字? + +| 分类 | 关键字 | | | | | | | +| :-------------------- | -------- | ---------- | -------- | ------------ | ---------- | --------- | ------ | +| 访问控制 | private | protected | public | | | | | +| 类,方法和变量修饰符 | abstract | class | extends | final | implements | interface | native | +| | new | static | strictfp | synchronized | transient | volatile | | +| 程序控制 | break | continue | return | do | while | if | else | +| | for | instanceof | switch | case | default | | | +| 错误处理 | try | catch | throw | throws | finally | | | +| 包相关 | import | package | | | | | | +| 基本类型 | boolean | byte | char | double | float | int | long | +| | short | null | true | false | | | | +| 变量引用 | super | this | void | | | | | +| 保留字 | goto | const | | | | | | + +### 自增自减运算符 + +在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(--)。 + +++和--运算符可以放在变量之前,也可以放在变量之后,当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。例如,当 `b = ++a` 时,先自增(自己增加 1),再赋值(赋值给 b);当 `b = a++` 时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。 + +### continue、break、和 return 的区别是什么? + +在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词: + +1. continue :指跳出当前的这一次循环,继续下一次循环。 +2. break :指跳出整个循环体,继续执行循环下面的语句。 + +return 用于跳出所在方法,结束该方法的运行。return 一般有两种用法: + +1. `return;` :直接使用 return 结束方法执行,用于没有返回值函数的方法 +2. `return value;` :return 一个特定值,用于有返回值函数的方法 + +### Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符? + +Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。 + +Java 的泛型是伪泛型,这是因为 Java 在运行期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。 + +```java +List list = new ArrayList<>(); + +list.add(12); +//这里直接添加会报错 +list.add("a"); +Class clazz = list.getClass(); +Method add = clazz.getDeclaredMethod("add", Object.class); +//但是通过反射添加,是可以的 +add.invoke(list, "kl"); + +System.out.println(list); +``` + +泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。 + +**1.泛型类**: + +```java +//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 +//在实例化泛型类时,必须指定T的具体类型 +public class Generic { + + private T key; + + public Generic(T key) { + this.key = key; + } + + public T getKey() { + return key; + } +} +``` + +如何实例化泛型类: + +```java +Generic genericInteger = new Generic(123456); +``` + +**2.泛型接口** : + +```java +public interface Generator { + public T method(); +} +``` + +实现泛型接口,不指定类型: + +```java +class GeneratorImpl implements Generator{ + @Override + public T method() { + return null; + } +} +``` + +实现泛型接口,指定类型: + +```java +class GeneratorImpl implements Generator{ + @Override + public String method() { + return "hello"; + } +} +``` + +**3.泛型方法** : + +```java +public static void printArray(E[] inputArray) { + for (E element : inputArray) { + System.out.printf("%s ", element); + } + System.out.println(); +} +``` + +使用: + +```java +// 创建不同类型数组: Integer, Double 和 Character +Integer[] intArray = { 1, 2, 3 }; +String[] stringArray = { "Hello", "World" }; +printArray(intArray); +printArray(stringArray); +``` + +**常用的通配符为: T,E,K,V,?** + +- ? 表示不确定的 java 类型 +- T (type) 表示具体的一个 java 类型 +- K V (key value) 分别代表 java 键值中的 Key Value +- E (element) 代表 Element + +### ==和 equals 的区别 + +对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。 + +> 因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。 + +**`equals()`** 作用不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。`equals()`方法存在于`Object`类中,而`Object`类是所有类的直接或间接父类。 + +`Object` 类 `equals()` 方法: + +```java +public boolean equals(Object obj) { + return (this == obj); +} +``` + +`equals()` 方法存在两种使用情况: + +- **类没有覆盖 `equals()`方法** :通过`equals()`比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 `Object`类`equals()`方法。 +- **类覆盖了 `equals()`方法** :一般我们都覆盖 `equals()`方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。 + +**举个例子:** + +```java +public class test1 { + public static void main(String[] args) { + String a = new String("ab"); // a 为一个引用 + String b = new String("ab"); // b为另一个引用,对象的内容一样 + String aa = "ab"; // 放在常量池中 + String bb = "ab"; // 从常量池中查找 + if (aa == bb) // true + System.out.println("aa==bb"); + if (a == b) // false,非同一对象 + System.out.println("a==b"); + if (a.equals(b)) // true + System.out.println("aEQb"); + if (42 == 42.0) { // true + System.out.println("true"); + } + } +} +``` + +**说明:** + +- `String` 中的 `equals` 方法是被重写过的,因为 `Object` 的 `equals` 方法是比较的对象的内存地址,而 `String` 的 `equals` 方法比较的是对象的值。 +- 当创建 `String` 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 `String` 对象。 + +`String`类`equals()`方法: + +```java +public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + while (n-- != 0) { + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; +} +``` + +### hashCode()与 equals() + +面试官可能会问你:“你重写过 `hashcode` 和 `equals`么,为什么重写 `equals` 时必须重写 `hashCode` 方法?” + +**1)hashCode()介绍:** + +`hashCode()` 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。`hashCode()`定义在 JDK 的 `Object` 类中,这就意味着 Java 中的任何类都包含有 `hashCode()` 函数。另外需要注意的是: `Object` 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。 + +```java +public native int hashCode(); +``` + +散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) + +**2)为什么要有 hashCode?** + +我们以“`HashSet` 如何检查重复”为例子来说明为什么要有 hashCode? + +当你把对象加入 `HashSet` 时,`HashSet` 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 `equals()` 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head First Java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 + +**3)为什么重写 `equals` 时必须重写 `hashCode` 方法?** + +如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。**因此,equals 方法被覆盖过,则 `hashCode` 方法也必须被覆盖。** + +> `hashCode()`的默认行为是对堆上的对象产生独特值。如果没有重写 `hashCode()`,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) + +**4)为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?** + +在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。 + +因为 `hashCode()` 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 `hashCode` )。 + +我们刚刚也提到了 `HashSet`,如果 `HashSet` 在对比的时候,同样的 hashcode 有多个对象,它会使用 `equals()` 来判断是否真的相同。也就是说 `hashcode` 只是用来缩小查找成本。 + +更多关于 `hashcode()` 和 `equals()` 的内容可以查看:[Java hashCode() 和 equals()的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html) + +## 基本数据类型 + +### Java 中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢? + +Java 中有 8 种基本数据类型,分别为: + +1. 6 种数字类型 :`byte`、`short`、`int`、`long`、`float`、`double` +2. 1 种字符类型:`char` +3. 1 种布尔型:`boolean`。 + +这 8 种基本数据类型的默认值以及所占空间的大小如下: + +| 基本类型 | 位数 | 字节 | 默认值 | +| :-------- | :--- | :--- | :------ | +| `int` | 32 | 4 | 0 | +| `short` | 16 | 2 | 0 | +| `long` | 64 | 8 | 0L | +| `byte` | 8 | 1 | 0 | +| `char` | 16 | 2 | 'u0000' | +| `float` | 32 | 4 | 0f | +| `double` | 64 | 8 | 0d | +| `boolean` | 1 | | false | + +另外,对于 `boolean`,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。 + +**注意:** + +1. Java 里使用 `long` 类型的数据一定要在数值后面加上 **L**,否则将作为整型解析。 +2. `char a = 'h'`char :单引号,`String a = "hello"` :双引号。 + +这八种基本类型都有对应的包装类分别为:`Byte`、`Short`、`Integer`、`Long`、`Float`、`Double`、`Character`、`Boolean` 。 + +包装类型不赋值就是 `Null` ,而基本类型有默认值且不是 `Null`。 + +另外,这个问题建议还可以先从 JVM 层面来分析。 + +基本数据类型直接存放在 Java 虚拟机栈中的局部变量表中,而包装类型属于对象类型,我们知道对象实例都存在于堆中。相比于对象类型, 基本数据类型占用的空间非常小。 + +> 《深入理解 Java 虚拟机》 :局部变量表主要存放了编译期可知的基本数据类型 **(boolean、byte、char、short、int、float、long、double)**、**对象引用**(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 + +### 自动装箱与拆箱 + +- **装箱**:将基本类型用它们对应的引用类型包装起来; +- **拆箱**:将包装类型转换为基本数据类型; + +举例: + +```java +Integer i = 10; //装箱 +int n = i; //拆箱 +``` + +上面这两行代码对应的字节码为: + +```java + L1 + + LINENUMBER 8 L1 + + ALOAD 0 + + BIPUSH 10 + + INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; + + PUTFIELD AutoBoxTest.i : Ljava/lang/Integer; + + L2 + + LINENUMBER 9 L2 + + ALOAD 0 + + ALOAD 0 + + GETFIELD AutoBoxTest.i : Ljava/lang/Integer; + + INVOKEVIRTUAL java/lang/Integer.intValue ()I + + PUTFIELD AutoBoxTest.n : I + + RETURN +``` + +从字节码中,我们发现装箱其实就是调用了 包装类的`valueOf()`方法,拆箱其实就是调用了 `xxxValue()`方法。 + +因此, + +- `Integer i = 10` 等价于 `Integer i = Integer.valueOf(10)` +- `int n = i` 等价于 `int n = i.intValue()`; + +### 8 种基本类型的包装类和常量池 + +Java 基本类型的包装类的大部分都实现了常量池技术。`Byte`,`Short`,`Integer`,`Long` 这 4 种包装类默认创建了数值 **[-128,127]** 的相应类型的缓存数据,`Character` 创建了数值在[0,127]范围的缓存数据,`Boolean` 直接返回 `True` Or `False`。 + +**Integer 缓存源码:** + +```java +/** + +*此方法将始终缓存-128 到 127(包括端点)范围内的值,并可以缓存此范围之外的其他值。 + +*/ + +public static Integer valueOf(int i) { + + if (i >= IntegerCache.low && i <= IntegerCache.high) + + return IntegerCache.cache[i + (-IntegerCache.low)]; + + return new Integer(i); + +} + +private static class IntegerCache { + + static final int low = -128; + + static final int high; + + static final Integer cache[]; + +} +``` + +**`Character` 缓存源码:** + +```java +public static Character valueOf(char c) { + + if (c <= 127) { // must cache + + return CharacterCache.cache[(int)c]; + + } + + return new Character(c); + +} + + + +private static class CharacterCache { + + private CharacterCache(){} + + + + static final Character cache[] = new Character[127 + 1]; + + static { + + for (int i = 0; i < cache.length; i++) + + cache[i] = new Character((char)i); + + } + +} +``` + +**`Boolean` 缓存源码:** + +```java +public static Boolean valueOf(boolean b) { + + return (b ? TRUE : FALSE); + +} +``` + +如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。 + +两种浮点数类型的包装类 `Float`,`Double` 并没有实现常量池技术。 + +```java +Integer i1 = 33; + +Integer i2 = 33; + +System.out.println(i1 == i2);// 输出 true + +Float i11 = 333f; + +Float i22 = 333f; + +System.out.println(i11 == i22);// 输出 false + +Double i3 = 1.2; + +Double i4 = 1.2; + +System.out.println(i3 == i4);// 输出 false +``` + +下面我们来看一下问题。下面的代码的输出结果是 `true` 还是 `flase` 呢? + +```java +Integer i1 = 40; + +Integer i2 = new Integer(40); + +System.out.println(i1==i2); +``` + +`Integer i1=40` 这一行代码会发生装箱,也就是说这行代码等价于 `Integer i1=Integer.valueOf(40)` 。因此,`i1` 直接使用的是常量池中的对象。而`Integer i1 = new Integer(40)` 会直接创建新的对象。 + +因此,答案是 `false` 。你答对了吗? + +记住:**所有整型包装类对象之间值的比较,全部使用 equals 方法比较**。 + +![](https://img-blog.csdnimg.cn/20210422164544846.png) + +## 方法(函数) + +### 什么是方法的返回值? + +方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用是接收出结果,使得它可以用于其他的操作! + +### 方法有哪几种类型? + +**1.无参数无返回值的方法** + +```java +// 无参数无返回值的方法(如果方法没有返回值,不能不写,必须写void,表示没有返回值) +public void f1() { + System.out.println("无参数无返回值的方法"); +} +``` + +**2.有参数无返回值的方法** + +```java +/** +* 有参数无返回值的方法 +* 参数列表由零组到多组“参数类型+形参名”组合而成,多组参数之间以英文逗号(,)隔开,形参类型和形参名之间以英文空格隔开 +*/ +public void f2(int a, String b, int c) { + System.out.println(a + "-->" + b + "-->" + c); +} +``` + +**3.有返回值无参数的方法** + +```java +// 有返回值无参数的方法(返回值可以是任意的类型,在函数里面必须有return关键字返回对应的类型) +public int f3() { + System.out.println("有返回值无参数的方法"); + return 2; +} +``` + +**4.有返回值有参数的方法** + +```java +// 有返回值有参数的方法 +public int f4(int a, int b) { + return a * b; +} +``` + +**5.return 在无返回值方法的特殊使用** + +```java +// return在无返回值方法的特殊使用 +public void f5(int a) { + if (a > 10) { + return;//表示结束所在方法 (f5方法)的执行,下方的输出语句不会执行 + } + System.out.println(a); +} +``` + +### 在一个静态方法内调用一个非静态成员为什么是非法的? + +这个需要结合 JVM 的相关知识,静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,然后通过类的实例对象去访问。在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。 + +### 静态方法和实例方法有何不同? + +**1、调用方式** + +在外部调用静态方法时,可以使用 `类名.方法名` 的方式,也可以使用 `对象.方法名` 的方式,而实例方法只有后面这种方式。也就是说,**调用静态方法可以无需创建对象** 。 + +不过,需要注意的是一般不建议使用 `对象.方法名` 的方式来调用静态方法。这种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类。 + +因此,一般建议使用 `类名.方法名` 的方式来调用静态方法。 + +```java + +public class Person { + public void method() { + //...... + } + + public static void staicMethod(){ + //...... + } + public static void main(String[] args) { + Person person = new Person(); + // 调用实例方法 + person.method(); + // 调用静态方法 + Person.staicMethod() + } +} +``` + +**2、访问类成员是否存在限制** + +静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。 + +### 为什么 Java 中只有值传递? + +首先,我们回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。 + +**按值调用(call by value)** 表示方法接收的是调用者提供的值,**按引用调用(call by reference)** 表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。它用来描述各种程序设计语言(不只是 Java)中方法参数传递方式。 + +**Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。** + +**下面通过 3 个例子来给大家说明** + +> **example 1** + +```java +public static void main(String[] args) { + int num1 = 10; + int num2 = 20; + + swap(num1, num2); + + System.out.println("num1 = " + num1); + System.out.println("num2 = " + num2); +} + +public static void swap(int a, int b) { + int temp = a; + a = b; + b = temp; + + System.out.println("a = " + a); + System.out.println("b = " + b); +} +``` + +**结果:** + +``` +a = 20 +b = 10 +num1 = 10 +num2 = 20 +``` + +**解析:** + +![example 1 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/22191348.jpg) + +在 swap 方法中,a、b 的值进行交换,并不会影响到 num1、num2。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。 + +**通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example2.** + +> **example 2** + +```java + public static void main(String[] args) { + int[] arr = { 1, 2, 3, 4, 5 }; + System.out.println(arr[0]); + change(arr); + System.out.println(arr[0]); + } + + public static void change(int[] array) { + // 将数组的第一个元素变为0 + array[0] = 0; + } +``` + +**结果:** + +``` +1 +0 +``` + +**解析:** + +![example 2](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/3825204.jpg) + +array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的是同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。 + +**通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。** + +**很多程序设计语言(特别是,C++和 Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。** + +> **example 3** + +```java +public class Test { + + public static void main(String[] args) { + // TODO Auto-generated method stub + Student s1 = new Student("小张"); + Student s2 = new Student("小李"); + Test.swap(s1, s2); + System.out.println("s1:" + s1.getName()); + System.out.println("s2:" + s2.getName()); + } + + public static void swap(Student x, Student y) { + Student temp = x; + x = y; + y = temp; + System.out.println("x:" + x.getName()); + System.out.println("y:" + y.getName()); + } +} +``` + +**结果:** + +``` +x:小李 +y:小张 +s1:小张 +s2:小李 +``` + +**解析:** + +交换之前: + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/88729818.jpg) + +交换之后: + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/34384414.jpg) + +通过上面两张图可以很清晰的看出: **方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝** + +> **总结** + +Java 程序设计语言对对象采用的不是引用调用,实际上,对象引用是按 +值传递的。 + +下面再总结一下 Java 中方法参数的使用情况: + +- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。 +- 一个方法可以改变一个对象参数的状态。 +- 一个方法不能让对象参数引用一个新的对象。 + +**参考:** + +《Java 核心技术卷 Ⅰ》基础知识第十版第四章 4.5 小节 + +### 重载和重写的区别 + +> 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理 +> +> 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法 + +#### 重载 + +发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 + +下面是《Java 核心技术》对重载这个概念的介绍: + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg) + +综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。 + +#### 重写 + +重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。 + +1. 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。 +2. 如果父类方法访问修饰符为 `private/final/static` 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。 +3. 构造方法无法被重写 + +综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变 + +暖心的 Guide 哥最后再来个图表总结一下! + +| 区别点 | 重载方法 | 重写方法 | +| :--------- | :------- | :----------------------------------------------------------- | +| 发生范围 | 同一个类 | 子类 | +| 参数列表 | 必须修改 | 一定不能修改 | +| 返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 | +| 异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; | +| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) | +| 发生阶段 | 编译期 | 运行期 | + +**方法的重写要遵循“两同两小一大”**(以下内容摘录自《疯狂 Java 讲义》,[issue#892](https://github.com/Snailclimb/JavaGuide/issues/892) ): + +- “两同”即方法名相同、形参列表相同; +- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; +- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。 + +⭐️ 关于 **重写的返回值类型** 这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。 + +```java +public class Hero { + public String name() { + return "超级英雄"; + } +} +public class SuperMan extends Hero{ + @Override + public String name() { + return "超人"; + } + public Hero hero() { + return new Hero(); + } +} + +public class SuperSuperMan extends SuperMan { + public String name() { + return "超级超级英雄"; + } + + @Override + public SuperMan hero() { + return new SuperMan(); + } +} +``` + +### 深拷贝 vs 浅拷贝 + +1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 +2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 + +![deep and shallow copy](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/java-deep-and-shallow-copy.jpg) + +## Java 面向对象 + +### 面向对象和面向过程的区别 + +- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。** +- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。 + +参见 issue : [面向过程 :面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) + +> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。 +> +> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。 + +### 成员变量与局部变量的区别有哪些? + +1. 从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 `public`,`private`,`static` 等修饰符所修饰,而局部变量不能被访问控制修饰符及 `static` 所修饰;但是,成员变量和局部变量都能被 `final` 所修饰。 +2. 从变量在内存中的存储方式来看,如果成员变量是使用 `static` 修饰的,那么这个成员变量是属于类的,如果没有使用 `static` 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 +3. 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 +4. 从变量是否有默认值来看,成员变量如果没有被赋初,则会自动以类型的默认值而赋值(一种情况例外:被 `final` 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 + +### 创建一个对象用什么运算符?对象实体与对象引用有何不同? + +new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。 + +一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。 + +### 对象的相等与指向他们的引用相等,两者有什么不同? + +对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。 + +### 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么? + +构造方法主要作用是完成对类对象的初始化工作。 + +如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,这时候,就不能直接 new 一个对象而不传递参数了,所以我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。 + +### 构造方法有哪些特点?是否可被 override? + +特点: + +1. 名字与类名相同。 +2. 没有返回值,但不能用 void 声明构造函数。 +3. 生成类的对象时自动执行,无需调用。 + +构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 + +### 面向对象三大特征 + +#### 封装 + +封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。 + +```java +public class Student { + private int id;//id属性私有化 + private String name;//name属性私有化 + + //获取id的方法 + public int getId() { + return id; + } + + //设置id的方法 + public void setId(int id) { + this.id = id; + } + + //获取name的方法 + public String getName() { + return name; + } + + //设置name的方法 + public void setName(String name) { + this.name = name; + } +} +``` + +#### 继承 + +不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。 + +**关于继承如下 3 点请记住:** + +1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 +2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 +3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 + +#### 多态 + +多态,顾名思义,表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例。 + +**多态的特点:** + +- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系; +- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定; +- 多态不能调用“只在子类存在但在父类不存在”的方法; +- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。 + +### String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? + +**可变性** + +简单的来说:`String` 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以`String` 对象是不可变的。 + +> 补充(来自[issue 675](https://github.com/Snailclimb/JavaGuide/issues/675)):在 Java 9 之后,String 、`StringBuilder` 与 `StringBuffer` 的实现改用 byte 数组存储字符串 `private final byte[] value` + +而 `StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串`char[]value` 但是没有用 `final` 关键字修饰,所以这两种对象都是可变的。 + +`StringBuilder` 与 `StringBuffer` 的构造方法都是调用父类构造方法也就是`AbstractStringBuilder` 实现的,大家可以自行查阅源码。 + +`AbstractStringBuilder.java` + +```java +abstract class AbstractStringBuilder implements Appendable, CharSequence { + /** + * The value is used for character storage. + */ + char[] value; + + /** + * The count is the number of characters used. + */ + int count; + + AbstractStringBuilder(int capacity) { + value = new char[capacity]; + }} +``` + +**线程安全性** + +`String` 中的对象是不可变的,也就可以理解为常量,线程安全。`AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。 + +**性能** + +每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。`StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 + +**对于三者使用的总结:** + +1. 操作少量的数据: 适用 `String` +2. 单线程操作字符串缓冲区下操作大量数据: 适用 `StringBuilder` +3. 多线程操作字符串缓冲区下操作大量数据: 适用 `StringBuffer` + +### Object 类的常见方法总结 + +Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法: + +```java +public final native Class getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。 + +public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。 +public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。 + +protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。 + +public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。 + +public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 + +public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。 + +public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。 + +public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。 + +public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念 + +protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作 +``` + + +## 反射 + +### 何为反射? + +如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。 + +反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。 + +通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。 + +### 反射机制优缺点 + +- **优点** : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利 +- **缺点** :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) + +### 反射的应用场景 + +像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。 + +但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 + +**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。** + +比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。 + +```java +public class DebugInvocationHandler implements InvocationHandler { + /** + * 代理类中的真实对象 + */ + private final Object target; + + public DebugInvocationHandler(Object target) { + this.target = target; + } + + + public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { + System.out.println("before method " + method.getName()); + Object result = method.invoke(target, args); + System.out.println("after method " + method.getName()); + return result; + } +} + +``` + +另外,像 Java 中的一大利器 **注解** 的实现也用到了反射。 + +为什么你使用 Spring 的时候 ,一个`@Component`注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 `@Value`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢? + +这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。 + +## 异常 + +### Java 异常类层次结构图 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12/Java%E5%BC%82%E5%B8%B8%E7%B1%BB%E5%B1%82%E6%AC%A1%E7%BB%93%E6%9E%84%E5%9B%BE.png) + +

图片来自:https://simplesnippets.tech/exception-handling-in-java-part-1/

+ +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12/Java%E5%BC%82%E5%B8%B8%E7%B1%BB%E5%B1%82%E6%AC%A1%E7%BB%93%E6%9E%84%E5%9B%BE2.png) + +

图片来自:https://chercher.tech/java-programming/exceptions-java

+ +在 Java 中,所有的异常都有一个共同的祖先 `java.lang` 包中的 `Throwable` 类。`Throwable` 类有两个重要的子类 `Exception`(异常)和 `Error`(错误)。`Exception` 能被程序本身处理(`try-catch`), `Error` 是无法处理的(只能尽量避免)。 + +`Exception` 和 `Error` 二者都是 Java 异常处理的重要子类,各自都包含大量子类。 + +- **`Exception`** :程序本身可以处理的异常,可以通过 `catch` 来进行捕获。`Exception` 又可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)。 +- **`Error`** :`Error` 属于程序无法处理的错误 ,我们没办法通过 `catch` 来进行捕获 。例如,Java 虚拟机运行错误(`Virtual MachineError`)、虚拟机内存不够错误(`OutOfMemoryError`)、类定义错误(`NoClassDefFoundError`)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。 + +**受检查异常** + +Java 代码在编译过程中,如果受检查异常没有被 `catch`/`throw` 处理的话,就没办法通过编译 。比如下面这段 IO 操作的代码。 + +![check-exception](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12/check-exception.png) + +除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、`ClassNotFoundException` 、`SQLException`...。 + +**不受检查异常** + +Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。 + +`RuntimeException` 及其子类都统称为非受检查异常,例如:`NullPointerException`、`NumberFormatException`(字符串转换为数字)、`ArrayIndexOutOfBoundsException`(数组越界)、`ClassCastException`(类型转换错误)、`ArithmeticException`(算术错误)等。 + +### Throwable 类常用方法 + +- **`public String getMessage()`**:返回异常发生时的简要描述 +- **`public String toString()`**:返回异常发生时的详细信息 +- **`public String getLocalizedMessage()`**:返回异常对象的本地化信息。使用 `Throwable` 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 `getMessage()`返回的结果相同 +- **`public void printStackTrace()`**:在控制台上打印 `Throwable` 对象封装的异常信息 + +### try-catch-finally + +- **`try`块:** 用于捕获异常。其后可接零个或多个 `catch` 块,如果没有 `catch` 块,则必须跟一个 `finally` 块。 +- **`catch`块:** 用于处理 try 捕获到的异常。 +- **`finally` 块:** 无论是否捕获或处理异常,`finally` 块里的语句都会被执行。当在 `try` 块或 `catch` 块中遇到 `return` 语句时,`finally` 语句块将在方法返回之前被执行。 + +**在以下 3 种特殊情况下,`finally` 块不会被执行:** + +1. 在 `try` 或 `finally`块中用了 `System.exit(int)`退出程序。但是,如果 `System.exit(int)` 在异常语句之后,`finally` 还是会被执行 +2. 程序所在的线程死亡。 +3. 关闭 CPU。 + +下面这部分内容来自 issue:。 + +**注意:** 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。如下: + +```java +public class Test { + public static int f(int value) { + try { + return value * value; + } finally { + if (value == 2) { + return 0; + } + } + } +} +``` + +如果调用 `f(2)`,返回值将是 0,因为 finally 语句的返回值覆盖了 try 语句块的返回值。 + +### 使用 `try-with-resources` 来代替`try-catch-finally` + +1. **适用范围(资源的定义):** 任何实现 `java.lang.AutoCloseable`或者 `java.io.Closeable` 的对象 +2. **关闭资源和 finally 块的执行顺序:** 在 `try-with-resources` 语句中,任何 catch 或 finally 块在声明的资源关闭后运行 + +《Effecitve Java》中明确指出: + +> 面对必须要关闭的资源,我们总是应该优先使用 `try-with-resources` 而不是`try-finally`。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。`try-with-resources`语句让我们更容易编写必须要关闭的资源的代码,若采用`try-finally`则几乎做不到这点。 + +Java 中类似于`InputStream`、`OutputStream` 、`Scanner` 、`PrintWriter`等的资源都需要我们调用`close()`方法来手动关闭,一般情况下我们都是通过`try-catch-finally`语句来实现这个需求,如下: + +```java + //读取文本文件的内容 + Scanner scanner = null; + try { + scanner = new Scanner(new File("D://read.txt")); + while (scanner.hasNext()) { + System.out.println(scanner.nextLine()); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } finally { + if (scanner != null) { + scanner.close(); + } + } +``` + +使用 Java 7 之后的 `try-with-resources` 语句改造上面的代码: + +```java +try (Scanner scanner = new Scanner(new File("test.txt"))) { + while (scanner.hasNext()) { + System.out.println(scanner.nextLine()); + } +} catch (FileNotFoundException fnfe) { + fnfe.printStackTrace(); +} +``` + +当然多个资源需要关闭的时候,使用 `try-with-resources` 实现起来也非常简单,如果你还是用`try-catch-finally`可能会带来很多问题。 + +通过使用分号分隔,可以在`try-with-resources`块中声明多个资源。 + +```java +try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt"))); + BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) { + int b; + while ((b = bin.read()) != -1) { + bout.write(b); + } + } + catch (IOException e) { + e.printStackTrace(); + } +``` + +## I/O 流 + +### 什么是序列化?什么是反序列化? + +如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。 + +简单来说: + +- **序列化**: 将数据结构或对象转换成二进制字节流的过程 +- **反序列化**:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程 + +对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。 + +维基百科是如是介绍序列化的: + +> **序列化**(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。 + +综上:**序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。** + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/a478c74d-2c48-40ae-9374-87aacf05188c.png) + +

https://www.corejavaguru.com/java/serialization/interview-questions-1

+ +### Java 序列化中如果有些字段不想进行序列化,怎么办? + +对于不想进行序列化的变量,使用 `transient` 关键字修饰。 + +`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。 + +关于 `transient` 还有几点注意: +- `transient` 只能修饰变量,不能修饰类和方法。 +- `transient` 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 `int` 类型,那么反序列后结果就是 `0`。 +- `static` 变量因为不属于任何对象(Object),所以无论有没有 `transient` 关键字修饰,均不会被序列化。 + +### 获取用键盘输入常用的两种方法 + +方法 1:通过 `Scanner` + +```java +Scanner input = new Scanner(System.in); +String s = input.nextLine(); +input.close(); +``` + +方法 2:通过 `BufferedReader` + +```java +BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); +String s = input.readLine(); +``` + +### Java 中 IO 流分为几种? + +- 按照流的流向分,可以分为输入流和输出流; +- 按照操作单元划分,可以划分为字节流和字符流; +- 按照流的角色划分为节点流和处理流。 + +Java IO 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。 + +- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 +- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 + +按操作方式分类结构图: + +![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png) + +按操作对象分类结构图: + +![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png) + +### 既然有了字节流,为什么还要有字符流? + +问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** + +回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 + +## 参考 + +- https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre +- https://www.educba.com/oracle-vs-openjdk/ +- https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk 基础概念与常识 + diff --git "a/docs/java/basis/\344\273\243\347\220\206\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/docs/java/basis/\344\273\243\347\220\206\346\250\241\345\274\217\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..0006caed0aa --- /dev/null +++ "b/docs/java/basis/\344\273\243\347\220\206\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -0,0 +1,404 @@ +--- +title: 代理详解!静态代理+JDK/CGLIB 动态代理实战 +category: Java +tag: + - Java基础 +--- + +## 1. 代理模式 + +代理模式是一种比较好理解的设计模式。简单来说就是 **我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。** + +**代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。** + +举个例子:你找了小红来帮你问话,小红就可以看作是代理你的代理对象,代理的行为(方法)是问话。 + +![Understanding the Proxy Design Pattern | by Mithun Sasidharan | Medium](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/1*DjWCgTFm-xqbhbNQVsaWQw.png) + +

https://medium.com/@mithunsasidharan/understanding-the-proxy-design-pattern-5e63fe38052a

+ +代理模式有静态代理和动态代理两种实现方式,我们 先来看一下静态代理模式的实现。 + +## 2. 静态代理 + +**静态代理中,我们对目标对象的每个方法的增强都是手动完成的(_后面会具体演示代码_),非常不灵活(_比如接口一旦新增加方法,目标对象和代理对象都要进行修改_)且麻烦(_需要对每个目标类都单独写一个代理类_)。** 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。 + +上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, **静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。** + +静态代理实现步骤: + +1. 定义一个接口及其实现类; +2. 创建一个代理类同样实现这个接口 +3. 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。 + +下面通过代码展示! + +**1.定义发送短信的接口** + +```java +public interface SmsService { + String send(String message); +} +``` + +**2.实现发送短信的接口** + +```java +public class SmsServiceImpl implements SmsService { + public String send(String message) { + System.out.println("send message:" + message); + return message; + } +} +``` + +**3.创建代理类并同样实现发送短信的接口** + +```java +public class SmsProxy implements SmsService { + + private final SmsService smsService; + + public SmsProxy(SmsService smsService) { + this.smsService = smsService; + } + + @Override + public String send(String message) { + //调用方法之前,我们可以添加自己的操作 + System.out.println("before method send()"); + smsService.send(message); + //调用方法之后,我们同样可以添加自己的操作 + System.out.println("after method send()"); + return null; + } +} +``` + +**4.实际使用** + +```java +public class Main { + public static void main(String[] args) { + SmsService smsService = new SmsServiceImpl(); + SmsProxy smsProxy = new SmsProxy(smsService); + smsProxy.send("java"); + } +} +``` + +运行上述代码之后,控制台打印出: + +```bash +before method send() +send message:java +after method send() +``` + +可以输出结果看出,我们已经增加了 `SmsServiceImpl` 的`send()`方法。 + +## 3. 动态代理 + +相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( _CGLIB 动态代理机制_)。 + +**从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。** + +说到动态代理,Spring AOP、RPC 框架应该是两个不得不提的,它们的实现都依赖了动态代理。 + +**动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助。** + +就 Java 来说,动态代理的实现方式有很多种,比如 **JDK 动态代理**、**CGLIB 动态代理**等等。 + +[guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 使用的是 JDK 动态代理,我们先来看看 JDK 动态代理的使用。 + +另外,虽然 [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 没有用到 **CGLIB 动态代理** ,我们这里还是简单介绍一下其使用以及和**JDK 动态代理**的对比。 + +### 3.1. JDK 动态代理机制 + +#### 3.1.1. 介绍 + +**在 Java 动态代理机制中 `InvocationHandler` 接口和 `Proxy` 类是核心。** + +`Proxy` 类中使用频率最高的方法是:`newProxyInstance()` ,这个方法主要用来生成一个代理对象。 + +```java + public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h) + throws IllegalArgumentException + { + ...... + } +``` + +这个方法一共有 3 个参数: + +1. **loader** :类加载器,用于加载代理对象。 +2. **interfaces** : 被代理类实现的一些接口; +3. **h** : 实现了 `InvocationHandler` 接口的对象; + +要实现动态代理的话,还必须需要实现`InvocationHandler` 来自定义处理逻辑。 当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现`InvocationHandler` 接口类的 `invoke` 方法来调用。 + +```java +public interface InvocationHandler { + + /** + * 当你使用代理对象调用方法的时候实际会调用到这个方法 + */ + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable; +} +``` + +`invoke()` 方法有下面三个参数: + +1. **proxy** :动态生成的代理类 +2. **method** : 与代理类对象调用的方法相对应 +3. **args** : 当前 method 方法的参数 + +也就是说:**你通过`Proxy` 类的 `newProxyInstance()` 创建的代理对象在调用方法的时候,实际会调用到实现`InvocationHandler` 接口的类的 `invoke()`方法。** 你可以在 `invoke()` 方法中自定义处理逻辑,比如在方法执行前后做什么事情。 + +#### 3.1.2. JDK 动态代理类使用步骤 + +1. 定义一个接口及其实现类; +2. 自定义 `InvocationHandler` 并重写`invoke`方法,在 `invoke` 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑; +3. 通过 `Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)` 方法创建代理对象; + +#### 3.1.3. 代码示例 + +这样说可能会有点空洞和难以理解,我上个例子,大家感受一下吧! + +**1.定义发送短信的接口** + +```java +public interface SmsService { + String send(String message); +} +``` + +**2.实现发送短信的接口** + +```java +public class SmsServiceImpl implements SmsService { + public String send(String message) { + System.out.println("send message:" + message); + return message; + } +} +``` + +**3.定义一个 JDK 动态代理类** + +```java +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * @author shuang.kou + * @createTime 2020年05月11日 11:23:00 + */ +public class DebugInvocationHandler implements InvocationHandler { + /** + * 代理类中的真实对象 + */ + private final Object target; + + public DebugInvocationHandler(Object target) { + this.target = target; + } + + + public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { + //调用方法之前,我们可以添加自己的操作 + System.out.println("before method " + method.getName()); + Object result = method.invoke(target, args); + //调用方法之后,我们同样可以添加自己的操作 + System.out.println("after method " + method.getName()); + return result; + } +} + +``` + +`invoke()` 方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 `invoke()` 方法,然后 `invoke()` 方法代替我们去调用了被代理对象的原生方法。 + +**4.获取代理对象的工厂类** + +```java +public class JdkProxyFactory { + public static Object getProxy(Object target) { + return Proxy.newProxyInstance( + target.getClass().getClassLoader(), // 目标类的类加载 + target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个 + new DebugInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler + ); + } +} +``` + +`getProxy()` :主要通过`Proxy.newProxyInstance()`方法获取某个类的代理对象 + +**5.实际使用** + +```java +SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl()); +smsService.send("java"); +``` + +运行上述代码之后,控制台打印出: + +``` +before method send +send message:java +after method send +``` + +### 3.2. CGLIB 动态代理机制 + +#### 3.2.1. 介绍 + +**JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。** + +**为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。** + +[CGLIB](https://github.com/cglib/cglib)(_Code Generation Library_)是一个基于[ASM](http://www.baeldung.com/java-asm)的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了[CGLIB](https://github.com/cglib/cglib), 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。 + +**在 CGLIB 动态代理机制中 `MethodInterceptor` 接口和 `Enhancer` 类是核心。** + +你需要自定义 `MethodInterceptor` 并重写 `intercept` 方法,`intercept` 用于拦截增强被代理类的方法。 + +```java +public interface MethodInterceptor +extends Callback{ + // 拦截被代理类中的方法 + public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, + MethodProxy proxy) throws Throwable; +} + +``` + +1. **obj** :被代理的对象(需要增强的对象) +2. **method** :被拦截的方法(需要增强的方法) +3. **args** :方法入参 +4. **proxy** :用于调用原始方法 + +你可以通过 `Enhancer`类来动态获取被代理类,当代理类调用方法的时候,实际调用的是 `MethodInterceptor` 中的 `intercept` 方法。 + +#### 3.2.2. CGLIB 动态代理类使用步骤 + +1. 定义一个类; +2. 自定义 `MethodInterceptor` 并重写 `intercept` 方法,`intercept` 用于拦截增强被代理类的方法,和 JDK 动态代理中的 `invoke` 方法类似; +3. 通过 `Enhancer` 类的 `create()`创建代理类; + +#### 3.2.3. 代码示例 + +不同于 JDK 动态代理不需要额外的依赖。[CGLIB](https://github.com/cglib/cglib)(_Code Generation Library_) 实际是属于一个开源项目,如果你要使用它的话,需要手动添加相关依赖。 + +```xml + + cglib + cglib + 3.3.0 + +``` + +**1.实现一个使用阿里云发送短信的类** + +```java +package github.javaguide.dynamicProxy.cglibDynamicProxy; + +public class AliSmsService { + public String send(String message) { + System.out.println("send message:" + message); + return message; + } +} +``` + +**2.自定义 `MethodInterceptor`(方法拦截器)** + +```java +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import java.lang.reflect.Method; + +/** + * 自定义MethodInterceptor + */ +public class DebugMethodInterceptor implements MethodInterceptor { + + + /** + * @param o 代理对象(增强的对象) + * @param method 被拦截的方法(需要增强的方法) + * @param args 方法入参 + * @param methodProxy 用于调用原始方法 + */ + @Override + public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + //调用方法之前,我们可以添加自己的操作 + System.out.println("before method " + method.getName()); + Object object = methodProxy.invokeSuper(o, args); + //调用方法之后,我们同样可以添加自己的操作 + System.out.println("after method " + method.getName()); + return object; + } + +} +``` + +**3.获取代理类** + +```java +import net.sf.cglib.proxy.Enhancer; + +public class CglibProxyFactory { + + public static Object getProxy(Class clazz) { + // 创建动态代理增强类 + Enhancer enhancer = new Enhancer(); + // 设置类加载器 + enhancer.setClassLoader(clazz.getClassLoader()); + // 设置被代理类 + enhancer.setSuperclass(clazz); + // 设置方法拦截器 + enhancer.setCallback(new DebugMethodInterceptor()); + // 创建代理类 + return enhancer.create(); + } +} +``` + +**4.实际使用** + +```java +AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class); +aliSmsService.send("java"); +``` + +运行上述代码之后,控制台打印出: + +```bash +before method send +send message:java +after method send +``` + +### 3.3. JDK 动态代理和 CGLIB 动态代理对比 + +1. **JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。** 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。 +2. 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。 + +## 4. 静态代理和动态代理的对比 + +1. **灵活性** :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的! +2. **JVM 层面** :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。 + +## 5. 总结 + +这篇文章中主要介绍了代理模式的两种实现:静态代理以及动态代理。涵盖了静态代理和动态代理实战、静态代理和动态代理的区别、JDK 动态代理和 Cglib 动态代理区别等内容。 + +文中涉及到的所有源码,你可以在这里找到:[https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy](https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy) 。 + diff --git "a/docs/java/basis/\345\217\215\345\260\204\346\234\272\345\210\266\350\257\246\350\247\243.md" "b/docs/java/basis/\345\217\215\345\260\204\346\234\272\345\210\266\350\257\246\350\247\243.md" new file mode 100644 index 00000000000..cac029638e4 --- /dev/null +++ "b/docs/java/basis/\345\217\215\345\260\204\346\234\272\345\210\266\350\257\246\350\247\243.md" @@ -0,0 +1,182 @@ +--- +title: 反射机制详解! +category: Java +tag: + - Java基础 +--- + +## 何为反射? + +如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。 + +反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。 + +通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。 + +## 反射的应用场景了解么? + +像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。 + +但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 + +**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。** + +比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。 + +```java +public class DebugInvocationHandler implements InvocationHandler { + /** + * 代理类中的真实对象 + */ + private final Object target; + + public DebugInvocationHandler(Object target) { + this.target = target; + } + + + public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { + System.out.println("before method " + method.getName()); + Object result = method.invoke(target, args); + System.out.println("after method " + method.getName()); + return result; + } +} + +``` + +另外,像 Java 中的一大利器 **注解** 的实现也用到了反射。 + +为什么你使用 Spring 的时候 ,一个`@Component`注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 `@Value`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢? + +这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。 + +## 谈谈反射机制的优缺点 + +**优点** : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利 + +**缺点** :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。相关阅读:[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) + +## 反射实战 + +### 获取 Class 对象的四种方式 + +如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了四种方式获取 Class 对象: + +**1.知道具体类的情况下可以使用:** + +```java +Class alunbarClass = TargetObject.class; +``` + +但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化 + +**2.通过 `Class.forName()`传入类的路径获取:** + +```java +Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject"); +``` + +**3.通过对象实例`instance.getClass()`获取:** + +```java +TargetObject o = new TargetObject(); +Class alunbarClass2 = o.getClass(); +``` + +**4.通过类加载器`xxxClassLoader.loadClass()`传入类路径获取:** + +```java +Class clazz = ClassLoader.loadClass("cn.javaguide.TargetObject"); +``` + +通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行 + +### 反射的一些基本操作 + +1.创建一个我们要使用反射操作的类 `TargetObject`。 + +```java +package cn.javaguide; + +public class TargetObject { + private String value; + + public TargetObject() { + value = "JavaGuide"; + } + + public void publicMethod(String s) { + System.out.println("I love " + s); + } + + private void privateMethod() { + System.out.println("value is " + value); + } +} +``` + +2.使用反射操作这个类的方法以及参数 + +```java +package cn.javaguide; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class Main { + public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException { + /** + * 获取TargetObject类的Class对象并且创建TargetObject类实例 + */ + Class tagetClass = Class.forName("cn.javaguide.TargetObject"); + TargetObject targetObject = (TargetObject) tagetClass.newInstance(); + /** + * 获取所有类中所有定义的方法 + */ + Method[] methods = tagetClass.getDeclaredMethods(); + for (Method method : methods) { + System.out.println(method.getName()); + } + /** + * 获取指定方法并调用 + */ + Method publicMethod = tagetClass.getDeclaredMethod("publicMethod", + String.class); + + publicMethod.invoke(targetObject, "JavaGuide"); + /** + * 获取指定参数并对参数进行修改 + */ + Field field = tagetClass.getDeclaredField("value"); + //为了对类中的参数进行修改我们取消安全检查 + field.setAccessible(true); + field.set(targetObject, "JavaGuide"); + /** + * 调用 private 方法 + */ + Method privateMethod = tagetClass.getDeclaredMethod("privateMethod"); + //为了调用private方法我们取消安全检查 + privateMethod.setAccessible(true); + privateMethod.invoke(targetObject); + } +} + +``` + +输出内容: + +``` +publicMethod +privateMethod +I love JavaGuide +value is JavaGuide +``` + +**注意** : 有读者提到上面代码运行会抛出 `ClassNotFoundException` 异常,具体原因是你没有下面把这段代码的包名替换成自己创建的 `TargetObject` 所在的包 。 + +```java +Class tagetClass = Class.forName("cn.javaguide.TargetObject"); +``` + diff --git a/docs/java/collection/ArrayList-Grow.md b/docs/java/collection/ArrayList-Grow.md deleted file mode 100644 index 0a758cca79b..00000000000 --- a/docs/java/collection/ArrayList-Grow.md +++ /dev/null @@ -1,358 +0,0 @@ - -## 一 先从 ArrayList 的构造函数说起 - -**ArrayList有三种方式来初始化,构造方法源码如下:** - -```java - /** - * 默认初始容量大小 - */ - private static final int DEFAULT_CAPACITY = 10; - - - private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; - - /** - *默认构造函数,使用初始容量10构造一个空列表(无参数构造) - */ - public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; - } - - /** - * 带初始容量参数的构造函数。(用户自己指定容量) - */ - public ArrayList(int initialCapacity) { - if (initialCapacity > 0) {//初始容量大于0 - //创建initialCapacity大小的数组 - this.elementData = new Object[initialCapacity]; - } else if (initialCapacity == 0) {//初始容量等于0 - //创建空数组 - this.elementData = EMPTY_ELEMENTDATA; - } else {//初始容量小于0,抛出异常 - throw new IllegalArgumentException("Illegal Capacity: "+ - initialCapacity); - } - } - - - /** - *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回 - *如果指定的集合为null,throws NullPointerException。 - */ - public ArrayList(Collection c) { - elementData = c.toArray(); - if ((size = elementData.length) != 0) { - // c.toArray might (incorrectly) not return Object[] (see 6260652) - if (elementData.getClass() != Object[].class) - elementData = Arrays.copyOf(elementData, size, Object[].class); - } else { - // replace with empty array. - this.elementData = EMPTY_ELEMENTDATA; - } - } - -``` - -细心的同学一定会发现 :**以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。** 下面在我们分析 ArrayList 扩容时会讲到这一点内容! - -## 二 一步一步分析 ArrayList 扩容机制 - -这里以无参构造函数创建的 ArrayList 为例分析 - -### 1. 先来看 `add` 方法 - -```java - /** - * 将指定的元素追加到此列表的末尾。 - */ - public boolean add(E e) { - //添加元素之前,先调用ensureCapacityInternal方法 - ensureCapacityInternal(size + 1); // Increments modCount!! - //这里看到ArrayList添加元素的实质就相当于为数组赋值 - elementData[size++] = e; - return true; - } -``` -### 2. 再来看看 `ensureCapacityInternal()` 方法 - -可以看到 `add` 方法 首先调用了`ensureCapacityInternal(size + 1)` - -```java - //得到最小扩容量 - private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取默认的容量和传入参数的较大值 - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); - } - - ensureExplicitCapacity(minCapacity); - } -``` -**当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10。** - -### 3. `ensureExplicitCapacity()` 方法 - -如果调用 `ensureCapacityInternal()` 方法就一定会进过(执行)这个方法,下面我们来研究一下这个方法的源码! - -```java - //判断是否需要扩容 - private void ensureExplicitCapacity(int minCapacity) { - modCount++; - - // overflow-conscious code - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 - grow(minCapacity); - } - -``` - -我们来仔细分析一下: - -- 当我们要 add 进第1个元素到 ArrayList 时,elementData.length 为0 (因为还是一个空的 list),因为执行了 `ensureCapacityInternal()` 方法 ,所以 minCapacity 此时为10。此时,`minCapacity - elementData.length > 0 `成立,所以会进入 `grow(minCapacity)` 方法。 -- 当add第2个元素时,minCapacity 为2,此时e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,`minCapacity - elementData.length > 0 ` 不成立,所以不会进入 (执行)`grow(minCapacity)` 方法。 -- 添加第3、4···到第10个元素时,依然不会执行grow方法,数组容量都为10。 - -直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进入grow方法进行扩容。 - -### 4. `grow()` 方法 - -```java - /** - * 要分配的最大数组大小 - */ - private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - - /** - * ArrayList扩容的核心方法。 - */ - private void grow(int minCapacity) { - // oldCapacity为旧容量,newCapacity为新容量 - int oldCapacity = elementData.length; - //将oldCapacity 右移一位,其效果相当于oldCapacity /2, - //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, - int newCapacity = oldCapacity + (oldCapacity >> 1); - //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE, - //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); - } -``` - -**int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity为偶数就是1.5倍,否则是1.5倍左右)!** 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数. - -> ">>"(移位运算符):>>1 右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源   - -**我们再来通过例子探究一下`grow()` 方法 :** - -- 当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 `hugeCapacity` 方法。数组容量为10,add方法中 return true,size增为1。 -- 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。 -- 以此类推······ - -**这里补充一点比较重要,但是容易被忽视掉的知识点:** - -- java 中的 `length `属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. -- java 中的 `length()` 方法是针对字符串说的,如果想看这个字符串的长度则用到 `length()` 这个方法. -- java 中的 `size()` 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! - -### 5. `hugeCapacity()` 方法。 - -从上面 `grow()` 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 - - -```java - private static int hugeCapacity(int minCapacity) { - if (minCapacity < 0) // overflow - throw new OutOfMemoryError(); - //对minCapacity和MAX_ARRAY_SIZE进行比较 - //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小 - //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小 - //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; - } -``` - - - -## 三 `System.arraycopy()` 和 `Arrays.copyOf()`方法 - - -阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及`add(int index, E element)`、`toArray()` 等方法中都用到了该方法! - - -### 3.1 `System.arraycopy()` 方法 - -```java - /** - * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 - */ - public void add(int index, E element) { - rangeCheckForAdd(index); - - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()方法实现数组自己复制自己 - //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; - System.arraycopy(elementData, index, elementData, index + 1, size - index); - elementData[index] = element; - size++; - } -``` - -我们写一个简单的方法测试以下: - -```java -public class ArraycopyTest { - - public static void main(String[] args) { - // TODO Auto-generated method stub - int[] a = new int[10]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - a[3] = 3; - System.arraycopy(a, 2, a, 3, 3); - a[2]=99; - for (int i = 0; i < a.length; i++) { - System.out.println(a[i]); - } - } - -} -``` - -结果: - -``` -0 1 99 2 3 0 0 0 0 0 -``` - -### 3.2 `Arrays.copyOf()`方法 - -```java - /** - 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 - */ - public Object[] toArray() { - //elementData:要复制的数组;size:要复制的长度 - return Arrays.copyOf(elementData, size); - } -``` - -个人觉得使用 `Arrays.copyOf()`方法主要是为了给原有数组扩容,测试代码如下: - -```java -public class ArrayscopyOfTest { - - public static void main(String[] args) { - int[] a = new int[3]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - int[] b = Arrays.copyOf(a, 10); - System.out.println("b.length"+b.length); - } -} -``` - -结果: - -``` -10 -``` - -### 3.3 两者联系和区别 - -**联系:** - -看两者源代码可以发现 copyOf() 内部实际调用了 `System.arraycopy()` 方法 - -**区别:** - -`arraycopy()` 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 `copyOf()` 是系统自动在内部新建一个数组,并返回该数组。 - -## 四 `ensureCapacity`方法 - -ArrayList 源码中有一个 `ensureCapacity` 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢? - -```java - /** - 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。 - * - * @param minCapacity 所需的最小容量 - */ - public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } - } - -``` - -**最好在 add 大量元素之前用 `ensureCapacity` 方法,以减少增量重新分配的次数** - -我们通过下面的代码实际测试以下这个方法的效果: - -```java -public class EnsureCapacityTest { - public static void main(String[] args) { - ArrayList list = new ArrayList(); - final int N = 10000000; - long startTime = System.currentTimeMillis(); - for (int i = 0; i < N; i++) { - list.add(i); - } - long endTime = System.currentTimeMillis(); - System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); - - } -} -``` - -运行结果: - -``` -使用ensureCapacity方法前:2158 -``` - -```java -public class EnsureCapacityTest { - public static void main(String[] args) { - ArrayList list = new ArrayList(); - final int N = 10000000; - list = new ArrayList(); - long startTime1 = System.currentTimeMillis(); - list.ensureCapacity(N); - for (int i = 0; i < N; i++) { - list.add(i); - } - long endTime1 = System.currentTimeMillis(); - System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1)); - } -} -``` - -运行结果: - -``` - -使用ensureCapacity方法前:1773 -``` - -通过运行结果,我们可以看出向 ArrayList 添加大量元素之前最好先使用`ensureCapacity` 方法,以减少增量重新分配的次数。 diff --git "a/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" "b/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index c664ca8f2ec..00000000000 --- "a/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,456 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [剖析面试最常见问题之Java集合框架](#剖析面试最常见问题之java集合框架) - - [说说List,Set,Map三者的区别?](#说说listsetmap三者的区别) - - [Arraylist 与 LinkedList 区别?](#arraylist-与-linkedlist-区别) - - [补充内容:RandomAccess接口](#补充内容randomaccess接口) - - [补充内容:双向链表和双向循环链表](#补充内容双向链表和双向循环链表) - - [ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?](#arraylist-与-vector-区别呢为什么要用arraylist取代vector呢) - - [说一说 ArrayList 的扩容机制吧](#说一说-arraylist-的扩容机制吧) - - [HashMap 和 Hashtable 的区别](#hashmap-和-hashtable-的区别) - - [HashMap 和 HashSet区别](#hashmap-和-hashset区别) - - [HashSet如何检查重复](#hashset如何检查重复) - - [HashMap的底层实现](#hashmap的底层实现) - - [JDK1.8之前](#jdk18之前) - - [JDK1.8之后](#jdk18之后) - - [HashMap 的长度为什么是2的幂次方](#hashmap-的长度为什么是2的幂次方) - - [HashMap 多线程操作导致死循环问题](#hashmap-多线程操作导致死循环问题) - - [ConcurrentHashMap 和 Hashtable 的区别](#concurrenthashmap-和-hashtable-的区别) - - [ConcurrentHashMap线程安全的具体实现方式/底层具体实现](#concurrenthashmap线程安全的具体实现方式底层具体实现) - - [JDK1.7(上面有示意图)](#jdk17上面有示意图) - - [JDK1.8 (上面有示意图)](#jdk18-上面有示意图) - - [comparable 和 Comparator的区别](#comparable-和-comparator的区别) - - [Comparator定制排序](#comparator定制排序) - - [重写compareTo方法实现按年龄来排序](#重写compareto方法实现按年龄来排序) - - [集合框架底层数据结构总结](#集合框架底层数据结构总结) - - [Collection](#collection) - - [1. List](#1-list) - - [2. Set](#2-set) - - [Map](#map) - - [如何选用集合?](#如何选用集合) - - - -# 剖析面试最常见问题之Java集合框架 - -## 说说List,Set,Map三者的区别? - -- **List(对付顺序的好帮手):** List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 -- **Set(注重独一无二的性质):** 不允许重复的集合。不会有多个元素引用相同的对象。 -- **Map(用Key来搜索的专家):** 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。 - -## Arraylist 与 LinkedList 区别? - -- **1. 是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; - -- **2. 底层数据结构:** `Arraylist` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) - -- **3. 插入和删除是否受元素位置的影响:** ① **`ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e) `方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element) `)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **`LinkedList` 采用链表存储,所以对于`add(E e)`方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置`i`插入和删除元素的话(`(add(int index, E element)`) 时间复杂度近似为`o(n))`因为需要先移动到指定位置再插入。** - -- **4. 是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList` 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index) `方法)。 - -- **5. 内存空间占用:** ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。 - -### **补充内容:RandomAccess接口** - -```java -public interface RandomAccess { -} -``` - -查看源码我们发现实际上 `RandomAccess` 接口中什么都没有定义。所以,在我看来 `RandomAccess` 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 - -在 `binarySearch()` 方法中,它要判断传入的list 是否 `RamdomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法 - -```java - public static - int binarySearch(List> list, T key) { - if (list instanceof RandomAccess || list.size() MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + - loadFactor); - this.loadFactor = loadFactor; - this.threshold = tableSizeFor(initialCapacity); - } - public HashMap(int initialCapacity) { - this(initialCapacity, DEFAULT_LOAD_FACTOR); - } -``` - -下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。 - -```java - /** - * Returns a power of two size for the given target capacity. - */ - static final int tableSizeFor(int cap) { - int n = cap - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; - } -``` - -## HashMap 和 HashSet区别 - -如果你看过 `HashSet` 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 `clone() `、`writeObject()`、`readObject()`是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。 - -| HashMap | HashSet | -| :------------------------------: | :----------------------------------------------------------: | -| 实现了Map接口 | 实现Set接口 | -| 存储键值对 | 仅存储对象 | -| 调用 `put()`向map中添加元素 | 调用 `add()`方法向Set中添加元素 | -| HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性, | - -## HashSet如何检查重复 - -当你把对象加入`HashSet`时,HashSet会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用`equals()`方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。(摘自我的Java启蒙书《Head fist java》第二版) - -**hashCode()与equals()的相关规定:** - -1. 如果两个对象相等,则hashcode一定也是相同的 -2. 两个对象相等,对两个equals方法返回true -3. 两个对象有相同的hashcode值,它们也不一定是相等的 -4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 - -**==与equals的区别** - -1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 -2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较 -3. ==指引用是否相同 equals()指的是值是否相同 - -## HashMap的底层实现 - -### JDK1.8之前 - -JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** - -**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** - -**JDK 1.8 HashMap 的 hash 方法源码:** - -JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 - -```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^ :按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } -``` - -对比一下 JDK1.7的 HashMap 的 hash 方法源码. - -```java -static int hash(int h) { - // This function ensures that hashCodes that differ only by - // constant multiples at each bit position have a bounded - // number of collisions (approximately 8 at default load factor). - - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); -} -``` - -相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 - -所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 - -![jdk1.8之前的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/jdk1.8之前的内部结构-HashMap.jpg) - -### JDK1.8之后 - -相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 - -![jdk1.8之后的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8之后的HashMap底层数据结构.jpg) - -> TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 - -**推荐阅读:** - -- 《Java 8系列之重新认识HashMap》 : - -## HashMap 的长度为什么是2的幂次方 - -为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。 - -**这个算法应该如何设计呢?** - -我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。** - -## HashMap 多线程操作导致死循环问题 - -主要原因在于并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。 - -详情请查看: - -## ConcurrentHashMap 和 Hashtable 的区别 - -ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 - -- **底层数据结构:** JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; -- **实现线程安全的方式(重要):** ① **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 - -**两者的对比图:** - -图片来源: - -**HashTable:** - -![HashTable全表锁](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/HashTable全表锁.png) - -**JDK1.7的ConcurrentHashMap:** - -![JDK1.7的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ConcurrentHashMap分段锁.jpg) - -**JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):** - -![JDK1.8的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8-ConcurrentHashMap-Structure.jpg) - -## ConcurrentHashMap线程安全的具体实现方式/底层具体实现 - -### JDK1.7(上面有示意图) - -首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 - -**ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成**。 - -Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。 - -```java -static class Segment extends ReentrantLock implements Serializable { -} -``` - -一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 - -### JDK1.8 (上面有示意图) - -ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N))) - -synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。 - -## comparable 和 Comparator的区别 - -- comparable接口实际上是出自java.lang包 它有一个 `compareTo(Object obj)`方法用来排序 -- comparator接口实际上是出自 java.util 包它有一个`compare(Object obj1, Object obj2)`方法用来排序 - -一般我们需要对一个集合使用自定义排序时,我们就要重写`compareTo()`方法或`compare()`方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写`compareTo()`方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 `Collections.sort()`. - -### Comparator定制排序 - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - System.out.println("原始数组:"); - System.out.println(arrayList); - // void reverse(List list):反转 - Collections.reverse(arrayList); - System.out.println("Collections.reverse(arrayList):"); - System.out.println(arrayList); - - // void sort(List list),按自然排序的升序排序 - Collections.sort(arrayList); - System.out.println("Collections.sort(arrayList):"); - System.out.println(arrayList); - // 定制排序的用法 - Collections.sort(arrayList, new Comparator() { - - @Override - public int compare(Integer o1, Integer o2) { - return o2.compareTo(o1); - } - }); - System.out.println("定制排序后:"); - System.out.println(arrayList); -``` - -Output: - -``` -原始数组: -[-1, 3, 3, -5, 7, 4, -9, -7] -Collections.reverse(arrayList): -[-7, -9, 4, 7, -5, 3, 3, -1] -Collections.sort(arrayList): -[-9, -7, -5, -1, 3, 3, 4, 7] -定制排序后: -[7, 4, 3, 3, -1, -5, -7, -9] -``` - -### 重写compareTo方法实现按年龄来排序 - -```java -// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列 -// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他 -// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了 - -public class Person implements Comparable { - private String name; - private int age; - - public Person(String name, int age) { - super(); - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - /** - * TODO重写compareTo方法实现按年龄来排序 - */ - @Override - public int compareTo(Person o) { - // TODO Auto-generated method stub - if (this.age > o.getAge()) { - return 1; - } else if (this.age < o.getAge()) { - return -1; - } - return age; - } -} - -``` - -```java - public static void main(String[] args) { - TreeMap pdata = new TreeMap(); - pdata.put(new Person("张三", 30), "zhangsan"); - pdata.put(new Person("李四", 20), "lisi"); - pdata.put(new Person("王五", 10), "wangwu"); - pdata.put(new Person("小红", 5), "xiaohong"); - // 得到key的值的同时得到key所对应的值 - Set keys = pdata.keySet(); - for (Person key : keys) { - System.out.println(key.getAge() + "-" + key.getName()); - - } - } -``` - -Output: - -``` -5-小红 -10-王五 -20-李四 -30-张三 -``` - -## 集合框架底层数据结构总结 - -### Collection - -#### 1. List - -- **Arraylist:** Object数组 -- **Vector:** Object数组 -- **LinkedList:** 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) - -#### 2. Set - -- **HashSet(无序,唯一):** 基于 HashMap 实现的,底层采用 HashMap 来保存元素 -- **LinkedHashSet:** LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的 -- **TreeSet(有序,唯一):** 红黑树(自平衡的排序二叉树) - -### Map - -- **HashMap:** JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间 -- **LinkedHashMap:** LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931) -- **Hashtable:** 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 -- **TreeMap:** 红黑树(自平衡的排序二叉树) - -## 如何选用集合? - -主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/collection/LinkedList.md b/docs/java/collection/LinkedList.md deleted file mode 100644 index d26bc752267..00000000000 --- a/docs/java/collection/LinkedList.md +++ /dev/null @@ -1,515 +0,0 @@ - - - -- [简介](#简介) -- [内部结构分析](#内部结构分析) -- [LinkedList源码分析](#linkedlist源码分析) - - [构造方法](#构造方法) - - [添加(add)方法](#add方法) - - [根据位置取数据的方法](#根据位置取数据的方法) - - [根据对象得到索引的方法](#根据对象得到索引的方法) - - [检查链表是否包含某对象的方法:](#检查链表是否包含某对象的方法:) - - [删除(remove/pop)方法](#删除方法) -- [LinkedList类常用方法测试:](#linkedlist类常用方法测试) - - - -## 简介 -LinkedList是一个实现了List接口Deque接口双端链表。 -LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; -LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法: -```java -List list=Collections.synchronizedList(new LinkedList(...)); -``` -## 内部结构分析 -**如下图所示:** -![LinkedList内部结构](https://user-gold-cdn.xitu.io/2018/3/19/1623e363fe0450b0?w=600&h=481&f=jpeg&s=18502) -看完了图之后,我们再看LinkedList类中的一个**内部私有类Node**就很好理解了: -```java -private static class Node { - E item;//节点值 - Node next;//后继节点 - Node prev;//前驱节点 - - Node(Node prev, E element, Node next) { - this.item = element; - this.next = next; - this.prev = prev; - } - } -``` -这个类就代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。 - -## LinkedList源码分析 -### 构造方法 -**空构造方法:** -```java - public LinkedList() { - } -``` -**用已有的集合创建链表的构造方法:** -```java - public LinkedList(Collection c) { - this(); - addAll(c); - } -``` -### add方法 -**add(E e)** 方法:将元素添加到链表尾部 -```java -public boolean add(E e) { - linkLast(e);//这里就只调用了这一个方法 - return true; - } -``` - -```java - /** - * 链接使e作为最后一个元素。 - */ - void linkLast(E e) { - final Node l = last; - final Node newNode = new Node<>(l, e, null); - last = newNode;//新建节点 - if (l == null) - first = newNode; - else - l.next = newNode;//指向后继元素也就是指向下一个元素 - size++; - modCount++; - } -``` -**add(int index,E e)**:在指定位置添加元素 -```java -public void add(int index, E element) { - checkPositionIndex(index); //检查索引是否处于[0-size]之间 - - if (index == size)//添加在链表尾部 - linkLast(element); - else//添加在链表中间 - linkBefore(element, node(index)); - } -``` -linkBefore方法需要给定两个参数,一个插入节点的值,一个指定的node,所以我们又调用了Node(index)去找到index对应的node - -**addAll(Collection c ):将集合插入到链表尾部** - -```java -public boolean addAll(Collection c) { - return addAll(size, c); - } -``` -**addAll(int index, Collection c):** 将集合从指定位置开始插入 -```java -public boolean addAll(int index, Collection c) { - //1:检查index范围是否在size之内 - checkPositionIndex(index); - - //2:toArray()方法把集合的数据存到对象数组中 - Object[] a = c.toArray(); - int numNew = a.length; - if (numNew == 0) - return false; - - //3:得到插入位置的前驱节点和后继节点 - Node pred, succ; - //如果插入位置为尾部,前驱节点为last,后继节点为null - if (index == size) { - succ = null; - pred = last; - } - //否则,调用node()方法得到后继节点,再得到前驱节点 - else { - succ = node(index); - pred = succ.prev; - } - - // 4:遍历数据将数据插入 - for (Object o : a) { - @SuppressWarnings("unchecked") E e = (E) o; - //创建新节点 - Node newNode = new Node<>(pred, e, null); - //如果插入位置在链表头部 - if (pred == null) - first = newNode; - else - pred.next = newNode; - pred = newNode; - } - - //如果插入位置在尾部,重置last节点 - if (succ == null) { - last = pred; - } - //否则,将插入的链表与先前链表连接起来 - else { - pred.next = succ; - succ.prev = pred; - } - - size += numNew; - modCount++; - return true; - } -``` -上面可以看出addAll方法通常包括下面四个步骤: -1. 检查index范围是否在size之内 -2. toArray()方法把集合的数据存到对象数组中 -3. 得到插入位置的前驱和后继节点 -4. 遍历数据,将数据插入到指定位置 - -**addFirst(E e):** 将元素添加到链表头部 -```java - public void addFirst(E e) { - linkFirst(e); - } -``` -```java -private void linkFirst(E e) { - final Node f = first; - final Node newNode = new Node<>(null, e, f);//新建节点,以头节点为后继节点 - first = newNode; - //如果链表为空,last节点也指向该节点 - if (f == null) - last = newNode; - //否则,将头节点的前驱指针指向新节点,也就是指向前一个元素 - else - f.prev = newNode; - size++; - modCount++; - } -``` -**addLast(E e):** 将元素添加到链表尾部,与 **add(E e)** 方法一样 -```java -public void addLast(E e) { - linkLast(e); - } -``` -### 根据位置取数据的方法 -**get(int index):** 根据指定索引返回数据 -```java -public E get(int index) { - //检查index范围是否在size之内 - checkElementIndex(index); - //调用Node(index)去找到index对应的node然后返回它的值 - return node(index).item; - } -``` -**获取头节点(index=0)数据方法:** -```java -public E getFirst() { - final Node f = first; - if (f == null) - throw new NoSuchElementException(); - return f.item; - } -public E element() { - return getFirst(); - } -public E peek() { - final Node f = first; - return (f == null) ? null : f.item; - } - -public E peekFirst() { - final Node f = first; - return (f == null) ? null : f.item; - } -``` -**区别:** -getFirst(),element(),peek(),peekFirst() -这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中**getFirst()** 和**element()** 方法将会在链表为空时,抛出异常 - -element()方法的内部就是使用getFirst()实现的。它们会在链表为空时,抛出NoSuchElementException -**获取尾节点(index=-1)数据方法:** -```java - public E getLast() { - final Node l = last; - if (l == null) - throw new NoSuchElementException(); - return l.item; - } - public E peekLast() { - final Node l = last; - return (l == null) ? null : l.item; - } -``` -**两者区别:** -**getLast()** 方法在链表为空时,会抛出**NoSuchElementException**,而**peekLast()** 则不会,只是会返回 **null**。 -### 根据对象得到索引的方法 -**int indexOf(Object o):** 从头遍历找 -```java -public int indexOf(Object o) { - int index = 0; - if (o == null) { - //从头遍历 - for (Node x = first; x != null; x = x.next) { - if (x.item == null) - return index; - index++; - } - } else { - //从头遍历 - for (Node x = first; x != null; x = x.next) { - if (o.equals(x.item)) - return index; - index++; - } - } - return -1; - } -``` -**int lastIndexOf(Object o):** 从尾遍历找 -```java -public int lastIndexOf(Object o) { - int index = size; - if (o == null) { - //从尾遍历 - for (Node x = last; x != null; x = x.prev) { - index--; - if (x.item == null) - return index; - } - } else { - //从尾遍历 - for (Node x = last; x != null; x = x.prev) { - index--; - if (o.equals(x.item)) - return index; - } - } - return -1; - } -``` -### 检查链表是否包含某对象的方法: -**contains(Object o):** 检查对象o是否存在于链表中 -```java - public boolean contains(Object o) { - return indexOf(o) != -1; - } -``` -### 删除方法 -**remove()** ,**removeFirst(),pop():** 删除头节点 -``` -public E pop() { - return removeFirst(); - } -public E remove() { - return removeFirst(); - } -public E removeFirst() { - final Node f = first; - if (f == null) - throw new NoSuchElementException(); - return unlinkFirst(f); - } -``` -**removeLast(),pollLast():** 删除尾节点 -```java -public E removeLast() { - final Node l = last; - if (l == null) - throw new NoSuchElementException(); - return unlinkLast(l); - } -public E pollLast() { - final Node l = last; - return (l == null) ? null : unlinkLast(l); - } -``` -**区别:** removeLast()在链表为空时将抛出NoSuchElementException,而pollLast()方法返回null。 - -**remove(Object o):** 删除指定元素 -```java -public boolean remove(Object o) { - //如果删除对象为null - if (o == null) { - //从头开始遍历 - for (Node x = first; x != null; x = x.next) { - //找到元素 - if (x.item == null) { - //从链表中移除找到的元素 - unlink(x); - return true; - } - } - } else { - //从头开始遍历 - for (Node x = first; x != null; x = x.next) { - //找到元素 - if (o.equals(x.item)) { - //从链表中移除找到的元素 - unlink(x); - return true; - } - } - } - return false; - } -``` -当删除指定对象时,只需调用remove(Object o)即可,不过该方法一次只会删除一个匹配的对象,如果删除了匹配对象,返回true,否则false。 - -unlink(Node x) 方法: -```java -E unlink(Node x) { - // assert x != null; - final E element = x.item; - final Node next = x.next;//得到后继节点 - final Node prev = x.prev;//得到前驱节点 - - //删除前驱指针 - if (prev == null) { - first = next;//如果删除的节点是头节点,令头节点指向该节点的后继节点 - } else { - prev.next = next;//将前驱节点的后继节点指向后继节点 - x.prev = null; - } - - //删除后继指针 - if (next == null) { - last = prev;//如果删除的节点是尾节点,令尾节点指向该节点的前驱节点 - } else { - next.prev = prev; - x.next = null; - } - - x.item = null; - size--; - modCount++; - return element; - } -``` -**remove(int index)**:删除指定位置的元素 -```java -public E remove(int index) { - //检查index范围 - checkElementIndex(index); - //将节点删除 - return unlink(node(index)); - } -``` -## LinkedList类常用方法测试 - -```java -package list; - -import java.util.Iterator; -import java.util.LinkedList; - -public class LinkedListDemo { - public static void main(String[] srgs) { - //创建存放int类型的linkedList - LinkedList linkedList = new LinkedList<>(); - /************************** linkedList的基本操作 ************************/ - linkedList.addFirst(0); // 添加元素到列表开头 - linkedList.add(1); // 在列表结尾添加元素 - linkedList.add(2, 2); // 在指定位置添加元素 - linkedList.addLast(3); // 添加元素到列表结尾 - - System.out.println("LinkedList(直接输出的): " + linkedList); - - System.out.println("getFirst()获得第一个元素: " + linkedList.getFirst()); // 返回此列表的第一个元素 - System.out.println("getLast()获得第最后一个元素: " + linkedList.getLast()); // 返回此列表的最后一个元素 - System.out.println("removeFirst()删除第一个元素并返回: " + linkedList.removeFirst()); // 移除并返回此列表的第一个元素 - System.out.println("removeLast()删除最后一个元素并返回: " + linkedList.removeLast()); // 移除并返回此列表的最后一个元素 - System.out.println("After remove:" + linkedList); - System.out.println("contains()方法判断列表是否包含1这个元素:" + linkedList.contains(1)); // 判断此列表包含指定元素,如果是,则返回true - System.out.println("该linkedList的大小 : " + linkedList.size()); // 返回此列表的元素个数 - - /************************** 位置访问操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.set(1, 3); // 将此列表中指定位置的元素替换为指定的元素 - System.out.println("After set(1, 3):" + linkedList); - System.out.println("get(1)获得指定位置(这里为1)的元素: " + linkedList.get(1)); // 返回此列表中指定位置处的元素 - - /************************** Search操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.add(3); - System.out.println("indexOf(3): " + linkedList.indexOf(3)); // 返回此列表中首次出现的指定元素的索引 - System.out.println("lastIndexOf(3): " + linkedList.lastIndexOf(3));// 返回此列表中最后出现的指定元素的索引 - - /************************** Queue操作 ************************/ - System.out.println("-----------------------------------------"); - System.out.println("peek(): " + linkedList.peek()); // 获取但不移除此列表的头 - System.out.println("element(): " + linkedList.element()); // 获取但不移除此列表的头 - linkedList.poll(); // 获取并移除此列表的头 - System.out.println("After poll():" + linkedList); - linkedList.remove(); - System.out.println("After remove():" + linkedList); // 获取并移除此列表的头 - linkedList.offer(4); - System.out.println("After offer(4):" + linkedList); // 将指定元素添加到此列表的末尾 - - /************************** Deque操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.offerFirst(2); // 在此列表的开头插入指定的元素 - System.out.println("After offerFirst(2):" + linkedList); - linkedList.offerLast(5); // 在此列表末尾插入指定的元素 - System.out.println("After offerLast(5):" + linkedList); - System.out.println("peekFirst(): " + linkedList.peekFirst()); // 获取但不移除此列表的第一个元素 - System.out.println("peekLast(): " + linkedList.peekLast()); // 获取但不移除此列表的第一个元素 - linkedList.pollFirst(); // 获取并移除此列表的第一个元素 - System.out.println("After pollFirst():" + linkedList); - linkedList.pollLast(); // 获取并移除此列表的最后一个元素 - System.out.println("After pollLast():" + linkedList); - linkedList.push(2); // 将元素推入此列表所表示的堆栈(插入到列表的头) - System.out.println("After push(2):" + linkedList); - linkedList.pop(); // 从此列表所表示的堆栈处弹出一个元素(获取并移除列表第一个元素) - System.out.println("After pop():" + linkedList); - linkedList.add(3); - linkedList.removeFirstOccurrence(3); // 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表) - System.out.println("After removeFirstOccurrence(3):" + linkedList); - linkedList.removeLastOccurrence(3); // 从此列表中移除最后一次出现的指定元素(从尾部到头部遍历列表) - System.out.println("After removeFirstOccurrence(3):" + linkedList); - - /************************** 遍历操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.clear(); - for (int i = 0; i < 100000; i++) { - linkedList.add(i); - } - // 迭代器遍历 - long start = System.currentTimeMillis(); - Iterator iterator = linkedList.iterator(); - while (iterator.hasNext()) { - iterator.next(); - } - long end = System.currentTimeMillis(); - System.out.println("Iterator:" + (end - start) + " ms"); - - // 顺序遍历(随机遍历) - start = System.currentTimeMillis(); - for (int i = 0; i < linkedList.size(); i++) { - linkedList.get(i); - } - end = System.currentTimeMillis(); - System.out.println("for:" + (end - start) + " ms"); - - // 另一种for循环遍历 - start = System.currentTimeMillis(); - for (Integer i : linkedList) - ; - end = System.currentTimeMillis(); - System.out.println("for2:" + (end - start) + " ms"); - - // 通过pollFirst()或pollLast()来遍历LinkedList - LinkedList temp1 = new LinkedList<>(); - temp1.addAll(linkedList); - start = System.currentTimeMillis(); - while (temp1.size() != 0) { - temp1.pollFirst(); - } - end = System.currentTimeMillis(); - System.out.println("pollFirst()或pollLast():" + (end - start) + " ms"); - - // 通过removeFirst()或removeLast()来遍历LinkedList - LinkedList temp2 = new LinkedList<>(); - temp2.addAll(linkedList); - start = System.currentTimeMillis(); - while (temp2.size() != 0) { - temp2.removeFirst(); - } - end = System.currentTimeMillis(); - System.out.println("removeFirst()或removeLast():" + (end - start) + " ms"); - } -} -``` diff --git a/docs/java/collection/ArrayList.md b/docs/java/collection/arraylist-source-code.md similarity index 53% rename from docs/java/collection/ArrayList.md rename to docs/java/collection/arraylist-source-code.md index f6578a7a784..36dfcbdbfc8 100644 --- a/docs/java/collection/ArrayList.md +++ b/docs/java/collection/arraylist-source-code.md @@ -1,34 +1,43 @@ - +--- +title: ArrayList 源码+扩容机制分析 +category: Java +tag: + - Java集合 +--- -- [ArrayList简介](#arraylist简介) -- [ArrayList核心源码](#arraylist核心源码) -- [ArrayList源码分析](#arraylist源码分析) - - [System.arraycopy\(\)和Arrays.copyOf\(\)方法](#systemarraycopy和arrayscopyof方法) - - [两者联系与区别](#两者联系与区别) - - [ArrayList核心扩容技术](#arraylist核心扩容技术) - - [内部类](#内部类) -- [ArrayList经典Demo](#arraylist经典demo) - +## 1. ArrayList 简介 +`ArrayList` 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用`ensureCapacity`操作来增加 `ArrayList` 实例的容量。这可以减少递增式再分配的数量。 -### ArrayList简介 -  ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用`ensureCapacity`操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。 - - 它继承于 **AbstractList**,实现了 **List**, **RandomAccess**, **Cloneable**, **java.io.Serializable** 这些接口。 - - 在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为**O(n)**,求表长以及增加元素,取第 i 元素的时间复杂度为**O(1)** +`ArrayList`继承于 **`AbstractList`** ,实现了 **`List`**, **`RandomAccess`**, **`Cloneable`**, **`java.io.Serializable`** 这些接口。 -  ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。 +```java + +public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable{ + + } +``` + +- `RandomAccess` 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 +- `ArrayList` 实现了 **`Cloneable` 接口** ,即覆盖了函数`clone()`,能被克隆。 +- `ArrayList` 实现了 `java.io.Serializable`接口,这意味着`ArrayList`支持序列化,能通过序列化去传输。 -  ArrayList 实现了**RandomAccess 接口**, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 +### 1.1. Arraylist 和 Vector 的区别? -  ArrayList 实现了**Cloneable 接口**,即覆盖了函数 clone(),**能被克隆**。 +1. `ArrayList` 是 `List` 的主要实现类,底层使用 `Object[ ]`存储,适用于频繁的查找工作,线程不安全 ; +2. `Vector` 是 `List` 的古老实现类,底层使用 `Object[ ]`存储,线程安全的。 -  ArrayList 实现**java.io.Serializable 接口**,这意味着ArrayList**支持序列化**,**能通过序列化去传输**。 +### 1.2. Arraylist 与 LinkedList 区别? -  和 Vector 不同,**ArrayList 中的操作不是线程安全的**!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。 -### ArrayList核心源码 +1. **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; +2. **底层数据结构:** `Arraylist` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) +3. **插入和删除是否受元素位置的影响:** ① **`ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e)`方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **`LinkedList` 采用链表存储,所以对于`add(E e)`方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置`i`插入和删除元素的话(`(add(int index, E element)`) 时间复杂度近似为`o(n))`因为需要先移动到指定位置再插入。** +4. **是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList` 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 +5. **内存空间占用:** `ArrayList` 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 `LinkedList` 的空间花费则体现在它的每一个元素都需要消耗比 `ArrayList` 更多的空间(因为要存放直接后继和直接前驱以及数据)。 + +## 2. ArrayList 核心源码解读 ```java package java.util; @@ -68,23 +77,25 @@ public class ArrayList extends AbstractList private int size; /** - * 带初始容量参数的构造函数。(用户自己指定容量) + * 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小) */ public ArrayList(int initialCapacity) { if (initialCapacity > 0) { - //创建initialCapacity大小的数组 + //如果传入的参数大于0,创建initialCapacity大小的数组 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { - //创建空数组 + //如果传入的参数等于0,创建空数组 this.elementData = EMPTY_ELEMENTDATA; } else { + //其他情况,抛出异常 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } /** - *默认构造函数,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10 + *默认无参构造函数 + *DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10 */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; @@ -94,22 +105,22 @@ public class ArrayList extends AbstractList * 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。 */ public ArrayList(Collection c) { - // + //将指定集合转换为数组 elementData = c.toArray(); - //如果指定集合元素个数不为0 + //如果elementData数组的长度不为0 if ((size = elementData.length) != 0) { - // c.toArray 可能返回的不是Object类型的数组所以加上下面的语句用于判断, - //这里用到了反射里面的getClass()方法 + // 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断) if (elementData.getClass() != Object[].class) + //将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组 elementData = Arrays.copyOf(elementData, size, Object[].class); } else { - // 用空数组代替 + // 其他情况,用空数组代替 this.elementData = EMPTY_ELEMENTDATA; } } /** - * 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。 + * 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。 */ public void trimToSize() { modCount++; @@ -127,13 +138,14 @@ public class ArrayList extends AbstractList * @param minCapacity 所需的最小容量 */ public void ensureCapacity(int minCapacity) { + //如果是true,minExpand的值为0,如果是false,minExpand的值为10 int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; - + //如果最小容量大于已有的最大容量 if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } @@ -141,7 +153,7 @@ public class ArrayList extends AbstractList //得到最小扩容量 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取默认的容量和传入参数的较大值 + // 获取“默认的容量”和“传入参数”两者之间的最大值 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } @@ -192,7 +204,7 @@ public class ArrayList extends AbstractList } /** - *返回此列表中的元素数。 + *返回此列表中的元素数。 */ public int size() { return size; @@ -210,12 +222,12 @@ public class ArrayList extends AbstractList * 如果此列表包含指定的元素,则返回true 。 */ public boolean contains(Object o) { - //indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 + //indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 return indexOf(o) >= 0; } /** - *返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 + *返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 */ public int indexOf(Object o) { if (o == null) { @@ -248,7 +260,7 @@ public class ArrayList extends AbstractList } /** - * 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。) + * 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。) */ public Object clone() { try { @@ -264,7 +276,7 @@ public class ArrayList extends AbstractList } /** - *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 + *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 *返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。 *因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。 */ @@ -273,11 +285,11 @@ public class ArrayList extends AbstractList } /** - * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); - *返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。 - *否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。 + * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); + *返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。 + *否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。 *如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。 - *(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。) + *(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。) */ @SuppressWarnings("unchecked") public T[] toArray(T[] a) { @@ -308,7 +320,7 @@ public class ArrayList extends AbstractList } /** - * 用指定的元素替换此列表中指定位置的元素。 + * 用指定的元素替换此列表中指定位置的元素。 */ public E set(int index, E element) { //对index进行界限检查 @@ -321,7 +333,7 @@ public class ArrayList extends AbstractList } /** - * 将指定的元素追加到此列表的末尾。 + * 将指定的元素追加到此列表的末尾。 */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! @@ -331,7 +343,7 @@ public class ArrayList extends AbstractList } /** - * 在此列表中的指定位置插入指定的元素。 + * 在此列表中的指定位置插入指定的元素。 *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 */ @@ -347,7 +359,7 @@ public class ArrayList extends AbstractList } /** - * 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。 + * 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。 */ public E remove(int index) { rangeCheck(index); @@ -360,7 +372,7 @@ public class ArrayList extends AbstractList System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work - //从列表中删除的元素 + //从列表中删除的元素 return oldValue; } @@ -399,7 +411,7 @@ public class ArrayList extends AbstractList } /** - * 从列表中删除所有元素。 + * 从列表中删除所有元素。 */ public void clear() { modCount++; @@ -485,7 +497,7 @@ public class ArrayList extends AbstractList } /** - * 从此列表中删除指定集合中包含的所有元素。 + * 从此列表中删除指定集合中包含的所有元素。 */ public boolean removeAll(Collection c) { Objects.requireNonNull(c); @@ -495,7 +507,7 @@ public class ArrayList extends AbstractList /** * 仅保留此列表中包含在指定集合中的元素。 - *换句话说,从此列表中删除其中不包含在指定集合中的所有元素。 + *换句话说,从此列表中删除其中不包含在指定集合中的所有元素。 */ public boolean retainAll(Collection c) { Objects.requireNonNull(c); @@ -505,8 +517,8 @@ public class ArrayList extends AbstractList /** * 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。 - *指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。 - *返回的列表迭代器是fail-fast 。 + *指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。 + *返回的列表迭代器是fail-fast 。 */ public ListIterator listIterator(int index) { if (index < 0 || index > size) @@ -515,7 +527,7 @@ public class ArrayList extends AbstractList } /** - *返回列表中的列表迭代器(按适当的顺序)。 + *返回列表中的列表迭代器(按适当的顺序)。 *返回的列表迭代器是fail-fast 。 */ public ListIterator listIterator() { @@ -523,75 +535,103 @@ public class ArrayList extends AbstractList } /** - *以正确的顺序返回该列表中的元素的迭代器。 - *返回的迭代器是fail-fast 。 + *以正确的顺序返回该列表中的元素的迭代器。 + *返回的迭代器是fail-fast 。 */ public Iterator iterator() { return new Itr(); } - + ``` -### ArrayList源码分析 -#### System.arraycopy()和Arrays.copyOf()方法 -  通过上面源码我们发现这两个实现数组复制的方法被广泛使用而且很多地方都特别巧妙。比如下面add(int index, E element)方法就很巧妙的用到了arraycopy()方法让数组自己复制自己实现让index开始之后的所有成员后移一个位置: -```java - /** - * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 + +## 3. ArrayList 扩容机制分析 + +### 3.1. 先从 ArrayList 的构造函数说起 + +**(JDK8)ArrayList 有三种方式来初始化,构造方法源码如下:** + +```java + /** + * 默认初始容量大小 */ - public void add(int index, E element) { - rangeCheckForAdd(index); + private static final int DEFAULT_CAPACITY = 10; - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()方法实现数组自己复制自己 - //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; - System.arraycopy(elementData, index, elementData, index + 1, size - index); - elementData[index] = element; - size++; + + private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; + + /** + *默认构造函数,使用初始容量10构造一个空列表(无参数构造) + */ + public ArrayList() { + this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } -``` -又如toArray()方法中用到了copyOf()方法 -```java /** - *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 - *返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。 - *因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。 + * 带初始容量参数的构造函数。(用户自己指定容量) */ - public Object[] toArray() { - //elementData:要复制的数组;size:要复制的长度 - return Arrays.copyOf(elementData, size); + public ArrayList(int initialCapacity) { + if (initialCapacity > 0) {//初始容量大于0 + //创建initialCapacity大小的数组 + this.elementData = new Object[initialCapacity]; + } else if (initialCapacity == 0) {//初始容量等于0 + //创建空数组 + this.elementData = EMPTY_ELEMENTDATA; + } else {//初始容量小于0,抛出异常 + throw new IllegalArgumentException("Illegal Capacity: "+ + initialCapacity); + } + } + + + /** + *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回 + *如果指定的集合为null,throws NullPointerException。 + */ + public ArrayList(Collection c) { + elementData = c.toArray(); + if ((size = elementData.length) != 0) { + // c.toArray might (incorrectly) not return Object[] (see 6260652) + if (elementData.getClass() != Object[].class) + elementData = Arrays.copyOf(elementData, size, Object[].class); + } else { + // replace with empty array. + this.elementData = EMPTY_ELEMENTDATA; + } } + ``` -##### 两者联系与区别 -**联系:** -看两者源代码可以发现`copyOf()`内部调用了`System.arraycopy()`方法 -**区别:** -1. arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置 -2. copyOf()是系统自动在内部新建一个数组,并返回该数组。 -#### ArrayList 核心扩容技术 + +细心的同学一定会发现 :**以无参数构造方法创建 `ArrayList` 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。** 下面在我们分析 ArrayList 扩容时会讲到这一点内容! + +> 补充:JDK6 new 无参构造的 `ArrayList` 对象时,直接创建了长度是 10 的 `Object[]` 数组 elementData 。 + +### 3.2. 一步一步分析 ArrayList 扩容机制 + +这里以无参构造函数创建的 ArrayList 为例分析 + +#### 3.2.1. 先来看 `add` 方法 + ```java -//下面是ArrayList的扩容机制 -//ArrayList的扩容机制提高了性能,如果每次只扩充一个, -//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。 /** - * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量 - * @param minCapacity 所需的最小容量 + * 将指定的元素追加到此列表的末尾。 */ - public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } + public boolean add(E e) { + //添加元素之前,先调用ensureCapacityInternal方法 + ensureCapacityInternal(size + 1); // Increments modCount!! + //这里看到ArrayList添加元素的实质就相当于为数组赋值 + elementData[size++] = e; + return true; } +``` + +> **注意** :JDK11 移除了 `ensureCapacityInternal()` 和 `ensureExplicitCapacity()` 方法 + +#### 3.2.2. 再来看看 `ensureCapacityInternal()` 方法 + +(JDK7)可以看到 `add` 方法 首先调用了`ensureCapacityInternal(size + 1)` + +```java //得到最小扩容量 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { @@ -601,25 +641,49 @@ public class ArrayList extends AbstractList ensureExplicitCapacity(minCapacity); } - //判断是否需要扩容,上面两个方法都要调用 +``` + +**当 要 add 进第 1 个元素时,minCapacity 为 1,在 Math.max()方法比较后,minCapacity 为 10。** + +> 此处和后续 JDK8 代码格式化略有不同,核心代码基本一样。 + +#### 3.2.3. `ensureExplicitCapacity()` 方法 + +如果调用 `ensureCapacityInternal()` 方法就一定会进入(执行)这个方法,下面我们来研究一下这个方法的源码! + +```java + //判断是否需要扩容 private void ensureExplicitCapacity(int minCapacity) { modCount++; - // 如果说minCapacity也就是所需的最小容量大于保存ArrayList数据的数组的长度的话,就需要调用grow(minCapacity)方法扩容。 - //这个minCapacity到底为多少呢?举个例子在添加元素(add)方法中这个minCapacity的大小就为现在数组的长度加1 + // overflow-conscious code if (minCapacity - elementData.length > 0) //调用grow方法进行扩容,调用此方法代表已经开始扩容了 grow(minCapacity); } ``` + +我们来仔细分析一下: + +- 当我们要 add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list),因为执行了 `ensureCapacityInternal()` 方法 ,所以 minCapacity 此时为 10。此时,`minCapacity - elementData.length > 0`成立,所以会进入 `grow(minCapacity)` 方法。 +- 当 add 第 2 个元素时,minCapacity 为 2,此时 e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,`minCapacity - elementData.length > 0` 不成立,所以不会进入 (执行)`grow(minCapacity)` 方法。 +- 添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。 + +直到添加第 11 个元素,minCapacity(为 11)比 elementData.length(为 10)要大。进入 grow 方法进行扩容。 + +#### 3.2.4. `grow()` 方法 + ```java + /** + * 要分配的最大数组大小 + */ + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + /** * ArrayList扩容的核心方法。 */ private void grow(int minCapacity) { - //elementData为保存ArrayList数据的数组 - ///elementData.length求数组长度elementData.size是求数组中的元素个数 // oldCapacity为旧容量,newCapacity为新容量 int oldCapacity = elementData.length; //将oldCapacity 右移一位,其效果相当于oldCapacity /2, @@ -628,110 +692,251 @@ public class ArrayList extends AbstractList //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, if (newCapacity - minCapacity < 0) newCapacity = minCapacity; - //再检查新容量是否超出了ArrayList所定义的最大容量, - //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, - //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 + // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE, + //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } - ``` -  扩容机制代码已经做了详细的解释。另外值得注意的是大家很容易忽略的一个运算符:**移位运算符** -  **简介**:移位运算符就是在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)>>(带符号右移)>>>(无符号右移)。 -  **作用**:**对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源** -  比如这里:int newCapacity = oldCapacity + (oldCapacity >> 1); -右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。 -**另外需要注意的是:** +**int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)!** 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数. + +> ">>"(移位运算符):>>1 右移一位相当于除 2,右移 n 位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了 1 位所以相当于 oldCapacity /2。对于大数据的 2 进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源 + +**我们再来通过例子探究一下`grow()` 方法 :** + +- 当 add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入 `hugeCapacity` 方法。数组容量为 10,add 方法中 return true,size 增为 1。 +- 当 add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。 +- 以此类推······ + +**这里补充一点比较重要,但是容易被忽视掉的知识点:** + +- java 中的 `length`属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. +- java 中的 `length()` 方法是针对字符串说的,如果想看这个字符串的长度则用到 `length()` 这个方法. +- java 中的 `size()` 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! + +#### 3.2.5. `hugeCapacity()` 方法。 + +从上面 `grow()` 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果 minCapacity 大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 + +```java + private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) // overflow + throw new OutOfMemoryError(); + //对minCapacity和MAX_ARRAY_SIZE进行比较 + //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小 + //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小 + //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + return (minCapacity > MAX_ARRAY_SIZE) ? + Integer.MAX_VALUE : + MAX_ARRAY_SIZE; + } +``` -1. java 中的**length 属性**是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. +### 3.3. `System.arraycopy()` 和 `Arrays.copyOf()`方法 -2. java 中的**length()方法**是针对字 符串String说的,如果想看这个字符串的长度则用到 length()这个方法. +阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及`add(int index, E element)`、`toArray()` 等方法中都用到了该方法! -3. .java 中的**size()方法**是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! +#### 3.3.1. `System.arraycopy()` 方法 +源码: -#### 内部类 ```java - (1)private class Itr implements Iterator - (2)private class ListItr extends Itr implements ListIterator - (3)private class SubList extends AbstractList implements RandomAccess - (4)static final class ArrayListSpliterator implements Spliterator + // 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义 + /** + * 复制数组 + * @param src 源数组 + * @param srcPos 源数组中的起始位置 + * @param dest 目标数组 + * @param destPos 目标数组中的起始位置 + * @param length 要复制的数组元素的数量 + */ + public static native void arraycopy(Object src, int srcPos, + Object dest, int destPos, + int length); ``` -  ArrayList有四个内部类,其中的**Itr是实现了Iterator接口**,同时重写了里面的**hasNext()**, **next()**, **remove()** 等方法;其中的**ListItr** 继承 **Itr**,实现了**ListIterator接口**,同时重写了**hasPrevious()**, **nextIndex()**, **previousIndex()**, **previous()**, **set(E e)**, **add(E e)** 等方法,所以这也可以看出了 **Iterator和ListIterator的区别:** ListIterator在Iterator的基础上增加了添加对象,修改对象,逆向遍历等方法,这些是Iterator不能实现的。 -### ArrayList经典Demo + +场景: ```java -package list; -import java.util.ArrayList; -import java.util.Iterator; - -public class ArrayListDemo { - - public static void main(String[] srgs){ - ArrayList arrayList = new ArrayList(); - - System.out.printf("Before add:arrayList.size() = %d\n",arrayList.size()); - - arrayList.add(1); - arrayList.add(3); - arrayList.add(5); - arrayList.add(7); - arrayList.add(9); - System.out.printf("After add:arrayList.size() = %d\n",arrayList.size()); - - System.out.println("Printing elements of arrayList"); - // 三种遍历方式打印元素 - // 第一种:通过迭代器遍历 - System.out.print("通过迭代器遍历:"); - Iterator it = arrayList.iterator(); - while(it.hasNext()){ - System.out.print(it.next() + " "); - } - System.out.println(); - - // 第二种:通过索引值遍历 - System.out.print("通过索引值遍历:"); - for(int i = 0; i < arrayList.size(); i++){ - System.out.print(arrayList.get(i) + " "); - } - System.out.println(); - - // 第三种:for循环遍历 - System.out.print("for循环遍历:"); - for(Integer number : arrayList){ - System.out.print(number + " "); - } - - // toArray用法 - // 第一种方式(最常用) - Integer[] integer = arrayList.toArray(new Integer[0]); - - // 第二种方式(容易理解) - Integer[] integer1 = new Integer[arrayList.size()]; - arrayList.toArray(integer1); - - // 抛出异常,java不支持向下转型 - //Integer[] integer2 = new Integer[arrayList.size()]; - //integer2 = arrayList.toArray(); - System.out.println(); - - // 在指定位置添加元素 - arrayList.add(2,2); - // 删除指定位置上的元素 - arrayList.remove(2); - // 删除指定元素 - arrayList.remove((Object)3); - // 判断arrayList是否包含5 - System.out.println("ArrayList contains 5 is: " + arrayList.contains(5)); - - // 清空ArrayList - arrayList.clear(); - // 判断ArrayList是否为空 - System.out.println("ArrayList is empty: " + arrayList.isEmpty()); + /** + * 在此列表中的指定位置插入指定的元素。 + *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; + *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 + */ + public void add(int index, E element) { + rangeCheckForAdd(index); + + ensureCapacityInternal(size + 1); // Increments modCount!! + //arraycopy()方法实现数组自己复制自己 + //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; + System.arraycopy(elementData, index, elementData, index + 1, size - index); + elementData[index] = element; + size++; } +``` + +我们写一个简单的方法测试以下: + +```java +public class ArraycopyTest { + + public static void main(String[] args) { + // TODO Auto-generated method stub + int[] a = new int[10]; + a[0] = 0; + a[1] = 1; + a[2] = 2; + a[3] = 3; + System.arraycopy(a, 2, a, 3, 3); + a[2]=99; + for (int i = 0; i < a.length; i++) { + System.out.print(a[i] + " "); + } + } + } ``` +结果: + +``` +0 1 99 2 3 0 0 0 0 0 +``` + +#### 3.3.2. `Arrays.copyOf()`方法 + +源码: + +```java + public static int[] copyOf(int[] original, int newLength) { + // 申请一个新的数组 + int[] copy = new int[newLength]; + // 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组 + System.arraycopy(original, 0, copy, 0, + Math.min(original.length, newLength)); + return copy; + } +``` + +场景: + +```java + /** + 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 + */ + public Object[] toArray() { + //elementData:要复制的数组;size:要复制的长度 + return Arrays.copyOf(elementData, size); + } +``` + +个人觉得使用 `Arrays.copyOf()`方法主要是为了给原有数组扩容,测试代码如下: + +```java +public class ArrayscopyOfTest { + + public static void main(String[] args) { + int[] a = new int[3]; + a[0] = 0; + a[1] = 1; + a[2] = 2; + int[] b = Arrays.copyOf(a, 10); + System.out.println("b.length"+b.length); + } +} +``` + +结果: + +``` +10 +``` + +#### 3.3.3. 两者联系和区别 + +**联系:** + +看两者源代码可以发现 `copyOf()`内部实际调用了 `System.arraycopy()` 方法 + +**区别:** + +`arraycopy()` 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 `copyOf()` 是系统自动在内部新建一个数组,并返回该数组。 + +### 3.4. `ensureCapacity`方法 + +ArrayList 源码中有一个 `ensureCapacity` 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢? + +```java + /** + 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。 + * + * @param minCapacity 所需的最小容量 + */ + public void ensureCapacity(int minCapacity) { + int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) + // any size if not default element table + ? 0 + // larger than default for default empty table. It's already + // supposed to be at default size. + : DEFAULT_CAPACITY; + + if (minCapacity > minExpand) { + ensureExplicitCapacity(minCapacity); + } + } + +``` + +**最好在 add 大量元素之前用 `ensureCapacity` 方法,以减少增量重新分配的次数** + +我们通过下面的代码实际测试以下这个方法的效果: + +```java +public class EnsureCapacityTest { + public static void main(String[] args) { + ArrayList list = new ArrayList(); + final int N = 10000000; + long startTime = System.currentTimeMillis(); + for (int i = 0; i < N; i++) { + list.add(i); + } + long endTime = System.currentTimeMillis(); + System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); + + } +} +``` + +运行结果: + +``` +使用ensureCapacity方法前:2158 +``` + +```java +public class EnsureCapacityTest { + public static void main(String[] args) { + ArrayList list = new ArrayList(); + final int N = 10000000; + list = new ArrayList(); + long startTime1 = System.currentTimeMillis(); + list.ensureCapacity(N); + for (int i = 0; i < N; i++) { + list.add(i); + } + long endTime1 = System.currentTimeMillis(); + System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1)); + } +} +``` + +运行结果: + +``` +使用ensureCapacity方法后:1773 +``` + +通过运行结果,我们可以看出向 ArrayList 添加大量元素之前最好先使用`ensureCapacity` 方法,以减少增量重新分配的次数。 diff --git a/docs/java/collection/concurrent-hash-map-source-code.md b/docs/java/collection/concurrent-hash-map-source-code.md new file mode 100644 index 00000000000..6d765697df9 --- /dev/null +++ b/docs/java/collection/concurrent-hash-map-source-code.md @@ -0,0 +1,594 @@ +--- +title: ConcurrentHashMap源码+底层数据结构分析 +category: Java +tag: + - Java集合 +--- + + +> 本文来自公众号:末读代码的投稿,原文地址:https://mp.weixin.qq.com/s/AHWzboztt53ZfFZmsSnMSw 。 + +上一篇文章介绍了 HashMap 源码,反响不错,也有很多同学发表了自己的观点,这次又来了,这次是 `ConcurrentHashMap ` 了,作为线程安全的HashMap ,它的使用频率也是很高。那么它的存储结构和实现原理是怎么样的呢? + +## 1. ConcurrentHashMap 1.7 + +### 1. 存储结构 + +> 下图存在一个笔误 Segmeng -> Segment + +![Java 7 ConcurrentHashMap 存储结构](./images/image-20200405151029416.png) + +Java 7 中 `ConcurrentHashMap` 的存储结构如上图,`ConcurrnetHashMap` 由很多个 `Segment` 组合,而每一个 `Segment` 是一个类似于 HashMap 的结构,所以每一个 `HashMap` 的内部可以进行扩容。但是 `Segment` 的个数一旦**初始化就不能改变**,默认 `Segment` 的个数是 16 个,你也可以认为 `ConcurrentHashMap` 默认支持最多 16 个线程并发。 + +### 2. 初始化 + +通过 ConcurrentHashMap 的无参构造探寻 ConcurrentHashMap 的初始化流程。 + +```java + /** + * Creates a new, empty map with a default initial capacity (16), + * load factor (0.75) and concurrencyLevel (16). + */ + public ConcurrentHashMap() { + this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); + } +``` + +无参构造中调用了有参构造,传入了三个参数的默认值,他们的值是。 + +```java + /** + * 默认初始化容量 + */ + static final int DEFAULT_INITIAL_CAPACITY = 16; + + /** + * 默认负载因子 + */ + static final float DEFAULT_LOAD_FACTOR = 0.75f; + + /** + * 默认并发级别 + */ + static final int DEFAULT_CONCURRENCY_LEVEL = 16; +``` + +接着看下这个有参构造函数的内部实现逻辑。 + +```java +@SuppressWarnings("unchecked") +public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) { + // 参数校验 + if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) + throw new IllegalArgumentException(); + // 校验并发级别大小,大于 1<<16,重置为 65536 + if (concurrencyLevel > MAX_SEGMENTS) + concurrencyLevel = MAX_SEGMENTS; + // Find power-of-two sizes best matching arguments + // 2的多少次方 + int sshift = 0; + int ssize = 1; + // 这个循环可以找到 concurrencyLevel 之上最近的 2的次方值 + while (ssize < concurrencyLevel) { + ++sshift; + ssize <<= 1; + } + // 记录段偏移量 + this.segmentShift = 32 - sshift; + // 记录段掩码 + this.segmentMask = ssize - 1; + // 设置容量 + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + // c = 容量 / ssize ,默认 16 / 16 = 1,这里是计算每个 Segment 中的类似于 HashMap 的容量 + int c = initialCapacity / ssize; + if (c * ssize < initialCapacity) + ++c; + int cap = MIN_SEGMENT_TABLE_CAPACITY; + //Segment 中的类似于 HashMap 的容量至少是2或者2的倍数 + while (cap < c) + cap <<= 1; + // create segments and segments[0] + // 创建 Segment 数组,设置 segments[0] + Segment s0 = new Segment(loadFactor, (int)(cap * loadFactor), + (HashEntry[])new HashEntry[cap]); + Segment[] ss = (Segment[])new Segment[ssize]; + UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] + this.segments = ss; +} +``` + +总结一下在 Java 7 中 ConcurrnetHashMap 的初始化逻辑。 + +1. 必要参数校验。 +2. 校验并发级别 concurrencyLevel 大小,如果大于最大值,重置为最大值。无参构造**默认值是 16.** +3. 寻找并发级别 concurrencyLevel 之上最近的 **2 的幂次方**值,作为初始化容量大小,**默认是 16**。 +4. 记录 segmentShift 偏移量,这个值为【容量 = 2 的N次方】中的 N,在后面 Put 时计算位置时会用到。**默认是 32 - sshift = 28**. +5. 记录 segmentMask,默认是 ssize - 1 = 16 -1 = 15. +6. **初始化 segments[0]**,**默认大小为 2**,**负载因子 0.75**,**扩容阀值是 2*0.75=1.5**,插入第二个值时才会进行扩容。 + +### 3. put + +接着上面的初始化参数继续查看 put 方法源码。 + +```java +/** + * Maps the specified key to the specified value in this table. + * Neither the key nor the value can be null. + * + *

The value can be retrieved by calling the get method + * with a key that is equal to the original key. + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * @return the previous value associated with key, or + * null if there was no mapping for key + * @throws NullPointerException if the specified key or value is null + */ +public V put(K key, V value) { + Segment s; + if (value == null) + throw new NullPointerException(); + int hash = hash(key); + // hash 值无符号右移 28位(初始化时获得),然后与 segmentMask=15 做与运算 + // 其实也就是把高4位与segmentMask(1111)做与运算 + int j = (hash >>> segmentShift) & segmentMask; + if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck + (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment + // 如果查找到的 Segment 为空,初始化 + s = ensureSegment(j); + return s.put(key, hash, value, false); +} + +/** + * Returns the segment for the given index, creating it and + * recording in segment table (via CAS) if not already present. + * + * @param k the index + * @return the segment + */ +@SuppressWarnings("unchecked") +private Segment ensureSegment(int k) { + final Segment[] ss = this.segments; + long u = (k << SSHIFT) + SBASE; // raw offset + Segment seg; + // 判断 u 位置的 Segment 是否为null + if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u)) == null) { + Segment proto = ss[0]; // use segment 0 as prototype + // 获取0号 segment 里的 HashEntry 初始化长度 + int cap = proto.table.length; + // 获取0号 segment 里的 hash 表里的扩容负载因子,所有的 segment 的 loadFactor 是相同的 + float lf = proto.loadFactor; + // 计算扩容阀值 + int threshold = (int)(cap * lf); + // 创建一个 cap 容量的 HashEntry 数组 + HashEntry[] tab = (HashEntry[])new HashEntry[cap]; + if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck + // 再次检查 u 位置的 Segment 是否为null,因为这时可能有其他线程进行了操作 + Segment s = new Segment(lf, threshold, tab); + // 自旋检查 u 位置的 Segment 是否为null + while ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u)) + == null) { + // 使用CAS 赋值,只会成功一次 + if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) + break; + } + } + } + return seg; +} +``` + +上面的源码分析了 ConcurrentHashMap 在 put 一个数据时的处理流程,下面梳理下具体流程。 + +1. 计算要 put 的 key 的位置,获取指定位置的 Segment。 + +2. 如果指定位置的 Segment 为空,则初始化这个 Segment. + + **初始化 Segment 流程:** + + 1. 检查计算得到的位置的 Segment 是否为null. + 2. 为 null 继续初始化,使用 Segment[0] 的容量和负载因子创建一个 HashEntry 数组。 + 3. 再次检查计算得到的指定位置的 Segment 是否为null. + 4. 使用创建的 HashEntry 数组初始化这个 Segment. + 5. 自旋判断计算得到的指定位置的 Segment 是否为null,使用 CAS 在这个位置赋值为 Segment. + +3. Segment.put 插入 key,value 值。 + +上面探究了获取 Segment 段和初始化 Segment 段的操作。最后一行的 Segment 的 put 方法还没有查看,继续分析。 + +```java +final V put(K key, int hash, V value, boolean onlyIfAbsent) { + // 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取。 + HashEntry node = tryLock() ? null : scanAndLockForPut(key, hash, value); + V oldValue; + try { + HashEntry[] tab = table; + // 计算要put的数据位置 + int index = (tab.length - 1) & hash; + // CAS 获取 index 坐标的值 + HashEntry first = entryAt(tab, index); + for (HashEntry e = first;;) { + if (e != null) { + // 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 value + K k; + if ((k = e.key) == key || + (e.hash == hash && key.equals(k))) { + oldValue = e.value; + if (!onlyIfAbsent) { + e.value = value; + ++modCount; + } + break; + } + e = e.next; + } + else { + // first 有值没说明 index 位置已经有值了,有冲突,链表头插法。 + if (node != null) + node.setNext(first); + else + node = new HashEntry(hash, key, value, first); + int c = count + 1; + // 容量大于扩容阀值,小于最大容量,进行扩容 + if (c > threshold && tab.length < MAXIMUM_CAPACITY) + rehash(node); + else + // index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头 + setEntryAt(tab, index, node); + ++modCount; + count = c; + oldValue = null; + break; + } + } + } finally { + unlock(); + } + return oldValue; +} +``` + +由于 Segment 继承了 ReentrantLock,所以 Segment 内部可以很方便的获取锁,put 流程就用到了这个功能。 + +1. tryLock() 获取锁,获取不到使用 **`scanAndLockForPut`** 方法继续获取。 + +2. 计算 put 的数据要放入的 index 位置,然后获取这个位置上的 HashEntry 。 + +3. 遍历 put 新元素,为什么要遍历?因为这里获取的 HashEntry 可能是一个空元素,也可能是链表已存在,所以要区别对待。 + + 如果这个位置上的 **HashEntry 不存在**: + + 1. 如果当前容量大于扩容阀值,小于最大容量,**进行扩容**。 + 2. 直接头插法插入。 + + 如果这个位置上的 **HashEntry 存在**: + + 1. 判断链表当前元素 Key 和 hash 值是否和要 put 的 key 和 hash 值一致。一致则替换值 + 2. 不一致,获取链表下一个节点,直到发现相同进行值替换,或者链表表里完毕没有相同的。 + 1. 如果当前容量大于扩容阀值,小于最大容量,**进行扩容**。 + 2. 直接链表头插法插入。 + +4. 如果要插入的位置之前已经存在,替换后返回旧值,否则返回 null. + +这里面的第一步中的 scanAndLockForPut 操作这里没有介绍,这个方法做的操作就是不断的自旋 `tryLock()` 获取锁。当自旋次数大于指定次数时,使用 `lock()` 阻塞获取锁。在自旋时顺表获取下 hash 位置的 HashEntry。 + +```java +private HashEntry scanAndLockForPut(K key, int hash, V value) { + HashEntry first = entryForHash(this, hash); + HashEntry e = first; + HashEntry node = null; + int retries = -1; // negative while locating node + // 自旋获取锁 + while (!tryLock()) { + HashEntry f; // to recheck first below + if (retries < 0) { + if (e == null) { + if (node == null) // speculatively create node + node = new HashEntry(hash, key, value, null); + retries = 0; + } + else if (key.equals(e.key)) + retries = 0; + else + e = e.next; + } + else if (++retries > MAX_SCAN_RETRIES) { + // 自旋达到指定次数后,阻塞等到只到获取到锁 + lock(); + break; + } + else if ((retries & 1) == 0 && + (f = entryForHash(this, hash)) != first) { + e = first = f; // re-traverse if entry changed + retries = -1; + } + } + return node; +} + +``` + +### 4. 扩容 rehash + +ConcurrentHashMap 的扩容只会扩容到原来的两倍。老数组里的数据移动到新的数组时,位置要么不变,要么变为 index+ oldSize,参数里的 node 会在扩容之后使用链表**头插法**插入到指定位置。 + +```java +private void rehash(HashEntry node) { + HashEntry[] oldTable = table; + // 老容量 + int oldCapacity = oldTable.length; + // 新容量,扩大两倍 + int newCapacity = oldCapacity << 1; + // 新的扩容阀值 + threshold = (int)(newCapacity * loadFactor); + // 创建新的数组 + HashEntry[] newTable = (HashEntry[]) new HashEntry[newCapacity]; + // 新的掩码,默认2扩容后是4,-1是3,二进制就是11。 + int sizeMask = newCapacity - 1; + for (int i = 0; i < oldCapacity ; i++) { + // 遍历老数组 + HashEntry e = oldTable[i]; + if (e != null) { + HashEntry next = e.next; + // 计算新的位置,新的位置只可能是不便或者是老的位置+老的容量。 + int idx = e.hash & sizeMask; + if (next == null) // Single node on list + // 如果当前位置还不是链表,只是一个元素,直接赋值 + newTable[idx] = e; + else { // Reuse consecutive sequence at same slot + // 如果是链表了 + HashEntry lastRun = e; + int lastIdx = idx; + // 新的位置只可能是不便或者是老的位置+老的容量。 + // 遍历结束后,lastRun 后面的元素位置都是相同的 + for (HashEntry last = next; last != null; last = last.next) { + int k = last.hash & sizeMask; + if (k != lastIdx) { + lastIdx = k; + lastRun = last; + } + } + // ,lastRun 后面的元素位置都是相同的,直接作为链表赋值到新位置。 + newTable[lastIdx] = lastRun; + // Clone remaining nodes + for (HashEntry p = e; p != lastRun; p = p.next) { + // 遍历剩余元素,头插法到指定 k 位置。 + V v = p.value; + int h = p.hash; + int k = h & sizeMask; + HashEntry n = newTable[k]; + newTable[k] = new HashEntry(h, p.key, v, n); + } + } + } + } + // 头插法插入新的节点 + int nodeIndex = node.hash & sizeMask; // add the new node + node.setNext(newTable[nodeIndex]); + newTable[nodeIndex] = node; + table = newTable; +} +``` + +有些同学可能会对最后的两个 for 循环有疑惑,这里第一个 for 是为了寻找这样一个节点,这个节点后面的所有 next 节点的新位置都是相同的。然后把这个作为一个链表赋值到新位置。第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表。这样实现的原因可能是基于概率统计,有深入研究的同学可以发表下意见。 + +### 5. get + +到这里就很简单了,get 方法只需要两步即可。 + +1. 计算得到 key 的存放位置。 +2. 遍历指定位置查找相同 key 的 value 值。 + +```java +public V get(Object key) { + Segment s; // manually integrate access methods to reduce overhead + HashEntry[] tab; + int h = hash(key); + long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; + // 计算得到 key 的存放位置 + if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && + (tab = s.table) != null) { + for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile + (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); + e != null; e = e.next) { + // 如果是链表,遍历查找到相同 key 的 value。 + K k; + if ((k = e.key) == key || (e.hash == h && key.equals(k))) + return e.value; + } + } + return null; +} +``` + +## 2. ConcurrentHashMap 1.8 + +### 1. 存储结构 + +![Java8 ConcurrentHashMap 存储结构(图片来自 javadoop)](./images/java8_concurrenthashmap.png) + +可以发现 Java8 的 ConcurrentHashMap 相对于 Java7 来说变化比较大,不再是之前的 **Segment 数组 + HashEntry 数组 + 链表**,而是 **Node 数组 + 链表 / 红黑树**。当冲突链表达到一定长度时,链表会转换成红黑树。 + +### 2. 初始化 initTable + +```java +/** + * Initializes table, using the size recorded in sizeCtl. + */ +private final Node[] initTable() { + Node[] tab; int sc; + while ((tab = table) == null || tab.length == 0) { + // 如果 sizeCtl < 0 ,说明另外的线程执行CAS 成功,正在进行初始化。 + if ((sc = sizeCtl) < 0) + // 让出 CPU 使用权 + Thread.yield(); // lost initialization race; just spin + else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { + try { + if ((tab = table) == null || tab.length == 0) { + int n = (sc > 0) ? sc : DEFAULT_CAPACITY; + @SuppressWarnings("unchecked") + Node[] nt = (Node[])new Node[n]; + table = tab = nt; + sc = n - (n >>> 2); + } + } finally { + sizeCtl = sc; + } + break; + } + } + return tab; +} +``` + +从源码中可以发现 ConcurrentHashMap 的初始化是通过**自旋和 CAS** 操作完成的。里面需要注意的是变量 `sizeCtl` ,它的值决定着当前的初始化状态。 + +1. -1 说明正在初始化 +2. -N 说明有N-1个线程正在进行扩容 +3. 表示 table 初始化大小,如果 table 没有初始化 +4. 表示 table 容量,如果 table 已经初始化。 + +### 3. put + +直接过一遍 put 源码。 + +```java +public V put(K key, V value) { + return putVal(key, value, false); +} + +/** Implementation for put and putIfAbsent */ +final V putVal(K key, V value, boolean onlyIfAbsent) { + // key 和 value 不能为空 + if (key == null || value == null) throw new NullPointerException(); + int hash = spread(key.hashCode()); + int binCount = 0; + for (Node[] tab = table;;) { + // f = 目标位置元素 + Node f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值 + if (tab == null || (n = tab.length) == 0) + // 数组桶为空,初始化数组桶(自旋+CAS) + tab = initTable(); + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { + // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出 + if (casTabAt(tab, i, null,new Node(hash, key, value, null))) + break; // no lock when adding to empty bin + } + else if ((fh = f.hash) == MOVED) + tab = helpTransfer(tab, f); + else { + V oldVal = null; + // 使用 synchronized 加锁加入节点 + synchronized (f) { + if (tabAt(tab, i) == f) { + // 说明是链表 + if (fh >= 0) { + binCount = 1; + // 循环加入新的或者覆盖节点 + for (Node e = f;; ++binCount) { + K ek; + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + oldVal = e.val; + if (!onlyIfAbsent) + e.val = value; + break; + } + Node pred = e; + if ((e = e.next) == null) { + pred.next = new Node(hash, key, + value, null); + break; + } + } + } + else if (f instanceof TreeBin) { + // 红黑树 + Node p; + binCount = 2; + if ((p = ((TreeBin)f).putTreeVal(hash, key, + value)) != null) { + oldVal = p.val; + if (!onlyIfAbsent) + p.val = value; + } + } + } + } + if (binCount != 0) { + if (binCount >= TREEIFY_THRESHOLD) + treeifyBin(tab, i); + if (oldVal != null) + return oldVal; + break; + } + } + } + addCount(1L, binCount); + return null; +} +``` + +1. 根据 key 计算出 hashcode 。 + +2. 判断是否需要进行初始化。 + +3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 + +4. 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。 + +5. 如果都不满足,则利用 synchronized 锁写入数据。 + +6. 如果数量大于 `TREEIFY_THRESHOLD` 则要转换为红黑树。 + +### 4. get + +get 流程比较简单,直接过一遍源码。 + +```java +public V get(Object key) { + Node[] tab; Node e, p; int n, eh; K ek; + // key 所在的 hash 位置 + int h = spread(key.hashCode()); + if ((tab = table) != null && (n = tab.length) > 0 && + (e = tabAt(tab, (n - 1) & h)) != null) { + // 如果指定位置元素存在,头结点hash值相同 + if ((eh = e.hash) == h) { + if ((ek = e.key) == key || (ek != null && key.equals(ek))) + // key hash 值相等,key值相同,直接返回元素 value + return e.val; + } + else if (eh < 0) + // 头结点hash值小于0,说明正在扩容或者是红黑树,find查找 + return (p = e.find(h, key)) != null ? p.val : null; + while ((e = e.next) != null) { + // 是链表,遍历查找 + if (e.hash == h && + ((ek = e.key) == key || (ek != null && key.equals(ek)))) + return e.val; + } + } + return null; +} +``` + +总结一下 get 过程: + +1. 根据 hash 值计算位置。 +2. 查找到指定位置,如果头节点就是要找的,直接返回它的 value. +3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。 +4. 如果是链表,遍历查找之。 + +总结: + +总的来说 ConcurrentHashMap 在 Java8 中相对于 Java7 来说变化还是挺大的, + +## 3. 总结 + +Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。 + +Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 **Segment 数组 + HashEntry 数组 + 链表** 进化成了 **Node 数组 + 链表 / 红黑树**,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。 + +有些同学可能对 Synchronized 的性能存在疑问,其实 Synchronized 锁自从引入锁升级策略后,性能不再是问题,有兴趣的同学可以自己了解下 Synchronized 的**锁升级**。 diff --git a/docs/java/collection/HashMap.md b/docs/java/collection/hashmap-source-code.md similarity index 73% rename from docs/java/collection/HashMap.md rename to docs/java/collection/hashmap-source-code.md index c365065c13b..7996b2766d1 100644 --- a/docs/java/collection/HashMap.md +++ b/docs/java/collection/hashmap-source-code.md @@ -1,45 +1,48 @@ - +--- +title: HashMap源码+底层数据结构分析 +category: Java +tag: + - Java集合 +--- -- [HashMap 简介](#hashmap-简介) -- [底层数据结构分析](#底层数据结构分析) - - [JDK1.8之前](#jdk18之前) - - [JDK1.8之后](#jdk18之后) -- [HashMap源码分析](#hashmap源码分析) - - [构造方法](#构造方法) - - [put方法](#put方法) - - [get方法](#get方法) - - [resize方法](#resize方法) -- [HashMap常用方法测试](#hashmap常用方法测试) - - > 感谢 [changfubai](https://github.com/changfubai) 对本文的改进做出的贡献! ## HashMap 简介 -HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。 -JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间,具体可以参考 `treeifyBin`方法。 +HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。 + + `HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个 + +JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 + +`HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, `HashMap` 总是使用 2 的幂作为哈希表的大小。 ## 底层数据结构分析 -### JDK1.8之前 -JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** -**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** +### JDK1.8 之前 + +JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。 + +HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 + +所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。 **JDK 1.8 HashMap 的 hash 方法源码:** -JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 +JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 - ```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^ :按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } - ``` -对比一下 JDK1.7的 HashMap 的 hash 方法源码. +```java + static final int hash(Object key) { + int h; + // key.hashCode():返回散列值也就是hashcode + // ^ :按位异或 + // >>>:无符号右移,忽略符号位,空位都以0补齐 + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); + } +``` + +对比一下 JDK1.7 的 HashMap 的 hash 方法源码. ```java static int hash(int h) { @@ -58,55 +61,60 @@ static int hash(int h) { ![jdk1.8之前的内部结构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/jdk1.8之前的内部结构.png) -### JDK1.8之后 -相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 +### JDK1.8 之后 + +相比于之前的版本,JDK1.8 以后在解决哈希冲突时有了较大的变化。 + +当链表长度大于阈值(默认为 8)时,会首先调用 `treeifyBin()`方法。这个方法会根据 HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行 `resize()` 方法对数组扩容。相关源码这里就不贴了,重点关注 `treeifyBin()`方法即可! -![JDK1.8之后的HashMap底层数据结构](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/67233764.jpg) +![](https://oscimg.oschina.net/oscnet/up-bba283228693dae74e78da1ef7a9a04c684.png) **类的属性:** + ```java public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { // 序列号 - private static final long serialVersionUID = 362498820763181265L; + private static final long serialVersionUID = 362498820763181265L; // 默认的初始容量是16 - static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 - static final int MAXIMUM_CAPACITY = 1 << 30; + static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的填充因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 当桶(bucket)上的结点数大于这个值时会转成红黑树 - static final int TREEIFY_THRESHOLD = 8; + static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中结构转化为红黑树对应的table的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; // 存储元素的数组,总是2的幂次倍 - transient Node[] table; + transient Node[] table; // 存放具体元素的集 transient Set> entrySet; // 存放元素的个数,注意这个不等于数组的长度。 transient int size; // 每次扩容和更改map结构的计数器 - transient int modCount; + transient int modCount; // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 int threshold; // 加载因子 final float loadFactor; } ``` -- **loadFactor加载因子** - loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。 +- **loadFactor 加载因子** + + loadFactor 加载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。 - **loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值**。 - - 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。 + **loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值**。 + + 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 \* 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。 - **threshold** - **threshold = capacity * loadFactor**,**当Size>=threshold**的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 **衡量数组是否需要扩增的一个标准**。 + **threshold = capacity \* loadFactor**,**当 Size>=threshold**的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 **衡量数组是否需要扩增的一个标准**。 -**Node节点类源码:** +**Node 节点类源码:** ```java // 继承自 Map.Entry @@ -149,7 +157,9 @@ static class Node implements Map.Entry { } } ``` + **树节点类源码:** + ```java static final class TreeNode extends LinkedHashMap.Entry { TreeNode parent; // 父 @@ -168,7 +178,9 @@ static final class TreeNode extends LinkedHashMap.Entry { r = p; } ``` -## HashMap源码分析 + +## HashMap 源码分析 + ### 构造方法 HashMap 中有四个构造方法,它们分别如下: @@ -178,18 +190,18 @@ HashMap 中有四个构造方法,它们分别如下: public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } - + // 包含另一个“Map”的构造函数 public HashMap(Map m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false);//下面会分析到这个方法 } - + // 指定“容量大小”的构造函数 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } - + // 指定“容量大小”和“加载因子”的构造函数 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) @@ -203,7 +215,7 @@ HashMap 中有四个构造方法,它们分别如下: } ``` -**putMapEntries方法:** +**putMapEntries 方法:** ```java final void putMapEntries(Map m, boolean evict) { @@ -231,17 +243,22 @@ final void putMapEntries(Map m, boolean evict) { } } ``` -### put方法 -HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。 -**对putVal方法添加元素的分析如下:** +### put 方法 + +HashMap 只提供了 put 用于添加元素,putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。 + +**对 putVal 方法添加元素的分析如下:** -- ①如果定位到的数组位置没有元素 就直接插入。 -- ②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用`e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value)`将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。 +1. 如果定位到的数组位置没有元素 就直接插入。 +2. 如果定位到的数组位置有元素就和要插入的 key 比较,如果 key 相同就直接覆盖,如果 key 不相同,就判断 p 是否是一个树节点,如果是就调用`e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value)`将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。 -ps:下图有一个小问题,来自 [issue#608](https://github.com/Snailclimb/JavaGuide/issues/608)指出:直接覆盖之后应该就会 return,不会有后续操作。参考 JDK8 HashMap.java 658 行。 +![ ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/put方法.png) -![put方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/put方法.png) +说明:上图有两个小问题: + +- 直接覆盖之后应该就会 return,不会有后续操作。参考 JDK8 HashMap.java 658 行([issue#608](https://github.com/Snailclimb/JavaGuide/issues/608))。 +- 当链表长度大于阈值(默认为 8)并且 HashMap 数组长度超过 64 的时候才会执行链表转红黑树的操作,否则就只是对数组扩容。参考 HashMap 的 `treeifyBin()` 方法([issue#1087](https://github.com/Snailclimb/JavaGuide/issues/1087))。 ```java public V put(K key, V value) { @@ -277,7 +294,9 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, if ((e = p.next) == null) { // 在尾部插入新结点 p.next = newNode(hash, key, value, null); - // 结点数量达到阈值,转化为红黑树 + // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法 + // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。 + // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 跳出循环 @@ -293,7 +312,7 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, } } // 表示在桶中找到key值、hash值与插入元素相等的结点 - if (e != null) { + if (e != null) { // 记录e的value V oldValue = e.value; // onlyIfAbsent为false或者旧值为null @@ -314,21 +333,21 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, // 插入后回调 afterNodeInsertion(evict); return null; -} +} ``` -**我们再来对比一下 JDK1.7 put方法的代码** +**我们再来对比一下 JDK1.7 put 方法的代码** -**对于put方法的分析如下:** +**对于 put 方法的分析如下:** -- ①如果定位到的数组位置没有元素 就直接插入。 -- ②如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key相同就直接覆盖,不同就采用头插法插入元素。 +- ① 如果定位到的数组位置没有元素 就直接插入。 +- ② 如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的 key 比较,如果 key 相同就直接覆盖,不同就采用头插法插入元素。 ```java public V put(K key, V value) - if (table == EMPTY_TABLE) { - inflateTable(threshold); -} + if (table == EMPTY_TABLE) { + inflateTable(threshold); +} if (key == null) return putForNullKey(value); int hash = hash(key); @@ -339,7 +358,7 @@ public V put(K key, V value) V oldValue = e.value; e.value = value; e.recordAccess(this); - return oldValue; + return oldValue; } } @@ -349,9 +368,8 @@ public V put(K key, V value) } ``` +### get 方法 - -### get方法 ```java public V get(Object key) { Node e; @@ -382,8 +400,11 @@ final Node getNode(int hash, Object key) { return null; } ``` -### resize方法 -进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。 + +### resize 方法 + +进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。 + ```java final Node[] resize() { Node[] oldTab = table; @@ -402,7 +423,7 @@ final Node[] resize() { } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; - else { + else { // signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); @@ -426,7 +447,7 @@ final Node[] resize() { newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode)e).split(this, newTab, j, oldCap); - else { + else { Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; @@ -466,7 +487,9 @@ final Node[] resize() { return newTab; } ``` -## HashMap常用方法测试 + +## HashMap 常用方法测试 + ```java package map; @@ -512,7 +535,7 @@ public class HashMapDemo { } /** - * 如果既要遍历key又要value,那么建议这种方式,应为如果先获取keySet然后再执行map.get(key),map内部会执行两次遍历。 + * 如果既要遍历key又要value,那么建议这种方式,因为如果先获取keySet然后再执行map.get(key),map内部会执行两次遍历。 * 一次是在获取keySet的时候,一次是在遍历所有key的时候。 */ // 当我调用put(key,value)方法的时候,首先会把key和value封装到 @@ -523,7 +546,7 @@ public class HashMapDemo { for (java.util.Map.Entry entry : entrys) { System.out.println(entry.getKey() + "--" + entry.getValue()); } - + /** * HashMap其他常用方法 */ @@ -539,5 +562,4 @@ public class HashMapDemo { } } - -``` +``` \ No newline at end of file diff --git a/docs/java/collection/images/77c95eb733284dbd8ce4e85c9cb6b042.png b/docs/java/collection/images/77c95eb733284dbd8ce4e85c9cb6b042.png new file mode 100644 index 00000000000..54180092b12 Binary files /dev/null and b/docs/java/collection/images/77c95eb733284dbd8ce4e85c9cb6b042.png differ diff --git a/docs/java/collection/images/Java-Collections.jpeg b/docs/java/collection/images/Java-Collections.jpeg new file mode 100644 index 00000000000..cf9071ff6a2 Binary files /dev/null and b/docs/java/collection/images/Java-Collections.jpeg differ diff --git "a/docs/java/collection/images/TreeMap\347\273\247\346\211\277\347\273\223\346\236\204.png" "b/docs/java/collection/images/TreeMap\347\273\247\346\211\277\347\273\223\346\236\204.png" new file mode 100644 index 00000000000..553e41b84ac Binary files /dev/null and "b/docs/java/collection/images/TreeMap\347\273\247\346\211\277\347\273\223\346\236\204.png" differ diff --git a/docs/java/collection/images/ad28e3ba-e419-4724-869c-73879e604da1.png b/docs/java/collection/images/ad28e3ba-e419-4724-869c-73879e604da1.png new file mode 100644 index 00000000000..1c05ebaad67 Binary files /dev/null and b/docs/java/collection/images/ad28e3ba-e419-4724-869c-73879e604da1.png differ diff --git a/docs/java/collection/images/image-20200405151029416.png b/docs/java/collection/images/image-20200405151029416.png new file mode 100644 index 00000000000..26ea14ca479 Binary files /dev/null and b/docs/java/collection/images/image-20200405151029416.png differ diff --git a/docs/java/collection/images/java-collection-hierarchy.png b/docs/java/collection/images/java-collection-hierarchy.png new file mode 100644 index 00000000000..78daf980845 Binary files /dev/null and b/docs/java/collection/images/java-collection-hierarchy.png differ diff --git a/docs/java/collection/images/java8_concurrenthashmap.png b/docs/java/collection/images/java8_concurrenthashmap.png new file mode 100644 index 00000000000..a090c7cc25e Binary files /dev/null and b/docs/java/collection/images/java8_concurrenthashmap.png differ diff --git "a/docs/java/collection/images/jdk1.8\344\271\213\345\211\215\347\232\204\345\206\205\351\203\250\347\273\223\346\236\204-HashMap.png" "b/docs/java/collection/images/jdk1.8\344\271\213\345\211\215\347\232\204\345\206\205\351\203\250\347\273\223\346\236\204-HashMap.png" new file mode 100644 index 00000000000..54180092b12 Binary files /dev/null and "b/docs/java/collection/images/jdk1.8\344\271\213\345\211\215\347\232\204\345\206\205\351\203\250\347\273\223\346\236\204-HashMap.png" differ diff --git "a/docs/java/collection/images/jdk1.8\344\271\213\345\220\216\347\232\204\345\206\205\351\203\250\347\273\223\346\236\204-HashMap.png" "b/docs/java/collection/images/jdk1.8\344\271\213\345\220\216\347\232\204\345\206\205\351\203\250\347\273\223\346\236\204-HashMap.png" new file mode 100644 index 00000000000..7c95e73847e Binary files /dev/null and "b/docs/java/collection/images/jdk1.8\344\271\213\345\220\216\347\232\204\345\206\205\351\203\250\347\273\223\346\236\204-HashMap.png" differ diff --git "a/docs/java/collection/images/linkedlist/LinkedList\345\206\205\351\203\250\347\273\223\346\236\204.png" "b/docs/java/collection/images/linkedlist/LinkedList\345\206\205\351\203\250\347\273\223\346\236\204.png" new file mode 100644 index 00000000000..b70a93721e1 Binary files /dev/null and "b/docs/java/collection/images/linkedlist/LinkedList\345\206\205\351\203\250\347\273\223\346\236\204.png" differ diff --git "a/docs/java/collection/java\351\233\206\345\220\210\344\275\277\347\224\250\346\263\250\346\204\217\344\272\213\351\241\271.md" "b/docs/java/collection/java\351\233\206\345\220\210\344\275\277\347\224\250\346\263\250\346\204\217\344\272\213\351\241\271.md" new file mode 100644 index 00000000000..6a65c33a264 --- /dev/null +++ "b/docs/java/collection/java\351\233\206\345\220\210\344\275\277\347\224\250\346\263\250\346\204\217\344\272\213\351\241\271.md" @@ -0,0 +1,439 @@ +--- +title: Java集合使用注意事项总结 +category: Java +tag: + - Java集合 +--- + +这篇文章我根据《阿里巴巴 Java 开发手册》总结了关于集合使用常见的注意事项以及其具体原理。 + +强烈建议小伙伴们多多阅读几遍,避免自己写代码的时候出现这些低级的问题。 + +## 集合判空 + +《阿里巴巴 Java 开发手册》的描述如下: + +> **判断所有集合内部的元素是否为空,使用 `isEmpty()` 方法,而不是 `size()==0` 的方式。** + +这是因为 `isEmpty()` 方法的可读性更好,并且时间复杂度为 O(1)。 + +绝大部分我们使用的集合的 `size()` 方法的时间复杂度也是 O(1),不过,也有很多复杂度不是 O(1) 的,比如 `java.util.concurrent` 包下的某些集合(`ConcurrentLinkedQueue` 、`ConcurrentHashMap`...)。 + +下面是 `ConcurrentHashMap` 的 `size()` 方法和 `isEmpty()` 方法的源码。 + +```java +public int size() { + long n = sumCount(); + return ((n < 0L) ? 0 : + (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : + (int)n); +} +final long sumCount() { + CounterCell[] as = counterCells; CounterCell a; + long sum = baseCount; + if (as != null) { + for (int i = 0; i < as.length; ++i) { + if ((a = as[i]) != null) + sum += a.value; + } + } + return sum; +} +public boolean isEmpty() { + return sumCount() <= 0L; // ignore transient negative values +} +``` + +## 集合转 Map + +《阿里巴巴 Java 开发手册》的描述如下: + +> **在使用 `java.util.stream.Collectors` 类的 `toMap()` 方法转为 `Map` 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。** + +```java +class Person { + private String name; + private String phoneNumber; + // getters and setters +} + +List bookList = new ArrayList<>(); +bookList.add(new Person("jack","18163138123")); +bookList.add(new Person("martin",null)); +// 空指针异常 +bookList.stream().collect(Collectors.toMap(Person::getName, Person::getPhoneNumber)); +``` + +下面我们来解释一下原因。 + +首先,我们来看 `java.util.stream.Collectors` 类的 `toMap()` 方法 ,可以看到其内部调用了 `Map` 接口的 `merge()` 方法。 + +```java +public static > +Collector toMap(Function keyMapper, + Function valueMapper, + BinaryOperator mergeFunction, + Supplier mapSupplier) { + BiConsumer accumulator + = (map, element) -> map.merge(keyMapper.apply(element), + valueMapper.apply(element), mergeFunction); + return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID); +} +``` + +`Map` 接口的 `merge()` 方法如下,这个方法是接口中的默认实现。 + +> 如果你还不了解 Java 8 新特性的话,请看这篇文章:[《Java8 新特性总结》](https://mp.weixin.qq.com/s/ojyl7B6PiHaTWADqmUq2rw) 。 + +```java +default V merge(K key, V value, + BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + Objects.requireNonNull(value); + V oldValue = get(key); + V newValue = (oldValue == null) ? value : + remappingFunction.apply(oldValue, value); + if(newValue == null) { + remove(key); + } else { + put(key, newValue); + } + return newValue; +} +``` + +`merge()` 方法会先调用 `Objects.requireNonNull()` 方法判断 value 是否为空。 + +```java +public static T requireNonNull(T obj) { + if (obj == null) + throw new NullPointerException(); + return obj; +} +``` + +## 集合遍历 + +《阿里巴巴 Java 开发手册》的描述如下: + +> **不要在 foreach 循环里进行元素的 `remove/add` 操作。remove 元素请使用 `Iterator` 方式,如果并发操作,需要对 `Iterator` 对象加锁。** + +通过反编译你会发现 foreach 语法糖底层其实还是依赖 `Iterator` 。不过, `remove/add` 操作直接调用的是集合自己的方法,而不是 `Iterator` 的 `remove/add`方法 + +这就导致 `Iterator` 莫名其妙地发现自己有元素被 `remove/add` ,然后,它就会抛出一个 `ConcurrentModificationException` 来提示用户发生了并发修改异常。这就是单线程状态下产生的 **fail-fast 机制**。 + +> **fail-fast 机制** :多个线程对 fail-fast 集合进行修改的时候,可能会抛出`ConcurrentModificationException`。 即使是单线程下也有可能会出现这种情况,上面已经提到过。 + +Java8 开始,可以使用 `Collection#removeIf()`方法删除满足特定条件的元素,如 + +```java +List list = new ArrayList<>(); +for (int i = 1; i <= 10; ++i) { + list.add(i); +} +list.removeIf(filter -> filter % 2 == 0); /* 删除list中的所有偶数 */ +System.out.println(list); /* [1, 3, 5, 7, 9] */ +``` + +除了上面介绍的直接使用 `Iterator` 进行遍历操作之外,你还可以: + +- 使用普通的 for 循环 +- 使用 fail-safe 的集合类。`java.util`包下面的所有的集合类都是 fail-fast 的,而`java.util.concurrent`包下面的所有的类都是 fail-safe 的。 +- ...... + +## 集合去重 + +《阿里巴巴 Java 开发手册》的描述如下: + +> **可以利用 `Set` 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 `List` 的 `contains()` 进行遍历去重或者判断包含操作。** + +这里我们以 `HashSet` 和 `ArrayList` 为例说明。 + +```java +// Set 去重代码示例 +public static Set removeDuplicateBySet(List data) { + + if (CollectionUtils.isEmpty(data)) { + return new HashSet<>(); + } + return new HashSet<>(data); +} + +// List 去重代码示例 +public static List removeDuplicateByList(List data) { + + if (CollectionUtils.isEmpty(data)) { + return new ArrayList<>(); + + } + List result = new ArrayList<>(data.size()); + for (T current : data) { + if (!result.contains(current)) { + result.add(current); + } + } + return result; +} + +``` + +两者的核心差别在于 `contains()` 方法的实现。 + +`HashSet` 的 `contains()` 方法底部依赖的 `HashMap` 的 `containsKey()` 方法,时间复杂度接近于 O(1)(没有出现哈希冲突的时候为 O(1))。 + +```java +private transient HashMap map; +public boolean contains(Object o) { + return map.containsKey(o); +} +``` + +我们有 N 个元素插入进 Set 中,那时间复杂度就接近是 O (n)。 + +`ArrayList` 的 `contains()` 方法是通过遍历所有元素的方法来做的,时间复杂度接近是 O(n)。 + +```java +public boolean contains(Object o) { + return indexOf(o) >= 0; +} +public int indexOf(Object o) { + if (o == null) { + for (int i = 0; i < size; i++) + if (elementData[i]==null) + return i; + } else { + for (int i = 0; i < size; i++) + if (o.equals(elementData[i])) + return i; + } + return -1; +} + +``` + +我们的 `List` 有 N 个元素,那时间复杂度就接近是 O (n^2)。 + +## 集合转数组 + +《阿里巴巴 Java 开发手册》的描述如下: + +> **使用集合转数组的方法,必须使用集合的 `toArray(T[] array)`,传入的是类型完全一致、长度为 0 的空数组。** + +`toArray(T[] array)` 方法的参数是一个泛型数组,如果 `toArray` 方法中没有传递任何参数的话返回的是 `Object`类 型数组。 + +```java +String [] s= new String[]{ + "dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A" +}; +List list = Arrays.asList(s); +Collections.reverse(list); +//没有指定类型的话会报错 +s=list.toArray(new String[0]); +``` + +由于 JVM 优化,`new String[0]`作为`Collection.toArray()`方法的参数现在使用更好,`new String[0]`就是起一个模板的作用,指定了返回数组的类型,0 是为了节省空间,因为它只是为了说明返回的类型。详见: + +## 数组转集合 + +《阿里巴巴 Java 开发手册》的描述如下: + +> **使用工具类 `Arrays.asList()` 把数组转换成集合时,不能使用其修改集合相关的方法, 它的 `add/remove/clear` 方法会抛出 `UnsupportedOperationException` 异常。** + +我在之前的一个项目中就遇到一个类似的坑。 + +`Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个 `List` 集合。 + +```java +String[] myArray = {"Apple", "Banana", "Orange"}; +List myList = Arrays.asList(myArray); +//上面两个语句等价于下面一条语句 +List myList = Arrays.asList("Apple","Banana", "Orange"); +``` + +JDK 源码对于这个方法的说明: + +```java +/** + *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁, + * 与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。 + */ +public static List asList(T... a) { + return new ArrayList<>(a); +} +``` + +下面我们来总结一下使用注意事项。 + +**1、`Arrays.asList()`是泛型方法,传递的数组必须是对象数组,而不是基本类型。** + +```java +int[] myArray = {1, 2, 3}; +List myList = Arrays.asList(myArray); +System.out.println(myList.size());//1 +System.out.println(myList.get(0));//数组地址值 +System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException +int[] array = (int[]) myList.get(0); +System.out.println(array[0]);//1 +``` + +当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时 `List` 的唯一元素就是这个数组,这也就解释了上面的代码。 + +我们使用包装类型数组就可以解决这个问题。 + +```java +Integer[] myArray = {1, 2, 3}; +``` + +**2、使用集合的修改方法: `add()`、`remove()`、`clear()`会抛出异常。** + +```java +List myList = Arrays.asList(1, 2, 3); +myList.add(4);//运行时报错:UnsupportedOperationException +myList.remove(1);//运行时报错:UnsupportedOperationException +myList.clear();//运行时报错:UnsupportedOperationException +``` + +`Arrays.asList()` 方法返回的并不是 `java.util.ArrayList` ,而是 `java.util.Arrays` 的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。 + +```java +List myList = Arrays.asList(1, 2, 3); +System.out.println(myList.getClass());//class java.util.Arrays$ArrayList +``` + +下图是 `java.util.Arrays$ArrayList` 的简易源码,我们可以看到这个类重写的方法有哪些。 + +```java + private static class ArrayList extends AbstractList + implements RandomAccess, java.io.Serializable + { + ... + + @Override + public E get(int index) { + ... + } + + @Override + public E set(int index, E element) { + ... + } + + @Override + public int indexOf(Object o) { + ... + } + + @Override + public boolean contains(Object o) { + ... + } + + @Override + public void forEach(Consumer action) { + ... + } + + @Override + public void replaceAll(UnaryOperator operator) { + ... + } + + @Override + public void sort(Comparator c) { + ... + } + } +``` + +我们再看一下`java.util.AbstractList`的 `add/remove/clear` 方法就知道为什么会抛出 `UnsupportedOperationException` 了。 + +```java +public E remove(int index) { + throw new UnsupportedOperationException(); +} +public boolean add(E e) { + add(size(), e); + return true; +} +public void add(int index, E element) { + throw new UnsupportedOperationException(); +} + +public void clear() { + removeRange(0, size()); +} +protected void removeRange(int fromIndex, int toIndex) { + ListIterator it = listIterator(fromIndex); + for (int i=0, n=toIndex-fromIndex; i List arrayToList(final T[] array) { + final List l = new ArrayList(array.length); + + for (final T s : array) { + l.add(s); + } + return l; +} + + +Integer [] myArray = { 1, 2, 3 }; +System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList +``` + +2、最简便的方法 + +```java +List list = new ArrayList<>(Arrays.asList("a", "b", "c")) +``` + +3、使用 Java8 的 `Stream`(推荐) + +```java +Integer [] myArray = { 1, 2, 3 }; +List myList = Arrays.stream(myArray).collect(Collectors.toList()); +//基本类型也可以实现转换(依赖boxed的装箱操作) +int [] myArray2 = { 1, 2, 3 }; +List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList()); +``` + +4、使用 Guava + +对于不可变集合,你可以使用[`ImmutableList`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java)类及其[`of()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L101)与[`copyOf()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L225)工厂方法:(参数不能为空) + +```java +List il = ImmutableList.of("string", "elements"); // from varargs +List il = ImmutableList.copyOf(aStringArray); // from array +``` + +对于可变集合,你可以使用[`Lists`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java)类及其[`newArrayList()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java#L87)工厂方法: + +```java +List l1 = Lists.newArrayList(anotherListOrCollection); // from collection +List l2 = Lists.newArrayList(aStringArray); // from array +List l3 = Lists.newArrayList("or", "string", "elements"); // from varargs +``` + +5、使用 Apache Commons Collections + +```java +List list = new ArrayList(); +CollectionUtils.addAll(list, str); +``` + +6、 使用 Java9 的 `List.of()`方法 + +```java +Integer[] array = {1, 2, 3}; +List list = List.of(array); +``` \ No newline at end of file diff --git "a/docs/java/collection/java\351\233\206\345\220\210\346\241\206\346\236\266\345\237\272\347\241\200\347\237\245\350\257\206&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" "b/docs/java/collection/java\351\233\206\345\220\210\346\241\206\346\236\266\345\237\272\347\241\200\347\237\245\350\257\206&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" new file mode 100644 index 00000000000..1f901750654 --- /dev/null +++ "b/docs/java/collection/java\351\233\206\345\220\210\346\241\206\346\236\266\345\237\272\347\241\200\347\237\245\350\257\206&\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" @@ -0,0 +1,660 @@ +--- +category: Java +tag: + - Java集合 +--- + +# Java集合框架基础知识&面试题总结 + +## 集合概述 + +### Java 集合概览 + +Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 `Collecton`接口,主要用于存放单一元素;另一个是 `Map` 接口,主要用于存放键值对。对于`Collection` 接口,下面又有三个主要的子接口:`List`、`Set` 和 `Queue`。 + +Java 集合框架如下图所示: + +![](./images/java-collection-hierarchy.png) + + +注:图中只列举了主要的继承派生关系,并没有列举所有关系。比方省略了`AbstractList`, `NavigableSet`等抽象类以及其他的一些辅助类,如想深入了解,可自行查看源码。 + +### 说说 List, Set, Queue, Map 四者的区别? + +- `List`(对付顺序的好帮手): 存储的元素是有序的、可重复的。 +- `Set`(注重独一无二的性质): 存储的元素是无序的、不可重复的。 +- `Queue`(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。 +- `Map`(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。 + +### 集合框架底层数据结构总结 + +先来看一下 `Collection` 接口下面的集合。 + +#### List + +- `Arraylist`: `Object[]` 数组 +- `Vector`:`Object[]` 数组 +- `LinkedList`: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环) + +#### Set + +- `HashSet`(无序,唯一): 基于 `HashMap` 实现的,底层采用 `HashMap` 来保存元素 +- `LinkedHashSet`: `LinkedHashSet` 是 `HashSet` 的子类,并且其内部是通过 `LinkedHashMap` 来实现的。有点类似于我们之前说的 `LinkedHashMap` 其内部是基于 `HashMap` 实现一样,不过还是有一点点区别的 +- `TreeSet`(有序,唯一): 红黑树(自平衡的排序二叉树) + +#### Queue +- `PriorityQueue`: `Object[]` 数组来实现二叉堆 +- `ArrayQueue`: `Object[]` 数组 + 双指针 + +再来看看 `Map` 接口下面的集合。 + +#### Map + +- `HashMap`: JDK1.8 之前 `HashMap` 由数组+链表组成的,数组是 `HashMap` 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间 +- `LinkedHashMap`: `LinkedHashMap` 继承自 `HashMap`,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,`LinkedHashMap` 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931) +- `Hashtable`: 数组+链表组成的,数组是 `Hashtable` 的主体,链表则是主要为了解决哈希冲突而存在的 +- `TreeMap`: 红黑树(自平衡的排序二叉树) + +### 如何选用集合? + +主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 `Map` 接口下的集合,需要排序时选择 `TreeMap`,不需要排序时就选择 `HashMap`,需要保证线程安全就选用 `ConcurrentHashMap`。 + +当我们只需要存放元素值时,就选择实现`Collection` 接口的集合,需要保证元素唯一时选择实现 `Set` 接口的集合比如 `TreeSet` 或 `HashSet`,不需要就选择实现 `List` 接口的比如 `ArrayList` 或 `LinkedList`,然后再根据实现这些接口的集合的特点来选用。 + +### 为什么要使用集合? + +当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端, +因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。 + +数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。 +但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据。 + +## Collection 子接口之 List + +### Arraylist 和 Vector 的区别? + +- `ArrayList` 是 `List` 的主要实现类,底层使用 `Object[ ]`存储,适用于频繁的查找工作,线程不安全 ; +- `Vector` 是 `List` 的古老实现类,底层使用`Object[ ]` 存储,线程安全的。 + +### Arraylist 与 LinkedList 区别? + +1. **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; +2. **底层数据结构:** `Arraylist` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) +3. **插入和删除是否受元素位置的影响:** + - `ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行`add(E e)`方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 + - `LinkedList` 采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响(`add(E e)`、`addFirst(E e)`、`addLast(E e)`、`removeFirst()` 、 `removeLast()`),近似 O(1),如果是要在指定位置 `i` 插入和删除元素的话(`add(int index, E element)`,`remove(Object o)`) 时间复杂度近似为 O(n) ,因为需要先移动到指定位置再插入。 +4. **是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList` 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 +5. **内存空间占用:** ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。 + +#### 补充内容:双向链表和双向循环链表 + +**双向链表:** 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。 + +> 另外推荐一篇把双向链表讲清楚的文章:[https://juejin.cn/post/6844903648154271757](https://juejin.cn/post/6844903648154271757) + +![双向链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/双向链表.png) + +**双向循环链表:** 最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环。 + +![双向循环链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/双向循环链表.png) + +#### 补充内容:RandomAccess 接口 + +```java +public interface RandomAccess { +} +``` + +查看源码我们发现实际上 `RandomAccess` 接口中什么都没有定义。所以,在我看来 `RandomAccess` 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 + +在 `binarySearch()` 方法中,它要判断传入的 list 是否 `RamdomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法 + +```java + public static + int binarySearch(List> list, T key) { + if (list instanceof RandomAccess || list.size() arrayList = new ArrayList(); + arrayList.add(-1); + arrayList.add(3); + arrayList.add(3); + arrayList.add(-5); + arrayList.add(7); + arrayList.add(4); + arrayList.add(-9); + arrayList.add(-7); + System.out.println("原始数组:"); + System.out.println(arrayList); + // void reverse(List list):反转 + Collections.reverse(arrayList); + System.out.println("Collections.reverse(arrayList):"); + System.out.println(arrayList); + + // void sort(List list),按自然排序的升序排序 + Collections.sort(arrayList); + System.out.println("Collections.sort(arrayList):"); + System.out.println(arrayList); + // 定制排序的用法 + Collections.sort(arrayList, new Comparator() { + + @Override + public int compare(Integer o1, Integer o2) { + return o2.compareTo(o1); + } + }); + System.out.println("定制排序后:"); + System.out.println(arrayList); +``` + +Output: + +``` +原始数组: +[-1, 3, 3, -5, 7, 4, -9, -7] +Collections.reverse(arrayList): +[-7, -9, 4, 7, -5, 3, 3, -1] +Collections.sort(arrayList): +[-9, -7, -5, -1, 3, 3, 4, 7] +定制排序后: +[7, 4, 3, 3, -1, -5, -7, -9] +``` + +#### 重写 compareTo 方法实现按年龄来排序 + +```java +// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列 +// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他 +// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了 +public class Person implements Comparable { + private String name; + private int age; + + public Person(String name, int age) { + super(); + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + /** + * T重写compareTo方法实现按年龄来排序 + */ + @Override + public int compareTo(Person o) { + if (this.age > o.getAge()) { + return 1; + } + if (this.age < o.getAge()) { + return -1; + } + return 0; + } +} + +``` + +```java + public static void main(String[] args) { + TreeMap pdata = new TreeMap(); + pdata.put(new Person("张三", 30), "zhangsan"); + pdata.put(new Person("李四", 20), "lisi"); + pdata.put(new Person("王五", 10), "wangwu"); + pdata.put(new Person("小红", 5), "xiaohong"); + // 得到key的值的同时得到key所对应的值 + Set keys = pdata.keySet(); + for (Person key : keys) { + System.out.println(key.getAge() + "-" + key.getName()); + + } + } +``` + +Output: + +``` +5-小红 +10-王五 +20-李四 +30-张三 +``` + +### 无序性和不可重复性的含义是什么 + +1、什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。 + +2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法。 + +### 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同 + +- `HashSet`、`LinkedHashSet` 和 `TreeSet` 都是 `Set` 接口的实现类,都能保证元素唯一,并且都不是线程安全的。 +- `HashSet`、`LinkedHashSet` 和 `TreeSet` 的主要区别在于底层数据结构不同。`HashSet` 的底层数据结构是哈希表(基于 `HashMap` 实现)。`LinkedHashSet` 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。`TreeSet` 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。 +- 底层数据结构不同又导致这三者的应用场景不同。`HashSet` 用于不需要保证元素插入和取出顺序的场景,`LinkHashSet` 用于保证元素的插入和取出顺序满足 FIFO 的场景,`TreeSet` 用于支持对元素自定义排序规则的场景。 + +## Collection 子接口之 Queue + +### Queue 与 Deque 的区别 + +`Queue` 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 **先进先出(FIFO)** 规则。 + +`Queue` 扩展了 `Collection` 的接口,根据 **因为容量问题而导致操作失败后处理方式的不同** 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。 + +| `Queue` 接口| 抛出异常 | 返回特殊值 | +| ------------ | --------- | ---------- | +| 插入队尾 | add(E e) | offer(E e) | +| 删除队首 | remove() | poll() | +| 查询队首元素 | element() | peek() | + +`Deque` 是双端队列,在队列的两端均可以插入或删除元素。 + +`Deque` 扩展了 `Queue` 的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类: + +| `Deque` 接口 | 抛出异常 | 返回特殊值 | +| ------------ | ------------- | --------------- | +| 插入队首 | addFirst(E e) | offerFirst(E e) | +| 插入队尾 | addLast(E e) | offerLast(E e) | +| 删除队首 | removeFirst() | pollFirst() | +| 删除队尾 | removeLast() | pollLast() | +| 查询队首元素 | getFirst() | peekFirst() | +| 查询队尾元素 | getLast() | peekLast() | + +事实上,`Deque` 还提供有 `push()` 和 `pop()` 等其他方法,可用于模拟栈。 + + +### ArrayDeque 与 LinkedList 的区别 + +`ArrayDeque` 和 `LinkedList` 都实现了 `Deque` 接口,两者都具有队列的功能,但两者有什么区别呢? + +- `ArrayDeque` 是基于可变长的数组和双指针来实现,而 `LinkedList` 则通过链表来实现。 + +- `ArrayDeque` 不支持存储 `NULL` 数据,但 `LinkedList` 支持。 + +- `ArrayDeque` 是在 JDK1.6 才被引入的,而`LinkedList` 早在 JDK1.2 时就已经存在。 + +- `ArrayDeque` 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 `LinkedList` 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。 + +从性能的角度上,选用 `ArrayDeque` 来实现队列要比 `LinkedList` 更好。此外,`ArrayDeque` 也可以用于实现栈。 + +### 说一说 PriorityQueue + +`PriorityQueue` 是在 JDK1.5 中被引入的, 其与 `Queue` 的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。 + +这里列举其相关的一些要点: + +- `PriorityQueue` 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据 +- `PriorityQueue` 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。 +- `PriorityQueue` 是非线程安全的,且不支持存储 `NULL` 和 `non-comparable` 的对象。 +- `PriorityQueue` 默认是小顶堆,但可以接收一个 `Comparator` 作为构造参数,从而来自定义元素优先级的先后。 + +`PriorityQueue` 在面试中可能更多的会出现在手撕算法的时候,典型例题包括堆排序、求第K大的数、带权图的遍历等,所以需要会熟练使用才行。 + +## Map 接口 + +### HashMap 和 Hashtable 的区别 + +1. **线程是否安全:** `HashMap` 是非线程安全的,`Hashtable` 是线程安全的,因为 `Hashtable` 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 `ConcurrentHashMap` 吧!); +2. **效率:** 因为线程安全的问题,`HashMap` 要比 `Hashtable` 效率高一点。另外,`Hashtable` 基本被淘汰,不要在代码中使用它; +3. **对 Null key 和 Null value 的支持:** `HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 `NullPointerException`。 +4. **初始容量大小和每次扩充容量大小的不同 :** ① 创建时如果不指定容量初始值,`Hashtable` 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。`HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 `HashMap` 会将其扩充为 2 的幂次方大小(`HashMap` 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 `HashMap` 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 +5. **底层数据结构:** JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 + +**`HashMap` 中带有初始容量的构造函数:** + +```java + public HashMap(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal initial capacity: " + + initialCapacity); + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + + loadFactor); + this.loadFactor = loadFactor; + this.threshold = tableSizeFor(initialCapacity); + } + public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } +``` + +下面这个方法保证了 `HashMap` 总是使用 2 的幂作为哈希表的大小。 + +```java + /** + * Returns a power of two size for the given target capacity. + */ + static final int tableSizeFor(int cap) { + int n = cap - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; + } +``` + +### HashMap 和 HashSet 区别 + +如果你看过 `HashSet` 源码的话就应该知道:`HashSet` 底层就是基于 `HashMap` 实现的。(`HashSet` 的源码非常非常少,因为除了 `clone()`、`writeObject()`、`readObject()`是 `HashSet` 自己不得不实现之外,其他方法都是直接调用 `HashMap` 中的方法。 + +| `HashMap` | `HashSet` | +| :------------------------------------: | :----------------------------------------------------------: | +| 实现了 `Map` 接口 | 实现 `Set` 接口 | +| 存储键值对 | 仅存储对象 | +| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 `Set` 中添加元素 | +| `HashMap` 使用键(Key)计算 `hashcode` | `HashSet` 使用成员对象来计算 `hashcode` 值,对于两个对象来说 `hashcode` 可能相同,所以`equals()`方法用来判断对象的相等性 | + +### HashMap 和 TreeMap 区别 + +`TreeMap` 和`HashMap` 都继承自`AbstractMap` ,但是需要注意的是`TreeMap`它还实现了`NavigableMap`接口和`SortedMap` 接口。 + +![](./images/TreeMap继承结构.png) + +实现 `NavigableMap` 接口让 `TreeMap` 有了对集合内元素的搜索的能力。 + +实现`SortedMap`接口让 `TreeMap` 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下: + +```java +/** + * @author shuang.kou + * @createTime 2020年06月15日 17:02:00 + */ +public class Person { + private Integer age; + + public Person(Integer age) { + this.age = age; + } + + public Integer getAge() { + return age; + } + + + public static void main(String[] args) { + TreeMap treeMap = new TreeMap<>(new Comparator() { + @Override + public int compare(Person person1, Person person2) { + int num = person1.getAge() - person2.getAge(); + return Integer.compare(num, 0); + } + }); + treeMap.put(new Person(3), "person1"); + treeMap.put(new Person(18), "person2"); + treeMap.put(new Person(35), "person3"); + treeMap.put(new Person(16), "person4"); + treeMap.entrySet().stream().forEach(personStringEntry -> { + System.out.println(personStringEntry.getValue()); + }); + } +} +``` + +输出: + +``` +person1 +person4 +person2 +person3 +``` + +可以看出,`TreeMap` 中的元素已经是按照 `Person` 的 age 字段的升序来排列了。 + +上面,我们是通过传入匿名内部类的方式实现的,你可以将代码替换成 Lambda 表达式实现的方式: + +```java +TreeMap treeMap = new TreeMap<>((person1, person2) -> { + int num = person1.getAge() - person2.getAge(); + return Integer.compare(num, 0); +}); +``` + +**综上,相比于`HashMap`来说 `TreeMap` 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。** + +### HashSet 如何检查重复 + +以下内容摘自我的 Java 启蒙书《Head first java》第二版: + +当你把对象加入`HashSet`时,`HashSet` 会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的 `hashcode` 值作比较,如果没有相符的 `hashcode`,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 `hashcode` 值的对象,这时会调用`equals()`方法来检查 `hashcode` 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让加入操作成功。 + +在openjdk8中,`HashSet`的`add()`方法只是简单的调用了`HashMap`的`put()`方法,并且判断了一下返回值以确保是否有重复元素。直接看一下`HashSet`中的源码: +```java +// Returns: true if this set did not already contain the specified element +// 返回值:当set中没有包含add的元素时返回真 +public boolean add(E e) { + return map.put(e, PRESENT)==null; +} +``` + +而在`HashMap`的`putVal()`方法中也能看到如下说明: +```java +// Returns : previous value, or null if none +// 返回值:如果插入位置没有元素返回null,否则返回上一个元素 +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { +... +} +``` + +也就是说,在openjdk8中,实际上无论`HashSet`中是否已经存在了某元素,`HashSet`都会直接插入,只是会在`add()`方法的返回值处告诉我们插入前是否存在相同元素。 + +**`hashCode()`与 `equals()` 的相关规定:** + +1. 如果两个对象相等,则 `hashcode` 一定也是相同的 +2. 两个对象相等,对两个 `equals()` 方法返回 true +3. 两个对象有相同的 `hashcode` 值,它们也不一定是相等的 +4. 综上,`equals()` 方法被覆盖过,则 `hashCode()` 方法也必须被覆盖 +5. `hashCode()`的默认行为是对堆上的对象产生独特值。如果没有重写 `hashCode()`,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 + +**==与 equals 的区别** + +对于基本类型来说,== 比较的是值是否相等; + +对于引用类型来说,== 比较的是两个引用是否指向同一个对象地址(两者在内存中存放的地址(堆内存地址)是否指向同一个地方); + +对于引用类型(包括包装类型)来说,equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String),则比较的是地址里的内容。 + +### HashMap 的底层实现 + +#### JDK1.8 之前 + +JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** + +**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** + +**JDK 1.8 HashMap 的 hash 方法源码:** + +JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 + +```java + static final int hash(Object key) { + int h; + // key.hashCode():返回散列值也就是hashcode + // ^ :按位异或 + // >>>:无符号右移,忽略符号位,空位都以0补齐 + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); + } +``` + +对比一下 JDK1.7 的 HashMap 的 hash 方法源码. + +```java +static int hash(int h) { + // This function ensures that hashCodes that differ only by + // constant multiples at each bit position have a bounded + // number of collisions (approximately 8 at default load factor). + + h ^= (h >>> 20) ^ (h >>> 12); + return h ^ (h >>> 7) ^ (h >>> 4); +} +``` + +相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 + +所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 + +![jdk1.8之前的内部结构-HashMap](images/jdk1.8之前的内部结构-HashMap.png) + +#### JDK1.8 之后 + +相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 + +![jdk1.8之后的内部结构-HashMap](images/jdk1.8之后的内部结构-HashMap.png) + +> TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 + +### HashMap 的长度为什么是 2 的幂次方 + +为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。 + +**这个算法应该如何设计呢?** + +我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。** + +### HashMap 多线程操作导致死循环问题 + +主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。 + +详情请查看: + +### HashMap 有哪几种常见的遍历方式? + +[HashMap 的 7 种遍历方式与性能分析!](https://mp.weixin.qq.com/s/zQBN3UvJDhRTKP6SzcZFKw) + +### ConcurrentHashMap 和 Hashtable 的区别 + +`ConcurrentHashMap` 和 `Hashtable` 的区别主要体现在实现线程安全的方式上不同。 + +- **底层数据结构:** JDK1.7 的 `ConcurrentHashMap` 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟 `HashMap1.8` 的结构一样,数组+链表/红黑二叉树。`Hashtable` 和 JDK1.8 之前的 `HashMap` 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; +- **实现线程安全的方式(重要):** ① **在 JDK1.7 的时候,`ConcurrentHashMap`(分段锁)** 对整个桶数组进行了分割分段(`Segment`),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 **到了 JDK1.8 的时候已经摒弃了 `Segment` 的概念,而是直接用 `Node` 数组+链表+红黑树的数据结构来实现,并发控制使用 `synchronized` 和 CAS 来操作。(JDK1.6 以后 对 `synchronized` 锁做了很多优化)** 整个看起来就像是优化过且线程安全的 `HashMap`,虽然在 JDK1.8 中还能看到 `Segment` 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **`Hashtable`(同一把锁)** :使用 `synchronized` 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 + +**两者的对比图:** + +**Hashtable:** + +![Hashtable全表锁](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/HashTable全表锁.png) + +

https://www.cnblogs.com/chengxiao/p/6842045.html>

+ +**JDK1.7 的 ConcurrentHashMap:** + +![JDK1.7的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ConcurrentHashMap分段锁.jpg) + +

https://www.cnblogs.com/chengxiao/p/6842045.html>

+ +**JDK1.8 的 ConcurrentHashMap:** + +![Java8 ConcurrentHashMap 存储结构(图片来自 javadoop)](./images/java8_concurrenthashmap.png) + +JDK1.8 的 `ConcurrentHashMap` 不再是 **Segment 数组 + HashEntry 数组 + 链表**,而是 **Node 数组 + 链表 / 红黑树**。不过,Node 只能用于链表的情况,红黑树的情况需要使用 **`TreeNode`**。当冲突链表达到一定长度时,链表会转换成红黑树。 + +### ConcurrentHashMap 线程安全的具体实现方式/底层具体实现 + +#### JDK1.7(上面有示意图) + +首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 + +**`ConcurrentHashMap` 是由 `Segment` 数组结构和 `HashEntry` 数组结构组成**。 + +Segment 实现了 `ReentrantLock`,所以 `Segment` 是一种可重入锁,扮演锁的角色。`HashEntry` 用于存储键值对数据。 + +```java +static class Segment extends ReentrantLock implements Serializable { +} +``` + +一个 `ConcurrentHashMap` 里包含一个 `Segment` 数组。`Segment` 的结构和 `HashMap` 类似,是一种数组和链表结构,一个 `Segment` 包含一个 `HashEntry` 数组,每个 `HashEntry` 是一个链表结构的元素,每个 `Segment` 守护着一个 `HashEntry` 数组里的元素,当对 `HashEntry` 数组的数据进行修改时,必须首先获得对应的 `Segment` 的锁。 + +#### JDK1.8 (上面有示意图) + +`ConcurrentHashMap` 取消了 `Segment` 分段锁,采用 CAS 和 `synchronized` 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N))) + +`synchronized` 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。 + +## Collections 工具类 + +Collections 工具类常用方法: + +1. 排序 +2. 查找,替换操作 +3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合) + +### 排序操作 + +```java +void reverse(List list)//反转 +void shuffle(List list)//随机排序 +void sort(List list)//按自然排序的升序排序 +void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑 +void swap(List list, int i , int j)//交换两个索引位置的元素 +void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面 +``` + +### 查找,替换操作 + +```java +int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的 +int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll) +int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c) +void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素 +int frequency(Collection c, Object o)//统计元素出现次数 +int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target) +boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素 +``` + +### 同步控制 + +`Collections` 提供了多个`synchronizedXxx()`方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。 + +我们知道 `HashSet`,`TreeSet`,`ArrayList`,`LinkedList`,`HashMap`,`TreeMap` 都是线程不安全的。`Collections` 提供了多个静态方法可以把他们包装成线程同步的集合。 + +**最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。** + +方法如下: + +```java +synchronizedCollection(Collection c) //返回指定 collection 支持的同步(线程安全的)collection。 +synchronizedList(List list)//返回指定列表支持的同步(线程安全的)List。 +synchronizedMap(Map m) //返回由指定映射支持的同步(线程安全的)Map。 +synchronizedSet(Set s) //返回指定 set 支持的同步(线程安全的)set。 +``` diff --git a/docs/java/Multithread/AQS.md "b/docs/java/concurrent/aqs\345\216\237\347\220\206\344\273\245\345\217\212aqs\345\220\214\346\255\245\347\273\204\344\273\266\346\200\273\347\273\223.md" similarity index 56% rename from docs/java/Multithread/AQS.md rename to "docs/java/concurrent/aqs\345\216\237\347\220\206\344\273\245\345\217\212aqs\345\220\214\346\255\245\347\273\204\344\273\266\346\200\273\347\273\223.md" index facba05d1d7..43e59b4abb9 100644 --- a/docs/java/Multithread/AQS.md +++ "b/docs/java/concurrent/aqs\345\216\237\347\220\206\344\273\245\345\217\212aqs\345\220\214\346\255\245\347\273\204\344\273\266\346\200\273\347\273\223.md" @@ -1,52 +1,46 @@ -点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 - - - -- [1 AQS 简单介绍](#1-aqs-简单介绍) -- [2 AQS 原理](#2-aqs-原理) - - [2.1 AQS 原理概览](#21-aqs-原理概览) - - [2.2 AQS 对资源的共享方式](#22-aqs-对资源的共享方式) - - [2.3 AQS 底层使用了模板方法模式](#23-aqs-底层使用了模板方法模式) -- [3 Semaphore(信号量)-允许多个线程同时访问](#3-semaphore信号量-允许多个线程同时访问) -- [4 CountDownLatch (倒计时器)](#4-countdownlatch-倒计时器) - - [4.1 CountDownLatch 的三种典型用法](#41-countdownlatch-的三种典型用法) - - [4.2 CountDownLatch 的使用示例](#42-countdownlatch-的使用示例) - - [4.3 CountDownLatch 的不足](#43-countdownlatch-的不足) - - [4.4 CountDownLatch 常见面试题](#44-countdownlatch-相常见面试题) -- [5 CyclicBarrier(循环栅栏)](#5-cyclicbarrier循环栅栏) - - [5.1 CyclicBarrier 的应用场景](#51-cyclicbarrier-的应用场景) - - [5.2 CyclicBarrier 的使用示例](#52-cyclicbarrier-的使用示例) - - [5.3 `CyclicBarrier`源码分析](#53-cyclicbarrier源码分析) - - [5.4 CyclicBarrier 和 CountDownLatch 的区别](#54-cyclicbarrier-和-countdownlatch-的区别) -- [6 ReentrantLock 和 ReentrantReadWriteLock](#6-reentrantlock-和-reentrantreadwritelock) -- [参考](#参考) -- [公众号](#公众号) - - - -> 常见问题:AQS 原理?;CountDownLatch 和 CyclicBarrier 了解吗,两者的区别是什么?用过 Semaphore 吗? - -### 1 AQS 简单介绍 - -AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。 +--- +title: AQS 原理以及 AQS 同步组件总结 +category: Java +tag: + - Java并发 +--- + + +开始之前,先来看几道常见的面试题!建议你带着这些问题来看这篇文章: + +- 何为 AQS?AQS 原理了解吗? +- `CountDownLatch` 和 `CyclicBarrier` 了解吗?两者的区别是什么? +- 用过 `Semaphore` 吗?应用场景了解吗? +- ...... + +## AQS 简单介绍 + +AQS 的全称为 `AbstractQueuedSynchronizer` ,翻译过来的意思就是抽象队列同步器。这个类在 `java.util.concurrent.locks` 包下面。 ![enter image description here](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/AQS.png) -AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。 +AQS 就是一个抽象类,主要用来构建锁和同步器。 -### 2 AQS 原理 +```java +public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { +} +``` + +AQS 为构建锁和同步器提供了一些通用功能的是实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 `ReentrantLock`,`Semaphore`,其他的诸如 `ReentrantReadWriteLock`,`SynchronousQueue`,`FutureTask`(jdk1.7) 等等皆是基于 AQS 的。 + +## AQS 原理 > 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参考,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。 -#### 2.1 AQS 原理概览 +### AQS 原理概览 -**AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。** +AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 **CLH 队列锁**实现的,即将暂时获取不到锁的线程加入到队列中。 > CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。 -看个 AQS(AbstractQueuedSynchronizer)原理图: +看个 AQS(`AbstractQueuedSynchronizer`)原理图: ![enter image description here](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/CLH.png) @@ -56,7 +50,7 @@ AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO private volatile int state;//共享变量,使用volatile修饰保证线程可见性 ``` -状态信息通过 protected 类型的`getState`,`setState`,`compareAndSetState`进行操作 +状态信息通过 `protected` 类型的`getState()`,`setState()`,`compareAndSetState()` 进行操作 ```java //返回同步状态的当前值 @@ -73,22 +67,22 @@ protected final boolean compareAndSetState(int expect, int update) { } ``` -#### 2.2 AQS 对资源的共享方式 +### AQS 对资源的共享方式 -**AQS 定义两种资源共享方式** +AQS 定义两种资源共享方式 **1)Exclusive**(独占) -只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁,下面以 ReentrantLock 对这两种锁的定义做介绍: +只有一个线程能执行,如 `ReentrantLock`。又可分为公平锁和非公平锁,`ReentrantLock` 同时支持两种锁,下面以 `ReentrantLock` 对这两种锁的定义做介绍: -- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 -- 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。 +- **公平锁** :按照线程在队列中的排队顺序,先到者先拿到锁 +- **非公平锁** :当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。 > 说明:下面这部分关于 `ReentrantLock` 源代码内容节选自:https://www.javadoop.com/post/AbstractQueuedSynchronizer-2 ,这是一篇很不错文章,推荐阅读。 -**下面来看 ReentrantLock 中相关的源代码:** +**下面来看 `ReentrantLock` 中相关的源代码:** -ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)。 +`ReentrantLock` 默认采用非公平锁,因为考虑获得更好的性能,通过 `boolean` 来决定是否用公平锁(传入 true 用公平锁)。 ```java /** Synchronizer providing all implementation mechanics */ @@ -102,7 +96,7 @@ public ReentrantLock(boolean fair) { } ``` -ReentrantLock 中公平锁的 `lock` 方法 +`ReentrantLock` 中公平锁的 `lock` 方法 ```java static final class FairSync extends Sync { @@ -138,7 +132,7 @@ static final class FairSync extends Sync { } ``` -非公平锁的 lock 方法: +非公平锁的 `lock` 方法: ```java static final class NonfairSync extends Sync { @@ -187,7 +181,7 @@ final boolean nonfairTryAcquire(int acquires) { 总结:公平锁和非公平锁只有两处不同: 1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。 -2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。 +2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 `tryAcquire` 方法,在 `tryAcquire` 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。 公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。 @@ -195,22 +189,24 @@ final boolean nonfairTryAcquire(int acquires) { **2)Share**(共享) -多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 +多个线程可同时执行,如 `Semaphore/CountDownLatch`。`Semaphore`、`CountDownLatCh`、 `CyclicBarrier`、`ReadWriteLock` 我们都会在后面讲到。 -ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。 +`ReentrantReadWriteLock` 可以看成是组合式,因为 `ReentrantReadWriteLock` 也就是读写锁允许多个线程同时对某一资源进行读。 不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在上层已经帮我们实现好了。 -#### 2.3 AQS 底层使用了模板方法模式 +### AQS 底层使用了模板方法模式 同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): -1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放) +1. 使用者继承 `AbstractQueuedSynchronizer` 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放) 2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用,下面简单的给大家介绍一下模板方法模式,模板方法模式是一个很容易理解的设计模式之一。 -> 模板方法模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。举个很简单的例子假如我们要去一个地方的步骤是:购票`buyTicket()`->安检`securityCheck()`->乘坐某某工具回家`ride()`->到达目的地`arrive()`。我们可能乘坐不同的交通工具回家比如飞机或者火车,所以除了`ride()`方法,其他方法的实现几乎相同。我们可以定义一个包含了这些方法的抽象类,然后用户根据自己的需要继承该抽象类然后修改 `ride()`方法。 +> 模板方法模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。 +> +> 举个很简单的例子假如我们要去一个地方的步骤是:购票 `buyTicket()`->安检 `securityCheck()`->乘坐某某工具回家 `ride()` ->到达目的地 `arrive()`。我们可能乘坐不同的交通工具回家比如飞机或者火车,所以除了`ride()`方法,其他方法的实现几乎相同。我们可以定义一个包含了这些方法的抽象类,然后用户根据自己的需要继承该抽象类然后修改 `ride()`方法。 **AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:** @@ -220,25 +216,26 @@ tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失 tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 - ``` 默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 -以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。 +以 `ReentrantLock` 为例,state 初始化为 0,表示未锁定状态。A 线程 `lock()` 时,会调用 `tryAcquire()`独占该锁并将 state+1。此后,其他线程再 `tryAcquire()` 时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。 + +再以 `CountDownLatch` 以例,任务分为 N 个子线程去执行,state 也初始化为 N(也可以不初始化为 N,不初始化为 N,state 减到 0 也会从 await()返回)。这 N 个子线程是并行执行的,每个子线程执行完后 `countDown()` 一次,state 会 CAS(Compare and Swap)减 1。等到 `state=0`,会 `unpark()` 主调用线程,然后主调用线程就会从 `await()` 函数返回,继续后余动作。 -再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。 +所以 `CountDownLatch` 可以做倒计数器,减到 0 后唤醒的线程可以对线程池进行处理,比如关闭线程池。 一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 推荐两篇 AQS 原理和相关源码分析的文章: -- http://www.cnblogs.com/waterystone/p/4920797.html -- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html +- [Java 并发之 AQS 详解](https://www.cnblogs.com/waterystone/p/4920797.html) +- [Java 并发包基石-AQS 详解](https://www.cnblogs.com/chengxiao/p/7141160.html) -### 3 Semaphore(信号量)-允许多个线程同时访问 +## Semaphore(信号量) -**synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。** +`synchronized` 和 `ReentrantLock` 都是一次只允许一个线程访问某个资源,`Semaphore`(信号量)可以指定多个线程同时访问某个资源。 示例代码如下: @@ -285,24 +282,24 @@ public class SemaphoreExample1 { } ``` -执行 `acquire` 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 `release` 方法增加一个许可证,这可能会释放一个阻塞的 acquire 方法。然而,其实并没有实际的许可证这个对象,Semaphore 只是维持了一个可获得许可证的数量。 Semaphore 经常用于限制获取某种资源的线程数量。 +执行 `acquire()` 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 `release` 方法增加一个许可证,这可能会释放一个阻塞的 `acquire()` 方法。然而,其实并没有实际的许可证这个对象,`Semaphore` 只是维持了一个可获得许可证的数量。 `Semaphore` 经常用于限制获取某种资源的线程数量。 当然一次也可以一次拿取和释放多个许可,不过一般没有必要这样做: ```java semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4 test(threadnum); -semaphore.release(5);// 获取5个许可,所以可运行线程数量为20/5=4 +semaphore.release(5);// 释放5个许可 ``` -除了 `acquire`方法之外,另一个比较常用的与之对应的方法是`tryAcquire`方法,该方法如果获取不到许可就立即返回 false。 +除了 `acquire()` 方法之外,另一个比较常用的与之对应的方法是 `tryAcquire()` 方法,该方法如果获取不到许可就立即返回 false。 -Semaphore 有两种模式,公平模式和非公平模式。 +`Semaphore` 有两种模式,公平模式和非公平模式。 -- **公平模式:** 调用 acquire 的顺序就是获取许可证的顺序,遵循 FIFO; +- **公平模式:** 调用 `acquire()` 方法的顺序就是获取许可证的顺序,遵循 FIFO; - **非公平模式:** 抢占式的。 -**Semaphore 对应的两个构造方法如下:** +`Semaphore` 对应的两个构造方法如下: ```java public Semaphore(int permits) { @@ -316,23 +313,26 @@ Semaphore 有两种模式,公平模式和非公平模式。 **这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。** -[issue645补充内容](https://github.com/Snailclimb/JavaGuide/issues/645) :Semaphore与CountDownLatch一样,也是共享锁的一种实现。它默认构造AQS的state为permits。当执行任务的线程数量超出permits,那么多余的线程将会被放入阻塞队列Park,并自旋判断state是否大于0。只有当state大于0的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行release方法,release方法使得state的变量会加1,那么自旋的线程便会判断成功。 -如此,每次只有最多不超过permits数量的线程能自旋成功,便限制了执行任务线程的数量。 +[issue645 补充内容](https://github.com/Snailclimb/JavaGuide/issues/645) :`Semaphore` 与 `CountDownLatch` 一样,也是共享锁的一种实现。它默认构造 AQS 的 state 为 `permits`。当执行任务的线程数量超出 `permits`,那么多余的线程将会被放入阻塞队列 Park,并自旋判断 state 是否大于 0。只有当 state 大于 0 的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行 `release()` 方法,`release()` 方法使得 state 的变量会加 1,那么自旋的线程便会判断成功。 +如此,每次只有最多不超过 `permits` 数量的线程能自旋成功,便限制了执行任务线程的数量。 -由于篇幅问题,如果对 Semaphore 源码感兴趣的朋友可以看下这篇文章:https://juejin.im/post/5ae755366fb9a07ab508adc6 +## CountDownLatch (倒计时器) -### 4 CountDownLatch (倒计时器) +`CountDownLatch` 允许 `count` 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。 -CountDownLatch允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。在 Java 并发中,countdownlatch 的概念是一个常见的面试题,所以一定要确保你很好的理解了它。 +`CountDownLatch` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `count`。当线程使用 `countDown()` 方法时,其实使用了`tryReleaseShared`方法以 CAS 的操作来减少 `state`,直至 `state` 为 0 。当调用 `await()` 方法的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 方法就会一直阻塞,也就是说 `await()` 方法之后的语句不会被执行。然后,`CountDownLatch` 会自旋 CAS 判断 `state == 0`,如果 `state == 0` 的话,就会释放所有等待的线程,`await()` 方法之后的语句得到执行。 -CountDownLatch是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用countDown方法时,其实使用了`tryReleaseShared`方法以CAS的操作来减少state,直至state为0就代表所有的线程都调用了countDown方法。当调用await方法的时候,如果state不为0,就代表仍然有线程没有调用countDown方法,那么就把已经调用过countDown的线程都放入阻塞队列Park,并自旋CAS判断state == 0,直至最后一个线程调用了countDown,使得state == 0,于是阻塞的线程便判断成功,全部往下执行。 +### CountDownLatch 的两种典型用法 -#### 4.1 CountDownLatch 的两种典型用法 +**1、某一线程在开始运行前等待 n 个线程执行完毕。** -1. 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :`new CountDownLatch(n)`,每当一个任务线程执行完毕,就将计数器减 1 `countdownlatch.countDown()`,当计数器的值变为 0 时,在`CountDownLatch上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 -2. 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 :`new CountDownLatch(1)`,多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。 +将 `CountDownLatch` 的计数器初始化为 n (`new CountDownLatch(n)`),每当一个任务线程执行完毕,就将计数器减 1 (`countdownlatch.countDown()`),当计数器的值变为 0 时,在 `CountDownLatch 上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 -#### 4.2 CountDownLatch 的使用示例 +**2、实现多个线程开始执行任务的最大并行性。** + +注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 (`new CountDownLatch(1)`),多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 `countDown()` 时,计数器变为 0,多个线程同时被唤醒。 + +### CountDownLatch 的使用示例 ```java /** @@ -379,11 +379,11 @@ public class CountDownLatchExample1 { 上面的代码中,我们定义了请求的数量为 550,当这 550 个请求被处理完成之后,才会执行`System.out.println("finish");`。 -与 CountDownLatch 的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用 `CountDownLatch.await()` 方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。 +与 `CountDownLatch` 的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用 `CountDownLatch.await()` 方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。 其他 N 个线程必须引用闭锁对象,因为他们需要通知 `CountDownLatch` 对象,他们已经完成了各自的任务。这种通知机制是通过 `CountDownLatch.countDown()`方法来完成的;每调用一次这个方法,在构造函数中初始化的 count 值就减 1。所以当 N 个线程都调 用了这个方法,count 的值等于 0,然后主线程就能通过 `await()`方法,恢复执行自己的任务。 -再插一嘴:`CountDownLatch` 的 `await()` 方法使用不当很容易产生死锁,比如我们上面代码中的 for 循环改为: +再插一嘴:`CountDownLatch` 的 `await()` 方法使用不当很容易产生死锁,比如我们上面代码中的 for 循环改为: ```java for (int i = 0; i < threadCount-1; i++) { @@ -391,31 +391,27 @@ for (int i = 0; i < threadCount-1; i++) { } ``` -这样就导致 `count` 的值没办法等于 0,然后就会导致一直等待。 - -如果对CountDownLatch源码感兴趣的朋友,可以查看: [【JUC】JDK1.8源码分析之CountDownLatch(五)](https://www.cnblogs.com/leesf456/p/5406191.html) +这样就导致 `count` 的值没办法等于 0,然后就会导致一直等待。 -#### 4.3 CountDownLatch 的不足 +### CountDownLatch 的不足 -CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。 +`CountDownLatch` 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 `CountDownLatch` 使用完毕后,它不能再次被使用。 -#### 4.4 CountDownLatch 相常见面试题 +### CountDownLatch 相常见面试题 -解释一下 CountDownLatch 概念? +- `CountDownLatch` 怎么用?应用场景是什么? +- `CountDownLatch` 和 `CyclicBarrier` 的不同之处? +- `CountDownLatch` 类中主要的方法? -CountDownLatch 和 CyclicBarrier 的不同之处? +## CyclicBarrier(循环栅栏) -给出一些 CountDownLatch 使用的例子? +`CyclicBarrier` 和 `CountDownLatch` 非常类似,它也可以实现线程间的技术等待,但是它的功能比 `CountDownLatch` 更加复杂和强大。主要应用场景和 `CountDownLatch` 类似。 -CountDownLatch 类中主要的方法? +> `CountDownLatch` 的实现是基于 AQS 的,而 `CycliBarrier` 是基于 `ReentrantLock`(`ReentrantLock` 也属于 AQS 同步器)和 `Condition` 的。 -### 5 CyclicBarrier(循环栅栏) +`CyclicBarrier` 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 -CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。 - -> CountDownLatch的实现是基于AQS的,而CycliBarrier是基于 ReentrantLock(ReentrantLock也属于AQS同步器)和 Condition 的. - -CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用`await`方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 +`CyclicBarrier` 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用 `await()` 方法告诉 `CyclicBarrier` 我已经到达了屏障,然后当前线程被阻塞。 再来看一下它的构造函数: @@ -434,11 +430,11 @@ public CyclicBarrier(int parties, Runnable barrierAction) { 其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。 -#### 5.1 CyclicBarrier 的应用场景 +### CyclicBarrier 的应用场景 -CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。 +`CyclicBarrier` 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。 -#### 5.2 CyclicBarrier 的使用示例 +### CyclicBarrier 的使用示例 示例 1: @@ -517,9 +513,9 @@ threadnum:6is finish ...... ``` -可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候, `await`方法之后的方法才被执行。 +可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候, `await()` 方法之后的方法才被执行。 -另外,CyclicBarrier 还提供一个更高级的构造函数`CyclicBarrier(int parties, Runnable barrierAction)`,用于在线程到达屏障时,优先执行`barrierAction`,方便处理更复杂的业务场景。示例代码如下: +另外,`CyclicBarrier` 还提供一个更高级的构造函数 `CyclicBarrier(int parties, Runnable barrierAction)`,用于在线程到达屏障时,优先执行 `barrierAction`,方便处理更复杂的业务场景。示例代码如下: ```java /** @@ -595,18 +591,18 @@ threadnum:7is finish ...... ``` -#### 5.3 `CyclicBarrier`源码分析 +### CyclicBarrier 源码分析 -当调用 `CyclicBarrier` 对象调用 `await()` 方法时,实际上调用的是`dowait(false, 0L)`方法。 `await()` 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。 +当调用 `CyclicBarrier` 对象调用 `await()` 方法时,实际上调用的是 `dowait(false, 0L)`方法。 `await()` 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 `parties` 的值时,栅栏才会打开,线程才得以通过执行。 ```java - public int await() throws InterruptedException, BrokenBarrierException { - try { - return dowait(false, 0L); - } catch (TimeoutException toe) { - throw new Error(toe); // cannot happen - } - } +public int await() throws InterruptedException, BrokenBarrierException { + try { + return dowait(false, 0L); + } catch (TimeoutException toe) { + throw new Error(toe); // cannot happen + } +} ``` `dowait(false, 0L)`: @@ -692,38 +688,21 @@ threadnum:7is finish ``` -总结:`CyclicBarrier` 内部通过一个 count 变量作为计数器,cout 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。 +总结:`CyclicBarrier` 内部通过一个 count 变量作为计数器,count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。 -#### 5.4 CyclicBarrier 和 CountDownLatch 的区别 +### CyclicBarrier 和 CountDownLatch 的区别 -**下面这个是国外一个大佬的回答:** +下面这个是国外一个大佬的回答: -CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用。但是我不那么认为它们之间的区别仅仅就是这么简单的一点。我们来从 jdk 作者设计的目的来看,javadoc 是这么描述它们的: +`CountDownLatch` 是计数器,只能使用一次,而 `CyclicBarrier` 的计数器提供 `reset` 功能,可以多次使用。但是我不那么认为它们之间的区别仅仅就是这么简单的一点。我们来从 jdk 作者设计的目的来看,javadoc 是这么描述它们的: > CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;) > CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。) -对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。 - -CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。 - -### 6 ReentrantLock 和 ReentrantReadWriteLock - -ReentrantLock 和 synchronized 的区别在上面已经讲过了这里就不多做讲解。另外,需要注意的是:读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。 - -### 参考 - -- https://juejin.im/post/5ae755256fb9a07ac3634067 -- https://blog.csdn.net/u010185262/article/details/54692886 -- https://blog.csdn.net/tolcf/article/details/50925145?utm_source=blogxgwz0 - -### 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取! +对于 `CountDownLatch` 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 `CyclicBarrier`,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。 -**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 +`CountDownLatch` 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 `CyclicBarrier` 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。 -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) +### ReentrantLock 和 ReentrantReadWriteLock +`ReentrantLock` 和 `synchronized` 的区别在上面已经讲过了这里就不多做讲解。另外,需要注意的是:读写锁 `ReentrantReadWriteLock` 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。 diff --git a/docs/java/Multithread/Atomic.md "b/docs/java/concurrent/atomic\345\216\237\345\255\220\347\261\273\346\200\273\347\273\223.md" similarity index 82% rename from docs/java/Multithread/Atomic.md rename to "docs/java/concurrent/atomic\345\216\237\345\255\220\347\261\273\346\200\273\347\273\223.md" index a63d16d956c..7628e1fe7d2 100644 --- a/docs/java/Multithread/Atomic.md +++ "b/docs/java/concurrent/atomic\345\216\237\345\255\220\347\261\273\346\200\273\347\273\223.md" @@ -1,30 +1,12 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - -> 个人觉得这一节掌握基本的使用即可! - - - -- [1 Atomic 原子类介绍](#1-atomic-原子类介绍) -- [2 基本类型原子类](#2-基本类型原子类) - - [2.1 基本类型原子类介绍](#21-基本类型原子类介绍) - - [2.2 AtomicInteger 常见方法使用](#22-atomicinteger-常见方法使用) - - [2.3 基本数据类型原子类的优势](#23-基本数据类型原子类的优势) - - [2.4 AtomicInteger 线程安全原理简单分析](#24-atomicinteger-线程安全原理简单分析) -- [3 数组类型原子类](#3-数组类型原子类) - - [3.1 数组类型原子类介绍](#31-数组类型原子类介绍) - - [3.2 AtomicIntegerArray 常见方法使用](#32-atomicintegerarray-常见方法使用) -- [4 引用类型原子类](#4-引用类型原子类) - - [4.1 引用类型原子类介绍](#41--引用类型原子类介绍) - - [4.2 AtomicReference 类使用示例](#42-atomicreference-类使用示例) - - [4.3 AtomicStampedReference 类使用示例](#43-atomicstampedreference-类使用示例) - - [4.4 AtomicMarkableReference 类使用示例](#44-atomicmarkablereference-类使用示例) -- [5 对象的属性修改类型原子类](#5-对象的属性修改类型原子类) - - [5.1 对象的属性修改类型原子类介绍](#51-对象的属性修改类型原子类介绍) - - [5.2 AtomicIntegerFieldUpdater 类使用示例](#52-atomicintegerfieldupdater-类使用示例) - - - -### 1 Atomic 原子类介绍 +--- +title: Atomic 原子类总结 +category: Java +tag: + - Java并发 +--- + + +## Atomic 原子类介绍 Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 @@ -34,21 +16,20 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是 ![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) -根据操作的数据类型,可以将JUC包中的原子类分为4类 +根据操作的数据类型,可以将 JUC 包中的原子类分为 4 类 -**基本类型** +**基本类型** 使用原子的方式更新基本类型 - AtomicInteger:整型原子类 - AtomicLong:长整型原子类 -- AtomicBoolean :布尔型原子类 +- AtomicBoolean :布尔型原子类 **数组类型** 使用原子的方式更新数组里的某个元素 - - AtomicIntegerArray:整型数组原子类 - AtomicLongArray:长整型数组原子类 - AtomicReferenceArray :引用类型数组原子类 @@ -65,7 +46,7 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是 - AtomicLongFieldUpdater:原子更新长整型字段的更新器 - AtomicReferenceFieldUpdater:原子更新引用类型里的字段 -> 修正: **AtomicMarkableReference 不能解决ABA问题** **[issue#626](https://github.com/Snailclimb/JavaGuide/issues/626)** +> **🐛 修正(参见:[issue#626](https://github.com/Snailclimb/JavaGuide/issues/626))** : `AtomicMarkableReference` 不能解决 ABA 问题。 ```java /** @@ -80,7 +61,7 @@ AtomicMarkableReference是将一个boolean值作是否有更改的标记,本 */ public class SolveABAByAtomicMarkableReference { - + private static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(100, false); public static void main(String[] args) { @@ -113,9 +94,10 @@ public class SolveABAByAtomicMarkableReference { ``` **CAS ABA 问题** + - 描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。 - 例子描述(可能不太合适,但好理解): 年初,现金为零,然后通过正常劳动赚了三百万,之后正常消费了(比如买房子)三百万。年末,虽然现金零收入(可能变成其他形式了),但是赚了钱是事实,还是得交税的! -- 代码例子(以``` AtomicInteger ```为例) +- 代码例子(以`AtomicInteger`为例) ```java import java.util.concurrent.atomic.AtomicInteger; @@ -189,19 +171,19 @@ Thread-0 ------ currentValue=1, finalValue=2, compareAndSet Result=true 下面我们来详细介绍一下这些原子类。 -### 2 基本类型原子类 +## 基本类型原子类 -#### 2.1 基本类型原子类介绍 +### 基本类型原子类介绍 使用原子的方式更新基本类型 - AtomicInteger:整型原子类 - AtomicLong:长整型原子类 -- AtomicBoolean :布尔型原子类 +- AtomicBoolean :布尔型原子类 上面三个类提供的方法几乎相同,所以我们这里以 AtomicInteger 为例子来介绍。 - **AtomicInteger 类常用方法** +**AtomicInteger 类常用方法** ```java public final int get() //获取当前的值 @@ -213,7 +195,7 @@ boolean compareAndSet(int expect, int update) //如果输入的数值等于预 public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 ``` -#### 2.2 AtomicInteger 常见方法使用 +### AtomicInteger 常见方法使用 ```java import java.util.concurrent.atomic.AtomicInteger; @@ -235,18 +217,18 @@ public class AtomicIntegerTest { } ``` -#### 2.3 基本数据类型原子类的优势 +### 基本数据类型原子类的优势 通过一个简单例子带大家看一下基本数据类型原子类的优势 -**①多线程环境不使用原子类保证线程安全(基本数据类型)** +**① 多线程环境不使用原子类保证线程安全(基本数据类型)** ```java class Test { private volatile int count = 0; //若要线程安全执行执行count++,需要加锁 public synchronized void increment() { - count++; + count++; } public int getCount() { @@ -254,7 +236,8 @@ class Test { } } ``` -**②多线程环境使用原子类保证线程安全(基本数据类型)** + +**② 多线程环境使用原子类保证线程安全(基本数据类型)** ```java class Test2 { @@ -270,7 +253,8 @@ class Test2 { } ``` -#### 2.4 AtomicInteger 线程安全原理简单分析 + +### AtomicInteger 线程安全原理简单分析 AtomicInteger 类的部分源码: @@ -291,16 +275,14 @@ AtomicInteger 类的部分源码: AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 -CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 - +CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 -### 3 数组类型原子类 +## 数组类型原子类 -#### 3.1 数组类型原子类介绍 +### 数组类型原子类介绍 使用原子的方式更新数组里的某个元素 - - AtomicIntegerArray:整形数组原子类 - AtomicLongArray:长整形数组原子类 - AtomicReferenceArray :引用类型数组原子类 @@ -314,11 +296,12 @@ public final int get(int i) //获取 index=i 位置元素的值 public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增 public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减 -public final int getAndAdd(int delta) //获取 index=i 位置元素的值,并加上预期的值 -boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update) +public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值 +boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update) public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 ``` -#### 3.2 AtomicIntegerArray 常见方法使用 + +### AtomicIntegerArray 常见方法使用 ```java @@ -345,9 +328,9 @@ public class AtomicIntegerArrayTest { } ``` -### 4 引用类型原子类 +## 引用类型原子类 -#### 4.1 引用类型原子类介绍 +### 引用类型原子类介绍 基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。 @@ -357,7 +340,7 @@ public class AtomicIntegerArrayTest { 上面三个类提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。 -#### 4.2 AtomicReference 类使用示例 +### AtomicReference 类使用示例 ```java import java.util.concurrent.atomic.AtomicReference; @@ -404,13 +387,15 @@ class Person { } ``` + 上述代码首先创建了一个 Person 对象,然后把 Person 对象设置进 AtomicReference 对象中,然后调用 compareAndSet 方法,该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 person 的话,则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 方法相同。运行上面的代码后的输出结果如下: ``` Daisy 20 ``` -#### 4.3 AtomicStampedReference 类使用示例 + +### AtomicStampedReference 类使用示例 ```java import java.util.concurrent.atomic.AtomicStampedReference; @@ -459,6 +444,7 @@ public class AtomicStampedReferenceDemo { ``` 输出结果如下: + ``` currentValue=0, currentStamp=0 currentValue=666, currentStamp=999, casResult=true @@ -468,9 +454,9 @@ currentValue=0, currentStamp=0 currentValue=666, currentStamp=999, wCasResult=true ``` -#### 4.4 AtomicMarkableReference 类使用示例 +### AtomicMarkableReference 类使用示例 -``` java +```java import java.util.concurrent.atomic.AtomicMarkableReference; public class AtomicMarkableReferenceDemo { @@ -517,6 +503,7 @@ public class AtomicMarkableReferenceDemo { ``` 输出结果如下: + ``` currentValue=null, currentMark=false currentValue=true, currentMark=true, casResult=true @@ -526,9 +513,9 @@ currentValue=null, currentMark=false currentValue=true, currentMark=true, wCasResult=true ``` -### 5 对象的属性修改类型原子类 +## 对象的属性修改类型原子类 -#### 5.1 对象的属性修改类型原子类介绍 +### 对象的属性修改类型原子类介绍 如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。 @@ -540,7 +527,7 @@ currentValue=true, currentMark=true, wCasResult=true 上面三个类提供的方法几乎相同,所以我们这里以 `AtomicIntegerFieldUpdater`为例子来介绍。 -#### 5.2 AtomicIntegerFieldUpdater 类使用示例 +### AtomicIntegerFieldUpdater 类使用示例 ```java import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; @@ -593,14 +580,4 @@ class User { ## Reference -- 《Java并发编程的艺术》 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) \ No newline at end of file +- 《Java 并发编程的艺术》 diff --git a/docs/java/concurrent/completablefuture-intro.md b/docs/java/concurrent/completablefuture-intro.md new file mode 100644 index 00000000000..810d5a61708 --- /dev/null +++ b/docs/java/concurrent/completablefuture-intro.md @@ -0,0 +1,526 @@ +--- +title: CompletableFuture入门 +category: Java +tag: + - Java并发 +--- + + +自己在项目中使用 `CompletableFuture` 比较多,看到很多开源框架中也大量使用到了 `CompletableFuture` 。 + +因此,专门写一篇文章来介绍这个 Java 8 才被引入的一个非常有用的用于异步编程的类。 + +## 简单介绍 + +`CompletableFuture` 同时实现了 `Future` 和 `CompletionStage` 接口。 + +```java +public class CompletableFuture implements Future, CompletionStage { +} +``` + +`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程的能力。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/image-20210902092441434.png) + +`Future` 接口有 5 个方法: + +- `boolean cancel(boolean mayInterruptIfRunning)` :尝试取消执行任务。 +- `boolean isCancelled()` :判断任务是否被取消。 +- `boolean isDone()` : 判断任务是否已经被执行完成。 +- `get()` :等待任务执行完成并获取运算结果。 +- `get(long timeout, TimeUnit unit)` :多了一个超时时间。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/image-20210902093026059.png) + +`CompletionStage` 接口中的方法比较多,`CompletableFuture` 的函数式能力就是这个接口赋予的。从这个接口的方法参数你就可以发现其大量使用了 Java8 引入的函数式编程。 + +由于方法众多,所以这里不能一一讲解,下文中我会介绍大部分常见方法的使用。 + +## 常见操作 + +### 创建 CompletableFuture + +常见的创建 `CompletableFuture` 对象的方法如下: + +1. 通过 new 关键字。 +2. 基于 `CompletableFuture` 自带的静态工厂方法:`runAsync()` 、`supplyAsync()` 。 + +#### new 关键字 + +通过 new 关键字创建 `CompletableFuture` 对象这种使用方式可以看作是将 `CompletableFuture` 当做 `Future` 来使用。 + +我在我的开源项目 [guide-rpc-framework](https://github.com/Snailclimb/guide-rpc-framework) 中就是这种方式创建的 `CompletableFuture` 对象。 + +下面咱们来看一个简单的案例。 + +我们通过创建了一个结果值类型为 `RpcResponse` 的 `CompletableFuture`,你可以把 `resultFuture` 看作是异步运算结果的载体。 + +```java +CompletableFuture> resultFuture = new CompletableFuture<>(); +``` + +假设在未来的某个时刻,我们得到了最终的结果。这时,我们可以调用 `complete()` 方法为其传入结果,这表示 `resultFuture` 已经被完成了。 + +```java +// complete() 方法只能调用一次,后续调用将被忽略。 +resultFuture.complete(rpcResponse); +``` + +你可以通过 `isDone()` 方法来检查是否已经完成。 + +```java +public boolean isDone() { + return result != null; +} +``` + +获取异步计算的结果也非常简单,直接调用 `get()` 方法即可! + +```java +rpcResponse = completableFuture.get(); +``` + +注意 : `get()` 方法并不会阻塞,因为我们已经知道异步运算的结果了。 + +如果你已经知道计算的结果的话,可以使用静态方法 `completedFuture()` 来创建 `CompletableFuture` 。 + +```java +CompletableFuture future = CompletableFuture.completedFuture("hello!"); +assertEquals("hello!", future.get()); +``` + +`completedFuture()` 方法底层调用的是带参数的 new 方法,只不过,这个方法不对外暴露。 + +```java +public static CompletableFuture completedFuture(U value) { + return new CompletableFuture((value == null) ? NIL : value); +} +``` + +#### 静态工厂方法 + +这两个方法可以帮助我们封装计算逻辑。 + +```java +static CompletableFuture supplyAsync(Supplier supplier); +// 使用自定义线程池(推荐) +static CompletableFuture supplyAsync(Supplier supplier, Executor executor); +static CompletableFuture runAsync(Runnable runnable); +// 使用自定义线程池(推荐) +static CompletableFuture runAsync(Runnable runnable, Executor executor); +``` + +`runAsync()` 方法接受的参数是 `Runnable` ,这是一个函数式接口,不允许返回值。当你需要异步操作且不关心返回结果的时候可以使用 `runAsync()` 方法。 + +```java +@FunctionalInterface +public interface Runnable { + public abstract void run(); +} +``` + +`supplyAsync()` 方法接受的参数是 `Supplier` ,这也是一个函数式接口,`U` 是返回结果值的类型。 + +```java +@FunctionalInterface +public interface Supplier { + + /** + * Gets a result. + * + * @return a result + */ + T get(); +} +``` + +当你需要异步操作且关心返回结果的时候,可以使用 `supplyAsync()` 方法。 + +```java +CompletableFuture future = CompletableFuture.runAsync(() -> System.out.println("hello!")); +future.get();// 输出 "hello!" +CompletableFuture future2 = CompletableFuture.supplyAsync(() -> "hello!"); +assertEquals("hello!", future2.get()); +``` + +### 处理异步结算的结果 + +当我们获取到异步计算的结果之后,还可以对其进行进一步的处理,比较常用的方法有下面几个: + +- `thenApply()` +- `thenAccept()` +- `thenRun()` +- `whenComplete()` + +`thenApply()` 方法接受一个 `Function` 实例,用它来处理结果。 + +```java +// 沿用上一个任务的线程池 +public CompletableFuture thenApply( + Function fn) { + return uniApplyStage(null, fn); +} + +//使用默认的 ForkJoinPool 线程池(不推荐) +public CompletableFuture thenApplyAsync( + Function fn) { + return uniApplyStage(defaultExecutor(), fn); +} +// 使用自定义线程池(推荐) +public CompletableFuture thenApplyAsync( + Function fn, Executor executor) { + return uniApplyStage(screenExecutor(executor), fn); +} +``` + +`thenApply()` 方法使用示例如下: + +```java +CompletableFuture future = CompletableFuture.completedFuture("hello!") + .thenApply(s -> s + "world!"); +assertEquals("hello!world!", future.get()); +// 这次调用将被忽略。 +future.thenApply(s -> s + "nice!"); +assertEquals("hello!world!", future.get()); +``` + +你还可以进行 **流式调用**: + +```java +CompletableFuture future = CompletableFuture.completedFuture("hello!") + .thenApply(s -> s + "world!").thenApply(s -> s + "nice!"); +assertEquals("hello!world!nice!", future.get()); +``` + +**如果你不需要从回调函数中获取返回结果,可以使用 `thenAccept()` 或者 `thenRun()`。这两个方法的区别在于 `thenRun()` 不能访问异步计算的结果。** + +`thenAccept()` 方法的参数是 `Consumer` 。 + +```java +public CompletableFuture thenAccept(Consumer action) { + return uniAcceptStage(null, action); +} + +public CompletableFuture thenAcceptAsync(Consumer action) { + return uniAcceptStage(defaultExecutor(), action); +} + +public CompletableFuture thenAcceptAsync(Consumer action, + Executor executor) { + return uniAcceptStage(screenExecutor(executor), action); +} +``` + +顾名思义,`Consumer` 属于消费型接口,它可以接收 1 个输入对象然后进行“消费”。 + +```java +@FunctionalInterface +public interface Consumer { + + void accept(T t); + + default Consumer andThen(Consumer after) { + Objects.requireNonNull(after); + return (T t) -> { accept(t); after.accept(t); }; + } +} +``` + +`thenRun()` 的方法是的参数是 `Runnable` 。 + +```java +public CompletableFuture thenRun(Runnable action) { + return uniRunStage(null, action); +} + +public CompletableFuture thenRunAsync(Runnable action) { + return uniRunStage(defaultExecutor(), action); +} + +public CompletableFuture thenRunAsync(Runnable action, + Executor executor) { + return uniRunStage(screenExecutor(executor), action); +} +``` + +`thenAccept()` 和 `thenRun()` 使用示例如下: + +```java +CompletableFuture.completedFuture("hello!") + .thenApply(s -> s + "world!").thenApply(s -> s + "nice!").thenAccept(System.out::println);//hello!world!nice! + +CompletableFuture.completedFuture("hello!") + .thenApply(s -> s + "world!").thenApply(s -> s + "nice!").thenRun(() -> System.out.println("hello!"));//hello! +``` + +`whenComplete()` 的方法的参数是 `BiConsumer` 。 + +```java +public CompletableFuture whenComplete( + BiConsumer action) { + return uniWhenCompleteStage(null, action); +} + + +public CompletableFuture whenCompleteAsync( + BiConsumer action) { + return uniWhenCompleteStage(defaultExecutor(), action); +} +// 使用自定义线程池(推荐) +public CompletableFuture whenCompleteAsync( + BiConsumer action, Executor executor) { + return uniWhenCompleteStage(screenExecutor(executor), action); +} +``` + +相对于 `Consumer` , `BiConsumer` 可以接收 2 个输入对象然后进行“消费”。 + +```java +@FunctionalInterface +public interface BiConsumer { + void accept(T t, U u); + + default BiConsumer andThen(BiConsumer after) { + Objects.requireNonNull(after); + + return (l, r) -> { + accept(l, r); + after.accept(l, r); + }; + } +} +``` + +`whenComplete()` 使用示例如下: + +```java +CompletableFuture future = CompletableFuture.supplyAsync(() -> "hello!") + .whenComplete((res, ex) -> { + // res 代表返回的结果 + // ex 的类型为 Throwable ,代表抛出的异常 + System.out.println(res); + // 这里没有抛出异常所有为 null + assertNull(ex); + }); +assertEquals("hello!", future.get()); +``` + +### 异常处理 + +你可以通过 `handle()` 方法来处理任务执行过程中可能出现的抛出异常的情况。 + +```java +public CompletableFuture handle( + BiFunction fn) { + return uniHandleStage(null, fn); +} + +public CompletableFuture handleAsync( + BiFunction fn) { + return uniHandleStage(defaultExecutor(), fn); +} + +public CompletableFuture handleAsync( + BiFunction fn, Executor executor) { + return uniHandleStage(screenExecutor(executor), fn); +} +``` + +示例代码如下: + +```java +CompletableFuture future + = CompletableFuture.supplyAsync(() -> { + if (true) { + throw new RuntimeException("Computation error!"); + } + return "hello!"; +}).handle((res, ex) -> { + // res 代表返回的结果 + // ex 的类型为 Throwable ,代表抛出的异常 + return res != null ? res : "world!"; +}); +assertEquals("world!", future.get()); +``` + +你还可以通过 `exceptionally()` 方法来处理异常情况。 + +```java +CompletableFuture future + = CompletableFuture.supplyAsync(() -> { + if (true) { + throw new RuntimeException("Computation error!"); + } + return "hello!"; +}).exceptionally(ex -> { + System.out.println(ex.toString());// CompletionException + return "world!"; +}); +assertEquals("world!", future.get()); +``` + +如果你想让 `CompletableFuture` 的结果就是异常的话,可以使用 `completeExceptionally()` 方法为其赋值。 + +```java +CompletableFuture completableFuture = new CompletableFuture<>(); +// ... +completableFuture.completeExceptionally( + new RuntimeException("Calculation failed!")); +// ... +completableFuture.get(); // ExecutionException +``` + +### 组合 CompletableFuture + +你可以使用 `thenCompose()` 按顺序链接两个 `CompletableFuture` 对象。 + +```java +public CompletableFuture thenCompose( + Function> fn) { + return uniComposeStage(null, fn); +} + +public CompletableFuture thenComposeAsync( + Function> fn) { + return uniComposeStage(defaultExecutor(), fn); +} + +public CompletableFuture thenComposeAsync( + Function> fn, + Executor executor) { + return uniComposeStage(screenExecutor(executor), fn); +} +``` + +`thenCompose()` 方法会使用示例如下: + +```java +CompletableFuture future + = CompletableFuture.supplyAsync(() -> "hello!") + .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "world!")); +assertEquals("hello!world!", future.get()); +``` + +在实际开发中,这个方法还是非常有用的。比如说,我们先要获取用户信息然后再用用户信息去做其他事情。 + +和 `thenCompose()` 方法类似的还有 `thenCombine()` 方法, `thenCombine()` 同样可以组合两个 `CompletableFuture` 对象。 + +```java +CompletableFuture completableFuture + = CompletableFuture.supplyAsync(() -> "hello!") + .thenCombine(CompletableFuture.supplyAsync( + () -> "world!"), (s1, s2) -> s1 + s2) + .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "nice!")); +assertEquals("hello!world!nice!", completableFuture.get()); +``` + +**那 `thenCompose()` 和 `thenCombine()` 有什么区别呢?** + +- `thenCompose()` 可以两个 `CompletableFuture` 对象,并将前一个任务的返回结果作为下一个任务的参数,它们之间存在着先后顺序。 +- `thenCombine()` 会在两个任务都执行完成后,把两个任务的结果合并。两个任务是并行执行的,它们之间并没有先后依赖顺序。 + +### 并行运行多个 CompletableFuture + +你可以通过 `CompletableFuture` 的 `allOf()`这个静态方法来并行运行多个 `CompletableFuture` 。 + +实际项目中,我们经常需要并行运行多个互不相关的任务,这些任务之间没有依赖关系,可以互相独立地运行。 + +比说我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。像这种情况我们就可以使用并行运行多个 `CompletableFuture` 来处理。 + +示例代码如下: + +```java +CompletableFuture task1 = + CompletableFuture.supplyAsync(()->{ + //自定义业务操作 + }); +...... +CompletableFuture task6 = + CompletableFuture.supplyAsync(()->{ + //自定义业务操作 + }); +...... + CompletableFuture headerFuture=CompletableFuture.allOf(task1,.....,task6); + + try { + headerFuture.join(); + } catch (Exception ex) { + ...... + } +System.out.println("all done. "); +``` + +经常和 `allOf()` 方法拿来对比的是 `anyOf()` 方法。 + +**`allOf()` 方法会等到所有的 `CompletableFuture` 都运行完成之后再返回** + +```java +Random rand = new Random(); +CompletableFuture future1 = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(1000 + rand.nextInt(1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + System.out.println("future1 done..."); + } + return "abc"; +}); +CompletableFuture future2 = CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(1000 + rand.nextInt(1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + System.out.println("future2 done..."); + } + return "efg"; +}); +``` + +调用 `join()` 可以让程序等`future1` 和 `future2` 都运行完了之后再继续执行。 + +```java +CompletableFuture completableFuture = CompletableFuture.allOf(future1, future2); +completableFuture.join(); +assertTrue(completableFuture.isDone()); +System.out.println("all futures done..."); +``` + +输出: + +```java +future1 done... +future2 done... +all futures done... +``` + +**`anyOf()` 方法不会等待所有的 `CompletableFuture` 都运行完成之后再返回,只要有一个执行完成即可!** + +```java +CompletableFuture f = CompletableFuture.anyOf(future1, future2); +System.out.println(f.get()); +``` + +输出结果可能是: + +```java +future2 done... +efg +``` + +也可能是: + +``` +future1 done... +abc +``` + +## 后记 + +这篇文章只是简单介绍了 `CompletableFuture` 比较常用的一些 API 。 + +如果想要深入学习的话,可以多找一些书籍和博客看。 + +另外,建议G友们可以看看京东的 [asyncTool](https://gitee.com/jd-platform-opensource/asyncTool) 这个并发框架,里面大量使用到了 `CompletableFuture` 。 diff --git "a/docs/java/concurrent/images/ThreadLocal\345\206\205\351\203\250\347\261\273.png" "b/docs/java/concurrent/images/ThreadLocal\345\206\205\351\203\250\347\261\273.png" new file mode 100644 index 00000000000..6997f5cacfc Binary files /dev/null and "b/docs/java/concurrent/images/ThreadLocal\345\206\205\351\203\250\347\261\273.png" differ diff --git "a/docs/java/concurrent/images/interview-questions/synchronized\345\205\263\351\224\256\345\255\227.png" "b/docs/java/concurrent/images/interview-questions/synchronized\345\205\263\351\224\256\345\255\227.png" new file mode 100644 index 00000000000..24ac1a8cc26 Binary files /dev/null and "b/docs/java/concurrent/images/interview-questions/synchronized\345\205\263\351\224\256\345\255\227.png" differ diff --git "a/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/CachedThreadPool-execute.png" "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/CachedThreadPool-execute.png" new file mode 100644 index 00000000000..8b2ede8ab54 Binary files /dev/null and "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/CachedThreadPool-execute.png" differ diff --git "a/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executors\345\267\245\345\205\267\347\261\273.png" "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executors\345\267\245\345\205\267\347\261\273.png" new file mode 100644 index 00000000000..87658aa3f09 Binary files /dev/null and "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executors\345\267\245\345\205\267\347\261\273.png" differ diff --git "a/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executor\346\241\206\346\236\266\347\232\204\344\275\277\347\224\250\347\244\272\346\204\217\345\233\276.png" "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executor\346\241\206\346\236\266\347\232\204\344\275\277\347\224\250\347\244\272\346\204\217\345\233\276.png" new file mode 100644 index 00000000000..5cc148dd79a Binary files /dev/null and "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executor\346\241\206\346\236\266\347\232\204\344\275\277\347\224\250\347\244\272\346\204\217\345\233\276.png" differ diff --git "a/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/FixedThreadPool.png" "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/FixedThreadPool.png" new file mode 100644 index 00000000000..fc1c7034fdf Binary files /dev/null and "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/FixedThreadPool.png" differ diff --git "a/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\211\247\350\241\214\345\221\250\346\234\237\344\273\273\345\212\241\346\255\245\351\252\244.png" "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\211\247\350\241\214\345\221\250\346\234\237\344\273\273\345\212\241\346\255\245\351\252\244.png" new file mode 100644 index 00000000000..c56521d283e Binary files /dev/null and "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\211\247\350\241\214\345\221\250\346\234\237\344\273\273\345\212\241\346\255\245\351\252\244.png" differ diff --git "a/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\234\272\345\210\266.png" "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\234\272\345\210\266.png" new file mode 100644 index 00000000000..bae0dc5b2dc Binary files /dev/null and "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\234\272\345\210\266.png" differ diff --git "a/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/SingleThreadExecutor.png" "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/SingleThreadExecutor.png" new file mode 100644 index 00000000000..c933674fad4 Binary files /dev/null and "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/SingleThreadExecutor.png" differ diff --git "a/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" new file mode 100644 index 00000000000..30c298591bc Binary files /dev/null and "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" differ diff --git "a/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\344\273\273\345\212\241\347\232\204\346\211\247\350\241\214\347\233\270\345\205\263\346\216\245\345\217\243.png" "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\344\273\273\345\212\241\347\232\204\346\211\247\350\241\214\347\233\270\345\205\263\346\216\245\345\217\243.png" new file mode 100644 index 00000000000..6aebd60b591 Binary files /dev/null and "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\344\273\273\345\212\241\347\232\204\346\211\247\350\241\214\347\233\270\345\205\263\346\216\245\345\217\243.png" differ diff --git "a/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\345\233\276\350\247\243\347\272\277\347\250\213\346\261\240\345\256\236\347\216\260\345\216\237\347\220\206.png" "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\345\233\276\350\247\243\347\272\277\347\250\213\346\261\240\345\256\236\347\216\260\345\216\237\347\220\206.png" new file mode 100644 index 00000000000..bc661944a0a Binary files /dev/null and "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\345\233\276\350\247\243\347\272\277\347\250\213\346\261\240\345\256\236\347\216\260\345\216\237\347\220\206.png" differ diff --git "a/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\347\272\277\347\250\213\346\261\240\345\220\204\344\270\252\345\217\202\346\225\260\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\347\272\277\347\250\213\346\261\240\345\220\204\344\270\252\345\217\202\346\225\260\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" new file mode 100644 index 00000000000..d609943bafe Binary files /dev/null and "b/docs/java/concurrent/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\347\272\277\347\250\213\346\261\240\345\220\204\344\270\252\345\217\202\346\225\260\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" differ diff --git a/docs/java/concurrent/images/thread-local/1.png b/docs/java/concurrent/images/thread-local/1.png new file mode 100644 index 00000000000..b394e304a75 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/1.png differ diff --git a/docs/java/concurrent/images/thread-local/10.png b/docs/java/concurrent/images/thread-local/10.png new file mode 100644 index 00000000000..c9edb13f4f1 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/10.png differ diff --git a/docs/java/concurrent/images/thread-local/11.png b/docs/java/concurrent/images/thread-local/11.png new file mode 100644 index 00000000000..06d30638805 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/11.png differ diff --git a/docs/java/concurrent/images/thread-local/12.png b/docs/java/concurrent/images/thread-local/12.png new file mode 100644 index 00000000000..bd765217654 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/12.png differ diff --git a/docs/java/concurrent/images/thread-local/13.png b/docs/java/concurrent/images/thread-local/13.png new file mode 100644 index 00000000000..34c8d8c8ebe Binary files /dev/null and b/docs/java/concurrent/images/thread-local/13.png differ diff --git a/docs/java/concurrent/images/thread-local/14.png b/docs/java/concurrent/images/thread-local/14.png new file mode 100644 index 00000000000..b1b3abd63e3 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/14.png differ diff --git a/docs/java/concurrent/images/thread-local/15.png b/docs/java/concurrent/images/thread-local/15.png new file mode 100644 index 00000000000..25f5cb2b9ce Binary files /dev/null and b/docs/java/concurrent/images/thread-local/15.png differ diff --git a/docs/java/concurrent/images/thread-local/16.png b/docs/java/concurrent/images/thread-local/16.png new file mode 100644 index 00000000000..45d0424fe60 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/16.png differ diff --git a/docs/java/concurrent/images/thread-local/17.png b/docs/java/concurrent/images/thread-local/17.png new file mode 100644 index 00000000000..3194e4a3e25 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/17.png differ diff --git a/docs/java/concurrent/images/thread-local/18.png b/docs/java/concurrent/images/thread-local/18.png new file mode 100644 index 00000000000..2b340b0b565 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/18.png differ diff --git a/docs/java/concurrent/images/thread-local/19.png b/docs/java/concurrent/images/thread-local/19.png new file mode 100644 index 00000000000..4c9062794f7 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/19.png differ diff --git a/docs/java/concurrent/images/thread-local/2.png b/docs/java/concurrent/images/thread-local/2.png new file mode 100644 index 00000000000..c9af80e1e42 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/2.png differ diff --git a/docs/java/concurrent/images/thread-local/20.png b/docs/java/concurrent/images/thread-local/20.png new file mode 100644 index 00000000000..234c32a66ad Binary files /dev/null and b/docs/java/concurrent/images/thread-local/20.png differ diff --git a/docs/java/concurrent/images/thread-local/21.png b/docs/java/concurrent/images/thread-local/21.png new file mode 100644 index 00000000000..1b5a02b91c9 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/21.png differ diff --git a/docs/java/concurrent/images/thread-local/22.png b/docs/java/concurrent/images/thread-local/22.png new file mode 100644 index 00000000000..62eeff1215e Binary files /dev/null and b/docs/java/concurrent/images/thread-local/22.png differ diff --git a/docs/java/concurrent/images/thread-local/23.png b/docs/java/concurrent/images/thread-local/23.png new file mode 100644 index 00000000000..0b4a0409664 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/23.png differ diff --git a/docs/java/concurrent/images/thread-local/24.png b/docs/java/concurrent/images/thread-local/24.png new file mode 100644 index 00000000000..ae2fe0df682 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/24.png differ diff --git a/docs/java/concurrent/images/thread-local/25.png b/docs/java/concurrent/images/thread-local/25.png new file mode 100644 index 00000000000..9c66e254ec4 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/25.png differ diff --git a/docs/java/concurrent/images/thread-local/26.png b/docs/java/concurrent/images/thread-local/26.png new file mode 100644 index 00000000000..ef53f0a9bf7 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/26.png differ diff --git a/docs/java/concurrent/images/thread-local/27.png b/docs/java/concurrent/images/thread-local/27.png new file mode 100644 index 00000000000..617100507bf Binary files /dev/null and b/docs/java/concurrent/images/thread-local/27.png differ diff --git a/docs/java/concurrent/images/thread-local/28.png b/docs/java/concurrent/images/thread-local/28.png new file mode 100644 index 00000000000..68f67602412 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/28.png differ diff --git a/docs/java/concurrent/images/thread-local/29.png b/docs/java/concurrent/images/thread-local/29.png new file mode 100644 index 00000000000..d662f80b527 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/29.png differ diff --git a/docs/java/concurrent/images/thread-local/3.png b/docs/java/concurrent/images/thread-local/3.png new file mode 100644 index 00000000000..a0b418c0fc0 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/3.png differ diff --git a/docs/java/concurrent/images/thread-local/30.png b/docs/java/concurrent/images/thread-local/30.png new file mode 100644 index 00000000000..27ec27f74e6 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/30.png differ diff --git a/docs/java/concurrent/images/thread-local/31.png b/docs/java/concurrent/images/thread-local/31.png new file mode 100644 index 00000000000..96b83ca1c0a Binary files /dev/null and b/docs/java/concurrent/images/thread-local/31.png differ diff --git a/docs/java/concurrent/images/thread-local/4.png b/docs/java/concurrent/images/thread-local/4.png new file mode 100644 index 00000000000..b0278b70188 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/4.png differ diff --git a/docs/java/concurrent/images/thread-local/5.png b/docs/java/concurrent/images/thread-local/5.png new file mode 100644 index 00000000000..81f24a08b70 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/5.png differ diff --git a/docs/java/concurrent/images/thread-local/6.png b/docs/java/concurrent/images/thread-local/6.png new file mode 100644 index 00000000000..66dc77c110b Binary files /dev/null and b/docs/java/concurrent/images/thread-local/6.png differ diff --git a/docs/java/concurrent/images/thread-local/7.png b/docs/java/concurrent/images/thread-local/7.png new file mode 100644 index 00000000000..d13653a05c9 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/7.png differ diff --git a/docs/java/concurrent/images/thread-local/8.png b/docs/java/concurrent/images/thread-local/8.png new file mode 100644 index 00000000000..b7e466d838a Binary files /dev/null and b/docs/java/concurrent/images/thread-local/8.png differ diff --git a/docs/java/concurrent/images/thread-local/9.png b/docs/java/concurrent/images/thread-local/9.png new file mode 100644 index 00000000000..5964fb0c4d1 Binary files /dev/null and b/docs/java/concurrent/images/thread-local/9.png differ diff --git a/docs/java/concurrent/images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png b/docs/java/concurrent/images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png new file mode 100644 index 00000000000..62f2c3e3733 Binary files /dev/null and b/docs/java/concurrent/images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png differ diff --git a/docs/java/concurrent/images/thread-pool/1bc44c67-26ba-42ab-bcb8-4e29e6fd99b9.png b/docs/java/concurrent/images/thread-pool/1bc44c67-26ba-42ab-bcb8-4e29e6fd99b9.png new file mode 100644 index 00000000000..1dc7e4b6d7e Binary files /dev/null and b/docs/java/concurrent/images/thread-pool/1bc44c67-26ba-42ab-bcb8-4e29e6fd99b9.png differ diff --git a/docs/java/concurrent/images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png b/docs/java/concurrent/images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png new file mode 100644 index 00000000000..7dc9b398c90 Binary files /dev/null and b/docs/java/concurrent/images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png differ diff --git a/docs/java/concurrent/images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png b/docs/java/concurrent/images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png new file mode 100644 index 00000000000..27cdbee3ead Binary files /dev/null and b/docs/java/concurrent/images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png differ diff --git a/docs/java/concurrent/images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png b/docs/java/concurrent/images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png new file mode 100644 index 00000000000..f0a781d62cd Binary files /dev/null and b/docs/java/concurrent/images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png differ diff --git "a/docs/java/concurrent/images/thread-pool/\347\272\277\347\250\213\346\261\240\344\275\277\347\224\250\344\270\215\345\275\223\345\257\274\350\207\264\346\255\273\351\224\201.drawio" "b/docs/java/concurrent/images/thread-pool/\347\272\277\347\250\213\346\261\240\344\275\277\347\224\250\344\270\215\345\275\223\345\257\274\350\207\264\346\255\273\351\224\201.drawio" new file mode 100644 index 00000000000..3a2c775adc7 --- /dev/null +++ "b/docs/java/concurrent/images/thread-pool/\347\272\277\347\250\213\346\261\240\344\275\277\347\224\250\344\270\215\345\275\223\345\257\274\350\207\264\346\255\273\351\224\201.drawio" @@ -0,0 +1 @@ +7Vpbj5s4GP01fkwFBgw8QkLaSl2p0qzU3acVA+ayJTglziTpr18bTLg5WaYThrQdaTRjf75gn/Md+7M9QFtuju8Lf5v8QUKcAaiER6CtAISqqiD2h1tOlcWyYWWIizQUlRrDQ/odC6MirPs0xLtORUpIRtNt1xiQPMcB7dj8oiCHbrWIZN2vbv0YDwwPgZ8NrV/SkCZiFtBs7B9wGif1l1VkVyUbv64sZrJL/JAcWibNA9qyIIRWqc1xiTMOXo1L1W59ofQ8sALndEyDKF4s0OLpz4do5cF/PirF8tNuoVW9PPnZXkxYDJaeagQKss9DzDtRgeYekpTih60f8NID45zZErrJRHGUZtmSZKQo22qhj60oYPYdLchX3CpBgYUfI1YynIaY2RMuKD62TGJa7zHZYFqcWBVRaguEhYsZInto+FJrEpIWV0jYfOEi8bnjBkWWEEA+A1R9WlCjCKNACmpo2o+KMgmoqj43qsaNUb09RlCbGyM0xMgzgGsClyXYbxdYa+BZwPK4kRWxrI24xWVpHXgIuA5g02KVbae06LwV++GVHeCoA9AZVrSLbNcvc5LjnhMLk5+lcc6yAWMAM7vLkU/Z2uuIgk0ahvwzUiobsrnHRySnYvdQ0W3YPe9Bgl5Twq4pYRdOxa45il0ELBs4JieVsWUteR3HKxNvpA5IVdHcrFoSVhGwl8Bx3qT6g6xKY4BXZdWW7FYo4+jvtn7eoQV92/MYsMRlsSuBcVgFBsmxKWSpuPzLhe4Ad80TDnMPt/QEFTiKzDfWwF0Cy+IWVsG1ygVjDSxN3o/NSpXSkVhXTl63t+uxMyiq4YvB9B2MAcMC80tu0A5pej7QjxsNbIW6LMSx4KOGbuQ1RtdpdDh0GmgZQ6dR7am8RhLT4JCdR0SWFDQhMcn9zGusPXU1dT4RshVw/4spPQm8/T0lXTIYgsXpL9G+zPzNM++MOrs6tgtXp+cKeUf2RYCvzFutj3Z+EWN6raKgg6NyleICZz5Nn7qnuNvzpb3xdZ0vdFd81ePubLYmXyBdNNw/hxvoxGfhCXZGo78zyuKdVz2jQP031QwcqxnrrjQDjTn4mgN3+65wr8f9ghBSRZdCyAsrHhwb6N3/pWB/4dNnvxaE6Ddd+LSRAqzZuBMB1nf8b3xdAki9K75U2WPGxAtm/usumGj2SFEd8ZCC89Dhz3zgfFUV+rvkDHQLVG7/7FOKi7y0QEU7Q1o/7cGukKBxDdr/dfsWcIYEt9o2Wh3iC59JykbS8Ibsd0aXuf6tViV50a4hZdiV2XUBQ+91VC0Jg45Kds8TfwHhsjeeSl+clVEK1pmCS+kORGwAZ1Veig3OeQu1JePqSxPIeMS7XXsR128ja1PpHQCV2WUteaV67jqty9fpKxRPE9pOQJCmzk6Q5EliaoJgR4OTbaWv83bep1SfX3Mvf4/4AUp/Gs0Zs2uu/thrEpT/yppD81MquWyentKfRnOm5LntVgSxbPOPdVV02vx7oub9Bw== \ No newline at end of file diff --git "a/docs/java/concurrent/images/thread-pool/\347\272\277\347\250\213\346\261\240\344\275\277\347\224\250\344\270\215\345\275\223\345\257\274\350\207\264\346\255\273\351\224\201.png" "b/docs/java/concurrent/images/thread-pool/\347\272\277\347\250\213\346\261\240\344\275\277\347\224\250\344\270\215\345\275\223\345\257\274\350\207\264\346\255\273\351\224\201.png" new file mode 100644 index 00000000000..1ad4f811ac8 Binary files /dev/null and "b/docs/java/concurrent/images/thread-pool/\347\272\277\347\250\213\346\261\240\344\275\277\347\224\250\344\270\215\345\275\223\345\257\274\350\207\264\346\255\273\351\224\201.png" differ diff --git "a/docs/java/concurrent/images/threadlocal\346\225\260\346\215\256\347\273\223\346\236\204.png" "b/docs/java/concurrent/images/threadlocal\346\225\260\346\215\256\347\273\223\346\236\204.png" new file mode 100644 index 00000000000..a5791ce5af6 Binary files /dev/null and "b/docs/java/concurrent/images/threadlocal\346\225\260\346\215\256\347\273\223\346\236\204.png" differ diff --git "a/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/Java\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\350\211\272\346\234\257.png" "b/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/Java\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\350\211\272\346\234\257.png" new file mode 100644 index 00000000000..ff907c9caa9 Binary files /dev/null and "b/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/Java\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\350\211\272\346\234\257.png" differ diff --git "a/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/javaguide-\345\271\266\345\217\221.png" "b/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/javaguide-\345\271\266\345\217\221.png" new file mode 100644 index 00000000000..862f282f6f9 Binary files /dev/null and "b/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/javaguide-\345\271\266\345\217\221.png" differ diff --git "a/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\347\276\216.png" "b/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\347\276\216.png" new file mode 100644 index 00000000000..05e3bff506c Binary files /dev/null and "b/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\347\276\216.png" differ diff --git "a/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\345\256\236\346\210\230Java\351\253\230\345\271\266\345\217\221\347\250\213\345\272\217\350\256\276\350\256\241.png" "b/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\345\256\236\346\210\230Java\351\253\230\345\271\266\345\217\221\347\250\213\345\272\217\350\256\276\350\256\241.png" new file mode 100644 index 00000000000..ab61bff85dc Binary files /dev/null and "b/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\345\256\236\346\210\230Java\351\253\230\345\271\266\345\217\221\347\250\213\345\272\217\350\256\276\350\256\241.png" differ diff --git "a/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\346\267\261\345\205\245\346\265\205\345\207\272Java\345\244\232\347\272\277\347\250\213.png" "b/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\346\267\261\345\205\245\346\265\205\345\207\272Java\345\244\232\347\272\277\347\250\213.png" new file mode 100644 index 00000000000..14d1ea85d24 Binary files /dev/null and "b/docs/java/concurrent/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\346\267\261\345\205\245\346\265\205\345\207\272Java\345\244\232\347\272\277\347\250\213.png" differ diff --git a/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md "b/docs/java/concurrent/java\345\271\266\345\217\221\345\237\272\347\241\200\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" similarity index 54% rename from docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md rename to "docs/java/concurrent/java\345\271\266\345\217\221\345\237\272\347\241\200\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" index 39d73dd8e51..2cf5c7cf22e 100644 --- a/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md +++ "b/docs/java/concurrent/java\345\271\266\345\217\221\345\237\272\347\241\200\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" @@ -1,33 +1,13 @@ - - -- [Java 并发基础常见面试题总结](#java-并发基础常见面试题总结) - - [1. 什么是线程和进程?](#1-什么是线程和进程) - - [1.1. 何为进程?](#11-何为进程) - - [1.2. 何为线程?](#12-何为线程) - - [2. 请简要描述线程与进程的关系,区别及优缺点?](#2-请简要描述线程与进程的关系区别及优缺点) - - [2.1. 图解进程和线程的关系](#21-图解进程和线程的关系) - - [2.2. 程序计数器为什么是私有的?](#22-程序计数器为什么是私有的) - - [2.3. 虚拟机栈和本地方法栈为什么是私有的?](#23-虚拟机栈和本地方法栈为什么是私有的) - - [2.4. 一句话简单了解堆和方法区](#24-一句话简单了解堆和方法区) - - [3. 说说并发与并行的区别?](#3-说说并发与并行的区别) - - [4. 为什么要使用多线程呢?](#4-为什么要使用多线程呢) - - [5. 使用多线程可能带来什么问题?](#5-使用多线程可能带来什么问题) - - [6. 说说线程的生命周期和状态?](#6-说说线程的生命周期和状态) - - [7. 什么是上下文切换?](#7-什么是上下文切换) - - [8. 什么是线程死锁?如何避免死锁?](#8-什么是线程死锁如何避免死锁) - - [8.1. 认识线程死锁](#81-认识线程死锁) - - [8.2. 如何避免线程死锁?](#82-如何避免线程死锁) - - [9. 说说 sleep() 方法和 wait() 方法区别和共同点?](#9-说说-sleep-方法和-wait-方法区别和共同点) - - [10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#10-为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法) - - [公众号](#公众号) - - - -# Java 并发基础常见面试题总结 - -## 1. 什么是线程和进程? - -### 1.1. 何为进程? +--- +title: Java 并发常见知识点&面试题总结(基础篇) +category: Java +tag: + - Java并发 +--- + +## 什么是线程和进程? + +### 何为进程? 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 @@ -37,7 +17,7 @@ ![进程示例图片-Windows](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/进程示例图片-Windows.png) -### 1.2. 何为线程? +### 何为线程? 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 @@ -70,27 +50,25 @@ public class MultiThread { 从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。 -## 2. 请简要描述线程与进程的关系,区别及优缺点? +## 请简要描述线程与进程的关系,区别及优缺点? **从 JVM 角度说进程和线程之间的关系** -### 2.1. 图解进程和线程的关系 +### 图解进程和线程的关系 -下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md "《可能是把 Java 内存区域讲的最清楚的一篇文章》") +下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。 -
- -
+![](../jvm/pictures/java内存区域/Java运行时数据区域JDK1.8.png) 从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 -**总结:** 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反 +**总结:** **线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。** 下面是该知识点的扩展内容! 下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? -### 2.2. 程序计数器为什么是私有的? +### 程序计数器为什么是私有的? 程序计数器主要有下面两个作用: @@ -101,23 +79,23 @@ public class MultiThread { 所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 -### 2.3. 虚拟机栈和本地方法栈为什么是私有的? +### 虚拟机栈和本地方法栈为什么是私有的? - **虚拟机栈:** 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 - **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 -### 2.4. 一句话简单了解堆和方法区 +### 一句话简单了解堆和方法区 堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 -## 3. 说说并发与并行的区别? +## 说说并发与并行的区别? - **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行); - **并行:** 单位时间内,多个任务同时执行。 -## 4. 为什么要使用多线程呢? +## 为什么要使用多线程呢? 先从总体上来说: @@ -126,14 +104,14 @@ public class MultiThread { 再深入到计算机底层来探讨: -- **单核时代:** 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。 -- **多核时代:** 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。 +- **单核时代**: 在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。 +- **多核时代**: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。 -## 5. 使用多线程可能带来什么问题? +## 使用多线程可能带来什么问题? 并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。 -## 6. 说说线程的生命周期和状态? +## 说说线程的生命周期和状态? Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。 @@ -143,27 +121,35 @@ Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种 ![Java 线程状态变迁 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java+%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81.png) +> 订正(来自[issue736](https://github.com/Snailclimb/JavaGuide/issues/736)):原图中 wait 到 runnable 状态的转换中,`join`实际上是`Thread`类的方法,但这里写成了`Object`。 + 由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 -> 操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/ "HowToDoInJava"):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/ "Java Thread Life Cycle and Thread States")),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。 +> 在操作系统中层面线程有 READY 和 RUNNING 状态,而在 JVM 层面只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinJava.com/ "HowToDoInJava"):[Java Thread Life Cycle and Thread States](https://howtodoinJava.com/Java/multi-threading/Java-thread-life-cycle-and-thread-states/ "Java Thread Life Cycle and Thread States")),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。 +> +> **为什么 JVM 没有区分这两种状态呢?** (摘自:[java线程运行怎么有第六种状态? - Dawell的回答](https://www.zhihu.com/question/56494969/answer/154053599) ) 现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。 ![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png) -当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的`run()`方法之后将会进入到 **TERMINATED(终止)** 状态。 +当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIMED_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED_WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的`run()`方法之后将会进入到 **TERMINATED(终止)** 状态。 -## 7. 什么是上下文切换? +相关阅读:[挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误](https://mp.weixin.qq.com/s/UOrXql_LhOD8dhTq_EPI0w) 。 -多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 +## 什么是上下文切换? -概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 +线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。 +- 主动让出 CPU,比如调用了 `sleep()`, `wait()` 等。 +- 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。 +- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。 +- 被终止或结束运行 -上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 +这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 **上下文切换**。 -Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 +上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。 -## 8. 什么是线程死锁?如何避免死锁? +## 什么是线程死锁?如何避免死锁? -### 8.1. 认识线程死锁 +### 认识线程死锁 线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 @@ -221,23 +207,28 @@ Thread[线程 1,5,main]waiting get resource2 Thread[线程 2,5,main]waiting get resource1 ``` -线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。 +线程 A 通过 `synchronized (resource1)` 获得 `resource1` 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。 学过操作系统的朋友都知道产生死锁必须具备以下四个条件: 1. 互斥条件:该资源任意一个时刻只由一个线程占用。 2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 -3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 +3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 -### 8.2. 如何避免线程死锁? +### 如何预防和避免线程死锁? + +**如何预防死锁?** 破坏死锁的产生的必要条件即可: -我上面说了产生死锁的四个必要条件,为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了。现在我们来挨个分析一下: +1. **破坏请求与保持条件** :一次性申请所有的资源。 +2. **破坏不剥夺条件** :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 +3. **破坏循环等待条件** :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 -1. **破坏互斥条件** :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 -2. **破坏请求与保持条件** :一次性申请所有的资源。 -3. **破坏不剥夺条件** :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 -4. **破坏循环等待条件** :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 +**如何避免死锁?** + +避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。 + +**安全状态** 指的是系统能够按照某种进程推进顺序(P1、P2、P3.....Pn)来为每个进程分配所需资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利完成。称序列为安全序列。 我们对线程 2 的代码修改成下面这样就不会产生死锁了。 @@ -275,29 +266,17 @@ Process finished with exit code 0 线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 -## 9. 说说 sleep() 方法和 wait() 方法区别和共同点? +## 说说 sleep() 方法和 wait() 方法区别和共同点? -- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。 +- 两者最主要的区别在于:**`sleep()` 方法没有释放锁,而 `wait()` 方法释放了锁** 。 - 两者都可以暂停线程的执行。 -- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 -- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。 - -## 10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? - -这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! - -new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 - -**总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。** - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取! +- `wait()` 通常被用于线程间交互/通信,`sleep() `通常被用于暂停执行。 +- `wait()` 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 `notify() `或者 `notifyAll()` 方法。`sleep() `方法执行完成后,线程会自动苏醒。或者可以使用 `wait(long timeout)` 超时后线程会自动苏醒。 -**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 +## 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) +这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! +new 一个 Thread,线程进入了新建状态。调用 `start()`方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 `start()` 会执行线程的相应准备工作,然后自动执行 `run()` 方法的内容,这是真正的多线程工作。 但是,直接执行 `run()` 方法,会把 `run()` 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 +**总结: 调用 `start()` 方法方可启动线程并使线程进入就绪状态,直接执行 `run()` 方法的话不会以多线程的方式执行。** diff --git "a/docs/java/concurrent/java\345\271\266\345\217\221\350\277\233\351\230\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" "b/docs/java/concurrent/java\345\271\266\345\217\221\350\277\233\351\230\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" new file mode 100644 index 00000000000..2278eaf1b08 --- /dev/null +++ "b/docs/java/concurrent/java\345\271\266\345\217\221\350\277\233\351\230\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" @@ -0,0 +1,1052 @@ +--- +title: Java 并发常见知识点&面试题总结(进阶篇) +category: Java +tag: + - Java并发 +--- + +## 1.synchronized 关键字 + +![](images/interview-questions/synchronized关键字.png) + +### 1.1.说一说自己对于 synchronized 关键字的了解 + +**`synchronized` 关键字解决的是多个线程之间访问资源的同步性,`synchronized`关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。** + +另外,在 Java 早期版本中,`synchronized` 属于 **重量级锁**,效率低下。 + +**为什么呢?** + +因为监视器锁(monitor)是依赖于底层的操作系统的 `Mutex Lock` 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。 + +庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 `synchronized` 较大优化,所以现在的 `synchronized` 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 + +所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 `synchronized` 关键字。 + +### 1.2. 说说自己是怎么使用 synchronized 关键字 + +**synchronized 关键字最主要的三种使用方式:** + +**1.修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得 **当前对象实例的锁** + +```java +synchronized void method() { + //业务代码 +} +``` + +**2.修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 **当前 class 的锁**。因为静态成员不属于任何一个实例对象,是类成员( _static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份_)。所以,如果一个线程 A 调用一个实例对象的非静态 `synchronized` 方法,而线程 B 需要调用这个实例对象所属类的静态 `synchronized` 方法,是允许的,不会发生互斥现象,**因为访问静态 `synchronized` 方法占用的锁是当前类的锁,而访问非静态 `synchronized` 方法占用的锁是当前实例对象锁**。 + +```java +synchronized static void method() { + //业务代码 +} +``` + +**3.修饰代码块** :指定加锁对象,对给定对象/类加锁。`synchronized(this|object)` 表示进入同步代码库前要获得**给定对象的锁**。`synchronized(类.class)` 表示进入同步代码前要获得 **当前 class 的锁** + +```java +synchronized(this) { + //业务代码 +} +``` + +**总结:** + +- `synchronized` 关键字加到 `static` 静态方法和 `synchronized(class)` 代码块上都是是给 Class 类上锁。 +- `synchronized` 关键字加到实例方法上是给对象实例上锁。 +- 尽量不要使用 `synchronized(String a)` 因为 JVM 中,字符串常量池具有缓存功能! + +下面我以一个常见的面试题为例讲解一下 `synchronized` 关键字的具体使用。 + +面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” + +**双重校验锁实现对象单例(线程安全)** + +```java +public class Singleton { + + private volatile static Singleton uniqueInstance; + + private Singleton() { + } + + public static Singleton getUniqueInstance() { + //先判断对象是否已经实例过,没有实例化过才进入加锁代码 + if (uniqueInstance == null) { + //类对象加锁 + synchronized (Singleton.class) { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + } + } + return uniqueInstance; + } +} +``` + +另外,需要注意 `uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要。 + +`uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要的, `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行: + +1. 为 `uniqueInstance` 分配内存空间 +2. 初始化 `uniqueInstance` +3. 将 `uniqueInstance` 指向分配的内存地址 + +但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 `getUniqueInstance`() 后发现 `uniqueInstance` 不为空,因此返回 `uniqueInstance`,但此时 `uniqueInstance` 还未被初始化。 + +使用 `volatile` 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 + +### 1.3. 构造方法可以使用 synchronized 关键字修饰么? + +先说结论:**构造方法不能使用 synchronized 关键字修饰。** + +构造方法本身就属于线程安全的,不存在同步的构造方法一说。 + +### 1.3. 讲一下 synchronized 关键字的底层原理 + +**synchronized 关键字底层原理属于 JVM 层面。** + +#### 1.3.1. synchronized 同步语句块的情况 + +```java +public class SynchronizedDemo { + public void method() { + synchronized (this) { + System.out.println("synchronized 代码块"); + } + } +} + +``` + +通过 JDK 自带的 `javap` 命令查看 `SynchronizedDemo` 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 + +![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理.png) + +从上面我们可以看出: + +**`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。** + +当执行 `monitorenter` 指令时,线程试图获取锁也就是获取 **对象监视器 `monitor`** 的持有权。 + +> 在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由[ObjectMonitor](https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/runtime/objectMonitor.cpp)实现的。每个对象中都内置了一个 `ObjectMonitor`对象。 +> +> 另外,`wait/notify`等方法也依赖于`monitor`对象,这就是为什么只有在同步的块或者方法中才能调用`wait/notify`等方法,否则会抛出`java.lang.IllegalMonitorStateException`的异常的原因。 + +在执行`monitorenter`时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。 + +在执行 `monitorexit` 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 + +#### 1.3.2. synchronized 修饰方法的的情况 + +```java +public class SynchronizedDemo2 { + public synchronized void method() { + System.out.println("synchronized 方法"); + } +} + +``` + +![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理2.png) + +`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 + +#### 1.3.3.总结 + +`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。 + +`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。 + +**不过两者的本质都是对对象监视器 monitor 的获取。** + +### 1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗 + +JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 + +锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 + +关于这几种优化的详细信息可以查看下面这篇文章:[Java6 及以上版本对 synchronized 的优化](https://www.cnblogs.com/wuqinglong/p/9945618.html) + +### 1.5. 谈谈 synchronized 和 ReentrantLock 的区别 + +#### 1.5.1. 两者都是可重入锁 + +**“可重入锁”** 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。 + +#### 1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API + +`synchronized` 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 `synchronized` 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。`ReentrantLock` 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 + +#### 1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能 + +相比`synchronized`,`ReentrantLock`增加了一些高级功能。主要来说主要有三点: + +- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 +- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 +- **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()`和`notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法。 + +> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 Condition 接口默认提供的。而`synchronized`关键字就相当于整个 Lock 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而`Condition`实例的`signalAll()`方法 只会唤醒注册在该`Condition`实例中的所有等待线程。 + +**如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。性能已不是选择标准** + +## 2. volatile 关键字 + +我们先要从 **CPU 缓存模型** 说起! + +### 2.1. CPU 缓存模型 + +**为什么要弄一个 CPU 高速缓存呢?** + +类比我们开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。 **CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。** + +我们甚至可以把 **内存可以看作外存的高速缓存**,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。 + +总结:**CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。** + +为了更好地理解,我画了一个简单的 CPU Cache 示意图如下(实际上,现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache): + +![CPU Cache](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/303a300f-70dd-4ee1-9974-3f33affc6574.png) + +**CPU Cache 的工作方式:** + +先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 **内存缓存不一致性的问题** !比如我执行一个 i++操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。 + +**CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议或者其他手段来解决。** + +### 2.2. 讲一下 JMM(Java 内存模型) + +在 JDK1.2 之前,Java 的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。 + +![JMM(Java内存模型)](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/0ac7e663-7db8-4b95-8d8e-7d2b179f67e8.png) + +要解决这个问题,就需要把变量声明为 **`volatile`** ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。 + +所以,**`volatile` 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。** + +![volatile关键字的可见性](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/d49c5557-140b-4abf-adad-8aac3c9036cf.png) + +### 2.3. 并发编程的三个重要特性 + +1. **原子性** : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。`synchronized` 可以保证代码片段的原子性。 +2. **可见性** :当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。`volatile` 关键字可以保证共享变量的可见性。 +3. **有序性** :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。`volatile` 关键字可以禁止指令进行重排序优化。 + +### 2.4. 说说 synchronized 关键字和 volatile 关键字的区别 + +`synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在! + +- **`volatile` 关键字**是线程同步的**轻量级实现**,所以 **`volatile `性能肯定比` synchronized `关键字要好** 。但是 **`volatile` 关键字只能用于变量而 `synchronized` 关键字可以修饰方法以及代码块** 。 +- **`volatile` 关键字能保证数据的可见性,但不能保证数据的原子性。`synchronized` 关键字两者都能保证。** +- **`volatile`关键字主要用于解决变量在多个线程之间的可见性,而 `synchronized` 关键字解决的是多个线程之间访问资源的同步性。** + +## 3. ThreadLocal + +### 3.1. ThreadLocal 简介 + +通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK 中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** + +**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** + +再举个简单的例子: + +比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。 + +### 3.2. ThreadLocal 示例 + +相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 + +```java +import java.text.SimpleDateFormat; +import java.util.Random; + +public class ThreadLocalExample implements Runnable{ + + // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 + private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); + + public static void main(String[] args) throws InterruptedException { + ThreadLocalExample obj = new ThreadLocalExample(); + for(int i=0 ; i<10; i++){ + Thread t = new Thread(obj, ""+i); + Thread.sleep(new Random().nextInt(1000)); + t.start(); + } + } + + @Override + public void run() { + System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); + try { + Thread.sleep(new Random().nextInt(1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + //formatter pattern is changed here by thread, but it won't reflect to other threads + formatter.set(new SimpleDateFormat()); + + System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); + } + +} + +``` + +Output: + +``` +Thread Name= 0 default Formatter = yyyyMMdd HHmm +Thread Name= 0 formatter = yy-M-d ah:mm +Thread Name= 1 default Formatter = yyyyMMdd HHmm +Thread Name= 2 default Formatter = yyyyMMdd HHmm +Thread Name= 1 formatter = yy-M-d ah:mm +Thread Name= 3 default Formatter = yyyyMMdd HHmm +Thread Name= 2 formatter = yy-M-d ah:mm +Thread Name= 4 default Formatter = yyyyMMdd HHmm +Thread Name= 3 formatter = yy-M-d ah:mm +Thread Name= 4 formatter = yy-M-d ah:mm +Thread Name= 5 default Formatter = yyyyMMdd HHmm +Thread Name= 5 formatter = yy-M-d ah:mm +Thread Name= 6 default Formatter = yyyyMMdd HHmm +Thread Name= 6 formatter = yy-M-d ah:mm +Thread Name= 7 default Formatter = yyyyMMdd HHmm +Thread Name= 7 formatter = yy-M-d ah:mm +Thread Name= 8 default Formatter = yyyyMMdd HHmm +Thread Name= 9 default Formatter = yyyyMMdd HHmm +Thread Name= 8 formatter = yy-M-d ah:mm +Thread Name= 9 formatter = yy-M-d ah:mm +``` + +从输出中可以看出,Thread-0 已经改变了 formatter 的值,但仍然是 thread-2 默认格式化程序与初始化值相同,其他线程也一样。 + +上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA 会提示你转换为 Java8 的格式(IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法`withInitial()`,将 Supplier 功能接口作为参数。 + +```java +private static final ThreadLocal formatter = new ThreadLocal(){ + @Override + protected SimpleDateFormat initialValue(){ + return new SimpleDateFormat("yyyyMMdd HHmm"); + } +}; +``` + +### 3.3. ThreadLocal 原理 + +从 `Thread`类源代码入手。 + +```java +public class Thread implements Runnable { + //...... + //与此线程有关的ThreadLocal值。由ThreadLocal类维护 + ThreadLocal.ThreadLocalMap threadLocals = null; + + //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 + ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; + //...... +} +``` + +从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是 null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set()`方法。 + +`ThreadLocal`类的`set()`方法 + +```java +public void set(T value) { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + createMap(t, value); +} +ThreadLocalMap getMap(Thread t) { + return t.threadLocals; +} +``` + +通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。 + +**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为 key ,Object 对象为 value 的键值对。** + +```java +ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { + //...... +} +``` + +比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。 + +![ThreadLocal数据结构](images/threadlocal数据结构.png) + +`ThreadLocalMap`是`ThreadLocal`的静态内部类。 + +![ThreadLocal内部类](images/ThreadLocal内部类.png) + +### 3.4. ThreadLocal 内存泄露问题 + +`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 + +```java +static class Entry extends WeakReference> { + /** The value associated with this ThreadLocal. */ + Object value; + + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } +} +``` + +**弱引用介绍:** + +> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 +> +> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 + +## 4. 线程池 + +### 4.1. 为什么要用线程池? + +> **池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** + +**线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 + +这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: + +- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 +- **提高响应速度**。当任务到达时,任务可以不需要等到线程创建就能立即执行。 +- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +### 4.2. 实现 Runnable 接口和 Callable 接口的区别 + +`Runnable`自 Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口** 不会返回结果或抛出检查异常,但是 **`Callable` 接口** 可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口** ,这样代码看起来会更加简洁。 + +工具类 `Executors` 可以实现将 `Runnable` 对象转换成 `Callable` 对象。(`Executors.callable(Runnable task)` 或 `Executors.callable(Runnable task, Object result)`)。 + +`Runnable.java` + +```java +@FunctionalInterface +public interface Runnable { + /** + * 被线程执行,没有返回值也无法抛出异常 + */ + public abstract void run(); +} +``` + +`Callable.java` + +```java +@FunctionalInterface +public interface Callable { + /** + * 计算结果,或在无法这样做时抛出异常。 + * @return 计算得出的结果 + * @throws 如果无法计算结果,则抛出异常 + */ + V call() throws Exception; +} +``` + +### 4.3. 执行 execute()方法和 submit()方法的区别是什么呢? + +1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** +2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 + +我们以 **`AbstractExecutorService` 接口** 中的一个 `submit` 方法为例子来看看源代码: + +```java +public Future submit(Runnable task) { + if (task == null) throw new NullPointerException(); + RunnableFuture ftask = newTaskFor(task, null); + execute(ftask); + return ftask; +} +``` + +上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。 + +```java +protected RunnableFuture newTaskFor(Runnable runnable, T value) { + return new FutureTask(runnable, value); +} +``` + +我们再来看看`execute()`方法: + +```java +public void execute(Runnable command) { + ... +} +``` + +### 4.4. 如何创建线程池 + +《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 + +> Executors 返回线程池对象的弊端如下: +> +> - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。 +> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 + +**方式一:通过构造方法实现** + +![ThreadPoolExecutor构造方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadPoolExecutor构造方法.png) + +**方式二:通过 Executor 框架的工具类 Executors 来实现** + +我们可以创建三种类型的 ThreadPoolExecutor: + +- **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 +- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 +- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 + +对应 Executors 工具类中的方法如图所示: + +![Executor框架的工具类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Executor框架的工具类.png) + +### 4.5 ThreadPoolExecutor 类分析 + +`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。 + +```java +/** + * 用给定的初始参数创建一个新的ThreadPoolExecutor。 + */ +public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) { + if (corePoolSize < 0 || + maximumPoolSize <= 0 || + maximumPoolSize < corePoolSize || + keepAliveTime < 0) + throw new IllegalArgumentException(); + if (workQueue == null || threadFactory == null || handler == null) + throw new NullPointerException(); + this.corePoolSize = corePoolSize; + this.maximumPoolSize = maximumPoolSize; + this.workQueue = workQueue; + this.keepAliveTime = unit.toNanos(keepAliveTime); + this.threadFactory = threadFactory; + this.handler = handler; +} +``` + +**下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。** + +#### 4.5.1 `ThreadPoolExecutor`构造函数重要参数分析 + +**`ThreadPoolExecutor` 3 个最重要的参数:** + +- **`corePoolSize` :** 核心线程数定义了最小可以同时运行的线程数量。 +- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 +- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 + +`ThreadPoolExecutor`其他常见参数: + +1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; +2. **`unit`** : `keepAliveTime` 参数的时间单位。 +3. **`threadFactory`** :executor 创建新线程的时候会用到。 +4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。 + +#### 4.5.2 `ThreadPoolExecutor` 饱和策略 + +**`ThreadPoolExecutor` 饱和策略定义:** + +如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolTaskExecutor` 定义一些策略: + +- **`ThreadPoolExecutor.AbortPolicy`:** 抛出 `RejectedExecutionException`来拒绝新任务的处理。 +- **`ThreadPoolExecutor.CallerRunsPolicy`:** 调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 +- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 +- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 + +举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了) + +### 4.6 一个简单的线程池 Demo + +为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。 + +首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。) + +`MyRunnable.java` + +```java +import java.util.Date; + +/** + * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 + * @author shuang.kou + */ +public class MyRunnable implements Runnable { + + private String command; + + public MyRunnable(String s) { + this.command = s; + } + + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); + processCommand(); + System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); + } + + private void processCommand() { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public String toString() { + return this.command; + } +} + +``` + +编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。 + +`ThreadPoolExecutorDemo.java` + +```java +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ThreadPoolExecutorDemo { + + private static final int CORE_POOL_SIZE = 5; + private static final int MAX_POOL_SIZE = 10; + private static final int QUEUE_CAPACITY = 100; + private static final Long KEEP_ALIVE_TIME = 1L; + public static void main(String[] args) { + + //使用阿里巴巴推荐的创建线程池的方式 + //通过ThreadPoolExecutor构造函数自定义参数创建 + ThreadPoolExecutor executor = new ThreadPoolExecutor( + CORE_POOL_SIZE, + MAX_POOL_SIZE, + KEEP_ALIVE_TIME, + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(QUEUE_CAPACITY), + new ThreadPoolExecutor.CallerRunsPolicy()); + + for (int i = 0; i < 10; i++) { + //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) + Runnable worker = new MyRunnable("" + i); + //执行Runnable + executor.execute(worker); + } + //终止线程池 + executor.shutdown(); + while (!executor.isTerminated()) { + } + System.out.println("Finished all threads"); + } +} +``` + +可以看到我们上面的代码指定了: + +1. `corePoolSize`: 核心线程数为 5。 +2. `maximumPoolSize` :最大线程数 10 +3. `keepAliveTime` : 等待时间为 1L。 +4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。 +5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100; +6. `handler`:饱和策略为 `CallerRunsPolicy`。 + +**Output:** + +``` +pool-1-thread-3 Start. Time = Sun Apr 12 11:14:37 CST 2020 +pool-1-thread-5 Start. Time = Sun Apr 12 11:14:37 CST 2020 +pool-1-thread-2 Start. Time = Sun Apr 12 11:14:37 CST 2020 +pool-1-thread-1 Start. Time = Sun Apr 12 11:14:37 CST 2020 +pool-1-thread-4 Start. Time = Sun Apr 12 11:14:37 CST 2020 +pool-1-thread-3 End. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-4 End. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-1 End. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-5 End. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-1 Start. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-2 End. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-5 Start. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-4 Start. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-3 Start. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-2 Start. Time = Sun Apr 12 11:14:42 CST 2020 +pool-1-thread-1 End. Time = Sun Apr 12 11:14:47 CST 2020 +pool-1-thread-4 End. Time = Sun Apr 12 11:14:47 CST 2020 +pool-1-thread-5 End. Time = Sun Apr 12 11:14:47 CST 2020 +pool-1-thread-3 End. Time = Sun Apr 12 11:14:47 CST 2020 +pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 + +``` + +### 4.7 线程池原理分析 + +承接 4.6 节,我们通过代码输出结果可以看出:**线程池首先会先执行 5 个任务,然后这些任务有任务被执行完的话,就会去拿新的任务执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) + +现在,我们就分析上面的输出内容来简单分析一下线程池原理。 + +**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。** 在 4.6 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: + +```java +// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) +private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); + +private static int workerCountOf(int c) { + return c & CAPACITY; +} + +private final BlockingQueue workQueue; + +public void execute(Runnable command) { + // 如果任务为null,则抛出异常。 + if (command == null) + throw new NullPointerException(); + // ctl 中保存的线程池当前的一些状态信息 + int c = ctl.get(); + + // 下面会涉及到 3 步 操作 + // 1.首先判断当前线程池中执行的任务数量是否小于 corePoolSize + // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 + if (workerCountOf(c) < corePoolSize) { + if (addWorker(command, true)) + return; + c = ctl.get(); + } + // 2.如果当前执行的任务数量大于等于 corePoolSize 的时候就会走到这里 + // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去 + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); + // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 + if (!isRunning(recheck) && remove(command)) + reject(command); + // 如果当前线程池为空就新创建一个线程并执行。 + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } + //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 + //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 + else if (!addWorker(command, false)) + reject(command); +} +``` + +通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。 + +![图解线程池实现原理](images/java线程池学习总结/图解线程池实现原理.png) + +现在,让我们在回到 4.6 节我们写的 Demo, 现在是不是很容易就可以搞懂它的原理了呢? + +没搞懂的话,也没关系,可以看看我的分析: + +> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的5个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。 + +## 5. Atomic 原子类 + +### 5.1. 介绍一下 Atomic 原子类 + +`Atomic` 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 + +所以,所谓原子类说简单点就是具有原子/原子操作特征的类。 + +并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。 + +![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) + +### 5.2. JUC 包中的原子类是哪 4 类? + +**基本类型** + +使用原子的方式更新基本类型 + +- `AtomicInteger`:整形原子类 +- `AtomicLong`:长整型原子类 +- `AtomicBoolean`:布尔型原子类 + +**数组类型** + +使用原子的方式更新数组里的某个元素 + +- `AtomicIntegerArray`:整形数组原子类 +- `AtomicLongArray`:长整形数组原子类 +- `AtomicReferenceArray`:引用类型数组原子类 + +**引用类型** + +- `AtomicReference`:引用类型原子类 +- `AtomicStampedReference`:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 +- `AtomicMarkableReference` :原子更新带有标记位的引用类型 + +**对象的属性修改类型** + +- `AtomicIntegerFieldUpdater`:原子更新整形字段的更新器 +- `AtomicLongFieldUpdater`:原子更新长整形字段的更新器 +- `AtomicReferenceFieldUpdater`:原子更新引用类型字段的更新器 + +### 5.3. 讲讲 AtomicInteger 的使用 + +**AtomicInteger 类常用方法** + +```java +public final int get() //获取当前的值 +public final int getAndSet(int newValue)//获取当前的值,并设置新的值 +public final int getAndIncrement()//获取当前的值,并自增 +public final int getAndDecrement() //获取当前的值,并自减 +public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 +boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) +public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 +``` + +**AtomicInteger 类的使用示例** + +使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。 + +```java +class AtomicIntegerTest { + private AtomicInteger count = new AtomicInteger(); + //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。 + public void increment() { + count.incrementAndGet(); + } + + public int getCount() { + return count.get(); + } +} + +``` + +### 5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理 + +AtomicInteger 线程安全原理简单分析 + +AtomicInteger 类的部分源码: + +```java +// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) +private static final Unsafe unsafe = Unsafe.getUnsafe(); +private static final long valueOffset; + +static { + try { + valueOffset = unsafe.objectFieldOffset + (AtomicInteger.class.getDeclaredField("value")); + } catch (Exception ex) { throw new Error(ex); } +} + +private volatile int value; +``` + +AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 + +CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 + +关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484834&idx=1&sn=7d3835091af8125c13fc6db765f4c5bd&source=41#wechat_redirect) + +## 6. AQS + +### 6.1. AQS 介绍 + +AQS 的全称为(`AbstractQueuedSynchronizer`),这个类在` java.util.concurrent.locks `包下面。 + +![AQS类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS类.png) + +AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出大量应用广泛的同步器,比如我们提到的 `ReentrantLock`,`Semaphore`,其他的诸如 `ReentrantReadWriteLock`,`SynchronousQueue`,`FutureTask` 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。 + +### 6.2. AQS 原理分析 + +AQS 原理这部分参考了部分博客,在 5.2 节末尾放了链接。 + +> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 + +下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。 + +#### 6.2.1. AQS 原理概览 + +**AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。** + +> CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。 + +看个 AQS(AbstractQueuedSynchronizer)原理图: + +![AQS原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS原理图.png) + +AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。 + +```java +private volatile int state;//共享变量,使用volatile修饰保证线程可见性 +``` + +状态信息通过 protected 类型的 getState,setState,compareAndSetState 进行操作 + +```java + +//返回同步状态的当前值 +protected final int getState() { + return state; +} +//设置同步状态的值 +protected final void setState(int newState) { + state = newState; +} +//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) +protected final boolean compareAndSetState(int expect, int update) { + return unsafe.compareAndSwapInt(this, stateOffset, expect, update); +} +``` + +#### 6.2.2. AQS 对资源的共享方式 + +**AQS 定义两种资源共享方式** + +- **Exclusive**(独占):只有一个线程能执行,如 `ReentrantLock`。又可分为公平锁和非公平锁: + - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 + - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 +- **Share**(共享):多个线程可同时执行,如` CountDownLatch`、`Semaphore`、 `CyclicBarrier`、`ReadWriteLock` 我们都会在后面讲到。 + +`ReentrantReadWriteLock` 可以看成是组合式,因为 `ReentrantReadWriteLock` 也就是读写锁允许多个线程同时对某一资源进行读。 + +不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。 + +#### 6.2.3. AQS 底层使用了模板方法模式 + +同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): + +1. 使用者继承 `AbstractQueuedSynchronizer` 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放) +2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 + +这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。 + +**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:** + +```java +isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 +tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 +tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 +tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 +tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 + +``` + +默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 + +以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。 + +再以 `CountDownLatch` 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后` countDown()` 一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 `await()` 函数返回,继续后余动作。 + +一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 + +推荐两篇 AQS 原理和相关源码分析的文章: + +- https://www.cnblogs.com/waterystone/p/4920797.html +- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html + +### 6.3. AQS 组件总结 + +- **`Semaphore`(信号量)-允许多个线程同时访问:** `synchronized` 和 `ReentrantLock` 都是一次只允许一个线程访问某个资源,`Semaphore`(信号量)可以指定多个线程同时访问某个资源。 +- **`CountDownLatch `(倒计时器):** `CountDownLatch` 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 +- **`CyclicBarrier`(循环栅栏):** `CyclicBarrier` 和 `CountDownLatch` 非常类似,它也可以实现线程间的技术等待,但是它的功能比 `CountDownLatch` 更加复杂和强大。主要应用场景和 `CountDownLatch` 类似。`CyclicBarrier` 的字面意思是可循环使用(`Cyclic`)的屏障(`Barrier`)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。`CyclicBarrier` 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用 `await()` 方法告诉 `CyclicBarrier` 我已经到达了屏障,然后当前线程被阻塞。 + +### 6.4. 用过 CountDownLatch 么?什么场景下用的? + +`CountDownLatch` 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 `CountDownLatch` 。具体场景是下面这样的: + +我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。 + +为此我们定义了一个线程池和 count 为 6 的`CountDownLatch`对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用`CountDownLatch`对象的 `await()`方法,直到所有文件读取完之后,才会接着执行后面的逻辑。 + +伪代码是下面这样的: + +```java +public class CountDownLatchExample1 { + // 处理文件的数量 + private static final int threadCount = 6; + + public static void main(String[] args) throws InterruptedException { + // 创建一个具有固定线程数量的线程池对象(推荐使用构造方法创建) + ExecutorService threadPool = Executors.newFixedThreadPool(10); + final CountDownLatch countDownLatch = new CountDownLatch(threadCount); + for (int i = 0; i < threadCount; i++) { + final int threadnum = i; + threadPool.execute(() -> { + try { + //处理文件的业务操作 + //...... + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + //表示一个文件已经被完成 + countDownLatch.countDown(); + } + + }); + } + countDownLatch.await(); + threadPool.shutdown(); + System.out.println("finish"); + } +} +``` + +**有没有可以改进的地方呢?** + +可以使用 `CompletableFuture` 类来改进!Java8 的 `CompletableFuture` 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。 + +```java +CompletableFuture task1 = + CompletableFuture.supplyAsync(()->{ + //自定义业务操作 + }); +...... +CompletableFuture task6 = + CompletableFuture.supplyAsync(()->{ + //自定义业务操作 + }); +...... +CompletableFuture headerFuture=CompletableFuture.allOf(task1,.....,task6); + +try { + headerFuture.join(); +} catch (Exception ex) { + //...... +} +System.out.println("all done. "); +``` + +上面的代码还可以接续优化,当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。 + +```java +//文件夹位置 +List filePaths = Arrays.asList(...) +// 异步处理所有文件 +List> fileFutures = filePaths.stream() + .map(filePath -> doSomeThing(filePath)) + .collect(Collectors.toList()); +// 将他们合并起来 +CompletableFuture allFutures = CompletableFuture.allOf( + fileFutures.toArray(new CompletableFuture[fileFutures.size()]) +); + +``` + +## 7 Reference + +- 《深入理解 Java 虚拟机》 +- 《实战 Java 高并发程序设计》 +- 《Java 并发编程的艺术》 +- https://www.cnblogs.com/waterystone/p/4920797.html +- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html +- diff --git "a/docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" "b/docs/java/concurrent/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" similarity index 75% rename from "docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" rename to "docs/java/concurrent/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" index 329a227e31b..1140df91c48 100644 --- "a/docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" +++ "b/docs/java/concurrent/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" @@ -1,71 +1,33 @@ +--- +title: Java线程池学习总结 +category: Java +tag: + - Java并发 +--- - - -- [一 使用线程池的好处](#一-使用线程池的好处) -- [二 Executor 框架](#二-executor-框架) - - [2.1 简介](#21-简介) - - [2.2 Executor 框架结构(主要由三大部分组成)](#22-executor-框架结构主要由三大部分组成) - - [1) 任务(`Runnable` /`Callable`)](#1-任务runnable-callable) - - [2) 任务的执行(`Executor`)](#2-任务的执行executor) - - [3) 异步计算的结果(`Future`)](#3-异步计算的结果future) - - [2.3 Executor 框架的使用示意图](#23-executor-框架的使用示意图) -- [三 (重要)ThreadPoolExecutor 类简单介绍](#三-重要threadpoolexecutor-类简单介绍) - - [3.1 ThreadPoolExecutor 类分析](#31-threadpoolexecutor-类分析) - - [3.2 推荐使用 `ThreadPoolExecutor` 构造函数创建线程池](#32-推荐使用-threadpoolexecutor-构造函数创建线程池) -- [四 (重要)ThreadPoolExecutor 使用示例](#四-重要threadpoolexecutor-使用示例) - - [4.1 示例代码:`Runnable`+`ThreadPoolExecutor`](#41-示例代码runnablethreadpoolexecutor) - - [4.2 线程池原理分析](#42-线程池原理分析) - - [4.3 几个常见的对比](#43-几个常见的对比) - - [4.3.1 `Runnable` vs `Callable`](#431-runnable-vs-callable) - - [4.3.2 `execute()` vs `submit()`](#432-execute-vs-submit) - - [4.3.3 `shutdown()`VS`shutdownNow()`](#433-shutdownvsshutdownnow) - - [4.3.2 `isTerminated()` VS `isShutdown()`](#432-isterminated-vs-isshutdown) - - [4.4 加餐:`Callable`+`ThreadPoolExecutor`示例代码](#44-加餐callablethreadpoolexecutor示例代码) -- [五 几种常见的线程池详解](#五-几种常见的线程池详解) - - [5.1 FixedThreadPool](#51-fixedthreadpool) - - [5.1.1 介绍](#511-介绍) - - [5.1.2 执行任务过程介绍](#512-执行任务过程介绍) - - [5.1.3 为什么不推荐使用`FixedThreadPool`?](#513-为什么不推荐使用fixedthreadpool) - - [5.2 SingleThreadExecutor 详解](#52-singlethreadexecutor-详解) - - [5.2.1 介绍](#521-介绍) - - [5.2.2 执行任务过程介绍](#522-执行任务过程介绍) - - [5.2.3 为什么不推荐使用`SingleThreadExecutor`?](#523-为什么不推荐使用singlethreadexecutor) - - [5.3 CachedThreadPool 详解](#53-cachedthreadpool-详解) - - [5.3.1 介绍](#531-介绍) - - [5.3.2 执行任务过程介绍](#532-执行任务过程介绍) - - [5.3.3 为什么不推荐使用`CachedThreadPool`?](#533-为什么不推荐使用cachedthreadpool) -- [六 ScheduledThreadPoolExecutor 详解](#六-scheduledthreadpoolexecutor-详解) - - [6.1 简介](#61-简介) - - [6.2 运行机制](#62-运行机制) - - [6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤](#63-scheduledthreadpoolexecutor-执行周期任务的步骤) -- [七 线程池大小确定](#七-线程池大小确定) -- [八 参考](#八-参考) -- [九 其他推荐阅读](#九-其他推荐阅读) - - ## 一 使用线程池的好处 -> **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** +> **池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** **线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: - **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 +- **提高响应速度**。当任务到达时,任务可以不需要等到线程创建就能立即执行。 - **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 ## 二 Executor 框架 ### 2.1 简介 -Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。 +`Executor` 框架是 Java5 之后引进的,在 Java 5 之后,通过 `Executor` 来启动线程比使用 `Thread` 的 `start` 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。 > 补充:this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。 -Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。 +`Executor` 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,`Executor` 框架让并发编程变得更加简单。 ### 2.2 Executor 框架结构(主要由三大部分组成) @@ -91,13 +53,13 @@ public class ThreadPoolExecutor extends AbstractExecutorService **`ScheduledThreadPoolExecutor` 类描述:** ```java -//ScheduledExecutorService实现了ExecutorService接口 +//ScheduledExecutorService继承ExecutorService接口 public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService ``` -![任务的执行相关接口](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/任务的执行相关接口.png) +![任务的执行相关接口](images/java线程池学习总结/任务的执行相关接口.png) #### 3) 异步计算的结果(`Future`) @@ -107,7 +69,7 @@ public class ScheduledThreadPoolExecutor ### 2.3 Executor 框架的使用示意图 -![Executor 框架的使用示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC01LTMwLzg0ODIzMzMwLmpwZw?x-oss-process=image/format,png) +![Executor 框架的使用示意图](images/java线程池学习总结/Executor框架的使用示意图.png) 1. **主线程首先要创建实现 `Runnable` 或者 `Callable` 接口的任务对象。** 2. **把创建完成的实现 `Runnable`/`Callable`接口的 对象直接交给 `ExecutorService` 执行**: `ExecutorService.execute(Runnable command)`)或者也可以把 `Runnable` 对象或`Callable` 对象提交给 `ExecutorService` 执行(`ExecutorService.submit(Runnable task)`或 `ExecutorService.submit(Callable task)`)。 @@ -120,7 +82,7 @@ public class ScheduledThreadPoolExecutor ### 3.1 ThreadPoolExecutor 类分析 -`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。 +`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)。 ```java /** @@ -150,15 +112,15 @@ public class ScheduledThreadPoolExecutor } ``` -**下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。** +下面这些对创建非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。 **`ThreadPoolExecutor` 3 个最重要的参数:** - **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 - **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。 +- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 -`ThreadPoolExecutor`其他常见参数: +`ThreadPoolExecutor`其他常见参数 : 1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; 2. **`unit`** : `keepAliveTime` 参数的时间单位。 @@ -167,16 +129,16 @@ public class ScheduledThreadPoolExecutor 下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》): -![线程池各个参数的关系](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/线程池各个参数的关系.jpg) +![线程池各个参数的关系](images/java线程池学习总结/线程池各个参数之间的关系.png) **`ThreadPoolExecutor` 饱和策略定义:** -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略: +如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolTaskExecutor` 定义一些策略: -- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 -- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 +- **`ThreadPoolExecutor.AbortPolicy`** :抛出 `RejectedExecutionException`来拒绝新任务的处理。 +- **`ThreadPoolExecutor.CallerRunsPolicy`** :调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 +- **`ThreadPoolExecutor.DiscardPolicy`** :不处理新任务,直接丢弃掉。 +- **`ThreadPoolExecutor.DiscardOldestPolicy`** : 此策略将丢弃最早的未处理的任务请求。 举个例子: @@ -184,32 +146,34 @@ public class ScheduledThreadPoolExecutor ### 3.2 推荐使用 `ThreadPoolExecutor` 构造函数创建线程池 -**在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。** +在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 **为什么呢?** -> **使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。** +> 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 -**另外《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险** +另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 `Executors` 去创建,而是通过 `ThreadPoolExecutor` 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 -> Executors 返回线程池对象的弊端如下: +> `Executors` 返回线程池对象的弊端如下(后文会详细介绍到): > -> - **`FixedThreadPool` 和 `SingleThreadExecutor`** : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。 -> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 +> - **`FixedThreadPool` 和 `SingleThreadExecutor`** : 允许请求的队列长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。 +> - **`CachedThreadPool` 和 `ScheduledThreadPool`** : 允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。 **方式一:通过`ThreadPoolExecutor`构造函数实现(推荐)** -![通过构造方法实现](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzE3ODU4MjMwLmpwZw?x-oss-process=image/format,png) -**方式二:通过 Executor 框架的工具类 Executors 来实现** -我们可以创建三种类型的 ThreadPoolExecutor: +![通过构造方法实现](images/java线程池学习总结/threadpoolexecutor构造函数.png) -- **FixedThreadPool** -- **SingleThreadExecutor** -- **CachedThreadPool** +**方式二:通过 `Executor` 框架的工具类 `Executors` 来实现** +我们可以创建三种类型的 `ThreadPoolExecutor`: + +- `FixedThreadPool` +- `SingleThreadExecutor` +- CachedThreadPool 对应 Executors 工具类中的方法如图所示: -![通过Executor 框架的工具类Executors来实现](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzEzMjk2OTAxLmpwZw?x-oss-process=image/format,png) -## 四 (重要)ThreadPoolExecutor 使用示例 +![通过Executor 框架的工具类Executors来实现](images/java线程池学习总结/Executors工具类.png) + +## 四 ThreadPoolExecutor 使用+原理分析 我们上面讲解了 `Executor`框架以及 `ThreadPoolExecutor` 类,下面让我们实战一下,来通过写一个 `ThreadPoolExecutor` 的小 Demo 来回顾上面的内容。 @@ -337,16 +301,16 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 ### 4.2 线程池原理分析 -承接 4.1 节,我们通过代码输出结果可以看出:**线程首先会先执行 5 个任务,然后这些任务有任务被执行完的话,就会去拿新的任务执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) +承接 4.1 节,我们通过代码输出结果可以看出:**线程池首先会先执行 5 个任务,然后这些任务有任务被执行完的话,就会去拿新的任务执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) 现在,我们就分析上面的输出内容来简单分析一下线程池原理。 -**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.1 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: +**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。** 在 4.1 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: ```java // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); - + private static int workerCountOf(int c) { return c & CAPACITY; } @@ -388,11 +352,9 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。 -![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png) +![图解线程池实现原理](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/%E5%9B%BE%E8%A7%A3%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86.png) - - -**`addWorker` 这个方法主要用来创建新的工作线程,如果返回true说明创建和启动工作线程成功,否则的话返回的就是false。** +**`addWorker` 这个方法主要用来创建新的工作线程,如果返回 true 说明创建和启动工作线程成功,否则的话返回的就是 false。** ```java // 全局锁,并发操作必备 @@ -432,7 +394,7 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 for (;;) { //获取线程池中线程的数量 int wc = workerCountOf(c); - // core参数为true的话表明队列也满了,线程池大小变为 maximumPoolSize + // core参数为true的话表明队列也满了,线程池大小变为 maximumPoolSize if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; @@ -440,7 +402,7 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 if (compareAndIncrementWorkerCount(c)) break retry; // 如果线程的状态改变了就再次执行上述操作 - c = ctl.get(); + c = ctl.get(); if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop @@ -452,7 +414,7 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 boolean workerAdded = false; Worker w = null; try { - + w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { @@ -497,21 +459,21 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 } ``` -更多关于线程池源码分析的内容推荐这篇文章:《[JUC线程池ThreadPoolExecutor源码分析](http://www.throwable.club/2019/07/15/java-concurrency-thread-pool-executor/)》 +更多关于线程池源码分析的内容推荐这篇文章:《[JUC 线程池 ThreadPoolExecutor 源码分析](http://www.throwable.club/2019/07/15/java-concurrency-thread-pool-executor/)》 现在,让我们在回到 4.1 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢? 没搞懂的话,也没关系,可以看看我的分析: -> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的5个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。 +> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。 ### 4.3 几个常见的对比 #### 4.3.1 `Runnable` vs `Callable` -`Runnable`自 Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。 +`Runnable`自 Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是 **`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。 -工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callable(Runnable task`)或 `Executors.callable(Runnable task,Object resule)`)。 +工具类 `Executors` 可以实现将 `Runnable` 对象转换成 `Callable` 对象。(`Executors.callable(Runnable task)` 或 `Executors.callable(Runnable task, Object result)`)。 `Runnable.java` @@ -542,10 +504,10 @@ public interface Callable { #### 4.3.2 `execute()` vs `submit()` -1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** -2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 +- `execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否; +- `submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 -我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码: +我们以 `AbstractExecutorService` 接口中的一个 `submit()` 方法为例子来看看源代码: ```java public Future submit(Runnable task) { @@ -705,7 +667,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 `FixedThreadPool` 的 `execute()` 方法运行示意图(该图片来源:《Java 并发编程的艺术》): -![FixedThreadPool的execute()方法运行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzcxMzc1OTYzLmpwZw?x-oss-process=image/format,png) +![FixedThreadPool的execute()方法运行示意图](images/java线程池学习总结/FixedThreadPool.png) **上图说明:** @@ -715,7 +677,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 #### 5.1.3 为什么不推荐使用`FixedThreadPool`? -**`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :** +**`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`(队列的容量为 Integer.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :** 1. 当线程池中的线程数达到 `corePoolSize` 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize; 2. 由于使用无界队列时 `maximumPoolSize` 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 `FixedThreadPool`的源码可以看出创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 被设置为同一个值。 @@ -754,12 +716,12 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 #### 5.2.2 执行任务过程介绍 -**`SingleThreadExecutor` 的运行示意图(该图片来源:《Java 并发编程的艺术》):** -![SingleThreadExecutor的运行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzgyMjc2NDU4LmpwZw?x-oss-process=image/format,png) +`SingleThreadExecutor` 的运行示意图(该图片来源:《Java 并发编程的艺术》): +![SingleThreadExecutor的运行示意图](images/java线程池学习总结/SingleThreadExecutor.png) -**上图说明;** +**上图说明** : -1. 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务; +1. 如果当前运行的线程数少于 `corePoolSize`,则创建一个新的线程执行任务; 2. 当前线程池中有一个运行的线程后,将任务加入 `LinkedBlockingQueue` 3. 线程执行完当前的任务后,会在循环中反复从`LinkedBlockingQueue` 中获取任务来执行; @@ -794,12 +756,12 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 } ``` -`CachedThreadPool` 的`corePoolSize` 被设置为空(0),`maximumPoolSize`被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 `maximumPool` 中线程处理任务的速度时,`CachedThreadPool` 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。 +`CachedThreadPool` 的`corePoolSize` 被设置为空(0),`maximumPoolSize`被设置为 `Integer.MAX.VALUE`,即它是无界的,这也就意味着如果主线程提交任务的速度高于 `maximumPool` 中线程处理任务的速度时,`CachedThreadPool` 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。 #### 5.3.2 执行任务过程介绍 -**CachedThreadPool 的 execute()方法的执行示意图(该图片来源:《Java 并发编程的艺术》):** -![CachedThreadPool的execute()方法的执行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzE4NjExNzY3LmpwZw?x-oss-process=image/format,png) +`CachedThreadPool` 的 `execute()` 方法的执行示意图(该图片来源:《Java 并发编程的艺术》): +![CachedThreadPool的execute()方法的执行示意图](images/java线程池学习总结/CachedThreadPool-execute.png) **上图说明:** @@ -808,15 +770,15 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 #### 5.3.3 为什么不推荐使用`CachedThreadPool`? -`CachedThreadPool`允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 +`CachedThreadPool`允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。 ## 六 ScheduledThreadPoolExecutor 详解 -**`ScheduledThreadPoolExecutor` 主要用来在给定的延迟后运行任务,或者定期执行任务。** 这个在实际项目中基本不会被用到,因为有其他方案选择比如`quartz`。大家只需要简单了解一下它的思想。关于如何在 Spring Boot 中 实现定时任务,可以查看这篇文章[《5 分钟搞懂如何在 Spring Boot 中 Schedule Tasks》](https://github.com/Snailclimb/springboot-guide/blob/master/docs/advanced/SpringBoot-ScheduleTasks.md)。 +**`ScheduledThreadPoolExecutor` 主要用来在给定的延迟后运行任务,或者定期执行任务。** 这个在实际项目中基本不会被用到,也不推荐使用,大家只需要简单了解一下它的思想即可。 ### 6.1 简介 -**`ScheduledThreadPoolExecutor` 使用的任务队列 `DelayQueue` 封装了一个 `PriorityQueue`,`PriorityQueue` 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行(`ScheduledFutureTask` 的 `time` 变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(`ScheduledFutureTask` 的 `squenceNumber` 变量小的先执行)。** +`ScheduledThreadPoolExecutor` 使用的任务队列 `DelayQueue` 封装了一个 `PriorityQueue`,`PriorityQueue` 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行(`ScheduledFutureTask` 的 `time` 变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(`ScheduledFutureTask` 的 `squenceNumber` 变量小的先执行)。 **`ScheduledThreadPoolExecutor` 和 `Timer` 的比较:** @@ -826,15 +788,15 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 **综上,在 JDK1.5 之后,你没有理由再使用 Timer 进行任务调度了。** -> **备注:** Quartz 是一个由 java 编写的任务调度库,由 OpenSymphony 组织开源出来。在实际项目开发中使用 Quartz 的还是居多,比较推荐使用 Quartz。因为 Quartz 理论上能够同时对上万个任务进行调度,拥有丰富的功能特性,包括任务调度、任务持久化、可集群化、插件等等。 +> 关于定时任务的详细介绍,小伙伴们可以在 JavaGuide 的项目首页搜索“定时任务”找到对应的原创内容。 ### 6.2 运行机制 -![ScheduledThreadPoolExecutor运行机制](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzkyNTk0Njk4LmpwZw?x-oss-process=image/format,png) +![ScheduledThreadPoolExecutor运行机制](images/java线程池学习总结/ScheduledThreadPoolExecutor机制.png) **`ScheduledThreadPoolExecutor` 的执行主要分为两大部分:** -1. 当调用 `ScheduledThreadPoolExecutor` 的 **`scheduleAtFixedRate()`** 方法或者**`scheduleWirhFixedDelay()`** 方法时,会向 `ScheduledThreadPoolExecutor` 的 **`DelayQueue`** 添加一个实现了 **`RunnableScheduledFuture`** 接口的 **`ScheduledFutureTask`** 。 +1. 当调用 `ScheduledThreadPoolExecutor` 的 **`scheduleAtFixedRate()`** 方法或者 **`scheduleWithFixedDelay()`** 方法时,会向 `ScheduledThreadPoolExecutor` 的 **`DelayQueue`** 添加一个实现了 **`RunnableScheduledFuture`** 接口的 **`ScheduledFutureTask`** 。 2. 线程池中的线程从 `DelayQueue` 中获取 `ScheduledFutureTask`,然后执行任务。 **`ScheduledThreadPoolExecutor` 为了实现周期性的执行任务,对 `ThreadPoolExecutor`做了如下修改:** @@ -845,7 +807,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 ### 6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤 -![ScheduledThreadPoolExecutor执行周期任务的步骤](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC01LTMwLzU5OTE2Mzg5LmpwZw?x-oss-process=image/format,png) +![ScheduledThreadPoolExecutor执行周期任务的步骤](images/java线程池学习总结/ScheduledThreadPoolExecutor执行周期任务步骤.png) 1. 线程 1 从 `DelayQueue` 中获取已到期的 `ScheduledFutureTask(DelayQueue.take())`。到期任务是指 `ScheduledFutureTask`的 time 大于等于当前系统的时间; 2. 线程 1 执行这个 `ScheduledFutureTask`; @@ -879,7 +841,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 **如何判断是 CPU 密集任务还是 IO 密集任务?** -CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。 +CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。 ## 八 参考 diff --git a/docs/java/concurrent/reentrantlock.md b/docs/java/concurrent/reentrantlock.md new file mode 100644 index 00000000000..fad97ff4dc7 --- /dev/null +++ b/docs/java/concurrent/reentrantlock.md @@ -0,0 +1,994 @@ +--- +title: 从ReentrantLock的实现看AQS的原理及应用 +category: Java +tag: + - Java并发 +--- + +> 本文转载自:https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html +> +> 作者:美团技术团队 + +## 前言 + +Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。本文会从应用层逐渐深入到原理层,并通过ReentrantLock的基本特性和ReentrantLock与AQS的关联,来深入解读AQS相关独占锁的知识点,同时采取问答的模式来帮助大家理解AQS。由于篇幅原因,本篇文章主要阐述AQS中独占锁的逻辑和Sync Queue,不讲述包含共享锁和Condition Queue的部分(本篇文章核心为AQS原理剖析,只是简单介绍了ReentrantLock,感兴趣同学可以阅读一下ReentrantLock的源码)。 + +## 1 ReentrantLock + +### 1.1 ReentrantLock特性概览 + +ReentrantLock意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。为了帮助大家更好地理解ReentrantLock的特性,我们先将ReentrantLock跟常用的Synchronized进行比较,其特性如下(蓝色部分为本篇文章主要剖析的点): + +![img](https://p0.meituan.net/travelcube/412d294ff5535bbcddc0d979b2a339e6102264.png) + +下面通过伪代码,进行更加直观的比较: + +```java +// **************************Synchronized的使用方式************************** +// 1.用于代码块 +synchronized (this) {} +// 2.用于对象 +synchronized (object) {} +// 3.用于方法 +public synchronized void test () {} +// 4.可重入 +for (int i = 0; i < 100; i++) { + synchronized (this) {} +} +// **************************ReentrantLock的使用方式************************** +public void test () throw Exception { + // 1.初始化选择公平锁、非公平锁 + ReentrantLock lock = new ReentrantLock(true); + // 2.可用于代码块 + lock.lock(); + try { + try { + // 3.支持多种加锁方式,比较灵活; 具有可重入特性 + if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ } + } finally { + // 4.手动释放锁 + lock.unlock() + } + } finally { + lock.unlock(); + } +} +``` + +### 1.2 ReentrantLock与AQS的关联 + +通过上文我们已经了解,ReentrantLock支持公平锁和非公平锁(关于公平锁和非公平锁的原理分析,可参考《[不可不说的Java“锁”事](https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651749434&idx=3&sn=5ffa63ad47fe166f2f1a9f604ed10091&chksm=bd12a5778a652c61509d9e718ab086ff27ad8768586ea9b38c3dcf9e017a8e49bcae3df9bcc8&scene=38#wechat_redirect)》),并且ReentrantLock的底层就是由AQS来实现的。那么ReentrantLock是如何通过公平锁和非公平锁与AQS关联起来呢? 我们着重从这两者的加锁过程来理解一下它们与AQS之间的关系(加锁过程中与AQS的关联比较明显,解锁流程后续会介绍)。 + +非公平锁源码中的加锁流程如下: + +```java +// java.util.concurrent.locks.ReentrantLock#NonfairSync + +// 非公平锁 +static final class NonfairSync extends Sync { + ... + final void lock() { + if (compareAndSetState(0, 1)) + setExclusiveOwnerThread(Thread.currentThread()); + else + acquire(1); + } + ... +} +``` + +这块代码的含义为: + +- 若通过CAS设置变量State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。 +- 若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入Acquire方法进行后续处理。 + +第一步很好理解,但第二步获取锁失败后,后续的处理策略是怎么样的呢?这块可能会有以下思考: + +- 某个线程获取锁失败的后续流程是什么呢?有以下两种可能: + +(1) 将当前线程获锁结果设置为失败,获取锁流程结束。这种设计会极大降低系统的并发度,并不满足我们实际的需求。所以就需要下面这种流程,也就是AQS框架的处理流程。 + +(2) 存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。 + +- 对于问题1的第二种情况,既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢? +- 处于排队等候机制中的线程,什么时候可以有机会获取锁呢? +- 如果处于排队等候机制中的线程一直无法获取锁,还是需要一直等待吗,还是有别的策略来解决这一问题? + +带着非公平锁的这些问题,再看下公平锁源码中获锁的方式: + +``` +// java.util.concurrent.locks.ReentrantLock#FairSync + +static final class FairSync extends Sync { + ... + final void lock() { + acquire(1); + } + ... +} +``` + +看到这块代码,我们可能会存在这种疑问:Lock函数通过Acquire方法进行加锁,但是具体是如何加锁的呢? + +结合公平锁和非公平锁的加锁流程,虽然流程上有一定的不同,但是都调用了Acquire方法,而Acquire方法是FairSync和UnfairSync的父类AQS中的核心方法。 + +对于上边提到的问题,其实在ReentrantLock类源码中都无法解答,而这些问题的答案,都是位于Acquire方法所在的类AbstractQueuedSynchronizer中,也就是本文的核心——AQS。下面我们会对AQS以及ReentrantLock和AQS的关联做详细介绍(相关问题答案会在2.3.5小节中解答)。 + +## 2 AQS + +首先,我们通过下面的架构图来整体了解一下AQS框架: + +![](https://p1.meituan.net/travelcube/82077ccf14127a87b77cefd1ccf562d3253591.png) + +- 上图中有颜色的为Method,无颜色的为Attribution。 +- 总的来说,AQS框架共分为五层,自上而下由浅入深,从AQS对外暴露的API到底层基础数据。 +- 当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。 + +下面我们会从整体到细节,从流程到方法逐一剖析AQS框架,主要分析过程如下: + +![](https://p1.meituan.net/travelcube/d2f7f7fffdc30d85d17b44266c3ab05323338.png) + +### 2.1 原理概览 + +AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。 + +CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。 + +主要原理图如下: + +![](https://p0.meituan.net/travelcube/7132e4cef44c26f62835b197b239147b18062.png) + +AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。 + +#### 2.1.1 AQS数据结构 + +先来看下AQS中最基本的数据结构——Node,Node即为上面CLH变体队列中的节点。 + +![](https://p1.meituan.net/travelcube/960271cf2b5c8a185eed23e98b72c75538637.png) + +解释一下几个方法和属性值的含义: + +| 方法和属性值 | 含义 | +| :----------- | :----------------------------------------------------------- | +| waitStatus | 当前节点在队列中的状态 | +| thread | 表示处于该节点的线程 | +| prev | 前驱指针 | +| predecessor | 返回前驱节点,没有的话抛出npe | +| nextWaiter | 指向下一个处于CONDITION状态的节点(由于本篇文章不讲述Condition Queue队列,这个指针不多介绍) | +| next | 后继指针 | + +线程两种锁的模式: + +| 模式 | 含义 | +| :-------- | :----------------------------- | +| SHARED | 表示线程以共享的模式等待锁 | +| EXCLUSIVE | 表示线程正在以独占的方式等待锁 | + +waitStatus有下面几个枚举值: + +| 枚举 | 含义 | +| :-------- | :--------------------------------------------- | +| 0 | 当一个Node被初始化的时候的默认值 | +| CANCELLED | 为1,表示线程获取锁的请求已经取消了 | +| CONDITION | 为-2,表示节点在等待队列中,节点线程等待唤醒 | +| PROPAGATE | 为-3,当前线程处在SHARED情况下,该字段才会使用 | +| SIGNAL | 为-1,表示线程已经准备好了,就等资源释放了 | + +#### 2.1.2 同步状态State + +在了解数据结构后,接下来了解一下AQS的同步状态——State。AQS中维护了一个名为state的字段,意为同步状态,是由Volatile修饰的,用于展示当前临界资源的获锁情况。 + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private volatile int state; +``` + +下面提供了几个访问这个字段的方法: + +| 方法名 | 描述 | +| :----------------------------------------------------------- | :------------------- | +| protected final int getState() | 获取State的值 | +| protected final void setState(int newState) | 设置State的值 | +| protected final boolean compareAndSetState(int expect, int update) | 使用CAS方式更新State | + +这几个方法都是Final修饰的,说明子类中无法重写它们。我们可以通过修改State字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)。 + +![](https://p0.meituan.net/travelcube/27605d483e8935da683a93be015713f331378.png) + +![](https://p0.meituan.net/travelcube/3f1e1a44f5b7d77000ba4f9476189b2e32806.png) + +对于我们自定义的同步工具,需要自定义获取同步状态和释放状态的方式,也就是AQS架构图中的第一层:API层。 + +## 2.2 AQS重要方法与ReentrantLock的关联 + +从架构图中可以得知,AQS提供了大量用于自定义同步器实现的Protected方法。自定义同步器实现的相关方法也只是为了通过修改State字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法(ReentrantLock需要实现的方法如下,并不是全部): + +| 方法名 | 描述 | +| :------------------------------------------ | :----------------------------------------------------------- | +| protected boolean isHeldExclusively() | 该线程是否正在独占资源。只有用到Condition才需要去实现它。 | +| protected boolean tryAcquire(int arg) | 独占方式。arg为获取锁的次数,尝试获取资源,成功则返回True,失败则返回False。 | +| protected boolean tryRelease(int arg) | 独占方式。arg为释放锁的次数,尝试释放资源,成功则返回True,失败则返回False。 | +| protected int tryAcquireShared(int arg) | 共享方式。arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 | +| protected boolean tryReleaseShared(int arg) | 共享方式。arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。 | + +一般来说,自定义同步器要么是独占方式,要么是共享方式,它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease。 + +以非公平锁为例,这里主要阐述一下非公平锁与AQS之间方法的关联之处,具体每一处核心方法的作用会在文章后面详细进行阐述。 + +![](https://p1.meituan.net/travelcube/b8b53a70984668bc68653efe9531573e78636.png) + +为了帮助大家理解ReentrantLock和AQS之间方法的交互过程,以非公平锁为例,我们将加锁和解锁的交互流程单独拎出来强调一下,以便于对后续内容的理解。 + +![](https://p1.meituan.net/travelcube/7aadb272069d871bdee8bf3a218eed8136919.png) + +加锁: + +- 通过ReentrantLock的加锁方法Lock进行加锁操作。 +- 会调用到内部类Sync的Lock方法,由于Sync#lock是抽象方法,根据ReentrantLock初始化选择的公平锁和非公平锁,执行相关内部类的Lock方法,本质上都会执行AQS的Acquire方法。 +- AQS的Acquire方法会执行tryAcquire方法,但是由于tryAcquire需要自定义同步器实现,因此执行了ReentrantLock中的tryAcquire方法,由于ReentrantLock是通过公平锁和非公平锁内部类实现的tryAcquire方法,因此会根据锁类型不同,执行不同的tryAcquire。 +- tryAcquire是获取锁逻辑,获取失败后,会执行框架AQS的后续逻辑,跟ReentrantLock自定义同步器无关。 + +解锁: + +- 通过ReentrantLock的解锁方法Unlock进行解锁。 +- Unlock会调用内部类Sync的Release方法,该方法继承于AQS。 +- Release中会调用tryRelease方法,tryRelease需要自定义同步器实现,tryRelease只在ReentrantLock中的Sync实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。 +- 释放成功后,所有处理由AQS框架完成,与自定义同步器无关。 + +通过上面的描述,大概可以总结出ReentrantLock加锁解锁时API层核心方法的映射关系。 + +![](https://p0.meituan.net/travelcube/f30c631c8ebbf820d3e8fcb6eee3c0ef18748.png) + +## 2.3 通过ReentrantLock理解AQS + +ReentrantLock中公平锁和非公平锁在底层是相同的,这里以非公平锁为例进行分析。 + +在非公平锁中,有一段这样的代码: + +```java +// java.util.concurrent.locks.ReentrantLock + +static final class NonfairSync extends Sync { + ... + final void lock() { + if (compareAndSetState(0, 1)) + setExclusiveOwnerThread(Thread.currentThread()); + else + acquire(1); + } + ... +} +``` + +看一下这个Acquire是怎么写的: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +public final void acquire(int arg) { + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +``` + +再看一下tryAcquire方法: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +protected boolean tryAcquire(int arg) { + throw new UnsupportedOperationException(); +} +``` + +可以看出,这里只是AQS的简单实现,具体获取锁的实现方法是由各自的公平锁和非公平锁单独实现的(以ReentrantLock为例)。如果该方法返回了True,则说明当前线程获取锁成功,就不用往后执行了;如果获取失败,就需要加入到等待队列中。下面会详细解释线程是何时以及怎样被加入进等待队列中的。 + +### 2.3.1 线程加入等待队列 + +#### 2.3.1.1 加入队列的时机 + +当执行Acquire(1)时,会通过tryAcquire获取锁。在这种情况下,如果获取锁失败,就会调用addWaiter加入到等待队列中去。 + +#### 2.3.1.2 如何加入队列 + +获取锁失败后,会执行addWaiter(Node.EXCLUSIVE)加入等待队列,具体实现方法如下: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private Node addWaiter(Node mode) { + Node node = new Node(Thread.currentThread(), mode); + // Try the fast path of enq; backup to full enq on failure + Node pred = tail; + if (pred != null) { + node.prev = pred; + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + enq(node); + return node; +} +private final boolean compareAndSetTail(Node expect, Node update) { + return unsafe.compareAndSwapObject(this, tailOffset, expect, update); +} +``` + +主要的流程如下: + +- 通过当前的线程和锁模式新建一个节点。 +- Pred指针指向尾节点Tail。 +- 将New中Node的Prev指针指向Pred。 +- 通过compareAndSetTail方法,完成尾节点的设置。这个方法主要是对tailOffset和Expect进行比较,如果tailOffset的Node和Expect的Node地址是相同的,那么设置Tail的值为Update的值。 + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +static { + try { + stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state")); + headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head")); + tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail")); + waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus")); + nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next")); + } catch (Exception ex) { + throw new Error(ex); + } +} +``` + +从AQS的静态代码块可以看出,都是获取一个对象的属性相对于该对象在内存当中的偏移量,这样我们就可以根据这个偏移量在对象内存当中找到这个属性。tailOffset指的是tail对应的偏移量,所以这个时候会将new出来的Node置为当前队列的尾节点。同时,由于是双向链表,也需要将前一个节点指向尾节点。 + +- 如果Pred指针是Null(说明等待队列中没有元素),或者当前Pred指针和Tail指向的位置不同(说明被别的线程已经修改),就需要看一下Enq的方法。 + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private Node enq(final Node node) { + for (;;) { + Node t = tail; + if (t == null) { // Must initialize + if (compareAndSetHead(new Node())) + tail = head; + } else { + node.prev = t; + if (compareAndSetTail(t, node)) { + t.next = node; + return t; + } + } + } +} +``` + +如果没有被初始化,需要进行初始化一个头结点出来。但请注意,初始化的头结点并不是当前线程节点,而是调用了无参构造函数的节点。如果经历了初始化或者并发导致队列中有元素,则与之前的方法相同。其实,addWaiter就是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。 + +总结一下,线程获取锁的时候,过程大体如下: + +1. 当没有线程获取到锁时,线程1获取锁成功。 +2. 线程2申请锁,但是锁被线程1占有。 + +![img](https://p0.meituan.net/travelcube/e9e385c3c68f62c67c8d62ab0adb613921117.png) + +1. 如果再有线程要获取锁,依次在队列中往后排队即可。 + +回到上边的代码,hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回False,说明当前线程可以争取共享资源;如果返回True,说明队列中存在有效节点,当前线程必须加入到等待队列中。 + +```java +// java.util.concurrent.locks.ReentrantLock + +public final boolean hasQueuedPredecessors() { + // The correctness of this depends on head being initialized + // before tail and on head.next being accurate if the current + // thread is first in queue. + Node t = tail; // Read fields in reverse initialization order + Node h = head; + Node s; + return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); +} +``` + +看到这里,我们理解一下h != t && ((s = h.next) == null || s.thread != Thread.currentThread());为什么要判断的头结点的下一个节点?第一个节点储存的数据是什么? + +> 双向链表中,第一个节点为虚节点,其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是在第二个节点开始的。当h != t时: 如果(s = h.next) == null,等待队列正在有线程进行初始化,但只是进行到了Tail指向Head,没有将Head指向Tail,此时队列中有元素,需要返回True(这块具体见下边代码分析)。 如果(s = h.next) != null,说明此时队列中至少有一个有效节点。如果此时s.thread == Thread.currentThread(),说明等待队列的第一个有效节点中的线程与当前线程相同,那么当前线程是可以获取资源的;如果s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列。 + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer#enq + +if (t == null) { // Must initialize + if (compareAndSetHead(new Node())) + tail = head; +} else { + node.prev = t; + if (compareAndSetTail(t, node)) { + t.next = node; + return t; + } +} +``` + +节点入队不是原子操作,所以会出现短暂的head != tail,此时Tail指向最后一个节点,而且Tail指向Head。如果Head没有指向Tail(可见5、6、7行),这种情况下也需要将相关线程加入队列中。所以这块代码是为了解决极端情况下的并发问题。 + +#### 2.3.1.3 等待队列中线程出队列时机 + +回到最初的源码: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +public final void acquire(int arg) { + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +``` + +上文解释了addWaiter方法,这个方法其实就是把对应的线程以Node的数据结构形式加入到双端队列里,返回的是一个包含该线程的Node。而这个Node会作为参数,进入到acquireQueued方法中。acquireQueued方法可以对排队中的线程进行“获锁”操作。 + +总的来说,一个线程获取锁失败了,被放入等待队列,acquireQueued会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。 + +下面我们从“何时出队列?”和“如何出队列?”两个方向来分析一下acquireQueued源码: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +final boolean acquireQueued(final Node node, int arg) { + // 标记是否成功拿到资源 + boolean failed = true; + try { + // 标记等待过程中是否中断过 + boolean interrupted = false; + // 开始自旋,要么获取锁,要么中断 + for (;;) { + // 获取当前节点的前驱节点 + final Node p = node.predecessor(); + // 如果p是头结点,说明当前节点在真实数据队列的首部,就尝试获取锁(别忘了头结点是虚节点) + if (p == head && tryAcquire(arg)) { + // 获取锁成功,头指针移动到当前node + setHead(node); + p.next = null; // help GC + failed = false; + return interrupted; + } + // 说明p为头节点且当前没有获取到锁(可能是非公平锁被抢占了)或者是p不为头结点,这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源。具体两个方法下面细细分析 + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } +} +``` + +注:setHead方法是把当前节点置为虚节点,但并没有修改waitStatus,因为它是一直需要用的数据。 + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private void setHead(Node node) { + head = node; + node.thread = null; + node.prev = null; +} + +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +// 靠前驱节点判断当前线程是否应该被阻塞 +private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { + // 获取头结点的节点状态 + int ws = pred.waitStatus; + // 说明头结点处于唤醒状态 + if (ws == Node.SIGNAL) + return true; + // 通过枚举值我们知道waitStatus>0是取消状态 + if (ws > 0) { + do { + // 循环向前查找取消节点,把取消节点从队列中剔除 + node.prev = pred = pred.prev; + } while (pred.waitStatus > 0); + pred.next = node; + } else { + // 设置前任节点等待状态为SIGNAL + compareAndSetWaitStatus(pred, ws, Node.SIGNAL); + } + return false; +} +``` + +parkAndCheckInterrupt主要用于挂起当前线程,阻塞调用栈,返回当前线程的中断状态。 + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private final boolean parkAndCheckInterrupt() { + LockSupport.park(this); + return Thread.interrupted(); +} +``` + +上述方法的流程图如下: + +![](https://p0.meituan.net/travelcube/c124b76dcbefb9bdc778458064703d1135485.png) + +从上图可以看出,跳出当前循环的条件是当“前置节点是头结点,且当前线程获取锁成功”。为了防止因死循环导致CPU资源被浪费,我们会判断前置节点的状态来决定是否要将当前线程挂起,具体挂起流程用流程图表示如下(shouldParkAfterFailedAcquire流程): + +![](https://p0.meituan.net/travelcube/9af16e2481ad85f38ca322a225ae737535740.png) + +从队列中释放节点的疑虑打消了,那么又有新问题了: + +- shouldParkAfterFailedAcquire中取消节点是怎么生成的呢?什么时候会把一个节点的waitStatus设置为-1? +- 是在什么时间释放节点通知到被挂起的线程呢? + +### 2.3.2 CANCELLED状态节点生成 + +acquireQueued方法中的Finally代码: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +final boolean acquireQueued(final Node node, int arg) { + boolean failed = true; + try { + ... + for (;;) { + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + ... + failed = false; + ... + } + ... + } finally { + if (failed) + cancelAcquire(node); + } +} +``` + +通过cancelAcquire方法,将Node的状态标记为CANCELLED。接下来,我们逐行来分析这个方法的原理: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private void cancelAcquire(Node node) { + // 将无效节点过滤 + if (node == null) + return; + // 设置该节点不关联任何线程,也就是虚节点 + node.thread = null; + Node pred = node.prev; + // 通过前驱节点,跳过取消状态的node + while (pred.waitStatus > 0) + node.prev = pred = pred.prev; + // 获取过滤后的前驱节点的后继节点 + Node predNext = pred.next; + // 把当前node的状态设置为CANCELLED + node.waitStatus = Node.CANCELLED; + // 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点 + // 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为null + if (node == tail && compareAndSetTail(node, pred)) { + compareAndSetNext(pred, predNext, null); + } else { + int ws; + // 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SINGAL看是否成功 + // 如果1和2中有一个为true,再判断当前节点的线程是否为null + // 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点 + if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { + Node next = node.next; + if (next != null && next.waitStatus <= 0) + compareAndSetNext(pred, predNext, next); + } else { + // 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点 + unparkSuccessor(node); + } + node.next = node; // help GC + } +} +``` + +当前的流程: + +- 获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED。 +- 根据当前节点的位置,考虑以下三种情况: + +(1) 当前节点是尾节点。 + +(2) 当前节点是Head的后继节点。 + +(3) 当前节点不是Head的后继节点,也不是尾节点。 + +根据上述第二条,我们来分析每一种情况的流程。 + +当前节点是尾节点。 + +![](https://p1.meituan.net/travelcube/b845211ced57561c24f79d56194949e822049.png) + +当前节点是Head的后继节点。 + +![](https://p1.meituan.net/travelcube/ab89bfec875846e5028a4f8fead32b7117975.png) + +当前节点不是Head的后继节点,也不是尾节点。 + +![](https://p0.meituan.net/travelcube/45d0d9e4a6897eddadc4397cf53d6cd522452.png) + +通过上面的流程,我们对于CANCELLED节点状态的产生和变化已经有了大致的了解,但是为什么所有的变化都是对Next指针进行了操作,而没有对Prev指针进行操作呢?什么情况下会对Prev指针进行操作? + +> 执行cancelAcquire的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过Try代码块中的shouldParkAfterFailedAcquire方法了),如果此时修改Prev指针,有可能会导致Prev指向另一个已经移除队列的Node,因此这块变化Prev指针不安全。 shouldParkAfterFailedAcquire方法中,会执行下面的代码,其实就是在处理Prev指针。shouldParkAfterFailedAcquire是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更Prev指针比较安全。 +> +> ```java +> do { +> node.prev = pred = pred.prev; +> } while (pred.waitStatus > 0); +> ``` + +### 2.3.3 如何解锁 + +我们已经剖析了加锁过程中的基本流程,接下来再对解锁的基本流程进行分析。由于ReentrantLock在解锁的时候,并不区分公平锁和非公平锁,所以我们直接看解锁的源码: + +```java +// java.util.concurrent.locks.ReentrantLock + +public void unlock() { + sync.release(1); +} +``` + +可以看到,本质释放锁的地方,是通过框架来完成的。 + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +public final boolean release(int arg) { + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} +``` + +在ReentrantLock里面的公平锁和非公平锁的父类Sync定义了可重入锁的释放锁机制。 + +```java +// java.util.concurrent.locks.ReentrantLock.Sync + +// 方法返回当前锁是不是没有被线程持有 +protected final boolean tryRelease(int releases) { + // 减少可重入次数 + int c = getState() - releases; + // 当前线程不是持有锁的线程,抛出异常 + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + // 如果持有线程全部释放,将当前独占锁所有线程设置为null,并更新state + if (c == 0) { + free = true; + setExclusiveOwnerThread(null); + } + setState(c); + return free; +} +``` + +我们来解释下述源码: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +public final boolean release(int arg) { + // 上边自定义的tryRelease如果返回true,说明该锁没有被任何线程持有 + if (tryRelease(arg)) { + // 获取头结点 + Node h = head; + // 头结点不为空并且头结点的waitStatus不是初始化节点情况,解除线程挂起状态 + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} +``` + +这里的判断条件为什么是h != null && h.waitStatus != 0? + +> h == null Head还没初始化。初始情况下,head == null,第一个节点入队,Head会被初始化一个虚拟节点。所以说,这里如果还没来得及入队,就会出现head == null 的情况。 +> +> h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。 +> +> h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒。 + +再看一下unparkSuccessor方法: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private void unparkSuccessor(Node node) { + // 获取头结点waitStatus + int ws = node.waitStatus; + if (ws < 0) + compareAndSetWaitStatus(node, ws, 0); + // 获取当前节点的下一个节点 + Node s = node.next; + // 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点 + if (s == null || s.waitStatus > 0) { + s = null; + // 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。 + for (Node t = tail; t != null && t != node; t = t.prev) + if (t.waitStatus <= 0) + s = t; + } + // 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unpark + if (s != null) + LockSupport.unpark(s.thread); +} +``` + +为什么要从后往前找第一个非Cancelled的节点呢?原因如下。 + +之前的addWaiter方法: + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private Node addWaiter(Node mode) { + Node node = new Node(Thread.currentThread(), mode); + // Try the fast path of enq; backup to full enq on failure + Node pred = tail; + if (pred != null) { + node.prev = pred; + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + enq(node); + return node; +} +``` + +我们从这里可以看到,节点入队并不是原子操作,也就是说,node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作,但是此时pred.next = node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。 + +综上所述,如果是从前往后找,由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点。所以,唤醒对应的线程后,对应的线程就会继续往下执行。继续执行acquireQueued方法以后,中断如何处理? + +### 2.3.4 中断恢复后的执行流程 + +唤醒后,会执行return Thread.interrupted();,这个函数返回的是当前执行线程的中断状态,并清除。 + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private final boolean parkAndCheckInterrupt() { + LockSupport.park(this); + return Thread.interrupted(); +} +``` + +再回到acquireQueued代码,当parkAndCheckInterrupt返回True或者False的时候,interrupted的值不同,但都会执行下次循环。如果这个时候获取锁成功,就会把当前interrupted返回。 + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +final boolean acquireQueued(final Node node, int arg) { + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + setHead(node); + p.next = null; // help GC + failed = false; + return interrupted; + } + if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } +} +``` + +如果acquireQueued为True,就会执行selfInterrupt方法。 + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +static void selfInterrupt() { + Thread.currentThread().interrupt(); +} +``` + +该方法其实是为了中断线程。但为什么获取了锁以后还要中断线程呢?这部分属于Java提供的协作式中断知识内容,感兴趣同学可以查阅一下。这里简单介绍一下: + +1. 当中断线程被唤醒时,并不知道被唤醒的原因,可能是当前线程在等待中被中断,也可能是释放了锁以后被唤醒。因此我们通过Thread.interrupted()方法检查中断标记(该方法返回了当前线程的中断状态,并将当前线程的中断标识设置为False),并记录下来,如果发现该线程被中断过,就再中断一次。 +2. 线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止。也就是说,在整个流程中,并不响应中断,只是记录中断记录。最后抢到锁返回了,那么如果被中断过的话,就需要补充一次中断。 + +这里的处理方式主要是运用线程池中基本运作单元Worder中的runWorker,通过Thread.interrupted()进行额外的判断处理,感兴趣的同学可以看下ThreadPoolExecutor源码。 + +### 2.3.5 小结 + +我们在1.3小节中提出了一些问题,现在来回答一下。 + +> Q:某个线程获取锁失败的后续流程是什么呢? +> +> A:存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。 +> +> Q:既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢? +> +> A:是CLH变体的FIFO双端队列。 +> +> Q:处于排队等候机制中的线程,什么时候可以有机会获取锁呢? +> +> A:可以详细看下2.3.1.3小节。 +> +> Q:如果处于排队等候机制中的线程一直无法获取锁,需要一直等待么?还是有别的策略来解决这一问题? +> +> A:线程所在节点的状态会变成取消状态,取消状态的节点会从队列中释放,具体可见2.3.2小节。 +> +> Q:Lock函数通过Acquire方法进行加锁,但是具体是如何加锁的呢? +> +> A:AQS的Acquire会调用tryAcquire方法,tryAcquire由各个自定义同步器实现,通过tryAcquire完成加锁过程。 + +## 3 AQS应用 + +### 3.1 ReentrantLock的可重入应用 + +ReentrantLock的可重入性是AQS很好的应用之一,在了解完上述知识点以后,我们很容易得知ReentrantLock实现可重入的方法。在ReentrantLock里面,不管是公平锁还是非公平锁,都有一段逻辑。 + +公平锁: + +```java +// java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire + +if (c == 0) { + if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } +} +else if (current == getExclusiveOwnerThread()) { + int nextc = c + acquires; + if (nextc < 0) + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; +} +``` + +非公平锁: + +```java +// java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire + +if (c == 0) { + if (compareAndSetState(0, acquires)){ + setExclusiveOwnerThread(current); + return true; + } +} +else if (current == getExclusiveOwnerThread()) { + int nextc = c + acquires; + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; +} +``` + +从上面这两段都可以看到,有一个同步状态State来控制整体可重入的情况。State是Volatile修饰的,用于保证一定的可见性和有序性。 + +```java +// java.util.concurrent.locks.AbstractQueuedSynchronizer + +private volatile int state; +``` + +接下来看State这个字段主要的过程: + +1. State初始化的时候为0,表示没有任何线程持有锁。 +2. 当有线程持有该锁时,值就会在原来的基础上+1,同一个线程多次获得锁是,就会多次+1,这里就是可重入的概念。 +3. 解锁也是对这个字段-1,一直到0,此线程对锁释放。 + +### 3.2 JUC中的应用场景 + +除了上边ReentrantLock的可重入性的应用,AQS作为并发编程的框架,为很多其他同步工具提供了良好的解决方案。下面列出了JUC中的几种同步工具,大体介绍一下AQS的应用场景: + +| 同步工具 | 同步工具与AQS的关联 | +| :--------------------- | :----------------------------------------------------------- | +| ReentrantLock | 使用AQS保存锁重复持有的次数。当一个线程获取锁时,ReentrantLock记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时异常情况的处理。 | +| Semaphore | 使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数,acquireShared会减少计数。 | +| CountDownLatch | 使用AQS同步状态来表示计数。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过。 | +| ReentrantReadWriteLock | 使用AQS同步状态中的16位保存写锁持有的次数,剩下的16位用于保存读锁的持有次数。 | +| ThreadPoolExecutor | Worker利用AQS同步状态实现对独占线程变量的设置(tryAcquire和tryRelease)。 | + +### 3.3 自定义同步工具 + +了解AQS基本原理以后,按照上面所说的AQS知识点,自己实现一个同步工具。 + +```java +public class LeeLock { + + private static class Sync extends AbstractQueuedSynchronizer { + @Override + protected boolean tryAcquire (int arg) { + return compareAndSetState(0, 1); + } + + @Override + protected boolean tryRelease (int arg) { + setState(0); + return true; + } + + @Override + protected boolean isHeldExclusively () { + return getState() == 1; + } + } + + private Sync sync = new Sync(); + + public void lock () { + sync.acquire(1); + } + + public void unlock () { + sync.release(1); + } +} +``` + +通过我们自己定义的Lock完成一定的同步功能。 + +```java +public class LeeMain { + + static int count = 0; + static LeeLock leeLock = new LeeLock(); + + public static void main (String[] args) throws InterruptedException { + + Runnable runnable = new Runnable() { + @Override + public void run () { + try { + leeLock.lock(); + for (int i = 0; i < 10000; i++) { + count++; + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + leeLock.unlock(); + } + + } + }; + Thread thread1 = new Thread(runnable); + Thread thread2 = new Thread(runnable); + thread1.start(); + thread2.start(); + thread1.join(); + thread2.join(); + System.out.println(count); + } +} +``` + +上述代码每次运行结果都会是20000。通过简单的几行代码就能实现同步功能,这就是AQS的强大之处。 + +## 总结 + +我们日常开发中使用并发的场景太多,但是对并发内部的基本框架原理了解的人却不多。由于篇幅原因,本文仅介绍了可重入锁ReentrantLock的原理和AQS原理,希望能够成为大家了解AQS和ReentrantLock等同步器的“敲门砖”。 + +## 参考资料 + +- Lea D. The java. util. concurrent synchronizer framework[J]. Science of Computer Programming, 2005, 58(3): 293-309. +- 《Java并发编程实战》 +- [不可不说的Java“锁”事](https://tech.meituan.com/2018/11/15/java-lock.html) \ No newline at end of file diff --git a/docs/java/concurrent/threadlocal.md b/docs/java/concurrent/threadlocal.md new file mode 100644 index 00000000000..a4aff50f23a --- /dev/null +++ b/docs/java/concurrent/threadlocal.md @@ -0,0 +1,914 @@ +--- +title: 万字解析 ThreadLocal 关键字 +category: Java +tag: + - Java并发 +--- + + + +> 本文来自一枝花算不算浪漫投稿, 原文地址:[https://juejin.im/post/5eacc1c75188256d976df748](https://juejin.im/post/5eacc1c75188256d976df748)。 + +### 前言 + +![](./images/thread-local/1.png) + +**全文共 10000+字,31 张图,这篇文章同样耗费了不少的时间和精力才创作完成,原创不易,请大家点点关注+在看,感谢。** + +对于`ThreadLocal`,大家的第一反应可能是很简单呀,线程的变量副本,每个线程隔离。那这里有几个问题大家可以思考一下: + +- `ThreadLocal`的 key 是**弱引用**,那么在 `ThreadLocal.get()`的时候,发生**GC**之后,key 是否为**null**? +- `ThreadLocal`中`ThreadLocalMap`的**数据结构**? +- `ThreadLocalMap`的**Hash 算法**? +- `ThreadLocalMap`中**Hash 冲突**如何解决? +- `ThreadLocalMap`的**扩容机制**? +- `ThreadLocalMap`中**过期 key 的清理机制**?**探测式清理**和**启发式清理**流程? +- `ThreadLocalMap.set()`方法实现原理? +- `ThreadLocalMap.get()`方法实现原理? +- 项目中`ThreadLocal`使用情况?遇到的坑? +- ...... + +上述的一些问题你是否都已经掌握的很清楚了呢?本文将围绕这些问题使用图文方式来剖析`ThreadLocal`的**点点滴滴**。 + +### 目录 + +**注明:** 本文源码基于`JDK 1.8` + +### `ThreadLocal`代码演示 + +我们先看下`ThreadLocal`使用示例: + +```java +public class ThreadLocalTest { + private List messages = Lists.newArrayList(); + + public static final ThreadLocal holder = ThreadLocal.withInitial(ThreadLocalTest::new); + + public static void add(String message) { + holder.get().messages.add(message); + } + + public static List clear() { + List messages = holder.get().messages; + holder.remove(); + + System.out.println("size: " + holder.get().messages.size()); + return messages; + } + + public static void main(String[] args) { + ThreadLocalTest.add("一枝花算不算浪漫"); + System.out.println(holder.get().messages); + ThreadLocalTest.clear(); + } +} +``` + +打印结果: + +```java +[一枝花算不算浪漫] +size: 0 +``` + +`ThreadLocal`对象可以提供线程局部变量,每个线程`Thread`拥有一份自己的**副本变量**,多个线程互不干扰。 + +### `ThreadLocal`的数据结构 + +![](./images/thread-local/2.png) + +`Thread`类有一个类型为`ThreadLocal.ThreadLocalMap`的实例变量`threadLocals`,也就是说每个线程有一个自己的`ThreadLocalMap`。 + +`ThreadLocalMap`有自己的独立实现,可以简单地将它的`key`视作`ThreadLocal`,`value`为代码中放入的值(实际上`key`并不是`ThreadLocal`本身,而是它的一个**弱引用**)。 + +每个线程在往`ThreadLocal`里放值的时候,都会往自己的`ThreadLocalMap`里存,读也是以`ThreadLocal`作为引用,在自己的`map`里找对应的`key`,从而实现了**线程隔离**。 + +`ThreadLocalMap`有点类似`HashMap`的结构,只是`HashMap`是由**数组+链表**实现的,而`ThreadLocalMap`中并没有**链表**结构。 + +我们还要注意`Entry`, 它的`key`是`ThreadLocal k` ,继承自`WeakReference`, 也就是我们常说的弱引用类型。 + +### GC 之后 key 是否为 null? + +回应开头的那个问题, `ThreadLocal` 的`key`是弱引用,那么在`ThreadLocal.get()`的时候,发生`GC`之后,`key`是否是`null`? + +为了搞清楚这个问题,我们需要搞清楚`Java`的**四种引用类型**: + +- **强引用**:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候 +- **软引用**:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收 +- **弱引用**:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收 +- **虚引用**:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知 + +接着再来看下代码,我们使用反射的方式来看看`GC`后`ThreadLocal`中的数据情况:(下面代码来源自:https://blog.csdn.net/thewindkee/article/details/103726942 本地运行演示 GC 回收场景) + +```java +public class ThreadLocalDemo { + + public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException { + Thread t = new Thread(()->test("abc",false)); + t.start(); + t.join(); + System.out.println("--gc后--"); + Thread t2 = new Thread(() -> test("def", true)); + t2.start(); + t2.join(); + } + + private static void test(String s,boolean isGC) { + try { + new ThreadLocal<>().set(s); + if (isGC) { + System.gc(); + } + Thread t = Thread.currentThread(); + Class clz = t.getClass(); + Field field = clz.getDeclaredField("threadLocals"); + field.setAccessible(true); + Object ThreadLocalMap = field.get(t); + Class tlmClass = ThreadLocalMap.getClass(); + Field tableField = tlmClass.getDeclaredField("table"); + tableField.setAccessible(true); + Object[] arr = (Object[]) tableField.get(ThreadLocalMap); + for (Object o : arr) { + if (o != null) { + Class entryClass = o.getClass(); + Field valueField = entryClass.getDeclaredField("value"); + Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent"); + valueField.setAccessible(true); + referenceField.setAccessible(true); + System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o))); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +结果如下: + +```java +弱引用key:java.lang.ThreadLocal@433619b6,值:abc +弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12 +--gc后-- +弱引用key:null,值:def +``` + +![](./images/thread-local/3.png) + +如图所示,因为这里创建的`ThreadLocal`并没有指向任何值,也就是没有任何引用: + +```java +new ThreadLocal<>().set(s); +``` + +所以这里在`GC`之后,`key`就会被回收,我们看到上面`debug`中的`referent=null`, 如果**改动一下代码:** + +![](./images/thread-local/4.png) + +这个问题刚开始看,如果没有过多思考,**弱引用**,还有**垃圾回收**,那么肯定会觉得是`null`。 + +其实是不对的,因为题目说的是在做 `ThreadLocal.get()` 操作,证明其实还是有**强引用**存在的,所以 `key` 并不为 `null`,如下图所示,`ThreadLocal`的**强引用**仍然是存在的。 + +![image.png](./images/thread-local/5.png) + +如果我们的**强引用**不存在的话,那么 `key` 就会被回收,也就是会出现我们 `value` 没被回收,`key` 被回收,导致 `value` 永远存在,出现内存泄漏。 + +### `ThreadLocal.set()`方法源码详解 + +![](./images/thread-local/6.png) + +`ThreadLocal`中的`set`方法原理如上图所示,很简单,主要是判断`ThreadLocalMap`是否存在,然后使用`ThreadLocal`中的`set`方法进行数据处理。 + +代码如下: + +```java +public void set(T value) { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + createMap(t, value); +} + +void createMap(Thread t, T firstValue) { + t.threadLocals = new ThreadLocalMap(this, firstValue); +} +``` + +主要的核心逻辑还是在`ThreadLocalMap`中的,一步步往下看,后面还有更详细的剖析。 + +### `ThreadLocalMap` Hash 算法 + +既然是`Map`结构,那么`ThreadLocalMap`当然也要实现自己的`hash`算法来解决散列表数组冲突问题。 + +```java +int i = key.threadLocalHashCode & (len-1); +``` + +`ThreadLocalMap`中`hash`算法很简单,这里`i`就是当前 key 在散列表中对应的数组下标位置。 + +这里最关键的就是`threadLocalHashCode`值的计算,`ThreadLocal`中有一个属性为`HASH_INCREMENT = 0x61c88647` + +```java +public class ThreadLocal { + private final int threadLocalHashCode = nextHashCode(); + + private static AtomicInteger nextHashCode = new AtomicInteger(); + + private static final int HASH_INCREMENT = 0x61c88647; + + private static int nextHashCode() { + return nextHashCode.getAndAdd(HASH_INCREMENT); + } + + static class ThreadLocalMap { + ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { + table = new Entry[INITIAL_CAPACITY]; + int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); + + table[i] = new Entry(firstKey, firstValue); + size = 1; + setThreshold(INITIAL_CAPACITY); + } + } +} +``` + +每当创建一个`ThreadLocal`对象,这个`ThreadLocal.nextHashCode` 这个值就会增长 `0x61c88647` 。 + +这个值很特殊,它是**斐波那契数** 也叫 **黄金分割数**。`hash`增量为 这个数字,带来的好处就是 `hash` **分布非常均匀**。 + +我们自己可以尝试下: + +![](./images/thread-local/8.png) + +可以看到产生的哈希码分布很均匀,这里不去细纠**斐波那契**具体算法,感兴趣的可以自行查阅相关资料。 + +### `ThreadLocalMap` Hash 冲突 + +> **注明:** 下面所有示例图中,**绿色块**`Entry`代表**正常数据**,**灰色块**代表`Entry`的`key`值为`null`,**已被垃圾回收**。**白色块**表示`Entry`为`null`。 + +虽然`ThreadLocalMap`中使用了**黄金分割数**来作为`hash`计算因子,大大减少了`Hash`冲突的概率,但是仍然会存在冲突。 + +`HashMap`中解决冲突的方法是在数组上构造一个**链表**结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成**红黑树**。 + +而 `ThreadLocalMap` 中并没有链表结构,所以这里不能使用 `HashMap` 解决冲突的方式了。 + +![](./images/thread-local/7.png) + +如上图所示,如果我们插入一个`value=27`的数据,通过 `hash` 计算后应该落入槽位 4 中,而槽位 4 已经有了 `Entry` 数据。 + +此时就会线性向后查找,一直找到 `Entry` 为 `null` 的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了 `Entry` 不为 `null` 且 `key` 值相等的情况,还有 `Entry` 中的 `key` 值为 `null` 的情况等等都会有不同的处理,后面会一一详细讲解。 + +这里还画了一个`Entry`中的`key`为`null`的数据(**Entry=2 的灰色块数据**),因为`key`值是**弱引用**类型,所以会有这种数据存在。在`set`过程中,如果遇到了`key`过期的`Entry`数据,实际上是会进行一轮**探测式清理**操作的,具体操作方式后面会讲到。 + +### `ThreadLocalMap.set()`详解 + +#### `ThreadLocalMap.set()`原理图解 + +看完了`ThreadLocal` **hash 算法**后,我们再来看`set`是如何实现的。 + +往`ThreadLocalMap`中`set`数据(**新增**或者**更新**数据)分为好几种情况,针对不同的情况我们画图来说明。 + +**第一种情况:** 通过`hash`计算后的槽位对应的`Entry`数据为空: + +![](./images/thread-local/9.png) + +这里直接将数据放到该槽位即可。 + +**第二种情况:** 槽位数据不为空,`key`值与当前`ThreadLocal`通过`hash`计算获取的`key`值一致: + +![](./images/thread-local/10.png) + +这里直接更新该槽位的数据。 + +**第三种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry`为`null`的槽位之前,没有遇到`key`过期的`Entry`: + +![](./images/thread-local/11.png) + +遍历散列数组,线性往后查找,如果找到`Entry`为`null`的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了**key 值相等**的数据,直接更新即可。 + +**第四种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry`为`null`的槽位之前,遇到`key`过期的`Entry`,如下图,往后遍历过程中,遇到了`index=7`的槽位数据`Entry`的`key=null`: + +![](./images/thread-local/12.png) + +散列数组下标为 7 位置对应的`Entry`数据`key`为`null`,表明此数据`key`值已经被垃圾回收掉了,此时就会执行`replaceStaleEntry()`方法,该方法含义是**替换过期数据的逻辑**,以**index=7**位起点开始遍历,进行探测式数据清理工作。 + +初始化探测式清理过期数据扫描的开始位置:`slotToExpunge = staleSlot = 7` + +以当前`staleSlot`开始 向前迭代查找,找其他过期的数据,然后更新过期数据起始扫描下标`slotToExpunge`。`for`循环迭代,直到碰到`Entry`为`null`结束。 + +如果找到了过期的数据,继续向前迭代,直到遇到`Entry=null`的槽位才停止迭代,如下图所示,**slotToExpunge 被更新为 0**: + +![](./images/thread-local/13.png) + +以当前节点(`index=7`)向前迭代,检测是否有过期的`Entry`数据,如果有则更新`slotToExpunge`值。碰到`null`则结束探测。以上图为例`slotToExpunge`被更新为 0。 + +上面向前迭代的操作是为了更新探测清理过期数据的起始下标`slotToExpunge`的值,这个值在后面会讲解,它是用来判断当前过期槽位`staleSlot`之前是否还有过期元素。 + +接着开始以`staleSlot`位置(`index=7`)向后迭代,**如果找到了相同 key 值的 Entry 数据:** + +![](./images/thread-local/14.png) + +从当前节点`staleSlot`向后查找`key`值相等的`Entry`元素,找到后更新`Entry`的值并交换`staleSlot`元素的位置(`staleSlot`位置为过期元素),更新`Entry`数据,然后开始进行过期`Entry`的清理工作,如下图所示: + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/view.png)向后遍历过程中,如果没有找到相同 key 值的 Entry 数据: + +![](./images/thread-local/15.png) + +从当前节点`staleSlot`向后查找`key`值相等的`Entry`元素,直到`Entry`为`null`则停止寻找。通过上图可知,此时`table`中没有`key`值相同的`Entry`。 + +创建新的`Entry`,替换`table[stableSlot]`位置: + +![](./images/thread-local/16.png) + +替换完成后也是进行过期元素清理工作,清理工作主要是有两个方法:`expungeStaleEntry()`和`cleanSomeSlots()`,具体细节后面会讲到,请继续往后看。 + +#### `ThreadLocalMap.set()`源码详解 + +上面已经用图的方式解析了`set()`实现的原理,其实已经很清晰了,我们接着再看下源码: + +`java.lang.ThreadLocal`.`ThreadLocalMap.set()`: + +```java +private void set(ThreadLocal key, Object value) { + Entry[] tab = table; + int len = tab.length; + int i = key.threadLocalHashCode & (len-1); + + for (Entry e = tab[i]; + e != null; + e = tab[i = nextIndex(i, len)]) { + ThreadLocal k = e.get(); + + if (k == key) { + e.value = value; + return; + } + + if (k == null) { + replaceStaleEntry(key, value, i); + return; + } + } + + tab[i] = new Entry(key, value); + int sz = ++size; + if (!cleanSomeSlots(i, sz) && sz >= threshold) + rehash(); +} +``` + +这里会通过`key`来计算在散列表中的对应位置,然后以当前`key`对应的桶的位置向后查找,找到可以使用的桶。 + +```java +Entry[] tab = table; +int len = tab.length; +int i = key.threadLocalHashCode & (len-1); +``` + +什么情况下桶才是可以使用的呢? + +1. `k = key` 说明是替换操作,可以使用 +2. 碰到一个过期的桶,执行替换逻辑,占用过期桶 +3. 查找过程中,碰到桶中`Entry=null`的情况,直接使用 + +接着就是执行`for`循环遍历,向后查找,我们先看下`nextIndex()`、`prevIndex()`方法实现: + +![](./images/thread-local/17.png) + +```java +private static int nextIndex(int i, int len) { + return ((i + 1 < len) ? i + 1 : 0); +} + +private static int prevIndex(int i, int len) { + return ((i - 1 >= 0) ? i - 1 : len - 1); +} +``` + +接着看剩下`for`循环中的逻辑: + +1. 遍历当前`key`值对应的桶中`Entry`数据为空,这说明散列数组这里没有数据冲突,跳出`for`循环,直接`set`数据到对应的桶中 +2. 如果`key`值对应的桶中`Entry`数据不为空 + 2.1 如果`k = key`,说明当前`set`操作是一个替换操作,做替换逻辑,直接返回 + 2.2 如果`key = null`,说明当前桶位置的`Entry`是过期数据,执行`replaceStaleEntry()`方法(核心方法),然后返回 +3. `for`循环执行完毕,继续往下执行说明向后迭代的过程中遇到了`entry`为`null`的情况 + 3.1 在`Entry`为`null`的桶中创建一个新的`Entry`对象 + 3.2 执行`++size`操作 +4. 调用`cleanSomeSlots()`做一次启发式清理工作,清理散列数组中`Entry`的`key`过期的数据 + 4.1 如果清理工作完成后,未清理到任何数据,且`size`超过了阈值(数组长度的 2/3),进行`rehash()`操作 + 4.2 `rehash()`中会先进行一轮探测式清理,清理过期`key`,清理完成后如果**size >= threshold - threshold / 4**,就会执行真正的扩容逻辑(扩容逻辑往后看) + +接着重点看下`replaceStaleEntry()`方法,`replaceStaleEntry()`方法提供替换过期数据的功能,我们可以对应上面**第四种情况**的原理图来再回顾下,具体代码如下: + +`java.lang.ThreadLocal.ThreadLocalMap.replaceStaleEntry()`: + +```java +private void replaceStaleEntry(ThreadLocal key, Object value, + int staleSlot) { + Entry[] tab = table; + int len = tab.length; + Entry e; + + int slotToExpunge = staleSlot; + for (int i = prevIndex(staleSlot, len); + (e = tab[i]) != null; + i = prevIndex(i, len)) + + if (e.get() == null) + slotToExpunge = i; + + for (int i = nextIndex(staleSlot, len); + (e = tab[i]) != null; + i = nextIndex(i, len)) { + + ThreadLocal k = e.get(); + + if (k == key) { + e.value = value; + + tab[i] = tab[staleSlot]; + tab[staleSlot] = e; + + if (slotToExpunge == staleSlot) + slotToExpunge = i; + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); + return; + } + + if (k == null && slotToExpunge == staleSlot) + slotToExpunge = i; + } + + tab[staleSlot].value = null; + tab[staleSlot] = new Entry(key, value); + + if (slotToExpunge != staleSlot) + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); +} +``` + +`slotToExpunge`表示开始探测式清理过期数据的开始下标,默认从当前的`staleSlot`开始。以当前的`staleSlot`开始,向前迭代查找,找到没有过期的数据,`for`循环一直碰到`Entry`为`null`才会结束。如果向前找到了过期数据,更新探测清理过期数据的开始下标为 i,即`slotToExpunge=i` + +```java +for (int i = prevIndex(staleSlot, len); + (e = tab[i]) != null; + i = prevIndex(i, len)){ + + if (e.get() == null){ + slotToExpunge = i; + } +} +``` + +接着开始从`staleSlot`向后查找,也是碰到`Entry`为`null`的桶结束。 +如果迭代过程中,**碰到 k == key**,这说明这里是替换逻辑,替换新数据并且交换当前`staleSlot`位置。如果`slotToExpunge == staleSlot`,这说明`replaceStaleEntry()`一开始向前查找过期数据时并未找到过期的`Entry`数据,接着向后查找过程中也未发现过期数据,修改开始探测式清理过期数据的下标为当前循环的 index,即`slotToExpunge = i`。最后调用`cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);`进行启发式过期数据清理。 + +```java +if (k == key) { + e.value = value; + + tab[i] = tab[staleSlot]; + tab[staleSlot] = e; + + if (slotToExpunge == staleSlot) + slotToExpunge = i; + + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); + return; +} +``` + +`cleanSomeSlots()`和`expungeStaleEntry()`方法后面都会细讲,这两个是和清理相关的方法,一个是过期`key`相关`Entry`的启发式清理(`Heuristically scan`),另一个是过期`key`相关`Entry`的探测式清理。 + +**如果 k != key**则会接着往下走,`k == null`说明当前遍历的`Entry`是一个过期数据,`slotToExpunge == staleSlot`说明,一开始的向前查找数据并未找到过期的`Entry`。如果条件成立,则更新`slotToExpunge` 为当前位置,这个前提是前驱节点扫描时未发现过期数据。 + +```java +if (k == null && slotToExpunge == staleSlot) + slotToExpunge = i; +``` + +往后迭代的过程中如果没有找到`k == key`的数据,且碰到`Entry`为`null`的数据,则结束当前的迭代操作。此时说明这里是一个添加的逻辑,将新的数据添加到`table[staleSlot]` 对应的`slot`中。 + +```java +tab[staleSlot].value = null; +tab[staleSlot] = new Entry(key, value); +``` + +最后判断除了`staleSlot`以外,还发现了其他过期的`slot`数据,就要开启清理数据的逻辑: + +```java +if (slotToExpunge != staleSlot) + cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); +``` + +### `ThreadLocalMap`过期 key 的探测式清理流程 + +上面我们有提及`ThreadLocalMap`的两种过期`key`数据清理方式:**探测式清理**和**启发式清理**。 + +我们先讲下探测式清理,也就是`expungeStaleEntry`方法,遍历散列数组,从开始位置向后探测清理过期数据,将过期数据的`Entry`设置为`null`,沿途中碰到未过期的数据则将此数据`rehash`后重新在`table`数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的`Entry=null`的桶中,使`rehash`后的`Entry`数据距离正确的桶的位置更近一些。操作逻辑如下: + +![](./images/thread-local/18.png) + +如上图,`set(27)` 经过 hash 计算后应该落到`index=4`的桶中,由于`index=4`桶已经有了数据,所以往后迭代最终数据放入到`index=7`的桶中,放入后一段时间后`index=5`中的`Entry`数据`key`变为了`null` + +![](./images/thread-local/19.png) + +如果再有其他数据`set`到`map`中,就会触发**探测式清理**操作。 + +如上图,执行**探测式清理**后,`index=5`的数据被清理掉,继续往后迭代,到`index=7`的元素时,经过`rehash`后发现该元素正确的`index=4`,而此位置已经有了数据,往后查找离`index=4`最近的`Entry=null`的节点(刚被探测式清理掉的数据:`index=5`),找到后移动`index= 7`的数据到`index=5`中,此时桶的位置离正确的位置`index=4`更近了。 + +经过一轮探测式清理后,`key`过期的数据会被清理掉,没过期的数据经过`rehash`重定位后所处的桶位置理论上更接近`i= key.hashCode & (tab.len - 1)`的位置。这种优化会提高整个散列表查询性能。 + +接着看下`expungeStaleEntry()`具体流程,我们还是以先原理图后源码讲解的方式来一步步梳理: + +![](./images/thread-local/20.png) + +我们假设`expungeStaleEntry(3)` 来调用此方法,如上图所示,我们可以看到`ThreadLocalMap`中`table`的数据情况,接着执行清理操作: + +![](./images/thread-local/21.png) + +第一步是清空当前`staleSlot`位置的数据,`index=3`位置的`Entry`变成了`null`。然后接着往后探测: + +![](./images/thread-local/22.png) + +执行完第二步后,index=4 的元素挪到 index=3 的槽位中。 + +继续往后迭代检查,碰到正常数据,计算该数据位置是否偏移,如果被偏移,则重新计算`slot`位置,目的是让正常数据尽可能存放在正确位置或离正确位置更近的位置 + +![](./images/thread-local/23.png) + +在往后迭代的过程中碰到空的槽位,终止探测,这样一轮探测式清理工作就完成了,接着我们继续看看具体**实现源代码**: + +```java +private int expungeStaleEntry(int staleSlot) { + Entry[] tab = table; + int len = tab.length; + + tab[staleSlot].value = null; + tab[staleSlot] = null; + size--; + + Entry e; + int i; + for (i = nextIndex(staleSlot, len); + (e = tab[i]) != null; + i = nextIndex(i, len)) { + ThreadLocal k = e.get(); + if (k == null) { + e.value = null; + tab[i] = null; + size--; + } else { + int h = k.threadLocalHashCode & (len - 1); + if (h != i) { + tab[i] = null; + + while (tab[h] != null) + h = nextIndex(h, len); + tab[h] = e; + } + } + } + return i; +} +``` + +这里我们还是以`staleSlot=3` 来做示例说明,首先是将`tab[staleSlot]`槽位的数据清空,然后设置`size--` +接着以`staleSlot`位置往后迭代,如果遇到`k==null`的过期数据,也是清空该槽位数据,然后`size--` + +```java +ThreadLocal k = e.get(); + +if (k == null) { + e.value = null; + tab[i] = null; + size--; +} +``` + +如果`key`没有过期,重新计算当前`key`的下标位置是不是当前槽位下标位置,如果不是,那么说明产生了`hash`冲突,此时以新计算出来正确的槽位位置往后迭代,找到最近一个可以存放`entry`的位置。 + +```java +int h = k.threadLocalHashCode & (len - 1); +if (h != i) { + tab[i] = null; + + while (tab[h] != null) + h = nextIndex(h, len); + + tab[h] = e; +} +``` + +这里是处理正常的产生`Hash`冲突的数据,经过迭代后,有过`Hash`冲突数据的`Entry`位置会更靠近正确位置,这样的话,查询的时候 效率才会更高。 + +### `ThreadLocalMap`扩容机制 + +在`ThreadLocalMap.set()`方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中`Entry`的数量已经达到了列表的扩容阈值`(len*2/3)`,就开始执行`rehash()`逻辑: + +```java +if (!cleanSomeSlots(i, sz) && sz >= threshold) + rehash(); +``` + +接着看下`rehash()`具体实现: + +```java +private void rehash() { + expungeStaleEntries(); + + if (size >= threshold - threshold / 4) + resize(); +} + +private void expungeStaleEntries() { + Entry[] tab = table; + int len = tab.length; + for (int j = 0; j < len; j++) { + Entry e = tab[j]; + if (e != null && e.get() == null) + expungeStaleEntry(j); + } +} +``` + +这里首先是会进行探测式清理工作,从`table`的起始位置往后清理,上面有分析清理的详细流程。清理完成之后,`table`中可能有一些`key`为`null`的`Entry`数据被清理掉,所以此时通过判断`size >= threshold - threshold / 4` 也就是`size >= threshold * 3/4` 来决定是否扩容。 + +我们还记得上面进行`rehash()`的阈值是`size >= threshold`,所以当面试官套路我们`ThreadLocalMap`扩容机制的时候 我们一定要说清楚这两个步骤: + +![](./images/thread-local/24.png) + +接着看看具体的`resize()`方法,为了方便演示,我们以`oldTab.len=8`来举例: + +![](./images/thread-local/25.png) + +扩容后的`tab`的大小为`oldLen * 2`,然后遍历老的散列表,重新计算`hash`位置,然后放到新的`tab`数组中,如果出现`hash`冲突则往后寻找最近的`entry`为`null`的槽位,遍历完成之后,`oldTab`中所有的`entry`数据都已经放入到新的`tab`中了。重新计算`tab`下次扩容的**阈值**,具体代码如下: + +```java +private void resize() { + Entry[] oldTab = table; + int oldLen = oldTab.length; + int newLen = oldLen * 2; + Entry[] newTab = new Entry[newLen]; + int count = 0; + + for (int j = 0; j < oldLen; ++j) { + Entry e = oldTab[j]; + if (e != null) { + ThreadLocal k = e.get(); + if (k == null) { + e.value = null; + } else { + int h = k.threadLocalHashCode & (newLen - 1); + while (newTab[h] != null) + h = nextIndex(h, newLen); + newTab[h] = e; + count++; + } + } + } + + setThreshold(newLen); + size = count; + table = newTab; +} +``` + +### `ThreadLocalMap.get()`详解 + +上面已经看完了`set()`方法的源码,其中包括`set`数据、清理数据、优化数据桶的位置等操作,接着看看`get()`操作的原理。 + +#### `ThreadLocalMap.get()`图解 + +**第一种情况:** 通过查找`key`值计算出散列表中`slot`位置,然后该`slot`位置中的`Entry.key`和查找的`key`一致,则直接返回: + +![](./images/thread-local/26.png) + +**第二种情况:** `slot`位置中的`Entry.key`和要查找的`key`不一致: + +![](./images/thread-local/27.png) + +我们以`get(ThreadLocal1)`为例,通过`hash`计算后,正确的`slot`位置应该是 4,而`index=4`的槽位已经有了数据,且`key`值不等于`ThreadLocal1`,所以需要继续往后迭代查找。 + +迭代到`index=5`的数据时,此时`Entry.key=null`,触发一次探测式数据回收操作,执行`expungeStaleEntry()`方法,执行完后,`index 5,8`的数据都会被回收,而`index 6,7`的数据都会前移,此时继续往后迭代,到`index = 6`的时候即找到了`key`值相等的`Entry`数据,如下图所示: + +![](./images/thread-local/28.png) + +#### `ThreadLocalMap.get()`源码详解 + +`java.lang.ThreadLocal.ThreadLocalMap.getEntry()`: + +```java +private Entry getEntry(ThreadLocal key) { + int i = key.threadLocalHashCode & (table.length - 1); + Entry e = table[i]; + if (e != null && e.get() == key) + return e; + else + return getEntryAfterMiss(key, i, e); +} + +private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { + Entry[] tab = table; + int len = tab.length; + + while (e != null) { + ThreadLocal k = e.get(); + if (k == key) + return e; + if (k == null) + expungeStaleEntry(i); + else + i = nextIndex(i, len); + e = tab[i]; + } + return null; +} +``` + +### `ThreadLocalMap`过期 key 的启发式清理流程 + +上面多次提及到`ThreadLocalMap`过期key的两种清理方式:**探测式清理(expungeStaleEntry())**、**启发式清理(cleanSomeSlots())** + +探测式清理是以当前`Entry` 往后清理,遇到值为`null`则结束清理,属于**线性探测清理**。 + +而启发式清理被作者定义为:**Heuristically scan some cells looking for stale entries**. + +![](./images/thread-local/29.png) + +具体代码如下: + +```java +private boolean cleanSomeSlots(int i, int n) { + boolean removed = false; + Entry[] tab = table; + int len = tab.length; + do { + i = nextIndex(i, len); + Entry e = tab[i]; + if (e != null && e.get() == null) { + n = len; + removed = true; + i = expungeStaleEntry(i); + } + } while ( (n >>>= 1) != 0); + return removed; +} +``` + +### `InheritableThreadLocal` + +我们使用`ThreadLocal`的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的。 + +为了解决这个问题,JDK 中还有一个`InheritableThreadLocal`类,我们来看一个例子: + +```java +public class InheritableThreadLocalDemo { + public static void main(String[] args) { + ThreadLocal ThreadLocal = new ThreadLocal<>(); + ThreadLocal inheritableThreadLocal = new InheritableThreadLocal<>(); + ThreadLocal.set("父类数据:threadLocal"); + inheritableThreadLocal.set("父类数据:inheritableThreadLocal"); + + new Thread(new Runnable() { + @Override + public void run() { + System.out.println("子线程获取父类ThreadLocal数据:" + ThreadLocal.get()); + System.out.println("子线程获取父类inheritableThreadLocal数据:" + inheritableThreadLocal.get()); + } + }).start(); + } +} +``` + +打印结果: + +```java +子线程获取父类ThreadLocal数据:null +子线程获取父类inheritableThreadLocal数据:父类数据:inheritableThreadLocal +``` + +实现原理是子线程是通过在父线程中通过调用`new Thread()`方法来创建子线程,`Thread#init`方法在`Thread`的构造方法中被调用。在`init`方法中拷贝父线程数据到子线程中: + +```java +private void init(ThreadGroup g, Runnable target, String name, + long stackSize, AccessControlContext acc, + boolean inheritThreadLocals) { + if (name == null) { + throw new NullPointerException("name cannot be null"); + } + + if (inheritThreadLocals && parent.inheritableThreadLocals != null) + this.inheritableThreadLocals = + ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); + this.stackSize = stackSize; + tid = nextThreadID(); +} +``` + +但`InheritableThreadLocal`仍然有缺陷,一般我们做异步化处理都是使用的线程池,而`InheritableThreadLocal`是在`new Thread`中的`init()`方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。 + +当然,有问题出现就会有解决问题的方案,阿里巴巴开源了一个`TransmittableThreadLocal`组件就可以解决这个问题,这里就不再延伸,感兴趣的可自行查阅资料。 + +### `ThreadLocal`项目中使用实战 + +#### `ThreadLocal`使用场景 + +我们现在项目中日志记录用的是`ELK+Logstash`,最后在`Kibana`中进行展示和检索。 + +现在都是分布式系统统一对外提供服务,项目间调用的关系可以通过 `traceId` 来关联,但是不同项目之间如何传递 `traceId` 呢? + +这里我们使用 `org.slf4j.MDC` 来实现此功能,内部就是通过 `ThreadLocal` 来实现的,具体实现如下: + +当前端发送请求到**服务 A**时,**服务 A**会生成一个类似`UUID`的`traceId`字符串,将此字符串放入当前线程的`ThreadLocal`中,在调用**服务 B**的时候,将`traceId`写入到请求的`Header`中,**服务 B**在接收请求时会先判断请求的`Header`中是否有`traceId`,如果存在则写入自己线程的`ThreadLocal`中。 + +![](./images/thread-local/30.png) + +图中的`requestId`即为我们各个系统链路关联的`traceId`,系统间互相调用,通过这个`requestId`即可找到对应链路,这里还有会有一些其他场景: + +![](./images/thread-local/31.png) + +针对于这些场景,我们都可以有相应的解决方案,如下所示 + +#### Feign 远程调用解决方案 + +**服务发送请求:** + +```java +@Component +@Slf4j +public class FeignInvokeInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate template) { + String requestId = MDC.get("requestId"); + if (StringUtils.isNotBlank(requestId)) { + template.header("requestId", requestId); + } + } +} +``` + +**服务接收请求:** + +```java +@Slf4j +@Component +public class LogInterceptor extends HandlerInterceptorAdapter { + + @Override + public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) { + MDC.remove("requestId"); + } + + @Override + public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) { + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + String requestId = request.getHeader(BaseConstant.REQUEST_ID_KEY); + if (StringUtils.isBlank(requestId)) { + requestId = UUID.randomUUID().toString().replace("-", ""); + } + MDC.put("requestId", requestId); + return true; + } +} +``` + +#### 线程池异步调用,requestId 传递 + +因为`MDC`是基于`ThreadLocal`去实现的,异步过程中,子线程并没有办法获取到父线程`ThreadLocal`存储的数据,所以这里可以自定义线程池执行器,修改其中的`run()`方法: + +```java +public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { + + @Override + public void execute(Runnable runnable) { + Map context = MDC.getCopyOfContextMap(); + super.execute(() -> run(runnable, context)); + } + + @Override + private void run(Runnable runnable, Map context) { + if (context != null) { + MDC.setContextMap(context); + } + try { + runnable.run(); + } finally { + MDC.remove(); + } + } +} +``` + +#### 使用 MQ 发送消息给第三方系统 + +在 MQ 发送的消息体中自定义属性`requestId`,接收方消费消息后,自己解析`requestId`使用即可。 diff --git "a/docs/java/concurrent/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" "b/docs/java/concurrent/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" new file mode 100644 index 00000000000..d6f3691bfad --- /dev/null +++ "b/docs/java/concurrent/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" @@ -0,0 +1,202 @@ +--- +title: JDK 提供的并发容器总结 +category: Java +tag: + - Java并发 +--- + +JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 + +- **`ConcurrentHashMap`** : 线程安全的 `HashMap` +- **`CopyOnWriteArrayList`** : 线程安全的 `List`,在读多写少的场合性能非常好,远远好于 `Vector`。 +- **`ConcurrentLinkedQueue`** : 高效的并发队列,使用链表实现。可以看做一个线程安全的 `LinkedList`,这是一个非阻塞队列。 +- **`BlockingQueue`** : 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。 +- **`ConcurrentSkipListMap`** : 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。 + +## ConcurrentHashMap + +我们知道 `HashMap` 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 `Collections.synchronizedMap()` 方法来包装我们的 `HashMap`。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。 + +所以就有了 `HashMap` 的线程安全版本—— `ConcurrentHashMap` 的诞生。 + +在 `ConcurrentHashMap` 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。 + +## CopyOnWriteArrayList + +### CopyOnWriteArrayList 简介 + +```java +public class CopyOnWriteArrayList +extends Object +implements List, RandomAccess, Cloneable, Serializable +``` + +在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 `List` 的内部数据,毕竟读取操作是安全的。 + +这和我们之前在多线程章节讲过 `ReentrantReadWriteLock` 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 `CopyOnWriteArrayList` 类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,`CopyOnWriteArrayList` 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。**那它是怎么做的呢?** + +### CopyOnWriteArrayList 是如何做到的? + +`CopyOnWriteArrayList` 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。 + +从 `CopyOnWriteArrayList` 的名字就能看出 `CopyOnWriteArrayList` 是满足 `CopyOnWrite` 的。所谓 `CopyOnWrite` 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。 + +### CopyOnWriteArrayList 读取和写入源码简单分析 + +#### CopyOnWriteArrayList 读取操作的实现 + +读取操作没有任何同步控制和锁操作,理由就是内部数组 `array` 不会发生修改,只会被另外一个 `array` 替换,因此可以保证数据安全。 + +```java + /** The array, accessed only via getArray/setArray. */ + private transient volatile Object[] array; + public E get(int index) { + return get(getArray(), index); + } + @SuppressWarnings("unchecked") + private E get(Object[] a, int index) { + return (E) a[index]; + } + final Object[] getArray() { + return array; + } + +``` + +#### 3.3.2 CopyOnWriteArrayList 写入操作的实现 + +`CopyOnWriteArrayList` 写入操作 `add()`方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。 + +```java + /** + * Appends the specified element to the end of this list. + * + * @param e element to be appended to this list + * @return {@code true} (as specified by {@link Collection#add}) + */ + public boolean add(E e) { + final ReentrantLock lock = this.lock; + lock.lock();//加锁 + try { + Object[] elements = getArray(); + int len = elements.length; + Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组 + newElements[len] = e; + setArray(newElements); + return true; + } finally { + lock.unlock();//释放锁 + } + } +``` + +## ConcurrentLinkedQueue + +Java 提供的线程安全的 `Queue` 可以分为**阻塞队列**和**非阻塞队列**,其中阻塞队列的典型例子是 `BlockingQueue`,非阻塞队列的典型例子是 `ConcurrentLinkedQueue`,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 **阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。** + +从名字可以看出,`ConcurrentLinkedQueue`这个队列使用链表作为其数据结构.`ConcurrentLinkedQueue` 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。 + +`ConcurrentLinkedQueue` 内部代码我们就不分析了,大家知道 `ConcurrentLinkedQueue` 主要使用 CAS 非阻塞算法来实现线程安全就好了。 + +`ConcurrentLinkedQueue` 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 `ConcurrentLinkedQueue` 来替代。 + +## BlockingQueue + +### BlockingQueue 简介 + +上面我们己经提到了 `ConcurrentLinkedQueue` 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——`BlockingQueue`。阻塞队列(`BlockingQueue`)被广泛使用在“生产者-消费者”问题中,其原因是 `BlockingQueue` 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。 + +`BlockingQueue` 是一个接口,继承自 `Queue`,所以其实现类也可以作为 `Queue` 的实现来使用,而 `Queue` 又继承自 `Collection` 接口。下面是 `BlockingQueue` 的相关实现类: + +![BlockingQueue 的实现类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/51622268.jpg) + +下面主要介绍一下 3 个常见的 `BlockingQueue` 的实现类:`ArrayBlockingQueue`、`LinkedBlockingQueue` 、`PriorityBlockingQueue` 。 + +### ArrayBlockingQueue + +`ArrayBlockingQueue` 是 `BlockingQueue` 接口的有界队列实现类,底层采用数组来实现。 + +```java +public class ArrayBlockingQueue +extends AbstractQueue +implements BlockingQueue, Serializable{} +``` + +`ArrayBlockingQueue` 一旦创建,容量不能改变。其并发控制采用可重入锁 `ReentrantLock` ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。 + +`ArrayBlockingQueue` 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 `ArrayBlockingQueue`。而非公平性则是指访问 `ArrayBlockingQueue` 的顺序不是遵守严格的时间顺序,有可能存在,当 `ArrayBlockingQueue` 可以被访问时,长时间阻塞的线程依然无法访问到 `ArrayBlockingQueue`。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 `ArrayBlockingQueue`,可采用如下代码: + +```java +private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10,true); +``` + +### LinkedBlockingQueue + +`LinkedBlockingQueue` 底层基于**单向链表**实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 `ArrayBlockingQueue` 相比起来具有更高的吞吐量,为了防止 `LinkedBlockingQueue` 容量迅速增,损耗大量内存。通常在创建 `LinkedBlockingQueue` 对象时,会指定其大小,如果未指定,容量等于 `Integer.MAX_VALUE` 。 + +**相关构造方法:** + +```java + /** + *某种意义上的无界队列 + * Creates a {@code LinkedBlockingQueue} with a capacity of + * {@link Integer#MAX_VALUE}. + */ + public LinkedBlockingQueue() { + this(Integer.MAX_VALUE); + } + + /** + *有界队列 + * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. + * + * @param capacity the capacity of this queue + * @throws IllegalArgumentException if {@code capacity} is not greater + * than zero + */ + public LinkedBlockingQueue(int capacity) { + if (capacity <= 0) throw new IllegalArgumentException(); + this.capacity = capacity; + last = head = new Node(null); + } +``` + +### PriorityBlockingQueue + +`PriorityBlockingQueue` 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 `compareTo()` 方法来指定元素排序规则,或者初始化时通过构造器参数 `Comparator` 来指定排序规则。 + +`PriorityBlockingQueue` 并发控制采用的是可重入锁 `ReentrantLock`,队列为无界队列(`ArrayBlockingQueue` 是有界队列,`LinkedBlockingQueue` 也可以通过在构造函数中传入 `capacity` 指定队列最大的容量,但是 `PriorityBlockingQueue` 只能指定初始的队列大小,后面插入元素的时候,**如果空间不够的话会自动扩容**)。 + +简单地说,它就是 `PriorityQueue` 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 `ClassCastException` 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。 + +**推荐文章:** [《解读 Java 并发队列 BlockingQueue》](https://javadoop.com/post/java-concurrent-queue) + +## ConcurrentSkipListMap + +下面这部分内容参考了极客时间专栏[《数据结构与算法之美》](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster "《数据结构与算法之美》")以及《实战 Java 高并发程序设计》。 + +为了引出 `ConcurrentSkipListMap`,先带着大家简单理解一下跳表。 + +对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 **O(logn)** 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。 + +跳表的本质是同时维护了多个链表,并且链表是分层的, + +![2级索引跳表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/93666217.jpg) + +最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。 + +跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。 + +![在跳表中查找元素18](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/32005738.jpg) + +查找 18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。 + +从上面很容易看出,**跳表是一种利用空间换时间的算法。** + +使用跳表实现 `Map` 和使用哈希算法实现 `Map` 的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是 `ConcurrentSkipListMap`。 + +## 参考 + +- 《实战 Java 高并发程序设计》 +- https://javadoop.com/post/java-concurrent-queue +- https://juejin.im/post/5aeebd02518825672f19c546 diff --git "a/docs/java/concurrent/\346\213\277\346\235\245\345\215\263\347\224\250\347\232\204java\347\272\277\347\250\213\346\261\240\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/docs/java/concurrent/\346\213\277\346\235\245\345\215\263\347\224\250\347\232\204java\347\272\277\347\250\213\346\261\240\346\234\200\344\275\263\345\256\236\350\267\265.md" new file mode 100644 index 00000000000..45034bf3392 --- /dev/null +++ "b/docs/java/concurrent/\346\213\277\346\235\245\345\215\263\347\224\250\347\232\204java\347\272\277\347\250\213\346\261\240\346\234\200\344\275\263\345\256\236\350\267\265.md" @@ -0,0 +1,308 @@ +--- +title: 拿来即用的Java线程池最佳实践 +category: Java +tag: + - Java并发 +--- + +这篇文章篇幅虽短,但是绝对是干货。标题稍微有点夸张,嘿嘿,实际都是自己使用线程池的时候总结的一些个人感觉比较重要的点。 + +## 线程池知识回顾 + +开始这篇文章之前还是简单介绍一嘴线程池,之前写的[《新手也能看懂的线程池学习总结》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485808&idx=1&sn=1013253533d73450cef673aee13267ab&chksm=cea246bbf9d5cfad1c21316340a0ef1609a7457fea4113a1f8d69e8c91e7d9cd6285f5ee1490&token=510053261&lang=zh_CN&scene=21#wechat_redirect)这篇文章介绍的很详细了。 + +### 为什么要使用线程池? + +> **池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** + +**线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 + +这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: + +- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 +- **提高响应速度**。当任务到达时,任务可以不需要等到线程创建就能立即执行。 +- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +### 线程池在实际项目的使用场景 + +**线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池的话可让多个不相关联的任务同时执行。** + +假设我们要执行三个不相关的耗时任务,Guide 画图给大家展示了使用线程池前后的区别。 + +注意:**下面三个任务可能做的是同一件事情,也可能是不一样的事情。** + +![使用线程池前后对比](./images/thread-pool/1bc44c67-26ba-42ab-bcb8-4e29e6fd99b9.png) + +### 如何使用线程池? + +一般是通过 `ThreadPoolExecutor` 的构造函数来创建线程池,然后提交任务给线程池执行就可以了。 + + `ThreadPoolExecutor`构造函数如下: + +```java + /** + * 用给定的初始参数创建一个新的ThreadPoolExecutor。 + */ + public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 + int maximumPoolSize,//线程池的最大线程数 + long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 + TimeUnit unit,//时间单位 + BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列 + ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可 + RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 + ) { + if (corePoolSize < 0 || + maximumPoolSize <= 0 || + maximumPoolSize < corePoolSize || + keepAliveTime < 0) + throw new IllegalArgumentException(); + if (workQueue == null || threadFactory == null || handler == null) + throw new NullPointerException(); + this.corePoolSize = corePoolSize; + this.maximumPoolSize = maximumPoolSize; + this.workQueue = workQueue; + this.keepAliveTime = unit.toNanos(keepAliveTime); + this.threadFactory = threadFactory; + this.handler = handler; + } +``` + +简单演示一下如何使用线程池,更详细的介绍,请看:[《新手也能看懂的线程池学习总结》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485808&idx=1&sn=1013253533d73450cef673aee13267ab&chksm=cea246bbf9d5cfad1c21316340a0ef1609a7457fea4113a1f8d69e8c91e7d9cd6285f5ee1490&token=510053261&lang=zh_CN&scene=21#wechat_redirect) 。 + +```java + private static final int CORE_POOL_SIZE = 5; + private static final int MAX_POOL_SIZE = 10; + private static final int QUEUE_CAPACITY = 100; + private static final Long KEEP_ALIVE_TIME = 1L; + + public static void main(String[] args) { + + //使用阿里巴巴推荐的创建线程池的方式 + //通过ThreadPoolExecutor构造函数自定义参数创建 + ThreadPoolExecutor executor = new ThreadPoolExecutor( + CORE_POOL_SIZE, + MAX_POOL_SIZE, + KEEP_ALIVE_TIME, + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(QUEUE_CAPACITY), + new ThreadPoolExecutor.CallerRunsPolicy()); + + for (int i = 0; i < 10; i++) { + executor.execute(() -> { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("CurrentThread name:" + Thread.currentThread().getName() + "date:" + Instant.now()); + }); + } + //终止线程池 + executor.shutdown(); + try { + executor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("Finished all threads"); + } +``` + +控制台输出: + +```java +CurrentThread name:pool-1-thread-5date:2020-06-06T11:45:31.639Z +CurrentThread name:pool-1-thread-3date:2020-06-06T11:45:31.639Z +CurrentThread name:pool-1-thread-1date:2020-06-06T11:45:31.636Z +CurrentThread name:pool-1-thread-4date:2020-06-06T11:45:31.639Z +CurrentThread name:pool-1-thread-2date:2020-06-06T11:45:31.639Z +CurrentThread name:pool-1-thread-2date:2020-06-06T11:45:33.656Z +CurrentThread name:pool-1-thread-4date:2020-06-06T11:45:33.656Z +CurrentThread name:pool-1-thread-1date:2020-06-06T11:45:33.656Z +CurrentThread name:pool-1-thread-3date:2020-06-06T11:45:33.656Z +CurrentThread name:pool-1-thread-5date:2020-06-06T11:45:33.656Z +Finished all threads +``` + +## 线程池最佳实践 + +简单总结一下我了解的使用线程池的时候应该注意的东西,网上似乎还没有专门写这方面的文章。 + +因为Guide还比较菜,有补充和完善的地方,可以在评论区告知或者在微信上与我交流。 + +### 1. 使用 `ThreadPoolExecutor ` 的构造函数声明线程池 + +**1. 线程池必须手动通过 `ThreadPoolExecutor ` 的构造函数来声明,避免使用`Executors ` 类的 `newFixedThreadPool` 和 `newCachedThreadPool` ,因为可能会有 OOM 的风险。** + +> Executors 返回线程池对象的弊端如下: +> +> - **`FixedThreadPool` 和 `SingleThreadExecutor`** : 允许请求的队列长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。 +> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。 + +说白了就是:**使用有界队列,控制线程创建数量。** + +除了避免 OOM 的原因之外,不推荐使用 `Executors `提供的两种快捷的线程池的原因还有: + +1. 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。 +2. 我们应该显示地给我们的线程池命名,这样有助于我们定位问题。 + +### 2.监测线程池运行状态 + +你可以通过一些手段来检测线程池的运行状态比如 SpringBoot 中的 Actuator 组件。 + +除此之外,我们还可以利用 `ThreadPoolExecutor` 的相关 API做一个简陋的监控。从下图可以看出, `ThreadPoolExecutor`提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。 + +![](./images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png) + +下面是一个简单的 Demo。`printThreadPoolStatus()`会每隔一秒打印出线程池的线程数、活跃线程数、完成的任务数、以及队列中的任务数。 + +```java + /** + * 打印线程池的状态 + * + * @param threadPool 线程池对象 + */ + public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) { + ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-images/thread-pool-status", false)); + scheduledExecutorService.scheduleAtFixedRate(() -> { + log.info("========================="); + log.info("ThreadPool Size: [{}]", threadPool.getPoolSize()); + log.info("Active Threads: {}", threadPool.getActiveCount()); + log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount()); + log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size()); + log.info("========================="); + }, 0, 1, TimeUnit.SECONDS); + } +``` + +### 3.建议不同类别的业务用不同的线程池 + +很多人在实际项目中都会有类似这样的问题:**我的项目中多个业务需要用到线程池,是为每个线程池都定义一个还是说定义一个公共的线程池呢?** + +一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务。 + +**我们再来看一个真实的事故案例!** (本案例来源自:[《线程池运用不当的一次线上事故》](https://club.perfma.com/article/646639) ,很精彩的一个案例) + +![案例代码概览](./images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png) + +上面的代码可能会存在死锁的情况,为什么呢?画个图给大家捋一捋。 + +试想这样一种极端情况:假如我们线程池的核心线程数为 **n**,父任务(扣费任务)数量为 **n**,父任务下面有两个子任务(扣费任务下的子任务),其中一个已经执行完成,另外一个被放在了任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取到线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程池资源,这也就造成了 **"死锁"**。 + +![线程池使用不当导致死锁](./images/thread-pool/线程池使用不当导致死锁.png) + +解决方法也很简单,就是新增加一个用于执行子任务的线程池专门为其服务。 + +### 4.别忘记给线程池命名 + +初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题。 + +默认情况下创建的线程名字类似 pool-1-thread-n 这样的,没有业务含义,不利于我们定位问题。 + +给线程池里的线程命名通常有下面两种方式: + +**1.利用 guava 的 `ThreadFactoryBuilder` ** + +```java +ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(threadNamePrefix + "-%d") + .setDaemon(true).build(); +ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory) +``` + +**2.自己实现 `ThreadFactor`。** + +```java +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; +/** + * 线程工厂,它设置线程名称,有利于我们定位问题。 + */ +public final class NamingThreadFactory implements ThreadFactory { + + private final AtomicInteger threadNum = new AtomicInteger(); + private final ThreadFactory delegate; + private final String name; + + /** + * 创建一个带名字的线程池生产工厂 + */ + public NamingThreadFactory(ThreadFactory delegate, String name) { + this.delegate = delegate; + this.name = name; // TODO consider uniquifying this + } + + @Override + public Thread newThread(Runnable r) { + Thread t = delegate.newThread(r); + t.setName(name + " [#" + threadNum.incrementAndGet() + "]"); + return t; + } + +} +``` + +### 5.正确配置线程池参数 + +说到如何给线程池配置参数,美团的骚操作至今让我难忘(后面会提到)! + +我们先来看一下各种书籍和博客上一般推荐的配置线程池参数的方式,可以作为参考! + +#### 常规操作 + +很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:**并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。** 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了**上下文切换**成本。不清楚什么是上下文切换的话,可以看我下面的介绍。 + +> 上下文切换: +> +> 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 +> +> 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 +> +> Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 + +**类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。** + +**如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。** + +**但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。** + +有一个简单并且适用面比较广的公式: + +- **CPU 密集型任务(N+1):** 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 +- **I/O 密集型任务(2N):** 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。 + +**如何判断是 CPU 密集任务还是 IO 密集任务?** + +CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。 + +#### 美团的骚操作 + +美团技术团队在[《Java线程池实现原理及其在美团业务中的实践》](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。 + +美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是: + +- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 +- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 +- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 + +**为什么是这三个参数?** + +我在这篇[《新手也能看懂的线程池学习总结》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485808&idx=1&sn=1013253533d73450cef673aee13267ab&chksm=cea246bbf9d5cfad1c21316340a0ef1609a7457fea4113a1f8d69e8c91e7d9cd6285f5ee1490&token=510053261&lang=zh_CN&scene=21#wechat_redirect) 中就说过这三个参数是 `ThreadPoolExecutor` 最重要的参数,它们基本决定了线程池对于任务的处理策略。 + +**如何支持参数动态配置?** 且看 `ThreadPoolExecutor` 提供的下面这些方法。 + +![](./images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png) + +格外需要注意的是`corePoolSize`, 程序运行期间的时候,我们调用 `setCorePoolSize() `这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。 + +另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 `ResizableCapacityLinkedBlockIngQueue` 的队列(主要就是把`LinkedBlockingQueue`的capacity 字段的final关键字修饰给去掉了,让它变为可变的)。 + +最终实现的可动态修改线程池参数效果如下。👏👏👏 + +![动态配置线程池参数最终效果](./images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png) + +还没看够?推荐 why神的[《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》](https://mp.weixin.qq.com/s/9HLuPcoWmTqAeFKa1kj-_A)这篇文章,深度剖析,很不错哦! + + + diff --git a/docs/java/images/exception-architechture-java.png b/docs/java/images/exception-architechture-java.png deleted file mode 100644 index 9a9e3afdc59..00000000000 Binary files a/docs/java/images/exception-architechture-java.png and /dev/null differ diff --git a/docs/java/images/java-exception-handling-class-hierarchy-diagram.jpg b/docs/java/images/java-exception-handling-class-hierarchy-diagram.jpg deleted file mode 100644 index 6b5b7454c0a..00000000000 Binary files a/docs/java/images/java-exception-handling-class-hierarchy-diagram.jpg and /dev/null differ diff --git a/docs/java/images/performance-tuning/java-performance1.png b/docs/java/images/performance-tuning/java-performance1.png deleted file mode 100644 index 0975d26519e..00000000000 Binary files a/docs/java/images/performance-tuning/java-performance1.png and /dev/null differ diff --git a/docs/java/images/performance-tuning/java-performance2.png b/docs/java/images/performance-tuning/java-performance2.png deleted file mode 100644 index 76bbc0a82f1..00000000000 Binary files a/docs/java/images/performance-tuning/java-performance2.png and /dev/null differ diff --git a/docs/java/images/performance-tuning/java-performance3.png b/docs/java/images/performance-tuning/java-performance3.png deleted file mode 100644 index 150e5ce5d5a..00000000000 Binary files a/docs/java/images/performance-tuning/java-performance3.png and /dev/null differ diff --git a/docs/java/images/performance-tuning/java-performance4.png b/docs/java/images/performance-tuning/java-performance4.png deleted file mode 100644 index e3bf7450592..00000000000 Binary files a/docs/java/images/performance-tuning/java-performance4.png and /dev/null differ diff --git a/docs/java/images/performance-tuning/java-performance5.png b/docs/java/images/performance-tuning/java-performance5.png deleted file mode 100644 index c6b44840ff4..00000000000 Binary files a/docs/java/images/performance-tuning/java-performance5.png and /dev/null differ diff --git a/docs/java/images/performance-tuning/java-performance6.png b/docs/java/images/performance-tuning/java-performance6.png deleted file mode 100644 index b1e182ed89c..00000000000 Binary files a/docs/java/images/performance-tuning/java-performance6.png and /dev/null differ diff --git a/docs/java/images/performance-tuning/java-performance7.png b/docs/java/images/performance-tuning/java-performance7.png deleted file mode 100644 index 0796e30d239..00000000000 Binary files a/docs/java/images/performance-tuning/java-performance7.png and /dev/null differ diff --git a/docs/java/images/performance-tuning/java-performance8.png b/docs/java/images/performance-tuning/java-performance8.png deleted file mode 100644 index 75172dfad5f..00000000000 Binary files a/docs/java/images/performance-tuning/java-performance8.png and /dev/null differ diff --git a/docs/java/java-naming-conventions.md b/docs/java/java-naming-conventions.md deleted file mode 100644 index d205f5f5847..00000000000 --- a/docs/java/java-naming-conventions.md +++ /dev/null @@ -1,416 +0,0 @@ -> 原文链接:https://www.cnblogs.com/liqiangchn/p/12000361.html - -简洁清爽的代码风格应该是大多数工程师所期待的。在工作中笔者常常因为起名字而纠结,夸张点可以说是编程5分钟,命名两小时!究竟为什么命名成为了工作中的拦路虎。 - -每个公司都有不同的标准,目的是为了保持统一,减少沟通成本,提升团队研发效能。所以本文中是笔者结合阿里巴巴开发规范,以及工作中的见闻针对Java领域相关命名进行整理和总结,仅供参考。 - -## 一,Java中的命名规范 - -好的命名能体现出代码的特征,含义或者是用途,让阅读者可以根据名称的含义快速厘清程序的脉络。不同语言中采用的命名形式大相径庭,Java中常用到的命名形式共有三种,既首字母大写的UpperCamelCase,首字母小写的lowerCamelCase以及全部大写的并用下划线分割单词的UPPER_CAMEL_UNSER_SCORE。通常约定,**类一般采用大驼峰命名,方法和局部变量使用小驼峰命名,而大写下划线命名通常是常量和枚举中使用。** - -| 类型 | 约束 | 例 | -| :----: | :----------------------------------------------------------: | :--------------------------------------------: | -| 项目名 | 全部小写,多个单词用中划线分隔‘-’ | spring-cloud | -| 包名 | 全部小写 | com.alibaba.fastjson | -| 类名 | 单词首字母大写 | Feature, ParserConfig,DefaultFieldDeserializer | -| 变量名 | 首字母小写,多个单词组成时,除首个单词,其他单词首字母都要大写 | password, userName | -| 常量名 | 全部大写,多个单词,用'_'分隔 | CACHE_EXPIRED_TIME | -| 方法 | 同变量 | read(), readObject(), getById() | - -## 二,包命名 - -**包名**统一使用**小写**,**点分隔符**之间有且仅有一个自然语义的英文单词或者多个单词自然连接到一块(如 springframework,deepspace不需要使用任何分割)。包名统一使用单数形式,如果类命有复数含义,则可以使用复数形式。 - -包名的构成可以分为以下几四部分【前缀】 【发起者名】【项目名】【模块名】。常见的前缀可以分为以下几种: - -| 前缀名 | 例 | 含义 | -| :-------------: | :----------------------------: | :----------------------------------------------------------: | -| indi(或onem ) | indi.发起者名.项目名.模块名.…… | 个体项目,指个人发起,但非自己独自完成的项目,可公开或私有项目,copyright主要属于发起者。 | -| pers | pers.个人名.项目名.模块名.…… | 个人项目,指个人发起,独自完成,可分享的项目,copyright主要属于个人 | -| priv | priv.个人名.项目名.模块名.…… | 私有项目,指个人发起,独自完成,非公开的私人使用的项目,copyright属于个人。 | -| team | team.团队名.项目名.模块名.…… | 团队项目,指由团队发起,并由该团队开发的项目,copyright属于该团队所有 | -| 顶级域名 | com.公司名.项目名.模块名.…… | 公司项目,copyright由项目发起的公司所有 | - -## 三,类命名 - -**类名使用大驼峰命名形式**,类命通常时**名词或名词短语**,接口名除了用名词和名词短语以外,还可以使用形容词或形容词短语,如Cloneable,Callable等,表示实现该接口的类有某种功能或能力。对于测试类则以它要测试的类开头,以Test结尾,如HashMapTest。 - -对于一些特殊特有名词缩写也可以使用全大写命名,比如XMLHttpRequest,不过笔者认为缩写三个字母以内都大写,超过三个字母则按照要给单词算。这个没有标准如阿里巴巴中fastjson用JSONObject作为类命,而google则使用JsonObjectRequest命名,对于这种特殊的缩写,原则是统一就好。 - -| 属性 | 约束 | 例 | -| -------------- | ----------------------------------------- | ------------------------------------------------------------ | -| 抽象类 | Abstract 或者 Base 开头 | BaseUserService | -| 枚举类 | Enum 作为后缀 | GenderEnum | -| 工具类 | Utils作为后缀 | StringUtils | -| 异常类 | Exception结尾 | RuntimeException | -| 接口实现类 | 接口名+ Impl | UserServiceImpl | -| 领域模型相关 | /DO/DTO/VO/DAO | 正例:UserDAO 反例: UserDo, UserDao | -| 设计模式相关类 | Builder,Factory等 | 当使用到设计模式时,需要使用对应的设计模式作为后缀,如ThreadFactory | -| 处理特定功能的 | Handler,Predicate, Validator | 表示处理器,校验器,断言,这些类工厂还有配套的方法名如handle,predicate,validate | -| 测试类 | Test结尾 | UserServiceTest, 表示用来测试UserService类的 | -| MVC分层 | Controller,Service,ServiceImpl,DAO后缀 | UserManageController,UserManageDAO | - -## 四,方法 - -**方法命名采用小驼峰的形式**,首字小写,往后的每个单词首字母都要大写。 和类名不同的是,方法命名一般为**动词或动词短语**,与参数或参数名共同组成动宾短语,即动词 + 名词。一个好的函数名一般能通过名字直接获知该函数实现什么样的功能。 - -### 4.1 返回真伪值的方法 - -注:Prefix-前缀,Suffix-后缀,Alone-单独使用 - -| 位置 | 单词 | 意义 | 例 | -| ------ | ------ | ------------------------------------------------------------ | ------------- | -| Prefix | is | 对象是否符合期待的状态 | isValid | -| Prefix | can | 对象**能否执行**所期待的动作 | canRemove | -| Prefix | should | 调用方执行某个命令或方法是**好还是不好**,**应不应该**,或者说**推荐还是不推荐** | shouldMigrate | -| Prefix | has | 对象**是否持有**所期待的数据和属性 | hasObservers | -| Prefix | needs | 调用方**是否需要**执行某个命令或方法 | needsMigrate | - -### 4.2 用来检查的方法 - -| 单词 | 意义 | 例 | -| -------- | ---------------------------------------------------- | -------------- | -| ensure | 检查是否为期待的状态,不是则抛出异常或返回error code | ensureCapacity | -| validate | 检查是否为正确的状态,不是则抛出异常或返回error code | validateInputs | - -### 4.3 按需求才执行的方法 - -| 位置 | 单词 | 意义 | 例 | -| ------ | --------- | ----------------------------------------- | ---------------------- | -| Suffix | IfNeeded | 需要的时候执行,不需要的时候什么都不做 | drawIfNeeded | -| Prefix | might | 同上 | mightCreate | -| Prefix | try | 尝试执行,失败时抛出异常或是返回errorcode | tryCreate | -| Suffix | OrDefault | 尝试执行,失败时返回默认值 | getOrDefault | -| Suffix | OrElse | 尝试执行、失败时返回实际参数中指定的值 | getOrElse | -| Prefix | force | 强制尝试执行。error抛出异常或是返回值 | forceCreate, forceStop | - -### 4.4 异步相关方法 - -| 位置 | 单词 | 意义 | 例 | -| --------------- | ------------ | -------------------------------------------- | --------------------- | -| Prefix | blocking | 线程阻塞方法 | blockingGetUser | -| Suffix | InBackground | 执行在后台的线程 | doInBackground | -| Suffix | Async | 异步方法 | sendAsync | -| Suffix | Sync | 对应已有异步方法的同步方法 | sendSync | -| Prefix or Alone | schedule | Job和Task放入队列 | schedule, scheduleJob | -| Prefix or Alone | post | 同上 | postJob | -| Prefix or Alone | execute | 执行异步方法(注:我一般拿这个做同步方法名) | execute, executeTask | -| Prefix or Alone | start | 同上 | start, startJob | -| Prefix or Alone | cancel | 停止异步方法 | cancel, cancelJob | -| Prefix or Alone | stop | 同上 | stop, stopJob | - -### 4.5 回调方法 - -| 位置 | 单词 | 意义 | 例 | -| ------ | ------ | -------------------------- | ------------ | -| Prefix | on | 事件发生时执行 | onCompleted | -| Prefix | before | 事件发生前执行 | beforeUpdate | -| Prefix | pre | 同上 | preUpdate | -| Prefix | will | 同上 | willUpdate | -| Prefix | after | 事件发生后执行 | afterUpdate | -| Prefix | post | 同上 | postUpdate | -| Prefix | did | 同上 | didUpdate | -| Prefix | should | 确认事件是否可以发生时执行 | shouldUpdate | - -### 4.6 操作对象生命周期的方法 - -| 单词 | 意义 | 例 | -| ---------- | ------------------------------ | --------------- | -| initialize | 初始化。也可作为延迟初始化使用 | initialize | -| pause | 暂停 | onPause ,pause | -| stop | 停止 | onStop,stop | -| abandon | 销毁的替代 | abandon | -| destroy | 同上 | destroy | -| dispose | 同上 | dispose | - -### 4.7 与集合操作相关的方法 - -| 单词 | 意义 | 例 | -| -------- | ---------------------------- | ---------- | -| contains | 是否持有与指定对象相同的对象 | contains | -| add | 添加 | addJob | -| append | 添加 | appendJob | -| insert | 插入到下标n | insertJob | -| put | 添加与key对应的元素 | putJob | -| remove | 移除元素 | removeJob | -| enqueue | 添加到队列的最末位 | enqueueJob | -| dequeue | 从队列中头部取出并移除 | dequeueJob | -| push | 添加到栈头 | pushJob | -| pop | 从栈头取出并移除 | popJob | -| peek | 从栈头取出但不移除 | peekJob | -| find | 寻找符合条件的某物 | findById | - -### 4.8 与数据相关的方法 - -| 单词 | 意义 | 例 | -| ------ | -------------------------------------- | ------------- | -| create | 新创建 | createAccount | -| new | 新创建 | newAccount | -| from | 从既有的某物新建,或是从其他的数据新建 | fromConfig | -| to | 转换 | toString | -| update | 更新既有某物 | updateAccount | -| load | 读取 | loadAccount | -| fetch | 远程读取 | fetchAccount | -| delete | 删除 | deleteAccount | -| remove | 删除 | removeAccount | -| save | 保存 | saveAccount | -| store | 保存 | storeAccount | -| commit | 保存 | commitChange | -| apply | 保存或应用 | applyChange | -| clear | 清除数据或是恢复到初始状态 | clearAll | -| reset | 清除数据或是恢复到初始状态 | resetAll | - -### 4.9 成对出现的动词 - -| 单词 | 意义 | -| -------------- | ----------------- | -| get获取 | set 设置 | -| add 增加 | remove 删除 | -| create 创建 | destory 移除 | -| start 启动 | stop 停止 | -| open 打开 | close 关闭 | -| read 读取 | write 写入 | -| load 载入 | save 保存 | -| create 创建 | destroy 销毁 | -| begin 开始 | end 结束 | -| backup 备份 | restore 恢复 | -| import 导入 | export 导出 | -| split 分割 | merge 合并 | -| inject 注入 | extract 提取 | -| attach 附着 | detach 脱离 | -| bind 绑定 | separate 分离 | -| view 查看 | browse 浏览 | -| edit 编辑 | modify 修改 | -| select 选取 | mark 标记 | -| copy 复制 | paste 粘贴 | -| undo 撤销 | redo 重做 | -| insert 插入 | delete 移除 | -| add 加入 | append 添加 | -| clean 清理 | clear 清除 | -| index 索引 | sort 排序 | -| find 查找 | search 搜索 | -| increase 增加 | decrease 减少 | -| play 播放 | pause 暂停 | -| launch 启动 | run 运行 | -| compile 编译 | execute 执行 | -| debug 调试 | trace 跟踪 | -| observe 观察 | listen 监听 | -| build 构建 | publish 发布 | -| input 输入 | output 输出 | -| encode 编码 | decode 解码 | -| encrypt 加密 | decrypt 解密 | -| compress 压缩 | decompress 解压缩 | -| pack 打包 | unpack 解包 | -| parse 解析 | emit 生成 | -| connect 连接 | disconnect 断开 | -| send 发送 | receive 接收 | -| download 下载 | upload 上传 | -| refresh 刷新 | synchronize 同步 | -| update 更新 | revert 复原 | -| lock 锁定 | unlock 解锁 | -| check out 签出 | check in 签入 | -| submit 提交 | commit 交付 | -| push 推 | pull 拉 | -| expand 展开 | collapse 折叠 | -| begin 起始 | end 结束 | -| start 开始 | finish 完成 | -| enter 进入 | exit 退出 | -| abort 放弃 | quit 离开 | -| obsolete 废弃 | depreciate 废旧 | -| collect 收集 | aggregate 聚集 | - -## 五,变量&常量命名 - -### 5.1 变量命名 - -变量是指在程序运行中可以改变其值的量,包括成员变量和局部变量。变量名由多单词组成时,第一个单词的首字母小写,其后单词的首字母大写,俗称骆驼式命名法(也称驼峰命名法),如 computedValues,index、变量命名时,尽量简短且能清楚的表达变量的作用,命名体现具体的业务含义即可。 - -变量名不应以下划线或美元符号开头,尽管这在语法上是允许的。变量名应简短且富于描述。变量名的选用应该易于记忆,即,能够指出其用途。尽量避免单个字符的变量名,除非是一次性的临时变量。pojo中的布尔变量,都不要加is(数据库中的布尔字段全都要加 is_ 前缀)。 - -### 5.2 常量命名 - -常量命名CONSTANT_CASE,一般采用全部大写(作为方法参数时除外),单词间用下划线分割。那么什么是常量呢? - -常量是在作用域内保持不变的值,一般使用final进行修饰。一般分为三种,全局常量(public static final修饰),类内常量(private static final 修饰)以及局部常量(方法内,或者参数中的常量),局部常量比较特殊,通常采用小驼峰命名即可。 - -```java -/** - * 一个demo - * - * @author Jann Lee - * @date 2019-12-07 00:25 - **/ -public class HelloWorld { - - /** - * 局部常量(正例) - */ - public static final long USER_MESSAGE_CACHE_EXPIRE_TIME = 3600; - - /** - * 局部常量(反例,命名不清晰) - */ - public static final long MESSAGE_CACHE_TIME = 3600; - - /** - * 全局常量 - */ - private static final String ERROR_MESSAGE = " error message"; - - /** - * 成员变量 - */ - private int currentUserId; - - /** - * 控制台打印 {@code message} 信息 - * - * @param message 消息体,局部常量 - */ - public void sayHello(final String message){ - System.out.println("Hello world!"); - } - -} -``` - -常量一般都有自己的业务含义,**不要害怕长度过长而进行省略或者缩写**。如,用户消息缓存过期时间的表示,那种方式更佳清晰,交给你来评判。 - -## 通用命名规则[#](https://www.cnblogs.com/liqiangchn/p/12000361.html#450918152) - -1. 尽量不要使用拼音;杜绝拼音和英文混用。对于一些通用的表示或者难以用英文描述的可以采用拼音,一旦采用拼音就坚决不能和英文混用。 - 正例: BeiJing, HangZhou - 反例: validateCanShu -2. 命名过程中尽量不要出现特殊的字符,常量除外。 -3. 尽量不要和jdk或者框架中已存在的类重名,也不能使用java中的关键字命名。 -4. 妙用介词,如for(可以用同音的4代替), to(可用同音的2代替), from, with,of等。 - 如类名采用User4RedisDO,方法名getUserInfoFromRedis,convertJson2Map等。 - -## 六,代码注解 - -### 6.1 注解的原则 - -好的命名增加代码阅读性,代码的命名往往有严格的限制。而注解不同,程序员往往可以自由发挥,单并不意味着可以为所欲为之胡作非为。优雅的注解通常要满足三要素。 - -1. Nothing is strange - 没有注解的代码对于阅读者非常不友好,哪怕代码写的在清除,阅读者至少从心理上会有抵触,更何况代码中往往有许多复杂的逻辑,所以一定要写注解,不仅要记录代码的逻辑,还有说清楚修改的逻辑。 -2. Less is more - 从代码维护角度来讲,代码中的注解一定是精华中的精华。合理清晰的命名能让代码易于理解,对于逻辑简单且命名规范,能够清楚表达代码功能的代码不需要注解。滥用注解会增加额外的负担,更何况大部分都是废话。 - -```java -// 根据id获取信息【废话注解】 -getMessageById(id) -``` - -1. Advance with the time - 注解应该随着代码的变动而改变,注解表达的信息要与代码中完全一致。通常情况下修改代码后一定要修改注解。 - -### 6.2 注解格式 - -注解大体上可以分为两种,一种是javadoc注解,另一种是简单注解。javadoc注解可以生成JavaAPI为外部用户提供有效的支持javadoc注解通常在使用IDEA,或者Eclipse等开发工具时都可以自动生成,也支持自定义的注解模板,仅需要对对应的字段进行解释。参与同一项目开发的同学,尽量设置成相同的注解模板。 - -#### a. 包注解 - -包注解在工作中往往比较特殊,通过包注解可以快速知悉当前包下代码是用来实现哪些功能,强烈建议工作中加上,尤其是对于一些比较复杂的包,包注解一般在包的根目录下,名称统一为package-info.java。 - -```java -/** - * 落地也质量检测 - * 1. 用来解决什么问题 - * 对广告主投放的广告落地页进行性能检测,模拟不同的系统,如Android,IOS等; 模拟不同的网络:2G,3G,4G,wifi等 - * - * 2. 如何实现 - * 基于chrome浏览器,用chromedriver驱动浏览器,设置对应的网络,OS参数,获取到浏览器返回结果。 - * - * 注意: 网络环境配置信息{@link cn.mycookies.landingpagecheck.meta.NetWorkSpeedEnum}目前使用是常规速度,可以根据实际情况进行调整 - * - * @author cruder - * @time 2019/12/7 20:3 下午 - */ -package cn.mycookies.landingpagecheck; -``` - -#### b. 类注接 - -javadoc注解中,每个类都必须有注解。 - -```java -/** -* Copyright (C), 2019-2020, Jann balabala... -* -* 类的介绍:这是一个用来做什么事情的类,有哪些功能,用到的技术..... -* -* @author 类创建者姓名 保持对齐 -* @date 创建日期 保持对齐 -* @version 版本号 保持对齐 -*/ -``` - -#### c. 属性注解 - -在每个属性前面必须加上属性注释,通常有以下两种形式,至于怎么选择,你高兴就好,不过一个项目中要保持统一。 - -```java -/** 提示信息 */ -private String userName; -/** - * 密码 - */ -private String password; -``` - -#### d. 方法注释 - -在每个方法前面必须加上方法注释,对于方法中的每个参数,以及返回值都要有说明。 - -```java -/** - * 方法的详细说明,能干嘛,怎么实现的,注意事项... - * - * @param xxx 参数1的使用说明, 能否为null - * @return 返回结果的说明, 不同情况下会返回怎样的结果 - * @throws 异常类型 注明从此类方法中抛出异常的说明 - */ -``` - -#### e. 构造方法注释 - -在每个构造方法前面必须加上注释,注释模板如下: - -```java - /** - * 构造方法的详细说明 - * - * @param xxx 参数1的使用说明, 能否为null - * @throws 异常类型 注明从此类方法中抛出异常的说明 - */ -``` - -而简单注解往往是需要工程师字节定义,在使用注解时应该注意以下几点: - -1. 枚举类的各个属性值都要使用注解,枚举可以理解为是常量,通常不会发生改变,通常会被在多个地方引用,对枚举的修改和添加属性通常会带来很大的影响。 -2. 保持排版整洁,不要使用行尾注释;双斜杠和星号之后要用1个空格分隔。 - -```java -id = 1;// 反例:不要使用行尾注释 -//反例:换行符与注释之间没有缩进 -int age = 18; -// 正例:姓名 -String name; -/** - * 1. 多行注释 - * - * 2. 对于不同的逻辑说明,可以用空行分隔 - */ -``` - -## 总结 - -无论是命名和注解,他们的目的都是为了让代码和工程师进行对话,增强代码的可读性,可维护性。优秀的代码往往能够见名知意,注解往往是对命名的补充和完善。命名太南了! - -参考文献: - -- 《码出高效》 -- https://www.cnblogs.com/wangcp-2014/p/10215620.html -- https://qiita.com/KeithYokoma/items/2193cf79ba76563e3db6 -- https://google.github.io/styleguide/javaguide.html#s2.1-file-name \ No newline at end of file diff --git "a/docs/java/java-programming-problem/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" "b/docs/java/java-programming-problem/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" deleted file mode 100644 index 112c1bff5f7..00000000000 --- "a/docs/java/java-programming-problem/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" +++ /dev/null @@ -1,130 +0,0 @@ - - -- [0.0.1. 泛型的实际应用:实现最小值函数](#001-%e6%b3%9b%e5%9e%8b%e7%9a%84%e5%ae%9e%e9%99%85%e5%ba%94%e7%94%a8%e5%ae%9e%e7%8e%b0%e6%9c%80%e5%b0%8f%e5%80%bc%e5%87%bd%e6%95%b0) -- [0.0.2. 使用数组实现栈](#002-%e4%bd%bf%e7%94%a8%e6%95%b0%e7%bb%84%e5%ae%9e%e7%8e%b0%e6%a0%88) -- [0.0.3. 实现线程安全的 LRU 缓存](#003-%e5%ae%9e%e7%8e%b0%e7%ba%bf%e7%a8%8b%e5%ae%89%e5%85%a8%e7%9a%84-lru-%e7%bc%93%e5%ad%98) - - - -### 0.0.1. 泛型的实际应用:实现最小值函数 - -自己设计一个泛型的获取数组最小值的函数.并且这个方法只能接受Number的子类并且实现了Comparable接口。 - -```java -//注意:Number并没有实现Comparable -private static > T min(T[] values) { - if (values == null || values.length == 0) return null; - T min = values[0]; - for (int i = 1; i < values.length; i++) { - if (min.compareTo(values[i]) > 0) min = values[i]; - } - return min; -} -``` - -测试: - -```java -int minInteger = min(new Integer[]{1, 2, 3});//result:1 -double minDouble = min(new Double[]{1.2, 2.2, -1d});//result:-1d -String typeError = min(new String[]{"1","3"});//报错 -``` -### 0.0.2. 使用数组实现栈 - -**自己实现一个栈,要求这个栈具有`push()`、`pop()`(返回栈顶元素并出栈)、`peek()` (返回栈顶元素不出栈)、`isEmpty()`、`size()`这些基本的方法。** - -提示:每次入栈之前先判断栈的容量是否够用,如果不够用就用`Arrays.copyOf()`进行扩容; - -```java -public class MyStack { - private int[] storage;//存放栈中元素的数组 - private int capacity;//栈的容量 - private int count;//栈中元素数量 - private static final int GROW_FACTOR = 2; - - //不带初始容量的构造方法。默认容量为8 - public MyStack() { - this.capacity = 8; - this.storage=new int[8]; - this.count = 0; - } - - //带初始容量的构造方法 - public MyStack(int initialCapacity) { - if (initialCapacity < 1) - throw new IllegalArgumentException("Capacity too small."); - - this.capacity = initialCapacity; - this.storage = new int[initialCapacity]; - this.count = 0; - } - - //入栈 - public void push(int value) { - if (count == capacity) { - ensureCapacity(); - } - storage[count++] = value; - } - - //确保容量大小 - private void ensureCapacity() { - int newCapacity = capacity * GROW_FACTOR; - storage = Arrays.copyOf(storage, newCapacity); - capacity = newCapacity; - } - - //返回栈顶元素并出栈 - private int pop() { - if (count == 0) - throw new IllegalArgumentException("Stack is empty."); - count--; - return storage[count]; - } - - //返回栈顶元素不出栈 - private int peek() { - if (count == 0){ - throw new IllegalArgumentException("Stack is empty."); - }else { - return storage[count-1]; - } - } - - //判断栈是否为空 - private boolean isEmpty() { - return count == 0; - } - - //返回栈中元素的个数 - private int size() { - return count; - } - -} - -``` - -验证 - -```java -MyStack myStack = new MyStack(3); -myStack.push(1); -myStack.push(2); -myStack.push(3); -myStack.push(4); -myStack.push(5); -myStack.push(6); -myStack.push(7); -myStack.push(8); -System.out.println(myStack.peek());//8 -System.out.println(myStack.size());//8 -for (int i = 0; i < 8; i++) { - System.out.println(myStack.pop()); -} -System.out.println(myStack.isEmpty());//true -myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty. -``` - - - diff --git a/docs/java/java-programming-problem/a-thread-safe-implementation-of-lru-cache.md b/docs/java/java-programming-problem/a-thread-safe-implementation-of-lru-cache.md deleted file mode 100644 index 7c367e11bd0..00000000000 --- a/docs/java/java-programming-problem/a-thread-safe-implementation-of-lru-cache.md +++ /dev/null @@ -1,441 +0,0 @@ - - -- [1. LRU 缓存介绍](#1-lru-%e7%bc%93%e5%ad%98%e4%bb%8b%e7%bb%8d) -- [2. ConcurrentLinkedQueue简单介绍](#2-concurrentlinkedqueue%e7%ae%80%e5%8d%95%e4%bb%8b%e7%bb%8d) -- [3. ReadWriteLock简单介绍](#3-readwritelock%e7%ae%80%e5%8d%95%e4%bb%8b%e7%bb%8d) -- [4. ScheduledExecutorService 简单介绍](#4-scheduledexecutorservice-%e7%ae%80%e5%8d%95%e4%bb%8b%e7%bb%8d) -- [5. 徒手撸一个线程安全的 LRU 缓存](#5-%e5%be%92%e6%89%8b%e6%92%b8%e4%b8%80%e4%b8%aa%e7%ba%bf%e7%a8%8b%e5%ae%89%e5%85%a8%e7%9a%84-lru-%e7%bc%93%e5%ad%98) - - [5.1. 实现方法](#51-%e5%ae%9e%e7%8e%b0%e6%96%b9%e6%b3%95) - - [5.2. 原理](#52-%e5%8e%9f%e7%90%86) - - [5.3. put方法具体流程分析](#53-put%e6%96%b9%e6%b3%95%e5%85%b7%e4%bd%93%e6%b5%81%e7%a8%8b%e5%88%86%e6%9e%90) - - [5.4. 源码](#54-%e6%ba%90%e7%a0%81) -- [6. 实现一个线程安全并且带有过期时间的 LRU 缓存](#6-%e5%ae%9e%e7%8e%b0%e4%b8%80%e4%b8%aa%e7%ba%bf%e7%a8%8b%e5%ae%89%e5%85%a8%e5%b9%b6%e4%b8%94%e5%b8%a6%e6%9c%89%e8%bf%87%e6%9c%9f%e6%97%b6%e9%97%b4%e7%9a%84-lru-%e7%bc%93%e5%ad%98) - - - -最近被读者问到“不用LinkedHashMap的话,如何实现一个线程安全的 LRU 缓存?网上的代码太杂太乱,Guide哥哥能不能帮忙写一个?”。 - -*划重点,手写一个 LRU 缓存在面试中还是挺常见的!* - -很多人就会问了:“网上已经有这么多现成的缓存了!为什么面试官还要我们自己实现一个呢?” 。咳咳咳,当然是为了面试需要。哈哈!开个玩笑,我个人觉得更多地是为了学习吧!今天Guide哥教大家: - -1. 实现一个线程安全的 LRU 缓存 -2. 实现一个线程安全并且带有过期时间的 LRU 缓存 - -考虑到了线程安全性我们使用了 `ConcurrentHashMap` 、`ConcurrentLinkedQueue` 这两个线程安全的集合。另外,还用到 `ReadWriteLock`(读写锁)。为了实现带有过期时间的缓存,我们用到了 `ScheduledExecutorService`来做定时任务执行。 - -如果有任何不对或者需要完善的地方,请帮忙指出! - -### 1. LRU 缓存介绍 - -**LRU (Least Recently Used,最近最少使用)是一种缓存淘汰策略。** - -LRU缓存指的是当缓存大小已达到最大分配容量的时候,如果再要去缓存新的对象数据的话,就需要将缓存中最近访问最少的对象删除掉以便给新来的数据腾出空间。 - -### 2. ConcurrentLinkedQueue简单介绍 - -**ConcurrentLinkedQueue是一个基于单向链表的无界无锁线程安全的队列,适合在高并发环境下使用,效率比较高。** 我们在使用的时候,可以就把它理解为我们经常接触的数据结构——队列,不过是增加了多线程下的安全性保证罢了。**和普通队列一样,它也是按照先进先出(FIFO)的规则对接点进行排序。** 另外,队列元素中不可以放置null元素。 - -`ConcurrentLinkedQueue` 整个继承关系如下图所示: - -![](./../../../media/pictures/java/my-lru-cache/ConcurrentLinkedQueue-Diagram.png) - -`ConcurrentLinkedQueue中`最主要的两个方法是:`offer(value)`和`poll()`,分别实现队列的两个重要的操作:入队和出队(`offer(value)`等价于 `add(value)`)。 - -我们添加一个元素到队列的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。 - -![单链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/单链表2.png) - -利用`ConcurrentLinkedQueue`队列先进先出的特性,每当我们 `put`/`get`(缓存被使用)元素的时候,我们就将这个元素存放在队列尾部,这样就能保证队列头部的元素是最近最少使用的。 - -### 3. ReadWriteLock简单介绍 - -`ReadWriteLock` 是一个接口,位于`java.util.concurrent.locks`包下,里面只有两个方法分别返回读锁和写锁: - -```java -public interface ReadWriteLock { - /** - * 返回读锁 - */ - Lock readLock(); - - /** - * 返回写锁 - */ - Lock writeLock(); -} -``` - -`ReentrantReadWriteLock` 是`ReadWriteLock`接口的具体实现类。 - -**读写锁还是比较适合缓存这种读多写少的场景。读写锁可以保证多个线程和同时读取,但是只有一个线程可以写入。** - -读写锁的特点是:写锁和写锁互斥,读锁和写锁互斥,读锁之间不互斥。也就说:同一时刻只能有一个线程写,但是可以有多个线程 -读。读写之间是互斥的,两者不能同时发生(当进行写操作时,同一时刻其他线程的读操作会被阻塞;当进行读操作时,同一时刻所有线程的写操作会被阻塞)。 - -另外,**同一个线程持有写锁时是可以申请读锁,但是持有读锁的情况下不可以申请写锁。** - -### 4. ScheduledExecutorService 简单介绍 - -`ScheduledExecutorService` 是一个接口,`ScheduledThreadPoolExecutor` 是其主要实现类。 - -![](./../../../media/pictures/java/my-lru-cache/ScheduledThreadPoolExecutor-diagram.png) - -**`ScheduledThreadPoolExecutor` 主要用来在给定的延迟后运行任务,或者定期执行任务。** 这个在实际项目用到的比较少,因为有其他方案选择比如`quartz`。但是,在一些需求比较简单的场景下还是非常有用的! - -**`ScheduledThreadPoolExecutor` 使用的任务队列 `DelayQueue` 封装了一个 `PriorityQueue`,`PriorityQueue` 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行,如果执行所需时间相同则先提交的任务将被先执行。** - -### 5. 徒手撸一个线程安全的 LRU 缓存 - -#### 5.1. 实现方法 - - `ConcurrentHashMap` + `ConcurrentLinkedQueue` +`ReadWriteLock` - -#### 5.2. 原理 - -`ConcurrentHashMap` 是线程安全的Map,我们可以利用它缓存 key,value形式的数据。`ConcurrentLinkedQueue`是一个线程安全的基于链表的队列(先进先出),我们可以用它来维护 key 。每当我们put/get(缓存被使用)元素的时候,我们就将这个元素对应的 key 存放在队列尾部,这样就能保证队列头部的元素是最近最少使用的。当我们的缓存容量不够的时候,我们直接移除队列头部对应的key以及这个key对应的缓存即可! - -另外,我们用到了`ReadWriteLock`(读写锁)来保证线程安全。 - -#### 5.3. put方法具体流程分析 - -为了方便大家理解,我将代码中比较重要的 `put(key,value)`方法的原理图画了出来,如下图所示: - -![](./../../../media/pictures/java/my-lru-cache/MyLRUCachePut.png) - - - -#### 5.4. 源码 - -```java -/** - * @author shuang.kou - *

- * 使用 ConcurrentHashMap+ConcurrentLinkedQueue+ReadWriteLock实现线程安全的 LRU 缓存 - * 这里只是为了学习使用,本地缓存推荐使用 Guava 自带的,使用 Spring 的话,推荐使用Spring Cache - */ -public class MyLruCache { - - /** - * 缓存的最大容量 - */ - private final int maxCapacity; - - private ConcurrentHashMap cacheMap; - private ConcurrentLinkedQueue keys; - /** - * 读写锁 - */ - private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - private Lock writeLock = readWriteLock.writeLock(); - private Lock readLock = readWriteLock.readLock(); - - public MyLruCache(int maxCapacity) { - if (maxCapacity < 0) { - throw new IllegalArgumentException("Illegal max capacity: " + maxCapacity); - } - this.maxCapacity = maxCapacity; - cacheMap = new ConcurrentHashMap<>(maxCapacity); - keys = new ConcurrentLinkedQueue<>(); - } - - public V put(K key, V value) { - // 加写锁 - writeLock.lock(); - try { - //1.key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - moveToTailOfQueue(key); - cacheMap.put(key, value); - return value; - } - //2.是否超出缓存容量,超出的话就移除队列头部的元素以及其对应的缓存 - if (cacheMap.size() == maxCapacity) { - System.out.println("maxCapacity of cache reached"); - removeOldestKey(); - } - //3.key不存在于当前缓存。将key添加到队列的尾部并且缓存key及其对应的元素 - keys.add(key); - cacheMap.put(key, value); - return value; - } finally { - writeLock.unlock(); - } - } - - public V get(K key) { - //加读锁 - readLock.lock(); - try { - //key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - // 存在的话就将key移动到队列的尾部 - moveToTailOfQueue(key); - return cacheMap.get(key); - } - //不存在于当前缓存中就返回Null - return null; - } finally { - readLock.unlock(); - } - } - - public V remove(K key) { - writeLock.lock(); - try { - //key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - // 存在移除队列和Map中对应的Key - keys.remove(key); - return cacheMap.remove(key); - } - //不存在于当前缓存中就返回Null - return null; - } finally { - writeLock.unlock(); - } - } - - /** - * 将元素添加到队列的尾部(put/get的时候执行) - */ - private void moveToTailOfQueue(K key) { - keys.remove(key); - keys.add(key); - } - - /** - * 移除队列头部的元素以及其对应的缓存 (缓存容量已满的时候执行) - */ - private void removeOldestKey() { - K oldestKey = keys.poll(); - if (oldestKey != null) { - cacheMap.remove(oldestKey); - } - } - - public int size() { - return cacheMap.size(); - } - -} -``` - -**非并发环境测试:** - -```java -MyLruCache myLruCache = new MyLruCache<>(3); -myLruCache.put(1, "Java"); -System.out.println(myLruCache.get(1));// Java -myLruCache.remove(1); -System.out.println(myLruCache.get(1));// null -myLruCache.put(2, "C++"); -myLruCache.put(3, "Python"); -System.out.println(myLruCache.get(2));//C++ -myLruCache.put(4, "C"); -myLruCache.put(5, "PHP"); -System.out.println(myLruCache.get(2));// C++ -``` - -**并发环境测试:** - -我们初始化了一个固定容量为 10 的线程池和count为10的`CountDownLatch`。我们将1000000次操作分10次添加到线程池,然后我们等待线程池执行完成这10次操作。 - - -```java -int threadNum = 10; -int batchSize = 100000; -//init cache -MyLruCache myLruCache = new MyLruCache<>(batchSize * 10); -//init thread pool with 10 threads -ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadNum); -//init CountDownLatch with 10 count -CountDownLatch latch = new CountDownLatch(threadNum); -AtomicInteger atomicInteger = new AtomicInteger(0); -long startTime = System.currentTimeMillis(); -for (int t = 0; t < threadNum; t++) { - fixedThreadPool.submit(() -> { - for (int i = 0; i < batchSize; i++) { - int value = atomicInteger.incrementAndGet(); - myLruCache.put("id" + value, value); - } - latch.countDown(); - }); -} -//wait for 10 threads to complete the task -latch.await(); -fixedThreadPool.shutdown(); -System.out.println("Cache size:" + myLruCache.size());//Cache size:1000000 -long endTime = System.currentTimeMillis(); -long duration = endTime - startTime; -System.out.println(String.format("Time cost:%dms", duration));//Time cost:511ms -``` - -### 6. 实现一个线程安全并且带有过期时间的 LRU 缓存 - -实际上就是在我们上面时间的LRU缓存的基础上加上一个定时任务去删除缓存,单纯利用 JDK 提供的类,我们实现定时任务的方式有很多种: - -1. `Timer` :不被推荐,多线程会存在问题。 -2. `ScheduledExecutorService` :定时器线程池,可以用来替代 `Timer` -3. `DelayQueue` :延时队列 -4. `quartz` :一个很火的开源任务调度框架,很多其他框架都是基于 `quartz` 开发的,比如当当网的`elastic-job `就是基于`quartz`二次开发之后的分布式调度解决方案 -5. ...... - -最终我们选择了 `ScheduledExecutorService`,主要原因是它易用(基于`DelayQueue`做了很多封装)并且基本能满足我们的大部分需求。 - -我们在我们上面实现的线程安全的 LRU 缓存基础上,简单稍作修改即可!我们增加了一个方法: - -```java -private void removeAfterExpireTime(K key, long expireTime) { - scheduledExecutorService.schedule(() -> { - //过期后清除该键值对 - cacheMap.remove(key); - keys.remove(key); - }, expireTime, TimeUnit.MILLISECONDS); -} -``` -我们put元素的时候,如果通过这个方法就能直接设置过期时间。 - - -**完整源码如下:** - -```java -/** - * @author shuang.kou - *

- * 使用 ConcurrentHashMap+ConcurrentLinkedQueue+ReadWriteLock+ScheduledExecutorService实现线程安全的 LRU 缓存 - * 这里只是为了学习使用,本地缓存推荐使用 Guava 自带的,使用 Spring 的话,推荐使用Spring Cache - */ -public class MyLruCacheWithExpireTime { - - /** - * 缓存的最大容量 - */ - private final int maxCapacity; - - private ConcurrentHashMap cacheMap; - private ConcurrentLinkedQueue keys; - /** - * 读写锁 - */ - private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - private Lock writeLock = readWriteLock.writeLock(); - private Lock readLock = readWriteLock.readLock(); - - private ScheduledExecutorService scheduledExecutorService; - - public MyLruCacheWithExpireTime(int maxCapacity) { - if (maxCapacity < 0) { - throw new IllegalArgumentException("Illegal max capacity: " + maxCapacity); - } - this.maxCapacity = maxCapacity; - cacheMap = new ConcurrentHashMap<>(maxCapacity); - keys = new ConcurrentLinkedQueue<>(); - scheduledExecutorService = Executors.newScheduledThreadPool(3); - } - - public V put(K key, V value, long expireTime) { - // 加写锁 - writeLock.lock(); - try { - //1.key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - moveToTailOfQueue(key); - cacheMap.put(key, value); - return value; - } - //2.是否超出缓存容量,超出的话就移除队列头部的元素以及其对应的缓存 - if (cacheMap.size() == maxCapacity) { - System.out.println("maxCapacity of cache reached"); - removeOldestKey(); - } - //3.key不存在于当前缓存。将key添加到队列的尾部并且缓存key及其对应的元素 - keys.add(key); - cacheMap.put(key, value); - if (expireTime > 0) { - removeAfterExpireTime(key, expireTime); - } - return value; - } finally { - writeLock.unlock(); - } - } - - public V get(K key) { - //加读锁 - readLock.lock(); - try { - //key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - // 存在的话就将key移动到队列的尾部 - moveToTailOfQueue(key); - return cacheMap.get(key); - } - //不存在于当前缓存中就返回Null - return null; - } finally { - readLock.unlock(); - } - } - - public V remove(K key) { - writeLock.lock(); - try { - //key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - // 存在移除队列和Map中对应的Key - keys.remove(key); - return cacheMap.remove(key); - } - //不存在于当前缓存中就返回Null - return null; - } finally { - writeLock.unlock(); - } - } - - /** - * 将元素添加到队列的尾部(put/get的时候执行) - */ - private void moveToTailOfQueue(K key) { - keys.remove(key); - keys.add(key); - } - - /** - * 移除队列头部的元素以及其对应的缓存 (缓存容量已满的时候执行) - */ - private void removeOldestKey() { - K oldestKey = keys.poll(); - if (oldestKey != null) { - cacheMap.remove(oldestKey); - } - } - - private void removeAfterExpireTime(K key, long expireTime) { - scheduledExecutorService.schedule(() -> { - //过期后清除该键值对 - cacheMap.remove(key); - keys.remove(key); - }, expireTime, TimeUnit.MILLISECONDS); - } - - public int size() { - return cacheMap.size(); - } - -} - -``` - -**测试效果:** - -```java -MyLruCacheWithExpireTime myLruCache = new MyLruCacheWithExpireTime<>(3); -myLruCache.put(1,"Java",3000); -myLruCache.put(2,"C++",3000); -myLruCache.put(3,"Python",1500); -System.out.println(myLruCache.size());//3 -Thread.sleep(2000); -System.out.println(myLruCache.size());//2 -``` diff --git a/docs/java/jdk-new-features/new-features-from-jdk8-to-jdk14.md b/docs/java/jdk-new-features/new-features-from-jdk8-to-jdk14.md deleted file mode 100644 index 4fabcf3ca87..00000000000 --- a/docs/java/jdk-new-features/new-features-from-jdk8-to-jdk14.md +++ /dev/null @@ -1,441 +0,0 @@ -大家好,我是Guide哥!这篇文章来自读者的投稿,经过了两次较大的改动,两周的完善终于完成。Java 8新特性见这里:[Java8新特性最佳指南](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484744&idx=1&sn=9db31dca13d327678845054af75efb74&chksm=cea24a83f9d5c3956f4feb9956b068624ab2fdd6c4a75fe52d5df5dca356a016577301399548&token=1082669959&lang=zh_CN&scene=21#wechat_redirect) 。 - -*Guide 哥:别人家的特性都用了几年了,我 Java 才出来,哈哈!真实!* - -## Java9 - -发布于 2017 年 9 月 21 日 。作为 Java8 之后 3 年半才发布的新版本,Java 9 带 来了很多重大的变化其中最重要的改动是 Java 平台模块系统的引入,其他还有诸如集合、Stream 流 - -### Java 平台模块系统 - -Java 平台模块系统,也就是 Project Jigsaw,把模块化开发实践引入到了 Java 平台中。在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。 - -Java 9 模块的重要特征是在其工件(artifact)的根目录中包含了一个描述模块的 module-info.class 文 件。 工件的格式可以是传统的 JAR 文件或是 Java 9 新增的 JMOD 文件。 - -### Jshell - -jshell 是 Java 9 新增的一个实用工具。为 Java 提供了类似于 Python 的实时命令行交互工具。 - -在 Jshell 中可以直接输入表达式并查看其执行结果 - -### 集合、Stream 和 Optional - -- 增加 了 `List.of()`、`Set.of()`、`Map.of()` 和 `Map.ofEntries()`等工厂方法来创建不可变集合,比如`List.of("Java", "C++");`、`Map.of("Java", 1, "C++", 2)`;(这部分内容有点参考 Guava 的味道) -- `Stream` 中增加了新的方法 `ofNullable`、`dropWhile`、`takeWhile` 和 `iterate` 方法。`Collectors` 中增加了新的方法 `filtering` 和 `flatMapping` -- `Optional` 类中新增了 `ifPresentOrElse`、`or` 和 `stream` 等方法 - -### 进程 API - -Java 9 增加了 `ProcessHandle` 接口,可以对原生进程进行管理,尤其适合于管理长时间运行的进程 - -### 平台日志 API 和服务 - -Java 9 允许为 JDK 和应用配置同样的日志实现。新增了 `System.LoggerFinder` 用来管理 JDK 使 用的日志记录器实现。JVM 在运行时只有一个系统范围的 `LoggerFinder` 实例。 - -我们可以通过添加自己的 `System.LoggerFinder` 实现来让 JDK 和应用使用 SLF4J 等其他日志记录框架。 - -### 反应式流 ( Reactive Streams ) - -- 在 Java9 中的 `java.util.concurrent.Flow` 类中新增了反应式流规范的核心接口 -- Flow 中包含了 `Flow.Publisher`、`Flow.Subscriber`、`Flow.Subscription` 和 `Flow.Processor` 等 4 个核心接口。Java 9 还提供了`SubmissionPublisher` 作为`Flow.Publisher` 的一个实现。 - -### 变量句柄 - -- 变量句柄是一个变量或一组变量的引用,包括静态域,非静态域,数组元素和堆外数据结构中的组成部分等 -- 变量句柄的含义类似于已有的方法句柄`MethodHandle` -- 由 Java 类`java.lang.invoke.VarHandle` 来表示,可以使用类 `java.lang.invoke.MethodHandles.Lookup` 中的静态工厂方法来创建 `VarHandle` 对 象 - -### 改进方法句柄(Method Handle) - -- 方法句柄从 Java7 开始引入,Java9 在类`java.lang.invoke.MethodHandles` 中新增了更多的静态方法来创建不同类型的方法句柄 - -### 其它新特性 - -- **接口私有方法** :Java 9 允许在接口中使用私有方法 -- **try-with-resources 增强** :在 try-with-resources 语句中可以使用 effectively-final 变量(什么是 effectively-final 变量,见这篇文章 [http://ilkinulas.github.io/programming/java/2016/03/27/effectively-final-java.html](http://ilkinulas.github.io/programming/java/2016/03/27/effectively-final-java.html)) -- **类 `CompletableFuture` 中增加了几个新的方法(`completeAsync` ,`orTimeout` 等)** -- **Nashorn 引擎的增强** :Nashorn 从 Java8 开始引入的 JavaScript 引擎,Java9 对 Nashorn 做了些增强,实现了一些 ES6 的新特性 -- **I/O 流的新特性** :增加了新的方法来读取和复制 InputStream 中包含的数据 -- **改进应用的安全性能** :Java 9 新增了 4 个 SHA- 3 哈希算法,SHA3-224、SHA3-256、SHA3-384 和 S HA3-512 -- ...... - -## Java10 - -发布于 2018 年 3 月 20 日,最知名的特性应该是 var 关键字(局部变量类型推断)的引入了,其他还有垃圾收集器改善、GC 改进、性能提升、线程管控等一批新特性 - -### var 关键字 - -- **介绍** :提供了 var 关键字声明局部变量:`var list = new ArrayList(); // ArrayList` -- **局限性** :只能用于带有构造器的**局部变量**和 for 循环中 - -_Guide 哥:实际上 Lombok 早就体用了一个类似的关键字,使用它可以简化代码,但是可能会降低程序的易读性、可维护性。一般情况下,我个人都不太推荐使用。_ - -### 不可变集合 - -**list,set,map 提供了静态方法`copyOf()`返回入参集合的一个不可变拷贝(以下为 JDK 的源码)** - -```java -static List copyOf(Collection coll) { - return ImmutableCollections.listCopy(coll); -} -``` - -**`java.util.stream.Collectors`中新增了静态方法,用于将流中的元素收集为不可变的集合** - -### Optional - -- 新增了`orElseThrow()`方法来在没有值时抛出异常 - -### 并行全垃圾回收器 G1 - -从 Java9 开始 G1 就了默认的垃圾回收器,G1 是以一种低延时的垃圾回收器来设计的,旨在避免进行 Full GC,但是 Java9 的 G1 的 FullGC 依然是使用单线程去完成标记清除算法,这可能会导致垃圾回收期在无法回收内存的时候触发 Full GC。 - -为了最大限度地减少 Full GC 造成的应用停顿的影响,从 Java10 开始,G1 的 FullGC 改为并行的标记清除算法,同时会使用与年轻代回收和混合回收相同的并行工作线程数量,从而减少了 Full GC 的发生,以带来更好的性能提升、更大的吞吐量。 - -### 应用程序类数据共享 - -在 Java 5 中就已经引入了类数据共享机制 (Class Data Sharing,简称 CDS),允许将一组类预处理为共享归档文件,以便在运行时能够进行内存映射以减少 Java 程序的启动时间,当多个 Java 虚拟机(JVM)共享相同的归档文件时,还可以减少动态内存的占用量,同时减少多个虚拟机在同一个物理或虚拟的机器上运行时的资源占用 - -Java 10 在现有的 CDS 功能基础上再次拓展,以允许应用类放置在共享存档中。CDS 特性在原来的 bootstrap 类基础之上,扩展加入了应用类的 CDS (Application Class-Data Sharing) 支持。其原理为:在启动时记录加载类的过程,写入到文本文件中,再次启动时直接读取此启动文本并加载。设想如果应用环境没有大的变化,启动速度就会得到提升 - -### 其他特性 - -- **线程-局部管控**:Java 10 中线程管控引入 JVM 安全点的概念,将允许在不运行全局 JVM 安全点的情况下实现线程回调,由线程本身或者 JVM 线程来执行,同时保持线程处于阻塞状态,这种方式使得停止单个线程变成可能,而不是只能启用或停止所有线程 - -- **备用存储装置上的堆分配**:Java 10 中将使得 JVM 能够使用适用于不同类型的存储机制的堆,在可选内存设备上进行堆内存分配 -- **统一的垃圾回收接口**:Java 10 中,hotspot/gc 代码实现方面,引入一个干净的 GC 接口,改进不同 GC 源代码的隔离性,多个 GC 之间共享的实现细节代码应该存在于辅助类中。统一垃圾回收接口的主要原因是:让垃圾回收器(GC)这部分代码更加整洁,便于新人上手开发,便于后续排查相关问题。 - -## Java11 - -Java11 于 2018 年 9 月 25 日正式发布,这是很重要的一个版本!Java 11 和 2017 年 9 月份发布的 Java 9 以及 2018 年 3 月份发布的 Java 10 相比,其最大的区别就是:在长期支持(Long-Term-Support)方面,**Oracle 表示会对 Java 11 提供大力支持,这一支持将会持续至 2026 年 9 月。这是据 Java 8 以后支持的首个长期版本。** - -![](https://imgkr.cn-bj.ufileos.com/fc66979f-7974-40e8-88ae-6dbff15ac9ef.png) - -### 字符串加强 - -Java 11 增加了一系列的字符串处理方法,如以下所示。 - -_Guide 哥:说白点就是多了层封装,JDK 开发组的人没少看市面上常见的工具类框架啊!_ - -```java -//判断字符串是否为空 -" ".isBlank();//true -//去除字符串首尾空格 -" Java ".strip();// "Java" -//去除字符串首部空格 -" Java ".stripLeading(); // "Java " -//去除字符串尾部空格 -" Java ".stripTrailing(); // " Java" -//重复字符串多少次 -"Java".repeat(3); // "JavaJavaJava" - -//返回由行终止符分隔的字符串集合。 -"A\nB\nC".lines().count(); // 3 -"A\nB\nC".lines().collect(Collectors.toList()); -``` - -### ZGC:可伸缩低延迟垃圾收集器 - -**ZGC 即 Z Garbage Collector**,是一个可伸缩的、低延迟的垃圾收集器。 - -ZGC 主要为了满足如下目标进行设计: - -- GC 停顿时间不超过 10ms -- 即能处理几百 MB 的小堆,也能处理几个 TB 的大堆 -- 应用吞吐能力不会下降超过 15%(与 G1 回收算法相比) -- 方便在此基础上引入新的 GC 特性和利用 colord -- 针以及 Load barriers 优化奠定基础 -- 当前只支持 Linux/x64 位平台 - -ZGC 目前 **处在实验阶段**,只支持 Linux/x64 平台 - -### 标准 HTTP Client 升级 - -Java 11 对 Java 9 中引入并在 Java 10 中进行了更新的 Http Client API 进行了标准化,在前两个版本中进行孵化的同时,Http Client 几乎被完全重写,并且现在完全支持异步非阻塞。 - -并且,Java11 中,Http Client 的包名由 `jdk.incubator.http` 改为`java.net.http`,该 API 通过 `CompleteableFuture` 提供非阻塞请求和响应语义。 - -使用起来也很简单,如下: - -```java -var request = HttpRequest.newBuilder() - - .uri(URI.create("https://javastack.cn/")) - - .GET() - - .build(); - -var client = HttpClient.newHttpClient(); - -// 同步 - -HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - -System.out.println(response.body()); - -// 异步 - -client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) - - .thenApply(HttpResponse::body) - - .thenAccept(System.out::println); - - -``` - -### 简化启动单个源代码文件的方法 - -- 增强了 Java 启动器,使其能够运行单一文件的 Java 源代码。此功能允许使用 Java 解释器直接执行 Java 源代码。源代码在内存中编译,然后由解释器执行。唯一的约束在于所有相关的类必须定义在同一个 Java 文件中 -- 对于 Java 初学者并希望尝试简单程序的人特别有用,并且能和 jshell 一起使用 -- 一定能程度上增强了使用 Java 来写脚本程序的能力 - -### 用于 Lambda 参数的局部变量语法 - -- 从 Java 10 开始,便引入了局部变量类型推断这一关键特性。类型推断允许使用关键字 var 作为局部变量的类型而不是实际类型,编译器根据分配给变量的值推断出类型 -- Java 10 中对 var 关键字存在几个限制 - - 只能用于局部变量上 - - 声明时必须初始化 - - 不能用作方法参数 - - 不能在 Lambda 表达式中使用 -- Java11 开始允许开发者在 Lambda 表达式中使用 var 进行参数声明 - -### 其他特性 - -- 新的垃圾回收器 Epsilon,一个完全消极的 GC 实现,分配有限的内存资源,最大限度的降低内存占用和内存吞吐延迟时间 -- 低开销的 Heap Profiling:Java 11 中提供一种低开销的 Java 堆分配采样方法,能够得到堆分配的 Java 对象信息,并且能够通过 JVMTI 访问堆信息 -- TLS1.3 协议:Java 11 中包含了传输层安全性(TLS)1.3 规范(RFC 8446)的实现,替换了之前版本中包含的 TLS,包括 TLS 1.2,同时还改进了其他 TLS 功能,例如 OCSP 装订扩展(RFC 6066,RFC 6961),以及会话散列和扩展主密钥扩展(RFC 7627),在安全性和性能方面也做了很多提升 -- 飞行记录器:飞行记录器之前是商业版 JDK 的一项分析工具,但在 Java 11 中,其代码被包含到公开代码库中,这样所有人都能使用该功能了 - -## Java12 - -### 增强 Switch - -- 传统的 switch 语法存在容易漏写 break 的问题,而且从代码整洁性层面来看,多个 break 本质也是一种重复 - -- Java12 提供了 swtich 表达式,使用类似 lambda 语法条件匹配成功后的执行块,不需要多写 break - -- 作为预览特性加入,需要在`javac`编译和`java`运行时增加参数`--enable-preview` - - ```java - switch (day) { - case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); - case TUESDAY -> System.out.println(7); - case THURSDAY, SATURDAY -> System.out.println(8); - case WEDNESDAY -> System.out.println(9); - } - ``` - -### 数字格式化工具类 - -- `NumberFormat` 新增了对复杂的数字进行格式化的支持 - - ```java - NumberFormat fmt = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); - String result = fmt.format(1000); - System.out.println(result); // 输出为 1K,计算工资是多少K更方便了。。。 - ``` - -### Shenandoah GC - -- Redhat 主导开发的 Pauseless GC 实现,主要目标是 99.9% 的暂停小于 10ms,暂停与堆大小无关等 -- 和 Java11 开源的 ZGC 相比(需要升级到 JDK11 才能使用),Shenandoah GC 有稳定的 JDK8u 版本,在 Java8 占据主要市场份额的今天有更大的可落地性 - -### G1 收集器提升 - -- **Java12 为默认的垃圾收集器 G1 带来了两项更新:** - - 可中止的混合收集集合:JEP344 的实现,为了达到用户提供的停顿时间目标,JEP 344 通过把要被回收的区域集(混合收集集合)拆分为强制和可选部分,使 G1 垃圾回收器能中止垃圾回收过程。 G1 可以中止可选部分的回收以达到停顿时间目标 - - 及时返回未使用的已分配内存:JEP346 的实现,增强 G1 GC,以便在空闲时自动将 Java 堆内存返回给操作系统 - -## Java13 - -### 引入 yield 关键字到 Switch 中 - -- `Switch` 表达式中就多了一个关键字用于跳出 `Switch` 块的关键字 `yield`,主要用于返回一个值 - -- `yield`和 `return` 的区别在于:`return` 会直接跳出当前循环或者方法,而 `yield` 只会跳出当前 `Switch` 块,同时在使用 `yield` 时,需要有 `default` 条件 - - ```java - private static String descLanguage(String name) { - return switch (name) { - case "Java": yield "object-oriented, platform independent and secured"; - case "Ruby": yield "a programmer's best friend"; - default: yield name +" is a good language"; - }; - } - ``` - -### 文本块 - -- 解决 Java 定义多行字符串时只能通过换行转义或者换行连接符来变通支持的问题,引入**三重双引号**来定义多行文本 - -- 两个`"""`中间的任何内容都会被解释为字符串的一部分,包括换行符 - - ```java - String json ="{\n" + - " \"name\":\"mkyong\",\n" + - " \"age\":38\n" + - "}\n"; // 未支持文本块之前 - ``` - - ```java - - String json = """ - { - "name":"mkyong", - "age":38 - } - """; - ``` - -### 增强 ZGC 释放未使用内存 - -- 在 Java 11 中是实验性的引入的 ZGC 在实际的使用中存在未能主动将未使用的内存释放给操作系统的问题 -- ZGC 堆由一组称为 ZPages 的堆区域组成。在 GC 周期中清空 ZPages 区域时,它们将被释放并返回到页面缓存 **ZPageCache** 中,此缓存中的 ZPages 按最近最少使用(LRU)的顺序,并按照大小进行组织 -- 在 Java 13 中,ZGC 将向操作系统返回被标识为长时间未使用的页面,这样它们将可以被其他进程重用 - -### SocketAPI 重构 - -- Java 13 为 Socket API 带来了新的底层实现方法,并且在 Java 13 中是默认使用新的 Socket 实现,使其易于发现并在排除问题同时增加可维护性 - -### 动态应用程序类-数据共享 - -- Java 13 中对 Java 10 中引入的 应用程序类数据共享进行了进一步的简化、改进和扩展,即:**允许在 Java 应用程序执行结束时动态进行类归档**,具体能够被归档的类包括:所有已被加载,但不属于默认基层 CDS 的应用程序类和引用类库中的类 - -## Java14 - -### record 关键字 - -- 简化数据类的定义方式,使用 record 代替 class 定义的类,只需要声明属性,就可以在获得属性的访问方法,以及 toString,hashCode,equals 方法 - -- 类似于使用 Class 定义类,同时使用了 lomobok 插件,并打上了`@Getter,@ToString,@EqualsAndHashCode`注解 - -- 作为预览特性引入 - - ```java - /** - * 这个类具有两个特征 - * 1. 所有成员属性都是final - * 2. 全部方法由构造方法,和两个成员属性访问器组成(共三个) - * 那么这种类就很适合使用record来声明 - */ - final class Rectangle implements Shape { - final double length; - final double width; - - public Rectangle(double length, double width) { - this.length = length; - this.width = width; - } - - double length() { return length; } - double width() { return width; } - } - /** - * 1. 使用record声明的类会自动拥有上面类中的三个方法 - * 2. 在这基础上还附赠了equals(),hashCode()方法以及toString()方法 - * 3. toString方法中包括所有成员属性的字符串表示形式及其名称 - */ - record Rectangle(float length, float width) { } - ``` - -### 空指针异常精准提示 - -- 通过 JVM 参数中添加`-XX:+ShowCodeDetailsInExceptionMessages`,可以在空指针异常中获取更为详细的调用信息,更快的定位和解决问题 - - ```java - a.b.c.i = 99; // 假设这段代码会发生空指针 - ``` - - ```java - Exception in thread "main" java.lang.NullPointerException: - Cannot read field 'c' because 'a.b' is null. - at Prog.main(Prog.java:5) // 增加参数后提示的异常中很明确的告知了哪里为空导致 - ``` - -### switch 的增强终于转正 - -- JDK12 引入的 switch(预览特性)在 JDK14 变为正式版本,不需要增加参数来启用,直接在 JDK14 中就能使用 -- 主要是用`->`来替代以前的`:`+`break`;另外就是提供了 yield 来在 block 中返回值 - -_Before Java 14_ - -```java -switch (day) { - case MONDAY: - case FRIDAY: - case SUNDAY: - System.out.println(6); - break; - case TUESDAY: - System.out.println(7); - break; - case THURSDAY: - case SATURDAY: - System.out.println(8); - break; - case WEDNESDAY: - System.out.println(9); - break; -} -``` - -_Java 14 enhancements_ - -```java -switch (day) { - case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); - case TUESDAY -> System.out.println(7); - case THURSDAY, SATURDAY -> System.out.println(8); - case WEDNESDAY -> System.out.println(9); -} -``` - -### instanceof 增强 - -- instanceof 主要在**类型强转前探测对象的具体类型**,然后执行具体的强转 - -- 新版的 instanceof 可以在判断的是否属于具体的类型同时完成转换 - -```java -Object obj = "我是字符串"; -if(obj instanceof String str){ - System.out.println(str); -} -``` - -### 其他特性 - -- 从 Java11 引入的 ZGC 作为继 G1 过后的下一代 GC 算法,从支持 Linux 平台到 Java14 开始支持 MacOS 和 Window(个人感觉是终于可以在日常开发工具中先体验下 ZGC 的效果了,虽然其实 G1 也够用) -- 移除了 CMS 垃圾收集器(功成而退) -- 新增了 jpackage 工具,标配将应用打成 jar 包外,还支持不同平台的特性包,比如 linux 下的`deb`和`rpm`,window 平台下的`msi`和`exe` - -## 总结 - -### 关于预览特性 - -- 先贴一段 oracle 官网原文:`This is a preview feature, which is a feature whose design, specification, and implementation are complete, but is not permanent, which means that the feature may exist in a different form or not at all in future JDK releases. To compile and run code that contains preview features, you must specify additional command-line options.` -- 这是一个预览功能,该功能的设计,规格和实现是完整的,但不是永久性的,这意味着该功能可能以其他形式存在或在将来的 JDK 版本中根本不存在。 要编译和运行包含预览功能的代码,必须指定其他命令行选项。 -- 就以`switch`的增强为例子,从 Java12 中推出,到 Java13 中将继续增强,直到 Java14 才正式转正进入 JDK 可以放心使用,不用考虑后续 JDK 版本对其的改动或修改 -- 一方面可以看出 JDK 作为标准平台在增加新特性的严谨态度,另一方面个人认为是对于预览特性应该采取审慎使用的态度。特性的设计和实现容易,但是其实际价值依然需要在使用中去验证 - -### JVM 虚拟机优化 - -- 每次 Java 版本的发布都伴随着对 JVM 虚拟机的优化,包括对现有垃圾回收算法的改进,引入新的垃圾回收算法,移除老旧的不再适用于今天的垃圾回收算法等 -- 整体优化的方向是**高效,低时延的垃圾回收表现** -- 对于日常的应用开发者可能比较关注新的语法特性,但是从一个公司角度来说,在考虑是否升级 Java 平台时更加考虑的是**JVM 运行时的提升** - -## 参考信息 - -- IBM Developer Java9 -- Guide to Java10 -- Java 10 新特性介绍 -- IBM Devloper Java11 -- Java 11 – Features and Comparison: -- Oracle Java12 ReleaseNote -- Oracle Java13 ReleaseNote -- New Java13 Features -- Java13 新特性概述 -- Oracle Java14 record -- java14-features \ No newline at end of file diff --git "a/docs/java/jvm/GC\350\260\203\344\274\230\345\217\202\346\225\260.md" "b/docs/java/jvm/GC\350\260\203\344\274\230\345\217\202\346\225\260.md" deleted file mode 100644 index 917a337ca6b..00000000000 --- "a/docs/java/jvm/GC\350\260\203\344\274\230\345\217\202\346\225\260.md" +++ /dev/null @@ -1,63 +0,0 @@ -> 原文地址: https://juejin.im/post/5c94a123f265da610916081f。 - -## JVM 配置常用参数 - -1. 堆参数; -2. 回收器参数; -3. 项目中常用配置; -4. 常用组合; - -### 堆参数 - -![堆参数][1] - -### 回收器参数 - -![垃圾回收器参数][2] - -如上表所示,目前**主要有串行、并行和并发三种**,对于大内存的应用而言,串行的性能太低,因此使用到的主要是并行和并发两种。并行和并发 GC 的策略通过 `UseParallelGC `和` UseConcMarkSweepGC` 来指定,还有一些细节的配置参数用来配置策略的执行方式。例如:`XX:ParallelGCThreads`, `XX:CMSInitiatingOccupancyFraction` 等。 通常:Young 区对象回收只可选择并行(耗时间),Old 区选择并发(耗 CPU)。 - -### 项目中常用配置 - -> 备注:在Java8中永久代的参数`-XX:PermSize` 和`-XX:MaxPermSize`已经失效。 - -![项目中垃圾回收器常用配置][3] - -### 常用组合 - -![垃圾回收器常用组合][4] - -## 常用 GC 调优策略 - -1. GC 调优原则; -2. GC 调优目的; -3. GC 调优策略; - -### GC 调优原则 - -在调优之前,我们需要记住下面的原则: - -> 多数的 Java 应用不需要在服务器上进行 GC 优化; 多数导致 GC 问题的 Java 应用,都不是因为我们参数设置错误,而是代码问题; 在应用上线之前,先考虑将机器的 JVM 参数设置到最优(最适合); 减少创建对象的数量; 减少使用全局变量和大对象; GC 优化是到最后不得已才采用的手段; 在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多。 - -### GC 调优目的 - -将转移到老年代的对象数量降低到最小; 减少 GC 的执行时间。 - -### GC 调优策略 - -**策略 1:**将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。 - -**策略 2:**大对象进入老年代,虽然大部分情况下,将对象分配在新生代是合理的。但是对于大对象这种做法却值得商榷,大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代,破坏新生代的对象结构,可能会出现频繁的 full gc。因此,对于大对象,可以设置直接进入老年代(当然短命的大对象对于垃圾回收来说简直就是噩梦)。`-XX:PretenureSizeThreshold` 可以设置直接进入老年代的对象大小。 - -**策略 3:**合理设置进入老年代对象的年龄,`-XX:MaxTenuringThreshold` 设置对象进入老年代的年龄大小,减少老年代的内存占用,降低 full gc 发生的频率。 - -**策略 4:**设置稳定的堆大小,堆大小设置有两个参数:`-Xms` 初始化堆大小,`-Xmx` 最大堆大小。 - -**策略5:**注意: 如果满足下面的指标,**则一般不需要进行 GC 优化:** - -> MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次。 - -[1]: ./../../../media/pictures/jvm/java_jvm_heap_parameters.png -[2]: ./../../../media/pictures/jvm/java_jvm_garbage_collector_parameters.png -[3]: ./../../../media/pictures/jvm/java_jvm_suggest_parameters.png -[4]: ./../../../media/pictures/jvm/java_jvm_compose_garbage_collector.png \ No newline at end of file diff --git "a/docs/java/jvm/Java\345\206\205\345\255\230\345\214\272\345\237\237.md" "b/docs/java/jvm/Java\345\206\205\345\255\230\345\214\272\345\237\237.md" deleted file mode 100644 index 050401f979c..00000000000 --- "a/docs/java/jvm/Java\345\206\205\345\255\230\345\214\272\345\237\237.md" +++ /dev/null @@ -1,507 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [Java 内存区域详解](#java-内存区域详解) - - [写在前面 (常见面试题)](#写在前面-常见面试题) - - [基本问题](#基本问题) - - [拓展问题](#拓展问题) - - [一 概述](#一-概述) - - [二 运行时数据区域](#二-运行时数据区域) - - [2.1 程序计数器](#21-程序计数器) - - [2.2 Java 虚拟机栈](#22-java-虚拟机栈) - - [2.3 本地方法栈](#23-本地方法栈) - - [2.4 堆](#24-堆) - - [2.5 方法区](#25-方法区) - - [2.5.1 方法区和永久代的关系](#251-方法区和永久代的关系) - - [2.5.2 常用参数](#252-常用参数) - - [2.5.3 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?](#253-为什么要将永久代-permgen-替换为元空间-metaspace-呢) - - [2.6 运行时常量池](#26-运行时常量池) - - [2.7 直接内存](#27-直接内存) - - [三 HotSpot 虚拟机对象探秘](#三-hotspot-虚拟机对象探秘) - - [3.1 对象的创建](#31-对象的创建) - - [Step1:类加载检查](#step1类加载检查) - - [Step2:分配内存](#step2分配内存) - - [Step3:初始化零值](#step3初始化零值) - - [Step4:设置对象头](#step4设置对象头) - - [Step5:执行 init 方法](#step5执行-init-方法) - - [3.2 对象的内存布局](#32-对象的内存布局) - - [3.3 对象的访问定位](#33-对象的访问定位) - - [四 重点补充内容](#四--重点补充内容) - - [4.1 String 类和常量池](#41-string-类和常量池) - - [4.2 String s1 = new String("abc");这句话创建了几个字符串对象?](#42-string-s1--new-stringabc这句话创建了几个字符串对象) - - [4.3 8 种基本类型的包装类和常量池](#43-8-种基本类型的包装类和常量池) - - [参考](#参考) - - [公众号](#公众号) - - - -# Java 内存区域详解 - -如果没有特殊说明,都是针对的是 HotSpot 虚拟机。 - -## 写在前面 (常见面试题) - -### 基本问题 - -- **介绍下 Java 内存区域(运行时数据区)** -- **Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)** -- **对象的访问定位的两种方式(句柄和直接指针两种方式)** - -### 拓展问题 - -- **String 类和常量池** -- **8 种基本类型的包装类和常量池** - -## 一 概述 - -对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为每一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。 - -## 二 运行时数据区域 -Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同,下面会介绍到。 - -**JDK 1.8 之前:** - -

- -
-**JDK 1.8 :** - -
- -
- -**线程私有的:** - -- 程序计数器 -- 虚拟机栈 -- 本地方法栈 - -**线程共享的:** - -- 堆 -- 方法区 -- 直接内存 (非运行时数据区的一部分) - -### 2.1 程序计数器 -程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。**字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。** - -另外,**为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。** - -**从上面的介绍中我们知道程序计数器主要有两个作用:** - -1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 - -**注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。** - -### 2.2 Java 虚拟机栈 - -**与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。** - -**Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。** (实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。) - -**局部变量表主要存放了编译器可知的各种数据类型**(boolean、byte、char、short、int、float、long、double)、**对象引用**(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 - -**Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。** - -- **StackOverFlowError:** 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。 -- **OutOfMemoryError:** 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 错误。 - -Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。 - -**扩展:那么方法/函数如何调用?** - -Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。 - -Java 方法有两种返回方式: - -1. return 语句。 -2. 抛出异常。 - -不管哪种返回方式都会导致栈帧被弹出。 - -### 2.3 本地方法栈 - -和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 - -本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。 - -方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。 - -### 2.4 堆 - -Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。** - -**Java世界中“几乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。** - -Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(Garbage Collected Heap)**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。** - -在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分: - -1. 新生代内存(Young Generation) -2. 老生代(Old Generation) -3. 永生代(Permanent Generation) - -![JVM堆内存结构-JDK7](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/JVM堆内存结构-JDK7.jpg) - -JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。 - -![JVM堆内存结构-JDK8](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/JVM堆内存结构-jdk8.jpg) - -**上图所示的 Eden 区、两个 Survivor 区都属于新生代(为了区分,这两个 Survivor 区域按照顺序被命名为 from 和 to),中间一层属于老年代。** - -大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 - -> 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。 -> -> **动态年龄计算的代码如下** -> -> ```c++ -> uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { -> //survivor_capacity是survivor空间的大小 -> size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); -> size_t total = 0; -> uint age = 1; -> while (age < table_size) { -> total += sizes[age];//sizes数组是每个年龄段对象大小 -> if (total > desired_survivor_size) break; -> age++; -> } -> uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; -> ... -> } -> -> ``` -> -> - -堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如: - -1. **`OutOfMemoryError: GC Overhead Limit Exceeded`** : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。 -2. **`java.lang.OutOfMemoryError: Java heap space`** :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发`java.lang.OutOfMemoryError: Java heap space` 错误。(和本机物理内存无关,和你配置的内存大小有关!) -3. ...... - -### 2.5 方法区 - -方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 **Java 虚拟机规范把方法区描述为堆的一个逻辑部分**,但是它却有一个别名叫做 **Non-Heap(非堆)**,目的应该是与 Java 堆区分开来。 - -方法区也被称为永久代。很多人都会分不清方法区和永久代的关系,为此我也查阅了文献。 - -#### 2.5.1 方法区和永久代的关系 - -> 《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 **方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。** 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。 - -#### 2.5.2 常用参数 - -JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小 - -```java --XX:PermSize=N //方法区 (永久代) 初始大小 --XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen -``` - -相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。 - -JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。 - -下面是一些常用参数: - -```java --XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小) --XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小 -``` - -与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。 - -#### 2.5.3 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢? - -1. 整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。 ->当你元空间溢出时会得到如下错误: `java.lang.OutOfMemoryError: MetaSpace` - -你可以使用 `-XX:MaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。`-XX:MetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。 - -2. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 `MaxPermSize` 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。 - -3. 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。 - -### 2.6 运行时常量池 - -运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用) - -既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。 - -~~**JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。**~~ - -> 修正([issue747](https://github.com/Snailclimb/JavaGuide/issues/747),[reference](https://blog.csdn.net/q5706503/article/details/84640762)): -> -> 1. **JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代** -> 2. **JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代** 。 -> 3. **JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)** -> -> -> - -相关问题:JVM 常量池中存储的是对象还是引用呢?: https://www.zhihu.com/question/57109429/answer/151717241 by RednaxelaFX - - -### 2.7 直接内存 - -**直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。** - -JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**通道(Channel)** 与**缓存区(Buffer)** 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为**避免了在 Java 堆和 Native 堆之间来回复制数据**。 - -本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。 - - -## 三 HotSpot 虚拟机对象探秘 -通过上面的介绍我们大概知道了虚拟机的内存情况,下面我们来详细的了解一下 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。 - -### 3.1 对象的创建 -下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。 -![Java创建对象的过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Java创建对象的过程.png) - -#### Step1:类加载检查 - - 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 - -#### Step2:分配内存 - -在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。 - - -**内存分配的两种方式:(补充内容,需要掌握)** - -选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的 - -![内存分配的两种方式](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/内存分配的两种方式.png) - -**内存分配并发问题(补充内容,需要掌握)** - -在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全: - -- **CAS+失败重试:** CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。**虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。** -- **TLAB:** 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配 - -#### Step3:初始化零值 - -内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 - -#### Step4:设置对象头 - -初始化零值完成之后,**虚拟机要对对象进行必要的设置**,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 **这些信息存放在对象头中。** 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 - -#### Step5:执行 init 方法 - - 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,`` 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 `` 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 - - -### 3.2 对象的内存布局 - -在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:**对象头**、**实例数据**和**对齐填充**。 - -**Hotspot 虚拟机的对象头包括两部分信息**,**第一部分用于存储对象自身的运行时数据**(哈希码、GC 分代年龄、锁状态标志等等),**另一部分是类型指针**,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 - -**实例数据部分是对象真正存储的有效信息**,也是在程序中所定义的各种类型的字段内容。 - -**对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。** 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 - -### 3.3 对象的访问定位 -建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有**①使用句柄**和**②直接指针**两种: - -1. **句柄:** 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; - - ![对象的访问定位-使用句柄](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/对象的访问定位-使用句柄.png) - -2. **直接指针:** 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。 - -![对象的访问定位-直接指针](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/对象的访问定位-直接指针.png) - -**这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。** - - -## 四 重点补充内容 - -### 4.1 String 类和常量池 - -**String 对象的两种创建方式:** - -```java -String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd""; -String str2 = new String("abcd");//堆中创建一个新的对象 -String str3 = new String("abcd");//堆中创建一个新的对象 -System.out.println(str1==str2);//false -System.out.println(str2==str3);//false -``` - -这两种不同的创建方法是有差别的。 - -- 第一种方式是在常量池中拿对象; -- 第二种方式是直接在堆内存空间创建一个新的对象。 - -记住一点:**只要使用 new 方法,便需要创建新的对象。** - -再给大家一个图应该更容易理解,图片来源:: - -![String-Pool-Java](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3String-Pool-Java1-450x249.png) - -**String 类型的常量池比较特殊。它的主要使用方法有两种:** - -- 直接使用双引号声明出来的 String 对象会直接存储在常量池中。 -- 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。 - -```java - String s1 = new String("计算机"); - String s2 = s1.intern(); - String s3 = "计算机"; - System.out.println(s2);//计算机 - System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象, - System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象 -``` -**字符串拼接:** - -```java - String str1 = "str"; - String str2 = "ing"; - - String str3 = "str" + "ing";//常量池中的对象 - String str4 = str1 + str2; //在堆上创建的新的对象 - String str5 = "string";//常量池中的对象 - System.out.println(str3 == str4);//false - System.out.println(str3 == str5);//true - System.out.println(str4 == str5);//false -``` -![字符串拼接](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/字符串拼接-常量池2.png) - -尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。 -### 4.2 String s1 = new String("abc");这句话创建了几个字符串对象? - -**将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。** - -**验证:** - -```java - String s1 = new String("abc");// 堆内存的地址值 - String s2 = "abc"; - System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。 - System.out.println(s1.equals(s2));// 输出 true -``` - -**结果:** - -``` -false -true -``` - -### 4.3 8 种基本类型的包装类和常量池 - -**Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。** 为啥把缓存设置为[-128,127]区间?([参见issue/461](https://github.com/Snailclimb/JavaGuide/issues/461))性能和资源之间的权衡。 - -```java -public static Boolean valueOf(boolean b) { - return (b ? TRUE : FALSE); -} -``` - -```java -private static class CharacterCache { - private CharacterCache(){} - - static final Character cache[] = new Character[127 + 1]; - static { - for (int i = 0; i < cache.length; i++) - cache[i] = new Character((char)i); - } -} -``` - -两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。** - -```java - Integer i1 = 33; - Integer i2 = 33; - System.out.println(i1 == i2);// 输出 true - Integer i11 = 333; - Integer i22 = 333; - System.out.println(i11 == i22);// 输出 false - Double i3 = 1.2; - Double i4 = 1.2; - System.out.println(i3 == i4);// 输出 false -``` - -**Integer 缓存源代码:** - -```java -/** -*此方法将始终缓存-128 到 127(包括端点)范围内的值,并可以缓存此范围之外的其他值。 -*/ - public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); - } - -``` - -**应用场景:** -1. Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。 -2. Integer i1 = new Integer(40);这种情况下会创建新的对象。 - -```java - Integer i1 = 40; - Integer i2 = new Integer(40); - System.out.println(i1==i2);//输出 false -``` -**Integer 比较更丰富的一个例子:** - -```java - Integer i1 = 40; - Integer i2 = 40; - Integer i3 = 0; - Integer i4 = new Integer(40); - Integer i5 = new Integer(40); - Integer i6 = new Integer(0); - - System.out.println("i1=i2 " + (i1 == i2)); - System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); - System.out.println("i1=i4 " + (i1 == i4)); - System.out.println("i4=i5 " + (i4 == i5)); - System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); - System.out.println("40=i5+i6 " + (40 == i5 + i6)); -``` - -结果: - -``` -i1=i2 true -i1=i2+i3 true -i1=i4 false -i4=i5 false -i4=i5+i6 true -40=i5+i6 true -``` - -解释: - -语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。 - -## 参考 - -- 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版》 -- 《实战 java 虚拟机》 -- -- -- -- -- 深入解析String#intern - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源[公众号](#公众号)后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" b/docs/java/jvm/class-file-structure.md similarity index 64% rename from "docs/java/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" rename to docs/java/jvm/class-file-structure.md index fe282352150..b5a2611397e 100644 --- "a/docs/java/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" +++ b/docs/java/jvm/class-file-structure.md @@ -1,23 +1,10 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 +--- +category: Java +tag: + - JVM +--- - - -- [类文件结构](#类文件结构) - - [一 概述](#一-概述) - - [二 Class 文件结构总结](#二-class-文件结构总结) - - [2.1 魔数](#21-魔数) - - [2.2 Class 文件版本](#22-class-文件版本) - - [2.3 常量池](#23-常量池) - - [2.4 访问标志](#24-访问标志) - - [2.5 当前类索引,父类索引与接口索引集合](#25-当前类索引父类索引与接口索引集合) - - [2.6 字段表集合](#26-字段表集合) - - [2.7 方法表集合](#27-方法表集合) - - [2.8 属性表集合](#28-属性表集合) - - [参考](#参考) - - - -# 类文件结构 +# 类文件结构详解 ## 一 概述 @@ -27,11 +14,13 @@ Clojure(Lisp 语言的一种方言)、Groovy、Scala 等语言都是运行 ![java虚拟机](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktop类文件结构概览.png) -**可以说`.class`文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。** +可以说`.class`文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。 ## 二 Class 文件结构总结 -根据 Java 虚拟机规范,类文件由单个 ClassFile 结构组成: +根据 Java 虚拟机规范,Class 文件通过 `ClassFile` 定义,有点类似 C 语言的结构体。 + +`ClassFile` 的结构如下: ```java ClassFile { @@ -46,7 +35,7 @@ ClassFile { u2 interfaces_count;//接口 u2 interfaces[interfaces_count];//一个类可以实现多个接口 u2 fields_count;//Class 文件的字段属性 - field_info fields[fields_count];//一个类会可以有个字段 + field_info fields[fields_count];//一个类会可以有多个字段 u2 methods_count;//Class 文件的方法数量 method_info methods[methods_count];//一个类可以有个多个方法 u2 attributes_count;//此类的属性表中的属性数 @@ -54,53 +43,61 @@ ClassFile { } ``` -下面详细介绍一下 Class 文件结构涉及到的一些组件。 +通过分析 `ClassFile` 的内容,我们便可以知道 class 文件的组成。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/16d5ec47609818fc.jpeg) + +下面这张图是通过 IDEA 插件 `jclasslib` 查看的,你可以更直观看到 Class 文件结构。 -**Class文件字节码结构组织示意图** (之前在网上保存的,非常不错,原出处不明): +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/image-20210401170711475.png) -![类文件字节码结构组织示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/类文件字节码结构组织示意图.png) +使用 `jclasslib` 不光可以直观地查看某个类对应的字节码文件,还可以查看类的基本信息、常量池、接口、属性、函数等信息。 -### 2.1 魔数 +下面详细介绍一下 Class 文件结构涉及到的一些组件。 + +### 2.1 魔数(Magic Number) ```java u4 magic; //Class 文件的标志 ``` -每个 Class 文件的头四个字节称为魔数(Magic Number),它的唯一作用是**确定这个文件是否为一个能被虚拟机接收的 Class 文件**。 +每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是**确定这个文件是否为一个能被虚拟机接收的 Class 文件**。 程序设计者很多时候都喜欢用一些特殊的数字表示固定的文件类型或者其它特殊的含义。 -### 2.2 Class 文件版本 +### 2.2 Class 文件版本号(Minor&Major Version) ```java u2 minor_version;//Class 的小版本号 u2 major_version;//Class 的大版本号 ``` -紧接着魔数的四个字节存储的是 Class 文件的版本号:第五和第六是**次版本号**,第七和第八是**主版本号**。 +紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 位是**次版本号**,第 7 和第 8 位是**主版本号**。 + +每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 `javap -v` 命令来快速查看 Class 文件的版本号信息。 高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。 -### 2.3 常量池 +### 2.3 常量池(Constant Pool) ```java u2 constant_pool_count;//常量池的数量 cp_info constant_pool[constant_pool_count-1];//常量池 ``` -紧接着主次版本号之后的是常量池,常量池的数量是 constant_pool_count-1(**常量池计数器是从1开始计数的,将第0项常量空出来是有特殊考虑的,索引值为0代表“不引用任何一个常量池项”**)。 +紧接着主次版本号之后的是常量池,常量池的数量是 `constant_pool_count-1`(**常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”**)。 -常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量: +常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量: -- 类和接口的全限定名 -- 字段的名称和描述符 +- 类和接口的全限定名 +- 字段的名称和描述符 - 方法的名称和描述符 -常量池中每一项常量都是一个表,这14种表有一个共同的特点:**开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型.** +常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:**开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型.** | 类型 | 标志(tag) | 描述 | | :------------------------------: | :---------: | :--------------------: | -| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 | +| CONSTANT_utf8_info | 1 | UTF-8 编码的字符串 | | CONSTANT_Integer_info | 3 | 整形字面量 | | CONSTANT_Float_info | 4 | 浮点型字面量 | | CONSTANT_Long_info | 5 | 长整型字面量 | @@ -115,11 +112,11 @@ ClassFile { | CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | | CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | -`.class` 文件可以通过`javap -v class类名` 指令来看一下其常量池中的信息(`javap -v class类名-> temp.txt` :将结果输出到 temp.txt 文件)。 +`.class` 文件可以通过`javap -v class类名` 指令来看一下其常量池中的信息(`javap -v class类名-> temp.txt` :将结果输出到 temp.txt 文件)。 -### 2.4 访问标志 +### 2.4 访问标志(Access Flags) -在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。 +在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 `public` 或者 `abstract` 类型,如果是类的话是否声明为 `final` 等等。 类访问和属性修饰符: @@ -138,7 +135,7 @@ public class Employee { ![查看类的访问标志](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/查看类的访问标志.png) -### 2.5 当前类索引,父类索引与接口索引集合 +### 2.5 当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合 ```java u2 this_class;//当前类 @@ -147,11 +144,11 @@ public class Employee { u2 interfaces[interfaces_count];//一个类可以实现多个接口 ``` -**类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 `java.lang.Object` 之外,所有的 java 类都有父类,因此除了 `java.lang.Object` 外,所有 Java 类的父类索引都不为 0。** +类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 `java.lang.Object` 之外,所有的 java 类都有父类,因此除了 `java.lang.Object` 外,所有 Java 类的父类索引都不为 0。 -**接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按`implents`(如果这个类本身是接口的话则是`extends`) 后的接口顺序从左到右排列在接口索引集合中。** +接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 `implements` (如果这个类本身是接口的话则是`extends`) 后的接口顺序从左到右排列在接口索引集合中。 -### 2.6 字段表集合 +### 2.6 字段表集合(Fields) ```java u2 fields_count;//Class 文件的字段的个数 @@ -164,7 +161,7 @@ public class Employee { ![字段表的结构 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/字段表的结构.png) -- **access_flags:** 字段的作用域(`public` ,`private`,`protected`修饰符),是实例变量还是类变量(`static`修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。 +- **access_flags:** 字段的作用域(`public` ,`private`,`protected`修饰符),是实例变量还是类变量(`static`修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。 - **name_index:** 对常量池的引用,表示的字段的名称; - **descriptor_index:** 对常量池的引用,表示字段和方法的描述符; - **attributes_count:** 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数; @@ -172,20 +169,20 @@ public class Employee { 上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中常量来描述。 -**字段的 access_flags 的取值:** +**字段的 access_flag 的取值:** -![字段的access_flags的取值](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/字段的access_flags的取值.png) +![字段的 access_flag 的取值](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/JVM/image-20201031084342859.png) -### 2.7 方法表集合 +### 2.7 方法表集合(Methods) ```java u2 methods_count;//Class 文件的方法的数量 method_info methods[methods_count];//一个类可以有个多个方法 ``` -methods_count 表示方法的数量,而 method_info 表示的方法表。 +methods_count 表示方法的数量,而 method_info 表示方法表。 -Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。 +Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。 **method_info(方法表的) 结构:** @@ -193,11 +190,11 @@ Class 文件存储格式中对方法的描述与对字段的描述几乎采用 **方法表的 access_flag 取值:** -![方法表的 access_flag 取值](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/方法表的access_flag的所有标志位.png) +![方法表的 access_flag 取值](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/JVM/image-20201031084248965.png) 注意:因为`volatile`修饰符和`transient`修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了`synchronized`、`native`、`abstract`等关键字修饰方法,所以也就多了这些关键字对应的标志。 -### 2.8 属性表集合 +### 2.8 属性表集合(Attributes) ```java u2 attributes_count;//此类的属性表中的属性数 @@ -211,14 +208,4 @@ Class 文件存储格式中对方法的描述与对字段的描述几乎采用 - - - -- 《实战 Java 虚拟机》 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源[公众号](#公众号)后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) +- 《实战 Java 虚拟机》 \ No newline at end of file diff --git a/docs/java/jvm/class-loading-process.md b/docs/java/jvm/class-loading-process.md new file mode 100644 index 00000000000..fbe1b161d68 --- /dev/null +++ b/docs/java/jvm/class-loading-process.md @@ -0,0 +1,112 @@ +--- +category: Java +tag: + - JVM +--- + + +# 类加载过程详解 + +## 类的生命周期 + +一个类的完整生命周期如下: + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/类加载过程-完善.png) + +## 类加载过程 + +Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢? + +系统加载 Class 类型的文件主要三步:**加载->连接->初始化**。连接过程又可分为三步:**验证->准备->解析**。 + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/类加载过程.png) + +详见:[jvm规范5.4](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.4) 。 + +![](https://img-blog.csdnimg.cn/20210607102244508.png) + +### 加载 + +类加载过程的第一步,主要完成下面 3 件事情: + +1. 通过全类名获取定义此类的二进制字节流 +2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构 +3. 在内存中生成一个代表该类的 `Class` 对象,作为方法区这些数据的访问入口 + +虚拟机规范上面这 3 点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 `ZIP` 包中读取(日后出现的 `JAR`、`EAR`、`WAR` 格式的基础)、其他文件生成(典型应用就是 `JSP`)等等。 + +**一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 `loadClass()` 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。** + +类加载器、双亲委派模型也是非常重要的知识点,这部分内容会在后面的文章中单独介绍到。 + +加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。 + +### 验证 + +![验证阶段示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/验证阶段.png) + +### 准备 + +**准备阶段是正式为类变量分配内存并设置类变量初始值的阶段**,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意: + +1. 这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 `static` 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。 +2. 从概念上讲,类变量所使用的内存都应当在 **方法区** 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。相关阅读:[《深入理解Java虚拟机(第3版)》勘误#75](https://github.com/fenixsoft/jvm_book/issues/75) +3. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了`public static int value=111` ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字`public static final int value=111` ,那么准备阶段 value 的值就被赋值为 111。 + +**基本数据类型的零值** : (图片来自《深入理解 Java 虚拟机》第 3 版 7.33 ) + + + +![基本数据类型的零值](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/基本数据类型的零值.png) + +### 解析 + +解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。 + +符号引用就是一组符号来描述目标,可以是任何字面量。**直接引用**就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。 + +综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。 + +### 初始化 + +初始化阶段是执行初始化方法 ` ()`方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。 + +> 说明: ` ()`方法是编译之后自动生成的。 + +对于` ()` 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 ` ()` 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个进程阻塞,并且这种阻塞很难被发现。 + +对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类): + +1. 当遇到 `new` 、 `getstatic`、`putstatic` 或 `invokestatic` 这 4 条直接码指令时,比如 `new` 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。 + - 当 jvm 执行 `new` 指令时会初始化类。即当程序创建一个类的实例对象。 + - 当 jvm 执行 `getstatic` 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。 + - 当 jvm 执行 `putstatic` 指令时会初始化类。即程序给类的静态变量赋值。 + - 当 jvm 执行 `invokestatic` 指令时会初始化类。即程序调用类的静态方法。 +2. 使用 `java.lang.reflect` 包的方法对类进行反射调用时如 `Class.forname("...")`, `newInstance()` 等等。如果类没初始化,需要触发其初始化。 +3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。 +4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 `main` 方法的那个类),虚拟机会先初始化这个类。 +5. `MethodHandle` 和 `VarHandle` 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, + 就必须先使用 `findStaticVarHandle` 来初始化要调用的类。 +6. **「补充,来自[issue745](https://github.com/Snailclimb/JavaGuide/issues/745)」** 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。 + +## 卸载 + +> 卸载这部分内容来自 [issue#662](https://github.com/Snailclimb/JavaGuide/issues/662)由 **[guang19](https://github.com/guang19)** 补充完善。 + +卸载类即该类的 Class 对象被 GC。 + +卸载类需要满足 3 个要求: + +1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。 +2. 该类没有在其他任何地方被引用 +3. 该类的类加载器的实例已被 GC + +所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。 + +只要想通一点就好了,jdk 自带的 `BootstrapClassLoader`, `ExtClassLoader`, `AppClassLoader` 负责加载 jdk 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。 + +**参考** + +- 《深入理解 Java 虚拟机》 +- 《实战 Java 虚拟机》 +- diff --git "a/docs/java/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" b/docs/java/jvm/classloader.md similarity index 50% rename from "docs/java/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" rename to docs/java/jvm/classloader.md index 00a890475f1..93cd907e45a 100644 --- "a/docs/java/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" +++ b/docs/java/jvm/classloader.md @@ -1,44 +1,34 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 +--- +category: Java +tag: + - JVM +--- - - -- [回顾一下类加载过程](#回顾一下类加载过程) -- [类加载器总结](#类加载器总结) -- [双亲委派模型](#双亲委派模型) - - [双亲委派模型介绍](#双亲委派模型介绍) - - [双亲委派模型实现源码分析](#双亲委派模型实现源码分析) - - [双亲委派模型的好处](#双亲委派模型的好处) - - [如果我们不想要双亲委派模型怎么办?](#如果我们不想要双亲委派模型怎么办) -- [自定义类加载器](#自定义类加载器) -- [推荐](#推荐) - - - -> 公众号JavaGuide 后台回复关键字“1”,免费获取JavaGuide配套的Java工程师必备学习资源(文末有公众号二维码)。 +# 类加载器详解 ## 回顾一下类加载过程 -类加载过程:**加载->连接->初始化**。连接过程又可分为三步:**验证->准备->解析**。 +类加载过程:**加载->连接->初始化**。连接过程又可分为三步:**验证->准备->解析**。 ![类加载过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/类加载过程.png) -一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 `loadClass()` 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。 +一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去自定义类加载器去控制字节流的获取方式(重写一个类加载器的 `loadClass()` 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。 -所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存。 +所有的类都由类加载器加载,加载的作用就是将 `.class`文件加载到内存。 ## 类加载器总结 JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自`java.lang.ClassLoader`: -1. **BootstrapClassLoader(启动类加载器)** :最顶层的加载类,由C++实现,负责加载 `%JAVA_HOME%/lib`目录下的jar包和类或者或被 `-Xbootclasspath`参数指定的路径中的所有类。 -2. **ExtensionClassLoader(扩展类加载器)** :主要负责加载目录 `%JRE_HOME%/lib/ext` 目录下的jar包和类,或被 `java.ext.dirs` 系统变量所指定的路径下的jar包。 -3. **AppClassLoader(应用程序类加载器)** :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。 +1. **BootstrapClassLoader(启动类加载器)** :最顶层的加载类,由 C++实现,负责加载 `%JAVA_HOME%/lib`目录下的 jar 包和类或者被 `-Xbootclasspath`参数指定的路径中的所有类。 +2. **ExtensionClassLoader(扩展类加载器)** :主要负责加载 `%JRE_HOME%/lib/ext` 目录下的 jar 包和类,或被 `java.ext.dirs` 系统变量所指定的路径下的 jar 包。 +3. **AppClassLoader(应用程序类加载器)** :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。 ## 双亲委派模型 ### 双亲委派模型介绍 -每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 **双亲委派模型** 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 `loadClass()` 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 `BootstrapClassLoader` 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 `BootstrapClassLoader` 作为父类加载器。 +每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 **双亲委派模型** 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 `loadClass()` 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 `BootstrapClassLoader` 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加载器 `BootstrapClassLoader` 作为父类加载器。 ![ClassLoader](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/classloader_WPS图片.png) @@ -62,19 +52,19 @@ The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1 The GrandParent of ClassLodarDemo's ClassLoader is null ``` -`AppClassLoader`的父类加载器为`ExtClassLoader` -`ExtClassLoader`的父类加载器为null,**null并不代表`ExtClassLoader`没有父类加载器,而是 `BootstrapClassLoader`** 。 +`AppClassLoader`的父类加载器为`ExtClassLoader`, +`ExtClassLoader`的父类加载器为 null,**null 并不代表`ExtClassLoader`没有父类加载器,而是 `BootstrapClassLoader`** 。 -其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 Mother ClassLoader 和一个 Father ClassLoader 。另外,类加载器之间的“父子”关系也不是通过继承来体现的,是由“优先级”来决定。官方API文档对这部分的描述如下: +其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 Mother ClassLoader 和一个 Father ClassLoader 。另外,类加载器之间的“父子”关系也不是通过继承来体现的,是由“优先级”来决定。官方 API 文档对这部分的描述如下: ->The Java platform uses a delegation model for loading classes. **The basic idea is that every class loader has a "parent" class loader.** When loading a class, a class loader first "delegates" the search for the class to its parent class loader before attempting to find the class itself. +> The Java platform uses a delegation model for loading classes. **The basic idea is that every class loader has a "parent" class loader.** When loading a class, a class loader first "delegates" the search for the class to its parent class loader before attempting to find the class itself. ### 双亲委派模型实现源码分析 双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 `java.lang.ClassLoader` 的 `loadClass()` 中,相关代码如下所示。 ```java -private final ClassLoader parent; +private final ClassLoader parent; protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { @@ -92,7 +82,7 @@ protected Class loadClass(String name, boolean resolve) } catch (ClassNotFoundException e) { //抛出异常说明父类加载器无法完成加载请求 } - + if (c == null) { long t1 = System.nanoTime(); //自己尝试加载 @@ -114,11 +104,13 @@ protected Class loadClass(String name, boolean resolve) ### 双亲委派模型的好处 -双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 `java.lang.Object` 类的话,那么程序运行的时候,系统就会出现多个不同的 `Object` 类。 +双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 `java.lang.Object` 类的话,那么程序运行的时候,系统就会出现多个不同的 `Object` 类。 ### 如果我们不想用双亲委派模型怎么办? -为了避免双亲委托机制,我们可以自己定义一个类加载器,然后重写 `loadClass()` 即可。 +~~为了避免双亲委托机制,我们可以自己定义一个类加载器,然后重写 `loadClass()` 即可。~~ + +**🐛 修正(参见:[issue871](https://github.com/Snailclimb/JavaGuide/issues/871) )** :自定义加载器的话,需要继承 `ClassLoader` 。如果我们不想打破双亲委派模型,就重写 `ClassLoader` 类中的 `findClass()` 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 `loadClass()` 方法 ## 自定义类加载器 @@ -129,14 +121,3 @@ protected Class loadClass(String name, boolean resolve) - - - - -### 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源[公众号](#公众号)后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - diff --git "a/docs/java/jvm/JDK\347\233\221\346\216\247\345\222\214\346\225\205\351\232\234\345\244\204\347\220\206\345\267\245\345\205\267\346\200\273\347\273\223.md" b/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md similarity index 81% rename from "docs/java/jvm/JDK\347\233\221\346\216\247\345\222\214\346\225\205\351\232\234\345\244\204\347\220\206\345\267\245\345\205\267\346\200\273\347\273\223.md" rename to docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md index ebc166dcbb0..37e2c0d320e 100644 --- "a/docs/java/jvm/JDK\347\233\221\346\216\247\345\222\214\346\225\205\351\232\234\345\244\204\347\220\206\345\267\245\345\205\267\346\200\273\347\273\223.md" +++ b/docs/java/jvm/jdk-monitoring-and-troubleshooting-tools.md @@ -1,24 +1,8 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [JDK 监控和故障处理工具总结](#jdk-监控和故障处理工具总结) - - [JDK 命令行工具](#jdk-命令行工具) - - [`jps`:查看所有 Java 进程](#jps查看所有-java-进程) - - [`jstat`: 监视虚拟机各种运行状态信息](#jstat-监视虚拟机各种运行状态信息) - - [` jinfo`: 实时地查看和调整虚拟机各项参数](#-jinfo-实时地查看和调整虚拟机各项参数) - - [`jmap`:生成堆转储快照](#jmap生成堆转储快照) - - [**`jhat`**: 分析 heapdump 文件](#jhat-分析-heapdump-文件) - - [**`jstack`** :生成虚拟机当前时刻的线程快照](#jstack-生成虚拟机当前时刻的线程快照) - - [JDK 可视化分析工具](#jdk-可视化分析工具) - - [JConsole:Java 监视与管理控制台](#jconsolejava-监视与管理控制台) - - [连接 Jconsole](#连接-jconsole) - - [查看 Java 程序概况](#查看-java-程序概况) - - [内存监控](#内存监控) - - [线程监控](#线程监控) - - [Visual VM:多合一故障处理工具](#visual-vm多合一故障处理工具) - - +--- +category: Java +tag: + - JVM +--- # JDK 监控和故障处理工具总结 @@ -26,12 +10,12 @@ 这些命令在 JDK 安装目录下的 bin 目录下: -- **`jps`** (JVM Process Status): 类似 UNIX 的 `ps` 命令。用户查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息; -- **`jstat`**( JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据; -- **`jinfo`** (Configuration Info for Java) : Configuration Info forJava,显示虚拟机配置信息; -- **`jmap`** (Memory Map for Java) :生成堆转储快照; -- **`jhat`** (JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果; -- **`jstack`** (Stack Trace for Java):生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。 +- **`jps`** (JVM Process Status): 类似 UNIX 的 `ps` 命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息; +- **`jstat`**(JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据; +- **`jinfo`** (Configuration Info for Java) : Configuration Info for Java,显示虚拟机配置信息; +- **`jmap`** (Memory Map for Java) : 生成堆转储快照; +- **`jhat`** (JVM Heap Dump Browser) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果; +- **`jstack`** (Stack Trace for Java) : 生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。 ### `jps`:查看所有 Java 进程 @@ -90,7 +74,7 @@ jstat -