diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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 index c176e2c2..e30d3cc9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 +> 关于 JavaGuide 的相关介绍请看:[《从编程小白到做了一个接近 90k 点赞的一个国产 Java 开源项目》](https://www.yuque.com/snailclimb/dr6cvl/mr44yt#vu3ok) +> +> 准备面试的小伙伴可以考虑面试专版:[《Java 面试进阶指南》](https://xiaozhuanlan.com/javainterview?rel=javaguide) ,欢迎加入[我的星球](https://wx.zsxq.com/dweb2/index/group/48418884588288)获取更多实用干货。 +> +> 阿里云最近在做活动,服务器不到 10 元/月,小伙伴们搭建一个网站提高简历质量。支持国内开源做的比较好的公司![点击此链接直达活动首页。](https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=hf47liqn) +> +> 项目的发展离不开你的支持,如果 JavaGuide 帮助到了你找到自己满意的 offer,那就[请作者喝杯咖啡吧](https://www.yuque.com/snailclimb/dr6cvl/mr44yt#vu3ok)☕!我会继续将项目完善下去!加油! -[推荐一下:阿里云高性能服务器,1核1g最低89,不限性能。](https://www.aliyun.com/minisite/goods?userCode=hf47liqn) +如果 Github 访问速度比较慢或者图片无法刷新出来的话,可以转移到[码云](https://gitee.com/SnailClimb/JavaGuide)查看,或者[在线阅读](https://snailclimb.gitee.io/javaguide)。**如果你要提交 issue 或者 pr 的话请到 [Github](https://github.com/Snailclimb/JavaGuide) 提交。** -少部分原创文章更新在了知识星球,关于我为什么要弄知识星球,请看这里:**[犹豫了很久,还是做了一个很久没敢做的事情](https://javaguide.cn/2019/01/02/chat/%E5%81%9A%E4%BA%86%E4%B8%80%E4%B8%AA%E5%BE%88%E4%B9%85%E6%B2%A1%E6%95%A2%E5%81%9A%E7%9A%84%E4%BA%8B%E6%83%85/)** ,优惠卷地址:[https://t.zsxq.com/iIqZBUR](https://t.zsxq.com/iIqZBUR) 。 +《JavaGuide 面试突击版》PDF 版本+3 本 PDF Java 学习手册,在公众号 **[JavaGuide](#公众号)** 后台回复“**面试突击**”即可获取。 + +如要进群或者请教问题,请[联系我](#联系我) (备注来自 Github。请直入问题,工作时间不回复)。 + +**开始阅读之前必看** :[完结撒花!JavaGuide 面试突击版来啦!](./docs/javaguide面试突击版.md) 。

@@ -12,7 +22,6 @@

阅读 - 微信群 公众号 公众号 投稿 @@ -20,58 +29,82 @@

Sponsor

-

- - - -

-推荐使用 https://snailclimb.gitee.io/javaguide 在线阅读,在线阅读内容本仓库同步一致。这种方式阅读的优势在于:阅读体验会更好。 + + + + + + + + + + +
+ + + + + +
+ + +
## 目录 +- [目录](#目录) - [Java](#java) - - [基础](#基础) - - [容器](#容器) - - [并发](#并发) - - [JVM](#jvm) - - [I/O](#io) - - [Java 8](#java-8) - - [优雅 Java 代码必备实践(Java编程规范)](#优雅-java-代码必备实践java编程规范) + - [基础](#基础) + - [容器](#容器) + - [并发](#并发) + - [JVM](#jvm) + - [其他](#其他) - [网络](#网络) - [操作系统](#操作系统) - - [Linux相关](#linux相关) + - [Linux](#linux) - [数据结构与算法](#数据结构与算法) - - [数据结构](#数据结构) - - [算法](#算法) + - [数据结构](#数据结构) + - [算法](#算法) - [数据库](#数据库) - - [MySQL](#mysql) - - [Redis](#redis) - - [数据库扩展](#数据库扩展) + - [MySQL](#mysql) + - [Redis](#redis) - [系统设计](#系统设计) - - [常用框架(Spring,SpringBoot,MyBatis)](#常用框架) - - [数据通信/中间件(消息队列、RPC ... )](#数据通信中间件) - - [权限认证](#权限认证) - - [分布式 & 微服务](#分布式--微服务) - - [API 网关](#api-网关) - - [配置中心](#配置中心) - - [唯一 id 生成](#唯一-id-生成) - - [服务治理:服务注册与发现、服务路由控制](#服务治理服务注册与发现服务路由控制) - - [大型网站架构](#大型网站架构) - - [性能测试](#性能测试) - - [高并发](#高并发) - - [高可用](#高可用) - - [设计模式(工厂模式、单例模式 ... )](#设计模式) + - [必知](#必知) + - [常用框架](#常用框架) + - [Spring/SpringBoot](#springspringboot) + - [MyBatis](#mybatis) + - [Netty](#netty) + - [认证授权](#认证授权) + - [JWT](#jwt) + - [SSO(单点登录)](#sso单点登录) + - [分布式](#分布式) + - [分布式搜索引擎](#分布式搜索引擎) + - [RPC](#rpc) + - [消息队列](#消息队列) + - [API 网关](#api-网关) + - [分布式 id](#分布式id) + - [分布式限流](#分布式限流) + - [分布式接口幂等性](#分布式接口幂等性) + - [ZooKeeper](#zookeeper) + - [其他](#其他-1) + - [数据库扩展](#数据库扩展) + - [大型网站架构](#大型网站架构) + - [性能测试](#性能测试) + - [高并发](#高并发) + - [高可用](#高可用) + - [微服务](#微服务) + - [Spring Cloud](#spring-cloud) +- [必会工具](#必会工具) + - [Git](#git) + - [Docker](#docker) + - [其他](#其他-2) - [面试指南](#面试指南) - - [备战面试](#备战面试) - - [面经](#面经) -- [Java学习常见问题汇总](#java学习常见问题汇总) -- [工具](#工具) - - [Git](#git) - - [Docker](#Docker) +- [Java 学习常见问题汇总](#java学习常见问题汇总) - [资源](#资源) - - [书单](#书单) - - [Github榜单](#Github榜单) + - [Java 程序员必备书单](#java程序员必备书单) + - [实战项目推荐](#实战项目推荐) + - [Github](#github) - [待办](#待办) - [说明](#说明) @@ -81,82 +114,82 @@ **基础知识系统总结:** -* **[Java 基础知识回顾](docs/java/Java基础知识.md)** -* **[Java 基础知识疑难点/易错点](docs/java/Java疑难点.md)** -* **[一些重要的Java程序设计题](docs/java/Java程序设计题.md)** -* [J2EE 基础知识回顾](docs/java/J2EE基础知识.md) +1. **[Java 基础知识](docs/java/Java基础知识.md)** +2. **[Java 基础知识疑难点/易错点](docs/java/Java疑难点.md)** +3. [【选看】J2EE 基础知识](docs/java/J2EE基础知识.md) **重要知识点详解:** -- [用好Java中的枚举,真的没有那么简单!](docs/java/basis/用好Java中的枚举,真的没有那么简单!) -- [Java 常见关键字总结:final、static、this、super!](docs/java/basis/final、static、this、super.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) +4. [代理模式详解:静态代理+JDK/CGLIB 动态代理实战(动态代理和静态代理的区别?JDK 动态代理 和 CGLIB 动态代理的区别?)](docs/java/basic/java-proxy.md) + +**其他:** + +1. [JAD 反编译](docs/java/JAD反编译tricks.md) +2. [手把手教你定位常见 Java 性能问题](./docs/java/手把手教你定位常见Java性能问题.md) ### 容器 -**总结:** - -* **[Java容器常见面试题/知识点总结](docs/java/collection/Java集合框架常见面试题.md)** - -**源码学习:** - -* [ArrayList 源码学习](docs/java/collection/ArrayList.md) -* [LinkedList 源码学习](docs/java/collection/LinkedList.md) -* [HashMap(JDK1.8)源码学习](docs/java/collection/HashMap.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) 、[ConcurrentHashMap 源码](docs/java/collection/ConcurrentHashMap.md) ### 并发 +**[多线程学习指南](./docs/java/Multithread/多线程学习指南.md)** + **面试题总结:** -* **[Java 并发基础常见面试题总结](docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md)** -* **[Java 并发进阶常见面试题总结](docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md)** +1. **[Java 并发基础常见面试题总结](docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md)** +2. **[Java 并发进阶常见面试题总结](docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md)** -**必备知识点:** +**面试常问知识点:** -* [并发容器总结](docs/java/Multithread/并发容器总结.md) -* **[Java线程池学习总结](./docs/java/Multithread/java线程池学习总结.md)** -* [乐观锁与悲观锁](docs/essential-content-for-interview/面试必备之乐观锁与悲观锁.md) -* [JUC 中的 Atomic 原子类总结](docs/java/Multithread/Atomic.md) -* [AQS 原理以及 AQS 同步组件总结](docs/java/Multithread/AQS.md) +1. [并发容器总结](docs/java/Multithread/并发容器总结.md) +2. **线程池**:[Java 线程池学习总结](./docs/java/Multithread/java线程池学习总结.md)、[拿来即用的线程池最佳实践](./docs/java/Multithread/best-practice-of-threadpool.md) +3. [乐观锁与悲观锁](docs/essential-content-for-interview/面试必备之乐观锁与悲观锁.md) +4. [万字图文深度解析 ThreadLocal](docs/java/Multithread/ThreadLocal.md) +5. [JUC 中的 Atomic 原子类总结](docs/java/Multithread/Atomic.md) +6. [AQS 原理以及 AQS 同步组件总结](docs/java/Multithread/AQS.md) ### JVM -* **[一 Java内存区域](docs/java/jvm/Java内存区域.md)** -* **[二 JVM垃圾回收](docs/java/jvm/JVM垃圾回收.md)** -* [三 JDK 监控和故障处理工具](docs/java/jvm/JDK监控和故障处理工具总结.md) -* [四 类文件结构](docs/java/jvm/类文件结构.md) -* **[五 类加载过程](docs/java/jvm/类加载过程.md)** -* [六 类加载器](docs/java/jvm/类加载器.md) -* **[【待完成】八 最重要的 JVM 参数指南(翻译完善了一半)](docs/java/jvm/最重要的JVM参数指南.md)** -* [九 JVM 配置常用参数和常用 GC 调优策略](docs/java/jvm/GC调优参数.md) -* **[【加餐】大白话带你认识JVM](docs/java/jvm/[加餐]大白话带你认识JVM.md)** +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)** -### I/O +### 其他 -* [BIO,NIO,AIO 总结 ](docs/java/BIO-NIO-AIO.md) -* [Java IO 与 NIO系列文章](docs/java/Java%20IO与NIO.md) - -### 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) - -### 优雅 Java 代码必备实践(Java编程规范) - -* [Java 编程规范以及优雅 Java 代码实践总结](docs/java/Java编程规范.md) +1. **Linux IO** : [Linux IO](docs/java/Linux_IO.md) +2. **I/O** :[BIO,NIO,AIO 总结 ](docs/java/BIO-NIO-AIO.md) +3. **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) +4. **Java9~Java14** : [一文带你看遍 JDK9~14 的重要新特性!](./docs/java/jdk-new-features/new-features-from-jdk8-to-jdk14.md) +5. Java 编程规范:**[Java 编程规范以及优雅 Java 代码实践总结](docs/java/Java编程规范.md)** 、[告别编码 5 分钟,命名 2 小时!史上最全的 Java 命名规范参考!](docs/java/java-naming-conventions.md) +6. 设计模式 :[设计模式系列文章](docs/system-design/设计模式.md) ## 网络 -* [计算机网络常见面试题](docs/network/计算机网络.md) -* [计算机网络基础知识总结](docs/network/干货:计算机网络知识总结.md) -* [HTTPS中的TLS](docs/network/HTTPS中的TLS.md) +1. [计算机网络常见面试题](docs/network/计算机网络.md) +2. [计算机网络基础知识总结](docs/network/干货:计算机网络知识总结.md) ## 操作系统 -### Linux相关 +[最硬核的操作系统常见问题总结!](docs/operating-system/basis.md) -* [后端程序员必备的 Linux 基础知识](docs/operating-system/后端程序员必备的Linux基础知识.md) -* [Shell 编程入门](docs/operating-system/Shell.md) +### Linux + +- [后端程序员必备的 Linux 基础知识](docs/operating-system/linux.md) +- [Shell 编程入门](docs/operating-system/Shell.md) +- [我为什么从 Windows 转到 Linux?](docs/operating-system/完全使用GNU_Linux学习.md) +- [Linux IO 模型](docs/operating-system/Linux_IO.md) +- [Linux 性能分析工具合集](docs/operating-system/Linux性能分析工具合集.md) ## 数据结构与算法 @@ -167,116 +200,156 @@ ### 算法 -- [算法学习资源推荐](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) +- [硬核的算法学习书籍+资源推荐](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 -* **[【推荐】MySQL/数据库 知识点总结](docs/database/MySQL.md)** -* **[阿里巴巴开发手册数据库部分的一些最佳实践](docs/database/阿里巴巴开发手册数据库部分的一些最佳实践.md)** -* **[一千行MySQL学习笔记](docs/database/一千行MySQL命令.md)** -* [MySQL高性能优化规范建议](docs/database/MySQL高性能优化规范建议.md) -* [数据库索引总结](docs/database/MySQL%20Index.md) -* [事务隔离级别(图文详解)](docs/database/事务隔离级别(图文详解).md) -* [一条SQL语句在MySQL中如何执行的](docs/database/一条sql语句在mysql中如何执行的.md) +**总结:** + +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. [事务隔离级别(图文详解)]() +3. [一条 SQL 语句在 MySQL 中如何执行的](docs/database/一条sql语句在mysql中如何执行的.md) +4. **[关于数据库中如何存储时间的一点思考](docs/database/关于数据库存储时间的一点思考.md)** ### Redis -* [Redis 总结](docs/database/Redis/Redis.md) -* [Redlock分布式锁](docs/database/Redis/Redlock分布式锁.md) -* [如何做可靠的分布式锁,Redlock真的可行么](docs/database/Redis/如何做可靠的分布式锁,Redlock真的可行么.md) -* [几种常见的 Redis 集群以及使用场景](docs/database/Redis/redis集群以及应用场景.md) - -### 数据库扩展 - -待办...... +- [关于缓存的一些重要概念(Redis 前置菜)](docs/database/Redis/some-concepts-of-caching.md) +- [Redis 常见问题总结](docs/database/Redis/redis-all.md) ## 系统设计 +### 必知 + +1. **[RestFul API 简明教程](docs/system-design/restful-api.md)** +2. **[因为命名被 diss 无数次。Guide 简单聊聊编程最头疼的事情之一:命名](docs/system-design/naming.md)** + ### 常用框架 -#### Spring +#### Spring/SpringBoot -- [Spring 学习与面试](docs/system-design/framework/spring/Spring.md) -- **[Spring 常见问题总结](docs/system-design/framework/spring/SpringInterviewQuestions.md)** -- [Spring中 Bean 的作用域与生命周期](docs/system-design/framework/spring/SpringBean.md) -- [SpringMVC 工作原理详解](docs/system-design/framework/spring/SpringMVC-Principle.md) -- [Spring中都用到了那些设计模式?](docs/system-design/framework/spring/Spring-Design-Patterns.md) - -#### SpringBoot - -- **[SpringBoot 指南/常见面试题总结](https://github.com/Snailclimb/springboot-guide)** +1. **[Spring 常见问题总结](docs/system-design/framework/spring/SpringInterviewQuestions.md)** +2. **[SpringBoot 指南/常见面试题总结](https://github.com/Snailclimb/springboot-guide)** +3. **[Spring/Spring 常用注解总结!安排!](./docs/system-design/framework/spring/spring-annotations.md)** +4. **[Spring 事务总结](docs/system-design/framework/spring/spring-transaction.md)** +5. [Spring IoC 和 AOP 详解](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486938&idx=1&sn=c99ef0233f39a5ffc1b98c81e02dfcd4&chksm=cea24211f9d5cb07fa901183ba4d96187820713a72387788408040822ffb2ed575d28e953ce7&token=1666190828&lang=zh_CN#rd) +6. [Spring 中 Bean 的作用域与生命周期](docs/system-design/framework/spring/SpringBean.md) +7. [SpringMVC 工作原理详解](docs/system-design/framework/spring/SpringMVC-Principle.md) +8. [Spring 中都用到了那些设计模式?](docs/system-design/framework/spring/Spring-Design-Patterns.md) #### MyBatis -- [MyBatis常见面试题总结](docs/system-design/framework/mybatis/mybatis-interview.md) +- [MyBatis 常见面试题总结](docs/system-design/framework/mybatis/mybatis-interview.md) -### 数据通信/中间件 +#### Netty -- [数据通信(RESTful、RPC、消息队列)相关知识点总结](docs/system-design/data-communication/summary.md) +1. [剖析面试最常见问题之 Netty(上)](https://xiaozhuanlan.com/topic/4028536971) +2. [剖析面试最常见问题之 Netty(下)](https://xiaozhuanlan.com/topic/3985146207) + +### 认证授权 + +**[认证授权基础:搞清 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 和 Solr。 + +代办。 #### 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 入门](docs/system-design/data-communication/rabbitmq.md) -- [RocketMQ 入门](docs/system-design/data-communication/RocketMQ.md) -- [RocketMQ的几个简单问题与答案](docs/system-design/data-communication/RocketMQ-Questions.md) -- [Kafka入门看这一篇就够了](docs/system-design/data-communication/Kafka入门看这一篇就够了.md) -- [Kafka系统设计开篇-面试看这篇就够了](docs/system-design/data-communication/Kafka系统设计开篇-面试看这篇就够了.md) +消息队列在分布式系统中主要是为了解耦和削峰。相关阅读: **[消息队列总结](docs/system-design/data-communication/message-queue.md)** 。 -### 权限认证 +**RabbitMQ:** -- **[权限认证基础:区分Authentication,Authorization以及Cookie、Session、Token](docs/system-design/authority-certification/basis-of-authority-certification.md)** -- **[JWT 优缺点分析以及常见问题解决方案](docs/system-design/authority-certification/JWT-advantages-and-disadvantages.md)** -- **[适合初学者入门 Spring Security With JWT 的 Demo](https://github.com/Snailclimb/spring-security-jwt-guide)** +1. [RabbitMQ 入门](docs/system-design/data-communication/rabbitmq.md) -### 分布式 & 微服务 +**RocketMQ:** -- [分布式应该学什么](docs/system-design/website-architecture/分布式.md) +1. [RocketMQ 入门](docs/system-design/data-communication/RocketMQ.md) +2. [RocketMQ 的几个简单问题与答案](docs/system-design/data-communication/RocketMQ-Questions.md) -#### Spring Cloud +**Kafka:** -- [ 大白话入门 Spring Cloud](docs/system-design/micro-service/spring-cloud.md) +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 网关 网关主要用于请求转发、安全认证、协议转换、容灾。 -- [浅析如何设计一个亿级网关(API Gateway)](docs/system-design/micro-service/API网关.md) +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) -#### 唯一 id 生成 +#### 分布式限流 -- [分布式id生成方案总结](docs/system-design/micro-service/分布式id生成方案总结.md) +1. [限流算法有哪些?](docs/system-design/micro-service/limit-request.md) -#### 服务治理:服务注册与发现、服务路由控制 +#### 分布式接口幂等性 -**ZooKeeper:** +#### ZooKeeper > 前两篇文章可能有内容重合部分,推荐都看一遍。 -- [【入门】ZooKeeper 相关概念总结](docs/system-design/framework/ZooKeeper.md) -- [【进阶】Zookeeper 原理简单入门!](docs/system-design/framework/ZooKeeper-plus.md) -- [【拓展】ZooKeeper 数据模型和常见命令](docs/system-design/framework/ZooKeeper数据模型和常见命令.md) +1. [【入门】ZooKeeper 相关概念总结 01](docs/system-design/framework/zookeeper/zookeeper-intro.md) +2. [【进阶】ZooKeeper 相关概念总结 02](docs/system-design/framework/zookeeper/zookeeper-plus.md) +3. [【实战】ZooKeeper 实战](docs/system-design/framework/zookeeper/zookeeper-in-action.md) + +#### 其他 + +- 接口幂等性(代办):分布式系统必须要考虑接口的幂等性。 + +#### 数据库扩展 + +读写分离、分库分表。 + +代办..... ### 大型网站架构 - [8 张图读懂大型网站技术架构](docs/system-design/website-architecture/8%20张图读懂大型网站技术架构.md) -- [【面试精选】关于大型网站系统架构你不得不懂的10个问题](docs/system-design/website-architecture/关于大型网站系统架构你不得不懂的10个问题.md) +- [关于大型网站系统架构你不得不懂的 10 个问题](docs/system-design/website-architecture/关于大型网站系统架构你不得不懂的10个问题.md) #### 性能测试 @@ -288,134 +361,102 @@ #### 高可用 -- [如何设计一个高可用系统?要考虑哪些地方?](docs/system-design/website-architecture/如何设计一个高可用系统?要考虑哪些地方?.md) +高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的 。相关阅读: **《[如何设计一个高可用系统?要考虑哪些地方?](docs/system-design/website-architecture/如何设计一个高可用系统?要考虑哪些地方?.md)》** 。 -### 设计模式 +### 微服务 -- [设计模式系列文章](docs/system-design/设计模式.md) +#### Spring Cloud -## 面试指南 +- [ 大白话入门 Spring Cloud](docs/system-design/micro-service/spring-cloud.md) -### 备战面试 - -* **[【备战面试1】程序员的简历就该这样写](docs/essential-content-for-interview/PreparingForInterview/程序员的简历之道.md)** -* **[【备战面试2】初出茅庐的程序员该如何准备面试?](docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md)** -* **[【备战面试3】7个大部分程序员在面试前很关心的问题](docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md)** -* **[【备战面试4】Github上开源的Java面试/学习相关的仓库推荐](docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md)** -* **[【备战面试5】如果面试官问你“你有什么问题问我吗?”时,你该如何回答](docs/essential-content-for-interview/PreparingForInterview/面试官-你有什么问题要问我.md)** -* [【备战面试6】应届生面试最爱问的几道 Java 基础问题](docs/essential-content-for-interview/PreparingForInterview/应届生面试最爱问的几道Java基础问题.md) -* **[【备战面试6】美团面试常见问题总结(附详解答案)](docs/essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md)** -* **[【备战面试7】一些刁难的面试问题总结](https://xiaozhuanlan.com/topic/9056431872)** - -### 真实面试经历分析 - -- [我和阿里面试官的一次“邂逅”(附问题详解)](docs/essential-content-for-interview/real-interview-experience-analysis/alibaba-1.md) - -### 面经 - -- [5面阿里,终获offer(2018年秋招)](docs/essential-content-for-interview/BATJrealInterviewExperience/5面阿里,终获offer.md) -- [蚂蚁金服2019实习生面经总结(已拿口头offer)](docs/essential-content-for-interview/BATJrealInterviewExperience/蚂蚁金服实习生面经总结(已拿口头offer).md) -- [2019年蚂蚁金服、头条、拼多多的面试总结](docs/essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md) - -## Java学习常见问题汇总 - -- [Java学习路线和方法推荐](docs/questions/java-learning-path-and-methods.md) -- [Java培训四个月能学会吗?](docs/questions/java-training-4-month.md) -- [新手学习Java,有哪些Java相关的博客,专栏,和技术学习网站推荐?](docs/questions/java-learning-website-blog.md) -- [Java 还是大数据,你需要了解这些东西!](docs/questions/java-big-data) -- [Java 后台开发/大数据?你需要了解这些东西!](https://articles.zsxq.com/id_wto1iwd5g72o.html)(知识星球) - -## 工具 +## 必会工具 ### Git -* [Git入门](docs/tools/Git.md) +- [Git 入门](docs/tools/Git.md) ### Docker -* [Docker 基本概念解读](docs/tools/Docker.md) -* [一文搞懂 Docker 镜像的常用操作!](docs/tools/Docker-Image.md ) +1. [Docker 基本概念解读](docs/tools/Docker.md) +2. [一文搞懂 Docker 镜像的常用操作!](docs/tools/Docker-Image.md) ### 其他 -- [阿里云服务器使用经验](docs/tools/阿里云服务器使用经验.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.md) ## 资源 -### 书单 +### Java 程序员必备书单 -- [Java程序员必备书单](docs/data/java-recommended-books.md) +1. [「基础篇」Guide 的 Java 后端书架来啦!都是 Java 程序员必看的书籍?](./docs/books/java基础篇.md) ### 实战项目推荐 -- [Github 上热门的 Spring Boot 项目实战推荐](docs/data/spring-boot-practical-projects.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) +- [年末将至,值得你关注的 16 个 Java 开源项目!](docs/github-trending/2019-12.md) +- [Java 项目历史月榜单](docs/github-trending/JavaGithubTrending.md) -*** +--- ## 待办 -- [x] Java 多线程类别知识重构 -- [ ] Netty 总结(---正在进行中---) +- [x] Netty 总结 - [ ] 数据结构总结重构(---正在进行中---) ## 说明 -### JavaGuide介绍 +开源项目在于大家的参与,这才使得它的价值得到提升。感谢 🙏 有你! -* **对于 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/)。 -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) 。 -利用 docsify 生成文档部署在 Github pages: [docsify 官网介绍](https://docsify.js.org/#/) +Logo 下的小图标是使用[Shields.IO](https://shields.io/) 生成的。 -### 作者的其他开源项目推荐 +## 联系我 -1. [springboot-guide](https://github.com/Snailclimb/springboot-guide) : 适合新手入门以及有经验的开发人员查阅的 Spring Boot 教程(业余时间维护中,欢迎一起维护)。 -2. [programmer-advancement](https://github.com/Snailclimb/programmer-advancement) : 我觉得技术人员应该有的一些好习惯! -3. [spring-security-jwt-guide](https://github.com/Snailclimb/spring-security-jwt-guide) :从零入门 !Spring Security With JWT(含权限验证)后端部分代码。 +![个人微信](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-08/wechat3.jpeg) -### 关于转载 +## 捐赠支持 -如果你需要转载本仓库的一些文章到自己的博客的话,记得注明原文地址就可以了。 +项目的发展离不开你的支持,如果 JavaGuide 帮助到了你找到自己满意的 offer,请作者喝杯咖啡吧 ☕ 后续会继续完善更新!加油! -### 如何对该开源文档进行贡献 +[点击捐赠支持作者](https://www.yuque.com/snailclimb/dr6cvl/mr44yt#vu3ok) -1. 笔记内容大多是手敲,所以难免会有笔误,你可以帮我找错别字。 -2. 很多知识点我可能没有涉及到,所以你可以对其他知识点进行补充。 -3. 现有的知识点难免存在不完善或者错误,所以你可以对已有知识点进行修改/补充。 +## Contributor -### 为什么要做这个开源文档? +下面是笔主收集的一些对本仓库提过有价值的 pr 或者 issue 的朋友,人数较多,如果你也对本仓库提过不错的 pr 或者 issue 的话,你可以加我的微信与我联系。下面的排名不分先后! -初始想法源于自己的个人那一段比较迷茫的学习经历。主要目的是为了通过这个开源平台来帮助一些在学习 Java 或者面试过程中遇到问题的小伙伴。 - -### 投稿 - -由于我个人能力有限,很多知识点我可能没有涉及到,所以你可以对其他知识点进行补充。大家也可以对自己的文章进行自荐,对于不错的文章不仅可以成功在本仓库展示出来更可以获得作者送出的 50 元左右的任意书籍进行奖励(当然你也可以直接折现50元)。 - -### 联系我 - -添加我的微信备注“Github”,回复关键字 **“加群”** 即可入群。 - -![个人微信](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/wechat3.jpeg) - -### Contributor - -下面是笔主收集的一些对本仓库提过有价值的pr或者issue的朋友,人数较多,如果你也对本仓库提过不错的pr或者issue的话,你可以加我的微信与我联系。下面的排名不分先后! - - - - + + + @@ -461,12 +502,12 @@ Markdown 格式参考:[Github Markdown格式](https://guides.github.com/featur -### 公众号 +## 公众号 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! +**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java 面试突击"** 即可免费领取! -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 +**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) +![我的公众号](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-08/167598cd2e17b8ec.png) \ No newline at end of file diff --git a/_coverpage.md b/_coverpage.md index 9e45fbab..7310181a 100644 --- a/_coverpage.md +++ b/_coverpage.md @@ -1,13 +1,12 @@

- +

+

Java 学习/面试指南

[常用资源](https://shimo.im/docs/MuiACIg1HlYfVxrj/) [GitHub]() [开始阅读](#java) -![](./media/pictures/rostyslav-savchyn-5joK905gcGc-unsplash.jpg) - diff --git a/docs/books/images/0d6e5484-aea1-41cc-8417-4694c6028012.png b/docs/books/images/0d6e5484-aea1-41cc-8417-4694c6028012.png new file mode 100644 index 00000000..ab61bff8 Binary files /dev/null and b/docs/books/images/0d6e5484-aea1-41cc-8417-4694c6028012.png differ diff --git a/docs/books/images/18f7bbcf-7de7-49f5-b16b-f56b5185370a.png b/docs/books/images/18f7bbcf-7de7-49f5-b16b-f56b5185370a.png new file mode 100644 index 00000000..6e31a624 Binary files /dev/null and b/docs/books/images/18f7bbcf-7de7-49f5-b16b-f56b5185370a.png differ diff --git a/docs/books/images/20893364-3cc6-4fe5-8cb6-4bed676ce7bd.png b/docs/books/images/20893364-3cc6-4fe5-8cb6-4bed676ce7bd.png new file mode 100644 index 00000000..c23e2d37 Binary files /dev/null and b/docs/books/images/20893364-3cc6-4fe5-8cb6-4bed676ce7bd.png differ diff --git a/docs/books/images/2bb7f878-3514-4f10-99c9-7850318b33a9.png b/docs/books/images/2bb7f878-3514-4f10-99c9-7850318b33a9.png new file mode 100644 index 00000000..ebeb33be Binary files /dev/null and b/docs/books/images/2bb7f878-3514-4f10-99c9-7850318b33a9.png differ diff --git a/docs/books/images/3900e43f-c591-4748-acaf-affcb16d7d9d.png b/docs/books/images/3900e43f-c591-4748-acaf-affcb16d7d9d.png new file mode 100644 index 00000000..70ddae75 Binary files /dev/null and b/docs/books/images/3900e43f-c591-4748-acaf-affcb16d7d9d.png differ diff --git a/docs/books/images/3d2e12ad-b92e-4bb5-b330-f515750ff780.png b/docs/books/images/3d2e12ad-b92e-4bb5-b330-f515750ff780.png new file mode 100644 index 00000000..c88ae86a Binary files /dev/null and b/docs/books/images/3d2e12ad-b92e-4bb5-b330-f515750ff780.png differ diff --git a/docs/books/images/4b337376-e90d-4fdf-9a95-a3fac328b416.png b/docs/books/images/4b337376-e90d-4fdf-9a95-a3fac328b416.png new file mode 100644 index 00000000..4e982aa7 Binary files /dev/null and b/docs/books/images/4b337376-e90d-4fdf-9a95-a3fac328b416.png differ diff --git a/docs/books/images/4fd57829-82a9-4bf4-853a-56bd7413923a.png b/docs/books/images/4fd57829-82a9-4bf4-853a-56bd7413923a.png new file mode 100644 index 00000000..85f6c31a Binary files /dev/null and b/docs/books/images/4fd57829-82a9-4bf4-853a-56bd7413923a.png differ diff --git a/docs/books/images/5d94f552-5815-4b9e-aed4-623b88273355.png b/docs/books/images/5d94f552-5815-4b9e-aed4-623b88273355.png new file mode 100644 index 00000000..1d2e8628 Binary files /dev/null and b/docs/books/images/5d94f552-5815-4b9e-aed4-623b88273355.png differ diff --git a/docs/books/images/7001a206-8ac0-432c-bf62-ca7130487c12.png b/docs/books/images/7001a206-8ac0-432c-bf62-ca7130487c12.png new file mode 100644 index 00000000..14d1ea85 Binary files /dev/null and b/docs/books/images/7001a206-8ac0-432c-bf62-ca7130487c12.png differ diff --git a/docs/books/images/74a29a45-b770-4fd5-8480-c46bd72464a9.png b/docs/books/images/74a29a45-b770-4fd5-8480-c46bd72464a9.png new file mode 100644 index 00000000..dfcab814 Binary files /dev/null and b/docs/books/images/74a29a45-b770-4fd5-8480-c46bd72464a9.png differ diff --git a/docs/books/images/7ab7af22-d9ff-4fa8-9ffb-f5ba73e8b128.png b/docs/books/images/7ab7af22-d9ff-4fa8-9ffb-f5ba73e8b128.png new file mode 100644 index 00000000..f5366af8 Binary files /dev/null and b/docs/books/images/7ab7af22-d9ff-4fa8-9ffb-f5ba73e8b128.png differ diff --git a/docs/books/images/7e80418d-20b1-4066-b9af-cfe434b1bf1a.png b/docs/books/images/7e80418d-20b1-4066-b9af-cfe434b1bf1a.png new file mode 100644 index 00000000..3d7f669d Binary files /dev/null and b/docs/books/images/7e80418d-20b1-4066-b9af-cfe434b1bf1a.png differ diff --git a/docs/books/images/8ece325c-4491-4ffd-9d3d-77e95159ec40.png b/docs/books/images/8ece325c-4491-4ffd-9d3d-77e95159ec40.png new file mode 100644 index 00000000..f07e4043 Binary files /dev/null and b/docs/books/images/8ece325c-4491-4ffd-9d3d-77e95159ec40.png differ diff --git a/docs/books/images/9b472b41-391d-42de-a210-1457c5810618.png b/docs/books/images/9b472b41-391d-42de-a210-1457c5810618.png new file mode 100644 index 00000000..4081070a Binary files /dev/null and b/docs/books/images/9b472b41-391d-42de-a210-1457c5810618.png differ diff --git a/docs/books/images/b4c03ec2-f907-47a4-ad19-731c969a499b.png b/docs/books/images/b4c03ec2-f907-47a4-ad19-731c969a499b.png new file mode 100644 index 00000000..05e3bff5 Binary files /dev/null and b/docs/books/images/b4c03ec2-f907-47a4-ad19-731c969a499b.png differ diff --git a/docs/books/images/c7164eae-8509-4de4-af17-97933fb29f99.png b/docs/books/images/c7164eae-8509-4de4-af17-97933fb29f99.png new file mode 100644 index 00000000..08bbbb93 Binary files /dev/null and b/docs/books/images/c7164eae-8509-4de4-af17-97933fb29f99.png differ diff --git a/docs/books/images/c8188444-68ba-4b86-a22e-d3b2bb3565d6.png b/docs/books/images/c8188444-68ba-4b86-a22e-d3b2bb3565d6.png new file mode 100644 index 00000000..60ea3b8f Binary files /dev/null and b/docs/books/images/c8188444-68ba-4b86-a22e-d3b2bb3565d6.png differ diff --git a/docs/books/images/e2ed7d6a-1c08-4148-99f9-d284b8a7a4c1.png b/docs/books/images/e2ed7d6a-1c08-4148-99f9-d284b8a7a4c1.png new file mode 100644 index 00000000..b0350584 Binary files /dev/null and b/docs/books/images/e2ed7d6a-1c08-4148-99f9-d284b8a7a4c1.png differ diff --git a/docs/books/images/e7e11e32-a931-4261-804f-9586ec4f8476.png b/docs/books/images/e7e11e32-a931-4261-804f-9586ec4f8476.png new file mode 100644 index 00000000..5293b9f0 Binary files /dev/null and b/docs/books/images/e7e11e32-a931-4261-804f-9586ec4f8476.png differ diff --git a/docs/books/images/f16ae5d5-56a0-4b32-8e84-fb10157f3f0c.png b/docs/books/images/f16ae5d5-56a0-4b32-8e84-fb10157f3f0c.png new file mode 100644 index 00000000..d8305ba0 Binary files /dev/null and b/docs/books/images/f16ae5d5-56a0-4b32-8e84-fb10157f3f0c.png differ diff --git a/docs/books/images/format,png.png b/docs/books/images/format,png.png new file mode 100644 index 00000000..f03a22cb Binary files /dev/null and b/docs/books/images/format,png.png differ diff --git a/docs/books/images/s29925598.png b/docs/books/images/s29925598.png new file mode 100644 index 00000000..8f69be39 Binary files /dev/null and b/docs/books/images/s29925598.png differ diff --git a/docs/books/images/s32277130.png b/docs/books/images/s32277130.png new file mode 100644 index 00000000..3f9f425e Binary files /dev/null and b/docs/books/images/s32277130.png differ diff --git a/docs/books/images/s32282160.png b/docs/books/images/s32282160.png new file mode 100644 index 00000000..265f4c3b Binary files /dev/null and b/docs/books/images/s32282160.png differ diff --git a/docs/books/java.md b/docs/books/java.md new file mode 100644 index 00000000..61cce5d7 --- /dev/null +++ b/docs/books/java.md @@ -0,0 +1,184 @@ +**目录:** + + + +- [Java](#java) + - [基础](#基础) + - [并发](#并发) + - [JVM](#jvm) + - [Java8 新特性](#java8-新特性) + - [代码优化](#代码优化) + - [面试](#面试) +- [网络](#网络) +- [操作系统](#操作系统) +- [数据结构](#数据结构) +- [算法](#算法) + - [入门](#入门) + - [经典](#经典) + - [面试](#面试-1) +- [数据库](#数据库) +- [系统设计](#系统设计) + - [设计模式](#设计模式) + - [常用框架](#常用框架) + - [Spring/SpringBoot](#springspringboot) + - [Netty](#netty) + - [分布式](#分布式) + - [网站架构](#网站架构) + - [底层](#底层) +- [软件设计之道](#软件设计之道) +- [其他](#其他) + + + +## Java + +### 基础 + +- **[《Head First Java》](https://book.douban.com/subject/2000732/)** : 可以说是我的 Java 启蒙书籍了,我个人觉得还是很适合稍微有一点点经验的新手来阅读的当然也适合我们用来温故 Java 知识点。*ps:刚入门编程,最好的方式还是通过看视频来学习。* +- **[《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/books/java基础篇.md b/docs/books/java基础篇.md new file mode 100644 index 00000000..92c737c8 --- /dev/null +++ b/docs/books/java基础篇.md @@ -0,0 +1,246 @@ + + +这篇文章推荐了大部分我所读过的优秀书籍,虽然部分可能没看完。答应我,一定要看到最后,看完之后应该不会再纠结要看什么书了。走起!!! + +*这篇文章未涵盖计算机基础比如算法和数据结构、数据库、分布式、微服务方面的书籍,这个留在下一篇文章推荐。* + +## Java + +### 基础 + +#### 《Head First Java》 + +![](images/e7e11e32-a931-4261-804f-9586ec4f8476.png) + +*Guide的 Java 启蒙书籍了。因为是我学习Java看的第一本书,所以,我对其有不一样的情感。* + +*ps:我是当时学完了 C语言之后才开始学习 Java 的,刚开始看这本书感觉很轻松有趣,可以说是我学习编程初期最喜欢的一本书了。* + +有些人说这本书不适合编程新手阅读?(问号脸) 我个人觉得还是很适合稍微有一点点经验的新手来阅读的,当然也适合我们用来温故 Java 知识点。 + +> ps:刚入门编程,最好的方式还是通过看视频来学习。 + +#### 《Java 核心技术卷 1+卷 2》 + +![](images/2bb7f878-3514-4f10-99c9-7850318b33a9.png) + +*Guide拿来当做工具书的两本Java领域的好书!我当时在大学的时候就买了两本放在寝室,没事的时候就翻翻。* + +建议有点 Java 基础之后再读,介绍的还是比较深入和全面的,非常推荐。 + +这两本书的内容很多,全看的话比较费时间,我一般也会用来巩固知识点或者当做工具书参考,是两本适合放在自己身边的好书。 + +#### 《Java 编程思想 (第 4 版)》 + +![](images/3d2e12ad-b92e-4bb5-b330-f515750ff780.png) + +*这本书Guide第一次看的时候还觉得有点枯燥,那时候还在上大二,看了 1/3就没看下去了。* + +大部分人称之为Java领域的圣经(*感觉有点过了~~~*),但我不推荐初学者阅读,有点劝退的味道。稍微有点基础后阅读更好。 + +这本书到现在我也才看了一半左右,内容确实也比较多,而且稍微有点枯燥,但是比较权威。我一般也是拿来当做工具书参考。 + +#### 《Java性能权威指南》 + +![](images/18f7bbcf-7de7-49f5-b16b-f56b5185370a.png) + +*希望能有更多这Java性能优化方面的好书!* + +O'Reilly 家族书,性能调优的入门书,我个人觉得性能调优是每个 Java 从业者必备知识。 + +这本书介绍的实战内容很不错,尤其是 JVM 调优,缺点也比较明显,就是内容稍微有点老。市面上这种书很少。这本书不适合初学者,建议对 Java 语言已经比价掌握了再看。另外,阅读之前,最好先看看周志明大佬的《深入理解 Java 虚拟机》。 + +### 并发 + +#### 《Java 并发编程之美》 + +![《Java 并发编程之美》](images/b4c03ec2-f907-47a4-ad19-731c969a499b.png) + +*这本书还是非常适合我们用来学习 Java 多线程的。这本书的讲解非常通俗易懂,作者从并发编程基础到实战都是信手拈来。* + +另外,这本书的作者加多自身也会经常在网上发布各种技术文章。这本书也是加多大佬这么多年在多线程领域的沉淀所得的结果吧!他书中的内容基本都是结合代码讲解,非常有说服力! + +#### 《实战 Java 高并发程序设计》 + +![《实战 Java 高并发程序设计》](images/0d6e5484-aea1-41cc-8417-4694c6028012.png) + +这个是我第二本要推荐的书籍,比较适合作为多线程入门/进阶书籍来看。这本书内容同样是理论结合实战,对于每个知识点的讲解也比较通俗易懂,整体结构也比较清。 + +#### 《深入浅出 Java 多线程》 + +![《深入浅出Java多线程》](images/7001a206-8ac0-432c-bf62-ca7130487c12.png) + +这本书是几位大厂(如阿里)的大佬开源的,Github 地址:[https://github.com/RedSpider1/concurrent](https://github.com/RedSpider1/concurrent) + +几位作者为了写好《深入浅出 Java 多线程》这本书阅读了大量的 Java 多线程方面的书籍和博客,然后再加上他们的经验总结、Demo 实例、源码解析,最终才形成了这本书。 + +这本书的质量也是非常过硬!给作者们点个赞!这本书有统一的排版规则和语言风格、清晰的表达方式和逻辑。并且每篇文章初稿写完后,作者们就会互相审校,合并到主分支时所有成员会再次审校,最后再通篇修订了三遍。 + +### JVM + +JVM 这里就先只推荐一本书籍和一个关于 JVM 参数调优的免费教程(你假笨大佬将的)。 + +#### 《深入理解Java虚拟机(第3版)》 + +![](images/20893364-3cc6-4fe5-8cb6-4bed676ce7bd.png) + +*希望国内能有更多这样的优质书籍出现!加油!💪* + +这本书就一句话形容:**国产书籍中的战斗机,实实在在的优秀!** + +这本书的第三版去年年底已经出来了,新增了很多实在的内容比如ZGC等新一代GC的原理剖析。目前豆瓣上是 9.6 的高分,🐂不🐂我就不多说了! + +不论是你面试还是你想要在 Java 领域学习的更深,你都离不开这本书籍。这本书不光要看,你还要多看几遍,都是干货,里面很多实战内容自己还最好实践一篇。 + +这里额外推荐一个你假笨大佬的[《JVM 参数【Memory篇】》](https://club.perfma.com/course/438755/list)教程,很厉害了! + +![](images/74a29a45-b770-4fd5-8480-c46bd72464a9.png) + +### 面试 + +#### 《JavaGuide面试突击版》 + +![](images/c8188444-68ba-4b86-a22e-d3b2bb3565d6.png) + +*谁看谁说好!哈哈!* + +Guide自己开源的,涵盖了Java后端方面的大部分知识点比如 集合、JVM、多线程还有数据库MySQL等内容。 + +在我的公众号后台回复 :“**面试突击**”即可免费获取。 + +![我的公众号](images/format,png.png) + +### Java 8 + +#### 《Java 8实战》 + +![](images/4fd57829-82a9-4bf4-853a-56bd7413923a.png) + +*还没用上 Java 8 的可以反思一下了,还没用过 Lambda 也可以反思一下了。* + +现在大部分公司至少都用到了 Java 8 , Java 8算是一个里程碑式的版本,提供了很多有用的新特性比如 Lambda、流式处理等等。 + +这本书是学习 Java 8 新特性很好的选择,它内容包括 Lambda、流和函数式编程等Java8新特性。实战系列的一贯风格让自己快速上手应用起来。 + +## 软件质量 + +### 代码质量 + +#### 《重构_改善既有代码的设计》 + +![](images/7ab7af22-d9ff-4fa8-9ffb-f5ba73e8b128.png) + +*程序员必看!* + +世界顶级、国宝级别的 Martin Fowler 的书籍,可以说是软件开发领域最经典的基本书之一。目前已经出了第二版,我也在不久前买了第二版。 + +这本书我觉是每一个程序员都必须要看,并且需要看很多次的! + +#### 《Effective java 》 + +![Effective Java中文版(第3版)](images/s32282160.png) + +*程序员必看!* + +又是一本 Java 领域国宝级别的书,非常经典。这本书主要介绍了在 Java 编程中很多极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。这篇文章能够非常实际地帮助你写出更加清晰、健壮和高效的代码。本书中的每条规则都以简短、独立的小文章形式出现,并通过例子代码加以进一步说明。 + +#### 《代码整洁之道》 + +![](images/5d94f552-5815-4b9e-aed4-623b88273355.png) + +*程序员必看!* + +每个程序员都必须要看看的一本书籍,书中很多实际可体会的例子,可以教你写出更优质代码。 + +最后再推荐两个相关的文档: + +- **阿里巴巴 Java 开发手册** :[https://github.com/alibaba/p3c](https://github.com/alibaba/p3c) +- **Google Java 编程风格指南:** + +### 软件设计之道 + +#### 《人月神话》 + +![](images/8ece325c-4491-4ffd-9d3d-77e95159ec40.png) + +*主要描述了软件开发的基本定律:一个需要10天才能干完的活,不可能让10个人在1天干完!* + +非常值得阅读的一本书籍。看书名感觉的第一眼感觉不像是技术类的书籍。这本书对于现代软件尤其是复杂软件的开发的规范化有深刻的意义。 + +#### 《领域驱动设计:软件核心复杂性应对之道》 + +![](images/7e80418d-20b1-4066-b9af-cfe434b1bf1a.png) + +这本领域驱动设计方面的经典之作一直被各种推荐,但是我还来及读。 + +## 常用框架 + +### Spring/SpringBoot + +#### 《Spring 实战(第 5 版)》 + +![](images/3900e43f-c591-4748-acaf-affcb16d7d9d.png) + +*比较一般!* + +不建议当做入门书籍读,入门的话可以找点国人的书或者视频看。这本定位就相当于是关于 Spring 的一个概览,只有一些基本概念的介绍和示例,涵盖了 Spring 的各个方面,但都不够深入。就像作者在最后一页写的那样:“学习 Spring,这才刚刚开始”。 + +#### 《Spring 5高级编程(第5版)》 + +![](images/e2ed7d6a-1c08-4148-99f9-d284b8a7a4c1.png) + +*工具人!* + +对于Spring5的新特性介绍的比较详细,也说不上好。另外,感觉全书翻译的有一点蹩脚的味道,还有一点枯燥。全书的内容比较多,我一般拿来当做工具书参考。 + +#### 《Spring Boot编程思想(核心篇)》 + +![Spring Boot编程思想(核心篇)](images/s32277130.png) + +*稍微有点啰嗦,但是原理介绍的比较清楚。* + +SpringBoot 解析,不适合初学者。我是去年入手的,现在就看了几章,后面没看下去。书很厚,感觉很多很多知识点的讲解过于啰嗦和拖沓,不过,这本书对于SpringBoot内部原理讲解的还是很清楚。 + +#### 《Spring Boot实战》 + +![](images/4b337376-e90d-4fdf-9a95-a3fac328b416.png) + +比较一般的一本书,可以简单拿来看一下。 + +#### 《Spring Boot实战派》 + +![](images/c7164eae-8509-4de4-af17-97933fb29f99.png) + +这本书使用的Spring Boot 2.0+的版本,还算比较新。整本书采用“知识点+实例”的形式编写。 + +另外,这本书的干货很多,作者在注意实战的过程中还不忘记对于一些重要的基础知识的讲解。 + +如果你要学习 Spring Boot 的话,我还是比较推荐这本书的。 + +### Netty + +#### 《Netty实战》 + +![](images/f16ae5d5-56a0-4b32-8e84-fb10157f3f0c.png) + +*Guide学习Netty看的就是这本书籍,RPC框架乞丐版 Guide已经写完,Netty系列也在路上了!* + +这本书可以用来入门 Netty ,内容从BIO聊到了 NIO、之后才详细介绍为什么有 Netty 、Netty 为什么好用以及Netty重要的知识点讲解。 + +这本书基本把 Netty 一些重要的知识点都介绍到了,而且基本都是通过实战的形式讲解。 + +#### 《Netty进阶之路:跟着案例学Netty》 + +![Netty进阶之路:跟着案例学Netty](images/s29925598.png) + +*深入Netty必看!* + +内容都是关于使用 Netty 的实践案例比如内存泄露这些东西。如果你觉得你的 Netty 已经完全入门了,并且你想要对Netty掌握的更深的话,推荐你看一下这本书。 + +#### 《Netty 入门与实战:仿写微信 IM 即时通讯系统》 + +![](images/9b472b41-391d-42de-a210-1457c5810618.png) + +*质量很高的一个小册!* + +通过一个基于 Netty 框架实现 IM 核心系统为引子,带你学习Netty。整个小册的质量还是很高的,即使你没有 Netty 使用经验也能看懂。 \ No newline at end of file diff --git a/docs/data/java-recommended-books.md b/docs/data/java-recommended-books.md deleted file mode 100644 index 7bdadce5..00000000 --- a/docs/data/java-recommended-books.md +++ /dev/null @@ -1,119 +0,0 @@ - - - -- [Java](#java) - - [基础](#基础) - - [并发](#并发) - - [JVM](#jvm) - - [Java8 新特性](#java8-新特性) - - [代码优化](#代码优化) -- [网络](#网络) -- [操作系统](#操作系统) -- [数据结构与算法](#数据结构与算法) -- [数据库](#数据库) -- [系统设计](#系统设计) - - [设计模式](#设计模式) - - [常用框架](#常用框架) - - [网站架构](#网站架构) - - [软件底层](#软件底层) -- [其他](#其他) - - -## Java - -### 基础 - -- [《Head First Java》](https://book.douban.com/subject/2000732/)(推荐,豆瓣评分 8.7,1.0K+人评价): 可以说是我的 Java 启蒙书籍了,特别适合新手读当然也适合我们用来温故 Java 知识点。 -- [《Java 核心技术卷 1+卷 2》](https://book.douban.com/subject/25762168/)(推荐): 很棒的两本书,建议有点 Java 基础之后再读,介绍的还是比较深入的,非常推荐。这两本书我一般也会用来巩固知识点,是两本适合放在自己身边的好书。 -- [《JAVA 网络编程 第 4 版》](https://book.douban.com/subject/26259017/): 可以系统的学习一下网络的一些概念以及网络编程在 Java 中的使用。 -- [《Java 编程思想 (第 4 版)》](https://book.douban.com/subject/2130190/)(推荐,豆瓣评分 9.1,3.2K+人评价):大部分人称之为Java领域的圣经,但我不推荐初学者阅读,有点劝退的味道。稍微有点基础后阅读更好。 -- [《Java性能权威指南》](https://book.douban.com/subject/26740520/)(推荐,豆瓣评分 8.2,0.1K+人评价):O'Reilly 家族书,性能调优的入门书,我个人觉得性能调优是每个 Java 从业者必备知识,这本书的缺点就是太老了,但是这本书可以作为一个实战书,尤其是 JVM 调优!不适合初学者。前置书籍:《深入理解 Java 虚拟机》 - -### 并发 - -- [《Java 并发编程之美》]() (推荐):2018 年 10 月出版的一本书,个人感觉非常不错,对每个知识点的讲解都很棒。 -- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/)(推荐,豆瓣评分 7.2,0.2K+人评价): 这本书不是很适合作为 Java 并发入门书籍,需要具备一定的 JVM 基础。我感觉有些东西讲的还是挺深入的,推荐阅读。 -- [《实战 Java 高并发程序设计》](https://book.douban.com/subject/26663605/)(推荐,豆瓣评分 8.3): 书的质量没的说,推荐大家好好看一下。 -- [《Java 高并发编程详解》](https://book.douban.com/subject/30255689/)(豆瓣评分 7.6): 2018 年 6 月出版的一本书,内容很详细,但可能又有点过于啰嗦,不过这只是我的感觉。 - -### JVM - -- [《深入理解 Java 虚拟机(第 2 版)周志明》](https://book.douban.com/subject/24722612/)(推荐,豆瓣评分 8.9,1.0K+人评价):建议多刷几遍,书中的所有知识点可以通过 JAVA 运行时区域和 JAVA 的内存模型与线程两个大模块罗列完全。 -- [《实战 JAVA 虚拟机》](https://book.douban.com/subject/26354292/)(推荐,豆瓣评分 8.0,1.0K+人评价):作为入门的了解 Java 虚拟机的知识还是不错的。 - -### Java8 新特性 - -- [《Java 8 实战》](https://book.douban.com/subject/26772632/) (推荐,豆瓣评分 9.2 ):面向 Java 8 的技能升级,包括 Lambdas、流和函数式编程特性。实战系列的一贯风格让自己快速上手应用起来。Java 8 支持的 Lambda 是精简表达在语法上提供的支持。Java 8 提供了 Stream,学习和使用可以建立流式编程的认知。 -- [《Java 8 编程参考官方教程》](https://book.douban.com/subject/26556574/) (推荐,豆瓣评分 9.2):也还不错吧。 - -### 代码优化 - -- [《重构_改善既有代码的设计》](https://book.douban.com/subject/4262627/)(推荐):豆瓣 9.1 分,重构书籍的开山鼻祖。 -- [《Effective java 》](https://book.douban.com/subject/3360807/)(推荐,豆瓣评分 9.0,1.4K+人评价):本书介绍了在 Java 编程中 78 条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。通过对 Java 平台设计专家所使用的技术的全面描述,揭示了应该做什么,不应该做什么才能产生清晰、健壮和高效的代码。本书中的每条规则都以简短、独立的小文章形式出现,并通过例子代码加以进一步说明。本书内容全面,结构清晰,讲解详细。可作为技术人员的参考用书。 -- [《代码整洁之道》](https://book.douban.com/subject/5442024/)(推荐,豆瓣评分 9.1):虽然是用 Java 语言作为例子,全篇都是在阐述 Java 面向对象的思想,但是其中大部分内容其它语言也能应用到。 -- **阿里巴巴 Java 开发手册(详尽版)** [https://github.com/alibaba/p3c/blob/master/阿里巴巴 Java 开发手册(详尽版).pdf](https://github.com/alibaba/p3c/blob/master/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E8%AF%A6%E5%B0%BD%E7%89%88%EF%BC%89.pdf) -- **Google Java 编程风格指南:** - - -## 网络 - -- [《图解 HTTP》](https://book.douban.com/subject/25863515/)(推荐,豆瓣评分 8.1 , 1.6K+人评价): 讲漫画一样的讲 HTTP,很有意思,不会觉得枯燥,大概也涵盖也 HTTP 常见的知识点。因为篇幅问题,内容可能不太全面。不过,如果不是专门做网络方向研究的小伙伴想研究 HTTP 相关知识的话,读这本书的话应该来说就差不多了。 -- [《HTTP 权威指南》](https://book.douban.com/subject/10746113/) (推荐,豆瓣评分 8.6):如果要全面了解 HTTP 非此书不可! - -## 操作系统 - -- [《鸟哥的 Linux 私房菜》](https://book.douban.com/subject/4889838/)(推荐,,豆瓣评分 9.1,0.3K+人评价):本书是最具知名度的 Linux 入门书《鸟哥的 Linux 私房菜基础学习篇》的最新版,全面而详细地介绍了 Linux 操作系统。全书分为 5 个部分:第一部分着重说明 Linux 的起源及功能,如何规划和安装 Linux 主机;第二部分介绍 Linux 的文件系统、文件、目录与磁盘的管理;第三部分介绍文字模式接口 shell 和管理系统的好帮手 shell 脚本,另外还介绍了文字编辑器 vi 和 vim 的使用方法;第四部分介绍了对于系统安全非常重要的 Linux 账号的管理,以及主机系统与程序的管理,如查看进程、任务分配和作业管理;第五部分介绍了系统管理员 (root) 的管理事项,如了解系统运行状况、系统服务,针对登录文件进行解析,对系统进行备份以及核心的管理等。 - -## 数据结构与算法 - -- [《大话数据结构》](https://book.douban.com/subject/6424904/)(推荐,豆瓣评分 7.9 , 1K+人评价):入门类型的书籍,读起来比较浅显易懂,适合没有数据结构基础或者说数据结构没学好的小伙伴用来入门数据结构。 -- [《数据结构与算法分析:C 语言描述》](https://book.douban.com/subject/1139426/)(推荐,豆瓣评分 8.9,1.6K+人评价):本书是《Data Structures and Algorithm Analysis in C》一书第 2 版的简体中译本。原书曾被评为 20 世纪顶尖的 30 部计算机著作之一,作者 Mark Allen Weiss 在数据结构和算法分析方面卓有建树,他的数据结构和算法分析的著作尤其畅销,并受到广泛好评.已被世界 500 余所大学用作教材。 -- [《算法图解》](https://book.douban.com/subject/26979890/)(推荐,豆瓣评分 8.4,0.6K+人评价):入门类型的书籍,读起来比较浅显易懂,适合没有算法基础或者说算法没学好的小伙伴用来入门。示例丰富,图文并茂,以让人容易理解的方式阐释了算法.读起来比较快,内容不枯燥! -- [《算法 第四版》](https://book.douban.com/subject/10432347/)(推荐,豆瓣评分 9.3,0.4K+人评价):Java 语言描述,算法领域经典的参考书,全面介绍了关于算法和数据结构的必备知识,并特别针对排序、搜索、图处理和字符串处理进行了论述。书的内容非常多,可以说是 Java 程序员的必备书籍之一了。 - -## 数据库 - -- [《高性能 MySQL》](https://book.douban.com/subject/23008813/)(推荐,豆瓣评分 9.3,0.4K+人评价):mysql 领域的经典之作,拥有广泛的影响力。不但适合数据库管理员(dba)阅读,也适合开发人员参考学习。不管是数据库新手还是专家,相信都能从本书有所收获。 -- [《Redis 实战》](https://book.douban.com/subject/26612779/):如果你想了解 Redis 的一些概念性知识的话,这本书真的非常不错。 -- [《Redis 设计与实现》](https://book.douban.com/subject/25900156/)(推荐,豆瓣评分 8.5,0.5K+人评价):也还行吧! -- [《MySQL 技术内幕-InnoDB 存储引擎》]()(推荐,豆瓣评分 8.7):了解 InnoDB 存储引擎底层原理必备的一本书,比较深入。 - -## 系统设计 - -### 设计模式 - -- [《设计模式 : 可复用面向对象软件的基础》](https://book.douban.com/subject/1052241/) (推荐,豆瓣评分 9.1):设计模式的经典! -- [《Head First 设计模式(中文版)》](https://book.douban.com/subject/2243615/) (推荐,豆瓣评分 9.2):相当赞的一本设计模式入门书籍。用实际的编程案例讲解算法设计中会遇到的各种问题和需求变更(对的,连需求变更都考虑到了!),并以此逐步推导出良好的设计模式解决办法。 -- [《大话设计模式》](https://book.douban.com/subject/2334288/) (推荐,豆瓣评分 8.3):本书通篇都是以情景对话的形式,用多个小故事或编程示例来组织讲解GOF(即《设计模式 : 可复用面向对象软件的基础》这本书)),但是不像《设计模式 : 可复用面向对象软件的基础》难懂。但是设计模式只看书是不够的,还是需要在实际项目中运用,结合[设计模式](docs/system-design/设计模式.md)更佳! - -### 常用框架 - -- [《深入分析 Java Web 技术内幕》](https://book.douban.com/subject/25953851/): 感觉还行,涉及的东西也蛮多。 -- [《Netty 实战》](https://book.douban.com/subject/27038538/)(推荐,豆瓣评分 7.8,92 人评价):内容很细,如果想学 Netty 的话,推荐阅读这本书! -- [《从 Paxos 到 Zookeeper》](https://book.douban.com/subject/26292004/)(推荐,豆瓣评分 7.8,0.3K 人评价):简要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了 Paxos 和 ZAB 协议。同时,本书深入介绍了分布式一致性问题的工业解决方案——ZooKeeper,并着重向读者展示这一分布式协调框架的使用方法、内部实现及运维技巧,旨在帮助读者全面了解 ZooKeeper,并更好地使用和运维 ZooKeeper。 -- [《Spring 实战(第 4 版)》](https://book.douban.com/subject/26767354/)(推荐,豆瓣评分 8.3,0.3K+人评价):不建议当做入门书籍读,入门的话可以找点国人的书或者视频看。这本定位就相当于是关于 Spring 的新华字典,只有一些基本概念的介绍和示例,涵盖了 Spring 的各个方面,但都不够深入。就像作者在最后一页写的那样:“学习 Spring,这才刚刚开始”。 -- [《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 微服务实战》主要以示例与源码结合的方式来帮助读者更好地理解这些组件的使用方法以及运行原理。同时,在介绍的过程中,还包含了作者在实践中所遇到的一些问题和解决思路,可供读者在实践中作为参考。 -- [《第一本 Docker 书》](https://book.douban.com/subject/26780404/):Docker 入门书籍! -- [《Spring Boot编程思想(核心篇)》](https://book.douban.com/subject/33390560/)(推荐,豆瓣评分 6.2):SpringBoot深入书,不适合初学者。书尤其的厚,评分低的的理由是书某些知识过于拖沓,评分高的理由是书中对SpringBoot内部原理讲解很清楚。作者小马哥:Apache Dubbo PMC、Spring Cloud Alibaba项目架构师。B站作者地址:https://space.bilibili.com/327910845?from=search&seid=17095917016893398636。 - -### 网站架构 - -- [《大型网站技术架构:核心原理与案例分析+李智慧》](https://book.douban.com/subject/25723064/)(推荐):这本书我读过,基本不需要你有什么基础啊~读起来特别轻松,但是却可以学到很多东西,非常推荐了。另外我写过这本书的思维导图,关注我的微信公众号:“Java 面试通关手册”回复“大型网站技术架构”即可领取思维导图。 -- [《亿级流量网站架构核心技术》](https://book.douban.com/subject/26999243/)(推荐):一书总结并梳理了亿级流量网站高可用和高并发原则,通过实例详细介绍了如何落地这些原则。本书分为四部分:概述、高可用原则、高并发原则、案例实战。从负载均衡、限流、降级、隔离、超时与重试、回滚机制、压测与预案、缓存、池化、异步化、扩容、队列等多方面详细介绍了亿级流量网站的架构核心技术,让读者看后能快速运用到实践项目中。 - -### 软件底层 - -- [《深入剖析 Tomcat》](https://book.douban.com/subject/10426640/)(推荐,豆瓣评分 8.4,0.2K+人评价):本书深入剖析 Tomcat 4 和 Tomcat 5 中的每个组件,并揭示其内部工作原理。通过学习本书,你将可以自行开发 Tomcat 组件,或者扩展已有的组件。 读完这本书,基本可以摆脱背诵面试题的尴尬。 -- [《深入理解 Nginx(第 2 版)》](https://book.douban.com/subject/26745255/):作者讲的非常细致,注释都写的都很工整,对于 Nginx 的开发人员非常有帮助。优点是细致,缺点是过于细致,到处都是代码片段,缺少一些抽象。 - -## 其他 - -- [《黑客与画家》](https://read.douban.com/ebook/387525/?dcs=subject-rec&dcm=douban&dct=2243615):这本书是硅谷创业之父,Y Combinator 创始人 Paul Graham 的文集。之所以叫这个名字,是因为作者认为黑客(并非负面的那个意思)与画家有着极大的相似性,他们都是在创造,而不是完成某个任务。 -- [《图解密码技术》](https://book.douban.com/subject/26265544/)(推荐,豆瓣评分 9.1,0.3K+人评价):本书以**图配文**的形式,第一部分讲述了密码技术的历史沿革、对称密码、分组密码模式(包括ECB、CBC、CFB、OFB、CTR)、公钥、混合密码系统。第二部分重点介绍了认证方面的内容,涉及单向散列函数、消息认证码、数字签名、证书等。第三部分讲述了密钥、随机数、PGP、SSL/TLS 以及密码技术在现实生活中的应用。关键字:JWT 前置知识、区块链密码技术前置知识。属于密码知识入门书籍。 - - - - - - diff --git a/docs/data/spring-boot-practical-projects.md b/docs/data/spring-boot-practical-projects.md deleted file mode 100644 index 046af88a..00000000 --- a/docs/data/spring-boot-practical-projects.md +++ /dev/null @@ -1,66 +0,0 @@ -最近经常被读者问到有没有 Spring Boot 实战项目可以学习,于是,我就去 Github 上找了 10 个我觉得还不错的实战项目。对于这些实战项目,有部分是比较适合 Spring Boot 刚入门的朋友学习的,还有一部分可能要求你对 Spring Boot 相关技术比较熟悉。需要的朋友可以根据个人实际情况进行选择。如果你对 Spring Boot 不太熟悉的话,可以看我最近开源的 springboot-guide:https://github.com/Snailclimb/springboot-guide 入门(还在持续更新中)。 - -### mall - -- **Github地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 22.9k -- **介绍**: mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### jeecg-boot - -- **Github地址**:[https://github.com/zhangdaiscott/jeecg-boot](https://github.com/zhangdaiscott/jeecg-boot) -- **star**: 6.4k -- **介绍**: 一款基于代码生成器的JAVA快速开发平台!采用最新技术,前后端分离架构:SpringBoot 2.x,Ant Design&Vue,Mybatis,Shiro,JWT。强大的代码生成器让前后端代码一键生成,无需写任何代码,绝对是全栈开发福音!! JeecgBoot的宗旨是提高UI能力的同时,降低前后分离的开发成本,JeecgBoot还独创在线开发模式,No代码概念,一系列在线智能开发:在线配置表单、在线配置报表、在线设计流程等等。 - -### eladmin - -- **Github地址**:[https://github.com/elunez/eladmin](https://github.com/elunez/eladmin) -- **star**: 3.9k -- **介绍**: 项目基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、redis、Vue的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由。 - -### paascloud-master - -- **Github地址**:[https://github.com/paascloud/paascloud-master](https://github.com/paascloud/paascloud-master) -- **star**: 5.9k -- **介绍**: spring cloud + vue + oAuth2.0全家桶实战,前后端分离模拟商城,完整的购物流程、后端运营平台,可以实现快速搭建企业级微服务项目。支持微信登录等三方登录。 - -### vhr - -- **Github地址**:[https://github.com/lenve/vhr](https://github.com/lenve/vhr) -- **star**: 10.6k -- **介绍**: 微人事是一个前后端分离的人力资源管理系统,项目采用SpringBoot+Vue开发。 - -### One mall - -- **Github地址**:[https://github.com/YunaiV/onemall](https://github.com/YunaiV/onemall) -- **star**: 1.2k -- **介绍**: mall 商城,基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。 - -### Guns - -- **Github地址**:[https://github.com/stylefeng/Guns](https://github.com/stylefeng/Guns) -- **star**: 2.3k -- **介绍**: Guns基于SpringBoot 2,致力于做更简洁的后台管理系统,完美整合springmvc + shiro + mybatis-plus + beetl!Guns项目代码简洁,注释丰富,上手容易,同时Guns包含许多基础模块(用户管理,角色管理,部门管理,字典管理等10个模块),可以直接作为一个后台管理系统的脚手架! - -### SpringCloud - -- **Github地址**:[https://github.com/YunaiV/onemall](https://github.com/YunaiV/onemall) -- **star**: 1.2k -- **介绍**: mall 商城,基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。 - -### SpringBoot-Shiro-Vue - -- **Github地址**:[https://github.com/Heeexy/SpringBoot-Shiro-Vue](https://github.com/Heeexy/SpringBoot-Shiro-Vue) -- **star**: 1.8k -- **介绍**: 提供一套基于Spring Boot-Shiro-Vue的权限管理思路.前后端都加以控制,做到按钮/接口级别的权限。 - -### newbee-mall - -最近开源的一个商城项目。 - -- **Github地址**:[https://github.com/newbee-ltd/newbee-mall](https://github.com/newbee-ltd/newbee-mall) -- **star**: 50 -- **介绍**: newbee-mall 项目是一套电商系统,包括 newbee-mall 商城系统及 newbee-mall-admin 商城后台管理系统,基于 Spring Boot 2.X 及相关技术栈开发。 前台商城系统包含首页门户、商品分类、新品上线、首页轮播、商品推荐、商品搜索、商品展示、购物车、订单结算、订单流程、个人订单管理、会员中心、帮助中心等模块。 后台管理系统包含数据面板、轮播图管理、商品管理、订单管理、会员管理、分类管理、设置等模块。 - - - diff --git a/docs/dataStructures-algorithms/Backtracking-NQueens.md b/docs/dataStructures-algorithms/Backtracking-NQueens.md index bac262d0..1e1367d3 100644 --- a/docs/dataStructures-algorithms/Backtracking-NQueens.md +++ b/docs/dataStructures-algorithms/Backtracking-NQueens.md @@ -41,7 +41,7 @@ 若 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]; @@ -104,7 +104,7 @@ row - i + n 的最大值为 2n(当row = n,i = 0时),故anti_diag的容 **解法二时间复杂度为O(n!),在校验相同列和相同对角线时,引入三个布尔类型数组进行判断。相比解法一,少了一层循环,用空间换时间。** -``` +```java List> resultList = new LinkedList<>(); public List> solveNQueens(int n) { diff --git a/docs/dataStructures-algorithms/data-structure/bloom-filter.md b/docs/dataStructures-algorithms/data-structure/bloom-filter.md index 901c4720..b9d129d2 100644 --- a/docs/dataStructures-algorithms/data-structure/bloom-filter.md +++ b/docs/dataStructures-algorithms/data-structure/bloom-filter.md @@ -39,7 +39,7 @@ ![布隆过滤器hash计算](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-hash运算.png) -如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为0)。当第二次存储相同字符串时,因为先前的对应位置已设置为1,所以很容易知道此值已经存在(去重非常方便)。 +如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。 如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 @@ -49,7 +49,7 @@ ### 3.布隆过滤器使用场景 -1. 判断给定数据是否存在:比如判断一个数字是否在于包含大量数字的数字集中(数字集很大,5亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。 +1. 判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,5亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。 2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。 ### 4.通过 Java 编程手动实现布隆过滤器 @@ -232,9 +232,9 @@ true #### 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 diff --git a/docs/dataStructures-algorithms/images/Github-CodingInterviews.png b/docs/dataStructures-algorithms/images/Github-CodingInterviews.png new file mode 100644 index 00000000..a02d8a12 Binary files /dev/null and b/docs/dataStructures-algorithms/images/Github-CodingInterviews.png differ diff --git a/docs/dataStructures-algorithms/images/剑指Offer.png b/docs/dataStructures-algorithms/images/剑指Offer.png new file mode 100644 index 00000000..90c296a0 Binary files /dev/null and b/docs/dataStructures-algorithms/images/剑指Offer.png differ diff --git a/docs/dataStructures-algorithms/images/啊哈!算法.png b/docs/dataStructures-algorithms/images/啊哈!算法.png new file mode 100644 index 00000000..5a7c48cb Binary files /dev/null and b/docs/dataStructures-algorithms/images/啊哈!算法.png differ diff --git a/docs/dataStructures-algorithms/images/我的第一本算法书.png b/docs/dataStructures-algorithms/images/我的第一本算法书.png new file mode 100644 index 00000000..17c0418e Binary files /dev/null and b/docs/dataStructures-algorithms/images/我的第一本算法书.png differ diff --git a/docs/dataStructures-algorithms/images/程序员代码面试指南.png b/docs/dataStructures-algorithms/images/程序员代码面试指南.png new file mode 100644 index 00000000..fa50afec Binary files /dev/null and b/docs/dataStructures-algorithms/images/程序员代码面试指南.png differ diff --git a/docs/dataStructures-algorithms/images/算法-4.png b/docs/dataStructures-algorithms/images/算法-4.png new file mode 100644 index 00000000..bbfd3b5c Binary files /dev/null and b/docs/dataStructures-algorithms/images/算法-4.png differ diff --git a/docs/dataStructures-algorithms/images/算法图解.png b/docs/dataStructures-algorithms/images/算法图解.png new file mode 100644 index 00000000..ce1edb0a Binary files /dev/null and b/docs/dataStructures-algorithms/images/算法图解.png differ diff --git a/docs/dataStructures-algorithms/images/算法导论.png b/docs/dataStructures-algorithms/images/算法导论.png new file mode 100644 index 00000000..fc1f52b5 Binary files /dev/null and b/docs/dataStructures-algorithms/images/算法导论.png differ diff --git a/docs/dataStructures-algorithms/images/算法设计手册.png b/docs/dataStructures-algorithms/images/算法设计手册.png new file mode 100644 index 00000000..1fed6659 Binary files /dev/null and b/docs/dataStructures-algorithms/images/算法设计手册.png differ diff --git a/docs/dataStructures-algorithms/images/编程之美.png b/docs/dataStructures-algorithms/images/编程之美.png new file mode 100644 index 00000000..d137b618 Binary files /dev/null and b/docs/dataStructures-algorithms/images/编程之美.png differ diff --git a/docs/dataStructures-algorithms/images/编程珠玑.png b/docs/dataStructures-algorithms/images/编程珠玑.png new file mode 100644 index 00000000..7695d88e Binary files /dev/null and b/docs/dataStructures-algorithms/images/编程珠玑.png differ diff --git a/docs/dataStructures-algorithms/images/计算机程序设计艺术.png b/docs/dataStructures-algorithms/images/计算机程序设计艺术.png new file mode 100644 index 00000000..0a86066d Binary files /dev/null and b/docs/dataStructures-algorithms/images/计算机程序设计艺术.png differ diff --git a/docs/dataStructures-algorithms/几道常见的子符串算法题.md b/docs/dataStructures-algorithms/几道常见的子符串算法题.md index 8489082b..af63c584 100644 --- a/docs/dataStructures-algorithms/几道常见的子符串算法题.md +++ b/docs/dataStructures-algorithms/几道常见的子符串算法题.md @@ -15,11 +15,13 @@ -## 说明 -- 本文作者:wwwxmu -- 原文地址:https://www.weiweiblog.cn/13string/ -- 作者的博客站点:https://www.weiweiblog.cn/ (推荐哦!) +> 授权转载! +> +> - 本文作者:wwwxmu +> - 原文地址:https://www.weiweiblog.cn/13string/ + + 考虑到篇幅问题,我会分两次更新这个内容。本篇文章只是原文的一部分,我在原文的基础上增加了部分内容以及修改了部分代码和注释。另外,我增加了爱奇艺 2018 秋招 Java:`求给定合法括号序列的深度` 这道题。所有代码均编译成功,并带有注释,欢迎各位享用! @@ -190,7 +192,7 @@ public class Main { 我们上面已经知道了什么是回文串?现在我们考虑一下可以构成回文串的两种情况: - 字符出现次数为双数的组合 -- 字符出现次数为双数的组合+一个只出现一次的字符 +- **字符出现次数为偶数的组合+单个字符中出现次数最多且为奇数次的字符** (参见 **[issue665](https://github.com/Snailclimb/JavaGuide/issues/665)** ) 统计字符出现的次数即可,双数才能构成回文。因为允许中间一个数单独出现,比如“abcba”,所以如果最后有字母落单,总长度可以加 1。首先将字符串转变为字符数组。然后遍历该数组,判断对应字符是否在hashset中,如果不在就加进去,如果在就让count++,然后移除该字符!这样就能找到出现次数为双数的字符个数。 diff --git a/docs/dataStructures-algorithms/剑指offer部分编程题.md b/docs/dataStructures-algorithms/剑指offer部分编程题.md index 51de35ea..b7b077fb 100644 --- a/docs/dataStructures-algorithms/剑指offer部分编程题.md +++ b/docs/dataStructures-algorithms/剑指offer部分编程题.md @@ -48,15 +48,6 @@ n<=39 } ``` -#### **运行时间对比:** - -假设n为40我们分别使用迭代法和递归法计算,计算结果如下: - -1. 迭代法 - ![迭代法](https://ws1.sinaimg.cn/large/006rNwoDgy1fpydt5as85j308a025dfl.jpg) -2. 递归法 - ![递归法](https://ws1.sinaimg.cn/large/006rNwoDgy1fpydt2d1k3j30ed02kt8i.jpg) - ### 二 跳台阶问题 #### **题目描述:** diff --git a/docs/dataStructures-algorithms/数据结构.md b/docs/dataStructures-algorithms/数据结构.md index dfb5bc18..3a117dd3 100644 --- a/docs/dataStructures-algorithms/数据结构.md +++ b/docs/dataStructures-algorithms/数据结构.md @@ -95,90 +95,102 @@ Set 继承于 Collection 接口,是一个不允许出现重复元素,并且 - [集合框架源码学习之 HashMap(JDK1.8)](https://juejin.im/post/5ab0568b5188255580020e56) -- [ConcurrentHashMap 实现原理及源码分析](https://link.juejin.im/?target=http%3A%2F%2Fwww.cnblogs.com%2Fchengxiao%2Fp%2F6842045.html) +- [ConcurrentHashMap 实现原理及源码分析](https://www.cnblogs.com/chengxiao/p/6842045.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,并且左右两个子树都是一棵平衡二叉树。 +### 1 二叉树 - * ### 2 完全二叉树 +[二叉树](https://baike.baidu.com/item/%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科) - [完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%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层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。 - 完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。 - * ### 3 满二叉树 +(2)[满二叉树](https://baike.baidu.com/item/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91)——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。 - [满二叉树](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,并且左右两个子树都是一棵平衡二叉树。 - 国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。 - * ### 堆 - - [数据结构之堆的定义](https://blog.csdn.net/qq_33186366/article/details/51876191) +### 2 完全二叉树 - 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。 - * ### 4 二叉查找树(BST) +[完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科) - [浅谈算法和数据结构: 七 二叉查找树](http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html) +完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。 - 二叉查找树的特点: +### 3 满二叉树 - 1. 若任意节点的左子树不空,则左子树上所有结点的 值均小于它的根结点的值; - 2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值; - 3. 任意节点的左、右子树也分别为二叉查找树; - 4. 没有键值相等的节点(no duplicate nodes)。 +[满二叉树](https://baike.baidu.com/item/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科,国内外的定义不同) - * ### 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) +国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。 - [《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 树 +[数据结构之堆的定义](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. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。 + + +红黑树的应用: - [[HBase] LSM树 VS B+树](https://blog.csdn.net/dbanote/article/details/8897599) +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+树最大的性能问题是会产生大量的随机IO - 为了克服B+树的弱点,HBase引入了LSM树的概念,即Log-Structured Merge-Trees。 - - [LSM树由来、设计思想以及应用到HBase的索引](http://www.cnblogs.com/yanghuahui/p/3483754.html) +为了克服B+树的弱点,HBase引入了LSM树的概念,即Log-Structured Merge-Trees。 + +[LSM树由来、设计思想以及应用到HBase的索引](http://www.cnblogs.com/yanghuahui/p/3483754.html) ## 图 diff --git a/docs/dataStructures-algorithms/算法学习资源推荐.md b/docs/dataStructures-algorithms/算法学习资源推荐.md index 4c5df56a..5af08c9d 100644 --- a/docs/dataStructures-algorithms/算法学习资源推荐.md +++ b/docs/dataStructures-algorithms/算法学习资源推荐.md @@ -1,38 +1,128 @@ +先占个坑,说一下我觉得算法这部分学习比较好的规划: + +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+人评价)** + +入门类型的书籍,读起来比较浅显易懂,非常适合没有算法基础或者说算法没学好的小伙伴用来入门。示例丰富,图文并茂,以让人容易理解的方式阐释了算法.读起来比较快,内容不枯燥! + +![啊哈!算法](images/啊哈!算法.png) + +**[啊哈!算法](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+人评价)** + +![](images/计算机程序设计艺术.png) + +**[《计算机程序设计艺术(第1卷)》](https://book.douban.com/subject/1130500/)(豆瓣评分 9.4,0.4K+人评价)** + +### 面试 + +![](images/剑指Offer.png) + +**[《剑指Offer》](https://book.douban.com/subject/6966465/)(豆瓣评分 8.3,0.7K+人评价)** + +这本面试宝典上面涵盖了很多经典的算法面试题,如果你要准备大厂面试的话一定不要错过这本书。 + +《剑指Offer》 对应的算法编程题部分的开源项目解析:[CodingInterviews](https://github.com/gatieme/CodingInterviews) + +![](images/Github-CodingInterviews.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 +### [LeetCode](https://leetcode-cn.com/) -- [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/) -- [如何高效地使用 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)** + -- [牛客网官网](https://www.nowcoder.com) -- [剑指offer编程题](https://www.nowcoder.com/ta/coding-interviews) -- [2017校招真题](https://www.nowcoder.com/ta/2017test) -- [华为机试题](https://www.nowcoder.com/ta/huawei) - -## 公司真题 - -- [ 网易2018校园招聘编程题真题集合](https://www.nowcoder.com/test/6910869/summary) -- [ 网易2018校招内推编程题集合](https://www.nowcoder.com/test/6291726/summary) -- [2017年校招全国统一模拟笔试(第五场)编程题集合](https://www.nowcoder.com/test/5986669/summary) -- [2017年校招全国统一模拟笔试(第四场)编程题集合](https://www.nowcoder.com/test/5507925/summary) -- [2017年校招全国统一模拟笔试(第三场)编程题集合](https://www.nowcoder.com/test/5217106/summary) -- [2017年校招全国统一模拟笔试(第二场)编程题集合](https://www.nowcoder.com/test/4546329/summary) -- [ 2017年校招全国统一模拟笔试(第一场)编程题集合](https://www.nowcoder.com/test/4236887/summary) -- [百度2017春招笔试真题编程题集合](https://www.nowcoder.com/test/4998655/summary) -- [网易2017春招笔试真题编程题集合](https://www.nowcoder.com/test/4575457/summary) -- [网易2017秋招编程题集合](https://www.nowcoder.com/test/2811407/summary) -- [网易有道2017内推编程题](https://www.nowcoder.com/test/2385858/summary) -- [ 滴滴出行2017秋招笔试真题-编程题汇总](https://www.nowcoder.com/test/3701760/summary) -- [腾讯2017暑期实习生编程题](https://www.nowcoder.com/test/1725829/summary) -- [今日头条2017客户端工程师实习生笔试题](https://www.nowcoder.com/test/1649301/summary) -- [今日头条2017后端工程师实习生笔试题](https://www.nowcoder.com/test/1649268/summary) diff --git a/docs/database/MySQL Index.md b/docs/database/MySQL Index.md index b25589fc..8003a69f 100644 --- a/docs/database/MySQL Index.md +++ b/docs/database/MySQL Index.md @@ -1,13 +1,86 @@ +## 为什么要使用索引? -# 思维导图-索引篇 +1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 +2. 可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。 +3. 帮助服务器避免排序和临时表。 +4. 将随机IO变为顺序IO +5. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。 -> 系列思维导图源文件(数据库+架构)以及思维导图制作软件—XMind8 破解安装,公众号后台回复:**“思维导图”** 免费领取!(下面的图片不是很清楚,原图非常清晰,另外提供给大家源文件也是为了大家根据自己需要进行修改) +## 索引这么多优点,为什么不对表中的每一个列创建一个索引呢? -![【思维导图-索引篇】](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-2/70973487.jpg) +1. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。 +2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。 +3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。 -> **下面是我补充的一些内容** +## 使用索引的注意事项? -# 为什么索引能提高查询速度 +1. 在经常需要搜索的列上,可以加快搜索的速度; + +2. 在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。 + +3. 在经常需要排序的列上创 建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间; + +4. 对于中到大型表索引都是非常有效的,但是特大型表的话维护开销会很大,不适合建索引 + +5. 在经常用在连接的列上,这 些列主要是一些外键,可以加快连接的速度; + +6. 避免 where 子句中对字段施加函数,这会造成无法命中索引。 + +7. 在使用InnoDB时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。 + +8. ~~将打算加索引的列设置为 NOT NULL ,否则将导致引擎放弃使用索引而进行全表扫描。~~ + + 订正,来自[issue758](https://github.com/Snailclimb/JavaGuide/issues/758) 。**将某一列设置为default null,where 是可以走索引,另外索引列是否设置 null 是不影响性能的。** 但是,还是不建议列上允许为空。最好限制not null,因为null需要更多的存储空间并且null值无法参与某些运算。 + + 《高性能MySQL》第四章如是说:And, in case you’re wondering, allowing NULL values in the index really doesn’t impact performance 。NULL 值的索引查找流程参考:https://juejin.im/post/5d5defc2518825591523a1db ,相关阅读:[MySQL中IS NULL、IS NOT NULL、!=不能用索引?胡扯!](https://juejin.im/post/5d5defc2518825591523a1db) 。 + +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. 按顺序访问范围数据是很快的,这有两个原因。第一,顺序 I/O 不需要多次磁盘寻道,所以比随机I/O要快很多(特别是对机械硬盘)。第二,如果服务器能够按需要顺序读取数据,那么就不再需要额外的排序操作,并且GROUPBY查询也无须再做排序和将行按组进行聚合计算了。 +3. 索引覆盖查询是很快的。如果一个索引包含了查询需要的所有列,那么存储引擎就 + 不需要再回表查找行。这避免了大量的单行访问,而上面的第1点已经写明单行访 + 问是很慢的。 + +## 为什么索引能提高查询速度 > 以下内容整理自: > 地址: https://juejin.im/post/5b55b842f265da0f9e589e79 @@ -48,7 +121,7 @@ MySQL的基本存储结构是页(记录都存在页里边): 其实底层结构就是B+树,B+树作为树的一种实现,能够让我们很快地查找出对应的记录。 -# 关于索引其他重要的内容补充 +## 关于索引其他重要的内容补充 > 以下内容整理自:《Java工程师修炼之道》 @@ -61,7 +134,7 @@ MySQL中的索引可以以一定顺序引用多列,这种索引叫作联合索 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子句也遵循此规则。 @@ -84,19 +157,19 @@ ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` ) ``` 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.添加多列索引 ``` @@ -104,7 +177,7 @@ ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` ``` -# 参考 +## 参考 - 《Java工程师修炼之道》 - 《MySQL高性能书籍_第3版》 diff --git a/docs/database/MySQL.md b/docs/database/MySQL.md index 64dfad2f..ed841e87 100644 --- a/docs/database/MySQL.md +++ b/docs/database/MySQL.md @@ -98,7 +98,7 @@ MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能极佳, **两者的对比:** 1. **是否支持行级锁** : MyISAM 只有表级锁(table-level locking),而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。 -2. **是否支持事务和崩溃后的安全恢复: MyISAM** 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是**InnoDB** 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 +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. ...... @@ -148,7 +148,7 @@ set global query_cache_size=600000; 缓存建立之后,MySQL的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。 -**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启缓存查询要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。此外,**还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存:** +**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启查询缓存要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。此外,**还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存:** ```sql select sql_no_cache count(*) from usr; ``` @@ -164,7 +164,7 @@ select sql_no_cache count(*) from usr; ![事物的特性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/事务特性.png) 1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; -2. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的; +2. **一致性(Consistency):** 执行事务后,数据库从一个正确的状态变化到另一个正确的状态; 3. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; 4. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 @@ -199,7 +199,7 @@ select sql_no_cache count(*) from usr; | REPEATABLE-READ | × | × | √ | | SERIALIZABLE | × | × | × | -MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看 +MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;` ```sql mysql> SELECT @@tx_isolation; @@ -288,9 +288,9 @@ InnoDB 存储引擎在 **分布式事务** 的情况下一般会用到 **SERIALI ### 解释一下什么是池化设计思想。什么是数据库连接池?为什么需要数据库连接池? -池话设计应该不是一个新名词。我们常见的如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)介绍的还不错,直接复制过来,避免重复造轮子了。 +池化设计应该不是一个新名词。我们常见的如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 的连接。数据库服务端还要维护一些缓存和用户权限信息之类的 所以占用了一些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序的请求,既昂贵又浪费资源。**在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中。**连接池还减少了用户必须等待建立与数据库的连接的时间。 +数据库连接本质就是一个 socket 的连接。数据库服务端还要维护一些缓存和用户权限信息之类的 所以占用了一些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序的请求,既昂贵又浪费资源。**在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中**。 连接池还减少了用户必须等待建立与数据库的连接的时间。 ### 分库分表之后,id 主键如何处理? diff --git a/docs/database/MySQL高性能优化规范建议.md b/docs/database/MySQL高性能优化规范建议.md index f079ad77..60f17583 100644 --- a/docs/database/MySQL高性能优化规范建议.md +++ b/docs/database/MySQL高性能优化规范建议.md @@ -35,7 +35,7 @@ - [2. 避免数据类型的隐式转换](#2-避免数据类型的隐式转换) - [3. 充分利用表上已经存在的索引](#3-充分利用表上已经存在的索引) - [4. 数据库设计时,应该要对以后扩展进行考虑](#4-数据库设计时应该要对以后扩展进行考虑) - - [5. 程序连接不同的数据库使用不同的账号,进制跨库查询](#5-程序连接不同的数据库使用不同的账号进制跨库查询) + - [5. 程序连接不同的数据库使用不同的账号,禁止跨库查询](#5-程序连接不同的数据库使用不同的账号禁止跨库查询) - [6. 禁止使用 SELECT * 必须使用 SELECT <字段列表> 查询](#6-禁止使用-select--必须使用-select-字段列表-查询) - [7. 禁止使用不含字段列表的 INSERT 语句](#7-禁止使用不含字段列表的-insert-语句) - [8. 避免使用子查询,可以把子查询优化为 join 操作](#8-避免使用子查询可以把子查询优化为-join-操作) @@ -76,6 +76,8 @@ Innodb 支持事务,支持行级锁,更好的恢复性,高并发下性能 兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效,如果数据库中有存储 emoji 表情的需要,字符集需要采用 utf8mb4 字符集。 +参考文章:[MySQL 字符集不一致导致索引失效的一个真实案例](https://blog.csdn.net/horses/article/details/107243447) + ### 3. 所有表和字段都需要添加注释 使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护 diff --git a/docs/database/Redis/Redis.md b/docs/database/Redis/Redis.md deleted file mode 100644 index a17c7f04..00000000 --- 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持久化.md b/docs/database/Redis/Redis持久化.md index fbad9555..0408c276 100644 --- a/docs/database/Redis/Redis持久化.md +++ b/docs/database/Redis/Redis持久化.md @@ -1,5 +1,4 @@ - 非常感谢《redis实战》真本书,本文大多内容也参考了书中的内容。非常推荐大家看一下《redis实战》这本书,感觉书中的很多理论性东西还是很不错的。 为什么本文的名字要加上春夏秋冬又一春,哈哈 ,这是一部韩国的电影,我感觉电影不错,所以就用在文章名字上了,没有什么特别的含义,然后下面的有些配图也是电影相关镜头。 @@ -10,12 +9,10 @@ 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配置文件中默认有此下配置: @@ -46,6 +43,7 @@ save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生 ## **AOF(append-only file)持久化** 与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启: + ``` appendonly yes ``` 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 00000000..afa76715 --- /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 00000000..4fb4e36c 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 00000000..91f7f46d 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 00000000..23844d67 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 00000000..e7e19e52 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事件处理器.png b/docs/database/Redis/images/redis-all/redis事件处理器.png new file mode 100644 index 00000000..fc280fff Binary files /dev/null and b/docs/database/Redis/images/redis-all/redis事件处理器.png differ diff --git a/docs/database/Redis/images/redis-all/redis事务.png b/docs/database/Redis/images/redis-all/redis事务.png new file mode 100644 index 00000000..eb0c404c Binary files /dev/null and b/docs/database/Redis/images/redis-all/redis事务.png differ diff --git a/docs/database/Redis/images/redis-all/redis过期时间.png b/docs/database/Redis/images/redis-all/redis过期时间.png new file mode 100644 index 00000000..27df6ead Binary files /dev/null and b/docs/database/Redis/images/redis-all/redis过期时间.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 00000000..cd21a651 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 00000000..913881ac 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/使用缓存之后.png b/docs/database/Redis/images/redis-all/使用缓存之后.png new file mode 100644 index 00000000..2c73bd90 Binary files /dev/null and b/docs/database/Redis/images/redis-all/使用缓存之后.png differ diff --git a/docs/database/Redis/images/redis-all/加入布隆过滤器后的缓存处理流程.png b/docs/database/Redis/images/redis-all/加入布隆过滤器后的缓存处理流程.png new file mode 100644 index 00000000..a2c2ed69 Binary files /dev/null and b/docs/database/Redis/images/redis-all/加入布隆过滤器后的缓存处理流程.png differ diff --git a/docs/database/Redis/images/redis-all/单体架构.png b/docs/database/Redis/images/redis-all/单体架构.png new file mode 100644 index 00000000..648a404a Binary files /dev/null and b/docs/database/Redis/images/redis-all/单体架构.png differ diff --git a/docs/database/Redis/images/redis-all/缓存的处理流程.png b/docs/database/Redis/images/redis-all/缓存的处理流程.png new file mode 100644 index 00000000..11860ae1 Binary files /dev/null and b/docs/database/Redis/images/redis-all/缓存的处理流程.png differ diff --git a/docs/database/Redis/images/redis-all/缓存穿透情况.png b/docs/database/Redis/images/redis-all/缓存穿透情况.png new file mode 100644 index 00000000..e7298c15 Binary files /dev/null and b/docs/database/Redis/images/redis-all/缓存穿透情况.png differ diff --git a/docs/database/Redis/images/redis-all/集中式缓存架构.png b/docs/database/Redis/images/redis-all/集中式缓存架构.png new file mode 100644 index 00000000..5aff414b Binary files /dev/null and b/docs/database/Redis/images/redis-all/集中式缓存架构.png differ diff --git a/docs/database/Redis/redis-all.md b/docs/database/Redis/redis-all.md new file mode 100644 index 00000000..a5d8d715 --- /dev/null +++ b/docs/database/Redis/redis-all.md @@ -0,0 +1,714 @@ +点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 + + + + + + +- [1. 简单介绍一下 Redis 呗!](#1-简单介绍一下-redis-呗) +- [2. 分布式缓存常见的技术选型方案有哪些?](#2-分布式缓存常见的技术选型方案有哪些) +- [3. 说一下 Redis 和 Memcached 的区别和共同点](#3-说一下-redis-和-memcached-的区别和共同点) +- [4. 缓存数据的处理流程是怎样的?](#4-缓存数据的处理流程是怎样的) +- [5. 为什么要用 Redis/为什么要用缓存?](#5-为什么要用-redis为什么要用缓存) +- [6. Redis 常见数据结构以及使用场景分析](#6-redis-常见数据结构以及使用场景分析) + - [6.1. string](#61-string) + - [6.2. list](#62-list) + - [6.3. hash](#63-hash) + - [6.4. set](#64-set) + - [6.5. sorted set](#65-sorted-set) +- [7. Redis 单线程模型详解](#7-redis-单线程模型详解) +- [8. Redis 没有使用多线程?为什么不使用多线程?](#8-redis-没有使用多线程为什么不使用多线程) +- [9. Redis6.0 之后为何引入了多线程?](#9-redis60-之后为何引入了多线程) +- [10. Redis 给缓存数据设置过期时间有啥用?](#10-redis-给缓存数据设置过期时间有啥用) +- [11. Redis是如何判断数据是否过期的呢?](#11-redis是如何判断数据是否过期的呢) +- [12. 过期的数据的删除策略了解么?](#12-过期的数据的删除策略了解么) +- [13. Redis 内存淘汰机制了解么?](#13-redis-内存淘汰机制了解么) +- [14. Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)](#14-redis-持久化机制怎么保证-redis-挂掉之后再重启数据可以进行恢复) +- [15. Redis 事务](#15-redis-事务) +- [16. 缓存穿透](#16-缓存穿透) + - [16.1. 什么是缓存穿透?](#161-什么是缓存穿透) + - [16.2. 缓存穿透情况的处理流程是怎样的?](#162-缓存穿透情况的处理流程是怎样的) + - [16.3. 有哪些解决办法?](#163-有哪些解决办法) +- [17. 缓存雪崩](#17-缓存雪崩) + - [17.1. 什么是缓存雪崩?](#171-什么是缓存雪崩) + - [17.2. 有哪些解决办法?](#172-有哪些解决办法) +- [18. 如何保证缓存与数据库双写时的数据一致性?](#18-如何保证缓存与数据库双写时的数据一致性) +- [19. 参考](#19-参考) +- [20. 公众号](#20-公众号) + + + + +### 1. 简单介绍一下 Redis 呗! + +简单来说 **Redis 就是一个使用 C 语言开发的数据库**,不过与传统数据库不同的是 **Redis 的数据是存在内存中的** ,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。 + +另外,**Redis 除了做缓存之外,Redis 也经常用来做分布式锁,甚至是消息队列。** + +**Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。** + +### 2. 分布式缓存常见的技术选型方案有哪些? + +分布式缓存的话,使用的比较多的主要是 **Memcached** 和 **Redis**。不过,现在基本没有看过还有项目使用 **Memcached** 来做缓存,都是直接用 **Redis**。 + +Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。 + +分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用的信息。因为,本地缓存只在当前服务里有效,比如如果你部署了两个相同的服务,他们两者之间的缓存数据是无法共同的。 + +### 3. 说一下 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 来作为自己项目的分布式缓存了。 + +### 4. 缓存数据的处理流程是怎样的? + +作为暖男一号,我给大家画了一个草图。 + +![正常缓存处理流程](images/redis-all/缓存的处理流程.png) + +简单来说就是: + +1. 如果用户请求的数据在缓存中就直接返回。 +2. 缓存中不存在的话就看数据库中是否存在。 +3. 数据库中存在的话就更新缓存中的数据。 +4. 数据库中不存在的话就返回空数据。 + +### 5. 为什么要用 Redis/为什么要用缓存? + +_简单,来说使用缓存主要是为了提升用户体验以及应对更多的用户。_ + +下面我们主要从“高性能”和“高并发”这两点来看待这个问题。 + +![](./images/redis-all/使用缓存之后.png) + +**高性能** : + +对照上面 👆 我画的图。我们设想这样的场景: + +假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。 + +**这样有什么好处呢?** 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。 + +不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可! + +**高并发:** + +一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的情况,redis 集群的话会更高)。 + +> QPS(Query Per Second):服务器每秒可以执行的查询次数; + +所以,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高的系统整体的并发。 + +### 6. Redis 常见数据结构以及使用场景分析 + +你可以自己本机安装 redis 或者通过 redis 官网提供的[在线 redis 环境](https://try.redis.io/)。 + +![try-redis](./images/redis-all/try-redis.png) + +#### 6.1. 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,dect,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 +``` + +#### 6.2. 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 +``` + +#### 6.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> hset 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" +``` + +#### 6.4. 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" +``` + +#### 6.5. 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" +``` + +### 7. 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章》

+ +### 8. 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. 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。 + +### 9. 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/) + +### 10. 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 天内有效。 + +如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。 + +### 11. 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; +``` + +### 12. 过期的数据的删除策略了解么? + +如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢? + +常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西): + +1. **惰性删除** :只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。 +2. **定期删除** : 每隔一段时间抽取一批 key 执行删除过期key操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。 + +定期删除对内存更加友好,惰性删除对CPU更加友好。两者各有千秋,所以Redis 采用的是 **定期删除+惰性/懒汉式删除** 。 + +但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就Out of memory了。 + +怎么解决这个问题呢?答案就是: **Redis 内存淘汰机制。** + +### 13. 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 + +### 14. 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 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。 + +**相关 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 格式,可读性较差。 + +**补充内容:AOF 重写** + +AOF 重写可以产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。 + +AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。 + +在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作 + +### 15. Redis 事务 + +Redis 可以通过 **MULTI,EXEC,DISCARD 和 WATCH** 等命令来实现事务(transaction)功能。 + +``` bash +> MULTI +OK +> INCR foo +QUEUED +> INCR bar +QUEUED +> EXEC +1) (integer) 1 +2) (integer) 1 +``` + +使用 [MULTI](https://redis.io/commands/multi)命令后可以输入多个命令。Redis不会立即执行这些命令,而是将它们放到队列,当调用了[EXEC](https://redis.io/commands/exec)命令将执行所有命令。 + +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) ,推荐阅读:[https://zhuanlan.zhihu.com/p/43897838](https://zhuanlan.zhihu.com/p/43897838) 。 + +### 16. 缓存穿透 + +#### 16.1. 什么是缓存穿透? + +缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。 + +#### 16.2. 缓存穿透情况的处理流程是怎样的? + +如下图所示,用户的请求最终都要跑到数据库中查询一遍。 + +![缓存穿透情况](./images/redis-all/缓存穿透情况.png) + +#### 16.3. 有哪些解决办法? + +最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 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/dataStructures-algorithms/data-structure/bloom-filter.md) ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。 + +### 17. 缓存雪崩 + +#### 17.1. 什么是缓存雪崩? + +我发现缓存雪崩这名字起的有点意思,哈哈。 + +实际上,缓存雪崩描述的就是这样一个简单的场景:**缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。** 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。 + +举个例子:系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。 + +还有一种缓存雪崩的场景是:**有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。** 这样的情况,有下面几种解决办法: + +举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。 + +#### 17.2. 有哪些解决办法? + +**针对 Redis 服务不可用的情况:** + +1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。 +2. 限流,避免同时处理大量的请求。 + +**针对热点缓存失效的情况:** + +1. 设置不同的失效时间比如随机设置缓存的失效时间。 +2. 缓存永不失效。 + +### 18. 如何保证缓存和数据库数据的一致性? + +细说的话可以扯很多,但是我觉得其实没太大必要(小声BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。 + +下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。 + +Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。 + +如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案: + +1. **缓存失效时间变短(不推荐,治标不治本)** :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 +2. **增加cache更新重试机制(常用)**: 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可。 + +### 19. 参考 + +* 《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) + +### 20. 公众号 + +如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 + +**《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/some-concepts-of-caching.md b/docs/database/Redis/some-concepts-of-caching.md new file mode 100644 index 00000000..cda83d2b --- /dev/null +++ b/docs/database/Redis/some-concepts-of-caching.md @@ -0,0 +1,125 @@ + + + + + + +- [1. 缓存的基本思想](#1-缓存的基本思想) +- [2. 使用缓存为系统带来了什么问题](#2-使用缓存为系统带来了什么问题) +- [3. 本地缓存解决方案](#3-本地缓存解决方案) +- [4. 为什么要有分布式缓存?/为什么不直接用本地缓存?](#4-为什么要有分布式缓存为什么不直接用本地缓存) +- [5. 缓存读写模式/更新策略](#5-缓存读写模式更新策略) + - [5.1. Cache Aside Pattern(旁路缓存模式)](#51-cache-aside-pattern旁路缓存模式) + - [5.2. Read/Write Through Pattern(读写穿透)](#52-readwrite-through-pattern读写穿透) + - [5.3. Write Behind Pattern(异步缓存写入)](#53-write-behind-pattern异步缓存写入) + + + + +### 1. 缓存的基本思想 + +很多朋友,只知道缓存可以提高系统性能以及减少请求相应时间,但是,不太清楚缓存的本质思想是什么。 + +缓存的基本思想其实很简单,就是我们非常熟悉的空间换时间。不要把缓存想的太高大上,虽然,它的确对系统的性能提升的性价比非常高。 + +其实,我们在学习使用缓存的时候,你会发现缓存的思想实际在操作系统或者其他地方都被大量用到。 比如 **CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。** **再比如操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器(Cache)。** + +回归到业务系统来说:**我们为了避免用户在请求数据的时候获取速度过于缓慢,所以我们在数据库之上增加了缓存这一层来弥补。** + +当别人再问你,缓存的基本思想的时候,就把上面 👆 这段话告诉他,我觉得会让别人对你刮目相看。 + +### 2. 使用缓存为系统带来了什么问题 + +**软件系统设计中没有银弹,往往任何技术的引入都像是把双刃剑。** 但是,你使用好了之后,这把剑就是好剑。 + +简单来说,为系统引入缓存之后往往会带来下面这些问题: + +_ps:其实我觉得引入本地缓存来做一些简单业务场景的话,实际带来的代价几乎可以忽略,下面 👇 主要是针对分布式缓存来说的。_ + +1. **系统复杂性增加** :引入缓存之后,你要维护缓存和数据库的数据一致性、维护热点缓存等等。 +2. **系统开发成本往往会增加** :引入缓存意味着系统需要一个单独的缓存服务,这是需要花费相应的成本的,并且这个成本还是很贵的,毕竟耗费的是宝贵的内存。但是,如果你只是简单的使用一下本地缓存存储一下简单的数据,并且数据量不大的话,那么就不需要单独去弄一个缓存服务。 + +### 3. 本地缓存解决方案 + +_先来聊聊本地缓存,这个实际在很多项目中用的蛮多,特别是单体架构的时候。数据量不大,并且没有分布式要求的话,使用本地缓存还是可以的。_ + +常见的单体架构图如下,我们使用 **Nginx** 来做**负载均衡**,部署两个相同的服务到服务器,两个服务使用同一个数据库,并且使用的是本地缓存。 + +![单体架构](./images/redis-all/单体架构.png) + +_那本地缓存的方案有哪些呢?且听 Guide 给你来说一说。_ + +**一:JDK 自带的 `HashMap` 和 `ConcurrentHashMap` 了。** + +`ConcurrentHashMap` 可以看作是线程安全版本的 `HashMap` ,两者都是存放 key/value 形式的键值对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如过期时间之类的功能。一个稍微完善一点的缓存框架至少要提供:过期时间、淘汰机制、命中率统计这三点。 + +**二: `Ehcache` 、 `Guava Cache` 、 `Spring Cache` 这三者是使用的比较多的本地缓存框架。** + +`Ehcache` 的话相比于其他两者更加重量。不过,相比于 `Guava Cache` 、 `Spring Cache` 来说, `Ehcache` 支持可以嵌入到 hibernate 和 mybatis 作为多级缓存,并且可以将缓存的数据持久化到本地磁盘中、同时也提供了集群方案(比较鸡肋,可忽略)。 + +`Guava Cache` 和 `Spring Cache` 两者的话比较像。 + +`Guava` 相比于 `Spring Cache` 的话使用的更多一点,它提供了 API 非常方便我们使用,同时也提供了设置缓存有效时间等功能。它的内部实现也比较干净,很多地方都和 `ConcurrentHashMap` 的思想有异曲同工之妙。 + +使用 `Spring Cache` 的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题比如缓存穿透、内存溢出。 + +**三: 后起之秀 Caffeine。** + +相比于 `Guava` 来说 `Caffeine` 在各个方面比如性能要更加优秀,一般建议使用其来替代 `Guava` 。并且, `Guava` 和 `Caffeine` 的使用方式很像! + +本地缓存固然好,但是缺陷也很明显,比如多个相同服务之间的本地缓存的数据无法共享。 + +_下面我们从为什么要有分布式缓存为接入点来正式进入 Redis 的相关问题总结。_ + +### 4. 为什么要有分布式缓存?/为什么不直接用本地缓存? + +_我们可以把分布式缓存(Distributed Cache) 看作是一种内存数据库的服务,它的最终作用就是提供缓存数据的服务。_ + +如下图所示,就是一个简单的使用分布式缓存的架构图。我们使用 Nginx 来做负载均衡,部署两个相同的服务到服务器,两个服务使用同一个数据库和缓存。 + +![集中式缓存架构](./images/redis-all/集中式缓存架构.png) + +本地的缓存的优势是低依赖,比较轻量并且通常相比于使用分布式缓存要更加简单。 + +再来分析一下本地缓存的局限性: + +1. **本地缓存对分布式架构支持不友好**,比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。 +2. **本地缓存容量受服务部署所在的机器限制明显。** 如果当前系统服务所耗费的内存多,那么本地缓存可用的容量就很少。 + +使用分布式缓存之后,缓存部署在一台单独的服务器上,即使同一个相同的服务部署在再多机器上,也是使用的同一份缓存。 并且,单独的分布式缓存服务的性能、容量和提供的功能都要更加强大。 + +使用分布式缓存的缺点呢,也很显而易见,那就是你需要为分布式缓存引入额外的服务比如 Redis 或 Memcached,你需要单独保证 Redis 或 Memcached 服务的高可用。 + +### 5. 缓存读写模式/更新策略 + +**下面介绍到的三种模式各有优劣,不存在最佳模式,根据具体的业务场景选择适合自己的缓存读写模式。** + +#### 5.1. Cache Aside Pattern(旁路缓存模式) + +1. 写:更新 DB,然后直接删除 cache 。 +2. 读:从 cache 中读取数据,读取到就直接返回,读取不到的话,就从 DB 中取数据返回,然后再把数据放到 cache 中。 + +Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准。另外,Cache Aside Pattern 有首次请求数据一定不在 cache 的问题,对于热点数据可以提前放入缓存中。 + +**Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。** + +#### 5.2. Read/Write Through Pattern(读写穿透) + +Read/Write Through 套路是:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。 + +1. 写(Write Through):先查 cache,cache 中不存在,直接更新 DB。 cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(**同步更新 cache 和 DB**)。 +2. 读(Read Through): 从 cache 中读取数据,读取到就直接返回 。读取不到的话,先从 DB 加载,写入到 cache 后返回响应。 + +Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。 + +和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。 + +#### 5.3. Write Behind Pattern(异步缓存写入) + +Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。 + +但是,两个又有很大的不同:**Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。** + +**Write Behind Pattern 下 DB 的写性能非常高,尤其适合一些数据经常变化的业务场景比如说一篇文章的点赞数量、阅读数量。** 往常一篇文章被点赞 500 次的话,需要重复修改 500 次 DB,但是在 Write Behind Pattern 下可能只需要修改一次 DB 就可以了。 + +但是,这种模式同样也给 DB 和 Cache 一致性带来了新的考验,很多时候如果数据还没异步更新到 DB 的话,Cache 服务宕机就 gg 了。 \ No newline at end of file diff --git a/docs/database/事务隔离级别(图文详解).md b/docs/database/事务隔离级别(图文详解).md index 2c8ef1c1..449b7fa7 100644 --- a/docs/database/事务隔离级别(图文详解).md +++ b/docs/database/事务隔离级别(图文详解).md @@ -1,4 +1,4 @@ -> 本文由 [SnailClimb](https://github.com/Snailclimb) 和 [BugSpeak](https://github.com/BugSpeak) 共同完成。 +> 本文由 [SnailClimb](https://github.com/Snailclimb) 和 [guang19](https://github.com/guang19) 共同完成。 - [事务隔离级别(图文详解)](#事务隔离级别图文详解) @@ -80,7 +80,7 @@ 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(可重读)**,但是可以通过应用加锁读(例如 `select * from table for update` 语句)来保证不会产生幻读,而这个加锁度使用到的机制就是 Next-Key Lock 锁算法。从而达到了 SQL 标准的 **SERIALIZABLE(可串行化)** 隔离级别。 因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是**READ-COMMITTED(读取提交内容):**,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。 diff --git a/docs/database/关于数据库存储时间的一点思考.md b/docs/database/关于数据库存储时间的一点思考.md new file mode 100644 index 00000000..ab4e62c6 --- /dev/null +++ b/docs/database/关于数据库存储时间的一点思考.md @@ -0,0 +1,160 @@ +我们平时开发中不可避免的就是要存储时间,比如我们要记录操作表中这条记录的时间、记录转账的交易时间、记录出发时间等等。你会发现这个时间这个东西与我们开发的联系还是非常紧密的,用的好与不好会给我们的业务甚至功能带来很大的影响。所以,我们有必要重新出发,好好认识一下这个东西。 + +这是一篇短小精悍的文章,仔细阅读一定能学到不少东西! + +### 1.切记不要用字符串存储日期 + +我记得我在大学的时候就这样干过,而且现在很多对数据库不太了解的新手也会这样干,可见,这种存储日期的方式的优点还是有的,就是简单直白,容易上手。 + +但是,这是不正确的做法,主要会有下面两个问题: + +1. 字符串占用的空间更大! +2. 字符串存储的日期比较效率比较低(逐个字符进行比对),无法用日期相关的 API 进行计算和比较。 + +### 2.Datetime 和 Timestamp 之间抉择 + +Datetime 和 Timestamp 是 MySQL 提供的两种比较相似的保存时间的数据类型。他们两者究竟该如何选择呢? + +**通常我们都会首选 Timestamp。** 下面说一下为什么这样做! + +#### 2.1 DateTime 类型没有时区信息的 + +**DateTime 类型是没有时区信息的(时区无关)** ,DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。这样就会有什么问题呢?当你的时区更换之后,比如你的服务器更换地址或者更换客户端连接时区设置的话,就会导致你从数据库中读出的时间错误。不要小看这个问题,很多系统就是因为这个问题闹出了很多笑话。 + +**Timestamp 和时区有关**。Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间,说简单点就是在不同时区,查询到同一个条记录此字段的值会不一样。 + +下面实际演示一下! + +建表 SQL 语句: + +```sql +CREATE TABLE `time_zone_test` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `date_time` datetime DEFAULT NULL, + `time_stamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` + +插入数据: + +```sql +INSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW()); +``` + +查看数据: + +```sql +select date_time,time_stamp from time_zone_test; +``` + +结果: + +``` ++---------------------+---------------------+ +| date_time | time_stamp | ++---------------------+---------------------+ +| 2020-01-11 09:53:32 | 2020-01-11 09:53:32 | ++---------------------+---------------------+ +``` + +现在我们运行 + +修改当前会话的时区: + +```sql +set time_zone='+8:00'; +``` + +再次查看数据: + +``` ++---------------------+---------------------+ +| date_time | time_stamp | ++---------------------+---------------------+ +| 2020-01-11 09:53:32 | 2020-01-11 17:53:32 | ++---------------------+---------------------+ +``` + +**扩展:一些关于 MySQL 时区设置的一个常用 sql 命令** + +```sql +# 查看当前会话时区 +SELECT @@session.time_zone; +# 设置当前会话时区 +SET time_zone = 'Europe/Helsinki'; +SET time_zone = "+00:00"; +# 数据库全局时区设置 +SELECT @@global.time_zone; +# 设置全局时区 +SET GLOBAL time_zone = '+8:00'; +SET GLOBAL time_zone = 'Europe/Helsinki'; +``` + +#### 2.2 DateTime 类型耗费空间更大 + +Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。 + +- DateTime :1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 +- Timestamp: 1970-01-01 00:00:01 ~ 2037-12-31 23:59:59 + +> Timestamp 在不同版本的 MySQL 中有细微差别。 + +### 3 再看 MySQL 日期类型存储空间 + +下图是 MySQL 5.6 版本中日期类型所占的存储空间: + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/FhRGUVHFK0ujRPNA75f6CuOXQHTE.jpeg) + +可以看出 5.6.4 之后的 MySQL 多出了一个需要 0 ~ 3 字节的小数位。Datatime 和 Timestamp 会有几种不同的存储空间占用。 + +为了方便,本文我们还是默认 Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗费 8 个字节的存储空间。 + +### 4.数值型时间戳是更好的选择吗? + +很多时候,我们也会使用 int 或者 bigint 类型的数值也就是时间戳来表示时间。 + +这种存储方式的具有 Timestamp 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。 + +时间戳的定义如下: + +> 时间戳的定义是从一个基准时间开始算起,这个基准时间是「1970-1-1 00:00:00 +0:00」,从这个时间开始,用整数表示,以秒计时,随着时间的流逝这个时间整数不断增加。这样一来,我只需要一个数值,就可以完美地表示时间了,而且这个数值是一个绝对数值,即无论的身处地球的任何角落,这个表示时间的时间戳,都是一样的,生成的数值都是一样的,并且没有时区的概念,所以在系统的中时间的传输中,都不需要进行额外的转换了,只有在显示给用户的时候,才转换为字符串格式的本地时间。 + +数据库中实际操作: + +```sql +mysql> select UNIX_TIMESTAMP('2020-01-11 09:53:32'); ++---------------------------------------+ +| UNIX_TIMESTAMP('2020-01-11 09:53:32') | ++---------------------------------------+ +| 1578707612 | ++---------------------------------------+ +1 row in set (0.00 sec) + +mysql> select FROM_UNIXTIME(1578707612); ++---------------------------+ +| FROM_UNIXTIME(1578707612) | ++---------------------------+ +| 2020-01-11 09:53:32 | ++---------------------------+ +1 row in set (0.01 sec) +``` + +### 5.总结 + +MySQL 中时间到底怎么存储才好?Datetime?Timestamp? 数值保存的时间戳? + +好像并没有一个银弹,很多程序员会觉得数值型时间戳是真的好,效率又高还各种兼容,但是很多人又觉得它表现的不够直观。这里插一嘴,《高性能 MySQL 》这本神书的作者就是推荐 Timestamp,原因是数值表示时间不够直观。下面是原文: + + + +每种方式都有各自的优势,根据实际场景才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型: + + + +如果还有什么问题欢迎给我留言!如果文章有什么问题的话,也劳烦指出,Guide 哥感激不尽! + +后面的文章我会介绍: + +- [ ] Java8 对日期的支持以及为啥不能用 SimpleDateFormat。 +- [ ] SpringBoot 中如何实际使用(JPA 为例) \ No newline at end of file diff --git a/docs/database/数据库索引.md b/docs/database/数据库索引.md new file mode 100644 index 00000000..568ba833 --- /dev/null +++ b/docs/database/数据库索引.md @@ -0,0 +1,225 @@ +## 什么是索引? +**索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: 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/阿里巴巴开发手册数据库部分的一些最佳实践.md b/docs/database/阿里巴巴开发手册数据库部分的一些最佳实践.md index 7eb84001..abd9331d 100644 --- a/docs/database/阿里巴巴开发手册数据库部分的一些最佳实践.md +++ b/docs/database/阿里巴巴开发手册数据库部分的一些最佳实践.md @@ -21,7 +21,7 @@ > 1. **增加了复杂性:** a.每次做DELETE 或者UPDATE都必须考虑外键约束,会导致开发的时候很痛苦,测试数据极为不方便;b.外键的主从关系是定的,假如那天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。 > 2. **增加了额外工作**: 数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗资源;(个人觉得这个不是不用外键的原因,因为即使你不使用外键,你在应用层面也还是要保证的。所以,我觉得这个影响可以忽略不计。) > 3. 外键还会因为需要请求对其他表内部加锁而容易出现死锁情况; -> 4. **对分不分表不友好** :因为分库分表下外键是无法生效的。 +> 4. **对分库分表不友好** :因为分库分表下外键是无法生效的。 > 5. ...... 我个人觉得上面这种回答不是特别的全面,只是说了外键存在的一个常见的问题。实际上,我们知道外键也是有很多好处的,比如: diff --git a/docs/essential-content-for-interview/BATJrealInterviewExperience/2020-zijietiaodong.md b/docs/essential-content-for-interview/BATJrealInterviewExperience/2020-zijietiaodong.md new file mode 100644 index 00000000..0e63008b --- /dev/null +++ b/docs/essential-content-for-interview/BATJrealInterviewExperience/2020-zijietiaodong.md @@ -0,0 +1,61 @@ + + +> 本文来自读者 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 本文是鄙人薛某这位老哥的投稿,虽然面试最后挂了,但是老哥本身还是挺优秀的,而且通过这次面试学到了很多东西,我想这就足够了!加油!不要畏惧面试失败,好好修炼自己,多准备一下,后面一定会找到让自己满意的工作。 +> + +## 背景 + +前段时间家里出了点事,辞职回家待了一段时间,处理完老家的事情后就回到广州这边继续找工作,大概是国庆前几天我去面试了一家叫做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/蚂蚁金服实习生面经总结(已拿口头offer).md b/docs/essential-content-for-interview/BATJrealInterviewExperience/蚂蚁金服实习生面经总结(已拿口头offer).md index 2e2df23b..e0984325 100644 --- a/docs/essential-content-for-interview/BATJrealInterviewExperience/蚂蚁金服实习生面经总结(已拿口头offer).md +++ b/docs/essential-content-for-interview/BATJrealInterviewExperience/蚂蚁金服实习生面经总结(已拿口头offer).md @@ -1,4 +1,4 @@ -本文来自 Anonymous 的投稿 ,JavaGuide 对原文进行了重新排版和一点完善。 +本文来自 Anonymous 的投稿 ,Guide哥 对原文进行了重新排版和一点完善。 diff --git a/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md b/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md index d5156937..06d3ffc9 100644 --- a/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md +++ b/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md @@ -1,81 +1,101 @@ -  身边的朋友或者公众号的粉丝很多人都向我询问过:“我是双非/三本/专科学校的,我有机会进入大厂吗?”、“非计算机专业的学生能学好吗?”、“如何学习Java?”、“Java学习该学哪些东西?”、“我该如何准备Java面试?”......这些方面的问题。我会根据自己的一点经验对大部分人关心的这些问题进行答疑解惑。现在又刚好赶上考研结束,这篇文章也算是给考研结束准备往Java后端方向发展的朋友们指明一条学习之路。道理懂了如果没有实际行动,那这篇文章对你或许没有任何意义。 +身边的朋友或者公众号的粉丝很多人都向我询问过:“我是双非/三本/专科学校的,我有机会进入大厂吗?”、“非计算机专业的学生能学好吗?”、“如何学习 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学校的,结合自己的经历以及一些朋友的经历,我觉得让我回答这个问题再好不过。 +我自己也是非 985 非 211 学校的,结合自己的经历以及一些朋友的经历,我觉得让我回答这个问题再好不过。 -  首先,我觉得学校歧视很正常,真的太正常了,如果要抱怨的话,你只能抱怨自己没有进入名校。但是,千万不要动不动说自己学校差,动不动拿自己学校当做自己进不了大厂的借口,学历只是筛选简历的很多标准中的一个而已,如果你够优秀,简历够丰富,你也一样可以和名校同学一起同台竞争。 +首先,我觉得学校歧视很正常,真的太正常了,如果要抱怨的话,你只能抱怨自己没有进入名校。但是,千万不要动不动说自己学校差,动不动拿自己学校当做自己进不了大厂的借口,学历只是筛选简历的很多标准中的一个而已,如果你够优秀,简历够丰富,你也一样可以和名校同学一起同台竞争。 -  企业HR肯定是更喜欢高学历的人,毕竟985、211优秀人才比例肯定比普通学校高很多,HR团队肯定会优先在这些学校里选。这就好比相亲,你是愿意在很多优秀的人中选一个优秀的,还是愿意在很多普通的人中选一个优秀的呢? -   -  双非本科甚至是二本、三本甚至是专科的同学也有很多进入大厂的,不过比率相比于名校的低很多而已。从大厂招聘的结果上看,高学历人才的数量占据大头,那些成功进入BAT、美团,京东,网易等大厂的双非本科甚至是二本、三本甚至是专科的同学往往是因为具备丰富的项目经历或者在某个含金量比较高的竞赛比如ACM中取得了不错的成绩。**一部分学历不突出但能力出众的面试者能够进入大厂并不是说明学历不重要,而是学历的软肋能够通过其他的优势来弥补。** 所以,如果你的学校不够好而你自己又想去大厂的话,建议你可以从这几点来做:**①尽量在面试前最好有一个可以拿的出手的项目;②有实习条件的话,尽早出去实习,实习经历也会是你的简历的一个亮点(有能力在大厂实习最佳!);③参加一些含金量比较高的比赛,拿不拿得到名次没关系,重在锻炼。** +企业 HR 肯定是更喜欢高学历的人,毕竟 985、211 优秀人才比例肯定比普通学校高很多,HR 团队肯定会优先在这些学校里选。这就好比相亲,你是愿意在很多优秀的人中选一个优秀的,还是愿意在很多普通的人中选一个优秀的呢? +双非本科甚至是二本、三本甚至是专科的同学也有很多进入大厂的,不过比率相比于名校的低很多而已。从大厂招聘的结果上看,高学历人才的数量占据大头,那些成功进入 BAT、美团,京东,网易等大厂的双非本科甚至是二本、三本甚至是专科的同学往往是因为具备丰富的项目经历或者在某个含金量比较高的竞赛比如 ACM 中取得了不错的成绩。**一部分学历不突出但能力出众的面试者能够进入大厂并不是说明学历不重要,而是学历的软肋能够通过其他的优势来弥补。** 所以,如果你的学校不够好而你自己又想去大厂的话,建议你可以从这几点来做:**① 尽量在面试前最好有一个可以拿的出手的项目;② 有实习条件的话,尽早出去实习,实习经历也会是你的简历的一个亮点(有能力在大厂实习最佳!);③ 参加一些含金量比较高的比赛,拿不拿得到名次没关系,重在锻炼。** -### Question2:非计算机专业的学生能学好Java后台吗?我能进大厂吗? +### Question2:非计算机专业的学生能学好 Java 后台吗?我能进大厂吗? -  当然可以!现在非科班的程序员很多,很大一部分原因是互联网行业的工资比较高。我们学校外面的培训班里面90%都是非科班,我觉得他们很多人学的都还不错。另外,我的一个朋友本科是机械专业,大一开始自学安卓,技术贼溜,在我看来他比大部分本科是计算机的同学学的还要好。参考Question1的回答,即使你是非科班程序员,如果你想进入大厂的话,你也可以通过自己的其他优势来弥补。 - -  我觉得我们不应该因为自己的专业给自己划界限或者贴标签,说实话,很多科班的同学可能并不如你,你以为科班的同学就会认真听讲吗?还不是几乎全靠自己课下自学!不过如果你是非科班的话,你想要学好,那么注定就要舍弃自己本专业的一些学习时间,这是无可厚非的。 - -  建议非科班的同学,首先要打好计算机基础知识基础:①计算机网络、②操作系统、③数据机构与算法,我个人觉得这3个对你最重要。这些东西就像是内功,对你以后的长远发展非常有用。当然,如果你想要进大厂的话,这些知识也是一定会被问到的。另外,“一定学好数据结构与算法!一定学好数据结构与算法!一定学好数据结构与算法!”,重要的东西说3遍。 +当然可以!现在非科班的程序员很多,很大一部分原因是互联网行业的工资比较高。我们学校外面的培训班里面 90%都是非科班,我觉得他们很多人学的都还不错。另外,我的一个朋友本科是机械专业,大一开始自学安卓,技术贼溜,在我看来他比大部分本科是计算机的同学学的还要好。参考 Question1 的回答,即使你是非科班程序员,如果你想进入大厂的话,你也可以通过自己的其他优势来弥补。 +我觉得我们不应该因为自己的专业给自己划界限或者贴标签,说实话,很多科班的同学可能并不如你,你以为科班的同学就会认真听讲吗?还不是几乎全靠自己课下自学!不过如果你是非科班的话,你想要学好,那么注定就要舍弃自己本专业的一些学习时间,这是无可厚非的。 +建议非科班的同学,首先要打好计算机基础知识基础:① 计算机网络、② 操作系统、③ 数据机构与算法,我个人觉得这 3 个对你最重要。这些东西就像是内功,对你以后的长远发展非常有用。当然,如果你想要进大厂的话,这些知识也是一定会被问到的。另外,“一定学好数据结构与算法!一定学好数据结构与算法!一定学好数据结构与算法!”,重要的东西说 3 遍。 ### Question3: 我没有实习经历的话找工作是不是特别艰难? -  没有实习经历没关系,只要你有拿得出手的项目或者大赛经历的话,你依然有可能拿到大厂的 offer 。笔主当时找工作的时候就没有实习经历以及大赛获奖经历,单纯就是凭借自己的项目经验撑起了整个面试。 +没有实习经历没关系,只要你有拿得出手的项目或者大赛经历的话,你依然有可能拿到大厂的 offer 。笔主当时找工作的时候就没有实习经历以及大赛获奖经历,单纯就是凭借自己的项目经验撑起了整个面试。 -  如果你既没有实习经历,又没有拿得出手的项目或者大赛经历的话,我觉得在简历关,除非你有其他特别的亮点,不然,你应该就会被刷。 +如果你既没有实习经历,又没有拿得出手的项目或者大赛经历的话,我觉得在简历关,除非你有其他特别的亮点,不然,你应该就会被刷。 ### Question4: 我该如何准备面试呢?面试的注意事项有哪些呢? -下面是我总结的一些准备面试的Tips以及面试必备的注意事项: +下面是我总结的一些准备面试的 Tips 以及面试必备的注意事项: 1. **准备一份自己的自我介绍,面试的时候根据面试对象适当进行修改**(突出重点,突出自己的优势在哪里,切忌流水账); 2. **注意随身带上自己的成绩单和简历复印件;** (有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。) -3. **如果需要笔试就提前刷一些笔试题,大部分在线笔试的类型是选择题+编程题,有的还会有简答题。**(平时空闲时间多的可以刷一下笔试题目(牛客网上有很多),但是不要只刷面试题,不动手code,程序员不是为了考试而存在的。)另外,注意抓重点,因为题目太多了,但是有很多题目几乎次次遇到,像这样的题目一定要搞定。 +3. **如果需要笔试就提前刷一些笔试题,大部分在线笔试的类型是选择题+编程题,有的还会有简答题。**(平时空闲时间多的可以刷一下笔试题目(牛客网上有很多),但是不要只刷面试题,不动手 code,程序员不是为了考试而存在的。)另外,注意抓重点,因为题目太多了,但是有很多题目几乎次次遇到,像这样的题目一定要搞定。 4. **提前准备技术面试。** 搞清楚自己面试中可能涉及哪些知识点、哪些知识点是重点。面试中哪些问题会被经常问到、自己该如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!) 5. **面试之前做好定向复习。** 也就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你要面试的公司的面经。 -6. **准备好自己的项目介绍。** 如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:①对项目整体设计的一个感受(面试官可能会让你画系统的架构图);②在这个项目中你负责了什么、做了什么、担任了什么角色;③ 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用;④项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用 redis 做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 +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) +**一些还算不错的 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个小时以上都坚持不了,或者总是容易半途而废的话,我还是推荐你去培训班。观望身边同学去培训班的,大多是非计算机专业或者是没有自律能力以及自学能力非常差的人。 +我本人更加赞同自学(你要知道去了公司可没人手把手教你了,而且几乎所有的公司都对培训班出生的有偏见。为什么有偏见,你学个东西还要去培训班,说明什么,同等水平下,你的自学能力以及自律能力一定是比不上自学的人的)。但是如果,你连每天在寝室坚持学上 8 个小时以上都坚持不了,或者总是容易半途而废的话,我还是推荐你去培训班。观望身边同学去培训班的,大多是非计算机专业或者是没有自律能力以及自学能力非常差的人。 -  另外,如果自律能力不行,你也可以通过结伴学习、参加老师的项目等方式来督促自己学习。 +另外,如果自律能力不行,你也可以通过结伴学习、参加老师的项目等方式来督促自己学习。 -  总结:去不去培训班主要还是看自己,如果自己能坚持自学就自学,坚持不下来就去培训班。 +总结:去不去培训班主要还是看自己,如果自己能坚持自学就自学,坚持不下来就去培训班。 -### Question6: 没有项目经历/博客/Github开源项目怎么办? +### Question6: 没有项目经历/博客/Github 开源项目怎么办? -  从现在开始做! +从现在开始做! -  网上有很多非常不错的项目视频,你就跟着一步一步做,不光要做,还要改进,改善。另外,如果你的老师有相关 Java 后台项目的话,你也可以主动申请参与进来。 +网上有很多非常不错的项目视频,你就跟着一步一步做,不光要做,还要改进,改善。另外,如果你的老师有相关 Java 后台项目的话,你也可以主动申请参与进来。 -  如果有自己的博客,也算是简历上的一个亮点。建议可以在掘金、Segmentfault、CSDN等技术交流社区写博客,当然,你也可以自己搭建一个博客(采用 Hexo+Githu Pages 搭建非常简单)。写一些什么?学习笔记、实战内容、读书笔记等等都可以。 +如果有自己的博客,也算是简历上的一个亮点。建议可以在掘金、Segmentfault、CSDN 等技术交流社区写博客,当然,你也可以自己搭建一个博客(采用 Hexo+Githu Pages 搭建非常简单)。写一些什么?学习笔记、实战内容、读书笔记等等都可以。 -  多用 Github,用好 Github,上传自己不错的项目,写好 readme 文档,在其他技术社区做好宣传。相信你也会收获一个不错的开源项目! +多用 Github,用好 Github,上传自己不错的项目,写好 readme 文档,在其他技术社区做好宣传。相信你也会收获一个不错的开源项目! +### Question7: 大厂青睐什么样的人? -### Question7: 大厂到底青睐什么样的应届生? +**先从已经有两年左右开发经验的工程师角度来看:** 我们来看一下阿里官网支付宝 Java 高级开发工程师的招聘要求,从下面的招聘信息可以看出,除去 Java 基础/集合/多线程这些,这些能力格外重要: -  从阿里、腾讯等大厂招聘官网对于Java后端方向/后端方向的应届实习生的要求,我们大概可以总结归纳出下面这 4 点能给简历增加很多分数: +1. **底层知识比如 jvm** :不只是懂理论更会实操; +2. 面**向对象编程能力** :我理解这个不仅包括“面向对象编程”,还有 SOLID 软件设计原则,相关阅读:[《写了这么多年代码,你真的了解 SOLID 吗?》](https://insights.thoughtworks.cn/do-you-really-know-solid/)(我司大佬的一篇文章) +3. **框架能力** :不只是使用那么简单,更要搞懂原理和机制!搞懂原理和机制的基础是要学会看源码。 +4. **分布式系统开发能力** :缓存、消息队列等等都要掌握,关键是还要能使用这些技术解决实际问题而不是纸上谈兵。 +5. **不错的 sense** :喜欢和尝试新技术、追求编写优雅的代码等等。 -- 参加过竞赛(含金量超高的是ACM); -- 对数据结构与算法非常熟练; -- 参与过实际项目(比如学校网站); -- 参与过某个知名的开源项目或者自己的某个开源项目很不错; +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/支付宝-JAVA开发工程师-专家.jpg) -  除了我上面说的这三点,在面试Java工程师的时候,下面几点也提升你的个人竞争力: +**再从应届生的角度来看:** 我们还是看阿里巴巴的官网相关应届生 Java 工程师招聘岗位的相关要求。 -- 熟悉Python、Shell、Perl等脚本语言; -- 熟悉 Java 优化,JVM调优; -- 熟悉 SOA 模式; -- 熟悉自己所用框架的底层知识比如Spring; -- 了解分布式一些常见的理论; -- 具备高并发开发经验;大数据开发经验等等。 +![](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 index 1ae36a35..5a091e1c 100644 --- a/docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md +++ b/docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md @@ -1,11 +1,20 @@ -不论是校招还是社招都避免不了各种面试、笔试,如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有章可循的,我这个“有章可循”说的意思只是说应对技术面试是可以提前准备。 我其实特别不喜欢那种临近考试就提前背啊记啊各种题的行为,非常反对!我觉得这种方法特别极端,而且在稍有一点经验的面试官面前是根本没有用的。建议大家还是一步一个脚印踏踏实实地走。 +不论是笔试还是面试都是有章可循的,但是,一定要不要想着如何去应付面试,糊弄面试官,这样做终究是欺骗自己。这篇文章的目的也主要想让大家知道自己应该从哪些方向去准备面试,有哪些可以提高的方向。 + +网上已经有很多面经了,但是我认为网上的各种面经仅仅只能作为参考,你的实际面试与之还是有一些区别的。另外如果要在网上看别人的面经的话,建议即要看别人成功的案例也要适当看看别人失败的案例。**看面经没问题,不论是你要找工作还是平时学习,这都是一种比较好地检验自己水平的一种方式。但是,一定不要过分寄希望于各种面经,试着去提高自己的综合能力。** + +“ 80% 的 offer 掌握在 20% 的人手 ” 中这句话也不是不无道理的。决定你面试能否成功的因素中实力固然占有很大一部分比例,但是如果你的心态或者说运气不好的话,依然无法拿到满意的 offer。 + +运气暂且不谈,就拿心态来说,千万不要因为面试失败而气馁或者说怀疑自己的能力,面试失败之后多总结一下失败的原因,后面你就会发现自己会越来越强大。 + +另外,笔主只是在这里分享一下自己对于 “ 如何备战大厂面试 ” 的一个看法,以下大部分理论/言辞都经过过反复推敲验证,如果有不对的地方或者和你想法不同的地方,请您敬请雅正、不舍赐教。 - [1 如何获取大厂面试机会?](#1-如何获取大厂面试机会) - [2 面试前的准备](#2--面试前的准备) - [2.1 准备自己的自我介绍](#21-准备自己的自我介绍) - - [2.2 关于着装](#22-关于着装) + - [2.2 搞清楚技术面可能会问哪些方向的问题](#22-搞清楚技术面可能会问哪些方向的问题) + - [2.2 休闲着装即可](#22-休闲着装即可) - [2.3 随身带上自己的成绩单和简历](#23-随身带上自己的成绩单和简历) - [2.4 如果需要笔试就提前刷一些笔试题](#24-如果需要笔试就提前刷一些笔试题) - [2.5 花时间一些逻辑题](#25-花时间一些逻辑题) @@ -13,6 +22,9 @@ - [2.7 提前准备技术面试](#27-提前准备技术面试) - [2.7 面试之前做好定向复习](#27-面试之前做好定向复习) - [3 面试之后复盘](#3-面试之后复盘) +- [4 如何学习?学会各种框架有必要吗?](#4-如何学习学会各种框架有必要吗) + - [4.1 我该如何学习?](#41-我该如何学习) + - [4.2 学会各种框架有必要吗?](#42-学会各种框架有必要吗) @@ -42,13 +54,35 @@ ### 2.1 准备自己的自我介绍 -从HR面、技术面到高管面/部门主管面,面试官一般会让你先自我介绍一下,所以好好准备自己的自我介绍真的非常重要。网上一般建议的是准备好两份自我介绍:一份对hr说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。 +自我介绍一般是你和面试官的第一次面对面正式交流,换位思考一下,假如你是面试官的话,你想听到被你面试的人如何介绍自己呢?一定不是客套地说说自己喜欢编程、平时花了很多时间来学习、自己的兴趣爱好是打球吧? -我这里简单分享一下我自己的自我介绍的一个简单的模板吧: +我觉得一个好的自我介绍应该包含这几点要素: -> 面试官,您好!我叫某某。大学时间我主要利用课外时间学习某某。在校期间参与过一个某某系统的开发,另外,自己学习过程中也写过很多系统比如某某系统。在学习之余,我比较喜欢通过博客整理分享自己所学知识。我现在是某某社区的认证作者,写过某某很不错的文章。另外,我获得过某某奖,我的Github上开源的某个项目已经有多少Star了。 +1. 用简单的话说清楚自己主要的技术栈于擅长的领域; +2. 把重点放在自己在行的地方以及自己的优势之处; +3. 重点突出自己的能力比如自己的定位的bug的能力特别厉害; -### 2.2 关于着装 +从社招和校招两个角度来举例子吧!我下面的两个例子仅供参考,自我介绍并不需要死记硬背,记住要说的要点,面试的时候根据公司的情况临场发挥也是没问题的。另外,网上一般建议的是准备好两份自我介绍:一份对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!这是互联网公司面试又不是去走红毯,所以你只需要穿的简单大方就好,不需要太正式。 @@ -86,3 +120,32 @@ ## 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/应届生面试最爱问的几道Java基础问题.md b/docs/essential-content-for-interview/PreparingForInterview/应届生面试最爱问的几道Java基础问题.md index 2e93113d..42bcb5ed 100644 --- a/docs/essential-content-for-interview/PreparingForInterview/应届生面试最爱问的几道Java基础问题.md +++ b/docs/essential-content-for-interview/PreparingForInterview/应届生面试最爱问的几道Java基础问题.md @@ -223,57 +223,42 @@ public class test1 { - String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 - 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 -## 三 hashCode 与 equals(重要) +## 三 hashCode() 与 equals()(重要) -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?” +面试官可能会问你:“你重写过 `hashcode` 和 `equals `么,为什么重写 `equals` 时必须重写 `hashCode` 方法?” -### 3.1 hashCode()介绍 +### 3.1 hashCode()介绍 -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。 +`hashCode()` 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。`hashCode() `定义在 JDK 的 `Object` 类中,这就意味着 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(); +public native int hashCode(); ``` 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) -### 3.2 为什么要有 hashCode +### 3.2 为什么要有 hashCode? -**我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** +**我们以“`HashSet` 如何检查重复”为例子来说明为什么要有 hashCode:** -当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 +当你把对象加入 `HashSet` 时,`HashSet` 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 -### 3.3 hashCode()与 equals()的相关规定 +### 3.3 为什么重写 `equals` 时必须重写 `hashCode` 方法? -1. 如果两个对象相等,则 hashcode 一定也是相同的 -2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true -3. 两个对象有相同的 hashcode 值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) +如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。**因此,equals 方法被覆盖过,则 `hashCode` 方法也必须被覆盖。** + +> `hashCode()`的默认行为是对堆上的对象产生独特值。如果没有重写 `hashCode()`,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) ### 3.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的? 在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。 -因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。 +因为 `hashCode()` 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 `hashCode`。 -我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。 +我们刚刚也提到了 `HashSet`,如果 `HashSet` 在对比的时候,同样的 hashcode 有多个对象,它会使用 `equals()` 来判断是否真的相同。也就是说 `hashcode` 只是用来缩小查找成本。 + + +更多关于 `hashcode()` 和 `equals()` 的内容可以查看:[Java hashCode() 和 equals()的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html) ## 四 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的? diff --git a/docs/essential-content-for-interview/PreparingForInterview/程序员的简历之道.md b/docs/essential-content-for-interview/PreparingForInterview/程序员的简历之道.md index 7feead7d..a746892f 100644 --- a/docs/essential-content-for-interview/PreparingForInterview/程序员的简历之道.md +++ b/docs/essential-content-for-interview/PreparingForInterview/程序员的简历之道.md @@ -119,3 +119,4 @@ - 冷熊简历(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/美团面试常见问题总结.md b/docs/essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md index fcbc75dd..869416ce 100644 --- a/docs/essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md +++ b/docs/essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md @@ -229,7 +229,7 @@ HTTP 响应报文主要由状态行、响应头部、响应正文 3 部分组成 1. 避免 where 子句中对字段施加函数,这会造成无法命中索引。 2. 在使用 InnoDB 时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。 -3. 将打算加索引的列设置为 NOT NULL ,否则将导致引擎放弃使用索引而进行全表扫描 +3. 将打算加索引的列建议设置为 NOT NULL ,因为 NULL 比空字符串需要更多的存储空间(不仅仅是索引列,普通的列如果业务允许都建议设置为 NOT NULL) 4. 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用 5. 在使用 limit offset 查询缓慢时,可以借助索引来提高性能 @@ -786,7 +786,7 @@ synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团 **④ 两者的性能已经相差无几** -在 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 操作。 +在 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 线程池了解吗? @@ -922,4 +922,4 @@ Nginx 有以下 5 个优点: - Nginx 二进制可执行文件:由各模块源码编译出一个文件 - nginx.conf 配置文件:控制 Nginx 行为 - acess.log 访问日志: 记录每一条 HTTP 请求信息 -- error.log 错误日志:定位问题 \ No newline at end of file +- error.log 错误日志:定位问题 diff --git a/docs/essential-content-for-interview/面试必备之乐观锁与悲观锁.md b/docs/essential-content-for-interview/面试必备之乐观锁与悲观锁.md index 00aaecd8..2b7ef230 100644 --- a/docs/essential-content-for-interview/面试必备之乐观锁与悲观锁.md +++ b/docs/essential-content-for-interview/面试必备之乐观锁与悲观锁.md @@ -48,8 +48,8 @@ 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 的提交被驳回。 +3. 操作员 A 完成了修改工作,将数据版本号( version=1 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。 +4. 操作员 B 完成了操作,也将版本号( version=1 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。 diff --git a/docs/java/BIO-NIO-AIO.md b/docs/java/BIO-NIO-AIO.md index 36aac437..d56074cd 100644 --- a/docs/java/BIO-NIO-AIO.md +++ b/docs/java/BIO-NIO-AIO.md @@ -30,20 +30,23 @@ 在讲 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) @@ -164,8 +167,6 @@ public class IOServer { 在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 - - ## 2. NIO (New I/O) ### 2.1 NIO 简介 diff --git a/docs/java/JAD反编译tricks.md b/docs/java/JAD反编译tricks.md new file mode 100644 index 00000000..8a0a80de --- /dev/null +++ b/docs/java/JAD反编译tricks.md @@ -0,0 +1,375 @@ +[jad](https://varaneckas.com/jad/)反编译工具,已经不再更新,且只支持JDK1.4,但并不影响其强大的功能。 + +基本用法:`jad xxx.class`,会生成直接可读的xxx.jad文件。 + +## 自动拆装箱 + +对于基本类型和包装类型之间的转换,通过xxxValue()和valueOf()两个方法完成自动拆装箱,使用jad进行反编译可以看到该过程: + +```java +public class Demo { + public static void main(String[] args) { + int x = new Integer(10); // 自动拆箱 + Integer y = x; // 自动装箱 + } +} +``` +反编译后结果: + +```java +public class Demo +{ + public Demo(){} + + public static void main(String args[]) + { + int i = (new Integer(10)).intValue(); // intValue()拆箱 + Integer integer = Integer.valueOf(i); // valueOf()装箱 + } +} +``` + + + +## foreach语法糖 + +在遍历迭代时可以foreach语法糖,对于数组类型直接转换成for循环: + +```java +// 原始代码 +int[] arr = {1, 2, 3, 4, 5}; + for(int item: arr) { + System.out.println(item); + } +} + +// 反编译后代码 +int ai[] = { + 1, 2, 3, 4, 5 +}; +int ai1[] = ai; +int i = ai1.length; +// 转换成for循环 +for(int j = 0; j < i; j++) +{ + int k = ai1[j]; + System.out.println(k); +} +``` + + + +对于容器类的遍历会使用iterator进行迭代: + +```java +import java.io.PrintStream; +import java.util.*; + +public class Demo +{ + public Demo() {} + public static void main(String args[]) + { + ArrayList arraylist = new ArrayList(); + arraylist.add(Integer.valueOf(1)); + arraylist.add(Integer.valueOf(2)); + arraylist.add(Integer.valueOf(3)); + Integer integer; + // 使用的for循环+Iterator,类似于链表迭代: + // for (ListNode cur = head; cur != null; System.out.println(cur.val)){ + // cur = cur.next; + // } + for(Iterator iterator = arraylist.iterator(); iterator.hasNext(); System.out.println(integer)) + integer = (Integer)iterator.next(); + } +} +``` + + + +## Arrays.asList(T...) + +熟悉Arrays.asList(T...)用法的小伙伴都应该知道,asList()方法传入的参数不能是基本类型的数组,必须包装成包装类型再使用,否则对应生成的列表的大小永远是1: + +```java +import java.util.*; +public class Demo { + public static void main(String[] args) { + int[] arr1 = {1, 2, 3}; + Integer[] arr2 = {1, 2, 3}; + List lists1 = Arrays.asList(arr1); + List lists2 = Arrays.asList(arr2); + System.out.println(lists1.size()); // 1 + System.out.println(lists2.size()); // 3 + } +} +``` + +从反编译结果来解释,为什么传入基本类型的数组后,返回的List大小是1: + +```java +// 反编译后文件 +import java.io.PrintStream; +import java.util.Arrays; +import java.util.List; + +public class Demo +{ + public Demo() {} + + public static void main(String args[]) + { + int ai[] = { + 1, 2, 3 + }; + // 使用包装类型,全部元素由int包装为Integer + Integer ainteger[] = { + Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3) + }; + + // 注意这里被反编译成二维数组,而且是一个1行三列的二维数组 + // list.size()当然返回1 + List list = Arrays.asList(new int[][] { ai }); + List list1 = Arrays.asList(ainteger); + System.out.println(list.size()); + System.out.println(list1.size()); + } +} +``` + +从上面结果可以看到,传入基本类型的数组后,会被转换成一个二维数组,而且是**new int\[1]\[arr.length]**这样的数组,调用list.size()当然返回1。 + + + +## 注解 + +Java中的类、接口、枚举、注解都可以看做是类类型。使用jad来看一下@interface被转换成什么: + +```java +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Foo{ + String[] value(); + boolean bar(); +} +``` +查看反编译代码可以看出: + +- 自定义的注解类Foo被转换成接口Foo,并且继承Annotation接口 +- 原来自定义接口中的value()和bar()被转换成抽象方法 + +```java +import java.lang.annotation.Annotation; + +public interface Foo + extends Annotation +{ + public abstract String[] value(); + + public abstract boolean bar(); +} +``` +注解通常和反射配合使用,而且既然自定义的注解最终被转换成接口,注解中的属性被转换成接口中的抽象方法,那么通过反射之后拿到接口实例,在通过接口实例自然能够调用对应的抽象方法: +```java +import java.util.Arrays; + +@Foo(value={"sherman", "decompiler"}, bar=true) +public class Demo{ + public static void main(String[] args) { + Foo foo = Demo.class.getAnnotation(Foo.class); + System.out.println(Arrays.toString(foo.value())); // [sherman, decompiler] + System.out.println(foo.bar()); // true + } +} +``` + + +## 枚举 + +通过jad反编译可以很好地理解枚举类。 + + + +### 空枚举 + +先定义一个空的枚举类: + +```java +public enum DummyEnum { +} +``` +使用jad反编译查看结果: + +- 自定义枚举类被转换成final类,并且继承Enum +- 提供了两个参数(name,odinal)的私有构造器,并且调用了父类的构造器。注意即使没有提供任何参数,也会有该该构造器,其中name就是枚举实例的名称,odinal是枚举实例的索引号 +- 初始化了一个private static final自定义类型的空数组 **$VALUES** +- 提供了两个public static方法: + - values()方法通过clone()方法返回内部$VALUES的浅拷贝。这个方法结合私有构造器可以完美实现单例模式,想一想values()方法是不是和单例模式中getInstance()方法功能类似 + - valueOf(String s):调用父类Enum的valueOf方法并强转返回 + +```java +public final class DummyEnum extends Enum +{ + // 功能和单例模式的getInstance()方法相同 + public static DummyEnum[] values() + { + return (DummyEnum[])$VALUES.clone(); + } + // 调用父类的valueOf方法,并墙砖返回 + public static DummyEnum valueOf(String s) + { + return (DummyEnum)Enum.valueOf(DummyEnum, s); + } + // 默认提供一个私有的私有两个参数的构造器,并调用父类Enum的构造器 + private DummyEnum(String s, int i) + { + super(s, i); + } + // 初始化一个private static final的本类空数组 + private static final DummyEnum $VALUES[] = new DummyEnum[0]; + +} + +``` +### 包含抽象方法的枚举 + +枚举类中也可以包含抽象方法,但是必须定义枚举实例并且立即重写抽象方法,就像下面这样: + +```java +public enum DummyEnum { + DUMMY1 { + public void dummyMethod() { + System.out.println("[1]: implements abstract method in enum class"); + } + }, + + DUMMY2 { + public void dummyMethod() { + System.out.println("[2]: implements abstract method in enum class"); + } + }; + + abstract void dummyMethod(); + +} +``` +再来反编译看看有哪些变化: + +- 原来final class变成了abstract class:这很好理解,有抽象方法的类自然是抽象类 +- 多了两个public static final的成员DUMMY1、DUMMY2,这两个实例的初始化过程被放到了static代码块中,并且实例过程中直接重写了抽象方法,类似于匿名内部类的形式。 +- 数组**$VALUES[]**初始化时放入枚举实例 + +还有其它变化么? + +在反编译后的DummyEnum类中,是存在抽象方法的,而枚举实例在静态代码块中初始化过程中重写了抽象方法。在Java中,抽象方法和抽象方法重写同时放在一个类中,只能通过内部类形式完成。因此上面第二点应该说成就是以内部类形式初始化。 + +可以看一下DummyEnum.class存放的位置,应该多了两个文件: + +- DummyEnum$1.class +- DummyEnum$2.class + +Java中.class文件出现$符号表示有内部类存在,就像OutClass$InnerClass,这两个文件出现也应证了上面的匿名内部类初始化的说法。 + +```java +import java.io.PrintStream; + +public abstract class DummyEnum extends Enum +{ + public static DummyEnum[] values() + { + return (DummyEnum[])$VALUES.clone(); + } + + public static DummyEnum valueOf(String s) + { + return (DummyEnum)Enum.valueOf(DummyEnum, s); + } + + private DummyEnum(String s, int i) + { + super(s, i); + } + + // 抽象方法 + abstract void dummyMethod(); + + // 两个pubic static final实例 + public static final DummyEnum DUMMY1; + public static final DummyEnum DUMMY2; + private static final DummyEnum $VALUES[]; + + // static代码块进行初始化 + static + { + DUMMY1 = new DummyEnum("DUMMY1", 0) { + public void dummyMethod() + { + System.out.println("[1]: implements abstract method in enum class"); + } + } +; + DUMMY2 = new DummyEnum("DUMMY2", 1) { + public void dummyMethod() + { + System.out.println("[2]: implements abstract method in enum class"); + } + } +; + // 对本类数组进行初始化 + $VALUES = (new DummyEnum[] { + DUMMY1, DUMMY2 + }); + } +} +``` + + + +### 正常的枚举类 + +实际开发中,枚举类通常的形式是有两个参数(int code,Sring msg)的构造器,可以作为状态码进行返回。Enum类实际上也是提供了包含两个参数且是protected的构造器,这里为了避免歧义,将枚举类的构造器设置为三个,使用jad反编译: + +最大的变化是:现在的private构造器从2个参数变成5个,而且在内部仍然将前两个参数通过super传递给父类,剩余的三个参数才是真正自己提供的参数。可以想象,如果自定义的枚举类只提供了一个参数,最终生成底层代码中private构造器应该有三个参数,前两个依然通过super传递给父类。 + +```java +public final class CustomEnum extends Enum +{ + public static CustomEnum[] values() + { + return (CustomEnum[])$VALUES.clone(); + } + + public static CustomEnum valueOf(String s) + { + return (CustomEnum)Enum.valueOf(CustomEnum, s); + } + + private CustomEnum(String s, int i, int j, String s1, Object obj) + { + super(s, i); + code = j; + msg = s1; + data = obj; + } + + public static final CustomEnum FIRST; + public static final CustomEnum SECOND; + public static final CustomEnum THIRD; + private int code; + private String msg; + private Object data; + private static final CustomEnum $VALUES[]; + + static + { + FIRST = new CustomEnum("FIRST", 0, 10010, "first", Long.valueOf(100L)); + SECOND = new CustomEnum("SECOND", 1, 10020, "second", "Foo"); + THIRD = new CustomEnum("THIRD", 2, 10030, "third", new Object()); + $VALUES = (new CustomEnum[] { + FIRST, SECOND, THIRD + }); + } +} +``` diff --git a/docs/java/Java基础知识.md b/docs/java/Java基础知识.md index 2f214d01..1a4b9da8 100644 --- a/docs/java/Java基础知识.md +++ b/docs/java/Java基础知识.md @@ -1,75 +1,99 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 + + +点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《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-工具类常见方法总结) -- [参考](#参考) -- [公众号](#公众号) +- [1. Java 基本功](#1-java-基本功) + - [1.1. Java 入门(基础概念与常识)](#11-java-入门基础概念与常识) + - [1.1.1. Java 语言有哪些特点?](#111-java-语言有哪些特点) + - [1.1.2. 关于 JVM JDK 和 JRE 最详细通俗的解答](#112-关于-jvm-jdk-和-jre-最详细通俗的解答) + - [1.1.2.1. JVM](#1121-jvm) + - [1.1.2.2. JDK 和 JRE](#1122-jdk-和-jre) + - [1.1.3. Oracle JDK 和 OpenJDK 的对比](#113-oracle-jdk-和-openjdk-的对比) + - [1.1.4. Java 和 C++的区别?](#114-java-和-c的区别) + - [1.1.5. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?](#115-什么是-java-程序的主类-应用程序和小程序的主类有何不同) + - [1.1.6. Java 应用程序与小程序之间有哪些差别?](#116-java-应用程序与小程序之间有哪些差别) + - [1.1.7. import java 和 javax 有什么区别?](#117-import-java-和-javax-有什么区别) + - [1.1.8. 为什么说 Java 语言“编译与解释并存”?](#118-为什么说-java-语言编译与解释并存) + - [1.2. Java 语法](#12-java-语法) + - [1.2.1. 字符型常量和字符串常量的区别?](#121-字符型常量和字符串常量的区别) + - [1.2.2. 关于注释?](#122-关于注释) + - [1.2.3. 标识符和关键字的区别是什么?](#123-标识符和关键字的区别是什么) + - [1.2.4. Java中有哪些常见的关键字?](#124-java中有哪些常见的关键字) + - [1.2.5. 自增自减运算符](#125-自增自减运算符) + - [1.2.6. continue、break、和return的区别是什么?](#126-continuebreak和return的区别是什么) + - [1.2.7. Java泛型了解么?什么是类型擦除?介绍一下常用的通配符?](#127-java泛型了解么什么是类型擦除介绍一下常用的通配符) + - [1.2.8. ==和equals的区别](#128-和equals的区别) + - [1.2.9. hashCode()与 equals()](#129-hashcode与-equals) + - [1.3. 基本数据类型](#13-基本数据类型) + - [1.3.1. Java中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢?](#131-java中的几种基本数据类型是什么对应的包装类型是什么各自占用多少字节呢) + - [1.3.2. 自动装箱与拆箱](#132-自动装箱与拆箱) + - [1.3.3. 8种基本类型的包装类和常量池](#133-8种基本类型的包装类和常量池) + - [1.4. 方法(函数)](#14-方法函数) + - [1.4.1. 什么是方法的返回值?返回值在类的方法里的作用是什么?](#141-什么是方法的返回值返回值在类的方法里的作用是什么) + - [1.4.2. 为什么 Java 中只有值传递?](#142-为什么-java-中只有值传递) + - [1.4.3. 重载和重写的区别](#143-重载和重写的区别) + - [1.4.3.1. 重载](#1431-重载) + - [1.4.3.2. 重写](#1432-重写) + - [1.4.4. 深拷贝 vs 浅拷贝](#144-深拷贝-vs-浅拷贝) + - [1.4.5. 方法的四种类型](#145-方法的四种类型) +- [2. Java 面向对象](#2-java-面向对象) + - [2.1. 类和对象](#21-类和对象) + - [2.1.1. 面向对象和面向过程的区别](#211-面向对象和面向过程的区别) + - [2.1.2. 构造器 Constructor 是否可被 override?](#212-构造器-constructor-是否可被-override) + - [2.1.3. 在 Java 中定义一个不做事且没有参数的构造方法的作用](#213-在-java-中定义一个不做事且没有参数的构造方法的作用) + - [2.1.4. 成员变量与局部变量的区别有哪些?](#214-成员变量与局部变量的区别有哪些) + - [2.1.5. 创建一个对象用什么运算符?对象实体与对象引用有何不同?](#215-创建一个对象用什么运算符对象实体与对象引用有何不同) + - [2.1.6. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?](#216-一个类的构造方法的作用是什么-若一个类没有声明构造方法该程序能正确执行吗-为什么) + - [2.1.7. 构造方法有哪些特性?](#217-构造方法有哪些特性) + - [2.1.8. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?](#218-在调用子类构造方法之前会先调用父类没有参数的构造方法其目的是) + - [2.1.9. 对象的相等与指向他们的引用相等,两者有什么不同?](#219-对象的相等与指向他们的引用相等两者有什么不同) + - [2.2. 面向对象三大特征](#22-面向对象三大特征) + - [2.2.1. 封装](#221-封装) + - [2.2.2. 继承](#222-继承) + - [2.2.3. 多态](#223-多态) + - [2.3. 修饰符](#23-修饰符) + - [2.3.1. 在一个静态方法内调用一个非静态成员为什么是非法的?](#231-在一个静态方法内调用一个非静态成员为什么是非法的) + - [2.3.2. 静态方法和实例方法有何不同](#232-静态方法和实例方法有何不同) + - [2.3.3. 常见关键字总结:static,final,this,super](#233-常见关键字总结staticfinalthissuper) + - [2.4. 接口和抽象类](#24-接口和抽象类) + - [2.4.1. 接口和抽象类的区别是什么?](#241-接口和抽象类的区别是什么) + - [2.5. 其它重要知识点](#25-其它重要知识点) + - [2.5.1. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?](#251-string-stringbuffer-和-stringbuilder-的区别是什么-string-为什么是不可变的) + - [2.5.2. Object 类的常见方法总结](#252-object-类的常见方法总结) + - [2.5.3. == 与 equals(重要)](#253--与-equals重要) + - [2.5.4. hashCode 与 equals (重要)](#254-hashcode-与-equals-重要) + - [2.5.4.1. hashCode()介绍](#2541-hashcode介绍) + - [2.5.4.2. 为什么要有 hashCode](#2542-为什么要有-hashcode) + - [2.5.4.3. hashCode()与 equals()的相关规定](#2543-hashcode与-equals的相关规定) + - [2.5.5. Java 序列化中如果有些字段不想进行序列化,怎么办?](#255-java-序列化中如果有些字段不想进行序列化怎么办) + - [2.5.6. 获取用键盘输入常用的两种方法](#256-获取用键盘输入常用的两种方法) +- [3. Java 核心技术](#3-java-核心技术) + - [3.1. 集合](#31-集合) + - [3.1.1. Collections 工具类和 Arrays 工具类常见方法总结](#311-collections-工具类和-arrays-工具类常见方法总结) + - [3.2. 异常](#32-异常) + - [3.2.1. Java 异常类层次结构图](#321-java-异常类层次结构图) + - [3.2.2. Throwable 类常用方法](#322-throwable-类常用方法) + - [3.2.3. try-catch-finally](#323-try-catch-finally) + - [3.2.4. 使用 `try-with-resources` 来代替`try-catch-finally`](#324-使用-try-with-resources-来代替try-catch-finally) + - [3.3. 多线程](#33-多线程) + - [3.3.1. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?](#331-简述线程程序进程的基本概念以及他们之间关系是什么) + - [3.3.2. 线程有哪些基本状态?](#332-线程有哪些基本状态) + - [3.4. 文件与 I\O 流](#34-文件与-i\o-流) + - [3.4.1. Java 中 IO 流分为几种?](#341-java-中-io-流分为几种) + - [3.4.1.1. 既然有了字节流,为什么还要有字符流?](#3411-既然有了字节流为什么还要有字符流) + - [3.4.1.2. BIO,NIO,AIO 有什么区别?](#3412-bionioaio-有什么区别) +- [4. 参考](#4-参考) +- [5. 公众号](#5-公众号) -## 1. 面向对象和面向过程的区别 +## 1. Java 基本功 -- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。** -- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。 +### 1.1. Java 入门(基础概念与常识) -参见 issue : [面向过程 :面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) - -> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java性能差的主要原因并不是因为它是面向对象语言,而是Java是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机械码。 -> -> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比Java好。 - -## 2. Java 语言有哪些特点? +#### 1.1.1. Java 语言有哪些特点? 1. 简单易学; 2. 面向对象(封装,继承,多态); @@ -80,112 +104,861 @@ 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 +> 修正(参见: [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 最详细通俗的解答 +#### 1.1.2. 关于 JVM JDK 和 JRE 最详细通俗的解答 -### JVM +##### 1.1.2.1. JVM -Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 +Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 **什么是字节码?采用字节码的好处是什么?** -> 在 Java 中,JVM可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。 +> 在 Java 中,JVM 可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 -**Java 程序从源代码到运行一般有下面3步:** +**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 是编译与解释共存的语言。 +我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。 -> HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。 +> 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 语言“一次编译,随处可以运行”的关键所在。 +Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 -### JDK 和 JRE +##### 1.1.2.2. JDK 和 JRE -JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。它能够创建和编译程序。 +JDK 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。 -JRE 是 Java运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。但是,它不能用于创建新程序。 +JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。 -如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装JDK了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何Java开发,仍然需要安装JDK。例如,如果要使用JSP部署Web应用程序,那么从技术上讲,您只是在应用程序服务器中运行Java程序。那你为什么需要JDK呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 +如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 -## 4. Oracle JDK 和 OpenJDK 的对比 +#### 1.1.3. Oracle JDK 和 OpenJDK 的对比 -可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么Oracle和OpenJDK之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。 +可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么 Oracle 和 OpenJDK 之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。 -对于Java 7,没什么关键的地方。OpenJDK项目主要基于Sun捐赠的HotSpot源代码。此外,OpenJDK被选为Java 7的参考实现,由Oracle工程师维护。关于JVM,JDK,JRE和OpenJDK之间的区别,Oracle博客帖子在2012年有一个更详细的答案: +对于 Java 7,没什么关键的地方。OpenJDK 项目主要基于 Sun 捐赠的 HotSpot 源代码。此外,OpenJDK 被选为 Java 7 的参考实现,由 Oracle 工程师维护。关于 JVM,JDK,JRE 和 OpenJDK 之间的区别,Oracle 博客帖子在 2012 年有一个更详细的答案: -> 问:OpenJDK存储库中的源代码与用于构建Oracle JDK的代码之间有什么区别? +> 问:OpenJDK 存储库中的源代码与用于构建 Oracle JDK 的代码之间有什么区别? > -> 答:非常接近 - 我们的Oracle JDK版本构建过程基于OpenJDK 7构建,只添加了几个部分,例如部署代码,其中包括Oracle的Java插件和Java WebStart的实现,以及一些封闭的源代码派对组件,如图形光栅化器,一些开源的第三方组件,如Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源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。 -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许可获得许可。 +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++的区别? +#### 1.1.4. Java 和 C++的区别? -我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过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) +- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存 +- **在 C 语言中,字符串或字符数组最后都会有一个额外的字符`'\0'`来表示结束。但是,Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189](https://blog.csdn.net/sszgg2006/article/details/49148189) +#### 1.1.5. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同? -## 6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同? +一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 `main()` 方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。 -一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main()方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。 - -## 7. Java 应用程序与小程序之间有哪些差别? +#### 1.1.6. Java 应用程序与小程序之间有哪些差别? 简单说应用程序是从主线程启动(也就是 `main()` 方法)。applet 小程序没有 `main()` 方法,主要是嵌在浏览器页面上运行(调用`init()`或者`run()`来启动),嵌入浏览器这点跟 flash 的小游戏类似。 -## 8. 字符型常量和字符串常量的区别? +#### 1.1.7. import java 和 javax 有什么区别? -1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符 +刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。 + +所以,实际上 java 和 javax 没有区别。这都是一个名字。 + +#### 1.1.8. 为什么说 Java 语言“编译与解释并存”? + +高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著,你可以找一个英文翻译人员帮助你阅读, +有两种选择方式,你可以先等翻译人员将全本的英文名著(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。 + +Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(\*.class 文件),这种字节码必须由 Java 解释器来解释执行。因此,我们可以认为 Java 语言编译与解释并存。 + +### 1.2. Java 语法 + +#### 1.2.1. 字符型常量和字符串常量的区别? + +1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的0个或若干个字符 2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置) -3. 占内存大小 字符常量只占2个字节; 字符串常量占若干个字节 (**注意: char在Java中占两个字节**) +3. 占内存大小 字符常量只占 2 个字节; 字符串常量占若干个字节 (**注意: char 在 Java 中占两个字节**), +> 字符封装类 `Character` 有一个成员常量 `Character.SIZE` 值为16,单位是`bits`,该值除以8(`1byte=8bits`)后就可以得到2个字节 -> java编程思想第四版:2.2.2节 -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) +> java 编程思想第四版:2.2.2 节 +> ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) -## 9. 构造器 Constructor 是否可被 override? +#### 1.2.2. 关于注释? -Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 +Java 中的注释有三种: -## 10. 重载和重写的区别 +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()) +> ``` + +#### 1.2.3. 标识符和关键字的区别是什么? + +在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了标识符,简单来说,标识符就是一个名字。但是有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这种特殊的标识符就是关键字。因此,关键字是被赋予特殊含义的标识符。比如,在我们的日常生活中 ,“警察局”这个名字已经被赋予了特殊的含义,所以如果你开一家店,店的名字不能叫“警察局”,“警察局”就是我们日常生活中的关键字。 + +#### 1.2.4. 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.2.5. 自增自减运算符 + +在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(--)。 + +++和--运算符可以放在变量之前,也可以放在变量之后,当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。例如,当 `b = ++a` 时,先自增(自己增加 1),再赋值(赋值给 b);当 `b = a++` 时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。 + +#### 1.2.6. continue、break、和return的区别是什么? + +在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词: + +1. continue :指跳出当前的这一次循环,继续下一次循环。 +2. break :指跳出整个循环体,继续执行循环下面的语句。 + +return 用于跳出所在方法,结束该方法的运行。return 一般有两种用法: + +1. `return;` :直接使用 return 结束方法执行,用于没有返回值函数的方法 +2. `return value;` :return 一个特定值,用于有返回值函数的方法 + +#### 1.2.7. Java泛型了解么?什么是类型擦除?介绍一下常用的通配符? + +Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。 + +**Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。** 更多关于类型擦除的问题,可以查看这篇文章:[《Java泛型类型擦除以及类型擦除带来的问题》](https://www.cnblogs.com/wuqinglong/p/9456193.html) 。 + +```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 < E > 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 + +更多关于Java 泛型中的通配符可以查看这篇文章:[《聊一聊-JAVA 泛型中的通配符 T,E,K,V,?》](https://juejin.im/post/5d5789d26fb9a06ad0056bd9) + +#### 1.2.8. ==和equals的区别 + +**`==`** : 它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象。(**基本数据类型==比较的是值,引用数据类型==比较的是内存地址**) + +> 因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。 + +**`equals()`** : 它的作用也是判断两个对象是否相等,它不能用于比较基本数据类型的变量。`equals()`方法存在于`Object`类中,而`Object`类是所有类的直接或间接父类。 + +`Object`类`equals()`方法: + +```java +public boolean equals(Object obj) { + return (this == obj); +} +``` + +`equals()` 方法存在两种使用情况: + +- 情况 1:类没有覆盖 `equals()`方法。则通过` equals()`比较该类的两个对象时,等价于通过“==”比较这两个对象。使用的默认是 `Object`类`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` 对象。 + +`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; +} +``` + +#### 1.2.9. 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) + +### 1.3. 基本数据类型 + +#### 1.3.1. Java中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢? + +Java**中**有8种基本数据类型,分别为: + +1. 6种数字类型 :byte、short、int、long、float、double +2. 1种字符类型:char +3. 1种布尔型:boolean。 + +这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean + +| 基本类型 | 位数 | 字节 | 默认值 | +| :------- | :--- | :--- | ------- | +| 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"` :双引号 + +#### 1.3.2. 自动装箱与拆箱 + +- **装箱**:将基本类型用它们对应的引用类型包装起来; +- **拆箱**:将包装类型转换为基本数据类型; + +更多内容见:[深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html) + +#### 1.3.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 进行数值比较。 + +### 1.4. 方法(函数) + +#### 1.4.1. 什么是方法的返回值?返回值在类的方法里的作用是什么? + +方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用是接收出结果,使得它可以用于其他的操作! + +#### 1.4.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 ](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 小节 + +#### 1.4.3. 重载和重写的区别 + +> 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理 +> +> 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法 + +###### 1.4.3.1. 重载 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 -下面是《Java核心技术》对重载这个概念的介绍: +下面是《Java 核心技术》对重载这个概念的介绍: -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg)  +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg) -#### 重写 +**综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。** - 重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。** +###### 1.4.3.2. 重写 -## 11. Java 面向对象编程三大特性: 封装 继承 多态 +重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。 -### 封装 +1. 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。 +2. 如果父类方法访问修饰符为 `private/final/static` 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。 +3. 构造方法无法被重写 -封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 +**综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变** + +**暖心的 Guide 哥最后再来个图表总结一下!** + +| 区别点 | 重载方法 | 重写方法 | +| :--------- | :------- | :----------------------------------------------------------- | +| 发生范围 | 同一个类 | 子类 | +| 参数列表 | 必须修改 | 一定不能修改 | +| 返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 | +| 异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; | +| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) | +| 发生阶段 | 编译期 | 运行期 | -### 继承 -继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 + +**方法的重写要遵循“两同两小一大”**(以下内容摘录自《疯狂 Java 讲义》,[issue#892](https://github.com/Snailclimb/JavaGuide/issues/892) ): + +- “两同”即方法名相同、形参列表相同; +- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; +- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。 + +#### 1.4.4. 深拷贝 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) + +#### 1.4.5. 方法的四种类型 + +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); +} +``` + +## 2. Java 面向对象 + +### 2.1. 类和对象 + +#### 2.1.1. 面向对象和面向过程的区别 + +- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。** +- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。 + +参见 issue : [面向过程 :面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) + +> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。 +> +> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。 + +#### 2.1.2. 构造器 Constructor 是否可被 override? + +Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 + +#### 2.1.3. 在 Java 中定义一个不做事且没有参数的构造方法的作用 + +Java 程序在执行子类的构造方法之前,如果没有用 `super()`来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 `super()`来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。 + +#### 2.1.4. 成员变量与局部变量的区别有哪些? + +1. 从语法形式上看:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。 +2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 +3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 +4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 + +#### 2.1.5. 创建一个对象用什么运算符?对象实体与对象引用有何不同? + +new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。 + +#### 2.1.6. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么? + +主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,这时候,就不能直接 new 一个对象而不传递参数了,所以我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。 + +#### 2.1.7. 构造方法有哪些特性? + +1. 名字与类名相同。 +2. 没有返回值,但不能用 void 声明构造函数。 +3. 生成类的对象时自动执行,无需调用。 + +#### 2.1.8. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? + +帮助子类做初始化工作。 + +#### 2.1.9. 对象的相等与指向他们的引用相等,两者有什么不同? + +对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。 + +### 2.2. 面向对象三大特征 + +#### 2.2.1. 封装 + +封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。 + +```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; + } +} +``` + +#### 2.2.2. 继承 + +不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。 **关于继承如下 3 点请记住:** @@ -193,124 +966,138 @@ Constructor 不能被 override(重写),但是可以 overload(重载),所 2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 -### 多态 +#### 2.2.3. 多态 -所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 +多态,顾名思义,表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例。 -在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 +**多态的特点:** -## 12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? +- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系; +- 对象类型不可变,引用类型可变; +- 方法具有多态性,属性不具有多态性; +- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定; +- 多态不能调用“只在子类存在但在父类不存在”的方法; +- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。 -**可变性** +### 2.3. 修饰符 -简单的来说: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 - -## 13. 自动装箱与拆箱 - -- **装箱**:将基本类型用它们对应的引用类型包装起来; -- **拆箱**:将包装类型转换为基本数据类型; - -## 14. 在一个静态方法内调用一个非静态成员为什么是非法的? +#### 2.3.1. 在一个静态方法内调用一个非静态成员为什么是非法的? 由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 -## 15. 在 Java 中定义一个不做事且没有参数的构造方法的作用 +#### 2.3.2. 静态方法和实例方法有何不同 -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. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。 - -备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。(详见issue:[https://github.com/Snailclimb/JavaGuide/issues/146](https://github.com/Snailclimb/JavaGuide/issues/146)) - -## 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. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 +1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。 -## 24. 对象的相等与指向他们的引用相等,两者有什么不同? +#### 2.3.3. 常见关键字总结:static,final,this,super -对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。 +详见笔主的这篇文章: https://snailclimb.gitee.io/javaguide/#/docs/java/basic/final,static,this,super -## 25. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? +### 2.4. 接口和抽象类 -帮助子类做初始化工作。 +#### 2.4.1. 接口和抽象类的区别是什么? -## 26. == 与 equals(重要) +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 在接口中引入了私有方法和私有静态方法。 + +### 2.5. 其它重要知识点 + +#### 2.5.1. 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` + +#### 2.5.2. 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 { }//实例被垃圾回收器回收的时候触发的操作 + +``` + +#### 2.5.3. == 与 equals(重要) **==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。 **equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: -- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 +- 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 +- 情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 **举个例子:** @@ -339,120 +1126,114 @@ public class test1 { - String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 - 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 -## 27. hashCode 与 equals (重要) -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?” +#### 2.5.4. hashCode 与 equals (重要) -### hashCode()介绍 -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。 +面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?” + +##### 2.5.4.1. hashCode()介绍 + +hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) -### 为什么要有 hashCode +##### 2.5.4.2. 为什么要有 hashCode -**我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 `equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 +**我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与该位置其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 `equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 -通过我们可以看出:`hashCode()` 的作用就是**获取哈希码**,也称为散列码;它实际上是返回一个int整数。这个**哈希码的作用**是确定该对象在哈希表中的索引位置。**`hashCode() `在散列表中才有用,在其它情况下没用**。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。 +通过我们可以看出:`hashCode()` 的作用就是**获取哈希码**,也称为散列码;它实际上是返回一个 int 整数。这个**哈希码的作用**是确定该对象在哈希表中的索引位置。**`hashCode()`在散列表中才有用,在其它情况下没用**。在散列表中 hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。 -### hashCode()与equals()的相关规定 +##### 2.5.4.3. hashCode()与 equals()的相关规定 -1. 如果两个对象相等,则hashcode一定也是相同的 -2. 两个对象相等,对两个对象分别调用equals方法都返回true -3. 两个对象有相同的hashcode值,它们也不一定是相等的 +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) +#### 2.5.5. Java 序列化中如果有些字段不想进行序列化,怎么办? -## 28. 为什么Java中只有值传递? +对于不想进行序列化的变量,使用 transient 关键字修饰。 -[为什么Java中只有值传递?](https://juejin.im/post/5e18879e6fb9a02fc63602e2) +transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。 + +#### 2.5.6. 获取用键盘输入常用的两种方法 + +方法 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(); +``` + +## 3. Java 核心技术 + +### 3.1. 集合 + +#### 3.1.1. Collections 工具类和 Arrays 工具类常见方法总结 + +详见笔主的这篇文章: https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/basic/Arrays,CollectionsCommonMethods.md + +### 3.2. 异常 + +#### 3.2.1. Java 异常类层次结构图 + +![](images/Java异常类层次结构图.png) -## 29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么? +

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

-**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。 - -**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如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) +![](images/Java异常类层次结构图2.png) +

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

-由上图可以看出: +在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 **Throwable 类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。 -线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 +**Error(错误):是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。 -> 操作系统隐藏 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(运行中)** 状态 。 +这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java 虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。 -![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异常类层次结构图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/Exception.png) - - - -在 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** (下标越界异常)。 +**Exception(异常):是程序本身可以处理的异常**。Exception 类有一个重要的子类 **RuntimeException**。RuntimeException 异常由 Java 虚拟机抛出。**NullPointerException**(要访问的变量没有引用任何对象时,抛出该异常)、**ArithmeticException**(算术运算异常,一个整数除以 0 时,抛出该异常)和 **ArrayIndexOutOfBoundsException** (下标越界异常)。 **注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。** -### Throwable类常用方法 +#### 3.2.2. Throwable 类常用方法 -- **public string getMessage()**:返回异常发生时的简要描述 -- **public string toString()**:返回异常发生时的详细信息 -- **public string getLocalizedMessage()**:返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同 -- **public void printStackTrace()**:在控制台上打印Throwable对象封装的异常信息 +- **`public string getMessage()`**:返回异常发生时的简要描述 +- **`public string toString()`**:返回异常发生时的详细信息 +- **`public string getLocalizedMessage()`**:返回异常对象的本地化信息。使用 `Throwable` 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 `getMessage()`返回的结果相同 +- **`public void printStackTrace()`**:在控制台上打印 `Throwable` 对象封装的异常信息 -### 异常处理总结 +#### 3.2.3. try-catch-finally -- **try 块:** 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。 -- **catch 块:** 用于处理try捕获到的异常。 -- **finally 块:** 无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return -语句时,finally语句块将在方法返回之前被执行。 +- **try 块:** 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。 +- **catch 块:** 用于处理 try 捕获到的异常。 +- **finally 块:** 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。 -**在以下4种特殊情况下,finally块不会被执行:** +**在以下 4 种特殊情况下,finally 块不会被执行:** -1. 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行 -2. 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数 ;若该语句在异常语句之后,finally会执行 +1. 在 finally 语句块第一行发生了异常。 因为在其他行,finally 块还是会得到执行 +2. 在前面的代码中用了 System.exit(int)已退出程序。 exit 是带参函数 ;若该语句在异常语句之后,finally 会执行 3. 程序所在的线程死亡。 -4. 关闭CPU。 +4. 关闭 CPU。 -下面这部分内容来自issue:。 +下面这部分内容来自 issue:。 -**注意:** 当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会覆盖原始的返回值。如下: +**注意:** 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。如下: ```java +public class Test { public static int f(int value) { try { return value * value; @@ -462,95 +1243,144 @@ final关键字主要用在三个地方:变量、方法、类。 } } } +} ``` -如果调用 `f(2)`,返回值将是0,因为finally语句的返回值覆盖了try语句块的返回值。 +如果调用 `f(2)`,返回值将是 0,因为 finally 语句的返回值覆盖了 try 语句块的返回值。 -## 33 Java序列化中如果有些字段不想进行序列化,怎么办? +#### 3.2.4. 使用 `try-with-resources` 来代替`try-catch-finally` -对于不想进行序列化的变量,使用transient关键字修饰。 +1. **适用范围(资源的定义):** 任何实现 `java.lang.AutoCloseable`或者``java.io.Closeable` 的对象 +2. **关闭资源和final的执行顺序:** 在 `try-with-resources` 语句中,任何 catch 或 finally 块在声明的资源关闭后运行 -transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。 +《Effecitve Java》中明确指出: -## 34 获取用键盘输入常用的两种方法 +> 面对必须要关闭的资源,我们总是应该优先使用 `try-with-resources` 而不是`try-finally`。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。`try-with-resources`语句让我们更容易编写必须要关闭的资源的代码,若采用`try-finally`则几乎做不到这点。 -方法1:通过 Scanner +Java 中类似于`InputStream`、`OutputStream` 、`Scanner` 、`PrintWriter`等的资源都需要我们调用`close()`方法来手动关闭,一般情况下我们都是通过`try-catch-finally`语句来实现这个需求,如下: ```java -Scanner input = new Scanner(System.in); -String s = input.nextLine(); -input.close(); + //读取文本文件的内容 + 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(); + } + } ``` -方法2:通过 BufferedReader +使用Java 7之后的 `try-with-resources` 语句改造上面的代码: ```java -BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); -String s = input.readLine(); +try (Scanner scanner = new Scanner(new File("test.txt"))) { + while (scanner.hasNext()) { + System.out.println(scanner.nextLine()); + } +} catch (FileNotFoundException fnfe) { + fnfe.printStackTrace(); +} ``` -## 35 Java 中 IO 流 +当然多个资源需要关闭的时候,使用 `try-with-resources` 实现起来也非常简单,如果你还是用`try-catch-finally`可能会带来很多问题。 -### Java 中 IO 流分为几种? +通过使用分号分隔,可以在`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(); + } +``` -Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。 +### 3.3. 多线程 - - InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 - - OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 +#### 3.3.1. 简述线程、程序、进程的基本概念。以及他们之间关系是什么? + +**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 + +**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。 + +**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 +线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。 + +#### 3.3.2. 线程有哪些基本状态? + +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(终止)** 状态。 + +### 3.4. 文件与 I\O 流 + +#### 3.4.1. 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) -### 既然有了字节流,为什么还要有字符流? +##### 3.4.1.1. 既然有了字节流,为什么还要有字符流? 问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** 回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 -### BIO,NIO,AIO 有什么区别? +##### 3.4.1.2. BIO,NIO,AIO 有什么区别? -- **BIO (Blocking I/O):** 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 -- **NIO (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,不过又放弃了。 +- **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 - -详见笔主的这篇文章: - -## 37. Collections 工具类和 Arrays 工具类常见方法总结 - -详见笔主的这篇文章: - -### 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) - -## 参考 +## 4. 参考 - 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 -## 公众号 +## 5. 公众号 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! +**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java 面试突击"** 即可免费领取! -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 +**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - diff --git a/docs/java/Java疑难点.md b/docs/java/Java疑难点.md index 1a10e958..41199449 100644 --- a/docs/java/Java疑难点.md +++ b/docs/java/Java疑难点.md @@ -1,23 +1,23 @@ -- [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.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-操作) @@ -52,9 +52,9 @@ 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)); - } + // 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。 + return (a == b) || (a != null && a.equals(b)); +} ``` **注意:** @@ -104,14 +104,18 @@ System.out.println(a == b);// false 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 + +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 */ ``` ### 1.3.2. BigDecimal 的大小比较 -`a.compareTo(b)` : 返回 -1 表示小于,0 表示 等于, 1表示 大于。 +`a.compareTo(b)` : 返回 -1 表示 `a` 小于 `b`,0 表示 `a` 等于 `b` , 1表示 `a` 大于 `b`。 ```java BigDecimal a = new BigDecimal("1.0"); @@ -167,7 +171,7 @@ Reference:《阿里巴巴Java开发手册》 `Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个List集合。 ```java -String[] myArray = { "Apple", "Banana", "Orange" }; +String[] myArray = {"Apple", "Banana", "Orange"}; List myList = Arrays.asList(myArray); //上面两个语句等价于下面一条语句 List myList = Arrays.asList("Apple","Banana", "Orange"); @@ -177,8 +181,9 @@ JDK 源码对于这个方法的说明: ```java /** - *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。 - */ + *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁, + * 与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。 + */ public static List asList(T... a) { return new ArrayList<>(a); } @@ -197,12 +202,12 @@ public static List asList(T... a) { `Arrays.asList()`是泛型方法,传入的对象必须是对象数组。 ```java -int[] myArray = { 1, 2, 3 }; +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); +int[] array = (int[]) myList.get(0); System.out.println(array[0]);//1 ``` 当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时List 的唯一元素就是这个数组,这也就解释了上面的代码。 @@ -210,7 +215,7 @@ System.out.println(array[0]);//1 我们使用包装类型数组就可以解决这个问题。 ```java -Integer[] myArray = { 1, 2, 3 }; +Integer[] myArray = {1, 2, 3}; ``` **使用集合的修改方法:`add()`、`remove()`、`clear()`会抛出异常。** @@ -296,7 +301,7 @@ static List arrayToList(final T[] array) { for (final T s : array) { l.add(s); } - return (l); + return l; } ``` @@ -344,6 +349,14 @@ List list = new ArrayList(); CollectionUtils.addAll(list, str); ``` +**6. 使用 Java9 的 `List.of()`方法** +``` java +Integer[] array = {1, 2, 3}; +List list = List.of(array); +System.out.println(list); /* [1, 2, 3] */ +/* 不支持基本数据类型 */ +``` + ## 2.2. Collection.toArray()方法使用的坑&如何反转数组 该方法是一个泛型方法:` T[] toArray(T[] a);` 如果`toArray`方法中没有传递任何参数的话返回的是`Object`类型数组。 @@ -365,6 +378,16 @@ s=list.toArray(new String[0]);//没有指定类型的话会报错 > **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] */ +``` + `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/Multithread/AQS.md b/docs/java/Multithread/AQS.md index ab399087..facba05d 100644 --- a/docs/java/Multithread/AQS.md +++ b/docs/java/Multithread/AQS.md @@ -84,7 +84,7 @@ protected final boolean compareAndSetState(int expect, int update) { - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 - 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。 -> 说明:下面这部分关于 `ReentrantLock` 源代码内容节选自:https://www.javadoop.com/post/AbstractQueuedSynchronizer-2,这是一篇很不错文章,推荐阅读。 +> 说明:下面这部分关于 `ReentrantLock` 源代码内容节选自:https://www.javadoop.com/post/AbstractQueuedSynchronizer-2 ,这是一篇很不错文章,推荐阅读。 **下面来看 ReentrantLock 中相关的源代码:** @@ -238,7 +238,9 @@ tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true ### 3 Semaphore(信号量)-允许多个线程同时访问 -**synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。** 示例代码如下: +**synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。** + +示例代码如下: ```java /** @@ -288,9 +290,9 @@ public class SemaphoreExample1 { 当然一次也可以一次拿取和释放多个许可,不过一般没有必要这样做: ```java - semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4 - test(threadnum); - semaphore.release(5);// 获取5个许可,所以可运行线程数量为20/5=4 +semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4 +test(threadnum); +semaphore.release(5);// 获取5个许可,所以可运行线程数量为20/5=4 ``` 除了 `acquire`方法之外,另一个比较常用的与之对应的方法是`tryAcquire`方法,该方法如果获取不到许可就立即返回 false。 @@ -314,21 +316,21 @@ Semaphore 有两种模式,公平模式和非公平模式。 **这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。** -由于篇幅问题,如果对 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数量的线程能自旋成功,便限制了执行任务线程的数量。 -- https://blog.csdn.net/qq_19431333/article/details/70212663 +由于篇幅问题,如果对 Semaphore 源码感兴趣的朋友可以看下这篇文章:https://juejin.im/post/5ae755366fb9a07ab508adc6 ### 4 CountDownLatch (倒计时器) -CountDownLatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。在 Java 并发中,countdownlatch 的概念是一个常见的面试题,所以一定要确保你很好的理解了它。 +CountDownLatch允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。在 Java 并发中,countdownlatch 的概念是一个常见的面试题,所以一定要确保你很好的理解了它。 -#### 4.1 CountDownLatch 的三种典型用法 +CountDownLatch是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用countDown方法时,其实使用了`tryReleaseShared`方法以CAS的操作来减少state,直至state为0就代表所有的线程都调用了countDown方法。当调用await方法的时候,如果state不为0,就代表仍然有线程没有调用countDown方法,那么就把已经调用过countDown的线程都放入阻塞队列Park,并自旋CAS判断state == 0,直至最后一个线程调用了countDown,使得state == 0,于是阻塞的线程便判断成功,全部往下执行。 -① 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :`new CountDownLatch(n)`,每当一个任务线程执行完毕,就将计数器减 1 `countdownlatch.countDown()`,当计数器的值变为 0 时,在`CountDownLatch上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 +#### 4.1 CountDownLatch 的两种典型用法 -② 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 :`new CountDownLatch(1)`,多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。 - -③ 死锁检测:一个非常方便的使用场景是,你可以使用 n 个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。 +1. 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :`new CountDownLatch(n)`,每当一个任务线程执行完毕,就将计数器减 1 `countdownlatch.countDown()`,当计数器的值变为 0 时,在`CountDownLatch上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 +2. 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 :`new CountDownLatch(1)`,多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。 #### 4.2 CountDownLatch 的使用示例 @@ -377,15 +379,27 @@ 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()方法,恢复执行自己的任务。 +其他 N 个线程必须引用闭锁对象,因为他们需要通知 `CountDownLatch` 对象,他们已经完成了各自的任务。这种通知机制是通过 `CountDownLatch.countDown()`方法来完成的;每调用一次这个方法,在构造函数中初始化的 count 值就减 1。所以当 N 个线程都调 用了这个方法,count 的值等于 0,然后主线程就能通过 `await()`方法,恢复执行自己的任务。 + +再插一嘴:`CountDownLatch` 的 `await()` 方法使用不当很容易产生死锁,比如我们上面代码中的 for 循环改为: + +```java +for (int i = 0; i < threadCount-1; i++) { +....... +} +``` + +这样就导致 `count` 的值没办法等于 0,然后就会导致一直等待。 + +如果对CountDownLatch源码感兴趣的朋友,可以查看: [【JUC】JDK1.8源码分析之CountDownLatch(五)](https://www.cnblogs.com/leesf456/p/5406191.html) #### 4.3 CountDownLatch 的不足 CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。 -#### 4.4 CountDownLatch 相常见面试题: +#### 4.4 CountDownLatch 相常见面试题 解释一下 CountDownLatch 概念? @@ -399,6 +413,8 @@ CountDownLatch 类中主要的方法? CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。 +> CountDownLatch的实现是基于AQS的,而CycliBarrier是基于 ReentrantLock(ReentrantLock也属于AQS同步器)和 Condition 的. + CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用`await`方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 再来看一下它的构造函数: diff --git a/docs/java/Multithread/Atomic.md b/docs/java/Multithread/Atomic.md index 627ea3d4..14c5ac5d 100644 --- a/docs/java/Multithread/Atomic.md +++ b/docs/java/Multithread/Atomic.md @@ -56,15 +56,61 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是 **引用类型** - AtomicReference:引用类型原子类 -- AtomicReferenceFieldUpdater:原子更新引用类型里的字段 -- AtomicMarkableReference :原子更新带有标记位的引用类型 +- AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,~~也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。~~ +- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 **对象的属性修改类型** - AtomicIntegerFieldUpdater:原子更新整型字段的更新器 - AtomicLongFieldUpdater:原子更新长整型字段的更新器 -- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 -- AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 +- AtomicReferenceFieldUpdater:原子更新引用类型里的字段 + +> 修正: **AtomicMarkableReference 不能解决ABA问题** **[issue#626](https://github.com/Snailclimb/JavaGuide/issues/626)** + +```java + /** + +AtomicMarkableReference是将一个boolean值作是否有更改的标记,本质就是它的版本号只有两个,true和false, + +修改的时候在这两个版本号之间来回切换,这样做并不能解决ABA的问题,只是会降低ABA问题发生的几率而已 + +@author : mazh + +@Date : 2020/1/17 14:41 +*/ + +public class SolveABAByAtomicMarkableReference { + + private static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(100, false); + + public static void main(String[] args) { + + Thread refT1 = new Thread(() -> { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + atomicMarkableReference.compareAndSet(100, 101, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked()); + atomicMarkableReference.compareAndSet(101, 100, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked()); + }); + + Thread refT2 = new Thread(() -> { + boolean marked = atomicMarkableReference.isMarked(); + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + boolean c3 = atomicMarkableReference.compareAndSet(100, 101, marked, !marked); + System.out.println(c3); // 返回true,实际应该返回false + }); + + refT1.start(); + refT2.start(); + } + } +``` **CAS ABA 问题** - 描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。 @@ -268,8 +314,8 @@ 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 常见方法使用 @@ -306,8 +352,8 @@ public class AtomicIntegerArrayTest { 基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。 - AtomicReference:引用类型原子类 -- AtomicStampedReference:原子更新引用类型里的字段原子类 -- AtomicMarkableReference :原子更新带有标记位的引用类型 +- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 +- AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,~~也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。~~ 上面三个类提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。 @@ -488,7 +534,7 @@ currentValue=true, currentMark=true, wCasResult=true - AtomicIntegerFieldUpdater:原子更新整形字段的更新器 - AtomicLongFieldUpdater:原子更新长整形字段的更新器 -- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 +- AtomicReferenceFieldUpdater :原子更新引用类型里的字段的更新器 要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。 @@ -545,6 +591,10 @@ class User { 23 ``` +## Reference + +- 《Java并发编程的艺术》 + ## 公众号 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 @@ -553,4 +603,4 @@ class User { **Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) +![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) \ No newline at end of file diff --git a/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md b/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md index 51b21fd0..e5561246 100644 --- a/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md +++ b/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md @@ -1,65 +1,118 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 +点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《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) + - [1.synchronized 关键字](#1synchronized-关键字) + - [1.1.说一说自己对于 synchronized 关键字的了解](#11说一说自己对于-synchronized-关键字的了解) + - [1.2. 说说自己是怎么使用 synchronized 关键字](#12-说说自己是怎么使用-synchronized-关键字) + - [1.3. 构造方法可以使用 synchronized 关键字修饰么?](#13-构造方法可以使用-synchronized-关键字修饰么) + - [1.3. 讲一下 synchronized 关键字的底层原理](#13-讲一下-synchronized-关键字的底层原理) + - [1.3.1. synchronized 同步语句块的情况](#131-synchronized-同步语句块的情况) + - [1.3.2. `synchronized` 修饰方法的的情况](#132-synchronized-修饰方法的的情况) + - [1.3.3.总结](#133总结) + - [1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的-synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗) + - [1.5. 谈谈 synchronized 和 ReentrantLock 的区别](#15-谈谈-synchronized-和-reentrantlock-的区别) + - [1.5.1. 两者都是可重入锁](#151-两者都是可重入锁) + - [1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API](#152synchronized-依赖于-jvm-而-reentrantlock-依赖于-api) + - [1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能](#153reentrantlock-比-synchronized-增加了一些高级功能) + - [2. volatile 关键字](#2-volatile-关键字) + - [2.1. CPU 缓存模型](#21-cpu-缓存模型) + - [2.2. 讲一下 JMM(Java 内存模型)](#22-讲一下-jmmjava-内存模型) + - [2.3. 并发编程的三个重要特性](#23-并发编程的三个重要特性) + - [2.4. 说说 synchronized 关键字和 volatile 关键字的区别](#24-说说-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-如何创建线程池) + - [4.5 ThreadPoolExecutor 类分析](#45-threadpoolexecutor-类分析) + - [4.5.1 `ThreadPoolExecutor`构造函数重要参数分析](#451-threadpoolexecutor构造函数重要参数分析) + - [4.5.2 `ThreadPoolExecutor` 饱和策略](#452-threadpoolexecutor-饱和策略) + - [4.6 一个简单的线程池 Demo](#46-一个简单的线程池-demo) + - [4.7 线程池原理分析](#47-线程池原理分析) + - [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-组件总结) + - [6.4. 用过 CountDownLatch 么?什么场景下用的?](#64-用过-countdownlatch-么什么场景下用的) + - [7 Reference](#7-reference) + - [公众号](#公众号) + + - # Java 并发进阶常见面试题总结 -## 1. synchronized 关键字 +## 1.synchronized 关键字 -### 1.1. 说一说自己对于 synchronized 关键字的了解 +![](images/interview-questions/synchronized关键字.png) -synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 +### 1.1.说一说自己对于 synchronized 关键字的了解 -另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 +**`synchronized` 关键字解决的是多个线程之间访问资源的同步性,`synchronized`关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。** +另外,在 Java 早期版本中,`synchronized` 属于 **重量级锁**,效率低下。 -### 1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 +**为什么呢?** -**synchronized关键字最主要的三种使用方式:** +因为监视器锁(monitor)是依赖于底层的操作系统的 `Mutex Lock` 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。 -- **修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 -- **修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。 -- **修饰代码块:** 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 +庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 -**总结:** synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能! +所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。 -下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。 +### 1.2. 说说自己是怎么使用 synchronized 关键字 + +**synchronized 关键字最主要的三种使用方式:** + +**1.修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得 **当前对象实例的锁** + +```java +synchronized void method() { + //业务代码 +} +``` + +**2.修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 **当前 class 的锁**。因为静态成员不属于任何一个实例对象,是类成员( _static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份_)。所以,如果一个线程 A 调用一个实例对象的非静态 `synchronized` 方法,而线程 B 需要调用这个实例对象所属类的静态 `synchronized` 方法,是允许的,不会发生互斥现象,**因为访问静态 `synchronized` 方法占用的锁是当前类的锁,而访问非静态 `synchronized` 方法占用的锁是当前实例对象锁**。 + +```java +synchronized void staic method() { + //业务代码 +} +``` + +**3.修饰代码块** :指定加锁对象,对给定对象/类加锁。`synchronized(this|object)` 表示进入同步代码库前要获得**给定对象的锁**。`synchronized(类.class)` 表示进入同步代码前要获得 **当前 class 的锁** + +```java +synchronized(this) { + //业务代码 +} +``` + +**总结:** + +- `synchronized` 关键字加到 `static` 静态方法和 `synchronized(class)` 代码块上都是是给 Class 类上锁。 +- `synchronized` 关键字加到实例方法上是给对象实例上锁。 +- 尽量不要使用 `synchronized(String a)` 因为 JVM 中,字符串常量池具有缓存功能! + +下面我以一个常见的面试题为例讲解一下 `synchronized` 关键字的具体使用。 面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” @@ -73,7 +126,7 @@ public class Singleton { private Singleton() { } - public static Singleton getUniqueInstance() { + public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (uniqueInstance == null) { //类对象加锁 @@ -87,23 +140,30 @@ public class Singleton { } } ``` -另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。 -uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: +另外,需要注意 `uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要。 -1. 为 uniqueInstance 分配内存空间 -2. 初始化 uniqueInstance -3. 将 uniqueInstance 指向分配的内存地址 +`uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要的, `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行: -但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 +1. 为 `uniqueInstance` 分配内存空间 +2. 初始化 `uniqueInstance` +3. 将 `uniqueInstance` 指向分配的内存地址 -使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 +但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 `getUniqueInstance`() 后发现 `uniqueInstance` 不为空,因此返回 `uniqueInstance`,但此时 `uniqueInstance` 还未被初始化。 + +使用 `volatile` 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 + +### 1.3. 构造方法可以使用 synchronized 关键字修饰么? + +先说结论:**构造方法不能使用 synchronized 关键字修饰。** + +构造方法本身就属于线程安全的,不存在同步的构造方法一说。 ### 1.3. 讲一下 synchronized 关键字的底层原理 **synchronized 关键字底层原理属于 JVM 层面。** -**① synchronized 同步语句块的情况** +#### 1.3.1. synchronized 同步语句块的情况 ```java public class SynchronizedDemo { @@ -116,15 +176,25 @@ public class SynchronizedDemo { ``` -通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 +通过 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` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。** -**② synchronized 修饰方法的的情况** +当执行 `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 { @@ -137,78 +207,112 @@ public class SynchronizedDemo2 { ![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理2.png) -synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 +`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 +#### 1.3.3.总结 -### 1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗 +`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。 + +`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。 + +**不过两者的本质都是对对象监视器 monitor 的获取。** + +### 1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗 JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 -关于这几种优化的详细信息可以查看笔主的这篇文章: +关于这几种优化的详细信息可以查看下面这几篇文章: -### 1.5. 谈谈 synchronized和ReentrantLock 的区别 +- [Java 性能 -- synchronized 锁升级优化](https://blog.csdn.net/qq_34337272/article/details/108498442) +- [Java6 及以上版本对 synchronized 的优化](https://www.cnblogs.com/wuqinglong/p/9945618.html) +### 1.5. 谈谈 synchronized 和 ReentrantLock 的区别 -**① 两者都是可重入锁** +#### 1.5.1. 两者都是可重入锁 -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 +**“可重入锁”** 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。 -**② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API** +#### 1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 +`synchronized` 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 `synchronized` 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。`ReentrantLock` 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 -**③ ReentrantLock 比 synchronized 增加了一些高级功能** +#### 1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能 -相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)** +相比`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`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 +- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 +- **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()`和`notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法。 -如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。 +> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 Condition 接口默认提供的。而`synchronized`关键字就相当于整个 Lock 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而`Condition`实例的`signalAll()`方法 只会唤醒注册在该`Condition`实例中的所有等待线程。 -**④ 性能已不是选择标准** +**如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。性能已不是选择标准** -## 2. volatile关键字 +## 2. volatile 关键字 -### 2.1. 讲一下Java内存模型 +我们先要从 **CPU 缓存模型** 说起! +### 2.1. CPU 缓存模型 -在 JDK1.2 之前,Java的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。 +**为什么要弄一个 CPU 高速缓存呢?** -![数据不一致](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数据不一致.png) +类比我们开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。 **CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。** -要解决这个问题,就需要把变量声明为**volatile**,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。 +我们甚至可以把 **内存可以看作外存的高速缓存**,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。 -说白了, **volatile** 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。 +总结:**CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。** -![volatile关键字的可见性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/volatile关键字的可见性.png) +为了更好地理解,我画了一个简单的 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) -### 2.2. 说说 synchronized 关键字和 volatile 关键字的区别 +**CPU Cache 的工作方式:** - synchronized关键字和volatile关键字比较 +先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 **内存缓存不一致性的问题** !比如我执行一个 i++操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。 -- **volatile关键字**是线程同步的**轻量级实现**,所以**volatile性能肯定比synchronized关键字要好**。但是**volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块**。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,**实际开发中使用 synchronized 关键字的场景还是更多一些**。 -- **多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞** -- **volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。** -- **volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。** +**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简介 +### 3.1. ThreadLocal 简介 -通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** +通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK 中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** **如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** -再举个简单的例子: +再举个简单的例子: -比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来避免这两个线程竞争的。 +比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。 -### 3.2. ThreadLocal示例 +### 3.2. ThreadLocal 示例 相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 @@ -273,9 +377,9 @@ Thread Name= 8 formatter = yy-M-d ah:mm Thread Name= 9 formatter = yy-M-d ah:mm ``` -从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。 +从输出中可以看出,Thread-0 已经改变了 formatter 的值,但仍然是 thread-2 默认格式化程序与初始化值相同,其他线程也一样。 -上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。 +上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA 会提示你转换为 Java8 的格式(IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法`withInitial()`,将 Supplier 功能接口作为参数。 ```java private static final ThreadLocal formatter = new ThreadLocal(){ @@ -287,7 +391,7 @@ Thread Name= 9 formatter = yy-M-d ah:mm }; ``` -### 3.3. ThreadLocal原理 +### 3.3. ThreadLocal 原理 从 `Thread`类源代码入手。 @@ -303,7 +407,7 @@ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; } ``` -从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 +从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是 null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set()`方法。 `ThreadLocal`类的`set()`方法 @@ -323,19 +427,25 @@ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; 通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。 -**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。 +**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为 key ,Object 对象为 value 的键值对。** -`ThreadLocal` 内部维护的是一个类似 `Map` 的`ThreadLocalMap` 数据结构,`key` 为当前对象的 `Thread` 对象,值为 Object 对象。 +```java +ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { + ...... +} +``` -![ThreadLocal数据结构](https://upload-images.jianshu.io/upload_images/7432604-ad2ff581127ba8cc.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/806) +比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。 + +![ThreadLocal数据结构](images/threadlocal数据结构.png) `ThreadLocalMap`是`ThreadLocal`的静态内部类。 -![ThreadLocal内部类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadLocal内部类.png) +![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()`方法 +`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> { @@ -353,7 +463,7 @@ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; > 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 > -> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 +> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 ## 4. 线程池 @@ -369,9 +479,9 @@ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 - **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 -### 4.2. 实现Runnable接口和Callable接口的区别 +### 4.2. 实现 Runnable 接口和 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)`)。 @@ -401,7 +511,7 @@ public interface Callable { } ``` -### 4.3. 执行execute()方法和submit()方法的区别是什么呢? +### 4.3. 执行 execute()方法和 submit()方法的区别是什么呢? 1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** 2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 @@ -435,23 +545,23 @@ public interface Callable { ### 4.4. 如何创建线程池 -《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 +《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 > 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://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadPoolExecutor构造方法.png) -**方式二:通过Executor 框架的工具类Executors来实现** -我们可以创建三种类型的ThreadPoolExecutor: +**方式二:通过 Executor 框架的工具类 Executors 来实现** +我们可以创建三种类型的 ThreadPoolExecutor: - **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 - **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 - **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 -对应Executors工具类中的方法如图所示: +对应 Executors 工具类中的方法如图所示: ![Executor框架的工具类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Executor框架的工具类.png) ### 4.5 ThreadPoolExecutor 类分析 @@ -509,13 +619,13 @@ public interface Callable { 如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略: - **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 +- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 - **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 - **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了) -### 4.6 一个简单的线程池Demo:`Runnable`+`ThreadPoolExecutor` +### 4.6 一个简单的线程池 Demo 为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。 @@ -702,20 +812,19 @@ pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 ## 5. Atomic 原子类 -### 5.1. 介绍一下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类? +### 5.2. JUC 包中的原子类是哪 4 类? -**基本类型** +**基本类型** 使用原子的方式更新基本类型 @@ -727,7 +836,6 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是 使用原子的方式更新数组里的某个元素 - - AtomicIntegerArray:整形数组原子类 - AtomicLongArray:长整形数组原子类 - AtomicReferenceArray:引用类型数组原子类 @@ -744,10 +852,9 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是 - AtomicLongFieldUpdater:原子更新长整形字段的更新器 - AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - ### 5.3. 讲讲 AtomicInteger 的使用 - **AtomicInteger 类常用方法** +**AtomicInteger 类常用方法** ```java public final int get() //获取当前的值 @@ -759,9 +866,10 @@ boolean compareAndSet(int expect, int update) //如果输入的数值等于预 public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 ``` - **AtomicInteger 类的使用示例** +**AtomicInteger 类的使用示例** 使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。 + ```java class AtomicIntegerTest { private AtomicInteger count = new AtomicInteger(); @@ -769,7 +877,7 @@ class AtomicIntegerTest { public void increment() { count.incrementAndGet(); } - + public int getCount() { return count.get(); } @@ -800,7 +908,7 @@ AtomicInteger 类的部分源码: AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 -CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 +CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg) @@ -808,47 +916,46 @@ CAS的原理是拿期望的值和原本的一个值作比较,如果相同则 ### 6.1. AQS 介绍 -AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。 +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非常轻松容易地构造出符合我们自己需求的同步器。 +AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。 ### 6.2. AQS 原理分析 -AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。 +AQS 原理这部分参考了部分博客,在 5.2 节末尾放了链接。 -> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 +> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 -下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。 +下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。 #### 6.2.1. AQS 原理概览 -**AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。** +**AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。** -> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 - -看个AQS(AbstractQueuedSynchronizer)原理图: +> 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对该同步状态进行原子操作实现对其值的修改。 +AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。 ```java private volatile int state;//共享变量,使用volatile修饰保证线程可见性 ``` -状态信息通过protected类型的getState,setState,compareAndSetState进行操作 +状态信息通过 protected 类型的 getState,setState,compareAndSetState 进行操作 ```java //返回同步状态的当前值 -protected final int getState() { +protected final int getState() { return state; } // 设置同步状态的值 -protected final void setState(int newState) { +protected final void setState(int newState) { state = newState; } //原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) @@ -859,27 +966,27 @@ protected final boolean compareAndSetState(int expect, int update) { #### 6.2.2. AQS 对资源的共享方式 -**AQS定义两种资源共享方式** +**AQS 定义两种资源共享方式** -- **Exclusive**(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁: - - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 - - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 -- **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 +- **Exclusive**(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁: + - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 + - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 +- **Share**(共享):多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 -ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。 +ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。 -不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。 +不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。 -#### 6.2.3. AQS底层使用了模板方法模式 +#### 6.2.3. AQS 底层使用了模板方法模式 同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): -1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放) -2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 +1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放) +2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。 -**AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:** +**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:** ```java isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 @@ -890,13 +997,13 @@ tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true ``` -默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 +默认情况下,每个方法都抛出 `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个子线程是并行执行的,每个子线程执行完后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()函数返回,继续后余动作。 -一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 +一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 推荐两篇 AQS 原理和相关源码分析的文章: @@ -906,14 +1013,97 @@ tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true ### 6.3. AQS 组件总结 - **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。 -- **CountDownLatch (倒计时器):** CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 -- **CyclicBarrier(循环栅栏):** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 +- **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并发编程的艺术》 +- 《Java 并发编程的艺术》 - http://www.cnblogs.com/waterystone/p/4920797.html - https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html - @@ -922,8 +1112,8 @@ tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! +**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 +**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md b/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md index ca2518ee..59c14848 100644 --- a/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md +++ b/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md @@ -1,5 +1,4 @@ -点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 - + - [Java 并发基础常见面试题总结](#java-并发基础常见面试题总结) - [1. 什么是线程和进程?](#1-什么是线程和进程) @@ -22,6 +21,7 @@ - [10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#10-为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法) - [公众号](#公众号) + # Java 并发基础常见面试题总结 @@ -84,7 +84,7 @@ public class MultiThread { 从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 -**总结:** 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反 +**总结:** **线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。** 下面是该知识点的扩展内容! @@ -110,7 +110,7 @@ public class MultiThread { ### 2.4. 一句话简单了解堆和方法区 -堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 +堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 ## 3. 说说并发与并行的区别? @@ -131,7 +131,7 @@ public class MultiThread { ## 5. 使用多线程可能带来什么问题? -并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。 +并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。 ## 6. 说说线程的生命周期和状态? @@ -143,9 +143,11 @@ 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(运行中)** 状态 。 +> 操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 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(运行中)** 状态 。 ![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png) @@ -165,7 +167,7 @@ Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的 ### 8.1. 认识线程死锁 -多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 +线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 @@ -232,23 +234,12 @@ Thread[线程 2,5,main]waiting get resource1 ### 8.2. 如何避免线程死锁? -我们只要破坏产生死锁的四个条件中的其中一个就可以了。 +我上面说了产生死锁的四个必要条件,为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了。现在我们来挨个分析一下: -**破坏互斥条件** - -这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 - -**破坏请求与保持条件** - -一次性申请所有的资源。 - -**破坏不剥夺条件** - -占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 - -**破坏循环等待条件** - -靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 +1. **破坏互斥条件** :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 +2. **破坏请求与保持条件** :一次性申请所有的资源。 +3. **破坏不剥夺条件** :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 +4. **破坏循环等待条件** :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 我们对线程 2 的代码修改成下面这样就不会产生死锁了。 @@ -310,3 +301,5 @@ new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启 **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 index 06cdbea5..6383ef1d 100644 --- a/docs/java/Multithread/ThreadLocal.md +++ b/docs/java/Multithread/ThreadLocal.md @@ -1,170 +1,914 @@ -[ThreadLocal造成OOM内存溢出案例演示与原理分析](https://blog.csdn.net/xlgen157387/article/details/78298840) +> 本文来自一枝花算不算浪漫投稿, 原文地址:[https://juejin.im/post/5eacc1c75188256d976df748](https://juejin.im/post/5eacc1c75188256d976df748)。 -[深入理解 Java 之 ThreadLocal 工作原理]() +### 前言 -## ThreadLocal +![](./images/thread-local/1.png) -### ThreadLocal简介 +**全文共10000+字,31张图,这篇文章同样耗费了不少的时间和精力才创作完成,原创不易,请大家点点关注+在看,感谢。** -通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** +对于`ThreadLocal`,大家的第一反应可能是很简单呀,线程的变量副本,每个线程隔离。那这里有几个问题大家可以思考一下: -**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** +- `ThreadLocal`的key是**弱引用**,那么在 `ThreadLocal`.get()的时候,发生**GC**之后,key是否为**null**? +- `ThreadLocal`中`ThreadLocalMap`的**数据结构**? +- `ThreadLocalMap`的**Hash算法**? +- `ThreadLocalMap`中**Hash冲突**如何解决? +- `ThreadLocalMap`的**扩容机制**? +- `ThreadLocalMap`中**过期key的清理机制**?**探测式清理**和**启发式清理**流程? +- `ThreadLocalMap.set()`方法实现原理? +- `ThreadLocalMap.get()`方法实现原理? +- 项目中`ThreadLocal`使用情况?遇到的坑? +- ...... -再举个简单的例子: +上述的一些问题你是否都已经掌握的很清楚了呢?本文将围绕这些问题使用图文方式来剖析`ThreadLocal`的**点点滴滴**。 -比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来这两个线程竞争的。 +### 目录 -### ThreadLocal示例 -相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 + +**注明:** 本文源码基于`JDK 1.8` + +### `ThreadLocal`代码演示 + +我们先看下`ThreadLocal`使用示例: ```java -import java.text.SimpleDateFormat; -import java.util.Random; +public class ThreadLocalTest { + private List messages = Lists.newArrayList(); -public class ThreadLocalExample implements Runnable{ + public static final `ThreadLocal` holder = `ThreadLocal`.withInitial(ThreadLocalTest::new); - // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 - private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); + public static void add(String message) { + holder.get().messages.add(message); + } - 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(); + 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`的清理工作,如下图所示: + +![Yu4oWT.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba9af057e1e?w=1336&h=361&f=png&s=63049) + + +**向后遍历过程中,如果没有找到相同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`值不等于``ThreadLocal`1`,所以需要继续往后迭代查找。 + +迭代到`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`过期可以的两种清理方式:**探测式清理(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 run() { - System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); + 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 { - Thread.sleep(new Random().nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); + runnable.run(); + } finally { + MDC.remove(); } - //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() `方法。 +#### 使用MQ发送消息给第三方系统 -`ThreadLocal`类的`set()`方法 +在MQ发送的消息体中自定义属性`requestId`,接收方消费消息后,自己解析`requestId`使用即可。 -```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/ThreadLocal(未完成).md b/docs/java/Multithread/ThreadLocal(未完成).md new file mode 100644 index 00000000..f69cc1d6 --- /dev/null +++ b/docs/java/Multithread/ThreadLocal(未完成).md @@ -0,0 +1,170 @@ +[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 内存泄露问题 + +`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/Untitled.md b/docs/java/Multithread/Untitled.md new file mode 100644 index 00000000..ca7411cb --- /dev/null +++ b/docs/java/Multithread/Untitled.md @@ -0,0 +1,422 @@ +## synchronized / Lock + +1. JDK 1.5之前 + + ,Java通过 + + synchronized + + 关键字来实现 + + 锁 + + 功能 + + - synchronized是JVM实现的**内置锁**,锁的获取和释放都是由JVM**隐式**实现的 + +2. JDK 1.5 + + ,并发包中新增了 + + Lock接口 + + 来实现锁功能 + + - 提供了与synchronized类似的同步功能,但需要**显式**获取和释放锁 + +3. Lock同步锁是基于 + + Java + + 实现的,而synchronized是基于底层操作系统的 + + Mutex Lock + + 实现的 + + - 每次获取和释放锁都会带来**用户态和内核态的切换**,从而增加系统的**性能开销** + - 在锁竞争激烈的情况下,synchronized同步锁的性能很糟糕 + - 在**JDK 1.5**,在**单线程重复申请锁**的情况下,synchronized锁性能要比Lock的性能**差很多** + +4. **JDK 1.6**,Java对synchronized同步锁做了**充分的优化**,甚至在某些场景下,它的性能已经超越了Lock同步锁 + + + +## 实现原理 + +复制 + +``` +public class SyncTest { + public synchronized void method1() { + } + + public void method2() { + Object o = new Object(); + synchronized (o) { + } + } +} +``` + +复制 + +``` +$ javac -encoding UTF-8 SyncTest.java +$ javap -v SyncTest +``` + +### 修饰方法 + +复制 + +``` +public synchronized void method1(); + descriptor: ()V + flags: ACC_PUBLIC, ACC_SYNCHRONIZED + Code: + stack=0, locals=1, args_size=1 + 0: return +``` + +1. JVM使用**ACC_SYNCHRONIZED**访问标识来区分一个方法是否为**同步方法** + +2. 在方法调用时,会检查方法是否被设置了 + + ACC_SYNCHRONIZED + + 访问标识 + + - 如果是,执行线程会将先尝试**持有Monitor对象**,再执行方法,方法执行完成后,最后**释放Monitor对象** + +### 修饰代码块 + +复制 + +``` +public void method2(); + descriptor: ()V + flags: ACC_PUBLIC + Code: + stack=2, locals=4, args_size=1 + 0: new #2 // class java/lang/Object + 3: dup + 4: invokespecial #1 // Method java/lang/Object."":()V + 7: astore_1 + 8: aload_1 + 9: dup + 10: astore_2 + 11: monitorenter + 12: aload_2 + 13: monitorexit + 14: goto 22 + 17: astore_3 + 18: aload_2 + 19: monitorexit + 20: aload_3 + 21: athrow + 22: return +``` + +1. synchronized修饰同步代码块时,由**monitorenter**和**monitorexit**指令来实现同步 +2. 进入**monitorenter**指令后,线程将**持有**该**Monitor对象**,进入**monitorexit**指令,线程将**释放**该**Monitor对象** + +### 管程模型 + +1. JVM中的**同步**是基于进入和退出**管程**(**Monitor**)对象实现的 + +2. **每个Java对象实例都会有一个Monitor**,Monitor可以和Java对象实例一起被创建和销毁 + +3. Monitor是由**ObjectMonitor**实现的,对应[ObjectMonitor.hpp](https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/objectMonitor.hpp) + +4. 当多个线程同时访问一段同步代码时,会先被放在**EntryList**中 + +5. 当线程获取到Java对象的Monitor时(Monitor是依靠 + + 底层操作系统 + + 的 + + Mutex Lock + + 来实现 + + 互斥 + + 的) + + - 线程申请Mutex成功,则持有该Mutex,其它线程将无法获取到该Mutex + +6. 进入 + + WaitSet + + - 竞争锁**失败**的线程会进入**WaitSet** + - 竞争锁**成功**的线程如果调用**wait**方法,就会**释放当前持有的Mutex**,并且该线程会进入**WaitSet** + - 进入**WaitSet**的进程会等待下一次唤醒,然后进入EntryList**重新排队** + +7. 如果当前线程顺利执行完方法,也会释放Mutex + +8. Monitor依赖于**底层操作系统**的实现,存在**用户态**和**内核态之间**的**切换**,所以增加了**性能开销** + +[![img](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-monitor.png)](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-monitor.png) + +复制 + +``` +ObjectMonitor() { + _header = NULL; + _count = 0; // 记录个数 + _waiters = 0, + _recursions = 0; + _object = NULL; + _owner = NULL; // 持有该Monitor的线程 + _WaitSet = NULL; // 处于wait状态的线程,会被加入 _WaitSet + _WaitSetLock = 0 ; + _Responsible = NULL ; + _succ = NULL ; + _cxq = NULL ; + FreeNext = NULL ; + _EntryList = NULL ; // 多个线程访问同步块或同步方法,会首先被加入 _EntryList + _SpinFreq = 0 ; + _SpinClock = 0 ; + OwnerIsThread = 0 ; + _previous_owner_tid = 0; +} +``` + +## 锁升级优化 + +1. 为了提升性能,在**JDK 1.6**引入**偏向锁、轻量级锁、重量级锁**,用来**减少锁竞争带来的上下文切换** +2. 借助JDK 1.6新增的**Java对象头**,实现了**锁升级**功能 + +### Java对象头 + +1. 在**JDK 1.6**的JVM中,对象实例在**堆内存**中被分为三部分:**对象头**、**实例数据**、**对齐填充** +2. 对象头的组成部分:**Mark Word**、**指向类的指针**、**数组长度**(可选,数组类型时才有) +3. Mark Word记录了**对象**和**锁**有关的信息,在64位的JVM中,Mark Word为**64 bit** +4. 锁升级功能主要依赖于Mark Word中**锁标志位**和**是否偏向锁标志位** +5. synchronized同步锁的升级优化路径:***偏向锁** -> **轻量级锁** -> **重量级锁*** + +[![img](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-mark-word.jpg)](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-mark-word.jpg) + +### 偏向锁 + +1. 偏向锁主要用来优化**同一线程多次申请同一个锁**的竞争,在某些情况下,大部分时间都是同一个线程竞争锁资源 + +2. 偏向锁的作用 + + - 当一个线程再次访问同一个同步代码时,该线程只需对该对象头的**Mark Word**中去判断是否有偏向锁指向它 + - **无需再进入Monitor去竞争对象**(避免用户态和内核态的**切换**) + +3. 当对象被当做同步锁,并有一个线程抢到锁时 + + - 锁标志位还是**01**,是否偏向锁标志位设置为**1**,并且记录抢到锁的**线程ID**,进入***偏向锁状态*** + +4. 偏向锁 + + **不会主动释放锁** + + - 当线程1再次获取锁时,会比较**当前线程的ID**与**锁对象头部的线程ID**是否一致,如果一致,无需CAS来抢占锁 + + - 如果不一致,需要查看 + + 锁对象头部记录的线程 + + 是否存活 + + - 如果**没有存活**,那么锁对象被重置为**无锁**状态(也是一种撤销),然后重新偏向线程2 + + - 如果 + + 存活 + + ,查找线程1的栈帧信息 + + - 如果线程1还是需要继续持有该锁对象,那么暂停线程1(**STW**),**撤销偏向锁**,**升级为轻量级锁** + - 如果线程1不再使用该锁对象,那么将该锁对象设为**无锁**状态(也是一种撤销),然后重新偏向线程2 + +5. 一旦出现其他线程竞争锁资源时,偏向锁就会被 + + 撤销 + + - 偏向锁的撤销**可能需要**等待**全局安全点**,暂停持有该锁的线程,同时检查该线程**是否还在执行该方法** + - 如果还没有执行完,说明此刻有**多个线程**竞争,升级为**轻量级锁**;如果已经执行完毕,唤醒其他线程继续**CAS**抢占 + +6. 在 + + 高并发 + + 场景下,当 + + 大量线程 + + 同时竞争同一个锁资源时,偏向锁会被 + + 撤销 + + ,发生 + + STW + + ,加大了 + + 性能开销 + + - 默认配置 + + - `-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000` + - 默认开启偏向锁,并且**延迟生效**,因为JVM刚启动时竞争非常激烈 + + - 关闭偏向锁 + + - `-XX:-UseBiasedLocking` + + - 直接 + + 设置为重量级锁 + + - `-XX:+UseHeavyMonitors` + +红线流程部分:偏向锁的**获取**和**撤销** +[![img](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-1.png)](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-1.png) + +### 轻量级锁 + +1. 当有另外一个线程竞争锁时,由于该锁处于**偏向锁**状态 + +2. 发现对象头Mark Word中的线程ID不是自己的线程ID,该线程就会执行 + + CAS + + 操作获取锁 + + - 如果获取**成功**,直接替换Mark Word中的线程ID为自己的线程ID,该锁会***保持偏向锁状态*** + - 如果获取**失败**,说明当前锁有一定的竞争,将偏向锁**升级**为轻量级锁 + +3. 线程获取轻量级锁时会有两步 + + - 先把**锁对象的Mark Word**复制一份到线程的**栈帧**中(**DisplacedMarkWord**),主要为了**保留现场**!! + - 然后使用**CAS**,把对象头中的内容替换为**线程栈帧中DisplacedMarkWord的地址** + +4. 场景 + + - 在线程1复制对象头Mark Word的同时(CAS之前),线程2也准备获取锁,也复制了对象头Mark Word + - 在线程2进行CAS时,发现线程1已经把对象头换了,线程2的CAS失败,线程2会尝试使用**自旋锁**来等待线程1释放锁 + +5. 轻量级锁的适用场景:线程**交替执行**同步块,***绝大部分的锁在整个同步周期内都不存在长时间的竞争*** + +红线流程部分:升级轻量级锁 +[![img](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-2.png)](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-2.png) + +### 自旋锁 / 重量级锁 + +1. 轻量级锁 + + CAS + + 抢占失败,线程将会被挂起进入 + + 阻塞 + + 状态 + + - 如果正在持有锁的线程在**很短的时间**内释放锁资源,那么进入**阻塞**状态的线程被**唤醒**后又要**重新抢占**锁资源 + +2. JVM提供了**自旋锁**,可以通过**自旋**的方式**不断尝试获取锁**,从而***避免线程被挂起阻塞*** + +3. 从 + + JDK 1.7 + + 开始, + + 自旋锁默认启用 + + ,自旋次数 + + 不建议设置过大 + + (意味着 + + 长时间占用CPU + + ) + + - `-XX:+UseSpinning -XX:PreBlockSpin=10` + +4. 自旋锁重试之后如果依然抢锁失败,同步锁会升级至 + + 重量级锁 + + ,锁标志位为 + + 10 + + - 在这个状态下,未抢到锁的线程都会**进入Monitor**,之后会被阻塞在**WaitSet**中 + +5. 在 + + 锁竞争不激烈 + + 且 + + 锁占用时间非常短 + + 的场景下,自旋锁可以提高系统性能 + + - 一旦锁竞争激烈或者锁占用的时间过长,自旋锁将会导致大量的线程一直处于**CAS重试状态**,**占用CPU资源** + +6. 在 + + 高并发 + + 的场景下,可以通过 + + 关闭自旋锁 + + 来优化系统性能 + + - ``` + -XX:-UseSpinning + ``` + + - 关闭自旋锁优化 + + - ``` + -XX:PreBlockSpin + ``` + + - 默认的自旋次数,在**JDK 1.7**后,**由JVM控制** + +[![img](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-3.png)](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-3.png) + +## 小结 + +1. JVM在**JDK 1.6**中引入了**分级锁**机制来优化synchronized + +2. 当一个线程获取锁时,首先对象锁成为一个 + + 偏向锁 + + - 这是为了避免在**同一线程重复获取同一把锁**时,**用户态和内核态频繁切换** + +3. 如果有多个线程竞争锁资源,锁将会升级为 + + 轻量级锁 + + - 这适用于在**短时间**内持有锁,且分锁**交替切换**的场景 + - 轻量级锁还结合了**自旋锁**来**避免线程用户态与内核态的频繁切换** + +4. 如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁 + +5. 优化synchronized同步锁的关键: + + 减少锁竞争 + + - 应该尽量使synchronized同步锁处于**轻量级锁**或**偏向锁**,这样才能提高synchronized同步锁的性能 + - 常用手段 + - **减少锁粒度**:降低锁竞争 + - **减少锁的持有时间**,提高synchronized同步锁在自旋时获取锁资源的成功率,**避免升级为重量级锁** + +6. 在**锁竞争激烈**时,可以考虑**禁用偏向锁**和**禁用自旋锁** \ No newline at end of file diff --git a/docs/java/Multithread/best-practice-of-threadpool.md b/docs/java/Multithread/best-practice-of-threadpool.md new file mode 100644 index 00000000..06b2ccf4 --- /dev/null +++ b/docs/java/Multithread/best-practice-of-threadpool.md @@ -0,0 +1,305 @@ +# 线程池最佳实践 + +这篇文章篇幅虽短,但是绝对是干货。标题稍微有点夸张,嘿嘿,实际都是自己使用线程池的时候总结的一些个人感觉比较重要的点。 + +## 线程池知识回顾 + +开始这篇文章之前还是简单介绍一嘴线程池,之前写的[《新手也能看懂的线程池学习总结》](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/7888fb0d-4699-4d3a-8885-405cb5415617.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/Multithread/images/ThreadLocal内部类.png b/docs/java/Multithread/images/ThreadLocal内部类.png new file mode 100644 index 00000000..6997f5ca Binary files /dev/null and b/docs/java/Multithread/images/ThreadLocal内部类.png differ diff --git a/docs/java/Multithread/images/interview-questions/synchronized关键字.png b/docs/java/Multithread/images/interview-questions/synchronized关键字.png new file mode 100644 index 00000000..24ac1a8c Binary files /dev/null and b/docs/java/Multithread/images/interview-questions/synchronized关键字.png differ diff --git a/docs/java/Multithread/images/thread-local/1.png b/docs/java/Multithread/images/thread-local/1.png new file mode 100644 index 00000000..b394e304 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/1.png differ diff --git a/docs/java/Multithread/images/thread-local/10.png b/docs/java/Multithread/images/thread-local/10.png new file mode 100644 index 00000000..c9edb13f Binary files /dev/null and b/docs/java/Multithread/images/thread-local/10.png differ diff --git a/docs/java/Multithread/images/thread-local/11.png b/docs/java/Multithread/images/thread-local/11.png new file mode 100644 index 00000000..06d30638 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/11.png differ diff --git a/docs/java/Multithread/images/thread-local/12.png b/docs/java/Multithread/images/thread-local/12.png new file mode 100644 index 00000000..bd765217 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/12.png differ diff --git a/docs/java/Multithread/images/thread-local/13.png b/docs/java/Multithread/images/thread-local/13.png new file mode 100644 index 00000000..34c8d8c8 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/13.png differ diff --git a/docs/java/Multithread/images/thread-local/14.png b/docs/java/Multithread/images/thread-local/14.png new file mode 100644 index 00000000..b1b3abd6 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/14.png differ diff --git a/docs/java/Multithread/images/thread-local/15.png b/docs/java/Multithread/images/thread-local/15.png new file mode 100644 index 00000000..25f5cb2b Binary files /dev/null and b/docs/java/Multithread/images/thread-local/15.png differ diff --git a/docs/java/Multithread/images/thread-local/16.png b/docs/java/Multithread/images/thread-local/16.png new file mode 100644 index 00000000..45d0424f Binary files /dev/null and b/docs/java/Multithread/images/thread-local/16.png differ diff --git a/docs/java/Multithread/images/thread-local/17.png b/docs/java/Multithread/images/thread-local/17.png new file mode 100644 index 00000000..3194e4a3 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/17.png differ diff --git a/docs/java/Multithread/images/thread-local/18.png b/docs/java/Multithread/images/thread-local/18.png new file mode 100644 index 00000000..2b340b0b Binary files /dev/null and b/docs/java/Multithread/images/thread-local/18.png differ diff --git a/docs/java/Multithread/images/thread-local/19.png b/docs/java/Multithread/images/thread-local/19.png new file mode 100644 index 00000000..4c906279 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/19.png differ diff --git a/docs/java/Multithread/images/thread-local/2.png b/docs/java/Multithread/images/thread-local/2.png new file mode 100644 index 00000000..c9af80e1 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/2.png differ diff --git a/docs/java/Multithread/images/thread-local/20.png b/docs/java/Multithread/images/thread-local/20.png new file mode 100644 index 00000000..234c32a6 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/20.png differ diff --git a/docs/java/Multithread/images/thread-local/21.png b/docs/java/Multithread/images/thread-local/21.png new file mode 100644 index 00000000..1b5a02b9 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/21.png differ diff --git a/docs/java/Multithread/images/thread-local/22.png b/docs/java/Multithread/images/thread-local/22.png new file mode 100644 index 00000000..62eeff12 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/22.png differ diff --git a/docs/java/Multithread/images/thread-local/23.png b/docs/java/Multithread/images/thread-local/23.png new file mode 100644 index 00000000..0b4a0409 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/23.png differ diff --git a/docs/java/Multithread/images/thread-local/24.png b/docs/java/Multithread/images/thread-local/24.png new file mode 100644 index 00000000..ae2fe0df Binary files /dev/null and b/docs/java/Multithread/images/thread-local/24.png differ diff --git a/docs/java/Multithread/images/thread-local/25.png b/docs/java/Multithread/images/thread-local/25.png new file mode 100644 index 00000000..9c66e254 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/25.png differ diff --git a/docs/java/Multithread/images/thread-local/26.png b/docs/java/Multithread/images/thread-local/26.png new file mode 100644 index 00000000..ef53f0a9 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/26.png differ diff --git a/docs/java/Multithread/images/thread-local/27.png b/docs/java/Multithread/images/thread-local/27.png new file mode 100644 index 00000000..61710050 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/27.png differ diff --git a/docs/java/Multithread/images/thread-local/28.png b/docs/java/Multithread/images/thread-local/28.png new file mode 100644 index 00000000..68f67602 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/28.png differ diff --git a/docs/java/Multithread/images/thread-local/29.png b/docs/java/Multithread/images/thread-local/29.png new file mode 100644 index 00000000..d662f80b Binary files /dev/null and b/docs/java/Multithread/images/thread-local/29.png differ diff --git a/docs/java/Multithread/images/thread-local/3.png b/docs/java/Multithread/images/thread-local/3.png new file mode 100644 index 00000000..a0b418c0 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/3.png differ diff --git a/docs/java/Multithread/images/thread-local/30.png b/docs/java/Multithread/images/thread-local/30.png new file mode 100644 index 00000000..27ec27f7 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/30.png differ diff --git a/docs/java/Multithread/images/thread-local/31.png b/docs/java/Multithread/images/thread-local/31.png new file mode 100644 index 00000000..96b83ca1 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/31.png differ diff --git a/docs/java/Multithread/images/thread-local/4.png b/docs/java/Multithread/images/thread-local/4.png new file mode 100644 index 00000000..b0278b70 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/4.png differ diff --git a/docs/java/Multithread/images/thread-local/5.png b/docs/java/Multithread/images/thread-local/5.png new file mode 100644 index 00000000..81f24a08 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/5.png differ diff --git a/docs/java/Multithread/images/thread-local/6.png b/docs/java/Multithread/images/thread-local/6.png new file mode 100644 index 00000000..66dc77c1 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/6.png differ diff --git a/docs/java/Multithread/images/thread-local/7.png b/docs/java/Multithread/images/thread-local/7.png new file mode 100644 index 00000000..d13653a0 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/7.png differ diff --git a/docs/java/Multithread/images/thread-local/8.png b/docs/java/Multithread/images/thread-local/8.png new file mode 100644 index 00000000..b7e466d8 Binary files /dev/null and b/docs/java/Multithread/images/thread-local/8.png differ diff --git a/docs/java/Multithread/images/thread-local/9.png b/docs/java/Multithread/images/thread-local/9.png new file mode 100644 index 00000000..5964fb0c Binary files /dev/null and b/docs/java/Multithread/images/thread-local/9.png differ diff --git a/docs/java/Multithread/images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png b/docs/java/Multithread/images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png new file mode 100644 index 00000000..62f2c3e3 Binary files /dev/null and b/docs/java/Multithread/images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png differ diff --git a/docs/java/Multithread/images/thread-pool/1bc44c67-26ba-42ab-bcb8-4e29e6fd99b9.png b/docs/java/Multithread/images/thread-pool/1bc44c67-26ba-42ab-bcb8-4e29e6fd99b9.png new file mode 100644 index 00000000..1dc7e4b6 Binary files /dev/null and b/docs/java/Multithread/images/thread-pool/1bc44c67-26ba-42ab-bcb8-4e29e6fd99b9.png differ diff --git a/docs/java/Multithread/images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png b/docs/java/Multithread/images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png new file mode 100644 index 00000000..7dc9b398 Binary files /dev/null and b/docs/java/Multithread/images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png differ diff --git a/docs/java/Multithread/images/thread-pool/7888fb0d-4699-4d3a-8885-405cb5415617.png b/docs/java/Multithread/images/thread-pool/7888fb0d-4699-4d3a-8885-405cb5415617.png new file mode 100644 index 00000000..a3678a72 Binary files /dev/null and b/docs/java/Multithread/images/thread-pool/7888fb0d-4699-4d3a-8885-405cb5415617.png differ diff --git a/docs/java/Multithread/images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png b/docs/java/Multithread/images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png new file mode 100644 index 00000000..27cdbee3 Binary files /dev/null and b/docs/java/Multithread/images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png differ diff --git a/docs/java/Multithread/images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png b/docs/java/Multithread/images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png new file mode 100644 index 00000000..f0a781d6 Binary files /dev/null and b/docs/java/Multithread/images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png differ diff --git a/docs/java/Multithread/images/threadlocal数据结构.png b/docs/java/Multithread/images/threadlocal数据结构.png new file mode 100644 index 00000000..a5791ce5 Binary files /dev/null and b/docs/java/Multithread/images/threadlocal数据结构.png differ diff --git a/docs/java/Multithread/images/多线程学习指南/Java并发编程的艺术.png b/docs/java/Multithread/images/多线程学习指南/Java并发编程的艺术.png new file mode 100644 index 00000000..ff907c9c Binary files /dev/null and b/docs/java/Multithread/images/多线程学习指南/Java并发编程的艺术.png differ diff --git a/docs/java/Multithread/images/多线程学习指南/javaguide-并发.png b/docs/java/Multithread/images/多线程学习指南/javaguide-并发.png new file mode 100644 index 00000000..862f282f Binary files /dev/null and b/docs/java/Multithread/images/多线程学习指南/javaguide-并发.png differ diff --git a/docs/java/Multithread/images/多线程学习指南/java并发编程之美.png b/docs/java/Multithread/images/多线程学习指南/java并发编程之美.png new file mode 100644 index 00000000..05e3bff5 Binary files /dev/null and b/docs/java/Multithread/images/多线程学习指南/java并发编程之美.png differ diff --git a/docs/java/Multithread/images/多线程学习指南/实战Java高并发程序设计.png b/docs/java/Multithread/images/多线程学习指南/实战Java高并发程序设计.png new file mode 100644 index 00000000..ab61bff8 Binary files /dev/null and b/docs/java/Multithread/images/多线程学习指南/实战Java高并发程序设计.png differ diff --git a/docs/java/Multithread/images/多线程学习指南/深入浅出Java多线程.png b/docs/java/Multithread/images/多线程学习指南/深入浅出Java多线程.png new file mode 100644 index 00000000..14d1ea85 Binary files /dev/null and b/docs/java/Multithread/images/多线程学习指南/深入浅出Java多线程.png differ diff --git a/docs/java/Multithread/java线程池学习总结.md b/docs/java/Multithread/java线程池学习总结.md index bfbe99d4..54f4b650 100644 --- a/docs/java/Multithread/java线程池学习总结.md +++ b/docs/java/Multithread/java线程池学习总结.md @@ -1,3 +1,4 @@ + - [一 使用线程池的好处](#一-使用线程池的好处) @@ -28,7 +29,7 @@ - [5.2 SingleThreadExecutor 详解](#52-singlethreadexecutor-详解) - [5.2.1 介绍](#521-介绍) - [5.2.2 执行任务过程介绍](#522-执行任务过程介绍) - - [5.2.3 为什么不推荐使用`FixedThreadPool`?](#523-为什么不推荐使用fixedthreadpool) + - [5.2.3 为什么不推荐使用`SingleThreadExecutor`?](#523-为什么不推荐使用singlethreadexecutor) - [5.3 CachedThreadPool 详解](#53-cachedthreadpool-详解) - [5.3.1 介绍](#531-介绍) - [5.3.2 执行任务过程介绍](#532-执行任务过程介绍) @@ -43,6 +44,7 @@ + ## 一 使用线程池的好处 > **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** @@ -154,7 +156,7 @@ public class ScheduledThreadPoolExecutor - **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 - **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。 +- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 `ThreadPoolExecutor`其他常见参数: @@ -163,16 +165,16 @@ public class ScheduledThreadPoolExecutor 3. **`threadFactory`** :executor 创建新线程的时候会用到。 4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。 -下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java性能调优实战》): +下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》): ![线程池各个参数的关系](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/线程池各个参数的关系.jpg) **`ThreadPoolExecutor` 饱和策略定义:** -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略: +如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolTaskExecutor` 定义一些策略: - **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 +- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 - **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 - **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 @@ -205,7 +207,7 @@ public class ScheduledThreadPoolExecutor - **CachedThreadPool** 对应 Executors 工具类中的方法如图所示: -![通过Executor 框架的工具类Executors来实现](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzEzMjk2OTAxLmpwZw?x-oss-process=image/format,png) +![通过Executor 框架的工具类Executors来实现](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/format,png.jpeg) ## 四 (重要)ThreadPoolExecutor 使用示例 @@ -310,32 +312,32 @@ public class ThreadPoolExecutorDemo { **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 +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.2 线程池原理分析 -承接 4.1 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) +承接 4.1 节,我们通过代码输出结果可以看出:**线程首先会先执行 5 个任务,然后这些任务有任务被执行完的话,就会去拿新的任务执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) 现在,我们就分析上面的输出内容来简单分析一下线程池原理。 @@ -344,11 +346,11 @@ pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 ```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) { @@ -388,11 +390,120 @@ pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 ![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png) + + +**`addWorker` 这个方法主要用来创建新的工作线程,如果返回true说明创建和启动工作线程成功,否则的话返回的就是false。** + +```java + // 全局锁,并发操作必备 + private final ReentrantLock mainLock = new ReentrantLock(); + // 跟踪线程池的最大大小,只有在持有全局锁mainLock的前提下才能访问此集合 + private int largestPoolSize; + // 工作线程集合,存放线程池中所有的(活跃的)工作线程,只有在持有全局锁mainLock的前提下才能访问此集合 + private final HashSet workers = new HashSet<>(); + //获取线程池状态 + private static int runStateOf(int c) { return c & ~CAPACITY; } + //判断线程池的状态是否为 Running + private static boolean isRunning(int c) { + return c < SHUTDOWN; + } + + + /** + * 添加新的工作线程到线程池 + * @param firstTask 要执行 + * @param core参数为true的话表示使用线程池的基本大小,为false使用线程池最大大小 + * @return 添加成功就返回true否则返回false + */ + private boolean addWorker(Runnable firstTask, boolean core) { + retry: + for (;;) { + //这两句用来获取线程池的状态 + int c = ctl.get(); + int rs = runStateOf(c); + + // Check if queue empty only if necessary. + if (rs >= SHUTDOWN && + ! (rs == SHUTDOWN && + firstTask == null && + ! workQueue.isEmpty())) + return false; + + for (;;) { + //获取线程池中线程的数量 + int wc = workerCountOf(c); + // core参数为true的话表明队列也满了,线程池大小变为 maximumPoolSize + if (wc >= CAPACITY || + wc >= (core ? corePoolSize : maximumPoolSize)) + return false; + //原子操作将workcount的数量加1 + if (compareAndIncrementWorkerCount(c)) + break retry; + // 如果线程的状态改变了就再次执行上述操作 + c = ctl.get(); + if (runStateOf(c) != rs) + continue retry; + // else CAS failed due to workerCount change; retry inner loop + } + } + // 标记工作线程是否启动成功 + boolean workerStarted = false; + // 标记工作线程是否创建成功 + boolean workerAdded = false; + Worker w = null; + try { + + w = new Worker(firstTask); + final Thread t = w.thread; + if (t != null) { + // 加锁 + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + //获取线程池状态 + int rs = runStateOf(ctl.get()); + //rs < SHUTDOWN 如果线程池状态依然为RUNNING,并且线程的状态是存活的话,就会将工作线程添加到工作线程集合中 + //(rs=SHUTDOWN && firstTask == null)如果线程池状态小于STOP,也就是RUNNING或者SHUTDOWN状态下,同时传入的任务实例firstTask为null,则需要添加到工作线程集合和启动新的Worker + // firstTask == null证明只新建线程而不执行任务 + if (rs < SHUTDOWN || + (rs == SHUTDOWN && firstTask == null)) { + if (t.isAlive()) // precheck that t is startable + throw new IllegalThreadStateException(); + workers.add(w); + //更新当前工作线程的最大容量 + int s = workers.size(); + if (s > largestPoolSize) + largestPoolSize = s; + // 工作线程是否启动成功 + workerAdded = true; + } + } finally { + // 释放锁 + mainLock.unlock(); + } + //// 如果成功添加工作线程,则调用Worker内部的线程实例t的Thread#start()方法启动真实的线程实例 + if (workerAdded) { + t.start(); + /// 标记线程启动成功 + workerStarted = true; + } + } + } finally { + // 线程启动失败,需要从工作线程中移除对应的Worker + if (! workerStarted) + addWorkerFailed(w); + } + return workerStarted; + } +``` + +更多关于线程池源码分析的内容推荐这篇文章:《[JUC线程池ThreadPoolExecutor源码分析](http://www.throwable.club/2019/07/15/java-concurrency-thread-pool-executor/)》 + 现在,让我们在回到 4.1 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢? 没搞懂的话,也没关系,可以看看我的分析: -> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。 +> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的5个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。 ### 4.3 几个常见的对比 @@ -650,7 +761,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 1. 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务; 2. 当前线程池中有一个运行的线程后,将任务加入 `LinkedBlockingQueue` -3. 线程执行完当前的任务后,会在循环中反复从` LinkedBlockingQueue` 中获取任务来执行; +3. 线程执行完当前的任务后,会在循环中反复从`LinkedBlockingQueue` 中获取任务来执行; #### 5.2.3 为什么不推荐使用`SingleThreadExecutor`? @@ -683,7 +794,7 @@ 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 执行任务过程介绍 @@ -695,13 +806,13 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 1. 首先执行 `SynchronousQueue.offer(Runnable task)` 提交任务到任务队列。如果当前 `maximumPool` 中有闲线程正在执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`,那么主线程执行 offer 操作与空闲线程执行的 `poll` 操作配对成功,主线程把任务交给空闲线程执行,`execute()`方法执行完成,否则执行下面的步骤 2; 2. 当初始 `maximumPool` 为空,或者 `maximumPool` 中没有空闲线程时,将没有线程执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`。这种情况下,步骤 1 将失败,此时 `CachedThreadPool` 会创建新线程执行任务,execute 方法执行完成; -#### 5.3.3 为什么不推荐使用`CachedThreadPool`? +#### 5.3.3 为什么不推荐使用`CachedThreadPool`? `CachedThreadPool`允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 ## 六 ScheduledThreadPoolExecutor 详解 -**`ScheduledThreadPoolExecutor` 主要用来在给定的延迟后运行任务,或者定期执行任务。** 这个在实际项目中基本不会被用到,所以对这部分大家只需要简单了解一下它的思想。关于如何在Spring Boot 中 实现定时任务,可以查看这篇文章[《5分钟搞懂如何在Spring Boot中Schedule Tasks》](https://github.com/Snailclimb/springboot-guide/blob/master/docs/advanced/SpringBoot-ScheduleTasks.md)。 +**`ScheduledThreadPoolExecutor` 主要用来在给定的延迟后运行任务,或者定期执行任务。** 这个在实际项目中基本不会被用到,因为有其他方案选择比如`quartz`。大家只需要简单了解一下它的思想。关于如何在 Spring Boot 中 实现定时任务,可以查看这篇文章[《5 分钟搞懂如何在 Spring Boot 中 Schedule Tasks》](https://github.com/Snailclimb/springboot-guide/blob/master/docs/advanced/SpringBoot-ScheduleTasks.md)。 ### 6.1 简介 @@ -726,7 +837,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 1. 当调用 `ScheduledThreadPoolExecutor` 的 **`scheduleAtFixedRate()`** 方法或者**`scheduleWirhFixedDelay()`** 方法时,会向 `ScheduledThreadPoolExecutor` 的 **`DelayQueue`** 添加一个实现了 **`RunnableScheduledFuture`** 接口的 **`ScheduledFutureTask`** 。 2. 线程池中的线程从 `DelayQueue` 中获取 `ScheduledFutureTask`,然后执行任务。 -**`ScheduledThreadPoolExecutor` 为了实现周期性的执行任务,对 `ThreadPoolExecutor `做了如下修改:** +**`ScheduledThreadPoolExecutor` 为了实现周期性的执行任务,对 `ThreadPoolExecutor`做了如下修改:** - 使用 **`DelayQueue`** 作为任务队列; - 获取任务的方不同 @@ -736,38 +847,40 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 ![ScheduledThreadPoolExecutor执行周期任务的步骤](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC01LTMwLzU5OTE2Mzg5LmpwZw?x-oss-process=image/format,png) -1. 线程 1 从 `DelayQueue` 中获取已到期的 `ScheduledFutureTask(DelayQueue.take())`。到期任务是指 `ScheduledFutureTask `的 time 大于等于当前系统的时间; +1. 线程 1 从 `DelayQueue` 中获取已到期的 `ScheduledFutureTask(DelayQueue.take())`。到期任务是指 `ScheduledFutureTask`的 time 大于等于当前系统的时间; 2. 线程 1 执行这个 `ScheduledFutureTask`; 3. 线程 1 修改 `ScheduledFutureTask` 的 time 变量为下次将要被执行的时间; 4. 线程 1 把这个修改 time 之后的 `ScheduledFutureTask` 放回 `DelayQueue` 中(`DelayQueue.add()`)。 ## 七 线程池大小确定 -**线程池数量的确定一直是困扰着程序员的一个难题,大部分程序员在设定线程池大小的时候就是随心而定。我们并没有考虑过这样大小的配置是否会带来什么问题,我自己就是这大部分程序员中的一个代表。** +**线程池数量的确定一直是困扰着程序员的一个难题,大部分程序员在设定线程池大小的时候就是随心而定。** -由于笔主对如何确定线程池大小也没有什么实际经验,所以,这部分内容参考了网上很多文章/书籍。 - -**首先,可以肯定的一点是线程池大小设置过大或者过小都会有问题。合适的才是最好,貌似在 95 % 的场景下都是合适的。** - -如果阅读过我的上一篇关于线程池的文章的话,你一定知道: - -**如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。** - -**但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。** +很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:**并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。** 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了**上下文切换**成本。不清楚什么是上下文切换的话,可以看我下面的介绍。 > 上下文切换: > > 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 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 并发编程的艺术》 diff --git a/docs/java/Multithread/synchronized.md b/docs/java/Multithread/synchronized.md deleted file mode 100644 index 3c926654..00000000 --- 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/synchronized在JDK1.6之后的底层优化.md b/docs/java/Multithread/synchronized在JDK1.6之后的底层优化.md new file mode 100644 index 00000000..d8e454de --- /dev/null +++ b/docs/java/Multithread/synchronized在JDK1.6之后的底层优化.md @@ -0,0 +1,62 @@ +JDK1.6 对锁的实现引入了大量的优化来减少锁操作的开销,如: **偏向锁**、**轻量级锁**、**自旋锁**、**适应性自旋锁**、**锁消除**、**锁粗化** 等等技术。 + +锁主要存在四中状态,依次是: + +1. 无锁状态 +2. 偏向锁状态 +3. 轻量级锁状态 +4. 重量级锁状态 + +锁🔐会随着竞争的激烈而逐渐升级。 + +另外,需要注意:**锁可以升级不可降级,即 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的。** 这种策略是为了提高获得锁和释放锁的效率。 + +### 偏向锁 + +**引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。 + +偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!(关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。) + +#### 偏向锁的加锁 + +当一个线程访问同步块并获取锁时, 会在锁对象的对象头和栈帧中的锁记录里存储锁偏向的线程ID, 以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁, 只需要简单的测试一下锁对象的对象头的MarkWord里是否存储着指向当前线程的偏向锁(线程ID是当前线程), 如果测试成功, 表示线程已经获得了锁; 如果测试失败, 则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁), 如果没有设置, 则使用CAS竞争锁, 如果设置了, 则尝试使用CAS将锁对象的对象头的偏向锁指向当前线程. + +#### 偏向锁的撤销 + +偏向锁使用了一种等到竞争出现才释放锁的机制, 所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁. 偏向锁的撤销需要等到全局安全点(在这个时间点上没有正在执行的字节码). 首先会暂停持有偏向锁的线程, 然后检查持有偏向锁的线程是否存活, 如果线程不处于活动状态, 则将锁对象的对象头设置为无锁状态; 如果线程仍然活着, 则锁对象的对象头中的MarkWord和栈中的锁记录要么重新偏向于其它线程要么恢复到无锁状态, 最后唤醒暂停的线程(释放偏向锁的线程). + +但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 + +### 轻量级锁 + +倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。** 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。 + +**轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!** + +### 自旋锁和自适应自旋 + +轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。 + +互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。 + +**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。 + +百度百科对自旋锁的解释: + +> 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。 + +自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过`--XX:+UseSpinning`参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。**自旋次数的默认值是10次,用户可以修改`--XX:PreBlockSpin`来更改**。 + +另外,**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。 + +### 锁消除 + +锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。 + +### 锁粗化 + +原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。 + +大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。 + + diff --git a/docs/java/Multithread/创建线程的几种方式总结.md b/docs/java/Multithread/创建线程的几种方式总结.md new file mode 100644 index 00000000..764a63a4 --- /dev/null +++ b/docs/java/Multithread/创建线程的几种方式总结.md @@ -0,0 +1,40 @@ +## 面试官:“创建线程有哪几种常见的方式?” + +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/多线程学习指南.md b/docs/java/Multithread/多线程学习指南.md new file mode 100644 index 00000000..84830521 --- /dev/null +++ b/docs/java/Multithread/多线程学习指南.md @@ -0,0 +1,158 @@ +## 前言 + +这是我的第二篇专门介绍如何去学习某个知识点的文章,在上一篇[《写给 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=并发) + +![](images/多线程学习指南/javaguide-并发.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 并发编程之美》](images/多线程学习指南/java并发编程之美.png) + +**我觉得这本书还是非常适合我们用来学习 Java 多线程的。这本书的讲解非常通俗易懂,作者从并发编程基础到实战都是信手拈来。** + +另外,这本书的作者加多自身也会经常在网上发布各种技术文章。我觉得这本书也是加多大佬这么多年在多线程领域的沉淀所得的结果吧!他书中的内容基本都是结合代码讲解,非常有说服力! + +#### 《实战 Java 高并发程序设计》 + +![《实战 Java 高并发程序设计》](images/多线程学习指南/实战Java高并发程序设计.png) + +这个是我第二本要推荐的书籍,比较适合作为多线程入门/进阶书籍来看。这本书内容同样是理论结合实战,对于每个知识点的讲解也比较通俗易懂,整体结构也比较清。 + +#### 《深入浅出 Java 多线程》 + +![《深入浅出Java多线程》](images/多线程学习指南/深入浅出Java多线程.png) + +这本书是几位大厂(如阿里)的大佬开源的,Github 地址:[https://github.com/RedSpider1/concurrent](https://github.com/RedSpider1/concurrent) + +几位作者为了写好《深入浅出 Java 多线程》这本书阅读了大量的 Java 多线程方面的书籍和博客,然后再加上他们的经验总结、Demo 实例、源码解析,最终才形成了这本书。 + +这本书的质量也是非常过硬!给作者们点个赞!这本书有统一的排版规则和语言风格、清晰的表达方式和逻辑。并且每篇文章初稿写完后,作者们就会互相审校,合并到主分支时所有成员会再次审校,最后再通篇修订了三遍。 + +#### 《Java 并发编程的艺术》 + +![《Java 并发编程的艺术》](images/多线程学习指南/Java并发编程的艺术.png) + +这本书不是很适合作为 Java 多线程入门书籍,需要具备一定的 JVM 基础,有些东西讲的还是挺深入的。另外,就我自己阅读这本书的感觉来说,我觉得这本书的章节规划有点杂乱,但是,具体到某个知识点又很棒!这可能也和这本书由三名作者共同编写完成有关系吧! + +**综上:这本书并不是和 Java 多线程入门,你也不需要把这本书的每一章节都看一遍,建议挑选自己想要详细了解的知识点来看。** + +## 三.总结 + +在这篇文章中我主要总结了 Java 多线程方面的知识点,并且推荐了相关的书籍。并发这部分东西实战的话比较难,你可以尝试学会了某个知识点之后然后在自己写过的一些项目上实践。另外,leetcode 有一个练习多线程的类别: [https://leetcode-cn.com/problemset/concurrency](https://leetcode-cn.com/problemset/concurrency) 可以作为参考。 + +**为了这篇文章的内容更加完善,我还将本文的内容同步到了 Github 上,点击阅读原文即可直达。如果你觉得有任何需要完善和修改的地方,都可以去 Github 给我提交 Issue 或者 PR(推荐)。** \ No newline at end of file diff --git a/docs/java/Multithread/并发编程基础知识.md b/docs/java/Multithread/并发编程基础知识.md deleted file mode 100644 index 68509cdc..00000000 --- a/docs/java/Multithread/并发编程基础知识.md +++ /dev/null @@ -1,407 +0,0 @@ -# Java 并发基础知识 - -Java 并发的基础知识,可能会在笔试中遇到,技术面试中也可能以并发知识环节提问的第一个问题出现。比如面试官可能会问你:“谈谈自己对于进程和线程的理解,两者的区别是什么?” - -**本节思维导图:** - -## 一 进程和线程 - -进程和线程的对比这一知识点由于过于基础,所以在面试中很少碰到,但是极有可能会在笔试题中碰到。 - -常见的提问形式是这样的:**“什么是线程和进程?,请简要描述线程与进程的关系、区别及优缺点? ”**。 - -### 1.1. 何为进程? - -进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 - -在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。 - -如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。 - -![进程 ](https://images.gitbook.cn/a0929b60-d133-11e8-88a4-5328c5b70145) - -### 1.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 线程和多个其他线程同时运行**。 - -### 1.3 从 JVM 角度说进程和线程之间的关系(重要) - -#### 1.3.1 图解进程和线程的关系 - -下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下我的这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》]() - -
- -
- - -从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 - -下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? - -#### 1.3.2 程序计数器为什么是私有的? - -程序计数器主要有下面两个作用: - -1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 - -需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。 - -所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 - -#### 1.3.3 虚拟机栈和本地方法栈为什么是私有的? - -- **虚拟机栈:**每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 -- **本地方法栈:**和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 - -所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 - -#### 1.3.4 一句话简单了解堆和方法区 - -堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 - -## 二 多线程并发编程 - -### 2.1 并发与并行概念解读 - -- **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行); -- **并行:**单位时间内,多个任务同时执行。 - -### 2.2 为什么要使用多线程? - -先从总体上来说: - -- **从计算机底层来说:**线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 -- **从当代互联网发展趋势来说:**现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 - -再深入到计算机底层来探讨: - -- **单核时代:** 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。 -- **多核时代:** 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。 - -### 2.3 使用多线程可能带来的问题 - -并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。 - -## 三 线程的创建与运行 - -前两种实际上很少使用,一般都是用线程池的方式比较多一点。 - -### 3.1 继承 Thread 类的方式 - - -```java -public class MyThread extends Thread { - @Override - public void run() { - super.run(); - System.out.println("MyThread"); - } -} -``` -Run.java - -```java -public class Run { - - public static void main(String[] args) { - MyThread mythread = new MyThread(); - mythread.start(); - System.out.println("运行结束"); - } - -} - -``` -运行结果: -![结果 ](https://user-gold-cdn.xitu.io/2018/3/20/16243e80f22a2d54?w=161&h=54&f=jpeg&s=7380) - -从上面的运行结果可以看出:线程是一个子任务,CPU 以不确定的方式,或者说是以随机的时间来调用线程中的 run 方法。 - -### 3.2 实现 Runnable 接口的方式 - -推荐实现 Runnable 接口方式开发多线程,因为 Java 单继承但是可以实现多个接口。 - -MyRunnable.java - -```java -public class MyRunnable implements Runnable { - @Override - public void run() { - System.out.println("MyRunnable"); - } -} -``` - -Run.java - -```java -public class Run { - - public static void main(String[] args) { - Runnable runnable=new MyRunnable(); - Thread thread=new Thread(runnable); - thread.start(); - System.out.println("运行结束!"); - } - -} -``` -运行结果: -![运行结果 ](https://user-gold-cdn.xitu.io/2018/3/20/16243f4373c6141a?w=137&h=46&f=jpeg&s=7316) - -### 3.3 使用线程池的方式 - -使用线程池的方式也是最推荐的一种方式,另外,《阿里巴巴 Java 开发手册》在第一章第六节并发处理这一部分也强调到“线程资源必须通过线程池提供,不允许在应用中自行显示创建线程”。这里就不给大家演示代码了,线程池这一节会详细介绍到这部分内容。 - -## 四 线程的生命周期和状态 - -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)中的 RUNNABLE 和 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(终止)** 状态。 - -## 五 线程优先级 - -**理论上**来说系统会根据优先级来决定首先使哪个线程进入运行状态。当 CPU 比较闲的时候,设置线程优先级几乎不会有任何作用,而且很多操作系统压根不会不会理会你设置的线程优先级,所以不要让业务过度依赖于线程的优先级。 - -另外,**线程优先级具有继承特性**比如 A 线程启动 B 线程,则 B 线程的优先级和 A 是一样的。**线程优先级还具有随机性** 也就是说线程优先级高的不一定每一次都先执行完。 - -Thread 类中包含的成员变量代表了线程的某些优先级。如**Thread.MIN_PRIORITY(常数 1)**,**Thread.NORM_PRIORITY(常数 5)**,**Thread.MAX_PRIORITY(常数 10)**。其中每个线程的优先级都在**1** 到**10** 之间,在默认情况下优先级都是**Thread.NORM_PRIORITY(常数 5)**。 - -**一般情况下,不会对线程设定优先级别,更不会让某些业务严重地依赖线程的优先级别,比如权重,借助优先级设定某个任务的权重,这种方式是不可取的,一般定义线程的时候使用默认的优先级就好了。** - -**相关方法:** - -```java -public final void setPriority(int newPriority) //为线程设定优先级 -public final int getPriority() //获取线程的优先级 -``` -**设置线程优先级方法源码:** - -```java - public final void setPriority(int newPriority) { - ThreadGroup g; - checkAccess(); - //线程游戏优先级不能小于 1 也不能大于 10,否则会抛出异常 - if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { - throw new IllegalArgumentException(); - } - //如果指定的线程优先级大于该线程所在线程组的最大优先级,那么该线程的优先级将设为线程组的最大优先级 - if((g = getThreadGroup()) != null) { - if (newPriority > g.getMaxPriority()) { - newPriority = g.getMaxPriority(); - } - setPriority0(priority = newPriority); - } - } - -``` - -## 六 守护线程和用户线程 - -**守护线程和用户线程简介:** - -- **用户 (User) 线程:**运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程 -- **守护 (Daemon) 线程:**运行在后台,为其他前台线程服务.也可以说守护线程是 JVM 中非守护线程的 **“佣人”**。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作. - -main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。 - -**那么守护线程和用户线程有什么区别呢?** - -比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。 - -**注意事项:** - -1. `setDaemon(true)`必须在`start()`方法前执行,否则会抛出 `IllegalThreadStateException` 异常 -2. 在守护线程中产生的新线程也是守护线程 -3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑 -4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。 - -## 七 上下文切换 - -多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 - -概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 - -上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 - -Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 - -## 八 线程死锁 - -### 认识线程死锁 - -多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 - -如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 - -![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4死锁1.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. 互斥条件:该资源任意一个时刻只由一个线程占用。 -1. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 -1. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 -1. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 - -### 如何预防线程死锁? - -我们只要破坏产生死锁的四个条件中的其中一个就可以了。 - -**破坏互斥条件** - -这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 - -**破坏请求与保持条件** - -一次性申请所有的资源。 - -**破坏不剥夺条件** - -占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 - -**破坏循环等待条件** - -靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 - -我们对线程 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 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 - -## 参考 - -- 《Java 并发编程之美》 - -- 《Java 并发编程的艺术》 - -- https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/ - - \ No newline at end of file diff --git a/docs/java/What's New in JDK8/Java8Tutorial.md b/docs/java/What's New in JDK8/Java8Tutorial.md index 39d0224e..8cf809f7 100644 --- a/docs/java/What's New in JDK8/Java8Tutorial.md +++ b/docs/java/What's New in JDK8/Java8Tutorial.md @@ -15,12 +15,12 @@ - [访问字段和静态变量](#访问字段和静态变量) - [访问默认接口方法](#访问默认接口方法) - [内置函数式接口\(Built-in Functional Interfaces\)](#内置函数式接口built-in-functional-interfaces) - - [Predicates](#predicates) - - [Functions](#functions) - - [Suppliers](#suppliers) - - [Consumers](#consumers) - - [Comparators](#comparators) - - [Optionals](#optionals) + - [Predicate](#predicate) + - [Function](#function) + - [Supplier](#supplier) + - [Consumer](#consumer) + - [Comparator](#comparator) + - [Optional](#optional) - [Streams\(流\)](#streams流) - [Filter\(过滤\)](#filter过滤) - [Sorted\(排序\)](#sorted排序) @@ -73,7 +73,7 @@ Formula 接口中除了抽象方法计算接口公式还定义了默认方法 `s public class Main { public static void main(String[] args) { - // TODO 通过匿名内部类方式访问接口 + // 通过匿名内部类方式访问接口 Formula formula = new Formula() { @Override public double calculate(int a) { @@ -287,7 +287,7 @@ JDK 1.8 API包含许多内置函数式接口。 其中一些借口在老版本 但是 Java 8 API 同样还提供了很多全新的函数式接口来让你的编程工作更加方便,有一些接口是来自 [Google Guava](https://code.google.com/p/guava-libraries/) 库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到lambda上使用的。 -#### Predicates +#### Predicate Predicate 接口是只有一个参数的返回布尔类型值的 **断言型** 接口。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非): @@ -340,7 +340,7 @@ Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate(); ``` -#### Functions +#### Function Function 接口接受一个参数并生成结果。默认方法可用于将多个函数链接在一起(compose, andThen): @@ -382,7 +382,7 @@ Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123" ``` -#### Suppliers +#### Supplier Supplier 接口产生给定泛型类型的结果。 与 Function 接口不同,Supplier 接口不接受参数。 @@ -391,7 +391,7 @@ Supplier personSupplier = Person::new; personSupplier.get(); // new Person ``` -#### Consumers +#### Consumer Consumer 接口表示要对单个输入参数执行的操作。 @@ -400,7 +400,7 @@ Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker")); ``` -#### Comparators +#### Comparator Comparator 是老Java中的经典接口, Java 8在此之上添加了多种默认方法: @@ -414,9 +414,9 @@ comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0 ``` -## Optionals +## Optional -Optionals不是函数式接口,而是用于防止 NullPointerException 的漂亮工具。这是下一节的一个重要概念,让我们快速了解一下Optionals的工作原理。 +Optional不是函数式接口,而是用于防止 NullPointerException 的漂亮工具。这是下一节的一个重要概念,让我们快速了解一下Optional的工作原理。 Optional 是一个简单的容器,其值可能是null或者不是null。在Java 8之前一般某个函数应该返回非空对象但是有时却什么也没有返回,而在Java 8中,你应该返回 Optional 而不是 null。 diff --git a/docs/java/Basis/Arrays,CollectionsCommonMethods.md b/docs/java/basic/Arrays,CollectionsCommonMethods.md similarity index 100% rename from docs/java/Basis/Arrays,CollectionsCommonMethods.md rename to docs/java/basic/Arrays,CollectionsCommonMethods.md diff --git a/docs/java/Basis/final、static、this、super.md b/docs/java/basic/final,static,this,super.md similarity index 70% rename from docs/java/Basis/final、static、this、super.md rename to docs/java/basic/final,static,this,super.md index 77e8b094..d9eaf1a8 100644 --- a/docs/java/Basis/final、static、this、super.md +++ b/docs/java/basic/final,static,this,super.md @@ -23,13 +23,15 @@ ## final 关键字 -**final关键字主要用在三个地方:变量、方法、类。** +**final关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点:** -1. **对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。** +1. **final修饰的类不能被继承,final类中的所有成员方法都会被隐式的指定为final方法;** -2. **当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。** +2. **final修饰的方法不能被重写;** -3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 +3. **final修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。** + +说明:使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 ## static 关键字 @@ -90,7 +92,7 @@ public class Sub extends Super { **使用 this 和 super 要注意的问题:** -- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 +- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 - this、super不能用在static方法中。 **简单解释一下:** @@ -121,8 +123,8 @@ public class Sub extends Super { 调用格式: -- 类名.静态变量名 -- 类名.静态方法名() +- `类名.静态变量名` +- `类名.静态方法名()` 如果变量或者方法被 private 则代表该属性或者该方法只能在类的内部被访问而不能在类的外部被访问。 @@ -132,21 +134,21 @@ public class Sub extends Super { 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); + //静态方法 + static void sayHello() { + System.out.println("Hello i am java"); } @Override public String toString() { - return StaticBean{ + - name=' + name + ''' + age + age + - '}'; + return "StaticBean{"+ + "name=" + name + ",age=" + age + + "}"; } } ``` @@ -155,14 +157,14 @@ public class StaticBean { 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 staticBean = new StaticBean("1"); + StaticBean staticBean2 = new StaticBean("2"); + StaticBean staticBean3 = new StaticBean("3"); + StaticBean staticBean4 = new StaticBean("4"); StaticBean.age = 33; - StaticBean{name='1'age33} StaticBean{name='2'age33} StaticBean{name='3'age33} StaticBean{name='4'age33} - System.out.println(staticBean+ +staticBean2+ +staticBean3+ +staticBean4); - StaticBean.SayHello();Hello i am java + 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 } } @@ -171,7 +173,7 @@ public class StaticDemo { ### 静态代码块 -静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次. +静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块 —> 非静态代码块 —> 构造方法)。 该类不管创建多少对象,静态代码块只执行一次. 静态代码块的格式是 @@ -202,11 +204,11 @@ Example(静态内部类实现单例模式) ```java public class Singleton { - 声明为 private 避免调用默认构造方法创建对象 + //声明为 private 避免调用默认构造方法创建对象 private Singleton() { } - 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问 + // 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问 private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } @@ -230,12 +232,10 @@ public class Singleton { ```java - Math. --- 将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用 - 如果只想导入单一某个静态方法,只需要将换成对应的方法名即可 + //将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用 + //如果只想导入单一某个静态方法,只需要将换成对应的方法名即可 -import static java.lang.Math.; - - 换成import static java.lang.Math.max;具有一样的效果 +import static java.lang.Math.*;//换成import static java.lang.Math.max;具有一样的效果 public class Demo { public static void main(String[] args) { @@ -264,69 +264,83 @@ class Foo { } public static String method1() { - return An example string that doesn't depend on i (an instance variable); + return "An example string that doesn't depend on i (an instance variable)"; } public int method2() { - return this.i + 1; Depends on i + return this.i + 1; //Depends on i } } ``` -你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行:`Foo bar = new Foo(1);bar.method2();` +你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行 +``` java +Foo bar = new Foo(1); +bar.method2(); +``` 总结: - 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 - 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 -### static{}静态代码块与{}非静态代码块(构造代码块) +### `static{}`静态代码块与`{}`非静态代码块(构造代码块) 相同点: 都是在JVM加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些static变量进行赋值。 -不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。 +不同点: 静态代码块在非静态代码块之前执行(静态代码块 -> 非静态代码块 -> 构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。 + +> 修正 [issue #677](https://github.com/Snailclimb/JavaGuide/issues/677):静态代码块可能在第一次new的时候执行,但不一定只在第一次new的时候执行。比如通过 `Class.forName("ClassDemo")`创建 Class 对象的时候也会执行。 一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:Arrays类,Character类,String类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的. -Example +Example: ```java public class Test { public Test() { - System.out.print(默认构造方法!--); + System.out.print("默认构造方法!--"); } - 非静态代码块 + //非静态代码块 { - System.out.print(非静态代码块!--); - } - 静态代码块 - static { - System.out.print(静态代码块!--); + System.out.print("非静态代码块!--"); } - public static void test() { - System.out.print(静态方法中的内容! --); + //静态代码块 + static { + System.out.print("静态代码块!--"); + } + + private static void test() { + System.out.print("静态方法中的内容! --"); { - System.out.print(静态方法中的代码块!--); + System.out.print("静态方法中的代码块!--"); } } - public static void main(String[] args) { - Test test = new Test(); - Test.test();静态代码块!--静态方法中的内容! --静态方法中的代码块!-- + public static void main(String[] args) { + Test test = new Test(); + Test.test();//静态代码块!--静态方法中的内容! --静态方法中的代码块!-- } +} ``` -当执行 `Test.test();` 时输出: +上述代码输出: + +``` +静态代码块!--非静态代码块!--默认构造方法!--静态方法中的内容! --静态方法中的代码块!-- +``` + +当只执行 `Test.test();` 时输出: ``` 静态代码块!--静态方法中的内容! --静态方法中的代码块!-- ``` -当执行 `Test test = new Test();` 时输出: +当只执行 `Test test = new Test();` 时输出: ``` 静态代码块!--非静态代码块!--默认构造方法!-- @@ -337,6 +351,6 @@ public class Test { ### 参考 -- httpsblog.csdn.netchen13579867831articledetails78995480 -- httpwww.cnblogs.comchenssyp3388487.html -- httpwww.cnblogs.comQian123p5713440.html +- https://blog.csdn.net/chen13579867831/article/details/78995480 +- https://www.cnblogs.com/chenssy/p/3388487.html +- https://www.cnblogs.com/Qian123/p/5713440.html diff --git a/docs/java/basic/java-proxy.md b/docs/java/basic/java-proxy.md new file mode 100644 index 00000000..c35c8b14 --- /dev/null +++ b/docs/java/basic/java-proxy.md @@ -0,0 +1,420 @@ +> 本文首更于[《从零开始手把手教你实现一个简单的RPC框架》](https://t.zsxq.com/iIUv7Mn) 。 + + + + + +- [1. 代理模式](#1-代理模式) +- [2. 静态代理](#2-静态代理) +- [3. 动态代理](#3-动态代理) + - [3.1. JDK 动态代理机制](#31-jdk-动态代理机制) + - [3.1.1. 介绍](#311-介绍) + - [3.1.2. JDK 动态代理类使用步骤](#312-jdk-动态代理类使用步骤) + - [3.1.3. 代码示例](#313-代码示例) + - [3.2. CGLIB 动态代理机制](#32-cglib-动态代理机制) + - [3.2.1. 介绍](#321-介绍) + - [3.2.2. CGLIB 动态代理类使用步骤](#322-cglib-动态代理类使用步骤) + - [3.2.3. 代码示例](#323-代码示例) + - [3.3. JDK 动态代理和 CGLIB 动态代理对比](#33-jdk-动态代理和-cglib-动态代理对比) +- [4. 静态代理和动态代理的对比](#4-静态代理和动态代理的对比) +- [5. 总结](#5-总结) + + + + +## 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. **methodProxy** :用于调用原始方法 + +你可以通过 `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/basic/reflection.md b/docs/java/basic/reflection.md new file mode 100644 index 00000000..ae1cc384 --- /dev/null +++ b/docs/java/basic/reflection.md @@ -0,0 +1,148 @@ +### 反射机制介绍 + +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"); +``` +3.通过对象实例`instance.getClass()`获取: +``` +Employee e; +Class alunbarClass2 = e.getClass(); +``` + +### 代码实例 + +**简单用代码演示一下反射的一些操作!** + +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"); +``` + + +### 静态编译和动态编译 + +- **静态编译:** 在编译时确定类型,绑定对象 +- **动态编译:** 运行时确定类型,绑定对象 + +### 反射机制优缺点 + +- **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。 +- **缺点:** 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) + +## diff --git a/docs/java/Basis/用好Java中的枚举,真的没有那么简单!.md b/docs/java/basic/用好Java中的枚举真的没有那么简单.md similarity index 93% rename from docs/java/Basis/用好Java中的枚举,真的没有那么简单!.md rename to docs/java/basic/用好Java中的枚举真的没有那么简单.md index 20b02297..bfd347aa 100644 --- a/docs/java/Basis/用好Java中的枚举,真的没有那么简单!.md +++ b/docs/java/basic/用好Java中的枚举真的没有那么简单.md @@ -51,10 +51,7 @@ public class Pizza { } public boolean isDeliverable() { - if (getStatus() == PizzaStatus.READY) { - return true; - } - return false; + return getStatus() == PizzaStatus.READY; } // Methods that set and get the status variable. @@ -63,9 +60,9 @@ public class Pizza { ## 3.使用 == 比较枚举类型 -由于枚举类型确保JVM中仅存在一个常量实例,因此我们可以安全地使用“ ==”运算符比较两个变量,如上例所示;此外,“ ==”运算符可提供编译时和运行时的安全性。 +由于枚举类型确保JVM中仅存在一个常量实例,因此我们可以安全地使用 `==` 运算符比较两个变量,如上例所示;此外,`==` 运算符可提供编译时和运行时的安全性。 -首先,让我们看一下以下代码段中的运行时安全性,其中“ ==”运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException: +首先,让我们看一下以下代码段中的运行时安全性,其中 `==` 运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException: ```java if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); @@ -84,9 +81,12 @@ if(testPz.getStatus() == TestColor.GREEN); ```java public int getDeliveryTimeInDays() { switch (status) { - case ORDERED: return 5; - case READY: return 2; - case DELIVERED: return 0; + case ORDERED: + return 5; + case READY: + return 2; + case DELIVERED: + return 0; } return 0; } @@ -257,22 +257,17 @@ 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); - } +Iterator iterator = pizzaList.iterator(); +while (iterator.hasNext()) { + Pizza pz = iterator.next(); + 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; } ``` diff --git a/docs/java/collection/ArrayList-Grow.md b/docs/java/collection/ArrayList-Grow.md index ab56b802..2449190f 100644 --- a/docs/java/collection/ArrayList-Grow.md +++ b/docs/java/collection/ArrayList-Grow.md @@ -74,6 +74,9 @@ return true; } ``` + +> **注意** :JDK11 移除了 `ensureCapacityInternal()` 和 `ensureExplicitCapacity()` 方法 + ### 2. 再来看看 `ensureCapacityInternal()` 方法 可以看到 `add` 方法 首先调用了`ensureCapacityInternal(size + 1)` @@ -145,7 +148,7 @@ } ``` -**int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍!(JDK1.6版本以后)** JDk1.6版本时,扩容之后容量为 1.5 倍+1!详情请参考源码 +**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进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源   @@ -223,7 +226,7 @@ public class ArraycopyTest { System.arraycopy(a, 2, a, 3, 3); a[2]=99; for (int i = 0; i < a.length; i++) { - System.out.println(a[i]); + System.out.print(a[i] + " "); } } diff --git a/docs/java/collection/ArrayList.md b/docs/java/collection/ArrayList.md index f6578a7a..43e81ba6 100644 --- a/docs/java/collection/ArrayList.md +++ b/docs/java/collection/ArrayList.md @@ -68,23 +68,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,16 +96,16 @@ 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; } } @@ -127,13 +129,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 +144,7 @@ public class ArrayList extends AbstractList //得到最小扩容量 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取默认的容量和传入参数的较大值 + // 获取“默认的容量”和“传入参数”两者之间的最大值 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } diff --git a/docs/java/collection/ConcurrentHashMap.md b/docs/java/collection/ConcurrentHashMap.md new file mode 100644 index 00000000..3dd590ff --- /dev/null +++ b/docs/java/collection/ConcurrentHashMap.md @@ -0,0 +1,584 @@ +> 本文来自公众号:末读代码的投稿,原文地址:https://mp.weixin.qq.com/s/AHWzboztt53ZfFZmsSnMSw 。 + +上一篇文章介绍了 HashMap 源码,反响不错,也有很多同学发表了自己的观点,这次又来了,这次是 `ConcurrentHashMap ` 了,作为线程安全的HashMap ,它的使用频率也是很高。那么它的存储结构和实现原理是怎么样的呢? + +## 1. ConcurrentHashMap 1.7 + +### 1. 存储结构 + +![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. 如果是链表,遍历查找之。 + +总结: + +总的来说 ConcruuentHashMap 在 Java8 中相对于 Java7 来说变化还是挺大的, + +## 3. 总结 + +Java7 中 ConcruuentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。 + +Java8 中的 ConcruuentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 **Segment 数组 + HashEntry 数组 + 链表** 进化成了 **Node 数组 + 链表 / 红黑树**,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。 + +有些同学可能对 Synchronized 的性能存在疑问,其实 Synchronized 锁自从引入锁升级策略后,性能不再是问题,有兴趣的同学可以自己了解下 Synchronized 的**锁升级**。 \ No newline at end of file diff --git a/docs/java/collection/HashMap.md b/docs/java/collection/HashMap.md index c850cd57..c29d9223 100644 --- a/docs/java/collection/HashMap.md +++ b/docs/java/collection/HashMap.md @@ -512,7 +512,8 @@ public class HashMapDemo { } /** - * 另外一种不常用的遍历方式 + * 如果既要遍历key又要value,那么建议这种方式,因为如果先获取keySet然后再执行map.get(key),map内部会执行两次遍历。 + * 一次是在获取keySet的时候,一次是在遍历所有key的时候。 */ // 当我调用put(key,value)方法的时候,首先会把key和value封装到 // Entry这个静态内部类对象中,把Entry对象再添加到数组中,所以我们想获取 diff --git a/docs/java/collection/Java集合框架常见面试题.md b/docs/java/collection/Java集合框架常见面试题.md index c5280d53..71e89ac8 100644 --- a/docs/java/collection/Java集合框架常见面试题.md +++ b/docs/java/collection/Java集合框架常见面试题.md @@ -1,59 +1,198 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《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) - - [如何选用集合?](#如何选用集合) +- [1. 剖析面试最常见问题之 Java 集合框架](#1-剖析面试最常见问题之-java-集合框架) + - [1.1. 集合概述](#11-集合概述) + - [1.1.1. Java 集合概览](#111-java-集合概览) + - [1.1.2. 说说 List,Set,Map 三者的区别?](#112-说说-listsetmap-三者的区别) + - [1.1.3. 集合框架底层数据结构总结](#113-集合框架底层数据结构总结) + - [1.1.3.1. List](#1131-list) + - [1.1.3.2. Set](#1132-set) + - [1.1.3.3. Map](#1133-map) + - [1.1.4. 如何选用集合?](#114-如何选用集合) + - [1.1.5. 为什么要使用集合?](#115-为什么要使用集合) + - [1.1.6. Iterator 迭代器](#116-iterator-迭代器) + - [1.1.6.1. 迭代器 Iterator 是什么?](#1161-迭代器-iterator-是什么) + - [1.1.6.2. 迭代器 Iterator 有啥用?](#1162-迭代器-iterator-有啥用) + - [1.1.6.3. 如何使用?](#1163-如何使用) + - [1.1.7. 有哪些集合是线程不安全的?怎么解决呢?](#117-有哪些集合是线程不安全的怎么解决呢) + - [1.2. Collection 子接口之 List](#12-collection-子接口之-list) + - [1.2.1. Arraylist 和 Vector 的区别?](#121-arraylist-和-vector-的区别) + - [1.2.2. Arraylist 与 LinkedList 区别?](#122-arraylist-与-linkedlist-区别) + - [1.2.2.1. 补充内容:双向链表和双向循环链表](#1221-补充内容双向链表和双向循环链表) + - [1.2.2.2. 补充内容:RandomAccess 接口](#1222-补充内容randomaccess-接口) + - [1.2.3. 说一说 ArrayList 的扩容机制吧](#123-说一说-arraylist-的扩容机制吧) + - [1.3. Collection 子接口之 Set](#13-collection-子接口之-set) + - [1.3.1. comparable 和 Comparator 的区别](#131-comparable-和-comparator-的区别) + - [1.3.1.1. Comparator 定制排序](#1311-comparator-定制排序) + - [1.3.1.2. 重写 compareTo 方法实现按年龄来排序](#1312-重写-compareto-方法实现按年龄来排序) + - [1.3.2. 无序性和不可重复性的含义是什么](#132-无序性和不可重复性的含义是什么) + - [1.3.3. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同](#133-比较-hashsetlinkedhashset-和-treeset-三者的异同) + - [1.4. Map 接口](#14-map-接口) + - [1.4.1. HashMap 和 Hashtable 的区别](#141-hashmap-和-hashtable-的区别) + - [1.4.2. HashMap 和 HashSet 区别](#142-hashmap-和-hashset-区别) + - [1.4.3. HashMap 和 TreeMap 区别](#143-hashmap-和-treemap-区别) + - [1.4.4. HashSet 如何检查重复](#144-hashset-如何检查重复) + - [1.4.5. HashMap 的底层实现](#145-hashmap-的底层实现) + - [1.4.5.1. JDK1.8 之前](#1451-jdk18-之前) + - [1.4.5.2. JDK1.8 之后](#1452-jdk18-之后) + - [1.4.6. HashMap 的长度为什么是 2 的幂次方](#146-hashmap-的长度为什么是-2-的幂次方) + - [1.4.7. HashMap 多线程操作导致死循环问题](#147-hashmap-多线程操作导致死循环问题) + - [1.4.8. HashMap 有哪几种常见的遍历方式?](#148-hashmap-有哪几种常见的遍历方式) + - [1.4.9. ConcurrentHashMap 和 Hashtable 的区别](#149-concurrenthashmap-和-hashtable-的区别) + - [1.4.10. ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](#1410-concurrenthashmap-线程安全的具体实现方式底层具体实现) + - [1.4.10.1. JDK1.7(上面有示意图)](#14101-jdk17上面有示意图) + - [1.4.10.2. JDK1.8 (上面有示意图)](#14102-jdk18-上面有示意图) + - [1.5. Collections 工具类](#15-collections-工具类) + - [1.5.1. 排序操作](#151-排序操作) + - [1.5.2. 查找,替换操作](#152-查找替换操作) + - [1.5.3. 同步控制](#153-同步控制) + - [1.6. 其他重要问题](#16-其他重要问题) + - [1.6.1. 什么是快速失败(fail-fast)?](#161-什么是快速失败fail-fast) + - [1.6.2. 什么是安全失败(fail-safe)呢?](#162-什么是安全失败fail-safe呢) + - [1.6.3. Arrays.asList()避坑指南](#163-arraysaslist避坑指南) + - [1.6.3.1. 简介](#1631-简介) + - [1.6.3.2. 《阿里巴巴 Java 开发手册》对其的描述](#1632-阿里巴巴-java-开发手册对其的描述) + - [1.6.3.3. 使用时的注意事项总结](#1633-使用时的注意事项总结) -# 剖析面试最常见问题之Java集合框架 -## 说说List,Set,Map三者的区别? +# 1. 剖析面试最常见问题之 Java 集合框架 -- **List(对付顺序的好帮手):** List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 -- **Set(注重独一无二的性质):** 不允许重复的集合。不会有多个元素引用相同的对象。 -- **Map(用Key来搜索的专家):** 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。 +## 1.1. 集合概述 -## Arraylist 与 LinkedList 区别? +### 1.1.1. Java 集合概览 -- **1. 是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; +从下图可以看出,在 Java 中除了以 `Map` 结尾的类之外, 其他类都实现了 `Collection` 接口。 -- **2. 底层数据结构:** `Arraylist` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) +并且,以 `Map` 结尾的类都实现了 `Map` 接口。 -- **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))`因为需要先移动到指定位置再插入。** +![](./images/Java-Collections.jpeg) -- **4. 是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList` 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index) `方法)。 +### 1.1.2. 说说 List,Set,Map 三者的区别? -- **5. 内存空间占用:** ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。 +- `List`(对付顺序的好帮手): 存储的元素是有序的、可重复的。 +- `Set`(注重独一无二的性质): 存储的元素是无序的、不可重复的。 +- `Map`(用 Key 来搜索的专家): 使用键值对(kye-value)存储,类似于数学上的函数 y=f(x),“x”代表 key,"y"代表 value,Key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。 -### **补充内容:RandomAccess接口** +### 1.1.3. 集合框架底层数据结构总结 + +先来看一下 `Collection` 接口下面的集合。 + +#### 1.1.3.1. List + +- `Arraylist`: `Object[]`数组 +- `Vector`:`Object[]`数组 +- `LinkedList`: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环) + +#### 1.1.3.2. Set + +- `HashSet`(无序,唯一): 基于 `HashMap` 实现的,底层采用 `HashMap` 来保存元素 +- `LinkedHashSet`:`LinkedHashSet` 是 `HashSet` 的子类,并且其内部是通过 `LinkedHashMap` 来实现的。有点类似于我们之前说的 `LinkedHashMap` 其内部是基于 `HashMap` 实现一样,不过还是有一点点区别的 +- `TreeSet`(有序,唯一): 红黑树(自平衡的排序二叉树) + +再来看看 `Map` 接口下面的集合。 + +#### 1.1.3.3. 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`: 红黑树(自平衡的排序二叉树) + +### 1.1.4. 如何选用集合? + +主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 `Map` 接口下的集合,需要排序时选择 `TreeMap`,不需要排序时就选择 `HashMap`,需要保证线程安全就选用 `ConcurrentHashMap`。 + +当我们只需要存放元素值时,就选择实现`Collection` 接口的集合,需要保证元素唯一时选择实现 `Set` 接口的集合比如 `TreeSet` 或 `HashSet`,不需要就选择实现 `List` 接口的比如 `ArrayList` 或 `LinkedList`,然后再根据实现这些接口的集合的特点来选用。 + +### 1.1.5. 为什么要使用集合? + +当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端, +因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。 + +数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。 +但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据 + +### 1.1.6. Iterator 迭代器 + +#### 1.1.6.1. 迭代器 Iterator 是什么? + +```java +public interface Iterator { + //集合中是否还有元素 + boolean hasNext(); + //获得集合中的下一个元素 + E next(); + ...... +} +``` + +`Iterator` 对象称为迭代器(设计模式的一种),迭代器可以对集合进行遍历,但每一个集合内部的数据结构可能是不尽相同的,所以每一个集合存和取都很可能是不一样的,虽然我们可以人为地在每一个类中定义 `hasNext()` 和 `next()` 方法,但这样做会让整个集合体系过于臃肿。于是就有了迭代器。 + +迭代器是将这样的方法抽取出接口,然后在每个类的内部,定义自己迭代方式,这样做就规定了整个集合体系的遍历方式都是 `hasNext()`和`next()`方法,使用者不用管怎么实现的,会用即可。迭代器的定义为:提供一种方法访问一个容器对象中各个元素,而又不需要暴露该对象的内部细节。 + +#### 1.1.6.2. 迭代器 Iterator 有啥用? + +`Iterator` 主要是用来遍历集合用的,它的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 `ConcurrentModificationException` 异常。 + +#### 1.1.6.3. 如何使用? + +我们通过使用迭代器来遍历 `HashMap`,演示一下 迭代器 Iterator 的使用。 + +```java + +Map map = new HashMap(); +map.put(1, "Java"); +map.put(2, "C++"); +map.put(3, "PHP"); +Iterator> iterator = map.entrySet().iterator(); +while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + System.out.println(entry.getKey() + entry.getValue()); +} +``` + +### 1.1.7. 有哪些集合是线程不安全的?怎么解决呢? + +我们常用的 `Arraylist` ,`LinkedList`,`Hashmap`,`HashSet`,`TreeSet`,`TreeMap`,`PriorityQueue` 都不是线程安全的。解决办法很简单,可以使用线程安全的集合来代替。 + +如果你要使用线程安全的集合的话, `java.util.concurrent` 包中提供了很多并发容器供你使用: + +1. `ConcurrentHashMap`: 可以看作是线程安全的 `HashMap` +2. `CopyOnWriteArrayList`:可以看作是线程安全的 `ArrayList`,在读多写少的场合性能非常好,远远好于 `Vector`. +3. `ConcurrentLinkedQueue`:高效的并发队列,使用链表实现。可以看做一个线程安全的 `LinkedList`,这是一个非阻塞队列。 +4. `BlockingQueue`: 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。 +5. `ConcurrentSkipListMap` :跳表的实现。这是一个`Map`,使用跳表的数据结构进行快速查找。 + +## 1.2. Collection 子接口之 List + +### 1.2.1. Arraylist 和 Vector 的区别? + +1. ArrayList 是 List 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ; +2. Vector 是 List 的古老实现类,底层使用 Object[ ]存储,线程安全的。 + +### 1.2.2. 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 更多的空间(因为要存放直接后继和直接前驱以及数据)。 + +#### 1.2.2.1. 补充内容:双向链表和双向循环链表 + +**双向链表:** 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。 + +> 另外推荐一篇把双向链表讲清楚的文章:[https://juejin.im/post/5b5d1a9af265da0f47352f14](https://juejin.im/post/5b5d1a9af265da0f47352f14) + +![双向链表](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) + +#### 1.2.2.2. 补充内容:RandomAccess 接口 ```java public interface RandomAccess { @@ -62,7 +201,7 @@ public interface RandomAccess { 查看源码我们发现实际上 `RandomAccess` 接口中什么都没有定义。所以,在我看来 `RandomAccess` 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 -在 `binarySearch(`)方法中,它要判断传入的list 是否 `RamdomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法 +在 `binarySearch()` 方法中,它要判断传入的 list 是否 `RamdomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法 ```java public static @@ -74,228 +213,22 @@ public interface RandomAccess { } ``` -`ArrayList` 实现了 `RandomAccess` 接口, 而 `LinkedList` 没有实现。为什么呢?我觉得还是和底层数据结构有关!`ArrayList` 底层是数组,而 `LinkedList` 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,`ArrayList` 实现了 `RandomAccess` 接口,就表明了他具有快速随机访问功能。 `RandomAccess` 接口只是标识,并不是说 `ArrayList` 实现 `RandomAccess` 接口才具有快速随机访问功能的! +`ArrayList` 实现了 `RandomAccess` 接口, 而 `LinkedList` 没有实现。为什么呢?我觉得还是和底层数据结构有关!`ArrayList` 底层是数组,而 `LinkedList` 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,`ArrayList` 实现了 `RandomAccess` 接口,就表明了他具有快速随机访问功能。 `RandomAccess` 接口只是标识,并不是说 `ArrayList` 实现 `RandomAccess` 接口才具有快速随机访问功能的! -**下面再总结一下 list 的遍历方式选择:** +### 1.2.3. 说一说 ArrayList 的扩容机制吧 -- 实现了 `RandomAccess` 接口的list,优先选择普通 for 循环 ,其次 foreach, -- 未实现 `RandomAccess`接口的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的,),大size的数据,千万不要使用普通for循环 +详见笔主的这篇文章:[通过源码一步一步分析 ArrayList 扩容机制](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md) -### 补充内容:双向链表和双向循环链表 +## 1.3. Collection 子接口之 Set -**双向链表:** 包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。 +### 1.3.1. comparable 和 Comparator 的区别 -![双向链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/双向链表.png) +- `comparable` 接口实际上是出自`java.lang`包 它有一个 `compareTo(Object obj)`方法用来排序 +- `comparator`接口实际上是出自 java.util 包它有一个`compare(Object obj1, Object obj2)`方法用来排序 -**双向循环链表:** 最后一个节点的 next 指向head,而 head 的prev指向最后一个节点,构成一个环。 +一般我们需要对一个集合使用自定义排序时,我们就要重写`compareTo()`方法或`compare()`方法,当我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写`compareTo()`方法和使用自制的`Comparator`方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 `Collections.sort()`. -![双向循环链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/双向循环链表.png) - -## ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢? - -`Vector`类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。 - -`Arraylist`不是同步的,所以在不需要保证线程安全时建议使用Arraylist。 - -## 说一说 ArrayList 的扩容机制吧 - -详见笔主的这篇文章:[通过源码一步一步分析ArrayList 扩容机制](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md) - -## 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 没有这样的机制。 - -**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()方法用来判断对象的相等性, | - -## 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)时,将链表转化为红黑树,以减少搜索时间。 - -![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定制排序 +#### 1.3.1.1. Comparator 定制排序 ```java ArrayList arrayList = new ArrayList(); @@ -343,13 +276,12 @@ Collections.sort(arrayList): [7, 4, 3, 3, -1, -5, -7, -9] ``` -### 重写compareTo方法实现按年龄来排序 +#### 1.3.1.2. 重写 compareTo 方法实现按年龄来排序 ```java // person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列 // 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他 // 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了 - public class Person implements Comparable { private String name; private int age; @@ -377,17 +309,17 @@ public class Person implements Comparable { } /** - * TODO重写compareTo方法实现按年龄来排序 + * T重写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()) { + } + if (this.age < o.getAge()) { return -1; } - return age; + return 0; } } @@ -418,39 +350,497 @@ Output: 30-张三 ``` -## 集合框架底层数据结构总结 +### 1.3.2. 无序性和不可重复性的含义是什么 -### Collection +1、什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。 -#### 1. List +2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法。 -- **Arraylist:** Object数组 -- **Vector:** Object数组 -- **LinkedList:** 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) +### 1.3.3. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同 -#### 2. Set +HashSet 是 Set 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值; -- **HashSet(无序,唯一):** 基于 HashMap 实现的,底层采用 HashMap 来保存元素 -- **LinkedHashSet:** LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的 -- **TreeSet(有序,唯一):** 红黑树(自平衡的排序二叉树) +LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历; -### Map +TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。 -- **HashMap:** JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间 -- **LinkedHashMap:** LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931) -- **Hashtable:** 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 -- **TreeMap:** 红黑树(自平衡的排序二叉树) +## 1.4. Map 接口 -## 如何选用集合? +### 1.4.1. HashMap 和 Hashtable 的区别 -主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。 +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); + } +``` -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! +下面这个方法保证了 HashMap 总是使用 2 的幂作为哈希表的大小。 -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 +```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; + } +``` -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) +### 1.4.2. 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()方法用来判断对象的相等性, | + +### 1.4.3. HashMap 和 TreeMap 区别 + +`TreeMap` 和`HashMap` 都继承自`AbstractMap` ,但是需要注意的是`TreeMap`它还实现了`NavigableMap`接口和`SortedMap` 接口。 + +![](./images/TreeMap继承结构.png) + +实现 `NavigableMap` 接口让 `TreeMap` 有了对集合内元素的搜索的能力。 + +实现`SortMap`接口让 `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` 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。** + +### 1.4.4. 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 的区别** + +对于基本类型来说,== 比较的是值是否相等; + +对于引用类型来说,== 比较的是两个引用是否指向同一个对象地址(两者在内存中存放的地址(堆内存地址)是否指向同一个地方); + +对于引用类型(包括包装类型)来说,equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String),则比较的是地址里的内容。 + +### 1.4.5. HashMap 的底层实现 + +#### 1.4.5.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](images/jdk1.8之前的内部结构-HashMap.png) + +#### 1.4.5.2. JDK1.8 之后 + +相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 + +![jdk1.8之后的内部结构-HashMap](images/jdk1.8之后的内部结构-HashMap.png) + +> TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 + +### 1.4.6. 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 的幂次方。** + +### 1.4.7. HashMap 多线程操作导致死循环问题 + +主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。 + +详情请查看: + +### 1.4.8. HashMap 有哪几种常见的遍历方式? + +[HashMap 的 7 种遍历方式与性能分析!](https://mp.weixin.qq.com/s/Zz6mofCtmYpABDL1ap04ow) + +### 1.4.9. 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) + +

http://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) + +

http://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`**。当冲突链表达到一定长度时,链表会转换成红黑树。 + +### 1.4.10. ConcurrentHashMap 线程安全的具体实现方式/底层具体实现 + +#### 1.4.10.1. 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 的锁。 + +#### 1.4.10.2. JDK1.8 (上面有示意图) + +ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N))) + +synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。 + +## 1.5. Collections 工具类 + +Collections 工具类常用方法: + +1. 排序 +2. 查找,替换操作 +3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合) + +### 1.5.1. 排序操作 + +```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个元素整体移到后面 +``` + +### 1.5.2. 查找,替换操作 + +```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), 用新元素替换旧元素 +``` + +### 1.5.3. 同步控制 + +`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。 +``` + +## 1.6. 其他重要问题 + +### 1.6.1. 什么是快速失败(fail-fast)? + +**快速失败(fail-fast)** 是 Java 集合的一种错误检测机制。**在使用迭代器对集合进行遍历的时候,我们在多线程下操作非安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出 `ConcurrentModificationException` 异常。 另外,在单线程下,如果在遍历过程中对集合对象的内容进行了修改的话也会触发 fail-fast 机制。** + +> 注:增强 for 循环也是借助迭代器进行遍历。 + +举个例子:多线程下,如果线程 1 正在对集合进行遍历,此时线程 2 对集合进行修改(增加、删除、修改),或者线程 1 在遍历过程中对集合进行修改,都会导致线程 1 抛出 `ConcurrentModificationException` 异常。 + +**为什么呢?** + +每当迭代器使用 `hashNext()`/`next()`遍历下一个元素之前,都会检测 `modCount` 变量是否为 `expectedModCount` 值,是的话就返回遍历;否则抛出异常,终止遍历。 + +如果我们在集合被遍历期间对其进行修改的话,就会改变 `modCount` 的值,进而导致 `modCount != expectedModCount` ,进而抛出 `ConcurrentModificationException` 异常。 + +> 注:通过 `Iterator` 的方法修改集合的话会修改到 `expectedModCount` 的值,所以不会抛出异常。 + +```java +final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); +} +``` + +好吧!相信大家已经搞懂了快速失败(fail-fast)机制以及它的原理。 + +我们再来趁热打铁,看一个阿里巴巴手册相关的规定: + +![](images/ad28e3ba-e419-4724-869c-73879e604da1.png) + +有了前面讲的基础,我们应该知道:使用 `Iterator` 提供的 `remove` 方法,可以修改到 `expectedModCount` 的值。所以,才不会再抛出`ConcurrentModificationException` 异常。 + +### 1.6.2. 什么是安全失败(fail-safe)呢? + +明白了快速失败(fail-fast)之后,安全失败(fail-safe)我们就很好理解了。 + +采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以,在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 `ConcurrentModificationException` 异常。 + +### 1.6.3. Arrays.asList()避坑指南 + +最近使用`Arrays.asList()`遇到了一些坑,然后在网上看到这篇文章:[Java Array to List Examples](http://javadevnotes.com/java-array-to-list-examples) 感觉挺不错的,但是还不是特别全面。所以,自己对于这块小知识点进行了简单的总结。 + +#### 1.6.3.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); +} +``` + +#### 1.6.3.2. 《阿里巴巴 Java 开发手册》对其的描述 + +`Arrays.asList()`将数组转换为集合后,底层其实还是数组,《阿里巴巴 Java 开发手册》对于这个方法有如下描述: + +![阿里巴巴Java开发手-Arrays.asList()方法]() + +#### 1.6.3.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(); +} +``` + + + +**《Java面试突击》:** Java 程序员面试必备的《Java面试突击》V3.0 PDF 版本扫码关注下面的公众号,在后台回复 **"面试突击"** 即可免费领取! + +![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/format,png.jpeg) \ 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 00000000..54180092 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 00000000..cf9071ff Binary files /dev/null and b/docs/java/collection/images/Java-Collections.jpeg differ diff --git a/docs/java/collection/images/TreeMap继承结构.png b/docs/java/collection/images/TreeMap继承结构.png new file mode 100644 index 00000000..553e41b8 Binary files /dev/null and b/docs/java/collection/images/TreeMap继承结构.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 00000000..1c05ebaa 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 00000000..26ea14ca Binary files /dev/null and b/docs/java/collection/images/image-20200405151029416.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 00000000..a090c7cc Binary files /dev/null and b/docs/java/collection/images/java8_concurrenthashmap.png differ diff --git a/docs/java/collection/images/jdk1.8之前的内部结构-HashMap.png b/docs/java/collection/images/jdk1.8之前的内部结构-HashMap.png new file mode 100644 index 00000000..54180092 Binary files /dev/null and b/docs/java/collection/images/jdk1.8之前的内部结构-HashMap.png differ diff --git a/docs/java/collection/images/jdk1.8之后的内部结构-HashMap.png b/docs/java/collection/images/jdk1.8之后的内部结构-HashMap.png new file mode 100644 index 00000000..7c95e738 Binary files /dev/null and b/docs/java/collection/images/jdk1.8之后的内部结构-HashMap.png differ diff --git a/docs/java/images/Java异常类层次结构图.png b/docs/java/images/Java异常类层次结构图.png new file mode 100644 index 00000000..595dc8af Binary files /dev/null and b/docs/java/images/Java异常类层次结构图.png differ diff --git a/docs/java/images/Java异常类层次结构图2.png b/docs/java/images/Java异常类层次结构图2.png new file mode 100644 index 00000000..fd2a910d Binary files /dev/null and b/docs/java/images/Java异常类层次结构图2.png differ diff --git a/docs/java/images/image-20200405151029416.png b/docs/java/images/image-20200405151029416.png new file mode 100644 index 00000000..26ea14ca Binary files /dev/null and b/docs/java/images/image-20200405151029416.png differ diff --git a/docs/java/images/performance-tuning/java-performance1.png b/docs/java/images/performance-tuning/java-performance1.png new file mode 100644 index 00000000..0975d265 Binary files /dev/null and b/docs/java/images/performance-tuning/java-performance1.png differ diff --git a/docs/java/images/performance-tuning/java-performance2.png b/docs/java/images/performance-tuning/java-performance2.png new file mode 100644 index 00000000..76bbc0a8 Binary files /dev/null and b/docs/java/images/performance-tuning/java-performance2.png differ diff --git a/docs/java/images/performance-tuning/java-performance3.png b/docs/java/images/performance-tuning/java-performance3.png new file mode 100644 index 00000000..150e5ce5 Binary files /dev/null and b/docs/java/images/performance-tuning/java-performance3.png differ diff --git a/docs/java/images/performance-tuning/java-performance4.png b/docs/java/images/performance-tuning/java-performance4.png new file mode 100644 index 00000000..e3bf7450 Binary files /dev/null and b/docs/java/images/performance-tuning/java-performance4.png differ diff --git a/docs/java/images/performance-tuning/java-performance5.png b/docs/java/images/performance-tuning/java-performance5.png new file mode 100644 index 00000000..c6b44840 Binary files /dev/null and b/docs/java/images/performance-tuning/java-performance5.png differ diff --git a/docs/java/images/performance-tuning/java-performance6.png b/docs/java/images/performance-tuning/java-performance6.png new file mode 100644 index 00000000..b1e182ed Binary files /dev/null and b/docs/java/images/performance-tuning/java-performance6.png differ diff --git a/docs/java/images/performance-tuning/java-performance7.png b/docs/java/images/performance-tuning/java-performance7.png new file mode 100644 index 00000000..0796e30d Binary files /dev/null and b/docs/java/images/performance-tuning/java-performance7.png differ diff --git a/docs/java/images/performance-tuning/java-performance8.png b/docs/java/images/performance-tuning/java-performance8.png new file mode 100644 index 00000000..75172dfa Binary files /dev/null and b/docs/java/images/performance-tuning/java-performance8.png differ diff --git a/docs/java/java-naming-conventions.md b/docs/java/java-naming-conventions.md new file mode 100644 index 00000000..d205f5f5 --- /dev/null +++ b/docs/java/java-naming-conventions.md @@ -0,0 +1,416 @@ +> 原文链接: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程序设计题.md b/docs/java/java-programming-problem/Java程序设计题.md similarity index 77% rename from docs/java/Java程序设计题.md rename to docs/java/java-programming-problem/Java程序设计题.md index 46c9c169..112c1bff 100644 --- a/docs/java/Java程序设计题.md +++ b/docs/java/java-programming-problem/Java程序设计题.md @@ -1,6 +1,12 @@ -## 泛型的实际应用 + -### 实现最小值函数 +- [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接口。 @@ -23,10 +29,7 @@ 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()`这些基本的方法。** @@ -39,14 +42,14 @@ public class MyStack { private int count;//栈中元素数量 private static final int GROW_FACTOR = 2; - //TODO:不带初始容量的构造方法。默认容量为8 + //不带初始容量的构造方法。默认容量为8 public MyStack() { this.capacity = 8; this.storage=new int[8]; this.count = 0; } - //TODO:带初始容量的构造方法 + //带初始容量的构造方法 public MyStack(int initialCapacity) { if (initialCapacity < 1) throw new IllegalArgumentException("Capacity too small."); @@ -56,7 +59,7 @@ public class MyStack { this.count = 0; } - //TODO:入栈 + //入栈 public void push(int value) { if (count == capacity) { ensureCapacity(); @@ -64,23 +67,22 @@ public class MyStack { storage[count++] = value; } - //TODO:确保容量大小 + //确保容量大小 private void ensureCapacity() { int newCapacity = capacity * GROW_FACTOR; storage = Arrays.copyOf(storage, newCapacity); capacity = newCapacity; } - //TODO:返回栈顶元素并出栈 + //返回栈顶元素并出栈 private int pop() { - count--; - if (count == -1) + if (count == 0) throw new IllegalArgumentException("Stack is empty."); - + count--; return storage[count]; } - //TODO:返回栈顶元素不出栈 + //返回栈顶元素不出栈 private int peek() { if (count == 0){ throw new IllegalArgumentException("Stack is empty."); @@ -89,12 +91,12 @@ public class MyStack { } } - //TODO:判断栈是否为空 + //判断栈是否为空 private boolean isEmpty() { return count == 0; } - //TODO:返回栈中元素的个数 + //返回栈中元素的个数 private int size() { return count; } @@ -122,4 +124,7 @@ for (int i = 0; i < 8; i++) { } System.out.println(myStack.isEmpty());//true myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty. -``` \ No newline at end of file +``` + + + 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 new file mode 100644 index 00000000..7c367e11 --- /dev/null +++ b/docs/java/java-programming-problem/a-thread-safe-implementation-of-lru-cache.md @@ -0,0 +1,441 @@ + + +- [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/images/fc66979f-7974-40e8-88ae-6dbff15ac9ef.png b/docs/java/jdk-new-features/images/fc66979f-7974-40e8-88ae-6dbff15ac9ef.png new file mode 100644 index 00000000..7529c73f Binary files /dev/null and b/docs/java/jdk-new-features/images/fc66979f-7974-40e8-88ae-6dbff15ac9ef.png differ 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 new file mode 100644 index 00000000..5b193cd5 --- /dev/null +++ b/docs/java/jdk-new-features/new-features-from-jdk8-to-jdk14.md @@ -0,0 +1,441 @@ +大家好,我是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 以后支持的首个长期版本。** + +![](images/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调优参数.md b/docs/java/jvm/GC调优参数.md index b9475b08..31dd48b7 100644 --- a/docs/java/jvm/GC调优参数.md +++ b/docs/java/jvm/GC调优参数.md @@ -1,19 +1,24 @@ > 原文地址: https://juejin.im/post/5c94a123f265da610916081f。 -## JVM 配置常用参数 +## JVM 配置常用参数 -1. 堆参数; -2. 回收器参数; -3. 项目中常用配置; -4. 常用组合; +1. Java内存区域常见配置参数概览 +2. 堆参数; +3. 回收器参数; +4. 项目中常用配置; +5. 常用组合; + +### Java内存区域常见配置参数概览 + +![](pictures/内存区域常见配置参数.png) ### 堆参数 -![img](https://ask.qcloudimg.com/http-save/yehe-1130324/975rk4d0wx.jpeg?imageView2/2/w/1620) +![堆参数][1] ### 回收器参数 -![img](https://ask.qcloudimg.com/http-save/yehe-1130324/34nzellt71.jpeg?imageView2/2/w/1620) +![垃圾回收器参数][2] 如上表所示,目前**主要有串行、并行和并发三种**,对于大内存的应用而言,串行的性能太低,因此使用到的主要是并行和并发两种。并行和并发 GC 的策略通过 `UseParallelGC `和` UseConcMarkSweepGC` 来指定,还有一些细节的配置参数用来配置策略的执行方式。例如:`XX:ParallelGCThreads`, `XX:CMSInitiatingOccupancyFraction` 等。 通常:Young 区对象回收只可选择并行(耗时间),Old 区选择并发(耗 CPU)。 @@ -21,11 +26,11 @@ > 备注:在Java8中永久代的参数`-XX:PermSize` 和`-XX:MaxPermSize`已经失效。 -![img](https://ask.qcloudimg.com/http-save/yehe-1130324/urw285pczz.jpeg?imageView2/2/w/1620) +![项目中垃圾回收器常用配置][3] ### 常用组合 -![img](https://ask.qcloudimg.com/http-save/yehe-1130324/ff8ues5crb.jpeg?imageView2/2/w/1620) +![垃圾回收器常用组合][4] ## 常用 GC 调优策略 @@ -55,4 +60,9 @@ **策略5:**注意: 如果满足下面的指标,**则一般不需要进行 GC 优化:** -> MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次。 \ No newline at end of file +> 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/JDK监控和故障处理工具总结.md b/docs/java/jvm/JDK监控和故障处理工具总结.md index 8a8ec160..d5cb29de 100644 --- a/docs/java/jvm/JDK监控和故障处理工具总结.md +++ b/docs/java/jvm/JDK监控和故障处理工具总结.md @@ -83,9 +83,9 @@ jstat -

- -
+![](./pictures/jvm垃圾回收/01d330d8-2710-4fad-a91c-7bbbfaaefc0e.png) +上图所示的 Eden 区、From Survivor0("From") 区、To Survivor1("To") 区都属于新生代,Old Memory 区属于老年代。 -上图所示的 eden 区、s0("From") 区、s1("To") 区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s1("To"),并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。经过这次GC后,Eden区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。 +大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 -![堆内存常见分配策略 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/堆内存.jpg) +> 修正([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; +> ... +> } +> +> ``` +> +> + +经过这次GC后,Eden区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。 + +![堆内存常见分配策略 ](./pictures/jvm垃圾回收/堆内存.png) ### 1.1 对象优先在 eden 区分配 @@ -81,11 +105,6 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(G 大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.下面我们来进行实际测试以下。 -在测试之前我们先来看看 **Minor GC 和 Full GC 有什么不同呢?** - -- **新生代 GC(Minor GC)**:指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。 -- **老年代 GC(Major GC/Full GC)**:指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。 - **测试:** ```java @@ -99,10 +118,10 @@ public class GCTest { } ``` 通过以下方式运行: -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-26/25178350.jpg) +![](./pictures/jvm垃圾回收/25178350.png) 添加的参数:`-XX:+PrintGCDetails` -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-26/10317146.jpg) +![](./pictures/jvm垃圾回收/10317146.png) 运行结果 (红色字体描述有误,应该是对应于 JDK1.7 的永久代): @@ -157,28 +176,52 @@ public class GCTest { > ```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; +> 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; > ... > } > > ``` > -> +> 额外补充说明([issue672](https://github.com/Snailclimb/JavaGuide/issues/672)):**关于默认的晋升年龄是15,这个说法的来源大部分都是《深入理解Java虚拟机》这本书。** +> 如果你去Oracle的官网阅读[相关的虚拟机参数](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html),你会发现`-XX:MaxTenuringThreshold=threshold`这里有个说明 +> +> **Sets the maximum tenuring threshold for use in adaptive GC sizing. The largest value is 15. The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6.** +### 1.5主要进行 gc 的区域 + +周志明先生在《深入理解Java虚拟机》第二版中P92如是写道: + +> ~~*“老年代GC(Major GC/Full GC),指发生在老年代的GC……”*~~ + +上面的说法已经在《深入理解Java虚拟机》第三版中被改正过来了。感谢R大的回答: + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/b48228c2-ac00-4668-a78f-6f221f8563b5.png) + +**总结:** + +针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种: + +部分收集 (Partial GC): + +- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集; +- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集; +- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。 + +整堆收集 (Full GC):收集整个 Java 堆和方法区。 ## 2 对象已经死亡? 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象)。 -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/11034259.jpg) +![](./pictures/jvm垃圾回收/11034259.png) ### 2.1 引用计数法 @@ -207,8 +250,13 @@ public class ReferenceCountingGc { 这个算法的基本思想就是通过一系列的称为 **“GC Roots”** 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。 -![可达性分析算法 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/72762049.jpg) +![可达性分析算法 ](./pictures/jvm垃圾回收/72762049.png) +可作为GC Roots的对象包括下面几种: +* 虚拟机栈(栈帧中的本地变量表)中引用的对象 +* 本地方法栈(Native方法)中引用的对象 +* 方法区中类静态属性引用的对象 +* 方法区中常量引用的对象 ### 2.3 再谈引用 @@ -250,14 +298,12 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。 -### 2.5 如何判断一个常量是废弃常量 +### 2.5 如何判断一个常量是废弃常量? 运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢? 假如在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池。 -注意:我们在 [可能是把 Java 内存区域讲的最清楚的一篇文章 ](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484303&idx=1&sn=af0fd436cef755463f59ee4dd0720cbd&chksm=fd9855eecaefdcf8d94ac581cfda4e16c8a730bda60c3b50bc55c124b92f23b6217f7f8e58d5&token=506869459&lang=zh_CN#rd) 也讲了 JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。 - ### 2.6 如何判断一个类是无用的类 方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢? @@ -273,27 +319,28 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 ## 3 垃圾收集算法 -![垃圾收集算法分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/垃圾收集算法.jpg) +![垃圾收集算法分类](./pictures/jvm垃圾回收/垃圾收集算法.png) ### 3.1 标记-清除算法 -该算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题: +该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题: 1. **效率问题** 2. **空间问题(标记清除后会产生大量不连续的碎片)** -公众号 +![](./pictures/jvm垃圾回收/标记-清除算法.jpeg) ### 3.2 复制算法 为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。 -公众号 +公众号 ### 3.3 标记-整理算法 + 根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。 -![标记-整理算法 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/94057049.jpg) +![标记-整理算法 ](./pictures/jvm垃圾回收/94057049.png) ### 3.4 分代收集算法 @@ -307,7 +354,7 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 ## 4 垃圾收集器 -![垃圾收集器分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/垃圾收集器.jpg) +![垃圾收集器分类](./pictures/jvm垃圾回收/垃圾收集器.png) **如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。** @@ -318,7 +365,7 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 **“单线程”** 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( **"Stop The World"** ),直到它收集结束。 **新生代采用复制算法,老年代采用标记-整理算法。** -![ Serial 收集器 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/46873026.jpg) +![ Serial 收集器 ](./pictures/jvm垃圾回收/46873026.png) 虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。 @@ -328,7 +375,7 @@ Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收 **ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。** **新生代采用复制算法,老年代采用标记-整理算法。** -![ParNew 收集器 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/22018368.jpg) +![ParNew 收集器 ](./pictures/jvm垃圾回收/22018368.png) 它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。 @@ -357,8 +404,18 @@ Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它 **Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。** Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在困难的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。 **新生代采用复制算法,老年代采用标记-整理算法。** -![Parallel Scavenge 收集器 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/22018368.jpg) +![Parallel Scavenge 收集器 ](./pictures/jvm垃圾回收/parllel-scavenge收集器.png) +**是JDK1.8默认收集器** + 使用java -XX:+PrintCommandLineFlags -version命令查看 + +``` +-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC +java version "1.8.0_211" +Java(TM) SE Runtime Environment (build 1.8.0_211-b12) +Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode) +``` +JDK1.8默认使用的是Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC来禁用该功能 ### 4.4.Serial Old 收集器 **Serial 收集器的老年代版本**,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。 @@ -377,9 +434,9 @@ Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它 - **初始标记:** 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; - **并发标记:** 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 - **重新标记:** 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 -- **并发清除:** 开启用户线程,同时 GC 线程开始对为标记的区域做清扫。 +- **并发清除:** 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。 -![CMS 垃圾收集器 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/82825079.jpg) +![CMS 垃圾收集器 ](./pictures/jvm垃圾回收/CMS收集器.png) 从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:**并发收集、低停顿**。但是它有下面三个明显的缺点: @@ -408,7 +465,7 @@ G1 收集器的运作大致分为以下几个步骤: - **筛选回收** -**G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)**。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 GF 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。 +**G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)**。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。 ## 参考 diff --git a/docs/java/jvm/Java内存区域.md b/docs/java/jvm/Java内存区域.md index 60cc3c56..0cae9f0a 100644 --- a/docs/java/jvm/Java内存区域.md +++ b/docs/java/jvm/Java内存区域.md @@ -62,15 +62,12 @@ Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成 **JDK 1.8 之前:** -
- -
+![](./pictures/java内存区域/JVM运行时数据区域.png) **JDK 1.8 :** -
- -
+![](./pictures/java内存区域/2019-3Java运行时数据区域JDK1.8.png) + **线程私有的:** @@ -94,7 +91,7 @@ Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成 1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 -**注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。** +**注意:程序计数器是唯一一个不会出现 `OutOfMemoryError` 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。** ### 2.2 Java 虚拟机栈 @@ -102,12 +99,12 @@ Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成 **Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。** (实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。) -**局部变量表主要存放了编译器可知的各种数据类型**(boolean、byte、char、short、int、float、long、double)、**对象引用**(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 +**局部变量表主要存放了编译期可知的各种数据类型**(boolean、byte、char、short、int、float、long、double)、**对象引用**(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 -**Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。** +**Java 虚拟机栈会出现两种错误:`StackOverFlowError` 和 `OutOfMemoryError`。** -- **StackOverFlowError:** 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。 -- **OutOfMemoryError:** 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 错误。 +- **`StackOverFlowError`:** 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。 +- **`OutOfMemoryError`:** 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。 Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。 @@ -134,6 +131,8 @@ Java 方法有两种返回方式: Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。** +**Java世界中“几乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。** + Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(Garbage Collected Heap)**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。** 在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分: @@ -142,11 +141,11 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(G 2. 老生代(Old Generation) 3. 永生代(Permanent Generation) -![JVM堆内存结构-JDK7](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/JVM堆内存结构-JDK7.jpg) +![JVM堆内存结构-JDK7](./pictures/java内存区域/JVM堆内存结构-JDK7.png) JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。 -![JVM堆内存结构-JDK8](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/JVM堆内存结构-jdk8.jpg) +![JVM堆内存结构-JDK8](./pictures/java内存区域/JVM堆内存结构-jdk8.png) **上图所示的 Eden 区、两个 Survivor 区都属于新生代(为了区分,这两个 Survivor 区域按照顺序被命名为 from 和 to),中间一层属于老年代。** @@ -226,14 +225,21 @@ JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1 ### 2.6 运行时常量池 -运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用) +运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用) 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。 -**JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。** +~~**JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。**~~ -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-14/26038433.jpg) -——图片来源:https://blog.csdn.net/wangbiao007/article/details/78545189 +> 修正([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 直接内存 @@ -250,7 +256,7 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于** ### 3.1 对象的创建 下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。 -![Java创建对象的过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Java创建对象的过程.png) +![Java创建对象的过程](./pictures/java内存区域/Java创建对象的过程.png) #### Step1:类加载检查 @@ -258,14 +264,14 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于** #### Step2:分配内存 -在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。 +在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。 **内存分配的两种方式:(补充内容,需要掌握)** 选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的 -![内存分配的两种方式](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/内存分配的两种方式.png) +![内存分配的两种方式](./pictures/java内存区域/内存分配的两种方式.png) **内存分配并发问题(补充内容,需要掌握)** @@ -280,7 +286,7 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于** #### Step4:设置对象头 -初始化零值完成之后,**虚拟机要对对象进行必要的设置**,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 **这些信息存放在对象头中。** 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 +初始化零值完成之后,**虚拟机要对对象进行必要的设置**,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 **这些信息存放在对象头中。** 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 #### Step5:执行 init 方法 @@ -302,11 +308,11 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于** 1. **句柄:** 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; - ![对象的访问定位-使用句柄](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/对象的访问定位-使用句柄.png) + ![对象的访问定位-使用句柄](./pictures/java内存区域/对象的访问定位-使用句柄.png) 2. **直接指针:** 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。 -![对象的访问定位-直接指针](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/对象的访问定位-直接指针.png) +![对象的访问定位-直接指针](./pictures/java内存区域/对象的访问定位-直接指针.png) **这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。** @@ -334,7 +340,7 @@ System.out.println(str2==str3);//false 再给大家一个图应该更容易理解,图片来源:: -![String-Pool-Java](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3String-Pool-Java1-450x249.png) +![String-Pool-Java](./pictures/java内存区域/2019-3String-Pool-Java1-450x249.png) **String 类型的常量池比较特殊。它的主要使用方法有两种:** @@ -362,7 +368,7 @@ System.out.println(str2==str3);//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) +![字符串拼接](./pictures/java内存区域/字符串拼接-常量池2.png) 尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。 ### 4.2 String s1 = new String("abc");这句话创建了几个字符串对象? diff --git a/docs/java/jvm/jvm 知识点汇总.md b/docs/java/jvm/jvm 知识点汇总.md index 4529e858..7a835ecc 100644 --- a/docs/java/jvm/jvm 知识点汇总.md +++ b/docs/java/jvm/jvm 知识点汇总.md @@ -1,4 +1,3 @@ - 无论什么级别的Java从业者,JVM都是进阶时必须迈过的坎。不管是工作还是面试中,JVM都是必考题。如果不懂JVM的话,薪酬会非常吃亏(近70%的面试者挂在JVM上了)。 @@ -10,4 +9,4 @@ 掌握JVM,是深入Java技术栈的必经之路。 -![jv.png](https://i.loli.net/2019/09/10/HsJXU8S4oVtCTM7.png) +![jv.png](./pictures/HsJXU8S4oVtCTM7.png) \ No newline at end of file diff --git a/docs/java/jvm/pictures/HsJXU8S4oVtCTM7.png b/docs/java/jvm/pictures/HsJXU8S4oVtCTM7.png new file mode 100644 index 00000000..52f4b008 Binary files /dev/null and b/docs/java/jvm/pictures/HsJXU8S4oVtCTM7.png differ diff --git a/docs/java/jvm/pictures/java内存区域/2019-3Java运行时数据区域JDK1.8.png b/docs/java/jvm/pictures/java内存区域/2019-3Java运行时数据区域JDK1.8.png new file mode 100644 index 00000000..c5088be5 Binary files /dev/null and b/docs/java/jvm/pictures/java内存区域/2019-3Java运行时数据区域JDK1.8.png differ diff --git a/docs/java/jvm/pictures/java内存区域/2019-3String-Pool-Java1-450x249.png b/docs/java/jvm/pictures/java内存区域/2019-3String-Pool-Java1-450x249.png new file mode 100644 index 00000000..b6e24178 Binary files /dev/null and b/docs/java/jvm/pictures/java内存区域/2019-3String-Pool-Java1-450x249.png differ diff --git a/docs/java/jvm/pictures/java内存区域/JVM堆内存结构-JDK7.png b/docs/java/jvm/pictures/java内存区域/JVM堆内存结构-JDK7.png new file mode 100644 index 00000000..3e90da89 Binary files /dev/null and b/docs/java/jvm/pictures/java内存区域/JVM堆内存结构-JDK7.png differ diff --git a/docs/java/jvm/pictures/java内存区域/JVM堆内存结构-jdk8.png b/docs/java/jvm/pictures/java内存区域/JVM堆内存结构-jdk8.png new file mode 100644 index 00000000..829aede4 Binary files /dev/null and b/docs/java/jvm/pictures/java内存区域/JVM堆内存结构-jdk8.png differ diff --git a/docs/java/jvm/pictures/java内存区域/JVM运行时数据区域.png b/docs/java/jvm/pictures/java内存区域/JVM运行时数据区域.png new file mode 100644 index 00000000..bf52c66e Binary files /dev/null and b/docs/java/jvm/pictures/java内存区域/JVM运行时数据区域.png differ diff --git a/docs/java/jvm/pictures/java内存区域/Java创建对象的过程.png b/docs/java/jvm/pictures/java内存区域/Java创建对象的过程.png new file mode 100644 index 00000000..7c4a79f1 Binary files /dev/null and b/docs/java/jvm/pictures/java内存区域/Java创建对象的过程.png differ diff --git a/docs/java/jvm/pictures/java内存区域/内存分配的两种方式.png b/docs/java/jvm/pictures/java内存区域/内存分配的两种方式.png new file mode 100644 index 00000000..1d0081b1 Binary files /dev/null and b/docs/java/jvm/pictures/java内存区域/内存分配的两种方式.png differ diff --git a/docs/java/jvm/pictures/java内存区域/字符串拼接-常量池2.png b/docs/java/jvm/pictures/java内存区域/字符串拼接-常量池2.png new file mode 100644 index 00000000..b680fb60 Binary files /dev/null and b/docs/java/jvm/pictures/java内存区域/字符串拼接-常量池2.png differ diff --git a/docs/java/jvm/pictures/java内存区域/对象的访问定位-直接指针.png b/docs/java/jvm/pictures/java内存区域/对象的访问定位-直接指针.png new file mode 100644 index 00000000..145bd405 Binary files /dev/null and b/docs/java/jvm/pictures/java内存区域/对象的访问定位-直接指针.png differ diff --git a/docs/java/jvm/pictures/jdk监控和故障处理工具总结/1JConsole连接.png b/docs/java/jvm/pictures/jdk监控和故障处理工具总结/1JConsole连接.png new file mode 100644 index 00000000..ae1e6106 Binary files /dev/null and b/docs/java/jvm/pictures/jdk监控和故障处理工具总结/1JConsole连接.png differ diff --git a/docs/java/jvm/pictures/jdk监控和故障处理工具总结/2查看Java程序概况.png b/docs/java/jvm/pictures/jdk监控和故障处理工具总结/2查看Java程序概况.png new file mode 100644 index 00000000..3a997022 Binary files /dev/null and b/docs/java/jvm/pictures/jdk监控和故障处理工具总结/2查看Java程序概况.png differ diff --git a/docs/java/jvm/pictures/jdk监控和故障处理工具总结/3内存监控.png b/docs/java/jvm/pictures/jdk监控和故障处理工具总结/3内存监控.png new file mode 100644 index 00000000..56d98052 Binary files /dev/null and b/docs/java/jvm/pictures/jdk监控和故障处理工具总结/3内存监控.png differ diff --git a/docs/java/jvm/pictures/jdk监控和故障处理工具总结/4线程监控.png b/docs/java/jvm/pictures/jdk监控和故障处理工具总结/4线程监控.png new file mode 100644 index 00000000..2ad324bd Binary files /dev/null and b/docs/java/jvm/pictures/jdk监控和故障处理工具总结/4线程监控.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/01d330d8-2710-4fad-a91c-7bbbfaaefc0e.png b/docs/java/jvm/pictures/jvm垃圾回收/01d330d8-2710-4fad-a91c-7bbbfaaefc0e.png new file mode 100644 index 00000000..7934357e Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/01d330d8-2710-4fad-a91c-7bbbfaaefc0e.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/10317146.png b/docs/java/jvm/pictures/jvm垃圾回收/10317146.png new file mode 100644 index 00000000..a77222ba Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/10317146.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/11034259.png b/docs/java/jvm/pictures/jvm垃圾回收/11034259.png new file mode 100644 index 00000000..092dc12e Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/11034259.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/22018368.png b/docs/java/jvm/pictures/jvm垃圾回收/22018368.png new file mode 100644 index 00000000..c79c76f3 Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/22018368.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/22018368213213.png b/docs/java/jvm/pictures/jvm垃圾回收/22018368213213.png new file mode 100644 index 00000000..c79c76f3 Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/22018368213213.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/25178350.png b/docs/java/jvm/pictures/jvm垃圾回收/25178350.png new file mode 100644 index 00000000..cc307027 Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/25178350.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/29176325.png b/docs/java/jvm/pictures/jvm垃圾回收/29176325.png new file mode 100644 index 00000000..a6d2199e Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/29176325.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/46873026.png b/docs/java/jvm/pictures/jvm垃圾回收/46873026.png new file mode 100644 index 00000000..2145dce9 Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/46873026.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/72762049.png b/docs/java/jvm/pictures/jvm垃圾回收/72762049.png new file mode 100644 index 00000000..f326103f Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/72762049.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/82825079.png b/docs/java/jvm/pictures/jvm垃圾回收/82825079.png new file mode 100644 index 00000000..3ed3bd82 Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/82825079.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/90984624.png b/docs/java/jvm/pictures/jvm垃圾回收/90984624.png new file mode 100644 index 00000000..6909a605 Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/90984624.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/94057049.png b/docs/java/jvm/pictures/jvm垃圾回收/94057049.png new file mode 100644 index 00000000..86d43ee6 Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/94057049.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/CMS收集器.png b/docs/java/jvm/pictures/jvm垃圾回收/CMS收集器.png new file mode 100644 index 00000000..3ed3bd82 Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/CMS收集器.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/parllel-scavenge收集器.png b/docs/java/jvm/pictures/jvm垃圾回收/parllel-scavenge收集器.png new file mode 100644 index 00000000..c79c76f3 Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/parllel-scavenge收集器.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/垃圾收集器.png b/docs/java/jvm/pictures/jvm垃圾回收/垃圾收集器.png new file mode 100644 index 00000000..888f879d Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/垃圾收集器.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/垃圾收集算法.png b/docs/java/jvm/pictures/jvm垃圾回收/垃圾收集算法.png new file mode 100644 index 00000000..0a4973bd Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/垃圾收集算法.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/堆内存.png b/docs/java/jvm/pictures/jvm垃圾回收/堆内存.png new file mode 100644 index 00000000..14815710 Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/堆内存.png differ diff --git a/docs/java/jvm/pictures/jvm垃圾回收/标记-清除算法.jpeg b/docs/java/jvm/pictures/jvm垃圾回收/标记-清除算法.jpeg new file mode 100644 index 00000000..c4cdc750 Binary files /dev/null and b/docs/java/jvm/pictures/jvm垃圾回收/标记-清除算法.jpeg differ diff --git a/docs/java/jvm/pictures/内存区域常见配置参数.png b/docs/java/jvm/pictures/内存区域常见配置参数.png new file mode 100644 index 00000000..7199d806 Binary files /dev/null and b/docs/java/jvm/pictures/内存区域常见配置参数.png differ diff --git a/docs/java/jvm/类加载器.md b/docs/java/jvm/类加载器.md index 00a89047..1d0a826f 100644 --- a/docs/java/jvm/类加载器.md +++ b/docs/java/jvm/类加载器.md @@ -118,7 +118,11 @@ protected Class loadClass(String name, boolean resolve) ### 如果我们不想用双亲委派模型怎么办? -为了避免双亲委托机制,我们可以自己定义一个类加载器,然后重写 `loadClass()` 即可。 +~~为了避免双亲委托机制,我们可以自己定义一个类加载器,然后重写 `loadClass()` 即可。~~ + +完善修正([issue871](https://github.com/Snailclimb/JavaGuide/issues/871):类加载器一问的补充说明): + + **自定义加载器的话,需要继承 `ClassLoader` 。如果我们不想打破双亲委派模型,就重写 `ClassLoader` 类中的 `findClass()` 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 `loadClass()` 方法** ## 自定义类加载器 diff --git a/docs/java/jvm/类加载过程.md b/docs/java/jvm/类加载过程.md index 895ba43f..9330c581 100644 --- a/docs/java/jvm/类加载过程.md +++ b/docs/java/jvm/类加载过程.md @@ -1,27 +1,37 @@ 点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - -- [类加载过程](#类加载过程) - - [加载](#加载) - - [验证](#验证) - - [准备](#准备) - - [解析](#解析) - - [初始化](#初始化) - - - > 公众号JavaGuide 后台回复关键字“1”,免费获取JavaGuide配套的Java工程师必备学习资源(文末有公众号二维码)。 -# 类加载过程 + + +- [类的生命周期](#类的生命周期) + - [类加载过程](#类加载过程) + - [加载](#加载) + - [验证](#验证) + - [准备](#准备) + - [解析](#解析) + - [初始化](#初始化) + - [卸载](#卸载) + - [公众号](#公众号) + + + +# 类的生命周期 + +一个类的完整生命周期如下: + + +![](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) +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/类加载过程.png) -## 加载 +### 加载 类加载过程的第一步,主要完成下面3件事情: @@ -37,11 +47,11 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚 加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。 -## 验证 +### 验证 ![验证阶段示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/验证阶段.png) -## 准备 +### 准备 **准备阶段是正式为类变量分配内存并设置类变量初始值的阶段**,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意: @@ -52,7 +62,7 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚 ![基本数据类型的零值](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/基本数据类型的零值.png) -## 解析 +### 解析 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。 @@ -60,19 +70,41 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚 综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。 -## 初始化 +### 初始化 初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 ` ()`方法的过程。 对于`()` 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 `()` 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。 -对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化: +对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化(只有主动去使用类才会初始化类): 1. 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。 -2. 使用 `java.lang.reflect` 包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化。 + - 当jvm执行new指令时会初始化类。即当程序创建一个类的实例对象。 + - 当jvm执行getstatic指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。 + - 当jvm执行putstatic指令时会初始化类。即程序给类的静态变量赋值。 + - 当jvm执行invokestatic指令时会初始化类。即程序调用类的静态方法。 +2. 使用 `java.lang.reflect` 包的方法对类进行反射调用时如Class.forname("..."),newInstance()等等。 ,如果类没初始化,需要触发其初始化。 3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。 4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。 -5. 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化。 +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,PlatformClassLoader,AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。 **参考** @@ -89,3 +121,4 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚 **Java工程师必备学习资源:** 一些Java工程师常用学习资源[公众号](#公众号)后台回复关键字 **“1”** 即可免费无套路获取。 ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) + diff --git a/docs/java/jvm/类文件结构.md b/docs/java/jvm/类文件结构.md index 6cdc3120..d766aa80 100644 --- a/docs/java/jvm/类文件结构.md +++ b/docs/java/jvm/类文件结构.md @@ -46,7 +46,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;//此类的属性表中的属性数 @@ -144,12 +144,12 @@ public class Employee { u2 this_class;//当前类 u2 super_class;//父类 u2 interfaces_count;//接口 - u2 interfaces[interfaces_count];//一个雷可以实现多个接口 + u2 interfaces[interfaces_count];//一个类可以实现多个接口 ``` **类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 `java.lang.Object` 之外,所有的 java 类都有父类,因此除了 `java.lang.Object` 外,所有 Java 类的父类索引都不为 0。** -**接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按`implents`(如果这个类本身是接口的话则是`extends`) 后的接口顺序从左到右排列在接口索引集合中。** +**接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 `implements` (如果这个类本身是接口的话则是`extends`) 后的接口顺序从左到右排列在接口索引集合中。** ### 2.6 字段表集合 diff --git a/docs/java/手把手教你定位常见Java性能问题.md b/docs/java/手把手教你定位常见Java性能问题.md new file mode 100644 index 00000000..980eeefb --- /dev/null +++ b/docs/java/手把手教你定位常见Java性能问题.md @@ -0,0 +1,404 @@ +## 手把手教你定位常见Java性能问题 + +## 概述 + +性能优化一向是后端服务优化的重点,但是线上性能故障问题不是经常出现,或者受限于业务产品,根本就没办法出现性能问题,包括笔者自己遇到的性能问题也不多,所以为了提前储备知识,当出现问题的时候不会手忙脚乱,我们本篇文章来模拟下常见的几个Java性能故障,来学习怎么去分析和定位。 + +## 预备知识 + +既然是定位问题,肯定是需要借助工具,我们先了解下需要哪些工具可以帮忙定位问题。 + + **top命令** + +`top`命令使我们最常用的Linux命令之一,它可以实时的显示当前正在执行的进程的CPU使用率,内存使用率等系统信息。`top -Hp pid` 可以查看线程的系统资源使用情况。 + + **vmstat命令** + +vmstat是一个指定周期和采集次数的虚拟内存检测工具,可以统计内存,CPU,swap的使用情况,它还有一个重要的常用功能,用来观察进程的上下文切换。字段说明如下: + +- r: 运行队列中进程数量(当数量大于CPU核数表示有阻塞的线程) +- b: 等待IO的进程数量 +- swpd: 使用虚拟内存大小 +- free: 空闲物理内存大小 +- buff: 用作缓冲的内存大小(内存和硬盘的缓冲区) +- cache: 用作缓存的内存大小(CPU和内存之间的缓冲区) +- si: 每秒从交换区写到内存的大小,由磁盘调入内存 +- so: 每秒写入交换区的内存大小,由内存调入磁盘 +- bi: 每秒读取的块数 +- bo: 每秒写入的块数 +- in: 每秒中断数,包括时钟中断。 +- cs: 每秒上下文切换数。 +- us: 用户进程执行时间百分比(user time) +- sy: 内核系统进程执行时间百分比(system time) +- wa: IO等待时间百分比 +- id: 空闲时间百分比 + + **pidstat命令** + +pidstat 是 Sysstat 中的一个组件,也是一款功能强大的性能监测工具,`top` 和 `vmstat` 两个命令都是监测进程的内存、CPU 以及 I/O 使用情况,而 pidstat 命令可以检测到线程级别的。`pidstat`命令线程切换字段说明如下: + +- UID :被监控任务的真实用户ID。 + +- TGID :线程组ID。 + +- TID:线程ID。 + +- cswch/s:主动切换上下文次数,这里是因为资源阻塞而切换线程,比如锁等待等情况。 + +- nvcswch/s:被动切换上下文次数,这里指CPU调度切换了线程。 + + **jstack命令** + +jstack是JDK工具命令,它是一种线程堆栈分析工具,最常用的功能就是使用 `jstack pid` 命令查看线程的堆栈信息,也经常用来排除死锁情况。 + +**jstat 命令** + +它可以检测Java程序运行的实时情况,包括堆内存信息和垃圾回收信息,我们常常用来查看程序垃圾回收情况。常用的命令是`jstat -gc pid`。信息字段说明如下: + +- S0C:年轻代中 To Survivor 的容量(单位 KB); + +- S1C:年轻代中 From Survivor 的容量(单位 KB); + +- S0U:年轻代中 To Survivor 目前已使用空间(单位 KB); + +- S1U:年轻代中 From Survivor 目前已使用空间(单位 KB); + +- EC:年轻代中 Eden 的容量(单位 KB); + +- EU:年轻代中 Eden 目前已使用空间(单位 KB); + +- OC:老年代的容量(单位 KB); + +- OU:老年代目前已使用空间(单位 KB); + +- MC:元空间的容量(单位 KB); + +- MU:元空间目前已使用空间(单位 KB); + +- YGC:从应用程序启动到采样时年轻代中 gc 次数; + +- YGCT:从应用程序启动到采样时年轻代中 gc 所用时间 (s); + +- FGC:从应用程序启动到采样时 老年代(Full Gc)gc 次数; + +- FGCT:从应用程序启动到采样时 老年代代(Full Gc)gc 所用时间 (s); + +- GCT:从应用程序启动到采样时 gc 用的总时间 (s)。 + + + + **jmap命令** + +jmap也是JDK工具命令,他可以查看堆内存的初始化信息以及堆内存的使用情况,还可以生成dump文件来进行详细分析。查看堆内存情况命令`jmap -heap pid`。 + + **mat内存工具** + +MAT(Memory Analyzer Tool)工具是eclipse的一个插件(MAT也可以单独使用),它分析大内存的dump文件时,可以非常直观的看到各个对象在堆空间中所占用的内存大小、类实例数量、对象引用关系、利用OQL对象查询,以及可以很方便的找出对象GC Roots的相关信息。 + +**idea中也有这么一个插件,就是JProfiler**。 + +相关阅读: + +1. 《性能诊断利器 JProfiler 快速入门和最佳实践》:[https://segmentfault.com/a/1190000017795841](https://segmentfault.com/a/1190000017795841) + +## 模拟环境准备 + +基础环境jdk1.8,采用SpringBoot框架来写几个接口来触发模拟场景,首先是模拟CPU占满情况 + +## CPU占满 + +模拟CPU占满还是比较简单,直接写一个死循环计算消耗CPU即可。 + +````java + /** + * 模拟CPU占满 + */ + @GetMapping("/cpu/loop") + public void testCPULoop() throws InterruptedException { + System.out.println("请求cpu死循环"); + Thread.currentThread().setName("loop-thread-cpu"); + int num = 0; + while (true) { + num++; + if (num == Integer.MAX_VALUE) { + System.out.println("reset"); + } + num = 0; + } + + } +```` + +请求接口地址测试`curl localhost:8080/cpu/loop`,发现CPU立马飙升到100% + +![](./images/performance-tuning/java-performance1.png) + +通过执行`top -Hp 32805` 查看Java线程情况 + +![](./images/performance-tuning/java-performance2.png) + +执行 `printf '%x' 32826` 获取16进制的线程id,用于`dump`信息查询,结果为 `803a`。最后我们执行`jstack 32805 |grep -A 20 803a `来查看下详细的`dump`信息。 + +![](./images/performance-tuning/java-performance3.png) + +这里`dump`信息直接定位出了问题方法以及代码行,这就定位出了CPU占满的问题。 + +## 内存泄露 + +模拟内存泄漏借助了ThreadLocal对象来完成,ThreadLocal是一个线程私有变量,可以绑定到线程上,在整个线程的生命周期都会存在,但是由于ThreadLocal的特殊性,ThreadLocal是基于ThreadLocalMap实现的,ThreadLocalMap的Entry继承WeakReference,而Entry的Key是WeakReference的封装,换句话说Key就是弱引用,弱引用在下次GC之后就会被回收,如果ThreadLocal在set之后不进行后续的操作,因为GC会把Key清除掉,但是Value由于线程还在存活,所以Value一直不会被回收,最后就会发生内存泄漏。 + +````Java +/** + * 模拟内存泄漏 + */ + @GetMapping(value = "/memory/leak") + public String leak() { + System.out.println("模拟内存泄漏"); + ThreadLocal localVariable = new ThreadLocal(); + localVariable.set(new Byte[4096 * 1024]);// 为线程添加变量 + return "ok"; + } +```` + +我们给启动加上堆内存大小限制,同时设置内存溢出的时候输出堆栈快照并输出日志。 + +`java -jar -Xms500m -Xmx500m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heaplog.log analysis-demo-0.0.1-SNAPSHOT.jar` + +启动成功后我们循环执行100次,`for i in {1..500}; do curl localhost:8080/memory/leak;done`,还没执行完毕,系统已经返回500错误了。查看系统日志出现了如下异常: + +``` +java.lang.OutOfMemoryError: Java heap space +``` + +我们用`jstat -gc pid` 命令来看看程序的GC情况。 + +![](./images/performance-tuning/java-performance4.png) + +很明显,内存溢出了,堆内存经过45次 Full Gc 之后都没释放出可用内存,这说明当前堆内存中的对象都是存活的,有GC Roots引用,无法回收。那是什么原因导致内存溢出呢?是不是我只要加大内存就行了呢?如果是普通的内存溢出也许扩大内存就行了,但是如果是内存泄漏的话,扩大的内存不一会就会被占满,所以我们还需要确定是不是内存泄漏。我们之前保存了堆 Dump 文件,这个时候借助我们的MAT工具来分析下。导入工具选择`Leak Suspects Report`,工具直接就会给你列出问题报告。 + +![](./images/performance-tuning/java-performance5.png) + +这里已经列出了可疑的4个内存泄漏问题,我们点击其中一个查看详情。 + +![](./images/performance-tuning/java-performance6.png) + +这里已经指出了内存被线程占用了接近50M的内存,占用的对象就是ThreadLocal。如果想详细的通过手动去分析的话,可以点击`Histogram`,查看最大的对象占用是谁,然后再分析它的引用关系,即可确定是谁导致的内存溢出。 + +![](./images/performance-tuning/java-performance7.png) + +上图发现占用内存最大的对象是一个Byte数组,我们看看它到底被那个GC Root引用导致没有被回收。按照上图红框操作指引,结果如下图: + +![](./images/performance-tuning/java-performance8.png) + +我们发现Byte数组是被线程对象引用的,图中也标明,Byte数组对像的GC Root是线程,所以它是不会被回收的,展开详细信息查看,我们发现最终的内存占用对象是被ThreadLocal对象占据了。这也和MAT工具自动帮我们分析的结果一致。 + +## 死锁 + +死锁会导致耗尽线程资源,占用内存,表现就是内存占用升高,CPU不一定会飙升(看场景决定),如果是直接new线程,会导致JVM内存被耗尽,报无法创建线程的错误,这也是体现了使用线程池的好处。 + +```java + ExecutorService service = new ThreadPoolExecutor(4, 10, + 0, TimeUnit.SECONDS, new LinkedBlockingQueue(1024), + Executors.defaultThreadFactory(), + new ThreadPoolExecutor.AbortPolicy()); + /** + * 模拟死锁 + */ + @GetMapping("/cpu/test") + public String testCPU() throws InterruptedException { + System.out.println("请求cpu"); + Object lock1 = new Object(); + Object lock2 = new Object(); + service.submit(new DeadLockThread(lock1, lock2), "deadLookThread-" + new Random().nextInt()); + service.submit(new DeadLockThread(lock2, lock1), "deadLookThread-" + new Random().nextInt()); + return "ok"; + } + +public class DeadLockThread implements Runnable { + private Object lock1; + private Object lock2; + + public DeadLockThread1(Object lock1, Object lock2) { + this.lock1 = lock1; + this.lock2 = lock2; + } + + @Override + public void run() { + synchronized (lock2) { + System.out.println(Thread.currentThread().getName()+"get lock2 and wait lock1"); + try { + TimeUnit.MILLISECONDS.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + synchronized (lock1) { + System.out.println(Thread.currentThread().getName()+"get lock1 and lock2 "); + } + } + } +} +``` + +我们循环请求接口2000次,发现不一会系统就出现了日志错误,线程池和队列都满了,由于我选择的当队列满了就拒绝的策略,所以系统直接抛出异常。 + +``` +java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@2760298 rejected from java.util.concurrent.ThreadPoolExecutor@7ea7cd51[Running, pool size = 10, active threads = 10, queued tasks = 1024, completed tasks = 846] +``` + +通过`ps -ef|grep java`命令找出 Java 进程 pid,执行`jstack pid` 即可出现java线程堆栈信息,这里发现了5个死锁,我们只列出其中一个,很明显线程`pool-1-thread-2`锁住了`0x00000000f8387d88`等待`0x00000000f8387d98`锁,线程`pool-1-thread-1`锁住了`0x00000000f8387d98`等待锁`0x00000000f8387d88`,这就产生了死锁。 + +```JAVA +Java stack information for the threads listed above: +=================================================== +"pool-1-thread-2": + at top.luozhou.analysisdemo.controller.DeadLockThread2.run(DeadLockThread.java:30) + - waiting to lock <0x00000000f8387d98> (a java.lang.Object) + - locked <0x00000000f8387d88> (a java.lang.Object) + at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) + at java.util.concurrent.FutureTask.run(FutureTask.java:266) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) + at java.lang.Thread.run(Thread.java:748) +"pool-1-thread-1": + at top.luozhou.analysisdemo.controller.DeadLockThread1.run(DeadLockThread.java:30) + - waiting to lock <0x00000000f8387d88> (a java.lang.Object) + - locked <0x00000000f8387d98> (a java.lang.Object) + at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) + at java.util.concurrent.FutureTask.run(FutureTask.java:266) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) + at java.lang.Thread.run(Thread.java:748) + + Found 5 deadlocks. +``` + +## 线程频繁切换 + +上下文切换会导致将大量CPU时间浪费在寄存器、内核栈以及虚拟内存的保存和恢复上,导致系统整体性能下降。当你发现系统的性能出现明显的下降时候,需要考虑是否发生了大量的线程上下文切换。 + +```java + @GetMapping(value = "/thread/swap") + public String theadSwap(int num) { + System.out.println("模拟线程切换"); + for (int i = 0; i < num; i++) { + new Thread(new ThreadSwap1(new AtomicInteger(0)),"thread-swap"+i).start(); + } + return "ok"; + } +public class ThreadSwap1 implements Runnable { + private AtomicInteger integer; + + public ThreadSwap1(AtomicInteger integer) { + this.integer = integer; + } + + @Override + public void run() { + while (true) { + integer.addAndGet(1); + Thread.yield(); //让出CPU资源 + } + } +} +``` + +这里我创建多个线程去执行基础的原子+1操作,然后让出 CPU 资源,理论上 CPU 就会去调度别的线程,我们请求接口创建100个线程看看效果如何,`curl localhost:8080/thread/swap?num=100`。接口请求成功后,我们执行`vmstat 1 10,表示每1秒打印一次,打印10次,线程切换采集结果如下: + +``` +procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- + r b swpd free buff cache si so bi bo in cs us sy id wa st +101 0 128000 878384 908 468684 0 0 0 0 4071 8110498 14 86 0 0 0 +100 0 128000 878384 908 468684 0 0 0 0 4065 8312463 15 85 0 0 0 +100 0 128000 878384 908 468684 0 0 0 0 4107 8207718 14 87 0 0 0 +100 0 128000 878384 908 468684 0 0 0 0 4083 8410174 14 86 0 0 0 +100 0 128000 878384 908 468684 0 0 0 0 4083 8264377 14 86 0 0 0 +100 0 128000 878384 908 468688 0 0 0 108 4182 8346826 14 86 0 0 0 +``` + + + +这里我们关注4个指标,`r`,`cs`,`us`,`sy`。 + +**r=100**,说明等待的进程数量是100,线程有阻塞。 + +**cs=800多万**,说明每秒上下文切换了800多万次,这个数字相当大了。 + +**us=14**,说明用户态占用了14%的CPU时间片去处理逻辑。 + +**sy=86**,说明内核态占用了86%的CPU,这里明显就是做上下文切换工作了。 + +我们通过`top`命令以及`top -Hp pid`查看进程和线程CPU情况,发现Java线程CPU占满了,但是线程CPU使用情况很平均,没有某一个线程把CPU吃满的情况。 + +``` +PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 87093 root 20 0 4194788 299056 13252 S 399.7 16.1 65:34.67 java +``` + +``` + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 87189 root 20 0 4194788 299056 13252 R 4.7 16.1 0:41.11 java + 87129 root 20 0 4194788 299056 13252 R 4.3 16.1 0:41.14 java + 87130 root 20 0 4194788 299056 13252 R 4.3 16.1 0:40.51 java + 87133 root 20 0 4194788 299056 13252 R 4.3 16.1 0:40.59 java + 87134 root 20 0 4194788 299056 13252 R 4.3 16.1 0:40.95 java +``` + +结合上面用户态CPU只使用了14%,内核态CPU占用了86%,可以基本判断是Java程序线程上下文切换导致性能问题。 + +我们使用`pidstat`命令来看看Java进程内部的线程切换数据,执行`pidstat -p 87093 -w 1 10 `,采集数据如下: + +``` +11:04:30 PM UID TGID TID cswch/s nvcswch/s Command +11:04:30 PM 0 - 87128 0.00 16.07 |__java +11:04:30 PM 0 - 87129 0.00 15.60 |__java +11:04:30 PM 0 - 87130 0.00 15.54 |__java +11:04:30 PM 0 - 87131 0.00 15.60 |__java +11:04:30 PM 0 - 87132 0.00 15.43 |__java +11:04:30 PM 0 - 87133 0.00 16.02 |__java +11:04:30 PM 0 - 87134 0.00 15.66 |__java +11:04:30 PM 0 - 87135 0.00 15.23 |__java +11:04:30 PM 0 - 87136 0.00 15.33 |__java +11:04:30 PM 0 - 87137 0.00 16.04 |__java +``` + +根据上面采集的信息,我们知道Java的线程每秒切换15次左右,正常情况下,应该是个位数或者小数。结合这些信息我们可以断定Java线程开启过多,导致频繁上下文切换,从而影响了整体性能。 + +**为什么系统的上下文切换是每秒800多万,而 Java 进程中的某一个线程切换才15次左右?** + +系统上下文切换分为三种情况: + +1、多任务:在多任务环境中,一个进程被切换出CPU,运行另外一个进程,这里会发生上下文切换。 + +2、中断处理:发生中断时,硬件会切换上下文。在vmstat命令中是`in` + +3、用户和内核模式切换:当操作系统中需要在用户模式和内核模式之间进行转换时,需要进行上下文切换,比如进行系统函数调用。 + +Linux 为每个 CPU 维护了一个就绪队列,将活跃进程按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最高和等待 CPU 时间最长的进程来运行。也就是vmstat命令中的`r`。 + +那么,进程在什么时候才会被调度到 CPU 上运行呢? + +- 进程执行完终止了,它之前使用的 CPU 会释放出来,这时再从就绪队列中拿一个新的进程来运行 +- 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片被轮流分配给各个进程。当某个进程时间片耗尽了就会被系统挂起,切换到其它等待 CPU 的进程运行。 +- 进程在系统资源不足时,要等待资源满足后才可以运行,这时进程也会被挂起,并由系统调度其它进程运行。 +- 当进程通过睡眠函数 sleep 主动挂起时,也会重新调度。 +- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。 +- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。 + +结合我们之前的内容分析,阻塞的就绪队列是100左右,而我们的CPU只有4核,这部分原因造成的上下文切换就可能会相当高,再加上中断次数是4000左右和系统的函数调用等,整个系统的上下文切换到800万也不足为奇了。Java内部的线程切换才15次,是因为线程使用`Thread.yield()`来让出CPU资源,但是CPU有可能继续调度该线程,这个时候线程之间并没有切换,这也是为什么内部的某个线程切换次数并不是非常大的原因。 + +## 总结 + +本文模拟了常见的性能问题场景,分析了如何定位CPU100%、内存泄漏、死锁、线程频繁切换问题。分析问题我们需要做好两件事,第一,掌握基本的原理,第二,借助好工具。本文也列举了分析问题的常用工具和命令,希望对你解决问题有所帮助。当然真正的线上环境可能十分复杂,并没有模拟的环境那么简单,但是原理是一样的,问题的表现也是类似的,我们重点抓住原理,活学活用,相信复杂的线上问题也可以顺利解决。 + +## 参考 + +1、https://linux.die.net/man/1/pidstat + +2、https://linux.die.net/man/8/vmstat + +3、https://help.eclipse.org/2020-03/index.jsp?topic=/org.eclipse.mat.ui.help/welcome.html + +4、https://www.linuxblogs.cn/articles/18120200.html + +5、https://www.tutorialspoint.com/what-is-context-switching-in-operating-system \ No newline at end of file diff --git a/docs/javaguide面试突击版.md b/docs/javaguide面试突击版.md new file mode 100644 index 00000000..f1576d75 --- /dev/null +++ b/docs/javaguide面试突击版.md @@ -0,0 +1,55 @@ +今天(2020-03-07)终于把PDF版本的《JavaGuide面试突击版》搞定!废话不多说,直接上成品: + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/javaguide-面试突击版.jpg) + +### 如何获取 + +公众号后台回复:“面试突击”即可。 + +![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) + +### 关于《JavaGuide面试突击版》 + +JavaGuide 目前已经 70k+ Star ,目前已经是所有 Java 类别项目中 Star 数量第二的开源项目了。Star虽然很多,但是价值远远比不上 Dubbo 这些开源项目,希望以后可以多出现一些这样的国产开源项目。国产开源项目!加油!奥利给! + +随着越来越多的人参与完善这个项目,这个专注 “Java知识总结+面试指南 ” 项目的知识体系和内容的不断完善。JavaGuide 目前包括下面这两部分内容: + +1. **Java 核心知识总结**; +2. **面试方向**:面试题、面试经验、备战面试系列文章以及面试真实体验系列文章 + +内容的庞大让JavaGuide 显的有一点臃肿。所以,我决定将专门为 Java 面试所写的文章以及来自读者投稿的文章整理成 **《JavaGuide面试突击版》** 系列,同时也为了更加方便大家阅读查阅。起这个名字也犹豫了很久,大家如果有更好的名字的话也可以向我建议。暂时的定位是将其作为 PDF 电子书,并不会像 JavaGuide 提供在线阅读版本。我之前也免费分享过PDF 版本的《Java面试突击》,期间一共更新了 3 个版本,但是由于后面难以同步和订正所以就没有再更新。**《JavaGuide面试突击版》** pdf 版由于我工作流程的转变可以有效避免这个问题。 + +另外,这段时间,向我提这个建议的读者也不是一个两个,我自己当然也有这个感觉。只是自己一直没有抽出时间去做罢了!毕竟这算是一个比较耗费时间的工程。加油!奥利给! + +这件事情具体耗费时间的地方是内容的排版优化(为了方便导出PDF生成目录),导出 PDF 我是通过 Typora 来做的。 + +### 如何学习本项目 + +提供了非常详细的目录,建议可以从头看是看一遍,如果基础不错的话也可以挑自己需要的章节查看。看的过程中自己要多思考,碰到不懂的地方,自己记得要勤搜索,需要记忆的地方也不要吝啬自己的脑子。 + +### 关于更新 + +**《JavaGuide面试突击版》** 预计一个月左右会有一次内容更新和完善,大家在我的公众号 **JavaGuide** 后台回复**“面试突击”** 即可获取最新版!另外,为了保证自己的辛勤劳动不被恶意盗版滥用,所以我添加了水印并且在一些内容注明版权,希望大家理解。 + +### 如何贡献 + +大家阅读过程中如果遇到错误的地方可以通过微信与我交流(ps:加过我微信的就不要重复添加了,这是另外一个账号,前一个已经满了)。 + +![我的微信](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/JavaGuide2.jpeg) + +希望大家给我提反馈的时候可以按照如下格式: + +> 我觉得2.3节Java基础的 2.3.1 这部分的描述有问题,应该这样描述:~巴拉巴拉~ 会更好!具体可以参考Oracle 官方文档,地址:~~~~。 + +为了提高准确性已经不必要的时间花费,希望大家尽量确保自己想法的准确性。 + +### 如何赞赏 + +如果觉得本文档对你有帮助的话,欢迎加入我的知识星球。创建星球的目的主要是为了提高知识沉淀,微信群的弊端相比大家都了解。星球没有免费的原因是了设立门槛,提高进入读者的质量。我会在星球回答大家的问题,更新更多的大厂面试干货! + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/45e7b191-600d-4940-aba5-827ccd3a8d2c.png) + +我的知识星球的价格应该是我了解的圈子里面最低的,也就1顿饭钱吧!毕竟关注我的大部分还是学生,我打心底里希望自己分享的东西能对大家有帮助。 + + + diff --git a/docs/network/干货:计算机网络知识总结.md b/docs/network/干货:计算机网络知识总结.md index 5b5c8472..a20e6f85 100644 --- a/docs/network/干货:计算机网络知识总结.md +++ b/docs/network/干货:计算机网络知识总结.md @@ -1,4 +1,3 @@ -> # 目录结构 ### 1. [计算机概述 ](#一计算机概述) ### 2. [物理层 ](#二物理层) ### 3. [数据链路层 ](#三数据链路层 ) @@ -57,15 +56,15 @@ 4,互联网按工作方式可划分为边缘部分和核心部分。主机在网络的边缘部分,其作用是进行信息处理。由大量网络和连接这些网络的路由器组成核心部分,其作用是提供连通性和交换。 5,计算机通信是计算机中进程(即运行着的程序)之间的通信。计算机网络采用的通信方式是客户-服务器方式(C/S方式)和对等连接方式(P2P方式)。 - + 6,客户和服务器都是指通信中所涉及的应用进程。客户是服务请求方,服务器是服务提供方。 - + 7,按照作用范围的不同,计算机网络分为广域网WAN,城域网MAN,局域网LAN,个人区域网PAN。 8,计算机网络最常用的性能指标是:速率,带宽,吞吐量,时延(发送时延,处理时延,排队时延),时延带宽积,往返时间和信道利用率。 - + 9,网络协议即协议,是为进行网络中的数据交换而建立的规则。计算机网络的各层以及其协议集合,称为网络的体系结构。 - + 10,五层体系结构由应用层,运输层,网络层(网际层),数据链路层,物理层组成。运输层最主要的协议是TCP和UDP协议,网络层最重要的协议是IP协议。 ## 二物理层 @@ -165,7 +164,7 @@ 一种用于数据链路层实现中继,连接两个或多个局域网的网络互连设备。 #### 交换机(switch ): 广义的来说,交换机指的是一种通信系统中完成信息交换的设备。这里工作在数据链路层的交换机指的是交换式集线器,其实质是一个多接口的网桥 - + ### (2),重要知识点总结 @@ -295,7 +294,7 @@ 10,TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点。这样的端点就叫做套接字(socket)或插口。套接字用(IP地址:端口号)来表示。每一条TCP连接唯一被通信两端的两个端点所确定。 11,停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 - + 12,为了提高传输效率,发送方可以不使用低效率的停止等待协议,而是采用流水线传输。流水线传输就是发送方可连续发送多个分组,不必每发完一个分组就停下来等待对方确认。这样可使信道上一直有数据不间断的在传送。这种传输方式可以明显提高信道利用率。 13,停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重转时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求ARQ。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续ARQ协议可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。 @@ -335,7 +334,7 @@ FTP 是File TransferProtocol(文件传输协议)的英文简称,而中文简称为“文传协议”。用于Internet上的控制文件的双向传输。同时,它也是一个应用程序(Application)。 基于不同的操作系统有不同的FTP应用程序,而所有这些应用程序都遵守同一种协议以传输文件。在FTP的使用当中,用户经常遇到两个概念:"下载"(Download)和"上传"(Upload)。 "下载"文件就是从远程主机拷贝文件至自己的计算机上;"上传"文件就是将文件从自己的计算机中拷贝至远程主机上。用Internet语言来说,用户可通过客户机程序向(从)远程主机上传(下载)文件。 - + #### 简单文件传输协议(TFTP): TFTP(Trivial File Transfer Protocol,简单文件传输协议)是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为69。 @@ -364,7 +363,7 @@ 代理服务器(Proxy Server)是一种网络实体,它又称为万维网高速缓存。 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时,若代理服务器发现这个请求与暂时存放的的请求相同,就返回暂存的响应,而不需要按URL的地址再次去互联网访问该资源。 代理服务器可在客户端或服务器工作,也可以在中间系统工作。 - + #### http请求头: http请求头,HTTP客户程序(例如浏览器),向服务器发送请求的时候必须指明请求类型(一般是GET或者POST)。如有必要,客户程序还可以选择发送其他的请求头。 - Accept:浏览器可接受的MIME类型。 diff --git a/docs/network/计算机网络.md b/docs/network/计算机网络.md index 8ea49e02..1cfe2695 100644 --- a/docs/network/计算机网络.md +++ b/docs/network/计算机网络.md @@ -1,39 +1,3 @@ - - -- [一 OSI与TCP/IP各层的结构与功能,都有哪些协议?](#一-osi与tcpip各层的结构与功能都有哪些协议) - - [1.1 应用层](#11-应用层) - - [1.2 运输层](#12-运输层) - - [1.3 网络层](#13-网络层) - - [1.4 数据链路层](#14-数据链路层) - - [1.5 物理层](#15-物理层) - - [1.6 总结一下](#16-总结一下) -- [二 TCP 三次握手和四次挥手(面试常客)](#二-tcp-三次握手和四次挥手面试常客) - - [2.1 TCP 三次握手漫画图解](#21-tcp-三次握手漫画图解) - - [2.2 为什么要三次握手](#22-为什么要三次握手) - - [2.3 为什么要传回 SYN](#23-为什么要传回-syn) - - [2.4 传了 SYN,为啥还要传 ACK](#24-传了-syn为啥还要传-ack) - - [2.5 为什么要四次挥手](#25-为什么要四次挥手) -- [三 TCP,UDP 协议的区别](#三-tcpudp-协议的区别) -- [四 TCP 协议如何保证可靠传输](#四-tcp-协议如何保证可靠传输) - - [4.1 ARQ协议](#41-arq协议) - - [停止等待ARQ协议](#停止等待arq协议) - - [连续ARQ协议](#连续arq协议) - - [4.2 滑动窗口和流量控制](#42-滑动窗口和流量控制) - - [4.3 拥塞控制](#43-拥塞控制) -- [五 在浏览器中输入url地址 ->> 显示主页的过程(面试常客)](#五--在浏览器中输入url地址---显示主页的过程面试常客) -- [六 状态码](#六-状态码) -- [七 各种协议与HTTP协议之间的关系](#七-各种协议与http协议之间的关系) -- [八 HTTP长连接,短连接](#八--http长连接短连接) -- [九 HTTP是不保存状态的协议,如何保存用户状态?](#九-http是不保存状态的协议如何保存用户状态) -- [十 Cookie的作用是什么?和Session有什么区别?](#十-cookie的作用是什么和session有什么区别) -- [十一 HTTP 1.0和HTTP 1.1的主要区别是什么?](#十一-http-10和http-11的主要区别是什么) -- [十二 URI和URL的区别是什么?](#十二-uri和url的区别是什么) -- [十三 HTTP 和 HTTPS 的区别?](#十三-http-和-https-的区别) -- [建议](#建议) -- [参考](#参考) - - - ## 一 OSI与TCP/IP各层的结构与功能,都有哪些协议? 学习计算机网络时我们一般采用折中的办法,也就是中和 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚。 @@ -90,7 +54,7 @@ ### 1.6 总结一下 -上面我们对计算机网络的五层体系结构有了初步的了解,下面附送一张七层体系结构图总结一下。图片来源:https://blog.csdn.net/yaopeng_2005/article/details/7064869 +上面我们对计算机网络的五层体系结构有了初步的了解,下面附送一张七层体系结构图总结一下(图片来源于网络)。 ![七层体系结构图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/七层体系结构图.png) @@ -122,15 +86,13 @@ 所以三次握手就能确认双发收发功能都正常,缺一不可。 -### 2.3 为什么要传回 SYN -接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。 +### 2.3 第2次握手传回了ACK,为什么还要传回SYN? -> SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。 +接收端传回发送端所发送的ACK是为了告诉客户端,我接收到的信息确实就是你所发送的信号了,这表明从客户端到服务端的通信是正常的。而回传SYN则是为了建立并确认从服务端到客户端的通信。” +> SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务器之间传递。 -### 2.4 传了 SYN,为啥还要传 ACK - -双方通信无误必须是两者互相发送信息都无误。传了 SYN,证明发送方到接收方的通道没有问题,但是接收方到发送方的通道还需要 ACK 信号来进行验证。 +### 2.5 为什么要四次挥手 ![TCP四次挥手](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/TCP四次挥手.png) @@ -141,8 +103,6 @@ - 服务器-关闭与客户端的连接,发送一个FIN给客户端 - 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1 -### 2.5 为什么要四次挥手 - 任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。 举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。 @@ -222,12 +182,14 @@ TCP的拥塞控制采用了四种算法,即 **慢开始** 、 **拥塞避免** 百度好像最喜欢问这个问题。 -> 打开一个网页,整个过程会使用哪些协议 +> 打开一个网页,整个过程会使用哪些协议? 图解(图片来源:《图解HTTP》): +> 上图有一个错误,请注意,是OSPF不是OPSF。 OSPF(Open Shortest Path Fitst,ospf)开放最短路径优先协议,是由Internet工程任务组开发的路由选择协议 + 总体来说分为以下几个过程: 1. DNS解析 diff --git a/docs/operating-system/Linux_IO.md b/docs/operating-system/Linux_IO.md new file mode 100644 index 00000000..99478c22 --- /dev/null +++ b/docs/operating-system/Linux_IO.md @@ -0,0 +1,160 @@ + + +- [Linux IO](#linux-io) + - [操作系统的内核](#操作系统的内核) + - [操作系统的用户态与内核态](#操作系统的用户态与内核态) + - [为什么要有用户态与内核态?](#为什么要有用户态与内核态) + - [用户态切换到内核态的几种方式](#用户态切换到内核态的几种方式) + - [阻塞和非阻塞](#阻塞和非阻塞) + - [同步与异步](#同步与异步) + - [Linux IO 模型](#linux-io模型) + - [阻塞 IO](#阻塞io) + - [非阻塞 IO(网络 IO 模型)](#非阻塞io网络io模型) + - [IO 多路复用(网络 IO 模型)](#io多路复用网络io模型) + - [信号驱动 IO(网络 IO 模型)](#信号驱动io网络io模型) + - [异步 IO](#异步io) + + + +# Linux IO + +> 图源: https://www.jianshu.com/p/85e931636f27 (如有侵权,请联系俺,俺会立刻删除) + +### 操作系统的内核 + +**操作系统的内核是操作系统的核心部分。它负责系统的内存,硬件设备,文件系统以及应用程序的管理。** + +#### 操作系统的用户态与内核态 + +unix 与 linux 的体系架构:用户态与内核态。 + +用户态与内核态与内核态是操作系统对执行权限进行分级后的不同的运行模式。 + +![用户态与内核态](../../media/pictures/java/linux_io/用户态与内核态.png) + +#### 为什么要有用户态与内核态? + +在 cpu 的所有指令中,有些指令是非常危险的,如果使用不当,将会造成系统崩溃等后果。为了避免这种情况发生,cpu 将指令划分为**特权级(内核态)指令**和**非特权级(用户态)指令。** + +**对于那些危险的指令只允许内核及其相关模块调用,对于那些不会造成危险的指令,就允许用户应用程序调用。** + +- 内核态(核心态,特权态): **内核态是操作系统内核运行的模式。** 内核态控制计算机的硬件资源,如硬件设备,文件系统等等,并为上层应用程序提供执行环境。 +- 用户态: **用户态是用户应用程序运行的状态。** 应用程序必须依托于内核态运行,因此用户态的态的操作权限比内核态是要低的,如磁盘,文件等,访问操作都是受限的。 +- 系统调用: 系统调用是操作系统为应用程序提供能够访问到内核态的资源的接口。 + +#### 用户态切换到内核态的几种方式 + +- 系统调用: 系统调用是用户态主动要求切换到内核态的一种方式,用户应用程序通过操作系统调用内核为上层应用程序开放的接口来执行程序。 +- 异常: 当 cpu 在执行用户态的应用程序时,发生了某些不可知的异常。于是当前用户态的应用进程切换到处理此异常的内核的程序中去。 +- 硬件设备的中断: 当硬件设备完成用户请求后,会向 cpu 发出相应的中断信号,这时 cpu 会暂停执行下一条即将要执行的指令,转而去执行与中断信号对应的应用程序,如果先前执行的指令是用户态下程序的指令,那么这个转换过程也是用户态到内核台的转换。 + +#### 阻塞和非阻塞 + +1. 阻塞: 一个线程调用一个方法计算 1 - 100 的和,如果该方法没有返回结果, + 那么调用方法的线程就一直等待直到该方法执行完毕。 +2. 非阻塞: 一个线程调用一个方法计算 1 - 100 的和,该方法立刻返回,如果方法返回没有结果, + 调用者线程也无需一直等待该方法的结果,可以执行其他任务,但是在方法返回结果之前, + **线程仍然需要轮询的检查方法是否已经有结果。** + +**结论: 阻塞与非阻塞针对调用者的立场而言。** + +#### 同步与异步 + +1. **同步**: 一个线程调用一个方法计算 1 - 100 的和,如果方法没有计算完,就不返回。 +2. **异步**: 一个线程调用一个方法计算 1 - 100 的和,该方法立刻返回,但是由于方法没有返回结果, + 所以就需要被调用的这个方法来通知调用线程 1 - 100 的结果, + 或者线程在调用方法的时候指定一个回调函数来告诉被调用的方法执行完后就执行回调函数。 + +**结论:同步和异步是针对被调用者的立场而言的。** + +### Linux IO 模型 + +Linux 下共有 5 种 IO 模型: + +1. 阻塞 IO +2. 非阻塞 IO +3. IO 多路复用 +4. 信号驱动 IO +5. 异步 IO + +#### 阻塞 IO + +阻塞 IO 是很常见的一种 IO 模型。在这种模型中,**用户态的应用程序会执行一个操作系统的调用,检查内核的数据是否准备好。如果内核的数据已经准备好,就把数据复制到用户应用进程。如果内核没有准备好数据,那么用户应用进程(线程)就阻塞,直到内核准备好数据并把数据从内核复制到用户应用进程,** 最后应用程序再处理数据。 + +![BIO原理](../../media/pictures/java/linux_io/BIO原理.png) + +**阻塞 IO 是同步阻塞的。** + +1. 阻塞 IO 的同步体现在: **内核只有准备好数据并把数据复制到用户应用进程才会返回。** + +2. 阻塞 IO 的阻塞体现在:**用户应用进程等待内核准备数据和把数据从用户态拷贝到内核态的这整个过程, + 用户应用进程都必须一直等待。** 当然,如果是本地磁盘 IO,内核准备数据的时间可能会很短。但网络 IO 就不一样了,因为服务端不知道客户端何时发送数据,内核就仍需要等待 socket 数据,时间就可能会很长。 + +**阻塞 IO 的优点是对于数据是能够保证无延时的,因为应用程序进程会一直阻塞直到 IO 完成。**但应用程序的阻塞就意味着应用程序进程无法执行其他任务,这会大大降低程序性能。一个不太可行的办法是为每个客户端 socket 都分配一个线程,这样就会提升 server 处理请求的能力。不过操作系统的线程资源是有限的,如果请求过多,可能造成线程资源耗尽,系统卡死等后果。 + +#### 非阻塞 IO(网络 IO 模型) + +在非阻塞 IO 模型中,用户态的应用程序也会执行一个操作系统的调用,检查内核的数据是否准备完成。**如果内核没有准备好数据, +内核会立刻返回结果,用户应用进程不用一直阻塞等待内核准备数据,而是可以执行其他任务,但仍需要不断的向内核发起系统调用,检测数据是否准备好,这个过程就叫轮询。** 轮询直到内核准备好数据,然后内核把数据拷贝到用户应用进程,再进行数据处理。 + +![NIO原理](../../media/pictures/java/linux_io/NIO原理.png) + +非阻塞 IO 的非阻塞体现在: **用户应用进程不用阻塞在对内核的系统调用上** + +非阻塞 IO 的优点在于用户应用进程在轮询阶段可以执行其它任务。但这也是它的缺点,轮询就代表着用户应用进程不是时刻都会发起系统调用。 +**可能数据准备好了,而用户应用进程可能等待其它任务执行完毕才会发起系统调用,这就意味着数据可能会被延时获取。** + +#### IO 多路复用(网络 IO 模型) + +在 IO 多路复用模型中,**用户应用进程会调用操作系统的 select/poll/epoll 函数,它会使内核同步的轮询指定的 socket, +(在 NIO 中,socket 就是注册到 Selector 上的 SocketChannel,可以允许多个)直至监听的 socket 有数据可读或可写,select/poll/epoll 函数才会返回,用户应用进程也会阻塞的等待 select/poll/epoll 函数返回。** + +当 select/poll/epoll 函数返回后,即某个 socket 有事件发生了,用户应用进程就会发起系统调用,处理事件,将 socket 数据复制到用户进程内,然后进行数据处理。 + +![IO多路复用原理](../../media/pictures/java/linux_io/IO多路复用原理.png) + +**IO 多路复用模型是同步阻塞的** + +1. IO 多路复用模型的同步体现在: **select 函数只有监听到某个 socket 有事件才会返回。** + +2. IO 多路复用模型的阻塞体现在: **用户应用进程会阻塞在对 select 函数上的调用上。** + +**IO 多路复用的优点在于内核可以处理多个 socket,相当于一个用户进程(线程)就可以处理多个 socket 连接。** + +这样不仅降低了系统的开销,并且对于需要高并发的应用是非常有利的。而非阻塞 IO 和阻塞 IO 的一个用户应用进程只能处理一个 socket,要想处理多 socket,只能新开进程或线程,但这样很消耗系统资源。 + +**PS: +在 IO 多路复用模型中, socket 一般应该为非阻塞的,这就是 Java 中 NIO 被称为非阻塞 IO 的原因。但实际上 NIO 属于 IO 多路复用,它是同步阻塞的 IO。具体原因见 [知乎讨论](https://www.zhihu.com/question/37271342)** + +**PS: +select/poll/epoll 函数是 IO 多路复用模型的基础,所以如果想深入了解 IO 多路复用模型,就需要了解这 3 个函数以及它们的优缺点。** + +#### 信号驱动 IO(网络 IO 模型) + +在信号驱动 IO 模型中,**用户应用进程发起 sigaction 系统调用,内核收到并立即返回。用户应用进程可以继续执行其他任务,不会阻塞。当内核准备好数据后向用户应用进程发送 SIGIO 信号,应用进程收到信号后,发起系统调用,将数据从内核拷贝到用户进程,** 然后进行数据处理。 + +![信号驱动IO原理](../../media/pictures/java/linux_io/信号驱动IO原理.png) + +个人感觉在内核收到系统调用就立刻返回这一点很像异步 IO 的方式了,不过与异步 IO 仍有很大差别。 + +#### 异步 IO + +在异步 IO 模型中,**用户进程发起 aio_read 系统调用,无论内核的数据是否准备好,都会立即返回。用户应用进程不会阻塞,可以继续执行其他任务。当内核准备好数据,会直接把数据复制到用户应用进程。最后内核会通知用户应用进程 IO 完成。** + +![异步IO原理](../../media/pictures/java/linux_io/异步IO原理.png) + +**异步 IO 的异步体现在:内核不用等待数据准备好就立刻返回,所以内核肯定需要在 IO 完成后通知用户应用进程。** + +--- + +```text +弄清楚了阻塞与非阻塞,同步与异步和上面5种IO模型,相信再看 +Java中的IO模型,也只是换汤不换药。 +``` + +- BIO : 阻塞 IO +- NIO : IO 多路复用 +- AIO : 异步 IO + +本来打算写 Java 中的 IO 模型的,发现上面几乎讲完了(剩 API 使用吧),没啥要写的了, +所以暂时就这样吧。如果各位同学有好的建议,欢迎 pr 或 issue。 diff --git a/docs/operating-system/Linux_performance/image-20200604180850391.png b/docs/operating-system/Linux_performance/image-20200604180850391.png new file mode 100755 index 00000000..20b6a534 Binary files /dev/null and b/docs/operating-system/Linux_performance/image-20200604180850391.png differ diff --git a/docs/operating-system/Linux_performance/image-20200604180851790.png b/docs/operating-system/Linux_performance/image-20200604180851790.png new file mode 100755 index 00000000..20b6a534 Binary files /dev/null and b/docs/operating-system/Linux_performance/image-20200604180851790.png differ diff --git a/docs/operating-system/Linux_performance/image-20200604181133355.png b/docs/operating-system/Linux_performance/image-20200604181133355.png new file mode 100755 index 00000000..47d74f36 Binary files /dev/null and b/docs/operating-system/Linux_performance/image-20200604181133355.png differ diff --git a/docs/operating-system/Linux_performance/image-20200604203027136.png b/docs/operating-system/Linux_performance/image-20200604203027136.png new file mode 100755 index 00000000..753a72f6 Binary files /dev/null and b/docs/operating-system/Linux_performance/image-20200604203027136.png differ diff --git a/docs/operating-system/Linux_performance/image-20200605104607007.png b/docs/operating-system/Linux_performance/image-20200605104607007.png new file mode 100755 index 00000000..95b1791f Binary files /dev/null and b/docs/operating-system/Linux_performance/image-20200605104607007.png differ diff --git a/docs/operating-system/Linux_performance/iostat.png b/docs/operating-system/Linux_performance/iostat.png new file mode 100755 index 00000000..4e8c446f Binary files /dev/null and b/docs/operating-system/Linux_performance/iostat.png differ diff --git a/docs/operating-system/Linux_performance/linux_xn.png b/docs/operating-system/Linux_performance/linux_xn.png new file mode 100755 index 00000000..0eb28c01 Binary files /dev/null and b/docs/operating-system/Linux_performance/linux_xn.png differ diff --git a/docs/operating-system/Linux_performance/tcp_close.jpg b/docs/operating-system/Linux_performance/tcp_close.jpg new file mode 100755 index 00000000..f58c76cd Binary files /dev/null and b/docs/operating-system/Linux_performance/tcp_close.jpg differ diff --git a/docs/operating-system/Linux_performance/tcp_conn.jpg b/docs/operating-system/Linux_performance/tcp_conn.jpg new file mode 100755 index 00000000..fae229db Binary files /dev/null and b/docs/operating-system/Linux_performance/tcp_conn.jpg differ diff --git a/docs/operating-system/Linux_performance/tcpclose.png b/docs/operating-system/Linux_performance/tcpclose.png new file mode 100755 index 00000000..80d17430 Binary files /dev/null and b/docs/operating-system/Linux_performance/tcpclose.png differ diff --git a/docs/operating-system/Linux_performance/tcpconn.png b/docs/operating-system/Linux_performance/tcpconn.png new file mode 100755 index 00000000..1985847c Binary files /dev/null and b/docs/operating-system/Linux_performance/tcpconn.png differ diff --git a/docs/operating-system/Linux性能分析工具合集.md b/docs/operating-system/Linux性能分析工具合集.md new file mode 100755 index 00000000..41e17b97 --- /dev/null +++ b/docs/operating-system/Linux性能分析工具合集.md @@ -0,0 +1,634 @@ +# Linux性能分析工具合集 + +> 本文由读者投稿,原文地址:[https://ysshao.cn/Linux/Linux_performance/](https://ysshao.cn/Linux/Linux_performance/) 。 + +## 1. 背景 + +有时候会遇到一些疑难杂症,并且监控插件并不能一眼立马发现问题的根源。这时候就需要登录服务器进一步深入分析问题的根源。那么分析问题需要有一定的技术经验积累,并且有些问题涉及到的领域非常广,才能定位到问题。所以,分析问题和踩坑是非常锻炼一个人的成长和提升自我能力。如果我们有一套好的分析工具,那将是事半功倍,能够帮助大家快速定位问题,节省大家很多时间做更深入的事情。 + +## 2. 说明 + +本篇文章主要介绍各种问题定位的工具以及会结合案例分析问题。 + +## 3. 分析问题的方法论 + +套用5W2H方法,可以提出性能分析的几个问题 + +- What-现象是什么样的 +- When-什么时候发生 +- Why-为什么会发生 +- Where-哪个地方发生的问题 +- How much-耗费了多少资源 +- How to do-怎么解决问题 + +## 4.性能分析工具合集 + +img + +### CPU + +针对应用程序,我们通常关注的是内核CPU调度器功能和性能。 + +线程的状态分析主要是分析线程的时间用在什么地方,而线程状态的分类一般分为: + +a. on-CPU:执行中,执行中的时间通常又分为用户态时间user和系统态时间sys。 + b. off-CPU:等待下一轮上CPU,或者等待I/O、锁、换页等等,其状态可以细分为可执行、匿名换页、睡 眠、锁、空闲等状态。 + +#### **分析工具** + +| 工具 | 描述 | +| -------- | ------------------------------ | +| uptime/w | 查看服务器运行时间、平均负载 | +| top | 监控每个进程的CPU用量分解 | +| vmstat | 系统的CPU平均负载情况 | +| mpstat | 查看多核CPU信息 | +| sar -u | 查看CPU过去或未来时点CPU利用率 | +| pidstat | 查看每个进程的用量分解 | + +#### uptime + +uptime 命令可以用来查看服务器已经运行了多久,当前登录的用户有多少,以及服务器在过去的1分钟、5分钟、15分钟的系统平均负载值 + +image-20200604180851790 + +第一项是当前时间,up 表示系统正在运行,6:47是系统启动的总时间,最后是系统的负载load信息 + +w 同上,增加了具体登陆了那些用户及登陆时间。 + +#### top + +常用来监控[Linux](http://lib.csdn.net/base/linux)的系统状况,比如cpu、内存的使用,显示系统上正在运行的进程。 + +image-20200604181133355 + +1. **系统运行时间和平均负载:** + + top命令的顶部显示与uptime命令相似的输出。 + + 这些字段显示: + + - 当前时间 + - 系统已运行的时间 + - 当前登录用户的数量 + - 相应最近5、10和15分钟内的平均负载。 + +2. **任务** + + 第二行显示的是任务或者进程的总结。进程可以处于不同的状态。这里显示了全部进程的数量。除此之外,还有正在运行、睡眠、停止、僵尸进程的数量(僵尸是一种进程的状态)。这些进程概括信息可以用’t’切换显示。 + +3. **CPU状态** + + 下一行显示的是CPU状态。 这里显示了不同模式下的所占CPU时间的百分比。这些不同的CPU时间表示: + + - us, user: 运行(未调整优先级的) 用户进程的CPU时间 + - sy,system: 运行内核进程的CPU时间 + - ni,niced:运行已调整优先级的用户进程的CPU时间 + - wa,IO wait: 用于等待IO完成的CPU时间 + - hi:处理硬件中断的CPU时间 + - si: 处理软件中断的CPU时间 + - st:这个虚拟机被hypervisor偷去的CPU时间(译注:如果当前处于一个hypervisor下的vm,实际上hypervisor也是要消耗一部分CPU处理时间的)。 + +4. **内存使用** + + 接下来两行显示内存使用率,有点像’free’命令。第一行是物理内存使用,第二行是虚拟内存使用(交换空间)。 + + 物理内存显示如下:全部可用内存、已使用内存、空闲内存、缓冲内存。相似地:交换部分显示的是:全部、已使用、空闲和缓冲交换空间。 + + > 这里要说明的是不能用windows的内存概念理解这些数据,如果按windows的方式此台服务器“危矣”:8G的内存总量只剩下530M的可用内存。Linux的内存管理有其特殊性,复杂点需要一本书来说明,这里只是简单说点和我们传统概念(windows)的不同。 + > + > 第四行中使用中的内存总量(used)指的是现在系统内核控制的内存数,空闲内存总量(free)是内核还未纳入其管控范围的数量。纳入内核管理的内存不见得都在使用中,还包括过去使用过的现在可以被重复利用的内存,内核并不把这些可被重新使用的内存交还到free中去,因此在[linux](http://lib.csdn.net/base/linux)上free内存会越来越少,但不用为此担心。 + > + > 如果出于习惯去计算可用内存数,这里有个近似的计算公式: + > + > ​ **第四行的free + 第四行的buffers + 第五行的cached。** + > + > 对于内存监控,在top里我们要时刻监控第五行swap交换分区的used,如果这个数值在不断的变化,说明内核在不断进行内存和swap的数据交换,这是真正的内存不够用了。 + +5. **字段/列** + + | 进程的属性 | 属性含义 | + | ---------- | ------------------------------------------------------------ | + | PID | 进程ID,进程的唯一标识符 | + | USER | 进程所有者的实际用户名。 | + | PR | 进程的调度优先级。这个字段的一些值是’rt’。这意味这这些进程运行在实时态。 | + | NI | 进程的nice值(优先级)。越小的值意味着越高的优先级。 | + | VIRT | 进程使用的虚拟内存。 | + | RES | 驻留内存大小。驻留内存是任务使用的非交换物理内存大小。 | + | SHR | SHR是进程使用的共享内存。 | + | S | 这个是进程的状态。它有以下不同的值:
D–不可中断的睡眠态、R–运行态、S–睡眠态、T–被跟踪或已停止、Z – 僵尸态 | + | %CPU | 自从上一次更新时到现在任务所使用的CPU时间百分比。 | + | %MEM | 进程使用的可用物理内存百分比。 | + | TIME+ | 任务启动后到现在所使用的全部CPU时间,精确到百分之一秒。 | + | COMMAND | 运行进程所使用的命令。 | + + 还有许多在默认情况下不会显示的输出,它们可以显示进程的页错误、有效组和组ID和其他更多的信息。 + + 常用交互命令: + + ‘B’:一些重要信息会以加粗字体显示(高亮)。这个命令可以切换粗体显示。 + + ‘b’: + + ‘D’或’S‘: 你将被提示输入一个值(以秒为单位),它会以设置的值作为刷新间隔。如果你这里输入了1,top将会每秒刷新。 top默认为3秒刷新 + + ‘l’、‘t’、‘m’: 切换负载、任务、内存信息的显示,这会相应地切换顶部的平均负载、任务/CPU状态和内存信息的概况显示。 + + ‘z’ : 切换彩色显示 + + ‘x’ 或者 ‘y’ + + 切换高亮信息:’x’将排序字段高亮显示(纵列);’y’将运行进程高亮显示(横行)。依赖于你的显示设置,你可能需要让输出彩色来看到这些高亮。 + + ‘u’: 特定用户的进程 + + ‘n’ 或 ‘#’: 任务的数量 + + ‘k’: 结束任务 + + **命令行选项** + + > top //每隔3秒显式所有进程的资源占用情况 + > + > top -u oracle -c //按照用户显示进程、并显示完整命令 + > + > top -d 2 //每隔2秒显式所有进程的资源占用情况 + > + > top -c //每隔3秒显式进程的资源占用情况,并显示进程的命令行参数(默认只有进程名) + > + > top -p 12345 -p 6789//每隔3秒显示pid是12345和pid是6789的两个进程的资源占用情况 + > + > top -d 2 -c -p 123456 //每隔2秒显示pid是12345的进程的资源使用情况,并显式该进程启动的命令行参数 + > + > top -n 设置显示多少次后就退出 + + **补充** + + top命令是Linux上进行系统监控的首选命令,但有时候却达不到我们的要求,比如当前这台服务器,top监控有很大的局限性。这台服务器运行着websphere集群,有两个节点服务,就是【top视图 01】中的老大、老二两个java进程,top命令的监控最小单位是进程,所以看不到我关心的java线程数和客户连接数,而这两个指标是java的web服务非常重要的指标,通常我用ps和netstate两个命令来补充top的不足。 + +#### vmstat + +​ vmstat命令是最常见的Linux/Unix监控工具,可以展现给定时间间隔的服务器的状态值,包括服务器的CPU使用率,内存使用,虚拟内存交换情况,IO读写情况。 + +​ 一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数,单位是秒,第二个参数是采样的次数,如: + +image-20200604203027136 + +每个参数的含义: + +**Procs(进程)** + +| r: | 运行队列中进程数量,这个值也可以判断是否需要增加CPU。(长期大于1) | +| ---- | ------------------------------------------------------------ | +| b | 等待IO的进程数量。 | + +**Memory(内存)** + +| swpd | 使用虚拟内存大小,如果swpd的值不为0,但是SI,SO的值长期为0,这种情况不会影响系统性能。 | +| ----- | ------------------------------------------------------------ | +| free | 空闲物理内存大小。 | +| buff | 用作缓冲的内存大小。 | +| cache | 用作缓存的内存大小,如果cache的值大的时候,说明cache处的文件数多,如果频繁访问到的文件都能被cache处,那么磁盘的读IO bi会非常小。 | + +**Swap** + +| si | 每秒从交换区写到内存的大小,由磁盘调入内存。 | +| ---- | -------------------------------------------- | +| so | 每秒写入交换区的内存大小,由内存调入磁盘。 | + +注意:内存够用的时候,这2个值都是0,如果这2个值长期大于0时,系统性能会受到影响,磁盘IO和CPU资源都会被消耗。有些朋友看到空闲内存(free)很少的或接近于0时,就认为内存不够用了,不能光看这一点,还要结合si和so,如果free很少,但是si和so也很少(大多时候是0),那么不用担心,系统性能这时不会受到影响的。因为linux总是先把内存用光. + +**IO** + +| bi | 每秒读取的块数 | +| ---- | -------------- | +| bo | 每秒写入的块数 | + +注意:随机磁盘读写的时候,这2个值越大(如超出1024k),能看到CPU在IO等待的值也会越大。 + +**system(系统)** + +| in | 每秒中断数,包括时钟中断。 | +| ---- | -------------------------- | +| cs | 每秒上下文切换数。 | + +注意:上面2个值越大,会看到由内核消耗的CPU时间会越大。 + +**CPU(以百分比表示)** + +| us | 用户进程执行时间百分比(user time) us的值比较高时,说明用户进程消耗的CPU时间多,但是如果长期超50%的使用,那么我们就该考虑优化程序算法或者进行加速。 | +| ---- | ------------------------------------------------------------ | +| sy: | 内核系统进程执行时间百分比(system time) sy的值高时,说明系统内核消耗的CPU资源多,这并不是良性表现,我们应该检查原因。 | +| wa | IO等待时间百分比 wa的值高时,说明IO等待比较严重,这可能由于磁盘大量作随机访问造成,也有可能磁盘出现瓶颈(块操作)。 | +| id | 空闲时间百分比 | + +#### mpstat + +​ mpstat是一个实时监控工具,主要报告与CPU相关统计信息,在多核心cpu系统中,不仅可以查看cpu平均信息,还可以查看指定cpu信息。 + +> mpstat -P ALL //查看全部CPU的负载情况。 +> +> mpstat 2 5 //可指定间隔时间和次数。 + +| CPU: 处理器编号。关键字all表示统计信息计算为所有处理器之间的平均值。 | +| ------------------------------------------------------------ | +| %usr: 显示在用户级(应用程序)执行时发生的CPU利用率百分比。 | +| %nice: 显示以优先级较高的用户级别执行时发生的CPU利用率百分比。 | +| %sys: 显示在系统级(内核)执行时发生的CPU利用率百分比。请注意,这不包括维护硬件和软件的时间中断。 | +| %iowait: 显示系统具有未完成磁盘I / O请求的CPU或CPU空闲的时间百分比。 | +| %irq: 显示CPU或CPU用于服务硬件中断的时间百分比。 | +| %soft: 显示CPU或CPU用于服务软件中断的时间百分比。 | +| %steal: 显示虚拟CPU或CPU在管理程序为另一个虚拟处理器提供服务时非自愿等待的时间百分比。 | +| %guest: 显示CPU或CPU运行虚拟处理器所花费的时间百分比。 | + +#### sar + +系统活动情况报告,可以从多方面对系统的活动进行报告,包括:文件的读写情况、系统调用的使用情况、磁盘I/O、CPU效率、内存使用状况、进程活动及IPC有关的活动等 + +CPU相关: + +sar -p (查看全天) + +sar -u 1 10 (1:每隔一秒,10:写入10次) + +CPU输出项-详细说明 + +CPU:all 表示统计信息为所有 CPU 的平均值。 + +%user:显示在用户级别(application)运行使用 CPU 总时间的百分比。 + +%nice:显示在用户级别,用于nice操作,所占用 CPU 总时间的百分比。 + +%system:在核心级别(kernel)运行所使用 CPU 总时间的百分比。 + +%iowait:显示用于等待I/O操作占用 CPU 总时间的百分比。 + +%steal:管理程序(hypervisor)为另一个虚拟进程提供服务而等待虚拟 CPU 的百分比。 + +%idle:显示 CPU 空闲时间占用 CPU 总时间的百分比。 + +#### pidstat + +用于监控全部或指定进程的cpu、内存、线程、设备IO等系统资源的占用情况。 + +pidstat 和 pidstat -u -p ALL 是等效的。 + pidstat 默认显示了所有进程的cpu使用率。 + +详细说明 + +PID:进程ID + +%usr:进程在用户空间占用cpu的百分比 + +%system:进程在内核空间占用cpu的百分比 + +%guest:进程在虚拟机占用cpu的百分比 + +%CPU:进程占用cpu的百分比 + +CPU:处理进程的cpu编号 + +Command:当前进程对应的命令 + +### 内存 + +内存是为提高效率而生,实际分析问题的时候,内存出现问题可能不只是影响性能,而是影响服务或者引起其他问题。同样对于内存有些概念需要清楚: + +- 主存 +- 虚拟内存 +- 常驻内存 +- 地址空间 +- OOM +- 页缓存 +- 缺页 +- 换页 +- 交换空间 +- 交换 +- 用户分配器libc、glibc、libmalloc和mtmalloc +- LINUX内核级SLUB分配器 + +#### 分析工具 + +| 工具 | 描述 | +| ------- | ------------------------------ | +| free | 查看内存的使用情况 | +| top | 监控每个进程的内存使用情况 | +| vmstat | 虚拟内存统计信息 | +| sar -r | 查看内存 | +| sar | 查看CPU过去或未来时点CPU利用率 | +| pidstat | 查看每个进程的内存使用情况 | + +#### free + +free 命令显示系统内存的使用情况,包括物理内存、交换内存(swap)和内核缓冲区内存。 + +Mem 行(第二行)是内存的使用情况。 + Swap 行(第三行)是交换空间的使用情况。 + total 列显示系统总的可用物理内存和交换空间大小。 + used 列显示已经被使用的物理内存和交换空间。 + free 列显示还有多少物理内存和交换空间可用使用。 + shared 列显示被共享使用的物理内存大小。 + buff/cache 列显示被 buffer 和 cache 使用的物理内存大小。 + available 列显示还可以被应用程序使用的物理内存大小。 + +常用命令: + +> free +> +> free -g 以GB显示 +> +> free -m 以MB显示 +> +> free -h 自动转换展示 +> +> free -h -s 3 有时我们需要持续的观察内存的状况,此时可以使用 -s 选项并指定间隔的秒数 + +所以从应用程序的角度来说,**available = free + buffer + cache** + +可用内存=系统free memory+buffers+cached。 + +#### top + +请参考上面top的详解 + +#### vmstat + +请参考上面vmstat的详解 + +#### sar + +sar -r #查看内存使用情况 + +详解: + +kbmemfree 空闲的物理内存大小 + +kbmemused 使用中的物理内存大小 + +%memused 物理内存使用率 + +kbbuffers 内核中作为缓冲区使用的物理内存大小,kbbuffers和kbcached:这两个值就是free命令中的buffer 和cache. + +kbcached 缓存的文件大小 + +kbcommit 保证当前系统正常运行所需要的最小内存,即为了确保内存不溢出而需要的最少内存(物理内存 +Swap分区) + +commt 这个值是kbcommit与内存总量(物理内存+swap分区)的一个百分比的值 + +#### pidstat + +pidstat -r 查看内存使用情况 pidstat将显示各活动进程的内存使用统计 + +PID:进程标识符 + +Minflt/s:任务每秒发生的次要错误,不需要从磁盘中加载页 + +Majflt/s:任务每秒发生的主要错误,需要从磁盘中加载页 + +VSZ:虚拟地址大小,虚拟内存的使用KB + +RSS:常驻集合大小,非交换区五里内存使用KB + +Command:task命令名 + +### 磁盘IO + +磁盘通常是计算机最慢的子系统,也是最容易出现性能瓶颈的地方,因为磁盘离 CPU 距离最远而且 CPU 访问磁盘要涉及到机械操作,比如转轴、寻轨等。访问硬盘和访问内存之间的速度差别是以数量级来计算的,就像1天和1分钟的差别一样。要监测 IO 性能,有必要了解一下基本原理和 Linux 是如何处理硬盘和内存之间的 IO 的。 + +在理解磁盘IO之前,同样我们需要理解一些概念,例如: + +- 文件系统 +- VFS +- 文件系统缓存 +- 页缓存page cache +- 缓冲区高速缓存buffer cache +- 目录缓存 +- inode +- inode缓存 +- noop调用策略 + +#### 分析工具 + +| 工具 | 描述 | +| ------- | ---------------------------- | +| iostat | 磁盘详细统计信息 | +| iotop | 按进程查看磁盘IO统计信息 | +| pidstat | 查看每个进程的磁盘IO使用情况 | + +#### iostat + +iostat工具将对系统的磁盘操作活动进行监视。它的特点是汇报磁盘活动统计情况,同时也会汇报出CPU使用情况 + +image-20200604203027136 + +CPU属性 + +%user:CPU处在用户模式下的时间百分比。 + +%nice:CPU处在带NICE值的用户模式下的时间百分比。 + +%system:CPU处在系统模式下的时间百分比。 + +%iowait:CPU等待输入输出完成时间的百分比。 + +%steal:管理程序维护另一个虚拟处理器时,虚拟CPU的无意识等待时间百分比。 + +%idle:CPU空闲时间百分比。 + +备注: + +如果%iowait的值过高,表示硬盘存在I/O瓶颈 + +如果%idle值高,表示CPU较空闲 + +如果%idle值高但系统响应慢时,可能是CPU等待分配内存,应加大内存容量。 + +如果%idle值持续低于10,表明CPU处理能力相对较低,系统中最需要解决的资源是CPU。 + +Device属性 + +tps:该设备每秒的传输次数 + +kB_read/s:每秒从设备(drive expressed)读取的数据量; + +kB_wrtn/s:每秒向设备(drive expressed)写入的数据量; + +kB_read: 读取的总数据量; + +kB_wrtn:写入的总数量数据量 + +常用命令: + +iostat 2 3 每隔2秒刷新显示,且显示3次 + +iostat -m 以M为单位显示所有信息 + +查看设备使用率(%util)、响应时间(await) + +iostat -d -x -k 1 1 + +#### iotop + +在一般运维工作中经常会遇到这么一个场景,服务器的IO负载很高(iostat中的util),但是无法快速的定位到IO负载的来源进程和来源文件导致无法进行相应的策略来解决问题。 + +如果你想检查那个进程实际在做 I/O,那么运行 `iotop` 命令加上 `-o` 或者 `--only` 参数。 + +iotop --only + +#### pidstat + +显示各个进程的IO使用情况 + +pidstat -d + +报告IO统计显示以下信息: + +- PID:进程id +- kB_rd/s:每秒从磁盘读取的KB +- kB_wr/s:每秒写入磁盘KB +- kB_ccwr/s:任务取消的写入磁盘的KB。当任务截断脏的pagecache的时候会发生。 +- COMMAND:task的命令名 + +### 网络 + +#### 分析工具 + +| ping | 测试网络的连通性 | +| -------- | ---------------------------- | +| netstat | 检验本机各端口的网络连接情况 | +| hostname | 查看主机和域名 | + +#### ping + +常用命令参数: + +-d 使用Socket的SO_DEBUG功能。 + +-f 极限检测。大量且快速地送网络封包给一台机器,看它的回应。 + +-n 只输出数值。 + +-q 不显示任何传送封包的信息,只显示最后的结果。 + +-r 忽略普通的Routing Table,直接将数据包送到远端主机上。通常是查看本机的网络接口是否有问题。 + +-R 记录路由过程。 + +-v 详细显示指令的执行过程。 + +

-c 数目:在发送指定数目的包后停止。 + +-i 秒数:设定间隔几秒送一个网络封包给一台机器,预设值是一秒送一次。 + +-I 网络界面:使用指定的网络界面送出数据包。 + +-l 前置载入:设置在送出要求信息之前,先行发出的数据包。 + +-p 范本样式:设置填满数据包的范本样式。 + +-s 字节数:指定发送的数据字节数,预设值是56,加上8字节的ICMP头,一共是64ICMP数据字节。 + +-t 存活数值:设置存活数值TTL的大小。 + +> ping -b 192.168.120.1 --ping网关 +> +> ping -c 10 192.168.120.206 --ping指定次数 +> +> ping -c 10 -i 0.5 192.168.120.206 --时间间隔和次数限制的ping + +#### netstat + +netstat命令是一个监控TCP/IP网络的非常有用的工具,它可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息。 + +netstat [选项] + +``` +-a或--all:显示所有连线中的Socket; +-a (all) 显示所有选项,默认不显示LISTEN相关。 +-t (tcp) 仅显示tcp相关选项。 +-u (udp) 仅显示udp相关选项。 +-n 拒绝显示别名,能显示数字的全部转化成数字。 +-l 仅列出有在 Listen (监听) 的服务状态。 +-p 显示建立相关链接的程序名 +-r 显示路由信息,路由表 +-e 显示扩展信息,例如uid等 +-s 按各个协议进行统计 +-c 每隔一个固定时间,执行该netstat命令。 +``` + +常用命令: + +列出所有端口情况 + +``` +netstat -a # 列出所有端口 +netstat -at # 列出所有TCP端口 +netstat -au # 列出所有UDP端口 +``` + +列出所有处于监听状态的 Sockets + +``` +netstat -l # 只显示监听端口 +netstat -lt # 显示监听TCP端口 +netstat -lu # 显示监听UDP端口 +netstat -lx # 显示监听UNIX端口 +``` + +显示每个协议的统计信息 + +``` +netstat -s # 显示所有端口的统计信息 +netstat -st # 显示所有TCP的统计信息 +netstat -su # 显示所有UDP的统计信息 +``` + +显示 PID 和进程名称 + +``` +netstat -p +``` + +显示网络统计信息 + +``` +netstat -s +``` + +统计机器中网络连接各个状态个数 + +``` +netstat` `-an | ``awk` `'/^tcp/ {++S[$NF]} END {for (a in S) print a,S[a]} ' +``` + +**补充netstat网络状态详解:** + +一个正常的TCP连接,都会有三个阶段:1、TCP三次握手;2、数据传送;3、TCP四次挥手 + +在这里插入图片描述 + + + +**TCP的连接释放** + +在这里插入图片描述 + +``` +LISTEN:侦听来自远方的TCP端口的连接请求 +SYN-SENT:再发送连接请求后等待匹配的连接请求(如果有大量这样的状态包,检查是否中招了) +SYN-RECEIVED:再收到和发送一个连接请求后等待对方对连接请求的确认(如有大量此状态估计被flood攻击了) +ESTABLISHED:代表一个打开的连接 +FIN-WAIT-1:等待远程TCP连接中断请求,或先前的连接中断请求的确认 +FIN-WAIT-2:从远程TCP等待连接中断请求 +CLOSE-WAIT:等待从本地用户发来的连接中断请求 +CLOSING:等待远程TCP对连接中断的确认 +LAST-ACK:等待原来的发向远程TCP的连接中断请求的确认(不是什么好东西,此项出现,检查是否被攻击) +TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认 +CLOSED:没有任何连接状态 +``` + +------ + +本文参考的文章: https://rdc.hundsun.com/portal/article/731.html?ref=myread + + + diff --git a/docs/operating-system/basis.md b/docs/operating-system/basis.md new file mode 100644 index 00000000..af49fd46 --- /dev/null +++ b/docs/operating-system/basis.md @@ -0,0 +1,348 @@ +大家好,我是 Guide 哥!很多读者抱怨计算操作系统的知识点比较繁杂,自己也没有多少耐心去看,但是面试的时候又经常会遇到。所以,我带着我整理好的操作系统的常见问题来啦!这篇文章总结了一些我觉得比较重要的操作系统相关的问题比如**进程管理**、**内存管理**、**虚拟内存**等等。 + +文章形式通过大部分比较喜欢的面试官和求职者之间的对话形式展开。另外,Guide 哥也只是在大学的时候学习过操作系统,不过基本都忘了,为了写这篇文章这段时间看了很多相关的书籍和博客。如果文中有任何需要补充和完善的地方,你都可以在评论区指出。如果觉得内容不错的话,不要忘记点个在看哦! + +我个人觉得学好操作系统还是非常有用的,具体可以看我昨天在星球分享的一段话: + + + +这篇文章只是对一些操作系统比较重要概念的一个概览,深入学习的话,建议大家还是老老实实地去看书。另外, 这篇文章的很多内容参考了《现代操作系统》第三版这本书,非常感谢。 + + + +## 一 操作系统基础 + +面试官顶着蓬松的假发向我走来,只见他一手拿着厚重的 Thinkpad ,一手提着他那淡黄的长裙。 + + + +### 1.1 什么是操作系统? + +👨‍💻**面试官** : 先来个简单问题吧!**什么是操作系统?** + +🙋 **我** :我通过以下四点向您介绍一下什么是操作系统吧! + +1. **操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机的基石。** +2. **操作系统本质上是一个运行在计算机上的软件程序 ,用于管理计算机硬件和软件资源。** 举例:运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。 +3. **操作系统存在屏蔽了硬件层的复杂性。** 操作系统就像是硬件使用的负责人,统筹着各种相关事项。 +4. **操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理**。 内核是连接应用程序和硬件的桥梁,决定着系统的性能和稳定性。 + +![Kernel_Layout](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/Kernel_Layout.png) + +### 1.2 系统调用 + +👨‍💻**面试官** :**什么是系统调用呢?** 能不能详细介绍一下。 + +🙋 **我** :介绍系统调用之前,我们先来了解一下用户态和系统态。 + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/006r3PQBjw1fbimb5c3srj30b40b40t9-20200404224750646.jpg) + +根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别: + +1. 用户态(user mode) : 用户态运行的进程或可以直接读取用户程序的数据。 +2. 系统态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。 + +说了用户态和系统态之后,那么什么是系统调用呢? + +我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的系统态级别的子功能咋办呢?那就需要系统调用了! + +也就是说在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。 + +这些系统调用按功能大致可分为如下几类: + +- 设备管理。完成设备的请求或释放,以及设备启动等功能。 +- 文件管理。完成文件的读、写、创建及删除等功能。 +- 进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。 +- 进程通信。完成进程之间的消息传递或信号传递等功能。 +- 内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。 + +## 二 进程和线程 + +### 2.1 进程和线程的区别 + +👨‍💻**面试官**: 好的!我明白了!那你再说一下: **进程和线程的区别**。 + +🙋 **我:** 好的! 下图是 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内存区域)>) + +![jvm运行时数据区域](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/ff96fed0e2a354bb16bbc84dcedf503a.png) + +从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 + +**总结:** 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。 + +### 2.2 进程有哪几种状态? + +👨‍💻**面试官** : 那你再说说**进程有哪几种状态?** + +🙋 **我** :我们一般把进程大致分为 5 种状态,这一点和[线程](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md#6-%E8%AF%B4%E8%AF%B4%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%92%8C%E7%8A%B6%E6%80%81)很像! + +- **创建状态(new)** :进程正在被创建,尚未到就绪状态。 +- **就绪状态(ready)** :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。 +- **运行状态(running)** :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。 +- **阻塞状态(waiting)** :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。 +- **结束状态(terminated)** :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。 + +> 订正:下图中 running 状态被 interrupt 向 ready 状态转换的箭头方向反了。 + +![process-state](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/d38202593012b457debbcd74994c6292.png) + +### 2.3 进程间的通信方式 + +👨‍💻**面试官** :**进程间的通信常见的的有哪几种方式呢?** + +🙋 **我** :大概有 7 种常见的进程间的通信方式。 + +> 下面这部分总结参考了:[《进程间通信 IPC (InterProcess Communication)》](https://www.jianshu.com/p/c1015f5ffa74) 这篇文章,推荐阅读,总结的非常不错。 + +1. **管道/匿名管道(Pipes)** :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。 +1. **有名管道(Names Pipes)** : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循**先进先出(first in first out)**。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。 +1. **信号(Signal)** :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生; +1. **消息队列(Message Queuing)** :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。**消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。** +1. **信号量(Semaphores)** :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。 +1. **共享内存(Shared memory)** :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。 +1. **套接字(Sockets)** : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。 + +### 2.4 线程间的同步的方式 + +👨‍💻**面试官** :**那线程间的同步的方式有哪些呢?** + +🙋 **我** :线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。操作系统一般有下面三种线程同步的方式: + +1. **互斥量(Mutex)**:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。 +1. **信号量(Semphares)** :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量 +1. **事件(Event)** :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操 + +### 2.5 进程的调度算法 + +👨‍💻**面试官** :**你知道操作系统中进程的调度算法有哪些吗?** + +🙋 **我** :嗯嗯!这个我们大学的时候学过,是一个很重要的知识点! + +为了确定首先执行哪个进程以及最后执行哪个进程以实现最大 CPU 利用率,计算机科学家已经定义了一些算法,它们是: + +- **先到先服务(FCFS)调度算法** : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。 +- **短作业优先(SJF)的调度算法** : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。 +- **时间片轮转调度算法** : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。 +- **多级反馈队列调度算法** :前面介绍的几种进程调度的算法都有一定的局限性。如**短进程优先的调度算法,仅照顾了短进程而忽略了长进程** 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前**被公认的一种较好的进程调度算法**,UNIX 操作系统采取的便是这种调度算法。 +- **优先级调度** : 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。 + +## 三 操作系统内存管理基础 + +### 3.1 内存管理介绍 + +👨‍💻 **面试官**: **操作系统的内存管理主要是做什么?** + +🙋 **我:** 操作系统的内存管理主要负责内存的分配与回收(malloc 函数:申请内存,free 函数:释放内存),另外地址转换也就是将逻辑地址转换成相应的物理地址等功能也是操作系统内存管理做的事情。 + +### 3.2 常见的几种内存管理机制 + +👨‍💻 **面试官**: **操作系统的内存管理机制了解吗?内存管理有哪几种方式?** + +🙋 **我:** 这个在学习操作系统的时候有了解过。 + +简单分为**连续分配管理方式**和**非连续分配管理方式**这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,常见的如 **块式管理** 。同样地,非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中,常见的如**页式管理** 和 **段式管理**。 + +1. **块式管理** : 远古时代的计算机操系统的内存管理方式。将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为碎片。 +2. **页式管理** :把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。 +3. **段式管理** : 页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义。 段式管理把主存分为一段段的,每一段的空间又要比一页的空间小很多 。但是,最重要的是段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。 + +👨‍💻**面试官** : 回答的还不错!不过漏掉了一个很重要的 **段页式管理机制** 。段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 **段页式管理机制** 中段与段之间以及段的内部的都是离散的。 + +🙋 **我** :谢谢面试官!刚刚把这个给忘记了~ + +这就很尴尬了_尴尬表情 + +### 3.3 快表和多级页表 + +👨‍💻**面试官** : 页表管理机制中有两个很重要的概念:快表和多级页表,这两个东西分别解决了页表管理中很重要的两个问题。你给我简单介绍一下吧! + +🙋 **我** :在分页内存管理中,很重要的两点是: + +1. 虚拟地址到物理地址的转换要快。 +2. 解决虚拟地址空间大,页表也会很大的问题。 + +#### 快表 + +为了解决虚拟地址到物理地址的转换速度,操作系统在 **页表方案** 基础之上引入了 **快表** 来加速虚拟地址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。 + +使用快表之后的地址转换流程是这样的: + +1. 根据虚拟地址中的页号查快表; +2. 如果该页在快表中,直接从快表中读取相应的物理地址; +3. 如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中; +4. 当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。 + +看完了之后你会发现快表和我们平时经常在我们开发的系统使用的缓存(比如 Redis)很像,的确是这样的,操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。 + +#### 多级页表 + +引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中。多级页表属于时间换空间的典型场景,具体可以查看下面这篇文章 + +- 多级页表如何节约内存:[https://www.polarxiong.com/archives/多级页表如何节约内存.html](https://www.polarxiong.com/archives/多级页表如何节约内存.html) + +#### 总结 + +为了提高内存的空间性能,提出了多级页表的概念;但是提到空间性能是以浪费时间性能为基础的,因此为了补充损失的时间性能,提出了快表(即 TLB)的概念。 不论是快表还是多级页表实际上都利用到了程序的局部性原理,局部性原理在后面的虚拟内存这部分会介绍到。 + +### 3.4 分页机制和分段机制的共同点和区别 + +👨‍💻**面试官** : **分页机制和分段机制有哪些共同点和区别呢?** + +🙋 **我** : + + + +1. **共同点** : + - 分页机制和分段机制都是为了提高内存利用率,较少内存碎片。 + - 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。 +2. **区别** : + - 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。 + - 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。 + +### 3.5 逻辑(虚拟)地址和物理地址 + +👨‍💻**面试官** :你刚刚还提到了**逻辑地址和物理地址**这两个概念,我不太清楚,你能为我解释一下不? + +🙋 **我:** em...好的嘛!我们编程一般只有可能和逻辑地址打交道,比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统决定。物理地址指的是真实物理内存中地址,更具体一点来说就是内存地址寄存器中的地址。物理地址是内存单元真正的地址。 + +### 3.6 CPU 寻址了解吗?为什么需要虚拟地址空间? + +👨‍💻**面试官** :**CPU 寻址了解吗?为什么需要虚拟地址空间?** + +🙋 **我** :这部分我真不清楚! + + + +于是面试完之后我默默去查阅了相关文档!留下了没有技术的泪水。。。 + +> 这部分内容参考了 Microsoft 官网的介绍,地址: + +现代处理器使用的是一种称为 **虚拟寻址(Virtual Addressing)** 的寻址方式。**使用虚拟寻址,CPU 需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。** 实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为 **内存管理单元(Memory Management Unit, MMU)** 的硬件。如下图所示: + +![MMU_principle_updated](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/2b27dac8cc647f8aac989da2d1166db2.png) + +**为什么要有虚拟地址空间呢?** + +先从没有虚拟地址空间的时候说起吧!没有虚拟地址空间的时候,**程序都是直接访问和操作的都是物理内存** 。但是这样有什么问题呢? + +1. 用户程序可以访问任意内存,寻址内存的每个字节,这样就很容易(有意或者无意)破坏操作系统,造成操作系统崩溃。 +2. 想要同时运行多个程序特别困难,比如你想同时运行一个微信和一个 QQ 音乐都不行。为什么呢?举个简单的例子:微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就造成了微信这个程序就会崩溃。 + +**总结来说:如果直接把物理地址暴露出来的话会带来严重问题,比如可能对操作系统造成伤害以及给同时运行多个程序造成困难。** + +通过虚拟地址访问内存有以下优势: + +- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。 +- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。 +- 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。 + +## 四 虚拟内存 + +### 4.1 什么是虚拟内存(Virtual Memory)? + +👨‍💻**面试官** :再问你一个常识性的问题!**什么是虚拟内存(Virtual Memory)?** + +🙋 **我** :这个在我们平时使用电脑特别是 Windows 系统的时候太常见了。很多时候我们使用点开了很多占内存的软件,这些软件占用的内存可能已经远远超出了我们电脑本身具有的物理内存。**为什么可以这样呢?** 正是因为 **虚拟内存** 的存在,通过 **虚拟内存** 可以让程序可以拥有超过系统物理内存大小的可用内存空间。另外,**虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)**。这样会更加有效地管理内存并减少出错。 + +**虚拟内存**是计算机系统内存管理的一种技术,我们可以手动设置自己电脑的虚拟内存。不要单纯认为虚拟内存只是“使用硬盘空间来扩展内存“的技术。**虚拟内存的重要意义是它定义了一个连续的虚拟地址空间**,并且 **把内存扩展到硬盘空间**。推荐阅读:[《虚拟内存的那点事儿》](https://juejin.im/post/59f8691b51882534af254317) + +维基百科中有几句话是这样介绍虚拟内存的。 + +> **虚拟内存** 使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如 RAM)的使用也更有效率。目前,大多数操作系统都使用了虚拟内存,如 Windows 家族的“虚拟内存”;Linux 的“交换空间”等。From: + +### 4.2 局部性原理 + +👨‍💻**面试官** :要想更好地理解虚拟内存技术,必须要知道计算机中著名的**局部性原理**。另外,局部性原理既适用于程序结构,也适用于数据结构,是非常重要的一个概念。 + +🙋 **我** :局部性原理是虚拟内存技术的基础,正是因为程序运行具有局部性原理,才可以只装入部分程序到内存就开始运行。 + +> 以下内容摘自《计算机操作系统教程》 第 4 章存储器管理。 + +早在 1968 年的时候,就有人指出我们的程序在执行的时候往往呈现局部性规律,也就是说在某个较短的时间段内,程序执行局限于某一小部分,程序访问的存储空间也局限于某个区域。 + +局部性原理表现在以下两个方面: + +1. **时间局部性** :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。 +2. **空间局部性** :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。 + +时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。 + +### 4.3 虚拟存储器 + +👨‍💻**面试官** :都说了虚拟内存了。你再讲讲**虚拟存储器**把! + +🙋 **我** : + +> 这部分内容来自:[王道考研操作系统知识点整理](https://wizardforcel.gitbooks.io/wangdaokaoyan-os/content/13.html)。 + +基于局部性原理,在程序装入时,可以将程序的一部分装入内存,而将其他部分留在外存,就可以启动程序执行。由于外存往往比内存大很多,所以我们运行的软件的内存大小实际上是可以比计算机系统实际的内存大小大的。在程序执行过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后继续执行程序。另一方面,操作系统将内存中暂时不使用的内容换到外存上,从而腾出空间存放将要调入内存的信息。这样,计算机好像为用户提供了一个比实际内存大的多的存储器——**虚拟存储器**。 + +实际上,我觉得虚拟内存同样是一种时间换空间的策略,你用 CPU 的计算时间,页的调入调出花费的时间,换来了一个虚拟的更大的空间来支持程序的运行。不得不感叹,程序世界几乎不是时间换空间就是空间换时间。 + +### 4.4 虚拟内存的技术实现 + +👨‍💻**面试官** :**虚拟内存技术的实现呢?** + +🙋 **我** :**虚拟内存的实现需要建立在离散分配的内存管理方式的基础上。** 虚拟内存的实现有以下三种方式: + +1. **请求分页存储管理** :建立在分页管理之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能。请求分页是目前最常用的一种实现虚拟存储器的方法。请求分页存储管理系统中,在作业开始运行之前,仅装入当前要执行的部分段即可运行。假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存,同时操作系统也可以将暂时不用的页面置换到外存中。 +2. **请求分段存储管理** :建立在分段存储管理之上,增加了请求调段功能、分段置换功能。请求分段储存管理方式就如同请求分页储存管理方式一样,在作业开始运行之前,仅装入当前要执行的部分段即可运行;在执行过程中,可使用请求调入中断动态装入要访问但又不在内存的程序段;当内存空间已满,而又需要装入新的段时,根据置换功能适当调出某个段,以便腾出空间而装入新的段。 +3. **请求段页式存储管理** + +**这里多说一下?很多人容易搞混请求分页与分页存储管理,两者有何不同呢?** + +请求分页存储管理建立在分页管理之上。他们的根本区别是是否将程序全部所需的全部地址空间都装入主存,这也是请求分页存储管理可以提供虚拟内存的原因,我们在上面已经分析过了。 + +它们之间的根本区别在于是否将一作业的全部地址空间同时装入主存。请求分页存储管理不要求将作业全部地址空间同时装入主存。基于这一点,请求分页存储管理可以提供虚存,而分页存储管理却不能提供虚存。 + +不管是上面那种实现方式,我们一般都需要: + +1. 一定容量的内存和外存:在载入程序的时候,只需要将程序的一部分装入内存,而将其他部分留在外存,然后程序就可以执行了; +2. **缺页中断**:如果**需执行的指令或访问的数据尚未在内存**(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段**调入到内存**,然后继续执行程序; +3. **虚拟地址空间** :逻辑地址到物理地址的变换。 + +### 4.5 页面置换算法 + +👨‍💻**面试官** :虚拟内存管理很重要的一个概念就是页面置换算法。那你说一下 **页面置换算法的作用?常见的页面置换算法有哪些?** + +🙋 **我** : + +> 这个题目经常作为笔试题出现,网上已经给出了很不错的回答,我这里只是总结整理了一下。 + +地址映射过程中,若在页面中发现所要访问的页面不在内存中,则发生缺页中断 。 + +> **缺页中断** 就是要访问的**页**不在主存,需要操作系统将其调入主存后再进行访问。 在这个时候,被内存映射的文件实际上成了一个分页交换文件。 + +当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则。 + +- **OPT 页面置换算法(最佳页面置换算法)** :最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。一般作为衡量其他置换算法的方法。 +- **FIFO(First In First Out) 页面置换算法(先进先出页面置换算法)** : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。 +- **LRU (Least Currently Used)页面置换算法(最近最久未使用页面置换算法)** :LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。 +- **LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法)** : 该置换算法选择在之前时期使用最少的页面作为淘汰页。 + +## Reference + +- 《计算机操作系统—汤小丹》第四版 +- [《深入理解计算机系统》](https://book.douban.com/subject/1230413/) +- [https://zh.wikipedia.org/wiki/输入输出内存管理单元](https://zh.wikipedia.org/wiki/输入输出内存管理单元) +- [https://baike.baidu.com/item/快表/19781679](https://baike.baidu.com/item/快表/19781679) +- https://www.jianshu.com/p/1d47ed0b46d5 +- +- +- +- 王道考研操作系统知识点整理: https://wizardforcel.gitbooks.io/wangdaokaoyan-os/content/13.html + + + + + + + + + + + + + diff --git a/docs/operating-system/images/Linux-Logo.png b/docs/operating-system/images/Linux-Logo.png new file mode 100644 index 00000000..40e75aaa Binary files /dev/null and b/docs/operating-system/images/Linux-Logo.png differ diff --git a/docs/operating-system/images/Linux之父.png b/docs/operating-system/images/Linux之父.png new file mode 100644 index 00000000..33145373 Binary files /dev/null and b/docs/operating-system/images/Linux之父.png differ diff --git a/docs/operating-system/images/Linux权限命令.png b/docs/operating-system/images/Linux权限命令.png new file mode 100644 index 00000000..f59b2e63 Binary files /dev/null and b/docs/operating-system/images/Linux权限命令.png differ diff --git a/docs/operating-system/images/Linux权限解读.png b/docs/operating-system/images/Linux权限解读.png new file mode 100644 index 00000000..1292c125 Binary files /dev/null and b/docs/operating-system/images/Linux权限解读.png differ diff --git a/docs/operating-system/images/Linux目录树.png b/docs/operating-system/images/Linux目录树.png new file mode 100644 index 00000000..beef4203 Binary files /dev/null and b/docs/operating-system/images/Linux目录树.png differ diff --git a/docs/operating-system/images/linux.png b/docs/operating-system/images/linux.png new file mode 100644 index 00000000..20ead246 Binary files /dev/null and b/docs/operating-system/images/linux.png differ diff --git a/docs/operating-system/images/macos.png b/docs/operating-system/images/macos.png new file mode 100644 index 00000000..33294577 Binary files /dev/null and b/docs/operating-system/images/macos.png differ diff --git a/docs/operating-system/images/unix.png b/docs/operating-system/images/unix.png new file mode 100644 index 00000000..0afabcd8 Binary files /dev/null and b/docs/operating-system/images/unix.png differ diff --git a/docs/operating-system/images/windows.png b/docs/operating-system/images/windows.png new file mode 100644 index 00000000..c2687dc7 Binary files /dev/null and b/docs/operating-system/images/windows.png differ diff --git a/docs/operating-system/images/修改文件权限.png b/docs/operating-system/images/修改文件权限.png new file mode 100644 index 00000000..de940941 Binary files /dev/null and b/docs/operating-system/images/修改文件权限.png differ diff --git a/docs/operating-system/images/文件inode信息.png b/docs/operating-system/images/文件inode信息.png new file mode 100644 index 00000000..b47551e8 Binary files /dev/null and b/docs/operating-system/images/文件inode信息.png differ diff --git a/docs/operating-system/images/用户态与内核态.png b/docs/operating-system/images/用户态与内核态.png new file mode 100644 index 00000000..aa0dafc2 Binary files /dev/null and b/docs/operating-system/images/用户态与内核态.png differ diff --git a/docs/operating-system/linux.md b/docs/operating-system/linux.md new file mode 100644 index 00000000..7f318ccc --- /dev/null +++ b/docs/operating-system/linux.md @@ -0,0 +1,455 @@ +点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 + + + + + + +- [1. 从认识操作系统开始](#1-从认识操作系统开始) + - [1.1. 操作系统简介](#11-操作系统简介) + - [1.2. 操作系统简单分类](#12-操作系统简单分类) + - [1.2.1. Windows](#121-windows) + - [1.2.2. Unix](#122-unix) + - [1.2.3. Linux](#123-linux) + - [1.2.4. Mac OS](#124-mac-os) + - [1.3. 操作系统的内核(Kernel)](#13-操作系统的内核kernel) + - [1.4. 中央处理器(CPU,Central Processing Unit)](#14-中央处理器cpucentral-processing-unit) + - [1.5. CPU vs Kernel(内核)](#15-cpu-vs-kernel内核) + - [1.6. 系统调用](#16-系统调用) +- [2. 初探 Linux](#2-初探-linux) + - [2.1. Linux 简介](#21-linux-简介) + - [2.2. Linux 诞生](#22-linux-诞生) + - [2.3. 常见 Linux 发行版本有哪些?](#23-常见-linux-发行版本有哪些) +- [3. Linux 文件系统概览](#3-linux-文件系统概览) + - [3.1. Linux 文件系统简介](#31-linux-文件系统简介) + - [3.2. inode 介绍](#32-inode-介绍) + - [3.3. Linux 文件类型](#33-linux-文件类型) + - [3.4. Linux 目录树](#34-linux-目录树) +- [4. Linux 基本命令](#4-linux-基本命令) + - [4.1. 目录切换命令](#41-目录切换命令) + - [4.2. 目录的操作命令(增删改查)](#42-目录的操作命令增删改查) + - [4.3. 文件的操作命令(增删改查)](#43-文件的操作命令增删改查) + - [4.4. 压缩文件的操作命令](#44-压缩文件的操作命令) + - [4.5. Linux 的权限命令](#45-linux-的权限命令) + - [4.6. Linux 用户管理](#46-linux-用户管理) + - [4.7. Linux 系统用户组的管理](#47-linux-系统用户组的管理) + - [4.8. 其他常用命令](#48-其他常用命令) +- [5. 公众号](#5-公众号) + + + + +今天这篇文章中简单介绍一下一个 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 之父** : 一个编程领域的传奇式人物,真大佬!我辈崇拜敬仰之楷模。他是 **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`:** 做个重开机的模拟(只有纪录并不会真的重开机)。 + +## 5. 公众号 + +如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 + +**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V3.0 PDF 版本[公众号](#公众号)后台回复 **"Java 面试突击"** 即可免费领取! + +**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 + +![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/operating-system/后端程序员必备的Linux基础知识.md b/docs/operating-system/后端程序员必备的Linux基础知识.md deleted file mode 100644 index 3af99b4e..00000000 --- a/docs/operating-system/后端程序员必备的Linux基础知识.md +++ /dev/null @@ -1,372 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [一 从认识操作系统开始](#一-从认识操作系统开始) - - [1.1 操作系统简介](#11-操作系统简介) - - [1.2 操作系统简单分类](#12-操作系统简单分类) -- [二 初探Linux](#二-初探linux) - - [2.1 Linux简介](#21-linux简介) - - [2.2 Linux诞生简介](#22-linux诞生简介) - - [2.3 Linux的分类](#23-linux的分类) -- [三 Linux文件系统概览](#三-linux文件系统概览) - - [3.1 Linux文件系统简介](#31-linux文件系统简介) - - [3.2 文件类型与目录结构](#32-文件类型与目录结构) -- [四 Linux基本命令](#四-linux基本命令) - - [4.1 目录切换命令](#41-目录切换命令) - - [4.2 目录的操作命令(增删改查)](#42-目录的操作命令增删改查) - - [4.3 文件的操作命令(增删改查)](#43-文件的操作命令增删改查) - - [4.4 压缩文件的操作命令](#44-压缩文件的操作命令) - - [4.5 Linux的权限命令](#45-linux的权限命令) - - [4.6 Linux 用户管理](#46-linux-用户管理) - - [4.7 Linux系统用户组的管理](#47-linux系统用户组的管理) - - [4.8 其他常用命令](#48-其他常用命令) - - - -推荐一个Github开源的Linux学习指南(Java工程师向): - -> 学习Linux之前,我们先来简单的认识一下操作系统。 - -## 一 从认识操作系统开始 - -### 1.1 操作系统简介 - -我通过以下四点介绍什么是操作系统: - -- **操作系统(Operation System,简称OS)是管理计算机硬件与软件资源的程序,是计算机系统的内核与基石;** -- **操作系统本质上是运行在计算机上的软件程序 ;** -- **为用户提供一个与系统交互的操作界面 ;** -- **操作系统分内核与外壳(我们可以把外壳理解成围绕着内核的应用程序,而内核就是能操作硬件的程序)。** - -![操作系统分内核与外壳](https://user-gold-cdn.xitu.io/2018/7/3/1645ee3dc5cf626e?w=862&h=637&f=png&s=23899) -### 1.2 操作系统简单分类 - -1. **Windows:** 目前最流行的个人桌面操作系统 ,不做多的介绍,大家都清楚。 -2. **Unix:** 最早的多用户、多任务操作系统 .按照操作系统的分类,属于分时操作系统。Unix 大多被用在服务器、工作站,现在也有用在个人计算机上。它在创建互联网、计算机网络或客户端/服务器模型方面发挥着非常重要的作用。 -![Unix](https://user-gold-cdn.xitu.io/2018/7/3/1645ee83f036846d?w=1075&h=475&f=png&s=914462) -3. **Linux:** Linux是一套免费使用和自由传播的类Unix操作系统.Linux存在着许多不同的Linux版本,但它们都使用了 **Linux内核** 。Linux可安装在各种计算机硬件设备中,比如手机、平板电脑、路由器、视频游戏控制台、台式计算机、大型机和超级计算机。严格来讲,Linux这个词本身只表示Linux内核,但实际上人们已经习惯了用Linux来形容整个基于Linux内核,并且使用GNU 工程各种工具和数据库的操作系统。 - -![Linux](https://user-gold-cdn.xitu.io/2018/7/3/1645eeb8e843f29d?w=426&h=240&f=png&s=32650) - - -## 二 初探Linux - -### 2.1 Linux简介 - -我们上面已经介绍到了Linux,我们这里只强调三点。 -- **类Unix系统:** Linux是一种自由、开放源码的类似Unix的操作系统 -- **Linux内核:** 严格来说,Linux这个词本身只表示Linux内核 -- **Linux之父:** 一个编程领域的传奇式人物。他是Linux内核的最早作者,随后发起了这个开源项目,担任Linux内核的首要架构师与项目协调者,是当今世界最著名的电脑程序员、黑客之一。他还发起了Git这个开源项目,并为主要的开发者。 - -![Linux](https://user-gold-cdn.xitu.io/2018/7/3/1645ef0a5a4f137f?w=270&h=376&f=png&s=193487) - -### 2.2 Linux诞生简介 - -- 1991年,芬兰的业余计算机爱好者Linus Torvalds编写了一款类似Minix的系统(基于微内核架构的类Unix操作系统)被ftp管理员命名为Linux 加入到自由软件基金的GNU计划中; -- Linux以一只可爱的企鹅作为标志,象征着敢作敢为、热爱生活。 - - -### 2.3 Linux的分类 - -**Linux根据原生程度,分为两种:** - -1. **内核版本:** Linux不是一个操作系统,严格来讲,Linux只是一个操作系统中的内核。内核是什么?内核建立了计算机软件与硬件之间通讯的平台,内核提供系统服务,比如文件管理、虚拟内存、设备I/O等; -2. **发行版本:** 一些组织或公司在内核版基础上进行二次开发而重新发行的版本。Linux发行版本有很多种(ubuntu和CentOS用的都很多,初学建议选择CentOS),如下图所示: -![Linux发行版本](https://user-gold-cdn.xitu.io/2018/7/3/1645efa7048fd018?w=548&h=274&f=png&s=99213) - - -## 三 Linux文件系统概览 - -### 3.1 Linux文件系统简介 - -**在Linux操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文件或是目录都被看作是一个文件。** - -也就是说在LINUX系统中有一个重要的概念:**一切都是文件**。其实这是UNIX哲学的一个体现,而Linux是重写UNIX而来,所以这个概念也就传承了下来。在UNIX系统中,把一切资源都看作是文件,包括硬件设备。UNIX系统把每个硬件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。 - - -### 3.2 文件类型与目录结构 - -**Linux支持5种文件类型 :** -![文件类型](https://user-gold-cdn.xitu.io/2018/7/3/1645f1a7d64def1a?w=901&h=547&f=png&s=72692) - -**Linux的目录结构如下:** - -Linux文件系统的结构层次鲜明,就像一棵倒立的树,最顶层是其根目录: -![Linux的目录结构](https://user-gold-cdn.xitu.io/2018/7/3/1645f1c65676caf6?w=823&h=315&f=png&s=15226) - -**常见目录说明:** - -- **/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)就在这里。 - - -## 四 Linux基本命令 - -下面只是给出了一些比较常用的命令。推荐一个Linux命令快查网站,非常不错,大家如果遗忘某些命令或者对某些命令不理解都可以在这里得到解决。 - -Linux命令大全:[http://man.linuxde.net/](http://man.linuxde.net/) -### 4.1 目录切换命令 - -- **`cd usr`:** 切换到该目录下usr目录 -- **`cd ..(或cd../)`:** 切换到上一层目录 -- **`cd /`:** 切换到系统根目录 -- **`cd ~`:** 切换到用户主目录 -- **`cd -`:** 切换到上一个操作所在目录 - -### 4.2 目录的操作命令(增删改查) - -1. **`mkdir 目录名称`:** 增加目录 -2. **`ls或者ll`**(ll是ls -l的别名,ll命令可以看到该目录下的所有目录和文件的详细信息):查看目录信息 -3. **`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" ` - -4. **`mv 目录名称 新目录名称`:** 修改目录的名称(改) - - 注意:mv的语法不仅可以对目录进行重命名而且也可以对各种文件,压缩包等进行 重命名的操作。mv命令用来对文件或目录重新命名,或者将文件从一个目录移到另一个目录中。后面会介绍到mv命令的另一个用法。 -5. **`mv 目录名称 目录的新位置`:** 移动目录的位置---剪切(改) - - 注意:mv语法不仅可以对目录进行剪切操作,对文件和压缩包等都可执行剪切操作。另外mv与cp的结果不同,mv好像文件“搬家”,文件个数并未增加。而cp对文件进行复制,文件个数增加了。 -6. **`cp -r 目录名称 目录拷贝的目标位置`:** 拷贝目录(改),-r代表递归拷贝 - - 注意:cp命令不仅可以拷贝目录还可以拷贝文件,压缩包等,拷贝文件和压缩包时不 用写-r递归 -7. **`rm [-rf] 目录`:** 删除目录(删) - - 注意:rm不仅可以删除目录,也可以删除其他文件或压缩包,为了增强大家的记忆, 无论删除任何目录或文件,都直接使用`rm -rf` 目录/文件/压缩包 - - -### 4.3 文件的操作命令(增删改查) - -1. **`touch 文件名称`:** 文件的创建(增) -2. **`cat/more/less/tail 文件名称`** 文件的查看(查) - - **`cat`:** 查看显示文件内容 - - **`more`:** 可以显示百分比,回车可以向下一行, 空格可以向下一页,q可以退出查看 - - **`less`:** 可以使用键盘上的PgUp和PgDn向上 和向下翻页,q结束查看 - - **`tail-10` :** 查看文件的后10行,Ctrl+C结束 - - 注意:命令 tail -f 文件 可以对某个文件进行动态监控,例如tomcat的日志文件, 会随着程序的运行,日志会变化,可以使用tail -f catalina-2016-11-11.log 监控 文 件的变化 -3. **`vim 文件`:** 修改文件的内容(改) - - vim编辑器是Linux中的强大组件,是vi编辑器的加强版,vim编辑器的命令和快捷方式有很多,但此处不一一阐述,大家也无需研究的很透彻,使用vim编辑修改文件的方式基本会使用就可以了。 - - **在实际开发中,使用vim编辑器主要作用就是修改配置文件,下面是一般步骤:** - - vim 文件------>进入文件----->命令模式------>按i进入编辑模式----->编辑文件 ------->按Esc进入底行模式----->输入:wq/q! (输入wq代表写入内容并退出,即保存;输入q!代表强制退出不保存。) -4. **`rm -rf 文件`:** 删除文件(删) - - 同目录删除:熟记 `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:代表解压 - -示例: - -1 将/test下的test.tar.gz解压到当前目录下可以使用命令:**`tar -xvf test.tar.gz`** - -2 将/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` - -![](https://user-gold-cdn.xitu.io/2018/7/5/1646955be781daaa?w=589&h=228&f=png&s=16360) - -第一列的内容的信息解释如下: - -![](https://user-gold-cdn.xitu.io/2018/7/5/16469565b6951791?w=489&h=209&f=png&s=39791) - -> 下面将详细讲解文件的类型、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中每个文件有所有者、所在组、其它组的概念。** - -- **所有者** - - 一般为文件的创建者,谁创建了该文件,就天然的成为该文件的所有者,用ls ‐ahl命令可以看到文件的所有者 也可以使用chown 用户名 文件名来修改文件的所有者 。 -- **文件所在组** - - 当某个用户创建了一个文件后,这个文件的所在组就是该用户所在的组 用ls ‐ahl命令可以看到文件的所有组 也可以使用chgrp 组名 文件名来修改文件所在的组。 -- **其它组** - - 除开文件的所有者和所在组的用户外,系统的其它用户都是文件的其它组 - -> 我们再来看看如何修改文件/目录的权限。 - -**修改文件/目录的权限的命令:`chmod`** - -示例:修改/test下的aaa.txt的权限为属主有全部权限,属主所在的组有读写权限, -其他用户只有读的权限 - -**`chmod u=rwx,g=rw,o=r aaa.txt`** - -![](https://user-gold-cdn.xitu.io/2018/7/5/164697447dc6ecac?w=525&h=246&f=png&s=12362) - -上述示例还可以使用数字表示: - -chmod 764 aaa.txt - - -**补充一个比较常用的东西:** - -假如我们装了一个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工具箱,后来成为老版本Linux内核中配置网络功能的工具。但自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`:** 做个重开机的模拟(只有纪录并不会真的重开机)。 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《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/operating-system/完全使用GNU_Linux学习.md b/docs/operating-system/完全使用GNU_Linux学习.md new file mode 100644 index 00000000..d9e171da --- /dev/null +++ b/docs/operating-system/完全使用GNU_Linux学习.md @@ -0,0 +1,252 @@ + + + * [完全使用GNU/Linux学习](#完全使用gnulinux学习) + * [为什么要写这篇文章?](#为什么要写这篇文章) + * [为什么我要从Windows切换到Linux?](#为什么我要从windows切换到linux) + * [Linux作为日常使用的缺点](#linux作为日常使用的缺点) + * [硬件驱动问题](#硬件驱动问题) + * [软件问题](#软件问题) + * [你真的需要完全使用Linux吗?](#你真的需要完全使用linux吗) + * [结尾](#结尾) + * [我使用Debian/Ubuntu时遇到的问题](#我使用debianubuntu时遇到的问题) + * [IDEA编辑Markdown预渲染问题](#idea编辑markdown预渲染问题) + * [wifi适配器找不到](#wifi适配器找不到) + * [XMind安装](#xmind安装) + * [Fcitx候选框的定位问题](#fcitx候选框的定位问题) + + + +# 完全使用GNU/Linux学习 + +喔,看到这个标题千万不要以为我要写和王垠前辈一样的内容啊,嘿嘿。不过在这里还是献上王垠前辈的那篇文章的链接吧:[完全用Linux工作](https://www.douban.com/group/topic/12121637/)。 + +## 为什么要写这篇文章? + +首先介绍本篇文章产出的时间,现在是2020/04/06。在三,四天之前,我其实并没有写这篇文章的打算,但是这三,四天以来,我一直在忙活从Ubuntu18换到Debian10 Buster的事情,没有时间写代码,手确实有些痒了。你可能想象不到,我这个之前一直使用Ubuntu的人,只是切换到Debian就花这么长时间,你可能以为我是在劝退各位同学,其实不是的,我只是想表达:我对Linux并不熟悉,这其中一部分原因是我使用的是对用户较为友好的发行版Ubuntu,另一部分原因是我仍然没有那么大的动力去学习Linux,即使它一直作为我的日常使用。 + +这篇文章并不是吹嘘或贬低Windows和Linux系统,而是想记录一下我一直以来使用Linux作为日常学习的心得,以及这几天再度折腾Debian以来的感触。 + + + +## 为什么我要从Windows切换到Linux? + +Windows是商业软件,这使它具备易用的性质。Linux是自由软件,这使得它拥有开源的性质。 + +易用软件通常带来的是对用户的友好度,以致于Windows发展至今,被许许多多的普通用户所采用。自由软件通常带来的是其社区的发展,所以你现在可以在网上看到许多如 ask ubuntu 这样的论坛。 + +我非常赞同《完全用Linux工作》中的一个观点: **UNIX 不是计算机专家的专利。** + +我对这句话的理解就是:即使你学习或工作的方向不是计算机,但你仍然可以去学习Unix/Linux,如果你是计算机方向的同学,那么,你就更应该去学习Unix/Linux了。 + +但这只是我从Win切换到Linux的一部分原因,另一个很重要的原因是我受够了Windows的 “易用性”。这里的易用性并不是说我排斥Windows的人性化,而是因为人性化给我带来了很多学习上的困难。举个很简单的栗子:你在学习一项技术的时候,无论是否有面试造火箭的需要,你是否都会好奇想了解其原理和实现,即使你可能知道它很复杂。 + +**为什么你会好奇一个事物的源头?** + +我个人认为的答案是:有趣的事情就在眼前,为什么不去了解它呢? + +而Windows只是有趣,但它并不在“眼前”。 + +我个人的体验哈,不知道有没有同学和我一样的经历,在很多时候,你的Windows可能会出现一些莫名奇妙的问题,但你却不知道如何解决它,你只能求助搜索引擎,当你解决完问题后,你不会想要去了解为什么会发生这种问题,因为Windows太庞大了。 + +就比如: 我现在安装了Git,使用起来没有任何问题。但等到过一段时间后,Git莫名奇妙的不能使用了,明明你啥都没干。更甚之,有一些流氓问题或流氓软件不能被解决和被屏蔽。 + +问题出现了,总得要解决吧,所以此时你开始在互联网上查询相关问题的解决方法,如果你的运气好,那么有人可能遇到过和你出现相同的问题,你也因此可能会得到答案。不过一般的答案只是教你怎么解决的,如打开注册表,添加或删除某个key,你不会想要知道为什么做,因为对于初学者来说,当你看到注册表那么多的内容时,你只想着不出错就行了,哪还有心思去学习这玩意啊。如果你的运气不好,且并没有更换系统的打算,那么你可能会将就着使用,但此时,你的心里可能已经衍生了对Windows的厌烦情绪。 + +我对流氓软件的定义是:当你想让一个软件如你的想法停止运行或停止弹出广告的时候,这个软件不能或不能做的很好的达到你的要求时,这就是一个流氓软件。你也许会说,每个人都有不同的要求,软件怎么可能达到每个人的标准呢?但我指的是停止和停止弹出广告等这样最基本的诉求,如果一个软件连最基本的诉求都实现不了,又何必再使用它呢? + +综上所述,我从Window切换到Linux的最主要的原因有:**学习和自由。** + +是的,你不得不承认Linux是你学习计算机的非常好的环境,与C/C++天然的集成,比你在Windows上冷冰冰的安装一个IDE就开始敲起代码来,显得多了那么一点感觉。 + +还有一点,可能有的同学和我一样,刚接触Linux的时候,是在Windows上安装一个虚拟机环境或使用Docker来进行学习。不可否认,这确实是在Windows上学习Linux的主要途径了,但是你有没有感觉到,你在采取这种方式学习的时候,对Linux始终有种陌生感,似乎我只是在为了学习而学习。 + +产生这种想法的主要原因就是你没有融入到Linux环境之中,当你融入到Linux环境之中时,你不再只是需要学习那些操作命令,你会不可避免的遇到某个你从来没有接触过的问题,这个问题不是你在Windows上“丢失图标”的那种烦人问题,而可能是令你有些害怕的因为Nvidia的驱动而黑屏的问题。你也会在互联网上查询为什么会出现这种问题,但你得到的并不是“修改注册表”这种答案,而是会学习到:为什么Nvidia在Linux上会出现这种问题?我怎么做才能解决驱动问题?其他驱动是否也有类似Nvidia这种问题? 当你解决问题后,你的电脑开始正常工作了,你便开始使用它作为你的日常使用... + +关于使用Linux学习的原因的最后一点是我认为自己不够慎独,不够克制。当我使用Windows的时候,并不能完全克制住自己接触那些新鲜游戏的念头,我玩起游戏来,通常会连续很长时间,可能是一天-_-。不过我并不是说Linux上没有游戏,相反,Linux是对很多游戏的支持是很好的,你可以玩到很多游戏,但你是否会因为使用Linux对游戏不再那么执着,至少我是如此了。这一点可以归结为“使用Linux对戒游戏有帮助吧” ,哈哈。 + +再谈谈自由: + +我对自由的理解是:软件在你的掌控之中,你可以了解它的每一部分,你可以去到你想到达的地方,不受任何限制,这只取决于你愿不愿意。 + +来看看基本的Linux目录吧: + +![Linux目录](../../media/pictures/linux/Linux目录.png) + +这些目录你可能有很多都不认识,但没关系,因为这就是Linux系统(大部分)所有的目录了,你稍微了解下,就知道这些目录里放的是什么文件了。 + +这也是我个人的体验而已,总之,Linux的自由是一种开源精神,比我描述的可大的多。至于Windows,我到现在连C盘的目录放了些什么都不太熟悉,但我并不是在贬低Windows,因为这就是Windows易用性的代价,相应的,Linux作为自由软件,它也有很多缺点。 + + + +## Linux作为日常使用的缺点 + +### 硬件驱动问题 + +硬件驱动问题一般是在安装Linux时会出现的问题,根据个人电脑配置的不同,你的电脑的硬件驱动可能与要安装的Linux发行版不兼容,导致系统出现相应的问题。我这几天对驱动问题最深刻的体会就明白了为啥Linus大神会吐槽: “Nvidia Fuck You”。很多驱动厂商对Linux系统是闭源的,你可以下载这些厂商的驱动,但是能不能用,或者用起来有什么毛病都得你自己买单。 + +随着Linux开始在普通用户中变得流行起来,我相信今后Linux的生态会发展的越来越好,且现在很多Linux发行版对各种硬件的兼容性也越来越好,就以我之前使用的Ubuntu18来说,Nvidia,Wifi,蓝牙等驱动使用都是没啥问题的。我现在使用的Debian10 Buster对Nvidia的支持可能还不是那么好,使用起来总有一些小毛病,不过无伤大雅,其实没毛病我还有点不适应,不是说Debian是Ubuntu的爸爸吗,哈哈。 + + + +### 软件问题 + +不得不承认的一点是Linux的软件生态确实没有Windows那么丰富,你在考虑切换系统之前,必须先调查清楚Linux上是否有你必需的软件,你所需的软件是否支持跨平台或者是否有可替代的应用。我个人对软件要求较为简单,大部分都是生产力工具,其他的应用如娱乐软件之类的都可以使用网页版作为替代。如果你在Linux系统上想尝试游戏的话,我认为也是OK的,因为我也尝试过Linux Dota2 ,体验非常好(不是广告-_-)。不过大多数国内游戏厂商对Linux的支持都是很差的,所以如果过不了这道坎,也不要切换系统了。 + +软件问题其实可以分为2部分看待,一部分就是刚刚介绍过的生态问题,另一部分就是当你在使用某些软件的时候,总会出现某些小Bug。 + +就以Fcitx来说,Fcitx是一款通用的Linux输入法框架,被称为小企鹅输入法,很多输入法都是在Fcitx之上开发的,如搜狗,Googlepinyin,Sunpinyin等。使用过Fcitx的同学可能会遇到这种问题:当你在使用Fcitx在某些软件上打字时,候选框并不会跟随你光标的位置,而是总会固定在某一个位置,并且你无法改变,这个问题是我目前见过的最大Bug。不过这个Bug只在部分软件上有,在Chrome,Typora上都没有这个问题,这让我怀疑是软件的国际化问题,而非Fcitx问题。 + +所以第二个部分总结起来就是某些软件可能会出现某些未知的Bug,你得寻求解决的办法,或者忍耐使用,使用Linux也是得牺牲一些代价的。 + + + +## 你真的需要完全使用Linux吗? + +说到这里,其实我想借用知乎某位前辈的话来表达一下我的真实想法: “**Linux最好的地方在与开放自由,最大的毛病也是在这里。普通人没有能力去选择,也没有时间做选择。透明就一定好么?也有很多人喜欢被安排啊!**“ ([知乎 - 汉卿](https://www.zhihu.com/question/309704636)) + +就像我开头说过的: “我对Linux并不熟悉,这其中一部分原因是我使用的是对用户较为友好的发行版Ubuntu,另一部分原因是我仍然没有那么大的动力去学习Linux,即使它一直作为我的日常使用。” + +我完全使用Linux是为了学习和自由,我确实在Linux上感受到了自由,且学到了很多东西,但我却一直沉溺在这种使用Linux带来的满足感之中,并不能真正理解Linux给我们带来的到底是什么。 + +这次从Ubuntu切换到Debian的原因是我想尝试换个新的环境,但是当我花了3,4天后,我明白了:我只是呆在一个地方久了,想换个新地方而已,但老地方不一定坏,因为我都没怎么了解过这个老地方,就像当初我从Windows换到Linux那样,我都没有深入的了解过Windows就换了,那一段时间我还抱怨Windows的各种缺点,现在看来,非常可笑。 + + + +#### 结尾 + +一文把想说的话几乎都给说了,个人文笔有限,且本文主观意识太强,如果觉得本文不符合您的胃口,就当看个笑话吧。 + + +--- + +## 我使用Debian/Ubuntu时遇到的问题 + +**以下内容是我在Debian10 Buster下遇到的问题以及相关解决办法, +使用Ubuntu和Debian其他版本的同学也可借鉴。** + +PS:欢迎各位同学在此处写下你遇到的问题和解决办法。 + +### IDEA编辑Markdown预渲染问题 +这个问题花了我很长时间。 + +当我安装IDEA后,使用它编辑markdown文件的时候,就出现了如下图所示的情况: + +![Debian10下IDEA的Markdown预渲染问题](../../media/pictures/linux/Debian10下IDEA的Markdown预渲染问题.png) + +你可以看到右边渲染的画面明显有问题。刚开始的时候我一度怀疑是IDEA版本的问题, +于是我又安装IDEA其他版本,但也没有任何作用,这时我怀疑是显卡的原因: + +![我的电脑配置](../../media/pictures/linux/我的电脑配置.png) + +可以看到使用的是Intel的核显,于是当我查询相关资料,使用脚本将核显换为了独显,这里没留截图,当你换到独显后, +图形会显示独显的配置,使用nvidia-smi命令可以查看独显使用状态。 +于是我满怀期待的打开IDEA,但还是无济于事。当我以为真的是Debian的Bug的时候, +我又发现Bumblebee可以管理显卡,何不一试?于是我安装Bumblebee后,使用optirun命令启动IDEA,没想到啊, +还真是可以: + +![Debian10下IDEA的Markdown预渲染解决后](../../media/pictures/linux/Debian10下IDEA的Markdown预渲染解决后.png) + +我真的就很奇怪,同样是使用了独显,为什么optirun启动就可以正常显示。 +于是我后来又查询optirun是否开启了gpu加速,但很可惜,我并没有得到相关答案,不过这让我确定了这个问题出现在 +显卡上。如果有知道原因的同学,敬请告之,感激不尽。 + + +### wifi适配器找不到 +我猜(不确定)这个问题应该发生在大多数使用联想笔记本的同学的电脑上,不止Debian,且Ubuntu也有这个问题。 +当安装完系统后,我们打开设置会发现wifi一栏显示 “wifi适配器找不到” 此类的错误信息。 +这个问题的大概原因是:无线网络适配器被阻塞了,需要手动将电脑上的wifi开关打开,而在我的笔记本上并wifi开关, +所以可以猜测是联想网络驱动的问题。 +可以使用 rfkill list all命令查询你的wlan是否被阻塞了,没有此命令的同学可以使用 + +````text +sudo apt-get install rfkill +```` + +安装,当wlan显示Hard blocked: true , 就证明你的无线驱动被阻塞了。 +解决办法是将阻塞无限驱动的那个模块从内核中移除掉,直接在 /etc/modprobe.d +目录下编辑 blacklist.conf文件,其内容为: + +````text +blacklist ideapad_laptop +```` + +文件名不一定要与我的一致,但是要以.conf结尾。 +你可以将modprobe.d目录下的文件理解为黑名单文件, +当Linux启动时就不会加载conf文件指定的模块, +这里的 ideapad_laptop 就是我们需要移除的那个无线模块。 + +**后遗症: +当我们移除 ideapad_laptop 模块后,以后开机的时候,有时会出现 +蓝牙适配器找不到的情况,之前在Ubuntu上却并未发现这种问题, +看来Debian在驱动方面没有Ubuntu做的好,不过这也是可以理解的, +而且大多数时候还是可以正常使用的-_-。** + + +### XMind安装 +XMind是使用Java编写的,依赖于Openjdk8。所以在Linux上使用XMind, +首先需要有Openjdk8的环境。 +其次启动的时候需要编写Shell脚本来启动(不是唯一办法,但却是非常简单的办法),没想到吧,我也没想到, +这也是我趟过很多坑才玩出来的。 + +首先我们需要准备一张XMind的软件启动图片:XMind.png, +这个我已经放到[目录](https://github.com/guang19/framework-learning/tree/dev/img/linux) +下了,需要的同学请自取。 + +其次我们进入XMind_amd64目录下,32位系统的同学进入XMind_i386目录, +我们创建并编辑 start.sh 脚本,其内容为: + +````text +#!/bin/bash +cd /home/guang19/SDK/xmind/XMind_amd64 (这个路径为你的XMind脚本的路径) +./XMind +```` + +这个脚本的内容很简单吧,当启动脚本的时候,进入目录,直接启动XMind。 + +脚本写完后需要让它能够被执行,使用 + +````text +chmod +x start.sh +```` + +命令让start.sh可以被执行。 + +此时你可以尝试执行 ./start.sh 命令来启动XMind,启动成功的话, +就已经完成了99%了,如果启动不成功,可以再检测下前面的步骤是否有误。 + +如果以后你只想用Shell启动XMind的话,那么到此也就为止了,连上面所说的图片都不需要了。 +如果你想更方便的启动的话,那么就需要创建桌面文件启动。 +在Debian/Ubuntu下,你所看到的桌面文件,都存储在 /usr/share/applications +目录下面(也有的在.local/share/applications目录下),这个目录下文件全是以.desktop结尾。 +我们现在就需要在这个目录下创建xmind.desktop文件(名字可以不叫xmind)。 + +其内容为: + +````text +[Desktop Entry] +Encoding=UTF-8 +Name=XMind +Type=Application +Exec=sh /home/guang19/SDK/xmind/XMind_amd64/start.sh +Icon=/home/guang19/SDK/xmind/XMind.png +```` + +我们暂时只需要理解Icon和Exec属性。 +Icon就是你在桌面上看到的应用的图标,把Icon的路径改为你XMind.png的路径就行了。 +再看Exec属性,当我们在桌面上点击XMind的图标的时候,就会执行Exec对应的命令或脚本, +我们把Exec改为start.sh文件的路径就行了,别掉了sh命令,因为start.sh是脚本, +需要sh命令启动。 + +以上步骤完成,保存desktop文件后,你就可以在桌面上看到XMind应用了。 + + +### Fcitx候选框的定位问题 +这个问题贴一张我处境的截图就明白了: + +![Fcitx候选框定位问题](../../media/pictures/linux/Fcitx候选框定位问题.png) + +可以看到我的光标定位在第207行,但是我输入法的候选框停留在IDEA的左下角。 +为什么我要说停留在IDEA的左下角?因为就目前我的使用而言,这个问题只在IDEA下存在, +不仅是Debian,Ubuntu也存在这种问题,我个人认为这应该是IDEA的问题, +查到的相关文章大部分都是说Swing的问题,看来这个问题还真是比较困难了。 +如果有同学知道解决办法,还请不吝分享,非常感谢。 \ No newline at end of file diff --git a/docs/questions/java-big-data.md b/docs/questions/java-big-data.md index 2ede8233..6bea7d6f 100644 --- a/docs/questions/java-big-data.md +++ b/docs/questions/java-big-data.md @@ -1,9 +1,3 @@ -写这篇文章主要是为了回答球友的一个提问,提问如下: - - - -刚好自己对这方面有一丁点的见解,所以回答一下这位老哥的问题,如果能够解决他的问题,我也会很高兴。下面仅仅代表个人一件,环境大家批评指正与完善! - 先说一下自己的经历,大学的时候我从大二开始学习 Java ,然后学了大半年多的安卓。之后就开始学习 Java 后台,学习完 Java 后台一些常用的知识比如 Java基础、Spring、MyBatis等等之后。因为感觉大数据领域发展也挺不错的,所以就接触了一些大数据方面的知识比如当时大数据领域的霸主 Hadoop 。 > 我当时学习了很多比较古老的技术比如现在基本不会用的 JSP、Struts2等等。另外,我 diff --git a/docs/questions/java-learning-path-and-methods.md b/docs/questions/java-learning-path-and-methods.md index fcb01584..87f7f19b 100644 --- a/docs/questions/java-learning-path-and-methods.md +++ b/docs/questions/java-learning-path-and-methods.md @@ -1,42 +1,134 @@ 到目前为止,我觉得不管是在公众号后台、知乎还是微信上面我被问的做多的就是:“大佬,有没有 Java 学习路线和方法”。所以,这部分单独就自己的学习经历来说点自己的看法。 -## 前言 - -大一的时候,我仅仅接触过 C 语言,对 C 语言的掌握程度仅仅是可以完成老师布置的课后习题。那时候我的主要的精力都放在了参加各种课外活动,跟着一个很不错的社团尝试了很多我之前从未尝试过的事情:露营、户外烧烤、公交车演讲、环跑古城墙、徒步旅行、异地求生、圣诞节卖苹果等等。 - -到了大二我才接触到 HTML、CSS、JS、Java、Linux、PHP 这些名词。最开始接触 Java 的时候因为工作的需要我选择的安卓方向,我自己是在学习了大概 3 个月的安卓方向的知识后才转向 Java 后台方向的。最开始自己学习的时候,走了一些弯路,但是总体路线相对来说还是没问题的。我读的第一本 Java Web 方向的书籍是《Java Web 整合开发王者归来》,这本书我现在已经不推荐别人看了,一是内容太冗杂,二是年代比较久远导致很多东西在现在都不适用了。 - -很多人在学完 Java 基础之后,不知道后面该如何进行下一步地进行学习,或者不知道如何去学习。如何系统地学习 Java 一直是困扰着很多新手或者期待在 Java 方向进阶的小伙伴的一个问题。我也在知乎上回答了好几个类似的问题,我觉得既然很多人都需要这方面的指导,那我就写一篇自己对于如何系统学习 Java 后端的看法。刚好关注公众号的很多朋友都是学 Java 不太久的,希望这篇文章对学习 Java 的朋友能有一点启示作用。 - -由于我个人能力有限,下面的学习路线以及方法推荐一定还有很多欠缺的地方。欢迎有想法的朋友在评论区说一下自己的看法。本文比较适合刚入门或者想打好 Java 基础的朋友,比较基础。 - -## 学习路线以及方法推荐 - **下面的学习路线以及方法是笔主根据个人学习经历总结改进后得出,我相信照着这条学习路线来你的学习效率会非常高。** -学习某个知识点的过程中如果不知道看什么书的话,可以查看这篇文章 :[Java 学习必备书籍推荐终极版!](https://github.com/Snailclimb/JavaGuide/blob/master/docs/data/java-recommended-books.md)。 +学习某个知识点的过程中如果不知道看什么书的话,可以查看这篇文章 :[Java 学习必备书籍推荐终极版!](https://github.com/Snailclimb/JavaGuide/blob/master/docs/books/java.md)。 另外,很重要的一点:**建议使用 Intellij IDEA 进行编码,可以单独抽点时间学习 Intellij IDEA 的使用。** **下面提到的一些视频,[公众号](#公众号 "公众号")后台回复关键“1”即可获取!** +先说一个初学者很容易犯的错误:上来就通过项目学习。 + +**很多初学者上来就像通过做项目学习,特别是在公司,我觉得这个是不太可取的。** 如果的 Java基础或者 Spring Boot 基础不好的话,建议自己先提前学习一下之后再开始看视频或者通过其他方式做项目。 **还有点事,我不知道为什么大家都会说边跟着项目边学习做的话效果最好,我觉得这个要加一个前提是你对这门技术有基本的了解或者说你对编程有了一定的了解。** + +**关于如何学习且听我从一个电商系统网站的创建来说起。假如我们要创建一个基于Java的分布式/微服务电商系统的话,我们可以按照下面的学习路线来做:** + +> 首选第一步我们肯定是要从 Java 基础来学习的(如果你没有计算机基础知识的话推荐看一下《计算机导论》这类入门书籍)。 + ### step 1:Java 基础 **《Java 核心技术卷 1/2》** 和 **《Head First Java》** 这两本书在我看来都是入门 Java 的很不错的书籍 (**《Java 核心技术卷 1/2》** 知识点更全,我更推荐这本书),我倒是觉得 **《Java 编程思想》** 有点属于新手劝退书的意思,慎看,建议有点基础后再看。你也可以边看视频边看书学习(黑马、尚硅谷、慕课网的视频都还行)。对于 Java8 新特性的东西,我建议你基础学好之后可以看一下,暂时看不太明白也没关系,后面抽时间再回过头来看。 看完之后,你可以用自己学的东西实现一个简单的 Java 程序,也可以尝试用 Java 解决一些编程问题,以此来将自己学到的东西付诸于实践。 +**不太建议学习 Java基础的时候通过做游戏来巩固。为什么培训班喜欢通过这种方式呢?说白点就是为了找到你的G点(不好意思开车了哈)。新手学习完Java基础后做游戏一般是不太现实的,还不如找一些简单的程序问题解决一下比如简单的算法题。** + **记得多总结!打好基础!把自己重要的东西都记录下来。** API 文档放在自己可以看到的地方,以备自己可以随时查阅。为了能让自己写出更优秀的代码,**《Effective Java》**、**《重构》** 这两本书没事也可以看看。 -学习完之后可以看一下下面这几篇文章,检查一下自己的学习情况: +另外,学习完之后可以看一下下面这几篇文章,检查一下自己的学习情况。这几篇文章不是我吹,可能是全网最具价值的 Java 基础知识总结,毕竟是在我的 JavaGuide开源的,经过了各路大佬以及我的不断完善。 -- **[Java 基础知识回顾](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Java%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md)** -- **[Java 基础知识疑难点/易错点](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Java%E7%96%91%E9%9A%BE%E7%82%B9.md)** -- **[一些重要的 Java 程序设计题](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Java%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E9%A2%98.md)** +这几篇文章总结的知识点在 Java 后端面试中的出场率也非常高哦! -检测一下自己的掌握情况,这 34 个问题都时 Java 中比较重要的知识点,最重要的是在 Java 后端面试中的出场率非常高。 +1. [**Java 基础知识**](https://snailclimb.gitee.io/javaguide/#/docs/java/Java基础知识) +2. [**Java 基础知识疑难点/易错点**](https://snailclimb.gitee.io/javaguide/#/docs/java/Java疑难点) +3. [【加餐】一些重要的Java程序设计题](https://snailclimb.gitee.io/javaguide/#/docs/java/Java程序设计题) +4. [【选看】J2EE 基础知识](https://snailclimb.gitee.io/javaguide/#/docs/java/J2EE基础知识) -### step 2:多线程的简单使用 +> 我们的网站需要运行在“操作系统”之上(一般是部署在Linux系统),并且我们与网站的每次交互都需要经过“网络”,需要经历三次握手和四次挥手才能简历连接,需要HTTP才能发出请求已经拿到网站后台的相应。所以第二步,我推荐可以适当花时间看一下 **操作系统与计算机网络 方面的知识。** 但是,不做强求!你抽时间一定要补上就行! + +### step 2(可选):操作系统与计算机网络 + +操作系统这方面我觉得掌握操作系统的基础知识和 Linux 的常用命令就行以及一些重要概念就行了。 + +关于操作系统的话,我没有什么操作系统方面的书籍可以推荐,因为我自己也没认真看过几本。因为操作系统比较枯燥的原因,我建议这部分看先看视频学可能会比较好一点。我推荐一个 Github 上开源的哈工大《操作系统》课程给大家吧!地址:https://github.com/hoverwinter/HIT-OSLab 。 + +另外,对于 Linux 我们要掌握基本的使用就需要对一些常用命令非常熟悉比如:目录切换命令、目录操作命令、文件的操作命令、压缩或者解压文件的命令等等。推荐一个 Github 上学习 Linux 的开源文档:[《Java 程序员眼中的 Linux》](https://github.com/judasn/Linux-Tutorial "《Java 程序员眼中的 Linux》") + +计算机网络方面的学习,我觉得掌握基本的知识就行了,不需要太深究,一般面试对这方面要求也不高,毕竟不是专门做网络的。推荐 **《网络是怎样连接的》** 、**《图解 HTTP》** 这两本书来看,这两本书都属于比较有趣易懂的类型,也适合没有基础的人来看。 + +> 我们写程序的都知道一个公式叫做 **“程序设计 = 算法 + 数据结构”**。我们想让我们的网站的地盘更加牢固的话,我觉得数据结构与算法还是很有必要学习的。所以第三步,我推荐可以适当花时间看一下 **数据结构与算法** 但是,同样不做强求!你抽时间一定要补上就行! + +### step 3(可选):数据结构与算法 + +如果你想进入大厂的话,我推荐你在学习完 Java基础之后,就开始每天抽出一点时间来学习算法和数据结构。为了提高自己的编程能力,你也可以坚持刷 **[Leetcode](https://leetcode-cn.com "Leetcode")**。就目前国内外的大厂面试来说,刷 Leetcode 可以说已经成了不得不走的一条路。 + +对于想要入门算法和数据结构的朋友,建议看这两本书 **《算法图解》** 和 **《大话数据结构》**,这两本书虽然算不上很经典的书籍,但是比较有趣,对于刚入门算法和数据结构的朋友非常友好。**《算法导论》** 非常经典,但是对于刚入门的就不那么友好了。 + +另外,还有一本非常赞的算法书推荐给各位,这本书的名字就叫 **《算法》**,书中的代码都是用 Java 语言编写。这本书的优点太多太多比如它的讲解基础而全面、对阅读者比较友好等等。我觉得这本书唯一的缺点就是太厚了 (小声 BB,可能和作者讲解某些知识点的时候有点啰嗦有关)。除了这本书之外,**《剑指 offer》** 、**《编程珠玑》** 、**《编程之美》** 这三本书都被很多大佬推荐过了,对于算法面试非常有帮助。**《算法之美》** 这本书也非常不错,非常适合闲暇的时候看。 + +> 我们网站的页面搭建需要前端的知识,我们前端也后端的交互也需要前端的知识。所以第四步,我推荐你可以了解一下前端知识,不过不需要学的太精通。自己对与前端知识有了基本的了解之后通过 + +### step 4:前端知识 + +这一步主要是学习前端基础 (HTML、CSS、JavaScript),当然 BootStrap、Layui 等等比较简单的前端框架你也可以了解一下。网上有很多这方面资源,我只推荐一个大部分初学这些知识都会看的网站:http://www.w3school.com.cn/ ,这个网站用来回顾知识也很不错 。推荐先把 HTML、CSS、JS 的基础知识过一遍,然后通过一个实际的前端项目来巩固。 + +另外,没记错的话 Spring Boot官方推荐的是模板引擎是 thymeleaf ,这东西和HTML很像,了解了基本语法之后很容易上手。 结合layui,booystrap这些框架的话也能做成比较美观的页面。开发一些简单的页面比如一个后端项目就是为了做个简单的前端页面做某些操作的话直接用thymeleaf就好。 + +现在都是前后端分离,就目前来看大部分项目都优先选择 React、Angular、Vue 这些厉害的框架来开发,这些框架的上手要求要高一些。如果你想往全栈方向发展的话(笔主目前的方向,我用 React 在公司做过两个小型项目),建议先把 JS 基础打好,然后再选择 React、Angular、Vue 其中的一个来认真学习一下。国内使用 Vue 比较多一点,国外一般用的是 React 和 Angular。 + + 如何和后端交互呢?一般使用在 React、Vue这些框架的时候使用Axios比较多。 + +> 我们网站的数据比如用户信息、订单信息都需要存储,所以,下一步我推荐你学习 MySQl这个被广泛运用于各大网站的数据库。不光要学会如何写 sql 语句,更好的是还要搞清诸如索引这类重要的概念。 + +### step 5:MySQL + +学习 MySQL 的基本使用,基本的增删改查,SQL 命令,索引、存储过程这些都学一下吧!推荐书籍 **《SQL 基础教程(第 2 版)》**(入门级)、**《高性能 MySQL : 第 3 版》**(进阶)、**《MySQL 必知必会》**。 + +下面这些 MySQL 相关的文章强烈推荐你看看: + +1. [**【推荐】MySQL/数据库 知识点总结**](https://snailclimb.gitee.io/javaguide/#/docs/database/MySQL) +2. [**阿里巴巴开发手册数据库部分的一些最佳实践**](https://snailclimb.gitee.io/javaguide/#/docs/database/阿里巴巴开发手册数据库部分的一些最佳实践) +3. [**一千行MySQL学习笔记**](https://snailclimb.gitee.io/javaguide/#/docs/database/一千行MySQL命令) +4. [MySQL高性能优化规范建议](https://snailclimb.gitee.io/javaguide/#/docs/database/MySQL高性能优化规范建议) +5. [数据库索引总结](https://snailclimb.gitee.io/javaguide/#/docs/database/MySQL Index) +6. [事务隔离级别(图文详解)](https://snailclimb.gitee.io/javaguide/#/docs/database/事务隔离级别(图文详解)) +7. [一条SQL语句在MySQL中如何执行的](https://snailclimb.gitee.io/javaguide/#/docs/database/一条sql语句在mysql中如何执行的) + +> 正式开发之前我们还要一些准备工具比如熟悉你的ide,熟悉Maven来帮助我们引入相关jar依赖,使用 Docker来帮助我们安装常用的软件。 + +### step 6:常用工具 + +非常重要!非常重要!特别是 Git和 Docker。 + +1. **IDEA**:熟悉基本操作以及常用快捷。 +2. **Maven** :建议学习常用框架之前可以提前花半天时间学习一下**Maven**的使用。(到处找 Jar 包,下载 Jar 包是真的麻烦费事,使用 Maven 可以为你省很多事情)。 +3. **Git** :基本的 Git 技能也是必备的,试着在学习的过程中将自己的代码托管在 Github 上。([Git 入门](https://github.com/Snailclimb/JavaGuide/blob/master/docs/tools/Git.md)) +4. **Docker** :学着用 Docker 安装学习中需要用到的软件比如 MySQL ,这样方便很多,可以为你节省不少时间。([Docker 入门](https://github.com/Snailclimb/JavaGuide/blob/master/docs/tools/Docker.md)) + +> 利用常用框架可以极大程度简化我们的开发工作。学习完了常用工具之后,我们就可以开始常用框架的学习啦! + +### step 7:常用框架 + +学习 Struts2(可不用学)、**Spring**、**SpringMVC**、**Hibernate**、**Mybatis**、**shiro** 等框架的使用, (可选) 熟悉 **Spring 原理**(大厂面试必备),然后很有必要学习一下 **SpringBoot** ,**学好 SpringBoot 真的很重要**。很多公司对于应届生都是直接上手 **SpringBoot**,不过如果时间允许的话,我还是推荐你把 **Spring**、**SpringMVC** 提前学一下。 + +关于 SpringBoot ,推荐看一下笔主开源的 [Spring Boot 教程](https://github.com/Snailclimb/springboot-guide "Spring Boot 教程") (SpringBoot 核心知识点总结。 基于 Spring Boot 2.19+)。 + +**Spring 真的很重要!** 一定要搞懂 AOP 和 IOC 这两个概念。Spring 中 bean 的作用域与生命周期、SpringMVC 工作原理详解等等知识点都是非常重要的,一定要搞懂。 + +推荐看文档+视频结合的方式,中途配合实战来学习,学习期间可以多看看 JavaGuide 对于[常用框架的总结](https://github.com/Snailclimb/JavaGuide#%E5%B8%B8%E7%94%A8%E6%A1%86%E6%9E%B6 "常用框架的总结")。 + +**另外,都 2019 年了,咱千万不要再学 JSP 了好不?** + +推荐阅读: + +#### [Spring](https://snailclimb.gitee.io/javaguide/#/?id=spring) + +1. [Spring 学习与面试](https://snailclimb.gitee.io/javaguide/#/docs/system-design/framework/spring/Spring) +2. **[Spring 常见问题总结](https://snailclimb.gitee.io/javaguide/#/docs/system-design/framework/spring/SpringInterviewQuestions)** +3. [Spring中 Bean 的作用域与生命周期](https://snailclimb.gitee.io/javaguide/#/docs/system-design/framework/spring/SpringBean) +4. [SpringMVC 工作原理详解](https://snailclimb.gitee.io/javaguide/#/docs/system-design/framework/spring/SpringMVC-Principle) +5. [Spring中都用到了那些设计模式?](https://snailclimb.gitee.io/javaguide/#/docs/system-design/framework/spring/Spring-Design-Patterns) + +#### [SpringBoot](https://snailclimb.gitee.io/javaguide/#/?id=springboot) + +- **[SpringBoot 指南/常见面试题总结](https://github.com/Snailclimb/springboot-guide)** + +#### [MyBatis](https://snailclimb.gitee.io/javaguide/#/?id=mybatis) + +- [MyBatis常见面试题总结](https://snailclimb.gitee.io/javaguide/#/docs/system-design/framework/mybatis/mybatis-interview) + +### step 8:多线程的简单使用 多线程这部分内容可能会比较难以理解和上手,前期可以先简单地了解一下基础,到了后面有精力和能力后再回来仔细看。推荐 **《Java 并发编程之美》** 或者 **《实战 Java 高并发程序设计》** 这两本书。我目前也在重构一份我之前写的多线程学习指南,后面会更新在公众号里面。 @@ -64,86 +156,56 @@ 5. Atomic 原子类: ① 介绍一下 Atomic 原子类;② JUC 包中的原子类是哪 4 类?;③ 讲讲 AtomicInteger 的使用;④ 能不能给我简单介绍一下 AtomicInteger 类的原理。 6. AQS :① 简介;② 原理;③ AQS 常用组件。 -另外,推荐看一下下面这几篇文章: +### **step 9:分布式** -- **[Java 并发基础常见面试题总结](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md)** -- **[Java 并发进阶常见面试题总结](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md)** -- [并发容器总结](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8%E6%80%BB%E7%BB%93.md) -- [乐观锁与悲观锁](https://github.com/Snailclimb/JavaGuide/blob/master/docs/essential-content-for-interview/%E9%9D%A2%E8%AF%95%E5%BF%85%E5%A4%87%E4%B9%8B%E4%B9%90%E8%A7%82%E9%94%81%E4%B8%8E%E6%82%B2%E8%A7%82%E9%94%81.md) -- [JUC 中的 Atomic 原子类总结](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/Atomic.md) -- [AQS 原理以及 AQS 同步组件总结](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/AQS.md) +1. 学习 **Dubbo、Zookeeper来实现简单的分布式服务** +2. **学习 Redis** 来提高访问速度,减少对 MySQL数据库的依赖; +3. **学习 Elasticsearch** 的使用,来为我们的网站增加搜索功能 +4. 学习常见的**消息队列**(比如**RabbitMQ、Kafka**)来解耦我们的服务(ActiveMq不要学了,已经淘汰); +5. ...... -### step 3(可选):操作系统与计算机网络 - -操作系统这方面我觉得掌握操作系统的基础知识和 Linux 的常用命令就行以及一些重要概念就行了。 - -关于操作系统的话,我没有什么操作系统方面的书籍可以推荐,因为我自己也没认真看过几本。因为操作系统比较枯燥的原因,我建议这部分看先看视频学可能会比较好一点。我推荐一个 Github 上开源的哈工大《操作系统》课程给大家吧!地址:https://github.com/hoverwinter/HIT-OSLab 。 - -另外,对于 Linux 我们要掌握基本的使用就需要对一些常用命令非常熟悉比如:目录切换命令、目录操作命令、文件的操作命令、压缩或者解压文件的命令等等。推荐一个 Github 上学习 Linux 的开源文档:[《Java 程序员眼中的 Linux》](https://github.com/judasn/Linux-Tutorial "《Java 程序员眼中的 Linux》") - -计算机网络方面的学习,我觉得掌握基本的知识就行了,不需要太深究,一般面试对这方面要求也不高,毕竟不是专门做网络的。推荐 **《网络是怎样连接的》** 、**《图解 HTTP》** 这两本书来看,这两本书都属于比较有趣易懂的类型,也适合没有基础的人来看。 - -### step 4(可选):数据结构与算法 - -如果你想进入大厂的话,我推荐你在学习完 Java 基础或者多线程之后,就开始每天抽出一点时间来学习算法和数据结构。为了提高自己的编程能力,你也可以坚持刷 **[Leetcode](https://leetcode-cn.com "Leetcode")**。就目前国内外的大厂面试来说,刷 Leetcode 可以说已经成了不得不走的一条路。 - -对于想要入门算法和数据结构的朋友,建议看这两本书 **《算法图解》** 和 **《大话数据结构》**,这两本书虽然算不上很经典的书籍,但是比较有趣,对于刚入门算法和数据结构的朋友非常友好。**《算法导论》** 非常经典,但是对于刚入门的就不那么友好了。 - -另外,还有一本非常赞的算法书推荐给各位,这本书的名字就叫 **《算法》**,书中的代码都是用 Java 语言编写。这本书的优点太多太多比如它的讲解基础而全面、对阅读者比较友好等等。我觉得这本书唯一的缺点就是太厚了 (小声 BB,可能和作者讲解某些知识点的时候有点啰嗦有关)。除了这本书之外,**《剑指 offer》** 、**《编程珠玑》** 、**《编程之美》** 这三本书都被很多大佬推荐过了,对于算法面试非常有帮助。**《算法之美》** 这本书也非常不错,非常适合闲暇的时候看。 - -### step 5:前端知识 - -这一步主要是学习前端基础 (HTML、CSS、JavaScript),当然 BootStrap、Layui 等等比较简单的前端框架你也可以了解一下。网上有很多这方面资源,我只推荐一个大部分初学这些知识都会看的网站:http://www.w3school.com.cn/ ,这个网站用来回顾知识也很不错 。推荐先把 HTML、CSS、JS 的基础知识过一遍,然后通过一个实际的前端项目来巩固。 - -现在都是前后端分离,就目前来看大部分项目都优先选择 React、Angular、Vue 这些厉害的框架来开发。如果你想往全栈方向发展的话(笔主目前的方向,我用 React 在公司做过两个小型项目),建议先把 JS 基础打好,然后再选择 React、Angular、Vue 其中的一个来认真学习一下。国内使用 Vue 比较多一点,国外一般用的是 React 和 Angular。 - -### step 5:MySQL - -学习 MySQL 的基本使用,基本的增删改查,SQL 命令,索引、存储过程这些都学一下吧!推荐书籍 **《SQL 基础教程(第 2 版)》**(入门级)、**《高性能 MySQL : 第 3 版》**(进阶)、**《MySQL 必知必会》**。 - -下面这些 MySQL 相关的文章强烈推荐你看看: - -- **[【推荐】MySQL/数据库 知识点总结](https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/MySQL.md)** -- **[阿里巴巴开发手册数据库部分的一些最佳实践](https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%E6%95%B0%E6%8D%AE%E5%BA%93%E9%83%A8%E5%88%86%E7%9A%84%E4%B8%80%E4%BA%9B%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.md)** -- **[一千行 MySQL 学习笔记](https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/%E4%B8%80%E5%8D%83%E8%A1%8CMySQL%E5%91%BD%E4%BB%A4.md)** -- [MySQL 高性能优化规范建议](https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/MySQL%E9%AB%98%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E8%A7%84%E8%8C%83%E5%BB%BA%E8%AE%AE.md) -- [数据库索引总结](https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/MySQL%20Index.md) -- [事务隔离级别(图文详解)](https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB(%E5%9B%BE%E6%96%87%E8%AF%A6%E8%A7%A3).md) -- [一条 SQL 语句在 MySQL 中如何执行的](https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/%E4%B8%80%E6%9D%A1sql%E8%AF%AD%E5%8F%A5%E5%9C%A8mysql%E4%B8%AD%E5%A6%82%E4%BD%95%E6%89%A7%E8%A1%8C%E7%9A%84.md) - -### step 6:常用工具 - -1. **Maven** :建议学习常用框架之前可以提前花半天时间学习一下**Maven**的使用。(到处找 Jar 包,下载 Jar 包是真的麻烦费事,使用 Maven 可以为你省很多事情)。 -2. **Git** :基本的 Git 技能也是必备的,试着在学习的过程中将自己的代码托管在 Github 上。([Git 入门](https://github.com/Snailclimb/JavaGuide/blob/master/docs/tools/Git.md)) -3. **Docker** :学着用 Docker 安装学习中需要用到的软件比如 MySQL ,这样方便很多,可以为你节省不少时间。([Docker 入门](https://github.com/Snailclimb/JavaGuide/blob/master/docs/tools/Docker.md)) - -### step 7:常用框架 - -学习 Struts2(可不用学)、**Spring**、**SpringMVC**、**Hibernate**、**Mybatis**、**shiro** 等框架的使用, (可选) 熟悉 **Spring 原理**(大厂面试必备),然后很有必要学习一下 **SpringBoot** ,**学好 SpringBoot 真的很重要**。很多公司对于应届生都是直接上手 **SpringBoot**,不过如果时间允许的话,我还是推荐你把 **Spring**、**SpringMVC** 提前学一下。 - -关于 SpringBoot ,推荐看一下笔主开源的 [Spring Boot 教程](https://github.com/Snailclimb/springboot-guide "Spring Boot 教程") (SpringBoot 核心知识点总结。 基于 Spring Boot 2.19+)。 - -**Spring 真的很重要!** 一定要搞懂 AOP 和 IOC 这两个概念。Spring 中 bean 的作用域与生命周期、SpringMVC 工作原理详解等等知识点都是非常重要的,一定要搞懂。 - -推荐看文档+视频结合的方式,中途配合实战来学习,学习期间可以多看看 JavaGuide 对于[常用框架的总结](https://github.com/Snailclimb/JavaGuide#%E5%B8%B8%E7%94%A8%E6%A1%86%E6%9E%B6 "常用框架的总结")。 - -**另外,都 2019 年了,咱千万不要再学 JSP 了好不?** - -### step 8:高性能网站架构 - -学习 **Dubbo、Zookeeper**、常见的**消息队列**(比如**ActiveMq、RabbitMQ**)、**Redis** 、**Elasticsearch** 的使用。 - -我当时学习这些东西的时候是通过黑马视频最后一个分布式项目来学的,我的这种方式也是很多人普遍采用和接受的方式。我觉得应该是掌握这些知识点比较好的一种方式了,另外,**推荐边看视频边自己做,遇到不懂的知识点要及时查阅网上博客和相关书籍,这样学习效果更好。** +到了这一步你应该是有基础的一个 Java程序员了,我推荐你可以通过一个分布式项目来学习。觉得应该是掌握这些知识点比较好的一种方式了,另外,**推荐边看视频边自己做,遇到不懂的知识点要及时查阅网上博客和相关书籍,这样学习效果更好。** **一定要学会拓展知识,养成自主学习的意识。** 黑马项目对这些知识点的介绍都比较蜻蜓点水。 -### step 9:其他 +> 继续深入学习的话,我们要了解Netty、JVM这些东西。 + +### step 10:深入学习 可以再回来看一下多线程方面的知识,还可以利用业余时间学习一下 **[NIO](https://github.com/Snailclimb/JavaGuide#io "NIO")** 和 **Netty** ,这样简历上也可以多点东西。如果想去大厂,**[JVM](https://github.com/Snailclimb/JavaGuide#jvm "JVM")** 的一些知识也是必学的(**Java 内存区域、虚拟机垃圾算法、虚拟垃圾收集器、JVM 内存管理**)推荐《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(最新第二版》和《实战 Java 虚拟机》,如果嫌看书麻烦的话,你也可以看我整理的文档。 另外,现在微服务特别火,很多公司在面试也明确要求需要微服务方面的知识。如果有精力的话可以去学一下 SpringCloud 生态系统微服务方面的东西。 -## 总结 +> **微服务的概念庞大,技术种类也很多,但是目前大型互联网公司广泛采用的,**实话实话这些东西我不在行,自己没有真实做过微服务的项目。不过下面是我自己总结的一些关于微服务比价重要的知识,选学。 + +### step 11:微服务 + +这部分太多了,选择性学习。 + +相关技术: + +1. **网关** :kong,soul; +2. **分布式调用链**: SkyWalking、Zipkin +3. **日志系统:** Kibana +4. ...... + +Spring Cloud 相关: + +1. Eureka:服务注册与发现; +2. Ribbon:负载均衡; +3. Hytrix :熔断; +4. Zuul :网关; +5. Spring Cloud Config:配置中心; + +Spring Cloud Alibaba也是很值得学习的: + +1. **[Sentinel](https://github.com/alibaba/Sentinel "Sentinel")** :A lightweight powerful flow control component enabling reliability and monitoring for microservices. (轻量级的流量控制、熔断降级 Java 库)。 +2. **[dubbo](https://github.com/apache/dubbo "dubbo")** :Apache Dubbo 是一个基于 Java 的高性能开源 RPC 框架。 +3. **[nacos](https://github.com/alibaba/nacos "nacos")** :Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos 可以作为 Dubbo 的注册中心来使用。 +4. **[seata](https://github.com/seata/seata "seata")** : Seata 是一种易于使用,高性能,基于 Java 的开源分布式事务解决方案。 +5. **[RocketMQ](https://github.com/apache/rocketmq "RocketMQ")** :阿里巴巴开源的一款高性能、高吞吐量的分布式消息中间件。 + +### 总结 我上面主要概括一下每一步要学习的内容,对学习规划有一个建议。知道要学什么之后,如何去学呢?我觉得学习每个知识点可以考虑这样去入手: diff --git a/docs/system-design/authority-certification/basis-of-authority-certification.md b/docs/system-design/authority-certification/basis-of-authority-certification.md index 896353f6..8441bc0f 100644 --- a/docs/system-design/authority-certification/basis-of-authority-certification.md +++ b/docs/system-design/authority-certification/basis-of-authority-certification.md @@ -4,8 +4,13 @@ 说简单点就是: -- **认证 (Authentication):** 你是谁。 -- **授权 (Authorization):** 你有权限干什么。 +**认证 (Authentication):** 你是谁。 + + + +**授权 (Authorization):** 你有权限干什么。 + + 稍微正式点(啰嗦点)的说法就是: @@ -16,6 +21,8 @@ ## 2. 什么是Cookie ? Cookie的作用是什么?如何在服务端使用 Cookie ? +![](../pictures/cookie-sessionId.png) + ### 2.1 什么是Cookie ? Cookie的作用是什么? Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。 @@ -28,7 +35,7 @@ Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但 2. 使用Cookie 保存 session 或者 token ,向后端发送请求的时候带上 Cookie,这样后端就能取到session或者token了。这样就能记录用户当前的状态了,因为 HTTP 协议是无状态的。 3. Cookie 还可以用来记录和分析用户行为。举个简单的例子你在网上购物的时候,因为HTTP协议是没有状态的,如果服务器想要获取你在某个页面的停留状态或者看了哪些商品,一种常用的实现方式就是将这些信息存放在Cookie -### 2.2 如何能在 服务端使用 Cookie 呢? +### 2.2 如何在服务端使用 Cookie 呢? 这部分内容参考:https://attacomsian.com/blog/cookies-spring-boot,更多如何在Spring Boot中使用Cookie 的内容可以查看这篇文章。 @@ -83,7 +90,7 @@ public String readAllCookies(HttpServletRequest request) { 很多时候我们都是通过 SessionID 来实现特定的用户,SessionID 一般会选择存放在 Redis 中。举个例子:用户成功登陆系统,然后返回给客户端具有 SessionID 的 Cookie,当用户向后端发起请求的时候会把 SessionID 带上,这样后端就知道你的身份状态了。关于这种认证方式更详细的过程如下: -![Session Based Authentication flow](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/Session-Based-Authentication-flow.png) +![Session Based Authentication flow](../pictures/Session-Based-Authentication-flow.png) 1. 用户向服务器发送用户名和密码用于登陆系统。 2. 服务器验证通过后,服务器为用户创建一个 Session,并将 Session信息存储 起来。 @@ -91,13 +98,56 @@ public String readAllCookies(HttpServletRequest request) { 4. 当用户保持登录状态时,Cookie 将与每个后续请求一起被发送出去。 5. 服务器可以将存储在 Cookie 上的 Session ID 与存储在内存中或者数据库中的 Session 信息进行比较,以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态。 +使用 Session 的时候需要注意下面几个点: + +1. 依赖Session的关键业务一定要确保客户端开启了Cookie。 +2. 注意Session的过期时间 + +花了个图简单总结了一下Session认证涉及的一些东西。 + + + 另外,Spring Session提供了一种跨多个应用程序或实例管理用户会话信息的机制。如果想详细了解可以查看下面几篇很不错的文章: - [Getting Started with Spring Session](https://codeboje.de/spring-session-tutorial/) - [Guide to Spring Session](https://www.baeldung.com/spring-session) - [Sticky Sessions with Spring Session & Redis](https://medium.com/@gvnix/sticky-sessions-with-spring-session-redis-bdc6f7438cc3) -## 4. 什么是 Token?什么是 JWT?如何基于Token进行身份验证? +## 4.如果没有Cookie的话Session还能用吗? + +这是一道经典的面试题! + +一般是通过 Cookie 来保存 SessionID ,假如你使用了 Cookie 保存 SessionID的方案的话, 如果客户端禁用了Cookie,那么Seesion就无法正常工作。 + +但是,并不是没有 Cookie 之后就不能用 Session 了,比如你可以将SessionID放在请求的 url 里面`https://javaguide.cn/?session_id=xxx` 。这种方案的话可行,但是安全性和用户体验感降低。当然,为了你也可以对 SessionID 进行一次加密之后再传入后端。 + +## 5.为什么Cookie 无法防止CSRF攻击,而token可以? + +**CSRF(Cross Site Request Forgery)**一般被翻译为 **跨站请求伪造** 。那么什么是 **跨站请求伪造** 呢?说简单用你的身份去发送一些对你不友好的请求。举个简单的例子: + +小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了10000元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。 + +``` +科学理财,年盈利率过万 +``` + +上面也提到过,进行Session 认证的时候,我们一般使用 Cookie 来存储 SessionId,当我们登陆后后端生成一个SessionId放在Cookie中返回给客户端,服务端通过Redis或者其他存储工具记录保存着这个Sessionid,客户端登录以后每次请求都会带上这个SessionId,服务端通过这个SessionId来标示你这个人。如果别人通过 cookie拿到了 SessionId 后就可以代替你的身份访问系统了。 + + Session 认证中 Cookie 中的 SessionId是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。 + +但是,我们使用 token 的话就不会存在这个问题,在我们登录成功获得 token 之后,一般会选择存放在 local storage 中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 token,这样就不会出现 CSRF 漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 token 的,所以这个请求将是非法的。 + +需要注意的是不论是 Cookie 还是 token 都无法避免跨站脚本攻击(Cross Site Scripting)XSS。 + +> 跨站脚本攻击(Cross Site Scripting)缩写为 CSS 但这会与层叠样式表(Cascading Style Sheets,CSS)的缩写混淆。因此,有人将跨站脚本攻击缩写为XSS。 + +XSS中攻击者会用各种方式将恶意代码注入到其他用户的页面中。就可以通过脚本盗用信息比如cookie。 + +推荐阅读: + +1. [如何防止CSRF攻击?—美团技术团队](https://tech.meituan.com/2018/10/11/fe-security-csrf.html) + +## 6. 什么是 Token?什么是 JWT?如何基于Token进行身份验证? 我们在上一个问题中探讨了使用 Session 来鉴别用户的身份,并且给出了几个 Spring Session 的案例分享。 我们知道 Session 信息需要保存一份在服务器端。这种方式会带来一些麻烦,比如需要我们保证保存 Session 信息服务器的可用性、不适合移动端(依赖Cookie)等等。 @@ -117,7 +167,7 @@ JWT 由 3 部分构成: 在基于 Token 进行身份验证的的应用程序中,服务器通过`Payload`、`Header`和一个密钥(`secret`)创建令牌(`Token`)并将 `Token` 发送给客户端,客户端将 `Token` 保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP Header 的 Authorization字段中:` Authorization: Bearer Token`。 -![Token Based Authentication flow](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/Token-Based-Authentication.png) +![Token Based Authentication flow](../pictures/Token-Based-Authentication.png) 1. 用户向服务器发送用户名和密码用于登陆系统。 2. 身份验证服务响应并返回了签名的 JWT,上面包含了用户是谁的内容。 @@ -132,7 +182,7 @@ JWT 由 3 部分构成: - [JSON Web Token 入门教程](https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html) - [彻底理解Cookie,Session,Token](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485603&idx=1&sn=c8d324f44d6102e7b44554733da10bb7&chksm=cea24768f9d5ce7efe7291ddabce02b68db34073c7e7d9a7dc9a7f01c5a80cebe33ac75248df&token=844918801&lang=zh_CN#rd) -## 5 什么是OAuth 2.0? +## 7 什么是OAuth 2.0? OAuth 是一个行业的标准授权协议,主要用来授权第三方应用获取有限的权限。而 OAuth 2.0是对 OAuth 1.0 的完全重新设计,OAuth 2.0更快,更容易实现,OAuth 1.0 已经被废弃。详情请见:[rfc6749](https://tools.ietf.org/html/rfc6749)。 @@ -140,6 +190,12 @@ OAuth 是一个行业的标准授权协议,主要用来授权第三方应用 OAuth 2.0 比较常用的场景就是第三方登录,当你的网站接入了第三方登录的时候一般就是使用的 OAuth 2.0 协议。 +另外,现在OAuth 2.0也常见于支付场景(微信支付、支付宝支付)和开发平台(微信开放平台、阿里开放平台等等)。 + +微信支付账户相关参数: + + + **推荐阅读:** - [OAuth 2.0 的一个简单解释](http://www.ruanyifeng.com/blog/2019/04/oauth_design.html) @@ -147,6 +203,14 @@ OAuth 2.0 比较常用的场景就是第三方登录,当你的网站接入了 - [OAuth 2.0 的四种方式](http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html) - [GitHub OAuth 第三方登录示例教程](http://www.ruanyifeng.com/blog/2019/04/github-oauth.html) +## 8 什么是 SSO? + +SSO(Single Sign On)即单点登录说的是用户登陆多个子系统的其中一个就有权访问与其相关的其他系统。举个例子我们在登陆了京东金融之后,我们同时也成功登陆京东的京东超市、京东家电等子系统。 + +## 9.SSO与OAuth2.0的区别 + +OAuth 是一个行业的标准授权协议,主要用来授权第三方应用获取有限的权限。SSO解决的是一个公司的多个相关的自系统的之间的登陆问题比如京东旗下相关子系统京东金融、京东超市、京东家电等等。 + ## 参考 - https://medium.com/@sherryhsu/session-vs-token-based-authentication-11a6c5ac45e4 diff --git a/docs/system-design/authority-certification/sso.md b/docs/system-design/authority-certification/sso.md new file mode 100644 index 00000000..a2042c61 --- /dev/null +++ b/docs/system-design/authority-certification/sso.md @@ -0,0 +1,125 @@ + + +> 本文授权转载自 : https://ken.io/note/sso-design-implement 作者:ken.io +> +> 相关推荐阅读:**[系统的讲解 - SSO单点登录](https://www.imooc.com/article/286710)** + +## 一、前言 + +### 1、SSO说明 + +SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。https://baike.baidu.com/item/SSO/3451380 + +例如访问在网易账号中心(http://reg.163.com/ )登录之后 +访问以下站点都是登录状态 + +- 网易直播 [http://v.163.com](http://v.163.com/) +- 网易博客 [http://blog.163.com](http://blog.163.com/) +- 网易花田 [http://love.163.com](http://love.163.com/) +- 网易考拉 [https://www.kaola.com](https://www.kaola.com/) +- 网易Lofter [http://www.lofter.com](http://www.lofter.com/) + +### 2、单点登录系统的好处 + +1. **用户角度** :用户能够做到一次登录多次使用,无需记录多套用户名和密码,省心。 +2. **系统管理员角度** : 管理员只需维护好一个统一的账号中心就可以了,方便。 +3. **新系统开发角度:** 新系统开发时只需直接对接统一的账号中心即可,简化开发流程,省时。 + +### 3、设计目标 + +本篇文章也主要是为了探讨如何设计&实现一个SSO系统 + +以下为需要实现的核心功能: + +- 单点登录 +- 单点登出 +- 支持跨域单点登录 +- 支持跨域单点登出 + +## 二、SSO设计与实现 + +### 1、核心应用与依赖 + +![单点登录(SSO)设计](https://img.ken.io/blog/sso/sso-system.png-kblb.png) + +| 应用/模块/对象 | 说明 | +| ---------------- | ----------------------------------- | +| 前台站点 | 需要登录的站点 | +| SSO站点-登录 | 提供登录的页面 | +| SSO站点-登出 | 提供注销登录的入口 | +| SSO服务-登录 | 提供登录服务 | +| SSO服务-登录状态 | 提供登录状态校验/登录信息查询的服务 | +| SSO服务-登出 | 提供用户注销登录的服务 | +| 数据库 | 存储用户账户信息 | +| 缓存 | 存储用户的登录信息,通常使用Redis | + +### 2、用户登录状态的存储与校验 + +常见的Web框架对于[Session](https://ken.io/note/session-principle-skill)的实现都是生成一个SessionId存储在浏览器Cookie中。然后将Session内容存储在服务器端内存中,这个 ken.io 在之前[Session工作原理](https://ken.io/note/session-principle-skill)中也提到过。整体也是借鉴这个思路。 +用户登录成功之后,生成AuthToken交给客户端保存。如果是浏览器,就保存在Cookie中。如果是手机App就保存在App本地缓存中。本篇主要探讨基于Web站点的SSO。 +用户在浏览需要登录的页面时,客户端将AuthToken提交给SSO服务校验登录状态/获取用户登录信息 + +对于登录信息的存储,建议采用Redis,使用Redis集群来存储登录信息,既可以保证高可用,又可以线性扩充。同时也可以让SSO服务满足负载均衡/可伸缩的需求。 + +| 对象 | 说明 | +| --------- | ------------------------------------------------------------ | +| AuthToken | 直接使用UUID/GUID即可,如果有验证AuthToken合法性需求,可以将UserName+时间戳加密生成,服务端解密之后验证合法性 | +| 登录信息 | 通常是将UserId,UserName缓存起来 | + +### 3、用户登录/登录校验 + +- 登录时序图 + +![SSO系统设计-登录时序图](https://img.ken.io/blog/sso/sso-login-sequence.png-kbrb.png) + +按照上图,用户登录后Authtoken保存在Cookie中。 domian= test. com +浏览器会将domain设置成 .test.com, +这样访问所有*.test.com的web站点,都会将Authtoken携带到服务器端。 +然后通过SSO服务,完成对用户状态的校验/用户登录信息的获取 + +- 登录信息获取/登录状态校验 + +![SSO系统设计-登录信息获取/登录状态校验](https://img.ken.io/blog/sso/sso-logincheck-sequence.png-kbrb.png) + +### 4、用户登出 + +用户登出时要做的事情很简单: + +1. 服务端清除缓存(Redis)中的登录状态 +2. 客户端清除存储的AuthToken + +- 登出时序图 + +![SSO系统设计-用户登出](https://img.ken.io/blog/sso/sso-logout-sequence.png-kbrb.png) + +### 5、跨域登录、登出 + +前面提到过,核心思路是客户端存储AuthToken,服务器端通过Redis存储登录信息。由于客户端是将AuthToken存储在Cookie中的。所以跨域要解决的问题,就是如何解决Cookie的跨域读写问题。 + +> **Cookie是不能跨域的** ,比如我一个 + +解决跨域的核心思路就是: + +- 登录完成之后通过回调的方式,将AuthToken传递给主域名之外的站点,该站点自行将AuthToken保存在当前域下的Cookie中。 +- 登出完成之后通过回调的方式,调用非主域名站点的登出页面,完成设置Cookie中的AuthToken过期的操作。 +- 跨域登录(主域名已登录) + +![SSO系统设计-跨域登录(主域名已登录)](https://img.ken.io/blog/sso/sso-crossdomain-login-loggedin-sequence.png-kbrb.png) + +- 跨域登录(主域名未登录) + +![SSO系统设计-跨域登录(主域名未登录)](https://img.ken.io/blog/sso/sso-crossdomain-login-unlogin-sequence.png-kbrb.png) + +- 跨域登出 + +![SSO系统设计-跨域登出](https://img.ken.io/blog/sso/sso-crossdomain-logout-sequence.png-kbrb.png) + +## 三、备注 + +- 关于方案 + +这次设计方案更多是提供实现思路。如果涉及到APP用户登录等情况,在访问SSO服务时,增加对APP的签名验证就好了。当然,如果有无线网关,验证签名不是问题。 + +- 关于时序图 + +时序图中并没有包含所有场景,ken.io只列举了核心/主要场景,另外对于一些不影响理解思路的消息能省就省了。 \ No newline at end of file diff --git a/docs/system-design/data-communication/Kafka入门看这一篇就够了.md b/docs/system-design/data-communication/Kafka入门看这一篇就够了.md index cd45e064..3fd42c5e 100644 --- a/docs/system-design/data-communication/Kafka入门看这一篇就够了.md +++ b/docs/system-design/data-communication/Kafka入门看这一篇就够了.md @@ -112,7 +112,8 @@ Kafka 的一个关键性质是日志保留(retention),我们可以配置 | --00000000000000368769.log | --00000000000000737337.index | --00000000000000737337.log - | --00000000000001105814.index | --00000000000001105814.log + | --00000000000001105814.index + | --00000000000001105814.log | --topic2-0 | --topic2-1 @@ -174,6 +175,7 @@ Kafka 的一个关键性质是日志保留(retention),我们可以配置 Kafka消费者是**消费组**的一部分,当多个消费者形成一个消费组来消费主题时,每个消费者会收到不同分区的消息。假设有一个T1主题,该主题有4个分区;同时我们有一个消费组G1,这个消费组只有一个消费者C1。那么消费者C1将会收到这4个分区的消息,如下所示: ![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要1.png) + 如果我们增加新的消费者C2到消费组G1,那么每个消费者将会分别收到两个分区的消息,如下所示: ![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要2.png) @@ -200,7 +202,7 @@ Kafka消费者是**消费组**的一部分,当多个消费者形成一个消 可以看到,当新的消费者加入消费组,它会消费一个或多个分区,而这些分区之前是由其他消费者负责的;另外,当消费者离开消费组(比如重启、宕机等)时,它所消费的分区会分配给其他分区。这种现象称为**重平衡(rebalance)**。重平衡是 Kafka 一个很重要的性质,这个性质保证了高可用和水平扩展。**不过也需要注意到,在重平衡期间,所有消费者都不能消费消息,因此会造成整个消费组短暂的不可用。**而且,将分区进行重平衡也会导致原来的消费者状态过期,从而导致消费者需要重新更新状态,这段期间也会降低消费性能。后面我们会讨论如何安全的进行重平衡以及如何尽可能避免。 -消费者通过定期发送心跳(hearbeat)到一个作为组协调者(group coordinator)的 broker 来保持在消费组内存活。这个 broker 不是固定的,每个消费组都可能不同。当消费者拉取消息或者提交时,便会发送心跳。 +消费者通过定期发送心跳(heartbeat)到一个作为组协调者(group coordinator)的 broker 来保持在消费组内存活。这个 broker 不是固定的,每个消费组都可能不同。当消费者拉取消息或者提交时,便会发送心跳。 如果消费者超过一定时间没有发送心跳,那么它的会话(session)就会过期,组协调者会认为该消费者已经宕机,然后触发重平衡。可以看到,从消费者宕机到会话过期是有一定时间的,这段时间内该消费者的分区都不能进行消息消费;通常情况下,我们可以进行优雅关闭,这样消费者会发送离开的消息到组协调者,这样组协调者可以立即进行重平衡而不需要等待会话过期。 diff --git a/docs/system-design/data-communication/Kafka系统设计开篇-面试看这篇就够了.md b/docs/system-design/data-communication/Kafka系统设计开篇-面试看这篇就够了.md deleted file mode 100644 index 7132c426..00000000 --- a/docs/system-design/data-communication/Kafka系统设计开篇-面试看这篇就够了.md +++ /dev/null @@ -1,277 +0,0 @@ -> 原文链接:https://mp.weixin.qq.com/s/zxPz_aFEMrshApZQ727h4g - -## 引言 - -MQ(消息队列)是跨进程通信的方式之一,可理解为异步rpc,上游系统对调用结果的态度往往是重要不紧急。使用消息队列有以下好处:业务解耦、流量削峰、灵活扩展。接下来介绍消息中间件Kafka。 - -## Kafka是什么? - -Kafka是一个分布式的消息引擎。具有以下特征 - -能够发布和订阅消息流(类似于消息队列) -以容错的、持久的方式存储消息流 -多分区概念,提高了并行能力 - -## Kafka架构总览 - -![Kafka系统架构](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/kafka%E6%9E%B6%E6%9E%84.png) - -## Topic - -消息的主题、队列,每一个消息都有它的topic,Kafka通过topic对消息进行归类。Kafka中可以将Topic从物理上划分成一个或多个分区(Partition),每个分区在物理上对应一个文件夹,以”topicName_partitionIndex”的命名方式命名,该dir包含了这个分区的所有消息(.log)和索引文件(.index),这使得Kafka的吞吐率可以水平扩展。 - -## Partition - -每个分区都是一个 顺序的、不可变的消息队列, 并且可以持续的添加;分区中的消息都被分了一个序列号,称之为偏移量(offset),在每个分区中此偏移量都是唯一的。 -producer在发布消息的时候,可以为每条消息指定Key,这样消息被发送到broker时,会根据分区算法把消息存储到对应的分区中(一个分区存储多个消息),如果分区规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,这样就实现了负载均衡。 -![partition_info](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/partition.jpg) - -## Broker - -Kafka server,用来存储消息,Kafka集群中的每一个服务器都是一个Broker,消费者将从broker拉取订阅的消息 -Producer -向Kafka发送消息,生产者会根据topic分发消息。生产者也负责把消息关联到Topic上的哪一个分区。最简单的方式从分区列表中轮流选择。也可以根据某种算法依照权重选择分区。算法可由开发者定义。 - -## Cousumer - -Consermer实例可以是独立的进程,负责订阅和消费消息。消费者用consumerGroup来标识自己。同一个消费组可以并发地消费多个分区的消息,同一个partition也可以由多个consumerGroup并发消费,但是在consumerGroup中一个partition只能由一个consumer消费 - -## CousumerGroup - -Consumer Group:同一个Consumer Group中的Consumers,Kafka将相应Topic中的每个消息只发送给其中一个Consumer - -# Kafka producer 设计原理 - -## 发送消息的流程 - -![partition_info](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/sendMsg.jpg) -**1.序列化消息&&.计算partition** -根据key和value的配置对消息进行序列化,然后计算partition: -ProducerRecord对象中如果指定了partition,就使用这个partition。否则根据key和topic的partition数目取余,如果key也没有的话就随机生成一个counter,使用这个counter来和partition数目取余。这个counter每次使用的时候递增。 - -**2发送到batch&&唤醒Sender 线程** -根据topic-partition获取对应的batchs(Deque),然后将消息append到batch中.如果有batch满了则唤醒Sender 线程。队列的操作是加锁执行,所以batch内消息时有序的。后续的Sender操作当前方法异步操作。 -![send_msg](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/send2Batch1.png)![send_msg2](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/send2Batch2.png) - - - -**3.Sender把消息有序发到 broker(tp replia leader)** -**3.1 确定tp relica leader 所在的broker** - -Kafka中 每台broker都保存了kafka集群的metadata信息,metadata信息里包括了每个topic的所有partition的信息: leader, leader_epoch, controller_epoch, isr, replicas等;Kafka客户端从任一broker都可以获取到需要的metadata信息;sender线程通过metadata信息可以知道tp leader的brokerId -producer也保存了metada信息,同时根据metadata更新策略(定期更新metadata.max.age.ms、失效检测,强制更新:检查到metadata失效以后,调用metadata.requestUpdate()强制更新 - -``` -public class PartitionInfo { - private final String topic; private final int partition; - private final Node leader; private final Node[] replicas; - private final Node[] inSyncReplicas; private final Node[] offlineReplicas; -} -``` - -**3.2 幂等性发送** - -为实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。对于每个PID,该Producer发送消息的每个都对应一个单调递增的Sequence Number。同样,Broker端也会为每个维护一个序号,并且每Commit一条消息时将其对应序号递增。对于接收的每条消息,如果其序号比Broker维护的序号)大一,则Broker会接受它,否则将其丢弃: - -如果消息序号比Broker维护的序号差值比一大,说明中间有数据尚未写入,即乱序,此时Broker拒绝该消息,Producer抛出InvalidSequenceNumber -如果消息序号小于等于Broker维护的序号,说明该消息已被保存,即为重复消息,Broker直接丢弃该消息,Producer抛出DuplicateSequenceNumber -Sender发送失败后会重试,这样可以保证每个消息都被发送到broker - -**4. Sender处理broker发来的produce response** -一旦broker处理完Sender的produce请求,就会发送produce response给Sender,此时producer将执行我们为send()设置的回调函数。至此producer的send执行完毕。 - -## 吞吐性&&延时: - -buffer.memory:buffer设置大了有助于提升吞吐性,但是batch太大会增大延迟,可搭配linger_ms参数使用 -linger_ms:如果batch太大,或者producer qps不高,batch添加的会很慢,我们可以强制在linger_ms时间后发送batch数据 -ack:producer收到多少broker的答复才算真的发送成功 -0表示producer无需等待leader的确认(吞吐最高、数据可靠性最差) -1代表需要leader确认写入它的本地log并立即确认 --1/all 代表所有的ISR都完成后确认(吞吐最低、数据可靠性最高) - -## Sender线程和长连接 - -每初始化一个producer实例,都会初始化一个Sender实例,新增到broker的长连接。 -代码角度:每初始化一次KafkaProducer,都赋一个空的client - -``` -public KafkaProducer(final Map configs) { - this(configs, null, null, null, null, null, Time.SYSTEM); -} -``` - -![Sender_io](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/SenderIO.jpg) - -终端查看TCP连接数: -lsof -p portNum -np | grep TCP - -# Consumer设计原理 - -## poll消息 - -![consumer-pool](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/consumerPoll.jpg) - -- 消费者通过fetch线程拉消息(单线程) -- 消费者通过心跳线程来与broker发送心跳。超时会认为挂掉 -- 每个consumer - group在broker上都有一个coordnator来管理,消费者加入和退出,以及消费消息的位移都由coordnator处理。 - -## 位移管理 - -consumer的消息位移代表了当前group对topic-partition的消费进度,consumer宕机重启后可以继续从该offset开始消费。 -在kafka0.8之前,位移信息存放在zookeeper上,由于zookeeper不适合高并发的读写,新版本Kafka把位移信息当成消息,发往__consumers_offsets 这个topic所在的broker,__consumers_offsets默认有50个分区。 -消息的key 是groupId+topic_partition,value 是offset. - -![consumerOffsetDat](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/consumerOffsetData.jpg)![consumerOffsetView](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/consumerOffsetView.jpg) - - - -## Kafka Group 状态 - -![groupState](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/groupState.jpg) - -- Empty:初始状态,Group 没有任何成员,如果所有的 offsets 都过期的话就会变成 Dead -- PreparingRebalance:Group 正在准备进行 Rebalance -- AwaitingSync:Group 正在等待来 group leader 的 分配方案 -- Stable:稳定的状态(Group is stable); -- Dead:Group 内已经没有成员,并且它的 Metadata 已经被移除 - -## 重平衡reblance - -当一些原因导致consumer对partition消费不再均匀时,kafka会自动执行reblance,使得consumer对partition的消费再次平衡。 -什么时候发生rebalance?: - -- 组订阅topic数变更 -- topic partition数变更 -- consumer成员变更 -- consumer 加入群组或者离开群组的时候 -- consumer被检测为崩溃的时候 - -## reblance过程 - -举例1 consumer被检测为崩溃引起的reblance -比如心跳线程在timeout时间内没和broker发送心跳,此时coordnator认为该group应该进行reblance。接下来其他consumer发来fetch请求后,coordnator将回复他们进行reblance通知。当consumer成员收到请求后,只有leader会根据分配策略进行分配,然后把各自的分配结果返回给coordnator。这个时候只有consumer leader返回的是实质数据,其他返回的都为空。收到分配方法后,consumer将会把分配策略同步给各consumer - -举例2 consumer加入引起的reblance - -使用join协议,表示有consumer 要加入到group中 -使用sync 协议,根据分配规则进行分配 -![reblance-join](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/reblance-join.jpg)![reblance-sync](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/reblance-sync.jpg) - -(上图图片摘自网络) - -## 引申:以上reblance机制存在的问题 - -在大型系统中,一个topic可能对应数百个consumer实例。这些consumer陆续加入到一个空消费组将导致多次的rebalance;此外consumer 实例启动的时间不可控,很有可能超出coordinator确定的rebalance timeout(即max.poll.interval.ms),将会再次触发rebalance,而每次rebalance的代价又相当地大,因为很多状态都需要在rebalance前被持久化,而在rebalance后被重新初始化。 - -## 新版本改进 - -**通过延迟进入PreparingRebalance状态减少reblance次数** - -![groupStateOfNewVersion](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/groupStateOfNewVersion.jpg) - -新版本新增了group.initial.rebalance.delay.ms参数。空消费组接受到成员加入请求时,不立即转化到PreparingRebalance状态来开启reblance。当时间超过group.initial.rebalance.delay.ms后,再把group状态改为PreparingRebalance(开启reblance)。实现机制是在coordinator底层新增一个group状态:InitialReblance。假设此时有多个consumer陆续启动,那么group状态先转化为InitialReblance,待group.initial.rebalance.delay.ms时间后,再转换为PreparingRebalance(开启reblance) - - - -# Broker设计原理 - -Broker 是Kafka 集群中的节点。负责处理生产者发送过来的消息,消费者消费的请求。以及集群节点的管理等。由于涉及内容较多,先简单介绍,后续专门抽出一篇文章分享 - -## broker zk注册 - -![brokersInZk](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/brokersInZk.jpg) - -## broker消息存储 - -Kafka的消息以二进制的方式紧凑地存储,节省了很大空间 -此外消息存在ByteBuffer而不是堆,这样broker进程挂掉时,数据不会丢失,同时避免了gc问题 -通过零拷贝和顺序寻址,让消息存储和读取速度都非常快 -处理fetch请求的时候通过zero-copy 加快速度 - -## broker状态数据 - -broker设计中,每台机器都保存了相同的状态数据。主要包括以下: - -controller所在的broker ID,即保存了当前集群中controller是哪台broker -集群中所有broker的信息:比如每台broker的ID、机架信息以及配置的若干组连接信息 -集群中所有节点的信息:严格来说,它和上一个有些重复,不过此项是按照broker ID和***类型进行分组的。对于超大集群来说,使用这一项缓存可以快速地定位和查找给定节点信息,而无需遍历上一项中的内容,算是一个优化吧 -集群中所有分区的信息:所谓分区信息指的是分区的leader、ISR和AR信息以及当前处于offline状态的副本集合。这部分数据按照topic-partitionID进行分组,可以快速地查找到每个分区的当前状态。(注:AR表示assigned replicas,即创建topic时为该分区分配的副本集合) - -## broker负载均衡 - -**分区数量负载**:各台broker的partition数量应该均匀 -partition Replica分配算法如下: - -将所有Broker(假设共n个Broker)和待分配的Partition排序 -将第i个Partition分配到第(i mod n)个Broker上 -将第i个Partition的第j个Replica分配到第((i + j) mod n)个Broker上 - -**容量大小负载:**每台broker的硬盘占用大小应该均匀 -在kafka1.1之前,Kafka能够保证各台broker上partition数量均匀,但由于每个partition内的消息数不同,可能存在不同硬盘之间内存占用差异大的情况。在Kafka1.1中增加了副本跨路径迁移功能kafka-reassign-partitions.sh,我们可以结合它和监控系统,实现自动化的负载均衡 - -# Kafka高可用 - -在介绍kafka高可用之前先介绍几个概念 - -同步复制:要求所有能工作的Follower都复制完,这条消息才会被认为commit,这种复制方式极大的影响了吞吐率 -异步复制:Follower异步的从Leader pull数据,data只要被Leader写入log认为已经commit,这种情况下如果Follower落后于Leader的比较多,如果Leader突然宕机,会丢失数据 - -## Isr - -Kafka结合同步复制和异步复制,使用ISR(与Partition Leader保持同步的Replica列表)的方式在确保数据不丢失和吞吐率之间做了平衡。Producer只需把消息发送到Partition Leader,Leader将消息写入本地Log。Follower则从Leader pull数据。Follower在收到该消息向Leader发送ACK。一旦Leader收到了ISR中所有Replica的ACK,该消息就被认为已经commit了,Leader将增加HW并且向Producer发送ACK。这样如果leader挂了,只要Isr中有一个replica存活,就不会丢数据。 - -## Isr动态更新 - -Leader会跟踪ISR,如果ISR中一个Follower宕机,或者落后太多,Leader将把它从ISR中移除。这里所描述的“落后太多”指Follower复制的消息落后于Leader后的条数超过预定值(replica.lag.max.messages)或者Follower超过一定时间(replica.lag.time.max.ms)未向Leader发送fetch请求。 - -broker Nodes In Zookeeper -/brokers/topics/[topic]/partitions/[partition]/state 保存了topic-partition的leader和Isr等信息 - -![partitionStateInZk](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/partitionStateInZk.jpg) - -## Controller负责broker故障检查&&故障转移(fail/recover) - -1. Controller在Zookeeper上注册Watch,一旦有Broker宕机,其在Zookeeper对应的znode会自动被删除,Zookeeper会触发 - Controller注册的watch,Controller读取最新的Broker信息 -2. Controller确定set_p,该集合包含了宕机的所有Broker上的所有Partition -3. 对set_p中的每一个Partition,选举出新的leader、Isr,并更新结果。 - -3.1 从/brokers/topics/[topic]/partitions/[partition]/state读取该Partition当前的ISR - -3.2 决定该Partition的新Leader和Isr。如果当前ISR中有至少一个Replica还幸存,则选择其中一个作为新Leader,新的ISR则包含当前ISR中所有幸存的Replica。否则选择该Partition中任意一个幸存的Replica作为新的Leader以及ISR(该场景下可能会有潜在的数据丢失) - -![electLeader](https://blog-article-resource.oss-cn-beijing.aliyuncs.com/kafka/electLeader.jpg) -3.3 更新Leader、ISR、leader_epoch、controller_epoch:写入/brokers/topics/[topic]/partitions/[partition]/state - -4. 直接通过RPC向set_p相关的Broker发送LeaderAndISRRequest命令。Controller可以在一个RPC操作中发送多个命令从而提高效率。 - -## Controller挂掉 - -每个 broker 都会在 zookeeper 的临时节点 "/controller" 注册 watcher,当 controller 宕机时 "/controller" 会消失,触发broker的watch,每个 broker 都尝试创建新的 controller path,只有一个竞选成功并当选为 controller。 - -# 使用Kafka如何保证幂等性 - -不丢消息 - -首先kafka保证了对已提交消息的at least保证 -Sender有重试机制 -producer业务方在使用producer发送消息时,注册回调函数。在onError方法中重发消息 -consumer 拉取到消息后,处理完毕再commit,保证commit的消息一定被处理完毕 - -不重复 - -consumer拉取到消息先保存,commit成功后删除缓存数据 - -# Kafka高性能 - -partition提升了并发 -zero-copy -顺序写入 -消息聚集batch -页缓存 -业务方对 Kafka producer的优化 - -增大producer数量 -ack配置 -batch diff --git a/docs/system-design/data-communication/dubbo.md b/docs/system-design/data-communication/dubbo.md index f2e0ac48..f5dfbfb4 100644 --- a/docs/system-design/data-communication/dubbo.md +++ b/docs/system-design/data-communication/dubbo.md @@ -44,7 +44,7 @@ Dubbo 是由阿里开源,后来加入了 Apache 。正式由于 Dubbo 的出 **什么是 RPC?** -RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务 A、B 部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个方法该怎么办呢?使用 HTTP请求 当然可以,但是可能会比较慢而且一些优化做的并不好。 RPC 的出现就是为了解决这个问题。 +RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务 A、B 部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个方法该怎么办呢?使用 HTTP请求当然可以,但是可能会比较麻烦。 RPC 的出现就是为了让你调用远程方法像调用本地方法一样简单。 **RPC原理是什么?** @@ -179,7 +179,7 @@ Dubbo 的诞生和 SOA 分布式架构的流行有着莫大的关系。SOA 面 #### 3.2.1 Random LoadBalance(默认,基于权重的随机负载均衡机制) - + - **随机,按权重设置随机概率。** - 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 diff --git a/docs/system-design/data-communication/kafka-inverview.md b/docs/system-design/data-communication/kafka-inverview.md new file mode 100644 index 00000000..42ef9746 --- /dev/null +++ b/docs/system-design/data-communication/kafka-inverview.md @@ -0,0 +1,210 @@ +------ + + + +## Kafka面试题总结 + +### Kafka 是什么?主要应用场景有哪些? + +Kafka 是一个分布式流式处理平台。这到底是什么意思呢? + +流平台具有三个关键功能: + +1. **消息队列**:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。 +2. **容错的持久方式存储记录消息流**: Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险·。 +3. **流式处理平台:** 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。 + +Kafka 主要有两大应用场景: + +1. **消息队列** :建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。 +2. **数据处理:** 构建实时的流数据处理程序来转换或处理数据流。 + +### 和其他消息队列相比,Kafka的优势在哪里? + +我们现在经常提到 Kafka 的时候就已经默认它是一个非常优秀的消息队列了,我们也会经常拿它给 RocketMQ、RabbitMQ 对比。我觉得 Kafka 相比其他消息队列主要的优势如下: + +1. **极致的性能** :基于 Scala 和 Java 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。 +2. **生态系统兼容性无可匹敌** :Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域。 + +实际上在早期的时候 Kafka 并不是一个合格的消息队列,早期的 Kafka 在消息队列领域就像是一个衣衫褴褛的孩子一样,功能不完备并且有一些小问题比如丢失消息、不保证消息可靠性等等。当然,这也和 LinkedIn 最早开发 Kafka 用于处理海量的日志有很大关系,哈哈哈,人家本来最开始就不是为了作为消息队列滴,谁知道后面误打误撞在消息队列领域占据了一席之地。 + +随着后续的发展,这些短板都被 Kafka 逐步修复完善。所以,**Kafka 作为消息队列不可靠这个说法已经过时!** + +### 队列模型了解吗?Kafka 的消息模型知道吗? + +> 题外话:早期的 JMS 和 AMQP 属于消息服务领域权威组织所做的相关的标准,我在 [JavaGuide](https://github.com/Snailclimb/JavaGuide)的 [《消息队列其实很简单》](https://github.com/Snailclimb/JavaGuide#%E6%95%B0%E6%8D%AE%E9%80%9A%E4%BF%A1%E4%B8%AD%E9%97%B4%E4%BB%B6)这篇文章中介绍过。但是,这些标准的进化跟不上消息队列的演进速度,这些标准实际上已经属于废弃状态。所以,可能存在的情况是:不同的消息队列都有自己的一套消息模型。 + +#### 队列模型:早期的消息模型 + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/队列模型23.png) + +**使用队列(Queue)作为消息通信载体,满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。** 比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) + +**队列模型存在的问题:** + +假如我们存在这样一种情况:我们需要将生产者产生的消息分发给多个消费者,并且每个消费者都能接收到完成的消息内容。 + +这种情况,队列模型就不好解决了。很多比较杠精的人就说:我们可以为每个消费者创建一个单独的队列,让生产者发送多份。这是一种非常愚蠢的做法,浪费资源不说,还违背了使用消息队列的目的。 + +#### 发布-订阅模型:Kafka 消息模型 + +发布-订阅模型主要是为了解决队列模型存在的问题。 + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/广播模型21312.png) + +发布订阅模型(Pub-Sub) 使用**主题(Topic)** 作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者,**在一条消息广播之后才订阅的用户则是收不到该条消息的**。 + +**在发布 - 订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。所以说,发布 - 订阅模型在功能层面上是可以兼容队列模型的。** + +**Kafka 采用的就是发布 - 订阅模型。** + +> **RocketMQ 的消息模型和 Kafka 基本是完全一样的。唯一的区别是 Kafka 中没有队列这个概念,与之对应的是 Partition(分区)。** + +### 什么是Producer、Consumer、Broker、Topic、Partition? + +Kafka 将生产者发布的消息发送到 **Topic(主题)** 中,需要这些消息的消费者可以订阅这些 **Topic(主题)**,如下图所示: + +![Kafka Topic Partition](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/KafkaTopicPartitioning.png) + +上面这张图也为我们引出了,Kafka 比较重要的几个概念: + +1. **Producer(生产者)** : 产生消息的一方。 +2. **Consumer(消费者)** : 消费消息的一方。 +3. **Broker(代理)** : 可以看作是一个独立的 Kafka 实例。多个 Kafka Broker 组成一个 Kafka Cluster。 + +同时,你一定也注意到每个 Broker 中又包含了 Topic 以及 Partition 这两个重要的概念: + +- **Topic(主题)** : Producer 将消息发送到特定的主题,Consumer 通过订阅特定的 Topic(主题) 来消费消息。 +- **Partition(分区)** : Partition 属于 Topic 的一部分。一个 Topic 可以有多个 Partition ,并且同一 Topic 下的 Partition 可以分布在不同的 Broker 上,这也就表明一个 Topic 可以横跨多个 Broker 。这正如我上面所画的图一样。 + +> 划重点:**Kafka 中的 Partition(分区) 实际上可以对应成为消息队列中的队列。这样是不是更好理解一点?** + +### Kafka 的多副本机制了解吗?带来了什么好处? + +还有一点我觉得比较重要的是 Kafka 为分区(Partition)引入了多副本(Replica)机制。分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。 + +> 生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。 + +**Kafka 的多分区(Partition)以及多副本(Replica)机制有什么好处呢?** + +1. Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力(负载均衡)。 +2. Partition 可以指定对应的 Replica 数, 这也极大地提高了消息存储的安全性, 提高了容灾能力,不过也相应的增加了所需要的存储空间。 + +### Zookeeper 在 Kafka 中的作用知道吗? + +> **要想搞懂 zookeeper 在 Kafka 中的作用 一定要自己搭建一个 Kafka 环境然后自己进 zookeeper 去看一下有哪些文件夹和 Kafka 有关,每个节点又保存了什么信息。** 一定不要光看不实践,这样学来的也终会忘记!这部分内容参考和借鉴了这篇文章:https://www.jianshu.com/p/a036405f989c 。 + + + +下图就是我的本地 Zookeeper ,它成功和我本地的 Kafka 关联上(以下文件夹结构借助 idea 插件 Zookeeper tool 实现)。 + + + +ZooKeeper 主要为 Kafka 提供元数据的管理的功能。 + +从图中我们可以看出,Zookeeper 主要为 Kafka 做了下面这些事情: + +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 个消息对应的操作分别对应的数据库操作是:更改用户会员等级、根据会员等级计算订单价格。假如这两条消息的消费顺序不一样造成的最终结果就会截然不同。 + +我们知道 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(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。 + +所以,我们就有一种很简单的保证消息消费顺序的方法:**1 个 Topic 只对应一个 Partition**。这样当然可以解决问题,但是破坏了 Kafka 的设计初衷。 + +Kafka 中发送 1 条消息的时候,可以指定 topic, partition, key,data(数据) 4 个参数。如果你发送消息的时候指定了 Partition 的话,所有消息都会被发送到指定的 Partition。并且,同一个 key 的消息可以保证只发送到同一个 partition,这个我们可以采用表/对象的 id 来作为 key 。 + +总结一下,对于如何保证 Kafka 中消息消费的顺序,有了下面两种方法: + +1. 1 个 Topic 只对应一个 Partition。 +2. (推荐)发送消息的时候指定 key/Partition。 + +当然不仅仅只有上面两种方法,上面两种方法是我觉得比较好理解的, + +### Kafka 如何保证消息不丢失 + +#### 生产者丢失消息的情况 + +生产者(Producer) 调用`send`方法发送消息之后,消息可能因为网络问题并没有发送过去。 + +所以,我们不能默认在调用`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)** + +```java +SendResult sendResult = kafkaTemplate.send(topic, o).get(); +if (sendResult.getRecordMetadata() != null) { + logger.info("生产者成功发送消息到" + sendResult.getProducerRecord().topic() + "-> " + sendRe + sult.getProducerRecord().value().toString()); +} +``` + +但是一般不推荐这么做!可以采用为其添加回调函数的形式,示例代码如下: + +````java + ListenableFuture> future = kafkaTemplate.send(topic, o); + future.addCallback(result -> logger.info("生产者成功发送消息到topic:{} partition:{}的消息", result.getRecordMetadata().topic(), result.getRecordMetadata().partition()), + ex -> logger.error("生产者发送消失败,原因:{}", ex.getMessage())); +```` + +如果消息发送失败的话,我们检查失败的原因之后重新发送即可! + +**另外这里推荐为 Producer 的`retries `(重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你3次一下子就重试完了** + +#### 消费者丢失消息的情况 + +我们知道消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。偏移量(offset)表示 Consumer 当前消费到的 Partition(分区)的所在的位置。Kafka 通过偏移量(offset)可以保证消息在分区内的顺序性。 + +![kafka offset](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/kafka-offset.jpg) + +当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。 + +**解决办法也比较粗暴,我们手动关闭闭自动提交 offset,每次在真正消费完消息之后之后再自己手动提交 offset 。** 但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两次。 + +#### Kafka 弄丢了消息 + + 我们知道 Kafka 为分区(Partition)引入了多副本(Replica)机制。分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。 + +**试想一种情况:假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。** + +**设置 acks = all** + +解决办法就是我们设置 **acks = all**。acks 是 Kafka 生产者(Producer) 很重要的一个参数。 + +acks 的默认值即为1,代表我们的消息被leader副本接收之后就算被成功发送。当我们配置 **acks = all** 代表则所有副本都要接收到该消息之后该消息才算真正成功被发送。 + +**设置 replication.factor >= 3** + +为了保证 leader 副本能有 follower 副本能同步消息,我们一般会为 topic 设置 **replication.factor >= 3**。这样就可以保证每个 分区(partition) 至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性。 + +**设置 min.insync.replicas > 1** + +一般情况下我们还需要设置 **min.insync.replicas> 1** ,这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。**min.insync.replicas** 的默认值为 1 ,在实际生产中应尽量避免默认值 1。 + +但是,为了保证整个 Kafka 服务的高可用性,你需要确保 **replication.factor > min.insync.replicas** 。为什么呢?设想一下假如两者相等的话,只要是有一个副本挂掉,整个分区就无法正常工作了。这明显违反高可用性!一般推荐设置成 **replication.factor = min.insync.replicas + 1**。 + +**设置 unclean.leader.election.enable = false** + +> **Kafka 0.11.0.0版本开始 unclean.leader.election.enable 参数的默认值由原来的true 改为false** + +我们最开始也说了我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。多个 follower 副本之间的消息同步情况不一样,当我们配置了 **unclean.leader.election.enable = false** 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。 + +### Kafka 如何保证消息不重复消费 + +代办... + +### Reference + +- Kafka 官方文档: https://kafka.apache.org/documentation/ +- 极客时间—《Kafka核心技术与实战》第11节:无消息丢失配置怎么实现? diff --git a/docs/system-design/data-communication/message-queue.md b/docs/system-design/data-communication/message-queue.md index 3f99c07c..a0db7269 100644 --- a/docs/system-design/data-communication/message-queue.md +++ b/docs/system-design/data-communication/message-queue.md @@ -20,47 +20,52 @@ # 消息队列其实很简单 -  “RabbitMQ?”“Kafka?”“RocketMQ?”...在日常学习与开发过程中,我们常常听到消息队列这个关键词。我也在我的多篇文章中提到了这个概念。可能你是熟练使用消息队列的老手,又或者你是不懂消息队列的新手,不论你了不了解消息队列,本文都将带你搞懂消息队列的一些基本理论。如果你是老手,你可能从本文学到你之前不曾注意的一些关于消息队列的重要概念,如果你是新手,相信本文将是你打开消息队列大门的一板砖。 +“RabbitMQ?”“Kafka?”“RocketMQ?”...在日常学习与开发过程中,我们常常听到消息队列这个关键词。我也在我的多篇文章中提到了这个概念。可能你是熟练使用消息队列的老手,又或者你是不懂消息队列的新手,不论你了不了解消息队列,本文都将带你搞懂消息队列的一些基本理论。如果你是老手,你可能从本文学到你之前不曾注意的一些关于消息队列的重要概念,如果你是新手,相信本文将是你打开消息队列大门的一板砖。 ## 一 什么是消息队列 -  我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。目前使用较多的消息队列有ActiveMQ,RabbitMQ,Kafka,RocketMQ,我们后面会一一对比这些消息队列。 +我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。目前使用较多的消息队列有ActiveMQ,RabbitMQ,Kafka,RocketMQ,我们后面会一一对比这些消息队列。 -  另外,我们知道队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。比如生产者发送消息1,2,3...对于消费者就会按照1,2,3...的顺序来消费。但是偶尔也会出现消息被消费的顺序不对的情况,比如某个消息消费失败又或者一个 queue 多个consumer 也会导致消息被消费的顺序不对,我们一定要保证消息被消费的顺序正确。 +另外,我们知道队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。比如生产者发送消息1,2,3...对于消费者就会按照1,2,3...的顺序来消费。但是偶尔也会出现消息被消费的顺序不对的情况,比如某个消息消费失败又或者一个 queue 多个consumer 也会导致消息被消费的顺序不对,我们一定要保证消息被消费的顺序正确。 -  除了上面说的消息消费顺序的问题,使用消息队列,我们还要考虑如何保证消息不被重复消费?如何保证消息的可靠性传输(如何处理消息丢失的问题)?......等等问题。所以说使用消息队列也不是十全十美的,使用它也会让系统可用性降低、复杂度提高,另外需要我们保障一致性等问题。 +除了上面说的消息消费顺序的问题,使用消息队列,我们还要考虑如何保证消息不被重复消费?如何保证消息的可靠性传输(如何处理消息丢失的问题)?......等等问题。所以说使用消息队列也不是十全十美的,使用它也会让系统可用性降低、复杂度提高,另外需要我们保障一致性等问题。 ## 二 为什么要用消息队列 -  我觉得使用消息队列主要有两点好处:1.通过异步处理提高系统性能(削峰、减少响应所需时间);2.降低系统耦合性。如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。 +我觉得使用消息队列主要有两点好处: + +1. 通过异步处理提高系统性能(削峰、减少响应所需时间) +2. 降低系统耦合性。 + +如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。 -  《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。 +《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。 ### (1) 通过异步处理提高系统性能(削峰、减少响应所需时间) ![通过异步处理提高系统性能](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) -  因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 +因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 ### (2) 降低系统耦合性 -   使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧: +使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧: ![解耦](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/消息队列-解耦.png) -  生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。 +生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。 -  **消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 +**消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 -  消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 +消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 -  **另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。** +**另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。** **备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的。**除了发布-订阅模式,还有点对点订阅模式(一个消息只有一个消费者),我们比较常用的是发布-订阅模式。** 另外,这两种消息模型是 JMS 提供的,AMQP 协议还提供了 5 种消息模型。 @@ -76,7 +81,7 @@ #### 4.1.1 JMS 简介 -  JMS(JAVA Message Service,java消息服务)是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。**JMS(JAVA Message Service,Java消息服务)API是一个消息服务的标准或者说是规范**,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 +JMS(JAVA Message Service,java消息服务)是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。**JMS(JAVA Message Service,Java消息服务)API是一个消息服务的标准或者说是规范**,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 **ActiveMQ 就是基于 JMS 规范实现的。** @@ -98,7 +103,7 @@ #### 4.1.3 JMS 五种不同的消息正文格式 -  JMS定义了五种不同的消息正文格式,以及调用的消息类型,允许你发送并接收以一些不同形式的数据,提供现有消息格式的一些级别的兼容性。 +JMS定义了五种不同的消息正文格式,以及调用的消息类型,允许你发送并接收以一些不同形式的数据,提供现有消息格式的一些级别的兼容性。 - StreamMessage -- Java原始值的数据流 - MapMessage--一套名称-值对 @@ -109,12 +114,10 @@ ### 4.2 AMQP -  ​ AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 **高级消息队列协议**(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。 +AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 **高级消息队列协议**(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。 **RabbitMQ 就是基于 AMQP 协议实现的。** - - ### 4.3 JMS vs AMQP @@ -151,7 +154,7 @@ - ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用。 - RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做erlang源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 - RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ 挺好的 -- kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 +- Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 参考:《Java工程师面试突击第1季-中华石杉老师》 diff --git a/docs/system-design/data-communication/rabbitmq.md b/docs/system-design/data-communication/rabbitmq.md index 28407cce..79f24fdc 100644 --- a/docs/system-design/data-communication/rabbitmq.md +++ b/docs/system-design/data-communication/rabbitmq.md @@ -253,7 +253,7 @@ wget https://www.rabbitmq.com/releases/rabbitmq-server/v3.6.8/rabbitmq-server-3. ``` 或者直接在官网下载 -https://www.rabbitmq.com/install-rpm.html[enter link description here](https://www.rabbitmq.com/install-rpm.html) +[https://www.rabbitmq.com/install-rpm.html](https://www.rabbitmq.com/install-rpm.html) **2. 安装rpm** diff --git a/docs/system-design/data-communication/summary.md b/docs/system-design/data-communication/summary.md deleted file mode 100644 index 209df6b6..00000000 --- a/docs/system-design/data-communication/summary.md +++ /dev/null @@ -1,100 +0,0 @@ -> ## RPC - -**RPC(Remote Procedure Call)—远程过程调用** ,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发分布式程序就像开发本地程序一样简单。 - -**RPC采用客户端(服务调用方)/服务器端(服务提供方)模式,** 都运行在自己的JVM中。客户端只需要引入要使用的接口,接口的实现和运行都在服务器端。RPC主要依赖的技术包括序列化、反序列化和数据传输协议,这是一种定义与实现相分离的设计。 - -**目前Java使用比较多的RPC方案主要有RMI(JDK自带)、Hessian、Dubbo以及Thrift等。** - -**注意: RPC主要指内部服务之间的调用,RESTful也可以用于内部服务之间的调用,但其主要用途还在于外部系统提供服务,因此没有将其包含在本知识点内。** - -### 常见RPC框架: - -- **RMI(JDK自带):** JDK自带的RPC - - 详细内容可以参考:[从懵逼到恍然大悟之Java中RMI的使用](https://blog.csdn.net/lmy86263/article/details/72594760) - -- **Dubbo:** Dubbo是 阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。 - - 详细内容可以参考: - - - [ 高性能优秀的服务框架-dubbo介绍](https://blog.csdn.net/qq_34337272/article/details/79862899) - - - [Dubbo是什么?能做什么?](https://blog.csdn.net/houshaolin/article/details/76408399) - - -- **Hessian:** Hessian是一个轻量级的remotingonhttp工具,使用简单的方法提供了RMI的功能。 相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。 - - 详细内容可以参考: [Hessian的使用以及理解](https://blog.csdn.net/sunwei_pyw/article/details/74002351) - -- **Thrift:** Apache Thrift是Facebook开源的跨语言的RPC通信框架,目前已经捐献给Apache基金会管理,由于其跨语言特性和出色的性能,在很多互联网公司得到应用,有能力的公司甚至会基于thrift研发一套分布式服务框架,增加诸如服务注册、服务发现等功能。 - - - 详细内容可以参考: [【Java】分布式RPC通信框架Apache Thrift 使用总结](https://www.cnblogs.com/zeze/p/8628585.html) - -### 如何进行选择: - -- **是否允许代码侵入:** 即需要依赖相应的代码生成器生成代码,比如Thrift。 -- **是否需要长连接获取高性能:** 如果对于性能需求较高的haul,那么可以果断选择基于TCP的Thrift、Dubbo。 -- **是否需要跨越网段、跨越防火墙:** 这种情况一般选择基于HTTP协议的Hessian和Thrift的HTTP Transport。 - -此外,Google推出的基于HTTP2.0的gRPC框架也开始得到应用,其序列化协议基于Protobuf,网络框架使用的是Netty4,但是其需要生成代码,可扩展性也比较差。 - -> ## 消息中间件 - -**消息中间件,也可以叫做中央消息队列或者是消息队列(区别于本地消息队列,本地消息队列指的是JVM内的队列实现)**,是一种独立的队列系统,消息中间件经常用来解决内部服务之间的 **异步调用问题** 。请求服务方把请求队列放到队列中即可返回,然后等待服务提供方去队列中获取请求进行处理,之后通过回调等机制把结果返回给请求服务方。 - -异步调用只是消息中间件一个非常常见的应用场景。此外,常用的消息队列应用场景还有如下几个: -- **解耦 :** 一个业务的非核心流程需要依赖其他系统,但结果并不重要,有通知即可。 -- **最终一致性 :** 指的是两个系统的状态保持一致,可以有一定的延迟,只要最终达到一致性即可。经常用在解决分布式事务上。 -- **广播 :** 消息队列最基本的功能。生产者只负责生产消息,订阅者接收消息。 -- **错峰和流控** - - -具体可以参考: - -[《消息队列深入解析》](https://blog.csdn.net/qq_34337272/article/details/80029918) - -当前使用较多的消息队列有ActiveMQ(性能差,不推荐使用)、RabbitMQ、RocketMQ、Kafka等等,我们之前提到的redis数据库也可以实现消息队列,不过不推荐,redis本身设计就不是用来做消息队列的。 - -- **ActiveMQ:** ActiveMQ是Apache出品,最流行的,能力强劲的开源消息总线。ActiveMQ是一个完全支持JMS1.1和J2EE 1.4规范的JMSProvider实现,尽管JMS规范出台已经是很久的事情了,但是JMS在当今的J2EE应用中间仍然扮演着特殊的地位。 - - 具体可以参考: - - [《消息队列ActiveMQ的使用详解》](https://blog.csdn.net/qq_34337272/article/details/80031702) - -- **RabbitMQ:** RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗 - > AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。 - - - 具体可以参考: - - [《消息队列之 RabbitMQ》](https://www.jianshu.com/p/79ca08116d57) - -- **RocketMQ:** - - 具体可以参考: - - [《RocketMQ 实战之快速入门》](https://www.jianshu.com/p/824066d70da8) - - [《十分钟入门RocketMQ》](http://jm.taobao.org/2017/01/12/rocketmq-quick-start-in-10-minutes/) (阿里中间件团队博客) - - -- **Kafka**:Kafka是一个分布式的、可分区的、可复制的、基于发布/订阅的消息系统(现在官方的描述是“一个分布式流平台”),Kafka主要用于大数据领域,当然在分布式系统中也有应用。目前市面上流行的消息队列RocketMQ就是阿里借鉴Kafka的原理、用Java开发而得。 - - 具体可以参考: - - [《Kafka应用场景》](http://book.51cto.com/art/201801/565244.htm) - - [《初谈Kafka》](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484106&idx=1&sn=aa1999895d009d91eb3692a3e6429d18&chksm=fd9854abcaefddbd1101ca5dc2c7c783d7171320d6300d9b2d8e68b7ef8abd2b02ea03e03600#rd) - -**推荐阅读:** - -[《Kafka、RabbitMQ、RocketMQ等消息中间件的对比 —— 消息发送性能和区别》](https://mp.weixin.qq.com/s?__biz=MzU5OTMyODAyNg==&mid=2247484721&idx=1&sn=11e4e29886e581dd328311d308ccc068&chksm=feb7d144c9c058529465b02a4e26a25ef76b60be8984ace9e4a0f5d3d98ca52e014ecb73b061&scene=21#wechat_redirect) - - - - - - - diff --git a/docs/system-design/framework/ZooKeeper.md b/docs/system-design/framework/ZooKeeper.md deleted file mode 100644 index 4b10ca0c..00000000 --- a/docs/system-design/framework/ZooKeeper.md +++ /dev/null @@ -1,185 +0,0 @@ -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/56385654.jpg) -## 前言 - -相信大家对 ZooKeeper 应该不算陌生。但是你真的了解 ZooKeeper 是个什么东西吗?如果别人/面试官让你给他讲讲 ZooKeeper 是个什么东西,你能回答到什么地步呢? - -我本人曾经使用过 ZooKeeper 作为 Dubbo 的注册中心,另外在搭建 solr 集群的时候,我使用到了 ZooKeeper 作为 solr 集群的管理工具。前几天,总结项目经验的时候,我突然问自己 ZooKeeper 到底是个什么东西?想了半天,脑海中只是简单的能浮现出几句话:“①Zookeeper 可以被用作注册中心。 ②Zookeeper 是 Hadoop 生态系统的一员;③构建 Zookeeper 集群的时候,使用的服务器最好是奇数台。” 可见,我对于 Zookeeper 的理解仅仅是停留在了表面。 - -所以,**通过本文,希望带大家稍微详细的了解一下 ZooKeeper 。如果没有学过 ZooKeeper ,那么本文将会是你进入 ZooKeeper 大门的垫脚砖。如果你已经接触过 ZooKeeper ,那么本文将带你回顾一下 ZooKeeper 的一些基础概念。** - -最后,**本文只涉及 ZooKeeper 的一些概念,并不涉及 ZooKeeper 的使用以及 ZooKeeper 集群的搭建。** 网上有介绍 ZooKeeper 的使用以及搭建 ZooKeeper 集群的文章,大家有需要可以自行查阅。 - -## 一 什么是 ZooKeeper - -### ZooKeeper 的由来 - -**下面这段内容摘自《从Paxos到Zookeeper 》第四章第一节的某段内容,推荐大家阅读以下:** - -> Zookeeper最早起源于雅虎研究院的一个研究小组。在当时,研究人员发现,在雅虎内部很多大型系统基本都需要依赖一个类似的系统来进行分布式协调,但是这些系统往往都存在分布式单点问题。所以,**雅虎的开发人员就试图开发一个通用的无单点问题的分布式协调框架,以便让开发人员将精力集中在处理业务逻辑上。** -> ->关于“ZooKeeper”这个项目的名字,其实也有一段趣闻。在立项初期,考虑到之前内部很多项目都是使用动物的名字来命名的(例如著名的Pig项目),雅虎的工程师希望给这个项目也取一个动物的名字。时任研究院的首席科学家RaghuRamakrishnan开玩笑地说:“在这样下去,我们这儿就变成动物园了!”此话一出,大家纷纷表示就叫动物园管理员吧一一一因为各个以动物命名的分布式组件放在一起,**雅虎的整个分布式系统看上去就像一个大型的动物园了,而Zookeeper正好要用来进行分布式环境的协调一一于是,Zookeeper的名字也就由此诞生了。** - - -### 1.1 ZooKeeper 概览 - -ZooKeeper 是一个开源的分布式协调服务,ZooKeeper框架最初是在“Yahoo!"上构建的,用于以简单而稳健的方式访问他们的应用程序。 后来,Apache ZooKeeper成为Hadoop,HBase和其他分布式框架使用的有组织服务的标准。 例如,Apache HBase使用ZooKeeper跟踪分布式数据的状态。**ZooKeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。** - -> **原语:** 操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程。具有不可分割性·即原语的执行必须是连续的,在执行过程中不允许被中断。 - -**ZooKeeper 是一个典型的分布式数据一致性解决方案,分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。** - -**Zookeeper 一个最常用的使用场景就是用于担任服务生产者和服务消费者的注册中心(提供发布订阅服务)。** 服务生产者将自己提供的服务注册到Zookeeper中心,服务的消费者在进行服务调用的时候先到Zookeeper中查找服务,获取到服务生产者的详细信息之后,再去调用服务生产者的内容与数据。如下图所示,在 Dubbo架构中 Zookeeper 就担任了注册中心这一角色。 - -![Dubbo](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/35571782.jpg) - -### 1.2 结合个人使用情况的讲一下 ZooKeeper - -在我自己做过的项目中,主要使用到了 ZooKeeper 作为 Dubbo 的注册中心(Dubbo 官方推荐使用 ZooKeeper注册中心)。另外在搭建 solr 集群的时候,我使用 ZooKeeper 作为 solr 集群的管理工具。这时,ZooKeeper 主要提供下面几个功能:1、集群管理:容错、负载均衡。2、配置文件的集中管理3、集群的入口。 - - -我个人觉得在使用 ZooKeeper 的时候,最好是使用 集群版的 ZooKeeper 而不是单机版的。官网给出的架构图就描述的是一个集群版的 ZooKeeper 。通常 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呢? - -## 二 关于 ZooKeeper 的一些重要概念 - -### 2.1 重要概念总结 - -- **ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。** -- **为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。** -- **ZooKeeper 将数据保存在内存中,这也就保证了 高吞吐量和低延迟**(但是内存限制了能够存储的容量不太大,此限制也是保持znode中存储的数据量较小的进一步原因)。 -- **ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。**(“读”多于“写”是协调服务的典型场景。) -- **ZooKeeper有临时节点的概念。 当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。而当会话终结时,瞬时节点被删除。持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上。** -- ZooKeeper 底层其实只提供了两个功能:①管理(存储、读取)用户程序提交的数据;②为用户程序提供数据节点监听服务。 - -**下面关于会话(Session)、 Znode、版本、Watcher、ACL概念的总结都在《从Paxos到Zookeeper 》第四章第一节以及第七章第八节有提到,感兴趣的可以看看!** - -### 2.2 会话(Session) - -Session 指的是 ZooKeeper 服务器与客户端会话。**在 ZooKeeper 中,一个客户端连接是指客户端和服务器之间的一个 TCP 长连接**。客户端启动的时候,首先会与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了。**通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知。** Session的`sessionTimeout`值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,**只要在`sessionTimeout`规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。** - -**在为客户端创建会话之前,服务端首先会为每个客户端都分配一个sessionID。由于 sessionID 是 Zookeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。** - -### 2.3 Znode - -**在谈到分布式的时候,我们通常说的“节点"是指组成集群的每一台机器。然而,在Zookeeper中,“节点"分为两类,第一类同样是指构成集群的机器,我们称之为机器节点;第二类则是指数据模型中的数据单元,我们称之为数据节点一一ZNode。** - -Zookeeper将所有数据存储在内存中,数据模型是一棵树(Znode Tree),由斜杠(/)的进行分割的路径,就是一个Znode,例如/foo/path1。每个上都会保存自己的数据内容,同时还会保存一系列属性信息。 - -**在Zookeeper中,node可以分为持久节点和临时节点两类。所谓持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上。而临时节点就不一样了,它的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。** 另外,ZooKeeper还允许用户为每个节点添加一个特殊的属性:**SEQUENTIAL**.一旦节点被标记上这个属性,那么在这个节点被创建的时候,Zookeeper会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。 - -### 2.4 版本 - -在前面我们已经提到,Zookeeper 的每个 ZNode 上都会存储数据,对应于每个ZNode,Zookeeper 都会为其维护一个叫作 **Stat** 的数据结构,Stat 中记录了这个 ZNode 的三个数据版本,分别是version(当前ZNode的版本)、cversion(当前ZNode子节点的版本)和 aversion(当前ZNode的ACL版本)。 - - -### 2.5 Watcher - -**Watcher(事件监听器),是Zookeeper中的一个很重要的特性。Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是Zookeeper实现分布式协调服务的重要特性。** - -### 2.6 ACL - -Zookeeper采用ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。Zookeeper 定义了如下5种权限。 - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/27473480.jpg) - -其中尤其需要注意的是,CREATE和DELETE这两种权限都是针对子节点的权限控制。 - -## 三 ZooKeeper 特点 - -- **顺序一致性:** 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。 -- **原子性:** 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。 -- **单一系统映像 :** 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。 -- **可靠性:** 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。 - -## 四 ZooKeeper 设计目标 - -### 4.1 简单的数据模型 - -ZooKeeper 允许分布式进程通过共享的层次结构命名空间进行相互协调,这与标准文件系统类似。 名称空间由 ZooKeeper 中的数据寄存器组成 - 称为znode,这些类似于文件和目录。 与为存储设计的典型文件系统不同,ZooKeeper数据保存在内存中,这意味着ZooKeeper可以实现高吞吐量和低延迟。 - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/94251757.jpg) - -### 4.2 可构建集群 - -**为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么zookeeper本身仍然是可用的。** 客户端在使用 ZooKeeper 时,需要知道集群机器列表,通过与集群中的某一台机器建立 TCP 连接来使用服务,客户端使用这个TCP链接来发送请求、获取结果、获取监听事件以及发送心跳包。如果这个连接异常断开了,客户端可以连接到另外的机器上。 - -**ZooKeeper 官方提供的架构图:** - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/68900686.jpg) - -上图中每一个Server代表一个安装Zookeeper服务的服务器。组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过 Zab 协议(Zookeeper Atomic Broadcast)来保持数据的一致性。 - -### 4.3 顺序访问 - -**对于来自客户端的每个更新请求,ZooKeeper 都会分配一个全局唯一的递增编号,这个编号反应了所有事务操作的先后顺序,应用程序可以使用 ZooKeeper 这个特性来实现更高层次的同步原语。** **这个编号也叫做时间戳——zxid(Zookeeper Transaction Id)** - -### 4.4 高性能 - -**ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。)** - -## 五 ZooKeeper 集群角色介绍 - -**最典型集群模式: Master/Slave 模式(主备模式)**。在这种模式中,通常 Master服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。 - -但是,**在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了Leader、Follower 和 Observer 三种角色**。如下图所示 - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/89602762.jpg) - - **ZooKeeper 集群中的所有机器通过一个 Leader 选举过程来选定一台称为 “Leader” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,Follower 和 Observer 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。** - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-13/91622395.jpg) - -**当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进人恢复模式并选举产生新的Leader服务器。这个过程大致是这样的:** - -1. Leader election(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。 -2. Discovery(发现阶段):在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。 -3. Synchronization(同步阶段):同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后 -准 leader 才会成为真正的 leader。 -4. Broadcast(广播阶段) -到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。 - -## 六 ZooKeeper &ZAB 协议&Paxos算法 - -### 6.1 ZAB 协议&Paxos算法 - -Paxos 算法应该可以说是 ZooKeeper 的灵魂了。但是,ZooKeeper 并没有完全采用 Paxos算法 ,而是使用 ZAB 协议作为其保证数据一致性的核心算法。另外,在ZooKeeper的官方文档中也指出,ZAB协议并不像 Paxos 算法那样,是一种通用的分布式一致性算法,它是一种特别为Zookeeper设计的崩溃可恢复的原子消息广播算法。 - -### 6.2 ZAB 协议介绍 - -**ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。** - -### 6.3 ZAB 协议两种基本的模式:崩溃恢复和消息广播 - -ZAB协议包括两种基本的模式,分别是 **崩溃恢复和消息广播**。当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进人恢复模式并选举产生新的Leader服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式。其中,**所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和Leader服务器的数据状态保持一致**。 - -**当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进人消息广播模式了。** 当一台同样遵守ZAB协议的服务器启动后加人到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加人的服务器就会自觉地进人数据恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。正如上文介绍中所说的,ZooKeeper设计成只允许唯一的一个Leader服务器来进行事务请求的处理。Leader服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;而如果集群中的其他机器接收到客户端的事务请求,那么这些非Leader服务器会首先将这个事务请求转发给Leader服务器。 - -关于 **ZAB 协议&Paxos算法** 需要讲和理解的东西太多了,说实话,笔主到现在不太清楚这俩兄弟的具体原理和实现过程。推荐阅读下面两篇文章: - -- [图解 Paxos 一致性协议](http://codemacro.com/2014/10/15/explain-poxos/) -- [Zookeeper ZAB 协议分析](https://dbaplus.cn/news-141-1875-1.html) - -关于如何使用 zookeeper 实现分布式锁,可以查看下面这篇文章: - -- -[10分钟看懂!基于Zookeeper的分布式锁](https://blog.csdn.net/qiangcuo6087/article/details/79067136) - -## 六 总结 - -通过阅读本文,想必大家已从 **①ZooKeeper的由来。** -> **②ZooKeeper 到底是什么 。**-> **③ ZooKeeper 的一些重要概念**(会话(Session)、 Znode、版本、Watcher、ACL)-> **④ZooKeeper 的特点。** -> **⑤ZooKeeper 的设计目标。**-> **⑥ ZooKeeper 集群角色介绍** (Leader、Follower 和 Observer 三种角色)-> **⑦ZooKeeper &ZAB 协议&Paxos算法。** 这七点了解了 ZooKeeper 。 - -## 参考 - -- 《从Paxos到Zookeeper 》 -- https://cwiki.apache.org/confluence/display/ZOOKEEPER/ProjectDescription -- https://cwiki.apache.org/confluence/display/ZOOKEEPER/Index -- https://www.cnblogs.com/raphael5200/p/5285583.html -- https://zhuanlan.zhihu.com/p/30024403 - diff --git a/docs/system-design/framework/ZooKeeper数据模型和常见命令.md b/docs/system-design/framework/ZooKeeper数据模型和常见命令.md deleted file mode 100644 index eceaf229..00000000 --- a/docs/system-design/framework/ZooKeeper数据模型和常见命令.md +++ /dev/null @@ -1,200 +0,0 @@ - - -- [ZooKeeper 数据模型](#zookeeper-数据模型) -- [ZNode\(数据节点\)的结构](#znode数据节点的结构) -- [测试 ZooKeeper 中的常见操作](#测试-zookeeper-中的常见操作) - - [连接 ZooKeeper 服务](#连接-zookeeper-服务) - - [查看常用命令\(help 命令\)](#查看常用命令help-命令) - - [创建节点\(create 命令\)](#创建节点create-命令) - - [更新节点数据内容\(set 命令\)](#更新节点数据内容set-命令) - - [获取节点的数据\(get 命令\)](#获取节点的数据get-命令) - - [查看某个目录下的子节点\(ls 命令\)](#查看某个目录下的子节点ls-命令) - - [查看节点状态\(stat 命令\)](#查看节点状态stat-命令) - - [查看节点信息和状态\(ls2 命令\)](#查看节点信息和状态ls2-命令) - - [删除节点\(delete 命令\)](#删除节点delete-命令) -- [参考](#参考) - - - -> 看本文之前如果你没有安装 ZooKeeper 的话,可以参考这篇文章:[《使用 SpringBoot+Dubbo 搭建一个简单分布式服务》](https://github.com/Snailclimb/springboot-integration-examples/blob/master/md/springboot-dubbo.md) 的 “开始实战 1 :zookeeper 环境安装搭建” 这部分进行安装(Centos7.4 环境下)。如果你想对 ZooKeeper 有一个整体了解的话,可以参考这篇文章:[《可能是把 ZooKeeper 概念讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/master/%E4%B8%BB%E6%B5%81%E6%A1%86%E6%9E%B6/ZooKeeper.md) - -### ZooKeeper 数据模型 - -ZNode(数据节点)是 ZooKeeper 中数据的最小单元,每个ZNode上都可以保存数据,同时还是可以有子节点(这就像树结构一样,如下图所示)。可以看出,节点路径标识方式和Unix文件 -系统路径非常相似,都是由一系列使用斜杠"/"进行分割的路径表示,开发人员可以向这个节点中写人数据,也可以在节点下面创建子节点。这些操作我们后面都会介绍到。 -![ZooKeeper 数据模型](https://images.gitbook.cn/95a192b0-1c56-11e9-9a8e-f3b01b1ea9aa) - -提到 ZooKeeper 数据模型,还有一个不得不得提的东西就是 **事务 ID** 。事务的ACID(Atomic:原子性;Consistency:一致性;Isolation:隔离性;Durability:持久性)四大特性我在这里就不多说了,相信大家也已经挺腻了。 - -在Zookeeper中,事务是指能够改变 ZooKeeper 服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新和客户端会话创建与失效等操作。**对于每一个事务请求,ZooKeeper 都会为其分配一个全局唯一的事务ID,用 ZXID 来表示**,通常是一个64位的数字。每一个ZXID对应一次更新操作,**从这些 ZXID 中可以间接地识别出Zookeeper处理这些更新操作请求的全局顺序**。 - -### 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 对象的格式化输出。Stat 类中包含了一个数据节点的所有状态信息的字段,包括事务ID、版本信息和子节点个数等,如下图所示(图源:《从Paxos到Zookeeper 分布式一致性原理与实践》,下面会介绍通过 stat 命令查看数据节点的状态)。 - -**Stat 类:** - -![Stat 类](https://images.gitbook.cn/a841e740-1c55-11e9-b5b7-abf0ec0c666a) - -关于数据节点的状态信息说明(也就是对Stat 类中的各字段进行说明),可以参考下图(图源:《从Paxos到Zookeeper 分布式一致性原理与实践》)。 - -![数据节点的状态信息说明](https://images.gitbook.cn/f44d8630-1c55-11e9-b5b7-abf0ec0c666a) - -### 测试 ZooKeeper 中的常见操作 - - -#### 连接 ZooKeeper 服务 - -进入安装 ZooKeeper文件夹的 bin 目录下执行下面的命令连接 ZooKeeper 服务(Linux环境下)(连接之前首选要确定你的 ZooKeeper 服务已经启动成功)。 - -```shell -./zkCli.sh -server 127.0.0.1:2181 -``` -![连接 ZooKeeper 服务](https://images.gitbook.cn/153b84c0-1c59-11e9-9a8e-f3b01b1ea9aa) - -从上图可以看出控制台打印出了很多信息,包括我们的主机名称、JDK 版本、操作系统等等。如果你成功看到这些信息,说明你成功连接到 ZooKeeper 服务。 - -#### 查看常用命令(help 命令) - -help 命令查看 zookeeper 常用命令 - -![help 命令](https://images.gitbook.cn/091db640-1c59-11e9-b5b7-abf0ec0c666a) - -#### 创建节点(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 -``` - -#### 更新节点数据内容(set 命令) - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 11] set /node1 "set node1" -``` - -#### 获取节点的数据(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 - -``` - -#### 查看某个目录下的子节点(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级,并不递归) - -#### 查看节点状态(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(数据节点)的结构” 这部分已经介绍到。 - -#### 查看节点信息和状态(ls2 命令) - - -ls2 命令更像是 ls 命令和 stat 命令的结合。ls2 命令返回的信息包括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 - -``` - -#### 删除节点(delete 命令) - -这个命令很简单,但是需要注意的一点是如果你要删除某一个节点,那么这个节点必须无子节点才行。 - -```shell -[zk: 127.0.0.1:2181(CONNECTED) 3] delete /node1/node1.1 -``` - -在后面我会介绍到 Java 客户端 API的使用以及开源 Zookeeper 客户端 ZkClient 和 Curator 的使用。 - - -### 参考 - -- 《从Paxos到Zookeeper 分布式一致性原理与实践》 - diff --git a/docs/system-design/framework/spring/Spring-Design-Patterns.md b/docs/system-design/framework/spring/Spring-Design-Patterns.md index 968937f1..b37e318b 100644 --- a/docs/system-design/framework/spring/Spring-Design-Patterns.md +++ b/docs/system-design/framework/spring/Spring-Design-Patterns.md @@ -30,7 +30,7 @@ Design Patterns(设计模式) 表示面向对象软件开发中最好的计算 ## 控制反转(IoC)和依赖注入(DI) -**IoC(Inversion of Control,控制翻转)** 是Spring 中一个非常非常重要的概念,它不是什么技术,而是一种解耦的设计思想。它的主要目的是借助于“第三方”(Spring 中的 IOC 容器) 实现具有依赖关系的对象之间的解耦(IOC容易管理对象,你只管使用即可),从而降低代码之间的耦合度。**IOC 是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC原则。** +**IoC(Inversion of Control,控制反转)** 是Spring 中一个非常非常重要的概念,它不是什么技术,而是一种解耦的设计思想。它的主要目的是借助于“第三方”(Spring 中的 IOC 容器) 实现具有依赖关系的对象之间的解耦(IOC容器管理对象,你只管使用即可),从而降低代码之间的耦合度。**IOC 是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC原则。** ![ioc-patterns](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ioc-patterns.png) @@ -38,7 +38,7 @@ Design Patterns(设计模式) 表示面向对象软件开发中最好的计算 在实际项目中一个 Service 类如果有几百甚至上千个类作为它的底层,我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IOC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。关于Spring IOC 的理解,推荐看这一下知乎的一个回答: ,非常不错。 -**控制翻转怎么理解呢?** 举个例子:"对象a 依赖了对象 b,当对象 a 需要使用 对象 b的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象a 和对象 b 之前就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b的时候, 我们可以指定 IOC 容器去创建一个对象b注入到对象 a 中"。 对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权翻转,这就是控制反转名字的由来。 +**控制反转怎么理解呢?** 举个例子:"对象a 依赖了对象 b,当对象 a 需要使用 对象 b的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象a 和对象 b 之前就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b的时候, 我们可以指定 IOC 容器去创建一个对象b注入到对象 a 中"。 对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权反转,这就是控制反转名字的由来。 **DI(Dependecy Inject,依赖注入)是实现控制反转的一种设计模式,依赖注入就是将实例变量传入到一个对象中去。** @@ -212,7 +212,7 @@ Spring 中默认存在以下事件,他们都是对 `ApplicationContextEvent` #### 事件监听者角色 -`ApplicationListener` 充当了事件监听者角色,它是一个接口,里面只定义了一个 `onApplicationEvent()`方法来处理`ApplicationEvent`。`ApplicationListener`接口类源码如下,可以看出接口定义看出接口中的事件只要实现了 `ApplicationEvent`就可以了。所以,在 Spring中我们只要实现 `ApplicationListener` 接口实现 `onApplicationEvent()` 方法即可完成监听事件 +`ApplicationListener` 充当了事件监听者角色,它是一个接口,里面只定义了一个 `onApplicationEvent()`方法来处理`ApplicationEvent`。`ApplicationListener`接口类源码如下,可以看出接口定义看出接口中的事件只要实现了 `ApplicationEvent`就可以了。所以,在 Spring中我们只要实现 `ApplicationListener` 接口的 `onApplicationEvent()` 方法即可完成监听事件 ```java package org.springframework.context; diff --git a/docs/system-design/framework/spring/Spring.md b/docs/system-design/framework/spring/Spring.md index 52f195a3..18efadcc 100644 --- a/docs/system-design/framework/spring/Spring.md +++ b/docs/system-design/framework/spring/Spring.md @@ -52,13 +52,11 @@ AOP思想的实现一般都是基于 **代理模式** ,在JAVA中一般采用J ### IOC -Spring IOC的初始化过程: -![Spring IOC的初始化过程](https://user-gold-cdn.xitu.io/2018/5/22/16387903ee72c831?w=709&h=56&f=png&s=4673) - - [[Spring框架]Spring IOC的原理及详解。](https://www.cnblogs.com/wang-meng/p/5597490.html) - - [Spring IOC核心源码学习](https://yikun.github.io/2015/05/29/Spring-IOC核心源码学习/) :比较简短,推荐阅读。 - [Spring IOC 容器源码分析](https://javadoop.com/post/spring-ioc) :强烈推荐,内容详尽,而且便于阅读。 +- [Bean初始化过程](https://www.qzztf.com/2019/08/21/Bean%E5%88%9D%E5%A7%8B%E5%8C%96/#Bean-%E5%AE%9E%E4%BE%8B%E5%8C%96) + ## Spring事务管理 diff --git a/docs/system-design/framework/spring/SpringBean.md b/docs/system-design/framework/spring/SpringBean.md index 4e8279e7..3291e7fa 100644 --- a/docs/system-design/framework/spring/SpringBean.md +++ b/docs/system-design/framework/spring/SpringBean.md @@ -177,7 +177,7 @@ public class GiraffeService { ``` -需要注意的是自定义的init-method和post-method方法可以抛异常但是不能有参数。 +需要注意的是自定义的init-method和destroy-method方法可以抛异常但是不能有参数。 这种方式比较推荐,因为可以自己创建方法,无需将Bean的实现直接依赖于spring的框架。 @@ -303,7 +303,7 @@ public class CustomerBeanPostProcessor implements BeanPostProcessor { - 如果涉及到一些属性值 利用set方法设置一些属性值。 - 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字。 - 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 -- 如果Bean实现了BeanFactoryAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 +- 如果Bean实现了BeanFactoryAware接口,调用setBeanFactory()方法,传入BeanFactory对象的实例。 - 与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 - 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessBeforeInitialization()方法 - 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。 diff --git a/docs/system-design/framework/spring/SpringInterviewQuestions.md b/docs/system-design/framework/spring/SpringInterviewQuestions.md index ff49cbbc..401b8255 100644 --- a/docs/system-design/framework/spring/SpringInterviewQuestions.md +++ b/docs/system-design/framework/spring/SpringInterviewQuestions.md @@ -54,7 +54,7 @@ Spring 官网列出的 Spring 的 6 个特征: Reference: -- https://dzone.com/articles/spring-framework-restcontroller-vs-controller(图片来源) +- https://dzone.com/articles/spring-framework-restcontroller-vs-controller (图片来源) - https://javarevisited.blogspot.com/2017/08/difference-between-restcontroller-and-controller-annotations-spring-mvc-rest.html?m=1 ## 4. Spring IOC & AOP @@ -63,7 +63,7 @@ Reference: #### IoC -IoC(Inverse of Control:控制反转)是一种**设计思想**,就是 **将原本在程序中手动创建对象的控制权,交由Spring框架来管理。** IoC 在其他语言中也有应用,并非 Spirng 特有。 **IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。** +IoC(Inverse of Control:控制反转)是一种**设计思想**,就是 **将原本在程序中手动创建对象的控制权,交由Spring框架来管理。** IoC 在其他语言中也有应用,并非 Spring 特有。 **IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。** 将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 **IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。** 在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。 @@ -170,7 +170,7 @@ public OneService getService(status) { - `@Component` :通用的注解,可标注任意类为 `Spring` 组件。如果一个Bean不知道属于哪个层,可以使用`@Component` 注解标注。 - `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 - `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。 -- `@Controller` : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。 +- `@Controller` : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。 ### 5.5 Spring 中的 bean 生命周期? @@ -203,7 +203,7 @@ public OneService getService(status) { 谈到这个问题,我们不得不提提之前 Model1 和 Model2 这两个没有 Spring MVC 的时代。 -- **Model1 时代** : 很多学 Java 后端比较晚的朋友可能并没有接触过 Model1 模式下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。这个模式下 JSP 即是控制层又是表现层。显而易见,这种模式存在很多问题。比如①将控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;②前端和后端相互依赖,难以进行测试并且开发效率极低; +- **Model1 时代** : 很多学 Java 后端比较晚的朋友可能并没有接触过 Model1 模式下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。这个模式下 JSP 既是控制层又是表现层。显而易见,这种模式存在很多问题。比如①将控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;②前端和后端相互依赖,难以进行测试并且开发效率极低; - **Model2 时代** :学过 Servlet 并做过相关 Demo 的朋友应该了解“Java Bean(Model)+ JSP(View,)+Servlet(Controller) ”这种开发模式,这就是早期的 JavaWeb MVC 开发模式。Model:系统涉及的数据,也就是 dao 和 bean。View:展示模型中的数据,只是用来展示。Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。 Model2 模式下还存在很多问题,Model2的抽象和封装程度还远远不够,使用Model2进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。于是很多JavaWeb开发相关的 MVC 框架应运而生比如Struts2,但是 Struts2 比较笨重。随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。 @@ -226,7 +226,7 @@ MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring M 1. 客户端(浏览器)发送请求,直接请求到 `DispatcherServlet`。 2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。 3. 解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由 `HandlerAdapter` 适配器处理。 -4. `HandlerAdapter` 会根据 `Handler `来调用真正的处理器开处理请求,并处理相应的业务逻辑。 +4. `HandlerAdapter` 会根据 `Handler `来调用真正的处理器来处理请求,并处理相应的业务逻辑。 5. 处理器处理完业务后,会返回一个 `ModelAndView` 对象,`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 6. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 diff --git a/docs/system-design/framework/spring/SpringMVC-Principle.md b/docs/system-design/framework/spring/SpringMVC-Principle.md index 0efcd3f9..0a2cc5f4 100644 --- a/docs/system-design/framework/spring/SpringMVC-Principle.md +++ b/docs/system-design/framework/spring/SpringMVC-Principle.md @@ -46,7 +46,7 @@ SpringMVC 框架是以请求为驱动,围绕 Servlet 设计,将请求发给 **简单来说:** -客户端发送请求-> 前端控制器 DispatcherServlet 接受客户端请求 -> 找到处理器映射 HandlerMapping 解析请求对应的 Handler-> HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑 -> 处理器返回一个模型视图 ModelAndView -> 视图解析器进行解析 -> 返回一个视图对象->前端控制器 DispatcherServlet 渲染数据(Moder)->将得到视图对象返回给用户 +客户端发送请求-> 前端控制器 DispatcherServlet 接受客户端请求 -> 找到处理器映射 HandlerMapping 解析请求对应的 Handler-> HandlerAdapter 会根据 Handler 来调用真正的处理器来处理请求,并处理相应的业务逻辑 -> 处理器返回一个模型视图 ModelAndView -> 视图解析器进行解析 -> 返回一个视图对象->前端控制器 DispatcherServlet 渲染数据(Model)->将得到视图对象返回给用户 diff --git a/docs/system-design/framework/spring/images/spring-transaction/a616b84d-9eea-4ad1-b4fc-461ff05e951d.png b/docs/system-design/framework/spring/images/spring-transaction/a616b84d-9eea-4ad1-b4fc-461ff05e951d.png new file mode 100644 index 00000000..f99c106a Binary files /dev/null and b/docs/system-design/framework/spring/images/spring-transaction/a616b84d-9eea-4ad1-b4fc-461ff05e951d.png differ diff --git a/docs/system-design/framework/spring/images/spring-transaction/ae964c2c-7289-441c-bddd-511161f51ee1.png b/docs/system-design/framework/spring/images/spring-transaction/ae964c2c-7289-441c-bddd-511161f51ee1.png new file mode 100644 index 00000000..c50b8748 Binary files /dev/null and b/docs/system-design/framework/spring/images/spring-transaction/ae964c2c-7289-441c-bddd-511161f51ee1.png differ diff --git a/docs/system-design/framework/spring/images/spring-transaction/bda7231b-ab05-4e23-95ee-89ac90ac7fcf.png b/docs/system-design/framework/spring/images/spring-transaction/bda7231b-ab05-4e23-95ee-89ac90ac7fcf.png new file mode 100644 index 00000000..4232f8b9 Binary files /dev/null and b/docs/system-design/framework/spring/images/spring-transaction/bda7231b-ab05-4e23-95ee-89ac90ac7fcf.png differ diff --git a/docs/system-design/framework/spring/images/spring-transaction/ed279f05-f5ad-443e-84e9-513a9e777139.png b/docs/system-design/framework/spring/images/spring-transaction/ed279f05-f5ad-443e-84e9-513a9e777139.png new file mode 100644 index 00000000..d868e051 Binary files /dev/null and b/docs/system-design/framework/spring/images/spring-transaction/ed279f05-f5ad-443e-84e9-513a9e777139.png differ diff --git a/docs/system-design/framework/spring/images/spring-transaction/f6c6f0aa-0f26-49e1-84b3-7f838c7379d1.png b/docs/system-design/framework/spring/images/spring-transaction/f6c6f0aa-0f26-49e1-84b3-7f838c7379d1.png new file mode 100644 index 00000000..d6b83731 Binary files /dev/null and b/docs/system-design/framework/spring/images/spring-transaction/f6c6f0aa-0f26-49e1-84b3-7f838c7379d1.png differ diff --git a/docs/system-design/framework/spring/spring-annotations.md b/docs/system-design/framework/spring/spring-annotations.md new file mode 100644 index 00000000..610c0908 --- /dev/null +++ b/docs/system-design/framework/spring/spring-annotations.md @@ -0,0 +1,1009 @@ + +### 文章目录 + + +- [文章目录](#%e6%96%87%e7%ab%a0%e7%9b%ae%e5%bd%95) +- [0.前言](#0%e5%89%8d%e8%a8%80) +- [1. `@SpringBootApplication`](#1-springbootapplication) +- [2. Spring Bean 相关](#2-spring-bean-%e7%9b%b8%e5%85%b3) + - [2.1. `@Autowired`](#21-autowired) + - [2.2. `@Component`,`@Repository`,`@Service`, `@Controller`](#22-componentrepositoryservice-controller) + - [2.3. `@RestController`](#23-restcontroller) + - [2.4. `@Scope`](#24-scope) + - [2.5. `@Configuration`](#25-configuration) +- [3. 处理常见的 HTTP 请求类型](#3-%e5%a4%84%e7%90%86%e5%b8%b8%e8%a7%81%e7%9a%84-http-%e8%af%b7%e6%b1%82%e7%b1%bb%e5%9e%8b) + - [3.1. GET 请求](#31-get-%e8%af%b7%e6%b1%82) + - [3.2. POST 请求](#32-post-%e8%af%b7%e6%b1%82) + - [3.3. PUT 请求](#33-put-%e8%af%b7%e6%b1%82) + - [3.4. **DELETE 请求**](#34-delete-%e8%af%b7%e6%b1%82) + - [3.5. **PATCH 请求**](#35-patch-%e8%af%b7%e6%b1%82) +- [4. 前后端传值](#4-%e5%89%8d%e5%90%8e%e7%ab%af%e4%bc%a0%e5%80%bc) + - [4.1. `@PathVariable` 和 `@RequestParam`](#41-pathvariable-%e5%92%8c-requestparam) + - [4.2. `@RequestBody`](#42-requestbody) +- [5. 读取配置信息](#5-%e8%af%bb%e5%8f%96%e9%85%8d%e7%bd%ae%e4%bf%a1%e6%81%af) + - [5.1. `@value`(常用)](#51-value%e5%b8%b8%e7%94%a8) + - [5.2. `@ConfigurationProperties`(常用)](#52-configurationproperties%e5%b8%b8%e7%94%a8) + - [5.3. `PropertySource`(不常用)](#53-propertysource%e4%b8%8d%e5%b8%b8%e7%94%a8) +- [6. 参数校验](#6-%e5%8f%82%e6%95%b0%e6%a0%a1%e9%aa%8c) + - [6.1. 一些常用的字段验证的注解](#61-%e4%b8%80%e4%ba%9b%e5%b8%b8%e7%94%a8%e7%9a%84%e5%ad%97%e6%ae%b5%e9%aa%8c%e8%af%81%e7%9a%84%e6%b3%a8%e8%a7%a3) + - [6.2. 验证请求体(RequestBody)](#62-%e9%aa%8c%e8%af%81%e8%af%b7%e6%b1%82%e4%bd%93requestbody) + - [6.3. 验证请求参数(Path Variables 和 Request Parameters)](#63-%e9%aa%8c%e8%af%81%e8%af%b7%e6%b1%82%e5%8f%82%e6%95%b0path-variables-%e5%92%8c-request-parameters) +- [7. 全局处理 Controller 层异常](#7-%e5%85%a8%e5%b1%80%e5%a4%84%e7%90%86-controller-%e5%b1%82%e5%bc%82%e5%b8%b8) +- [8. JPA 相关](#8-jpa-%e7%9b%b8%e5%85%b3) + - [8.1. 创建表](#81-%e5%88%9b%e5%bb%ba%e8%a1%a8) + - [8.2. 创建主键](#82-%e5%88%9b%e5%bb%ba%e4%b8%bb%e9%94%ae) + - [8.3. 设置字段类型](#83-%e8%ae%be%e7%bd%ae%e5%ad%97%e6%ae%b5%e7%b1%bb%e5%9e%8b) + - [8.4. 指定不持久化特定字段](#84-%e6%8c%87%e5%ae%9a%e4%b8%8d%e6%8c%81%e4%b9%85%e5%8c%96%e7%89%b9%e5%ae%9a%e5%ad%97%e6%ae%b5) + - [8.5. 声明大字段](#85-%e5%a3%b0%e6%98%8e%e5%a4%a7%e5%ad%97%e6%ae%b5) + - [8.6. 创建枚举类型的字段](#86-%e5%88%9b%e5%bb%ba%e6%9e%9a%e4%b8%be%e7%b1%bb%e5%9e%8b%e7%9a%84%e5%ad%97%e6%ae%b5) + - [8.7. 增加审计功能](#87-%e5%a2%9e%e5%8a%a0%e5%ae%a1%e8%ae%a1%e5%8a%9f%e8%83%bd) + - [8.8. 删除/修改数据](#88-%e5%88%a0%e9%99%a4%e4%bf%ae%e6%94%b9%e6%95%b0%e6%8d%ae) + - [8.9. 关联关系](#89-%e5%85%b3%e8%81%94%e5%85%b3%e7%b3%bb) +- [9. 事务 `@Transactional`](#9-%e4%ba%8b%e5%8a%a1-transactional) +- [10. json 数据处理](#10-json-%e6%95%b0%e6%8d%ae%e5%a4%84%e7%90%86) + - [10.1. 过滤 json 数据](#101-%e8%bf%87%e6%bb%a4-json-%e6%95%b0%e6%8d%ae) + - [10.2. 格式化 json 数据](#102-%e6%a0%bc%e5%bc%8f%e5%8c%96-json-%e6%95%b0%e6%8d%ae) + - [10.3. 扁平化对象](#103-%e6%89%81%e5%b9%b3%e5%8c%96%e5%af%b9%e8%b1%a1) +- [11. 测试相关](#11-%e6%b5%8b%e8%af%95%e7%9b%b8%e5%85%b3) + + +### 0.前言 + +_大家好,我是 Guide 哥!这是我的 221 篇优质原创文章。如需转载,请在文首注明地址,蟹蟹!_ + +本文已经收录进我的 75K Star 的 Java 开源项目 JavaGuide:[https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide)。 + +可以毫不夸张地说,这篇文章介绍的 Spring/SpringBoot 常用注解基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个注解我都说了具体用法,掌握搞懂,使用 SpringBoot 来开发项目基本没啥大问题了! + +**为什么要写这篇文章?** + +最近看到网上有一篇关于 SpringBoot 常用注解的文章被转载的比较多,我看了文章内容之后属实觉得质量有点低,并且有点会误导没有太多实际使用经验的人(这些人又占据了大多数)。所以,自己索性花了大概 两天时间简单总结一下了。 + +**因为我个人的能力和精力有限,如果有任何不对或者需要完善的地方,请帮忙指出!Guide 哥感激不尽!** + +### 1. `@SpringBootApplication` + +这里先单独拎出`@SpringBootApplication` 注解说一下,虽然我们一般不会主动去使用它。 + +_Guide 哥:这个注解是 Spring Boot 项目的基石,创建 SpringBoot 项目之后会默认在主类加上。_ + +```java +@SpringBootApplication +public class SpringSecurityJwtGuideApplication { + public static void main(java.lang.String[] args) { + SpringApplication.run(SpringSecurityJwtGuideApplication.class, args); + } +} +``` + +我们可以把 `@SpringBootApplication`看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合。 + +```java +package org.springframework.boot.autoconfigure; +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@SpringBootConfiguration +@EnableAutoConfiguration +@ComponentScan(excludeFilters = { + @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) +public @interface SpringBootApplication { + ...... +} + +package org.springframework.boot; +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Configuration +public @interface SpringBootConfiguration { + +} +``` + +根据 SpringBoot 官网,这三个注解的作用分别是: + +- `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制 +- `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的 bean,注解默认会扫描该类所在的包下所有的类。 +- `@Configuration`:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类 + +### 2. Spring Bean 相关 + +#### 2.1. `@Autowired` + +自动导入对象到类中,被注入进的类同样要被 Spring 容器管理比如:Service 类注入到 Controller 类中。 + +```java +@Service +public class UserService { + ...... +} + +@RestController +@RequestMapping("/users") +public class UserController { + @Autowired + private UserService userService; + ...... +} +``` + +#### 2.2. `@Component`,`@Repository`,`@Service`, `@Controller` + +我们一般使用 `@Autowired` 注解让 Spring 容器帮我们自动装配 bean。要想把类标识成可用于 `@Autowired` 注解自动装配的 bean 的类,可以采用以下注解实现: + +- `@Component` :通用的注解,可标注任意类为 `Spring` 组件。如果一个 Bean 不知道属于哪个层,可以使用`@Component` 注解标注。 +- `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 +- `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。 +- `@Controller` : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。 + +#### 2.3. `@RestController` + +`@RestController`注解是`@Controller和`@`ResponseBody`的合集,表示这是个控制器 bean,并且是将函数的返回值直 接填入 HTTP 响应体中,是 REST 风格的控制器。 + +_Guide 哥:现在都是前后端分离,说实话我已经很久没有用过`@Controller`。如果你的项目太老了的话,就当我没说。_ + +单独使用 `@Controller` 不加 `@ResponseBody`的话一般使用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。`@Controller` +`@ResponseBody` 返回 JSON 或 XML 形式数据 + +关于`@RestController` 和 `@Controller`的对比,请看这篇文章:[@RestController vs @Controller](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485544&idx=1&sn=3cc95b88979e28fe3bfe539eb421c6d8&chksm=cea247a3f9d5ceb5e324ff4b8697adc3e828ecf71a3468445e70221cce768d1e722085359907&token=1725092312&lang=zh_CN#rd)。 + +#### 2.4. `@Scope` + +声明 Spring Bean 的作用域,使用方法: + +```java +@Bean +@Scope("singleton") +public Person personSingleton() { + return new Person(); +} +``` + +**四种常见的 Spring Bean 的作用域:** + +- singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。 +- prototype : 每次请求都会创建一个新的 bean 实例。 +- request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。 +- session : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。 + +#### 2.5. `@Configuration` + +一般用来声明配置类,可以使用 `@Component`注解替代,不过使用`@Configuration`注解声明配置类更加语义化。 + +```java +@Configuration +public class AppConfig { + @Bean + public TransferService transferService() { + return new TransferServiceImpl(); + } + +} +``` + +### 3. 处理常见的 HTTP 请求类型 + +**5 种常见的请求类型:** + +- **GET** :请求从服务器获取特定资源。举个例子:`GET /users`(获取所有学生) +- **POST** :在服务器上创建一个新的资源。举个例子:`POST /users`(创建学生) +- **PUT** :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:`PUT /users/12`(更新编号为 12 的学生) +- **DELETE** :从服务器删除特定的资源。举个例子:`DELETE /users/12`(删除编号为 12 的学生) +- **PATCH** :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。 + +#### 3.1. GET 请求 + +`@GetMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.GET)` + +```java +@GetMapping("/users") +public ResponseEntity> getAllUsers() { + return userRepository.findAll(); +} +``` + +#### 3.2. POST 请求 + +`@PostMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.POST)` + +关于`@RequestBody`注解的使用,在下面的“前后端传值”这块会讲到。 + +```java +@PostMapping("/users") +public ResponseEntity createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) { + return userRespository.save(user); +} +``` + +#### 3.3. PUT 请求 + +`@PutMapping("/users/{userId}")` 等价于`@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)` + +```java +@PutMapping("/users/{userId}") +public ResponseEntity updateUser(@PathVariable(value = "userId") Long userId, + @Valid @RequestBody UserUpdateRequest userUpdateRequest) { + ...... +} +``` + +#### 3.4. **DELETE 请求** + +`@DeleteMapping("/users/{userId}")`等价于`@RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)` + +```java +@DeleteMapping("/users/{userId}") +public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){ + ...... +} +``` + +#### 3.5. **PATCH 请求** + +一般实际项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。 + +```java + @PatchMapping("/profile") + public ResponseEntity updateStudent(@RequestBody StudentUpdateRequest studentUpdateRequest) { + studentRepository.updateDetail(studentUpdateRequest); + return ResponseEntity.ok().build(); + } +``` + +### 4. 前后端传值 + +**掌握前后端传值的正确姿势,是你开始 CRUD 的第一步!** + +#### 4.1. `@PathVariable` 和 `@RequestParam` + +`@PathVariable`用于获取路径参数,`@RequestParam`用于获取查询参数。 + +举个简单的例子: + +```java +@GetMapping("/klasses/{klassId}/teachers") +public List getKlassRelatedTeachers( + @PathVariable("klassId") Long klassId, + @RequestParam(value = "type", required = false) String type ) { +... +} +``` + +如果我们请求的 url 是:`/klasses/{123456}/teachers?type=web` + +那么我们服务获取到的数据就是:`klassId=123456,type=web`。 + +#### 4.2. `@RequestBody` + +用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且**Content-Type 为 application/json** 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用`HttpMessageConverter`或者自定义的`HttpMessageConverter`将请求的 body 中的 json 字符串转换为 java 对象。 + +我用一个简单的例子来给演示一下基本使用! + +我们有一个注册的接口: + +```java +@PostMapping("/sign-up") +public ResponseEntity signUp(@RequestBody @Valid UserRegisterRequest userRegisterRequest) { + userService.save(userRegisterRequest); + return ResponseEntity.ok().build(); +} +``` + +`UserRegisterRequest`对象: + +```java +@Data +@AllArgsConstructor +@NoArgsConstructor +public class UserRegisterRequest { + @NotBlank + private String userName; + @NotBlank + private String password; + @NotBlank + private String fullName; +} +``` + +我们发送 post 请求到这个接口,并且 body 携带 JSON 数据: + +```json +{"userName":"coder","fullName":"shuangkou","password":"123456"} +``` + +这样我们的后端就可以直接把 json 格式的数据映射到我们的 `UserRegisterRequest` 类上。 + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-08/663d1ec1-7ebc-41ab-8431-159dc1ec6589.png) + +👉 需要注意的是:**一个请求方法只可以有一个`@RequestBody`,但是可以有多个`@RequestParam`和`@PathVariable`**。 如果你的方法必须要用两个 `@RequestBody`来接受数据的话,大概率是你的数据库设计或者系统设计出问题了! + +### 5. 读取配置信息 + +**很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信认证的相关配置信息等等放到配置文件中。** + +**下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。** + +我们的数据源`application.yml`内容如下:: + +```yaml +wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油! + +my-profile: + name: Guide哥 + email: koushuangbwcx@163.com + +library: + location: 湖北武汉加油中国加油 + books: + - name: 天才基本法 + description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。 + - name: 时间的秩序 + description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。 + - name: 了不起的我 + description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻? +``` + +#### 5.1. `@value`(常用) + +使用 `@Value("${property}")` 读取比较简单的配置信息: + +```java +@Value("${wuhan2020}") +String wuhan2020; +``` + +#### 5.2. `@ConfigurationProperties`(常用) + +通过`@ConfigurationProperties`读取配置信息并与 bean 绑定。 + +```java +@Component +@ConfigurationProperties(prefix = "library") +class LibraryProperties { + @NotEmpty + private String location; + private List books; + + @Setter + @Getter + @ToString + static class Book { + String name; + String description; + } + 省略getter/setter + ...... +} +``` + +你可以像使用普通的 Spring bean 一样,将其注入到类中使用。 + +#### 5.3. `PropertySource`(不常用) + +`@PropertySource`读取指定 properties 文件 + +```java +@Component +@PropertySource("classpath:website.properties") + +class WebSite { + @Value("${url}") + private String url; + + 省略getter/setter + ...... +} +``` + +更多内容请查看我的这篇文章:《[10 分钟搞定 SpringBoot 如何优雅读取配置文件?](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486181&idx=2&sn=10db0ae64ef501f96a5b0dbc4bd78786&chksm=cea2452ef9d5cc384678e456427328600971180a77e40c13936b19369672ca3e342c26e92b50&token=816772476&lang=zh_CN#rd)》 。 + +### 6. 参数校验 + +**数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。** + +**JSR(Java Specification Requests)** 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便! + +校验的时候我们实际用的是 **Hibernate Validator** 框架。Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现。 + +SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。如下图所示(通过 idea 插件—Maven Helper 生成): + +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-08/c7bacd12-1c1a-4e41-aaaf-4cad840fc073.png) + +非 SpringBoot 项目需要自行引入相关依赖包,这里不多做讲解,具体可以查看我的这篇文章:《[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485783&idx=1&sn=a407f3b75efa17c643407daa7fb2acd6&chksm=cea2469cf9d5cf8afbcd0a8a1c9cc4294d6805b8e01bee6f76bb2884c5bc15478e91459def49&token=292197051&lang=zh_CN#rd)》。 + +👉 需要注意的是: **所有的注解,推荐使用 JSR 注解,即`javax.validation.constraints`,而不是`org.hibernate.validator.constraints`** + +#### 6.1. 一些常用的字段验证的注解 + +- `@NotEmpty` 被注释的字符串的不能为 null 也不能为空 +- `@NotBlank` 被注释的字符串非 null,并且必须包含一个非空白字符 +- `@Null` 被注释的元素必须为 null +- `@NotNull` 被注释的元素必须不为 null +- `@AssertTrue` 被注释的元素必须为 true +- `@AssertFalse` 被注释的元素必须为 false +- `@Pattern(regex=,flag=)`被注释的元素必须符合指定的正则表达式 +- `@Email` 被注释的元素必须是 Email 格式。 +- `@Min(value)`被注释的元素必须是一个数字,其值必须大于等于指定的最小值 +- `@Max(value)`被注释的元素必须是一个数字,其值必须小于等于指定的最大值 +- `@DecimalMin(value)`被注释的元素必须是一个数字,其值必须大于等于指定的最小值 +- `@DecimalMax(value)` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 +- `@Size(max=, min=)`被注释的元素的大小必须在指定的范围内 +- `@Digits (integer, fraction)`被注释的元素必须是一个数字,其值必须在可接受的范围内 +- `@Past`被注释的元素必须是一个过去的日期 +- `@Future` 被注释的元素必须是一个将来的日期 +- ...... + +#### 6.2. 验证请求体(RequestBody) + +```java +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Person { + + @NotNull(message = "classId 不能为空") + private String classId; + + @Size(max = 33) + @NotNull(message = "name 不能为空") + private String name; + + @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围") + @NotNull(message = "sex 不能为空") + private String sex; + + @Email(message = "email 格式不正确") + @NotNull(message = "email 不能为空") + private String email; + +} +``` + +我们在需要验证的参数上加上了`@Valid`注解,如果验证失败,它将抛出`MethodArgumentNotValidException`。 + +```java +@RestController +@RequestMapping("/api") +public class PersonController { + + @PostMapping("/person") + public ResponseEntity getPerson(@RequestBody @Valid Person person) { + return ResponseEntity.ok().body(person); + } +} +``` + +#### 6.3. 验证请求参数(Path Variables 和 Request Parameters) + +**一定一定不要忘记在类上加上 `Validated` 注解了,这个参数可以告诉 Spring 去校验方法参数。** + +```java +@RestController +@RequestMapping("/api") +@Validated +public class PersonController { + + @GetMapping("/person/{id}") + public ResponseEntity getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) { + return ResponseEntity.ok().body(id); + } +} +``` + +更多关于如何在 Spring 项目中进行参数校验的内容,请看《[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485783&idx=1&sn=a407f3b75efa17c643407daa7fb2acd6&chksm=cea2469cf9d5cf8afbcd0a8a1c9cc4294d6805b8e01bee6f76bb2884c5bc15478e91459def49&token=292197051&lang=zh_CN#rd)》这篇文章。 + +### 7. 全局处理 Controller 层异常 + +介绍一下我们 Spring 项目必备的全局处理 Controller 层异常。 + +**相关注解:** + +1. `@ControllerAdvice` :注解定义全局异常处理类 +2. `@ExceptionHandler` :注解声明异常处理方法 + +如何使用呢?拿我们在第 5 节参数校验这块来举例子。如果方法参数不对的话就会抛出`MethodArgumentNotValidException`,我们来处理这个异常。 + +```java +@ControllerAdvice +@ResponseBody +public class GlobalExceptionHandler { + + /** + * 请求参数异常处理 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request) { + ...... + } +} +``` + +更多关于 Spring Boot 异常处理的内容,请看我的这两篇文章: + +1. [SpringBoot 处理异常的几种常见姿势](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485568&idx=2&sn=c5ba880fd0c5d82e39531fa42cb036ac&chksm=cea2474bf9d5ce5dcbc6a5f6580198fdce4bc92ef577579183a729cb5d1430e4994720d59b34&token=2133161636&lang=zh_CN#rd) +2. [使用枚举简单封装一个优雅的 Spring Boot 全局异常处理!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486379&idx=2&sn=48c29ae65b3ed874749f0803f0e4d90e&chksm=cea24460f9d5cd769ed53ad7e17c97a7963a89f5350e370be633db0ae8d783c3a3dbd58c70f8&token=1054498516&lang=zh_CN#rd) + +### 8. JPA 相关 + +#### 8.1. 创建表 + +`@Entity`声明一个类对应一个数据库实体。 + +`@Table` 设置表名 + +```java +@Entity +@Table(name = "role") +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + private String description; + 省略getter/setter...... +} +``` + +#### 8.2. 创建主键 + +`@Id` :声明一个字段为主键。 + +使用`@Id`声明之后,我们还需要定义主键的生成策略。我们可以使用 `@GeneratedValue` 指定主键生成策略。 + +**1.通过 `@GeneratedValue`直接使用 JPA 内置提供的四种主键生成策略来指定主键生成策略。** + +```java +@Id +@GeneratedValue(strategy = GenerationType.IDENTITY) +private Long id; +``` + +JPA 使用枚举定义了 4 中常见的主键生成策略,如下: + +_Guide 哥:枚举替代常量的一种用法_ + +```java +public enum GenerationType { + + /** + * 使用一个特定的数据库表格来保存主键 + * 持久化引擎通过关系数据库的一张特定的表格来生成主键, + */ + TABLE, + + /** + *在某些数据库中,不支持主键自增长,比如Oracle、PostgreSQL其提供了一种叫做"序列(sequence)"的机制生成主键 + */ + SEQUENCE, + + /** + * 主键自增长 + */ + IDENTITY, + + /** + *把主键生成策略交给持久化引擎(persistence engine), + *持久化引擎会根据数据库在以上三种主键生成 策略中选择其中一种 + */ + AUTO +} + +``` + +`@GeneratedValue`注解默认使用的策略是`GenerationType.AUTO` + +```java +public @interface GeneratedValue { + + GenerationType strategy() default AUTO; + String generator() default ""; +} +``` + +一般使用 MySQL 数据库的话,使用`GenerationType.IDENTITY`策略比较普遍一点(分布式系统的话需要另外考虑使用分布式 ID)。 + +**2.通过 `@GenericGenerator`声明一个主键策略,然后 `@GeneratedValue`使用这个策略** + +```java +@Id +@GeneratedValue(generator = "IdentityIdGenerator") +@GenericGenerator(name = "IdentityIdGenerator", strategy = "identity") +private Long id; +``` + +等价于: + +```java +@Id +@GeneratedValue(strategy = GenerationType.IDENTITY) +private Long id; +``` + +jpa 提供的主键生成策略有如下几种: + +```java +public class DefaultIdentifierGeneratorFactory + implements MutableIdentifierGeneratorFactory, Serializable, ServiceRegistryAwareService { + + @SuppressWarnings("deprecation") + public DefaultIdentifierGeneratorFactory() { + register( "uuid2", UUIDGenerator.class ); + register( "guid", GUIDGenerator.class ); // can be done with UUIDGenerator + strategy + register( "uuid", UUIDHexGenerator.class ); // "deprecated" for new use + register( "uuid.hex", UUIDHexGenerator.class ); // uuid.hex is deprecated + register( "assigned", Assigned.class ); + register( "identity", IdentityGenerator.class ); + register( "select", SelectGenerator.class ); + register( "sequence", SequenceStyleGenerator.class ); + register( "seqhilo", SequenceHiLoGenerator.class ); + register( "increment", IncrementGenerator.class ); + register( "foreign", ForeignGenerator.class ); + register( "sequence-identity", SequenceIdentityGenerator.class ); + register( "enhanced-sequence", SequenceStyleGenerator.class ); + register( "enhanced-table", TableGenerator.class ); + } + + public void register(String strategy, Class generatorClass) { + LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() ); + final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass ); + if ( previous != null ) { + LOG.debugf( " - overriding [%s]", previous.getName() ); + } + } + +} +``` + +#### 8.3. 设置字段类型 + +`@Column` 声明字段。 + +**示例:** + +设置属性 userName 对应的数据库字段名为 user_name,长度为 32,非空 + +```java +@Column(name = "user_name", nullable = false, length=32) +private String userName; +``` + +设置字段类型并且加默认值,这个还是挺常用的。 + +```java +Column(columnDefinition = "tinyint(1) default 1") +private Boolean enabled; +``` + +#### 8.4. 指定不持久化特定字段 + +`@Transient` :声明不需要与数据库映射的字段,在保存的时候不需要保存进数据库 。 + +如果我们想让`secrect` 这个字段不被持久化,可以使用 `@Transient`关键字声明。 + +```java +Entity(name="USER") +public class User { + + ...... + @Transient + private String secrect; // not persistent because of @Transient + +} +``` + +除了 `@Transient`关键字声明, 还可以采用下面几种方法: + +```java +static String secrect; // not persistent because of static +final String secrect = “Satish”; // not persistent because of final +transient String secrect; // not persistent because of transient +``` + +一般使用注解的方式比较多。 + +#### 8.5. 声明大字段 + +`@Lob`:声明某个字段为大字段。 + +```java +@Lob +private String content; +``` + +更详细的声明: + +```java +@Lob +//指定 Lob 类型数据的获取策略, FetchType.EAGER 表示非延迟 加载,而 FetchType. LAZY 表示延迟加载 ; +@Basic(fetch = FetchType.EAGER) +//columnDefinition 属性指定数据表对应的 Lob 字段类型 +@Column(name = "content", columnDefinition = "LONGTEXT NOT NULL") +private String content; +``` + +#### 8.6. 创建枚举类型的字段 + +可以使用枚举类型的字段,不过枚举字段要用`@Enumerated`注解修饰。 + +```java +public enum Gender { + MALE("男性"), + FEMALE("女性"); + + private String value; + Gender(String str){ + value=str; + } +} +``` + +```java +@Entity +@Table(name = "role") +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + private String description; + @Enumerated(EnumType.STRING) + private Gender gender; + 省略getter/setter...... +} +``` + +数据库里面对应存储的是 MAIL/FEMAIL。 + +#### 8.7. 增加审计功能 + +只要继承了 `AbstractAuditBase`的类都会默认加上下面四个字段。 + +```java +@Data +@AllArgsConstructor +@NoArgsConstructor +@MappedSuperclass +@EntityListeners(value = AuditingEntityListener.class) +public abstract class AbstractAuditBase { + + @CreatedDate + @Column(updatable = false) + @JsonIgnore + private Instant createdAt; + + @LastModifiedDate + @JsonIgnore + private Instant updatedAt; + + @CreatedBy + @Column(updatable = false) + @JsonIgnore + private String createdBy; + + @LastModifiedBy + @JsonIgnore + private String updatedBy; +} + +``` + +我们对应的审计功能对应地配置类可能是下面这样的(Spring Security 项目): + +```java + +@Configuration +@EnableJpaAuditing +public class AuditSecurityConfiguration { + @Bean + AuditorAware auditorAware() { + return () -> Optional.ofNullable(SecurityContextHolder.getContext()) + .map(SecurityContext::getAuthentication) + .filter(Authentication::isAuthenticated) + .map(Authentication::getName); + } +} +``` + +简单介绍一下上面设计到的一些注解: + +1. `@CreatedDate`: 表示该字段为创建时间时间字段,在这个实体被 insert 的时候,会设置值 +2. `@CreatedBy` :表示该字段为创建人,在这个实体被 insert 的时候,会设置值 + + `@LastModifiedDate`、`@LastModifiedBy`同理。 + +`@EnableJpaAuditing`:开启 JPA 审计功能。 + +#### 8.8. 删除/修改数据 + +`@Modifying` 注解提示 JPA 该操作是修改操作,注意还要配合`@Transactional`注解使用。 + +```java +@Repository +public interface UserRepository extends JpaRepository { + + @Modifying + @Transactional(rollbackFor = Exception.class) + void deleteByUserName(String userName); +} +``` + +#### 8.9. 关联关系 + +- `@OneToOne` 声明一对一关系 +- `@OneToMany` 声明一对多关系 +- `@ManyToOne`声明多对一关系 +- `MangToMang`声明多对多关系 + +更多关于 Spring Boot JPA 的文章请看我的这篇文章:[一文搞懂如何在 Spring Boot 正确中使用 JPA](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485689&idx=1&sn=061b32c2222869932be5631fb0bb5260&chksm=cea24732f9d5ce24a356fb3675170e7843addbfcc79ee267cfdb45c83fc7e90babf0f20d22e1&token=292197051&lang=zh_CN#rd) 。 + +### 9. 事务 `@Transactional` + +在要开启事务的方法上使用`@Transactional`注解即可! + +```java +@Transactional(rollbackFor = Exception.class) +public void save() { + ...... +} + +``` + +我们知道 Exception 分为运行时异常 RuntimeException 和非运行时异常。在`@Transactional`注解中如果不配置`rollbackFor`属性,那么事物只会在遇到`RuntimeException`的时候才会回滚,加上`rollbackFor=Exception.class`,可以让事物在遇到非运行时异常时也回滚。 + +`@Transactional` 注解一般用在可以作用在`类`或者`方法`上。 + +- **作用于类**:当把`@Transactional 注解放在类上时,表示所有该类的`public 方法都配置相同的事务属性信息。 +- **作用于方法**:当类配置了`@Transactional`,方法也配置了`@Transactional`,方法的事务会覆盖类的事务配置信息。 + +更多关于关于 Spring 事务的内容请查看: + +1. [可能是最漂亮的 Spring 事务管理详解](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484943&idx=1&sn=46b9082af4ec223137df7d1c8303ca24&chksm=cea249c4f9d5c0d2b8212a17252cbfb74e5fbe5488b76d829827421c53332326d1ec360f5d63&token=1082669959&lang=zh_CN#rd) +2. [一口气说出 6 种 @Transactional 注解失效场景](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486483&idx=2&sn=77be488e206186803531ea5d7164ec53&chksm=cea243d8f9d5cacecaa5c5daae4cde4c697b9b5b21f96dfc6cce428cfcb62b88b3970c26b9c2&token=816772476&lang=zh_CN#rd) + +### 10. json 数据处理 + +#### 10.1. 过滤 json 数据 + +**`@JsonIgnoreProperties` 作用在类上用于过滤掉特定字段不返回或者不解析。** + +```java +//生成json时将userRoles属性过滤 +@JsonIgnoreProperties({"userRoles"}) +public class User { + + private String userName; + private String fullName; + private String password; + @JsonIgnore + private List userRoles = new ArrayList<>(); +} +``` + +**`@JsonIgnore`一般用于类的属性上,作用和上面的`@JsonIgnoreProperties` 一样。** + +```java + +public class User { + + private String userName; + private String fullName; + private String password; + //生成json时将userRoles属性过滤 + @JsonIgnore + private List userRoles = new ArrayList<>(); +} +``` + +#### 10.2. 格式化 json 数据 + +`@JsonFormat`一般用来格式化 json 数据。: + +比如: + +```java +@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT") +private Date date; +``` + +#### 10.3. 扁平化对象 + +```java +@Getter +@Setter +@ToString +public class Account { + @JsonUnwrapped + private Location location; + @JsonUnwrapped + private PersonInfo personInfo; + + @Getter + @Setter + @ToString + public static class Location { + private String provinceName; + private String countyName; + } + @Getter + @Setter + @ToString + public static class PersonInfo { + private String userName; + private String fullName; + } +} + +``` + +未扁平化之前: + +```json +{ + "location": { + "provinceName":"湖北", + "countyName":"武汉" + }, + "personInfo": { + "userName": "coder1234", + "fullName": "shaungkou" + } +} +``` + +使用`@JsonUnwrapped` 扁平对象之后: + +```java +@Getter +@Setter +@ToString +public class Account { + @JsonUnwrapped + private Location location; + @JsonUnwrapped + private PersonInfo personInfo; + ...... +} +``` + +```json +{ + "provinceName":"湖北", + "countyName":"武汉", + "userName": "coder1234", + "fullName": "shaungkou" +} +``` + +### 11. 测试相关 + +**`@ActiveProfiles`一般作用于测试类上, 用于声明生效的 Spring 配置文件。** + +```java +@SpringBootTest(webEnvironment = RANDOM_PORT) +@ActiveProfiles("test") +@Slf4j +public abstract class TestBase { + ...... +} +``` + +**`@Test`声明一个方法为测试方法** + +**`@Transactional`被声明的测试方法的数据会回滚,避免污染测试数据。** + +**`@WithMockUser` Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。** + +```java + @Test + @Transactional + @WithMockUser(username = "user-id-18163138155", authorities = "ROLE_TEACHER") + void should_import_student_success() throws Exception { + ...... + } +``` + +_暂时总结到这里吧!虽然花了挺长时间才写完,不过可能还是会一些常用的注解的被漏掉,所以,我将文章也同步到了 Github 上去,Github 地址: 欢迎完善!_ + +本文已经收录进我的 75K Star 的 Java 开源项目 JavaGuide:[https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide)。 diff --git a/docs/system-design/framework/spring/spring-transaction.md b/docs/system-design/framework/spring/spring-transaction.md new file mode 100644 index 00000000..f465cd0c --- /dev/null +++ b/docs/system-design/framework/spring/spring-transaction.md @@ -0,0 +1,687 @@ +大家好,我是 Guide 哥,前段答应读者的 **Spring 事务**分析总结终于来了。这部分内容比较重要,不论是对于工作还是面试,但是网上比较好的参考资料比较少。 + +如果本文有任何不对或者需要完善的地方,请帮忙指出!Guide 哥感激不尽! + +## 1. 什么是事务? + +**事务是逻辑上的一组操作,要么都执行,要么都不执行。** + +_Guide 哥:大家应该都能背上面这句话了,下面我结合我们日常的真实开发来谈一谈。_ + +我们系统的每个业务方法可能包括了多个原子性的数据库操作,比如下面的 `savePerson()` 方法中就有两个原子性的数据库操作。这些原子性的数据库操作是有依赖的,它们要么都执行,要不就都不执行。 + +```java + public void savePerson() { + personDao.save(person); + personDetailDao.save(personDetail); + } +``` + +另外,需要格外注意的是:**事务能否生效数据库引擎是否支持事务是关键。比如常用的 MySQL 数据库默认使用支持事务的`innodb`引擎。但是,如果把数据库引擎变为 `myisam`,那么程序也就不再支持事务了!** + +事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作就是: + +1. 将小明的余额减少 1000 元 + +2. 将小红的余额增加 1000 元。 + +万一在这两个操作之间突然出现错误比如银行系统崩溃或者网络故障,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。 + +```java +public class OrdersService { + private AccountDao accountDao; + + public void setOrdersDao(AccountDao accountDao) { + this.accountDao = accountDao; + } + + @Transactional(propagation = Propagation.REQUIRED, + isolation = Isolation.DEFAULT, readOnly = false, timeout = -1) + public void accountMoney() { + //小红账户多1000 + accountDao.addMoney(1000,xiaohong); + //模拟突然出现的异常,比如银行中可能为突然停电等等 + //如果没有配置事务管理的话会造成,小红账户多了1000而小明账户没有少钱 + int i = 10 / 0; + //小王账户少1000 + accountDao.reduceMoney(1000,xiaoming); + } +} +``` + +另外,数据库事务的 ACID 四大特性是事务的基础,下面简单来了解一下。 + +## 2. 事物的特性(ACID)了解么? + +![](images/spring-transaction/bda7231b-ab05-4e23-95ee-89ac90ac7fcf.png) + +- **原子性(Atomicity):** 一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。 +- **一致性(Consistency):** 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。 +- **隔离性(Isolation):** 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)。 +- **持久性(Durability):** 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。 + +[参考]https://zh.wikipedia.org/wiki/ACID + +## 3. 详谈 Spring 对事务的支持 + +**再提醒一次:你的程序是否支持事务首先取决于数据库 ,比如使用 MySQL 的话,如果你选择的是 innodb 引擎,那么恭喜你,是可以支持事务的。但是,如果你的 MySQL 数据库使用的是 myisam 引擎的话,那不好意思,从根上就是不支持事务的。** + +这里再多提一下一个非常重要的知识点: **MySQL 怎么保证原子性的?** + +我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行**回滚**,在 MySQL 中,恢复机制是通过 **回滚日志(undo log)** 实现的,所有事务进行的修改都会先先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 **回滚日志** 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。 + +### 3.1. Spring 支持两种方式的事务管理 + +#### 1).编程式事务管理 + +通过 `TransactionTemplate`或者`TransactionManager`手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。 + +使用`TransactionTemplate` 进行编程式事务管理的示例代码如下: + +```java +@Autowired +private TransactionTemplate transactionTemplate; +public void testTransaction() { + + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { + + try { + + // .... 业务代码 + } catch (Exception e){ + //回滚 + transactionStatus.setRollbackOnly(); + } + + } + }); +} +``` + +使用 `TransactionManager` 进行编程式事务管理的示例代码如下: + +```java +@Autowired +private PlatformTransactionManager transactionManager; + +public void testTransaction() { + + TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); + try { + // .... 业务代码 + transactionManager.commit(status); + } catch (Exception e) { + transactionManager.rollback(status); + } +} +``` + +#### 2)声明式事务管理 + +推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于`@Transactional` 的全注解方式使用最多)。 + +使用 `@Transactional`注解进行事务管理的示例代码如下: + +```java +@Transactional(propagation=propagation.PROPAGATION_REQUIRED) +public void aMethod { + //do something + B b = new B(); + C c = new C(); + b.bMethod(); + c.cMethod(); +} +``` + +### 3.2. Spring 事务管理接口介绍 + +Spring 框架中,事务管理相关最重要的 3 个接口如下: + +- **`PlatformTransactionManager`**: (平台)事务管理器,Spring 事务策略的核心。 +- **`TransactionDefinition`**: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。 +- **`TransactionStatus`**: 事务运行状态。 + +我们可以把 **`PlatformTransactionManager`** 接口可以被看作是事务上层的管理者,而 **`TransactionDefinition`** 和 **`TransactionStatus`** 这两个接口可以看作是事物的描述。 + +**`PlatformTransactionManager`** 会根据 **`TransactionDefinition`** 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 **`TransactionStatus`** 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。 + +#### 3.2.1. PlatformTransactionManager:事务管理接口 + +**Spring 并不直接管理事务,而是提供了多种事务管理器** 。Spring 事务管理器的接口是: **`PlatformTransactionManager`** 。 + +通过这个接口,Spring 为各个平台如 JDBC(`DataSourceTransactionManager`)、Hibernate(`HibernateTransactionManager`)、JPA(`JpaTransactionManager`)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。 + +**`PlatformTransactionManager` 接口的具体实现如下:** + +![](images/spring-transaction/ae964c2c-7289-441c-bddd-511161f51ee1.png) + +`PlatformTransactionManager`接口中定义了三个方法: + +```java +package org.springframework.transaction; + +import org.springframework.lang.Nullable; + +public interface PlatformTransactionManager { + //获得事务 + TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; + //提交事务 + void commit(TransactionStatus var1) throws TransactionException; + //回滚事务 + void rollback(TransactionStatus var1) throws TransactionException; +} + +``` + +**这里多插一嘴。为什么要定义或者说抽象出来`PlatformTransactionManager`这个接口呢?** + +主要是因为要将事务管理行为抽象出来,然后不同的平台去实现它,这样我们可以保证提供给外部的行为不变,方便我们扩展。我前段时间分享过:**“为什么我们要用接口?”** + + + +#### 3.2.2. TransactionDefinition:事务属性 + +事务管理器接口 **`PlatformTransactionManager`** 通过 **`getTransaction(TransactionDefinition definition)`** 方法来得到一个事务,这个方法里面的参数是 **`TransactionDefinition`** 类 ,这个类就定义了一些基本的事务属性。 + +那么什么是 **事务属性** 呢? + +事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。 + +事务属性包含了 5 个方面: + +![](images/spring-transaction/a616b84d-9eea-4ad1-b4fc-461ff05e951d.png) + +`TransactionDefinition` 接口中定义了 5 个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等。 + +```java +package org.springframework.transaction; + +import org.springframework.lang.Nullable; + +public interface TransactionDefinition { + int PROPAGATION_REQUIRED = 0; + int PROPAGATION_SUPPORTS = 1; + int PROPAGATION_MANDATORY = 2; + int PROPAGATION_REQUIRES_NEW = 3; + int PROPAGATION_NOT_SUPPORTED = 4; + int PROPAGATION_NEVER = 5; + int PROPAGATION_NESTED = 6; + int ISOLATION_DEFAULT = -1; + int ISOLATION_READ_UNCOMMITTED = 1; + int ISOLATION_READ_COMMITTED = 2; + int ISOLATION_REPEATABLE_READ = 4; + int ISOLATION_SERIALIZABLE = 8; + int TIMEOUT_DEFAULT = -1; + // 返回事务的传播行为,默认值为 REQUIRED。 + int getPropagationBehavior(); + //返回事务的隔离级别,默认值是 DEFAULT + int getIsolationLevel(); + // 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。 + int getTimeout(); + // 返回是否为只读事务,默认值为 false + boolean isReadOnly(); + + @Nullable + String getName(); +} +``` + +#### 3.2.3. TransactionStatus:事务状态 + +`TransactionStatus`接口用来记录事务的状态 该接口定义了一组方法,用来获取或判断事务的相应状态信息。 + +`PlatformTransactionManager.getTransaction(…)`方法返回一个 `TransactionStatus` 对象。 + +**TransactionStatus 接口接口内容如下:** + +```java +public interface TransactionStatus{ + boolean isNewTransaction(); // 是否是新的事物 + boolean hasSavepoint(); // 是否有恢复点 + void setRollbackOnly(); // 设置为只回滚 + boolean isRollbackOnly(); // 是否为只回滚 + boolean isCompleted; // 是否已完成 +} +``` + +### 3.3. 事务属性详解 + +_实际业务开发中,大家一般都是使用 `@Transactional` 注解来开启事务,很多人并不清楚这个参数里面的参数是什么意思,有什么用。为了更好的在项目中使用事务管理,强烈推荐好好阅读一下下面的内容。_ + +#### 3.3.1. 事务传播行为 + +**事务传播行为是为了解决业务层方法之间互相调用的事务问题**。 + +当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。 + +**举个例子!** + +我们在 A 类的`aMethod()`方法中调用了 B 类的 `bMethod()` 方法。这个时候就涉及到业务层方法之间互相调用的事务问题。如果我们的 `bMethod()`如果发生异常需要回滚,如何配置事务传播行为才能让 `aMethod()`也跟着回滚呢?这个时候就需要事务传播行为的知识了,如果你不知道的话一定要好好看一下。 + +```java +Class A { + @Transactional(propagation=propagation.xxx) + public void aMethod { + //do something + B b = new B(); + b.bMethod(); + } +} + +Class B { + @Transactional(propagation=propagation.xxx) + public void bMethod { + //do something + } +} +``` + +在`TransactionDefinition`定义中包括了如下几个表示传播行为的常量: + +```java +public interface TransactionDefinition { + int PROPAGATION_REQUIRED = 0; + int PROPAGATION_SUPPORTS = 1; + int PROPAGATION_MANDATORY = 2; + int PROPAGATION_REQUIRES_NEW = 3; + int PROPAGATION_NOT_SUPPORTED = 4; + int PROPAGATION_NEVER = 5; + int PROPAGATION_NESTED = 6; + ...... +} +``` + +不过如此,为了方便使用,Spring 会相应地定义了一个枚举类:`Propagation` + +```java +package org.springframework.transaction.annotation; + +import org.springframework.transaction.TransactionDefinition; + +public enum Propagation { + + REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED), + + SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS), + + MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY), + + REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW), + + NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED), + + NEVER(TransactionDefinition.PROPAGATION_NEVER), + + NESTED(TransactionDefinition.PROPAGATION_NESTED); + + + private final int value; + + Propagation(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + +} + +``` + +**正确的事务传播行为可能的值如下** : + +**1.`TransactionDefinition.PROPAGATION_REQUIRED`** + +使用的最多的一个事务传播行为,我们平时经常使用的`@Transactional`注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说: + +1. 如果外部方法没有开启事务的话,`Propagation.REQUIRED`修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。 +2. 如果外部方法开启事务并且被`Propagation.REQUIRED`的话,所有`Propagation.REQUIRED`修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。 + +举个例子:如果我们上面的`aMethod()`和`bMethod()`使用的都是`PROPAGATION_REQUIRED`传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。 + +```java +Class A { + @Transactional(propagation=propagation.PROPAGATION_REQUIRED) + public void aMethod { + //do something + B b = new B(); + b.bMethod(); + } +} + +Class B { + @Transactional(propagation=propagation.PROPAGATION_REQUIRED) + public void bMethod { + //do something + } +} +``` + +**`2.TransactionDefinition.PROPAGATION_REQUIRES_NEW`** + +创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,`Propagation.REQUIRES_NEW`修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。 + +举个例子:如果我们上面的`bMethod()`使用`PROPAGATION_REQUIRES_NEW`事务传播行为修饰,`aMethod`还是用`PROPAGATION_REQUIRED`修饰的话。如果`aMethod()`发生异常回滚,`bMethod()`不会跟着回滚,因为 `bMethod()`开启了独立的事务。但是,如果 `bMethod()`抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,`aMethod()`同样也会回滚,因为这个异常被 `aMethod()`的事务管理机制检测到了。 + +```java +Class A { + @Transactional(propagation=propagation.PROPAGATION_REQUIRED) + public void aMethod { + //do something + B b = new B(); + b.bMethod(); + } +} + +Class B { + @Transactional(propagation=propagation.REQUIRES_NEW) + public void bMethod { + //do something + } +} +``` + +**3.`TransactionDefinition.PROPAGATION_NESTED`**: + +如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于`TransactionDefinition.PROPAGATION_REQUIRED`。也就是说: + +1. 在外部方法未开启事务的情况下`Propagation.NESTED`和`Propagation.REQUIRED`作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。 +2. 如果外部方法开启事务的话,`Propagation.NESTED`修饰的内部方法属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务。 + +这里还是简单举个例子: + +如果 `aMethod()` 回滚的话,`bMethod()`和`bMethod2()`都要回滚,而`bMethod()`回滚的话,并不会造成 `aMethod()` 和`bMethod()`回滚。 + +```java +Class A { + @Transactional(propagation=propagation.PROPAGATION_REQUIRED) + public void aMethod { + //do something + B b = new B(); + b.bMethod(); + b.bMethod2(); + } +} + +Class B { + @Transactional(propagation=propagation.PROPAGATION_NESTED) + public void bMethod { + //do something + } + @Transactional(propagation=propagation.PROPAGATION_NESTED) + public void bMethod2 { + //do something + } +} +``` + +**4.`TransactionDefinition.PROPAGATION_MANDATORY`** + +如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性) + +这个使用的很少,就不举例子来说了。 + +**若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚,这里不对照案例讲解了,使用的很少。** + +- **`TransactionDefinition.PROPAGATION_SUPPORTS`**: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 +- **`TransactionDefinition.PROPAGATION_NOT_SUPPORTED`**: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 +- **`TransactionDefinition.PROPAGATION_NEVER`**: 以非事务方式运行,如果当前存在事务,则抛出异常。 + +更多关于事务传播行为的内容请看这篇文章:[《太难了~面试官让我结合案例讲讲自己对 Spring 事务传播行为的理解。》](http://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486668&idx=2&sn=0381e8c836442f46bdc5367170234abb&chksm=cea24307f9d5ca11c96943b3ccfa1fc70dc97dd87d9c540388581f8fe6d805ff548dff5f6b5b&token=1776990505&lang=zh_CN#rd) + +#### 3.3.2 事务隔离级别 + +`TransactionDefinition` 接口中定义了五个表示隔离级别的常量: + +```java +public interface TransactionDefinition { + ...... + int ISOLATION_DEFAULT = -1; + int ISOLATION_READ_UNCOMMITTED = 1; + int ISOLATION_READ_COMMITTED = 2; + int ISOLATION_REPEATABLE_READ = 4; + int ISOLATION_SERIALIZABLE = 8; + ...... +} +``` + +和事务传播行为这块一样,为了方便使用,Spring 也相应地定义了一个枚举类:`Isolation` + +```java +public enum Isolation { + + DEFAULT(TransactionDefinition.ISOLATION_DEFAULT), + + READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED), + + READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED), + + REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ), + + SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE); + + private final int value; + + Isolation(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + +} +``` + +下面我依次对每一种事务隔离级别进行介绍: + +- **`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 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 + +因为平时使用 MySQL 数据库比较多,这里再多提一嘴! + +MySQL InnoDB 存储引擎的默认支持的隔离级别是 **`REPEATABLE-READ`(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;`: + +``` +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 存储引擎默认使用 **`REPEATABLE-READ`(可重读)** 并不会什么任何性能上的损失。 + +更多关于事务隔离级别的内容请看: + +1. [《一文带你轻松搞懂事务隔离级别(图文详解)》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485085&idx=1&sn=01e5c29c49f32886bc897af7632b34ba&chksm=cea24956f9d5c040a07e4d335219f11f888a2d32444c16cade3f69c294ae0a1e416bcd221fb6&token=1613452699&lang=zh_CN&scene=21#wechat_redirect) +2. [面试官:你说对 MySQL 事务很熟?那我问你 10 个问题](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486625&idx=2&sn=e235dab2757739438b8f33d205a9327f&chksm=cea2436af9d5ca7c9a1a8db9d020f71205687beca23ac958f9c9a711ee0185cab30173ad2b1a&token=1776990505&lang=zh_CN#rd) + +#### 3.3.3. 事务超时属性 + +所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 `TransactionDefinition` 中以 int 的值来表示超时时间,其单位是秒,默认值为-1。 + +#### 3.3.3. 事务只读属性 + +```java +package org.springframework.transaction; + +import org.springframework.lang.Nullable; + +public interface TransactionDefinition { + ...... + // 返回是否为只读事务,默认值为 false + boolean isReadOnly(); + +} +``` + +对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。 + +很多人就会疑问了,为什么我一个数据查询操作还要启用事务支持呢? + +拿 MySQL 的 innodb 举例子,根据官网 [https://dev.mysql.com/doc/refman/5.7/en/innodb-autocommit-commit-rollback.html](https://dev.mysql.com/doc/refman/5.7/en/innodb-autocommit-commit-rollback.html) 描述: + +> MySQL 默认对每一个新建立的连接都启用了`autocommit`模式。在该模式下,每一个发送到 MySQL 服务器的`sql`语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。 + +但是,如果你给方法加上了`Transactional`注解的话,这个方法执行的所有`sql`会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。 + +如果不加`Transactional`,每条`sql`会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。 + +分享一下关于事务只读属性,其他人的解答: + +1. 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性; +2. 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持 + +#### 3.3.4. 事务回滚规则 + +这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。 + +![](images/spring-transaction/f6c6f0aa-0f26-49e1-84b3-7f838c7379d1.png) + +如果你想要回滚你定义的特定的异常类型的话,可以这样: + +```java +@Transactional(rollbackFor= MyException.class) +``` + +### 3.4. @Transactional 注解使用详解 + +#### 1) `@Transactional` 的作用范围 + +1. **方法** :推荐将注解使用于方法上,不过需要注意的是:**该注解只能应用到 public 方法上,否则不生效。** +2. **类** :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。 +3. **接口** :不推荐在接口上使用。 + +#### 2) `@Transactional` 的常用配置参数 + +`@Transactional`注解源码如下,里面包含了基本事务属性的配置: + +```java +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Transactional { + + @AliasFor("transactionManager") + String value() default ""; + + @AliasFor("value") + String transactionManager() default ""; + + Propagation propagation() default Propagation.REQUIRED; + + Isolation isolation() default Isolation.DEFAULT; + + int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; + + boolean readOnly() default false; + + Class[] rollbackFor() default {}; + + String[] rollbackForClassName() default {}; + + Class[] noRollbackFor() default {}; + + String[] noRollbackForClassName() default {}; + +} +``` + +**`@Transactional` 的常用配置参数总结(只列巨额 5 个我平时比较常用的):** + +| 属性名 | 说明 | +| :---------- | :------------------------------------------------------------------------------------------- | +| propagation | 事务的传播行为,默认值为 REQUIRED,可选的值在上面介绍过 | +| isolation | 事务的隔离级别,默认值采用 DEFAULT,可选的值在上面介绍过 | +| timeout | 事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。 | +| readOnly | 指定事务是否为只读事务,默认值为 false。 | +| rollbackFor | 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。 | + +#### 3)`@Transactional` 事务注解原理 + +面试中在问 AOP 的时候可能会被问到的一个问题。简单说下吧! + +我们知道,**`@Transactional` 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。** + +多提一嘴:`createAopProxy()` 方法 决定了是使用 JDK 还是 Cglib 来做动态代理,源码如下: + +```java +public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { + + @Override + public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException("TargetSource cannot determine target class: " + + "Either an interface or a target is required for proxy creation."); + } + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { + return new JdkDynamicAopProxy(config); + } + return new ObjenesisCglibAopProxy(config); + } + else { + return new JdkDynamicAopProxy(config); + } + } + ....... +} +``` + +如果一个类或者一个类中的 public 方法上被标注`@Transactional` 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被`@Transactional` 注解的 public 方法的时候,实际调用的是,`TransactionInterceptor` 类中的 `invoke()`方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。 + +> `TransactionInterceptor` 类中的 `invoke()`方法内部实际调用的是 `TransactionAspectSupport` 类的 `invokeWithinTransaction()`方法。由于新版本的 Spring 对这部分重写很大,而且用到了很多响应式编程的知识,这里就不列源码了。 + +#### 4)Spring AOP 自调用问题 + +若同一类中的其他没有 `@Transactional` 注解的方法内部调用有 `@Transactional` 注解的方法,有`@Transactional` 注解的方法的事务会失效。 + +这是由于`Spring AOP`代理的原因造成的,因为只有当 `@Transactional` 注解的方法在类以外被调用的时候,Spring 事务管理才生效。 + +`MyService` 类中的`method1()`调用`method2()`就会导致`method2()`的事务失效。 + +```java +@Service +public class MyService { + +private void method1() { + method2(); + //...... +} +@Transactional + public void method2() { + //...... + } +} +``` + +解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。 + +#### 5) `@Transactional` 的使用注意事项总结 + +1. `@Transactional` 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用; +2. 避免同一个类中调用 `@Transactional` 注解的方法,这样会导致事务失效; +3. 正确的设置 `@Transactional` 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败 +4. ...... + +## 4. Reference + +3. [总结]Spring 事务管理中@Transactional 的参数:[http://www.mobabel.net/spring 事务管理中 transactional 的参数/](http://www.mobabel.net/spring事务管理中transactional的参数/) +4. Spring 官方文档:[https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/transaction.html](https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/transaction.html) +5. 《Spring5 高级编程》 +6. 透彻的掌握 Spring 中@transactional 的使用: [https://www.ibm.com/developerworks/cn/java/j-master-spring-transactional-use/index.html](https://www.ibm.com/developerworks/cn/java/j-master-spring-transactional-use/index.html) +7. Spring 事务的传播特性:[https://github.com/love-somnus/Spring/wiki/Spring 事务的传播特性](https://github.com/love-somnus/Spring/wiki/Spring事务的传播特性) +8. [Spring 事务传播行为详解](https://segmentfault.com/a/1190000013341344) :[https://segmentfault.com/a/1190000013341344](https://segmentfault.com/a/1190000013341344) +9. 全面分析 Spring 的编程式事务管理及声明式事务管理:[https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/index.html](https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/index.html) diff --git a/docs/system-design/framework/zookeeper/images/curator.png b/docs/system-design/framework/zookeeper/images/curator.png new file mode 100644 index 00000000..28da0247 Binary files /dev/null and b/docs/system-design/framework/zookeeper/images/curator.png differ diff --git a/docs/system-design/framework/zookeeper/images/watche机制.png b/docs/system-design/framework/zookeeper/images/watche机制.png new file mode 100644 index 00000000..68144db1 Binary files /dev/null and b/docs/system-design/framework/zookeeper/images/watche机制.png differ diff --git a/docs/system-design/framework/zookeeper/images/znode-structure.png b/docs/system-design/framework/zookeeper/images/znode-structure.png new file mode 100644 index 00000000..746c3f6e Binary files /dev/null and b/docs/system-design/framework/zookeeper/images/znode-structure.png differ diff --git a/docs/system-design/framework/zookeeper/images/zookeeper集群.png b/docs/system-design/framework/zookeeper/images/zookeeper集群.png new file mode 100644 index 00000000..a3067cda Binary files /dev/null and b/docs/system-design/framework/zookeeper/images/zookeeper集群.png differ diff --git a/docs/system-design/framework/zookeeper/images/zookeeper集群中的角色.png b/docs/system-design/framework/zookeeper/images/zookeeper集群中的角色.png new file mode 100644 index 00000000..6b118fe0 Binary files /dev/null and b/docs/system-design/framework/zookeeper/images/zookeeper集群中的角色.png differ diff --git a/docs/system-design/framework/zookeeper/images/连接ZooKeeper服务.png b/docs/system-design/framework/zookeeper/images/连接ZooKeeper服务.png new file mode 100644 index 00000000..d3913299 Binary files /dev/null and b/docs/system-design/framework/zookeeper/images/连接ZooKeeper服务.png differ diff --git a/docs/system-design/framework/zookeeper/zookeeper-in-action.md b/docs/system-design/framework/zookeeper/zookeeper-in-action.md new file mode 100644 index 00000000..4b1f9eb9 --- /dev/null +++ b/docs/system-design/framework/zookeeper/zookeeper-in-action.md @@ -0,0 +1,323 @@ + + + + + +- [1. 前言](#1-前言) +- [2. ZooKeeper 安装和使用](#2-zookeeper-安装和使用) + - [2.1. 使用Docker 安装 zookeeper](#21-使用docker-安装-zookeeper) + - [2.2. 连接 ZooKeeper 服务](#22-连接-zookeeper-服务) + - [2.3. 常用命令演示](#23-常用命令演示) + - [2.3.1. 查看常用命令(help 命令)](#231-查看常用命令help-命令) + - [2.3.2. 创建节点(create 命令)](#232-创建节点create-命令) + - [2.3.3. 更新节点数据内容(set 命令)](#233-更新节点数据内容set-命令) + - [2.3.4. 获取节点的数据(get 命令)](#234-获取节点的数据get-命令) + - [2.3.5. 查看某个目录下的子节点(ls 命令)](#235-查看某个目录下的子节点ls-命令) + - [2.3.6. 查看节点状态(stat 命令)](#236-查看节点状态stat-命令) + - [2.3.7. 查看节点信息和状态(ls2 命令)](#237-查看节点信息和状态ls2-命令) + - [2.3.8. 删除节点(delete 命令)](#238-删除节点delete-命令) +- [3. ZooKeeper Java客户端 Curator简单使用](#3-zookeeper-java客户端-curator简单使用) + - [3.1. 连接 ZooKeeper 客户端](#31-连接-zookeeper-客户端) + - [3.2. 数据节点的增删改查](#32-数据节点的增删改查) + - [3.2.1. 创建节点](#321-创建节点) + - [3.2.2. 删除节点](#322-删除节点) + - [3.2.3. 获取/更新节点数据内容](#323-获取更新节点数据内容) + - [3.2.4. 获取某个节点的所有子节点路径](#324-获取某个节点的所有子节点路径) + + + + +## 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/system-design/framework/zookeeper/zookeeper-intro.md b/docs/system-design/framework/zookeeper/zookeeper-intro.md new file mode 100644 index 00000000..4978fb1a --- /dev/null +++ b/docs/system-design/framework/zookeeper/zookeeper-intro.md @@ -0,0 +1,299 @@ + + + + + +- [1. 前言](#1-前言) +- [2. ZooKeeper 介绍](#2-zookeeper-介绍) + - [2.1. ZooKeeper 由来](#21-zookeeper-由来) + - [2.2. ZooKeeper 概览](#22-zookeeper-概览) + - [2.3. ZooKeeper 特点](#23-zookeeper-特点) + - [2.4. ZooKeeper 典型应用场景](#24-zookeeper-典型应用场景) + - [2.5. 有哪些著名的开源项目用到了 ZooKeeper?](#25-有哪些著名的开源项目用到了-zookeeper) +- [3. ZooKeeper 重要概念解读](#3-zookeeper-重要概念解读) + - [3.1. Data model(数据模型)](#31-data-model数据模型) + - [3.2. znode(数据节点)](#32-znode数据节点) + - [3.2.1. znode 4种类型](#321-znode-4种类型) + - [3.2.2. znode 数据结构](#322-znode-数据结构) + - [3.3. 版本(version)](#33-版本version) + - [3.4. ACL(权限控制)](#34-acl权限控制) + - [3.5. Watcher(事件监听器)](#35-watcher事件监听器) + - [3.6. 会话(Session)](#36-会话session) +- [4. ZooKeeper 集群](#4-zookeeper-集群) + - [4.1. ZooKeeper 集群角色](#41-zookeeper-集群角色) + - [4.2. ZooKeeper 集群中的服务器状态](#42-zookeeper-集群中的服务器状态) + - [4.3. ZooKeeper 集群为啥最好奇数台?](#43-zookeeper-集群为啥最好奇数台) +- [5. ZAB 协议和Paxos 算法](#5-zab-协议和paxos-算法) + - [5.1. ZAB 协议介绍](#51-zab-协议介绍) + - [5.2. ZAB 协议两种基本的模式:崩溃恢复和消息广播](#52-zab-协议两种基本的模式崩溃恢复和消息广播) +- [6. 总结](#6-总结) +- [7. 参考](#7-参考) + + + + +## 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 呢? + +## 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 分布式一致性原理与实践》 \ No newline at end of file diff --git a/docs/system-design/framework/ZooKeeper-plus.md b/docs/system-design/framework/zookeeper/zookeeper-plus.md similarity index 94% rename from docs/system-design/framework/ZooKeeper-plus.md rename to docs/system-design/framework/zookeeper/zookeeper-plus.md index d68a4c47..36772048 100644 --- a/docs/system-design/framework/ZooKeeper-plus.md +++ b/docs/system-design/framework/zookeeper/zookeeper-plus.md @@ -1,8 +1,41 @@ [FrancisQ](https://juejin.im/user/5c33853851882525ea106810) 投稿。 -# ZooKeeper -## 好久不见 + + + + +- [1. 好久不见](#1-好久不见) +- [2. 什么是ZooKeeper](#2-什么是zookeeper) +- [3. 一致性问题](#3-一致性问题) +- [4. 一致性协议和算法](#4-一致性协议和算法) + - [4.1. 2PC(两阶段提交)](#41-2pc两阶段提交) + - [4.2. 3PC(三阶段提交)](#42-3pc三阶段提交) + - [4.3. `Paxos` 算法](#43-paxos-算法) + - [4.3.1. prepare 阶段](#431-prepare-阶段) + - [4.3.2. accept 阶段](#432-accept-阶段) + - [4.3.3. `paxos` 算法的死循环问题](#433-paxos-算法的死循环问题) +- [5. 引出 `ZAB`](#5-引出-zab) + - [5.1. `Zookeeper` 架构](#51-zookeeper-架构) + - [5.2. `ZAB` 中的三个角色](#52-zab-中的三个角色) + - [5.3. 消息广播模式](#53-消息广播模式) + - [5.4. 崩溃恢复模式](#54-崩溃恢复模式) +- [6. Zookeeper的几个理论知识](#6-zookeeper的几个理论知识) + - [6.1. 数据模型](#61-数据模型) + - [6.2. 会话](#62-会话) + - [6.3. ACL](#63-acl) + - [6.4. Watcher机制](#64-watcher机制) +- [7. Zookeeper的几个典型应用场景](#7-zookeeper的几个典型应用场景) + - [7.1. 选主](#71-选主) + - [7.2. 分布式锁](#72-分布式锁) + - [7.3. 命名服务](#73-命名服务) + - [7.4. 集群管理和注册中心](#74-集群管理和注册中心) +- [8. 总结](#8-总结) + + + + +## 1. 好久不见 离上一篇文章的发布也快一个月了,想想已经快一个月没写东西了,其中可能有期末考试、课程设计和驾照考试,但这都不是借口! @@ -10,7 +43,7 @@ > 文章很长,先赞后看,养成习惯。❤️ 🧡 💛 💚 💙 💜 -## 什么是ZooKeeper +## 2. 什么是ZooKeeper `ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理、分布式事务等。 @@ -34,7 +67,7 @@ 比如各个分布式组件如何协调起来,如何减少各个系统之间的耦合度,分布式事务的处理,如何去配置整个分布式系统等等。`ZooKeeper` 主要就是解决这些问题的。 -## 一致性问题 +## 3. 一致性问题 设计一个分布式系统必定会遇到一个问题—— **因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡** 。这就是著名的 `CAP` 定理。 @@ -42,9 +75,9 @@ ![](http://img.francisqiang.top/img/垃圾例子.jpg) -而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要将的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。 +而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要讲的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。 -## 一致性协议和算法 +## 4. 一致性协议和算法 而为了解决数据一致性问题,在科学家和程序员的不断探索中,就出现了很多的一致性协议和算法。比如 2PC(两阶段提交),3PC(三阶段提交),Paxos算法等等。 @@ -56,7 +89,7 @@ 而为什么要去解决数据一致性的问题?你想想,如果一个秒杀系统将服务拆分成了下订单和加积分服务,这两个服务部署在不同的机器上了,万一在消息的传播过程中积分系统宕机了,总不能你这边下了订单却没加积分吧?你总得保证两边的数据需要一致吧? -### 2PC(两阶段提交) +### 4.1. 2PC(两阶段提交) 两阶段提交是一种保证分布式系统数据一致性的协议,现在很多数据库都是采用的两阶段提交协议来完成 **分布式事务** 的处理。 @@ -86,7 +119,7 @@ * **阻塞问题**,即当协调者发送 `prepare` 请求,参与者收到之后如果能处理那么它将会进行事务的处理但并不提交,这个时候会一直占用着资源不释放,如果此时协调者挂了,那么这些资源都不会再释放了,这会极大影响性能。 * **数据不一致问题**,比如当第二阶段,协调者只发送了一部分的 `commit` 请求就挂了,那么也就意味着,收到消息的参与者会进行事务的提交,而后面没收到的则不会进行事务提交,那么这时候就会产生数据不一致性问题。 -### 3PC(三阶段提交) +### 4.2. 3PC(三阶段提交) 因为2PC存在的一系列问题,比如单点,容错机制缺陷等等,从而产生了 **3PC(三阶段提交)** 。那么这三阶段又分别是什么呢? @@ -104,13 +137,13 @@ 所以,要解决一致性问题还需要靠 `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` 。 @@ -119,7 +152,7 @@ ![paxos第一阶段](http://img.francisqiang.top/img/paxos1.jpg) -#### accept 阶段 +#### 4.3.2. accept 阶段 当一个提案被 `Proposer` 提出后,如果 `Proposer` 收到了超过半数的 `Acceptor` 的批准(`Proposer` 本身同意),那么此时 `Proposer` 会给所有的 `Acceptor` 发送真正的提案(你可以理解为第一阶段为试探),这个时候 `Proposer` 就会发送提案的内容和提案编号。 @@ -135,7 +168,7 @@ > 对于 `Learner` 来说如何去学习 `Acceptor` 批准的提案内容,这有很多方式,读者可以自己去了解一下,这里不做过多解释。 -#### `paxos` 算法的死循环问题 +#### 4.3.3. `paxos` 算法的死循环问题 其实就有点类似于两个人吵架,小明说我是对的,小红说我才是对的,两个人据理力争的谁也不让谁🤬🤬。 @@ -147,15 +180,15 @@ 那么如何解决呢?很简单,人多了容易吵架,我现在 **就允许一个能提案** 就行了。 -## 引出 `ZAB` +## 5. 引出 `ZAB` -### `Zookeeper` 架构 +### 5.1. `Zookeeper` 架构 作为一个优秀高效且可靠的分布式协调框架,`ZooKeeper` 在解决分布式数据一致性问题时并没有直接使用 `Paxos` ,而是专门定制了一致性协议叫做 `ZAB(ZooKeeper Automic Broadcast)` 原子广播协议,该协议能够很好地支持 **崩溃恢复** 。 ![Zookeeper架构](http://img.francisqiang.top/img/Zookeeper架构.jpg) -### `ZAB` 中的三个角色 +### 5.2. `ZAB` 中的三个角色 和介绍 `Paxos` 一样,在介绍 `ZAB` 协议之前,我们首先来了解一下在 `ZAB` 中三个主要的角色,`Leader 领导者`、`Follower跟随者`、`Observer观察者` 。 @@ -165,7 +198,7 @@ 在 `ZAB` 协议中对 `zkServer`(即上面我们说的三个角色的总称) 还有两种模式的定义,分别是 **消息广播** 和 **崩溃恢复** 。 -### 消息广播模式 +### 5.3. 消息广播模式 说白了就是 `ZAB` 协议是如何处理写请求的,上面我们不是说只有 `Leader` 能处理写请求嘛?那么我们的 `Follower` 和 `Observer` 是不是也需要 **同步更新数据** 呢?总不能数据只在 `Leader` 中更新了,其他角色都没有得到更新吧? @@ -185,7 +218,7 @@ 定义这个的原因也是为了顺序性,每个 `proposal` 在 `Leader` 中生成后需要 **通过其 `ZXID` 来进行排序** ,才能得到处理。 -### 崩溃恢复模式 +### 5.4. 崩溃恢复模式 说到崩溃恢复我们首先要提到 `ZAB` 中的 `Leader` 选举算法,当系统出现崩溃影响最大应该是 `Leader` 的崩溃,因为我们只有一个 `Leader` ,所以当 `Leader` 出现问题的时候我们势必需要重新选举 `Leader` 。 @@ -229,13 +262,13 @@ ![崩溃恢复](http://img.francisqiang.top/img/崩溃恢复2.jpg) -## Zookeeper的几个理论知识 +## 6. Zookeeper的几个理论知识 了解了 `ZAB` 协议还不够,它仅仅是 `Zookeeper` 内部实现的一种方式,而我们如何通过 `Zookeeper` 去做一些典型的应用场景呢?比如说集群管理,分布式锁,`Master` 选举等等。 这就涉及到如何使用 `Zookeeper` 了,但在使用之前我们还需要掌握几个概念。比如 `Zookeeper` 的 **数据模型** 、**会话机制**、**ACL**、**Watcher机制** 等等。 -### 数据模型 +### 6.1. 数据模型 `zookeeper` 数据存储结构与标准的 `Unix` 文件系统非常相似,都是在根节点下挂很多子节点(树型)。但是 `zookeeper` 中没有文件系统中目录与文件的概念,而是 **使用了 `znode` 作为数据节点** 。`znode` 是 `zookeeper` 中的最小数据单元,每个 `znode` 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。 @@ -264,13 +297,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 +313,19 @@ * `DELETE`:删除子节点的权限。 * `ADMIN`:设置节点 ACL 的权限。 -### Watcher机制 +### 6.4. Watcher机制 `Watcher` 为事件监听器,是 `zk` 非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端 **注册** 指定的 `watcher` ,当服务端符合了 `watcher` 的某些事件或要求则会 **向客户端发送事件通知** ,客户端收到通知后找到自己定义的 `Watcher` 然后 **执行相应的回调方法** 。 ![watcher机制](http://img.francisqiang.top/img/watcher机制.jpg) -## Zookeeper的几个典型应用场景 +## 7. Zookeeper的几个典型应用场景 前面说了这么多的理论知识,你可能听得一头雾水,这些玩意有啥用?能干啥事?别急,听我慢慢道来。 ![](http://img.francisqiang.top/img/feijie.jpg) -### 选主 +### 7.1. 选主 还记得上面我们的所说的临时节点吗?因为 `Zookeeper` 的强一致性,能够很好地在保证 **在高并发的情况下保证节点创建的全局唯一性** (即无法重复创建同样的节点)。 @@ -306,7 +339,7 @@ 总的来说,我们可以完全 **利用 临时节点、节点状态 和 `watcher` 来实现选主的功能**,临时节点主要用来选举,节点状态和`watcher` 可以用来判断 `master` 的活性和进行重新选举。 -### 分布式锁 +### 7.2. 分布式锁 分布式锁的实现方式有很多种,比如 `Redis` 、数据库 、`zookeeper` 等。个人认为 `zookeeper` 在实现分布式锁这方面是非常非常简单的。 @@ -330,13 +363,13 @@ 具体怎么做呢?其实也很简单,你可以让 **读请求监听比自己小的最后一个写请求节点,写请求只监听比自己小的最后一个节点** ,感兴趣的小伙伴可以自己去研究一下。 -### 命名服务 +### 7.3. 命名服务 如何给一个对象设置ID,大家可能都会想到 `UUID`,但是 `UUID` 最大的问题就在于它太长了。。。(太长不一定是好事,嘿嘿嘿)。那么在条件允许的情况下,我们能不能使用 `zookeeper` 来实现呢? 我们之前提到过 `zookeeper` 是通过 **树形结构** 来存储数据节点的,那也就是说,对于每个节点的 **全路径**,它必定是唯一的,我们可以使用节点的全路径作为命名方式了。而且更重要的是,路径是我们可以自己定义的,这对于我们对有些有语意的对象的ID设置可以更加便于理解。 -### 集群管理和注册中心 +### 7.4. 集群管理和注册中心 看到这里是不是觉得 `zookeeper` 实在是太强大了,它怎么能这么能干! @@ -352,7 +385,7 @@ ![注册中心](http://img.francisqiang.top/img/注册中心.jpg) -## 总结 +## 8. 总结 看到这里的同学实在是太有耐心了👍👍👍,如果觉得我写得不错的话点个赞哈。 @@ -372,4 +405,4 @@ * `zookeeper` 的典型应用场景,比如选主,注册中心等等。 - 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出🤝🤝🤝。 \ No newline at end of file + 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出🤝🤝🤝。 diff --git a/docs/system-design/micro-service/API网关.md b/docs/system-design/micro-service/API网关.md index fe96bc07..01c8400e 100644 --- a/docs/system-design/micro-service/API网关.md +++ b/docs/system-design/micro-service/API网关.md @@ -2,14 +2,11 @@ > > 本文授权转载自:[https://github.com/javagrowing/JGrowing/blob/master/服务端开发/浅析如何设计一个亿级网关.md](https://github.com/javagrowing/JGrowing/blob/master/服务端开发/浅析如何设计一个亿级网关.md)。 -## 1.背景 - -### 1.1 什么是API网关 - +# 1.背景 +## 1.1 什么是API网关 API网关可以看做系统与外界联通的入口,我们可以在网关进行处理一些非业务逻辑的逻辑,比如权限验证,监控,缓存,请求路由等等。 -### 1.2 为什么需要API网关 - +## 1.2 为什么需要API网关 - RPC协议转成HTTP。 由于在内部开发中我们都是以RPC协议(thrift or dubbo)去做开发,暴露给内部服务,当外部服务需要使用这个接口的时候往往需要将RPC协议转换成HTTP协议。 @@ -31,9 +28,7 @@ API网关可以看做系统与外界联通的入口,我们可以在网关进 对于流量控制,熔断降级非业务逻辑可以统一放到网关层。 有很多业务都会自己去实现一层网关层,用来接入自己的服务,但是对于整个公司来说这还不够。 - -### 1.3 统一API网关 - +## 1.3 统一API网关 统一的API网关不仅有API网关的所有的特点,还有下面几个好处: - 统一技术组件升级 @@ -48,10 +43,8 @@ API网关可以看做系统与外界联通的入口,我们可以在网关进 不同业务不同部门如果按照我们上面的做法应该会都自己搞一个网关层,用来做这个事,可以想象如果一个公司有100个这种业务,每个业务配备4台机器,那么就需要400台机器。并且每个业务的开发RD都需要去开发这个网关层,去随时去维护,增加人力。如果有了统一网关层,那么也许只需要50台机器就可以做这100个业务的网关层的事,并且业务RD不需要随时关注开发,上线的步骤。 -## 2.统一网关的设计 - -### 2.1 异步化请求 - +# 2.统一网关的设计 +## 2.1 异步化请求 对于我们自己实现的网关层,由于只有我们自己使用,对于吞吐量的要求并不高所以,我们一般同步请求调用即可。 对于我们统一的网关层,如何用少量的机器接入更多的服务,这就需要我们的异步,用来提高更多的吞吐量。对于异步化一般有下面两种策略: @@ -67,86 +60,83 @@ Netty为高并发而生,目前唯品会的网关使用这个策略,在唯品 对于网关是HTTP请求场景比较多的情况可以采用Servlet,毕竟有更加成熟的处理HTTP协议。如果更加重视吞吐量那么可以采用Netty。 #### 2.1.1 全链路异步 - 对于来的请求我们已经使用异步了,为了达到全链路异步所以我们需要对去的请求也进行异步处理,对于去的请求我们可以利用我们rpc的异步支持进行异步请求所以基本可以达到下图: -[![img](https://camo.githubusercontent.com/ea78c61029cee6487f3aa0cecf5443f18d102173/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633837373331356535353761663f773d3133303026683d34383026663d706e6726733d3639323735)](https://camo.githubusercontent.com/ea78c61029cee6487f3aa0cecf5443f18d102173/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633837373331356535353761663f773d3133303026683d34383026663d706e6726733d3639323735) + +![](https://user-gold-cdn.xitu.io/2018/10/31/166c877315e557af?w=1300&h=480&f=png&s=69275) 由在web容器中开启servlet异步,然后进入到网关的业务线程池中进行业务处理,然后进行rpc的异步调用并注册需要回调的业务,最后在回调线程池中进行回调处理。 -### 2.2 链式处理 - +## 2.2 链式处理 在设计模式中有一个模式叫责任链模式,他的作用是避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。通过这种模式将请求的发送者和请求的处理者解耦了。在我们的各个框架中对此模式都有实现,比如servlet里面的filter,springmvc里面的Interceptor。 在Netflix Zuul中也应用了这种模式,如下图所示: -[![img](https://camo.githubusercontent.com/22d9288d6e9137b98aef563ff7a5a76a090a4609/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633837383234303735333733353f773d39363026683d37323026663d706e6726733d3439333134)](https://camo.githubusercontent.com/22d9288d6e9137b98aef563ff7a5a76a090a4609/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633837383234303735333733353f773d39363026683d37323026663d706e6726733d3439333134) +![](https://user-gold-cdn.xitu.io/2018/10/31/166c878240753735?w=960&h=720&f=png&s=49314) 这种模式在网关的设计中我们可以借鉴到自己的网关设计: - preFilters:前置过滤器,用来处理一些公共的业务,比如统一鉴权,统一限流,熔断降级,缓存处理等,并且提供业务方扩展。 + - routingFilters: 用来处理一些泛化调用,主要是做协议的转换,请求的路由工作。 + - postFilters: 后置过滤器,主要用来做结果的处理,日志打点,记录时间等等。 + - errorFilters: 错误过滤器,用来处理调用异常的情况。 这种设计在有赞的网关也有应用。 -### 2.3 业务隔离 - +## 2.3 业务隔离 上面在全链路异步的情况下不同业务之间的影响很小,但是如果在提供的自定义FiIlter中进行了某些同步调用,一旦超时频繁那么就会对其他业务产生影响。所以我们需要采用隔离之术,降低业务之间的互相影响。 #### 2.3.1 信号量隔离 - 信号量隔离只是限制了总的并发数,服务还是主线程进行同步调用。这个隔离如果远程调用超时依然会影响主线程,从而会影响其他业务。因此,如果只是想限制某个服务的总并发调用量或者调用的服务不涉及远程调用的话,可以使用轻量级的信号量来实现。有赞的网关由于没有自定义filter所以选取的是信号量隔离。 #### 2.3.2 线程池隔离 - 最简单的就是不同业务之间通过不同的线程池进行隔离,就算业务接口出现了问题由于线程池已经进行了隔离那么也不会影响其他业务。在京东的网关实现之中就是采用的线程池隔离,比较重要的业务比如商品或者订单 都是单独的通过线程池去处理。但是由于是统一网关平台,如果业务线众多,大家都觉得自己的业务比较重要需要单独的线程池隔离,如果使用的是Java语言开发的话那么,在Java中线程是比较重的资源比较受限,如果需要隔离的线程池过多不是很适用。如果使用一些其他语言比如Golang进行开发网关的话,线程是比较轻的资源,所以比较适合使用线程池隔离。 #### 2.3.3 集群隔离 - 如果有某些业务就需要使用隔离但是统一网关又没有线程池隔离那么应该怎么办呢?那么可以使用集群隔离,如果你的某些业务真的很重要那么可以为这一系列业务单独申请一个集群或者多个集群,通过机器之间进行隔离。 -### 2.4 请求限流 - +## 2.4 请求限流 流量控制可以采用很多开源的实现,比如阿里最近开源的Sentinel和比较成熟的Hystrix。 一般限流分为集群限流和单机限流: - - 利用统一存储保存当前流量的情况,一般可以采用Redis,这个一般会有一些性能损耗。 - 单机限流:限流每台机器我们可以直接利用Guava的令牌桶去做,由于没有远程调用性能消耗较小。 -### 2.5 熔断降级 - +## 2.5 熔断降级 这一块也可以参照开源的实现Sentinel和Hystrix,这里不是重点就不多提了。 - -### 2.6 泛化调用 - +## 2.6 泛化调用 泛化调用指的是一些通信协议的转换,比如将HTTP转换成Thrift。在一些开源的网关中比如Zuul是没有实现的,因为各个公司的内部服务通信协议都不同。比如在唯品会中支持HTTP1,HTTP2,以及二进制的协议,然后转化成内部的协议,淘宝的支持HTTPS,HTTP1,HTTP2这些协议都可以转换成,HTTP,HSF,Dubbo等协议。 #### 2.6.1泛化调用 - 如何去实现泛化调用呢?由于协议很难自动转换,那么其实每个协议对应的接口需要提供一种映射。简单来说就是把两个协议都能转换成共同语言,从而互相转换。 -[![img](https://camo.githubusercontent.com/d680a88d0dd9063fe53df26705836c4b7a19c121/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633838306163386264623537353f773d3133333826683d39363326663d706e6726733d3830313730)](https://camo.githubusercontent.com/d680a88d0dd9063fe53df26705836c4b7a19c121/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633838306163386264623537353f773d3133333826683d39363326663d706e6726733d3830313730)一般来说共同语言有三种方式指定: +![](https://user-gold-cdn.xitu.io/2018/10/31/166c880ac8bdb575?w=1338&h=963&f=png&s=80170) +一般来说共同语言有三种方式指定: - json:json数据格式比较简单,解析速度快,较轻量级。在Dubbo的生态中有一个HTTP转Dubbo的项目是用JsonRpc做的,将HTTP转化成JsonRpc再转化成Dubbo。 -比如可以将一个 [www.baidu.com/id](http://www.baidu.com/id) = 1 GET 可以映射为json: +比如可以将一个 www.baidu.com/id = 1 GET 可以映射为json: 代码块 ``` - +{ + “method”: "getBaidu" + "param" : { + "id" : 1 + } +} ``` - xml:xml数据比较重,解析比较困难,这里不过多讨论。 + - 自定义描述语言:一般来说这个成本比较高需要自己定义语言来进行描述并进行解析,但是其扩展性,自定义个性化性都是最高。例:spring自定义了一套自己的SPEL表达式语言 对于泛化调用如果要自己设计的话JSON基本可以满足,如果对于个性化的需要特别多的话倒是可以自己定义一套语言。 - -### 2.7 管理平台 - +## 2.7 管理平台 上面介绍的都是如何实现一个网关的技术关键。这里需要介绍网关的一个业务关键。有了网关之后,需要一个管理平台如何去对我们上面所描述的技术关键进行配置,包括但不限于下面这些配置: - 限流 @@ -156,29 +146,34 @@ Netty为高并发而生,目前唯品会的网关使用这个策略,在唯品 - 自定义filter - 泛化调用 -## 3.总结 - +# 3.总结 最后一个合理的标准网关应该按照如下去实现: -[![img](https://camo.githubusercontent.com/16eef64bd42ee7b2eb08c36039316fadb4e4d6b3/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633838343136633463633232373f773d3230313326683d3130303726663d706e6726733d313532363930)](https://camo.githubusercontent.com/16eef64bd42ee7b2eb08c36039316fadb4e4d6b3/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31302f33312f313636633838343136633463633232373f773d3230313326683d3130303726663d706e6726733d313532363930) +![](https://user-gold-cdn.xitu.io/2018/10/31/166c88416c4cc227?w=2013&h=1007&f=png&s=152690) -| --- | 京东 | 唯品会 | 有赞 | 阿里 | Zuul | -| -------- | ------------------------------ | ----------------------------- | ----------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 实现关键 | servlet3.0 | netty | servlet3.0 | servlet3.0 | servlet3.0 | -| 异步情况 | servlet异步,rpc是否异步不清楚 | 全链路异步 | 全链路异步 | 全链路异步 | Zuul1同步阻塞,Zuul2异步非阻塞 | -| 限流 | --- | --- | 平滑限流。最初是codis,后续换到每个单机的令牌桶限流。 | 1.基本流控:基于API的QPS做限流。2.运营流控:支持APP流量包,APP+API+USER的流控33.大促流控:APP访问API的权重流控。阿里开源:Sentinel | 提供了jar包:spring-cloud-zuul-ratelimit。1.对请求的目标URL进行限流(例如:某个URL每分钟只允许调用多少次)。2.对客户端的访问IP进行限流(例如:某个IP每分钟只允许请求多少次)3.对某些特定用户或者用户组进行限流(例如:非VIP用户限制每分钟只允许调用100次某个API等)4.多维度混合的限流。此时,就需要实现一些限流规则的编排机制。与、或、非等关系。支持四种存储方式ConcurrentHashMap,Consul,Redis,数据库。 | -| 熔断降级 | --- | --- | Hystrix | --- | 只支持服务级别熔断,不支持URL级别。 | -| 隔离 | 线程池隔离 | --- | 信号量隔离 | --- | 线程池隔离,信号量隔离 | -| 缓存 | redis | --- | 二级缓存,本地缓存+Codis | HDCC 本地缓存,远程缓存,数据库 | 需要自己开发 | -| 泛化调用 | --- | http,https,http1,http2,二进制 | dubbo,http,nova | hsf,dubbo,http,https,http2,http1 | 只支持http | + ---| 京东| 唯品会| 有赞| 阿里| Zuul +---|---|---|---|---|--- +实现关键|servlet3.0|netty|servlet3.0|servlet3.0|servlet3.0 +异步情况|servlet异步,rpc是否异步不清楚|全链路异步|全链路异步|全链路异步|Zuul1同步阻塞,Zuul2异步非阻塞 + 限流 |---|---|平滑限流。最初是codis,后续换到每个单机的令牌桶限流。|1.基本流控:基于API的QPS做限流。2.运营流控:支持APP流量包,APP+API+USER的流控33.大促流控:APP访问API的权重流控。阿里开源:Sentinel|提供了jar包:spring-cloud-zuul-ratelimit。1.对请求的目标URL进行限流(例如:某个URL每分钟只允许调用多少次)。2.对客户端的访问IP进行限流(例如:某个IP每分钟只允许请求多少次)3.对某些特定用户或者用户组进行限流(例如:非VIP用户限制每分钟只允许调用100次某个API等)4.多维度混合的限流。此时,就需要实现一些限流规则的编排机制。与、或、非等关系。支持四种存储方式ConcurrentHashMap,Consul,Redis,数据库。 +熔断降级|---|---|Hystrix|---|只支持服务级别熔断,不支持URL级别。 +隔离|线程池隔离|---|信号量隔离|---|线程池隔离,信号量隔离 +缓存|redis|---|二级缓存,本地缓存+Codis|HDCC 本地缓存,远程缓存,数据库|需要自己开发 +泛化调用|---|http,https,http1,http2,二进制|dubbo,http,nova|hsf,dubbo,http,https,http2,http1|只支持http -## 4.参考 + +# 4.参考 - 京东:http://www.yunweipai.com/archives/23653.html + - 有赞网关:https://tech.youzan.com/api-gateway-in-practice/ + - 唯品会:https://mp.weixin.qq.com/s/gREMe-G7nqNJJLzbZ3ed3A + - Zuul:http://www.scienjus.com/api-gateway-and-netflix-zuul/ + + ## 公众号 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 diff --git a/docs/system-design/micro-service/api-gateway-intro.md b/docs/system-design/micro-service/api-gateway-intro.md new file mode 100644 index 00000000..681f3dc8 --- /dev/null +++ b/docs/system-design/micro-service/api-gateway-intro.md @@ -0,0 +1,37 @@ +### 为什么要网关? + +微服务下一个系统被拆分为多个服务,但是像 安全认证,流量控制,日志,监控等功能是每个服务都需要的,没有网关的话,我们就需要在每个服务中单独实现,这使得我们做了很多重复的事情并且没有一个全局的视图来统一管理这些功能。 + +综上:**一般情况下,网关一般都会提供请求转发、安全认证(身份/权限认证)、流量控制、负载均衡、容灾、日志、监控这些功能。** + +上面介绍了这么多功能实际上网关主要做了一件事情:**请求过滤** 。权限校验、流量控制这些都可以通过过滤器实现,请求转也是通过过滤器实现的。 + +### 你知道有哪些常见的网关系统? + +我所了解的目前经常用到的开源 API 网关系统有: + +1. Kong +2. Netflix zuul + +下图来源:https://www.stackshare.io/stackups/kong-vs-zuul 。 + +![Kong vs Netflix zuul](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/kong-vs-zuul.jpg) + +可以看出不论是社区活跃度还是 Star数, Kong 都是略胜一筹。总的来说,Kong 相比于 Zuul 更加强大并且简单易用。Kong 基于 Openresty ,Zuul 基于 Java。 + +> OpenResty(也称为 ngx_openresty)是一个全功能的 Web 应用服务器。它打包了标准的 Nginx 核心,很多的常用的第三方模块,以及它们的大多数依赖项。 +> +> 通过揉和众多设计良好的 Nginx 模块,OpenResty 有效地把 Nginx 服务器转变为一个强大的 Web 应用服务器,基于它开发人员可以使用 Lua 编程语言对 Nginx 核心以及现有的各种 Nginx C 模块进行脚本编程,构建出可以处理一万以上并发请求的极端高性能的 Web 应用。——OpenResty + +另外, 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" +``` + +ps:这里没有太深入去探讨,需要深入了解的话可以自行查阅相关资料。 \ No newline at end of file diff --git a/docs/system-design/micro-service/limit-request.md b/docs/system-design/micro-service/limit-request.md new file mode 100644 index 00000000..3d42c29a --- /dev/null +++ b/docs/system-design/micro-service/limit-request.md @@ -0,0 +1,35 @@ +### 限流的算法有哪些? + +简单介绍 4 种非常好理解并且容易实现的限流算法! + +下图的图片不是 Guide 哥自己画的哦!图片来源于 InfoQ 的一篇文章[《分布式服务限流实战,已经为你排好坑了》](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673)。 + +#### 固定窗口计数器算法 + +规定我们单位时间处理的请求数量。比如我们规定我们的一个接口一分钟只能访问10次的话。使用固定窗口计数器算法的话可以这样实现:给定一个变量counter来记录处理的请求数量,当1分钟之内处理一个请求之后counter+1,1分钟之内的如果counter=100的话,后续的请求就会被全部拒绝。等到 1分钟结束后,将counter回归成0,重新开始计数(ps:只要过了一个周期就讲counter回归成0)。 + +这种限流算法无法保证限流速率,因而无法保证突然激增的流量。比如我们限制一个接口一分钟只能访问10次的话,前半分钟一个请求没有接收,后半分钟接收了10个请求。 + +![固定窗口计数器算法](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) + +### \ No newline at end of file diff --git a/docs/system-design/micro-service/spring-cloud.md b/docs/system-design/micro-service/spring-cloud.md index 872caab2..34c3680f 100644 --- a/docs/system-design/micro-service/spring-cloud.md +++ b/docs/system-design/micro-service/spring-cloud.md @@ -1,4 +1,8 @@ > 本文基于 Spring Cloud Netflix 。Spring Cloud Alibaba 也是非常不错的选择哦! +> +> 授权转载自:https://juejin.im/post/5de2553e5188256e885f4fa3 + + 首先我给大家看一张图,如果大家对这张图有些地方不太理解的话,我希望你们看完我这篇文章会恍然大悟。 diff --git a/docs/system-design/naming.md b/docs/system-design/naming.md new file mode 100644 index 00000000..aa4aee86 --- /dev/null +++ b/docs/system-design/naming.md @@ -0,0 +1,213 @@ +编程过程中,有太多太多让我们头疼的事情了,比如命名、维护其他人的代码、写测试、与其他人沟通交流等等。就连世界级软件大师 **Martin Fowler** 大神都说过 CS 领域有两大最难的事情,一是**缓存失效**,一是**程序命名**(@ [https://martinfowler.com/bliki/TwoHardThings.html](https://martinfowler.com/bliki/TwoHardThings.html))。 + +![](pictures/marting-naming.png) + +今天 Guide 就单独拎出 “**命名**” 来聊聊,据说之前在 Quora 网站,由接近 5000 名程序员票选出来的最难的事情就是“命名”。 + +这篇文章配合我之前发的 [《编码 5 分钟,命名 2 小时?史上最全的 Java 命名规范参考!》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486449&idx=1&sn=c3b502529ff991c7180281bcc22877af&chksm=cea2443af9d5cd2c1c87049ed15ccf6f88275419c7dbe542406166a703b27d0f3ecf2af901f8&token=999884676&lang=zh_CN#rd) 这篇文章阅读效果更佳哦! + +## 为什么需要重视命名? + +**好的命名即是注释,别人一看到你的命名就知道你的变量、方法或者类是做什么的!** 好的命名对于其他人(包括你自己)理解你的代码有着很大的帮助! + +简单举个例子说明一下命名的重要性。 + +《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()) +> ``` + +## 常见命名规则以及适用场景 + +这里只介绍 3 种最常见的命名规范。 + +### 驼峰命名法(CamelCase) + +驼峰命名法应该我们最常见的一个,这种命名方式使用大小写混合的格式来区别各个单词,并且单词之间不使用空格隔开或者连接字符连接的命名方式 + +#### 大驼峰命名法(CamelCase) + +**类名需要使用大驼峰命名法(UpperCamelCase)** + +正例: + +```java +ServiceDiscovery、ServiceInstance、LruCacheFactory +``` + +反例: + +```java +serviceDiscovery、Serviceinstance、LRUCacheFactory +``` + +#### 小驼峰命名法(lowerCamelCase) + +**方法名、参数名、成员变量、局部变量需要使用小驼峰命名法(lowerCamelCase)。** + +正例: + +```java +getUserInfo()、createCustomThreadPool()、setNameFormat(String nameFormat) +Uservice userService; +``` + +反例: + +```java +GetUserInfo()、CreateCustomThreadPool()、setNameFormat(String NameFormat) +Uservice user_service +``` + +### 蛇形命名法(snake_case) + +**测试方法名、常量、枚举名称需要使用蛇形命名法(snake_case)** + +在蛇形命名法中,各个单词之间通过下划线“\_”连接,比如`should_get_200_status_code_when_request_is_valid`、`CLIENT_CONNECT_SERVER_FAILURE`。 + +蛇形命名法的优势是命名所需要的单词比较多的时候,比如我把上面的命名通过小驼峰命名法给大家看一下:“shouldGet200StatusCodoWhenRequestIsValid”。**感觉如何? 相比于使用蛇形命名法(snake_case)来说是不是不那么易读?\*\*** + +正例: + +```java +@Test +void should_get_200_status_code_when_request_is_valid() { + ...... +} +``` + +反例: + +```java +@Test +void shouldGet200StatusCodoWhenRequestIsValid() { + ...... +} +``` + +### 串式命名法(kebab-case) + +在串式命名法中,各个单词之间通过下划线“-”连接,比如`dubbo-registry`。 + +建议项目文件夹名称使用串式命名法(kebab-case),比如 dubbo 项目的各个模块的命名是下面这样的。 + +![](./pictures/dubbo-naming.png) + +## 常见命名规范 + +### Java 语言基本命名规范 + +**1.类名需要使用大驼峰命名法(UpperCamelCase)风格。方法名、参数名、成员变量、局部变量需要使用小驼峰命名法(lowerCamelCase)。** + +**2.测试方法名、常量、枚举名称需要使用蛇形命名法(snake_case)**,比如`should_get_200_status_code_when_request_is_valid`、`CLIENT_CONNECT_SERVER_FAILURE`。并且,**测试方法名称要求全部小写,常量以及枚举名称需要全部大写。** + +**3.项目文件夹名称使用串式命名法(kebab-case),比如`dubbo-registry`。** + +**4.包名统一使用小写,尽量使用单个名词作为包名,各个单词通过 "." 分隔符连接,并且各个单词必须为单数。** + +正例: `org.apache.dubbo.common.threadlocal` + +反例: ~~`org.apache.dubbo.common.threadLocal`~~ + +**5.抽象类命名使用 Abstract 开头**。 + +```java +//为远程传输部分抽象出来的一个抽象类(出处:Dubbo源码) +public abstract class AbstractClient extends AbstractEndpoint implements Client { + +} +``` + +**6.异常类命名使用 Exception 结尾。** + +```java +//自定义的 NoSuchMethodException(出处:Dubbo源码) +public class NoSuchMethodException extends RuntimeException { + private static final long serialVersionUID = -2725364246023268766L; + + public NoSuchMethodException() { + super(); + } + + public NoSuchMethodException(String msg) { + super(msg); + } +} +``` + +**7.测试类命名以它要测试的类的名称开始,以 Test 结尾。** + +```java +//为 AnnotationUtils 类写的测试类(出处:Dubbo源码) +public class AnnotationUtilsTest { + ...... +} +``` + +POJO 类中布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。 + +如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。 + +### 命名易读性规范 + +**1.为了能让命名更加易懂和易读,尽量不要缩写/简写单词,除非这些单词已经被公认可以被这样缩写/简写。比如 `CustomThreadFactory` 不可以被写成 ~~`CustomTF` 。** + +**2.命名不像函数一样要尽量追求短,可读性强的名字优先于简短的名字,虽然可读性强的名字会比较长一点。** 这个对应我们上面说的第 1 点。 + +**3.避免无意义的命名,你起的每一个名字都要能表明意思。** + +正例:`UserService userService;` `int userCount`; + +反例: ~~`UserService service`~~ ~~`int count`~~ + +**4.避免命名过长(50 个字符以内最好),过长的命名难以阅读并且丑陋。** + +5.**不要使用拼音,更不要使用中文。** 注意:像 alibaba 、wuhan、taobao 这种国际通用名词可以当做英文来看待。 + +正例:discount + +反例:~~dazhe~~ + +## Codelf:变量命名神器? + +这是一个由国人开发的网站,网上有很多人称其为变量命名神器, Guide 在实际使用了几天之后感觉没那么好用。小伙伴们可以自行体验一下,然后再给出自己的判断。 + +Codelf 提供了在线网站版本,网址:[https://unbug.github.io/codelf/](https://unbug.github.io/codelf/),具体使用情况如下: + +我选择了 Java 编程语言,然后搜索了“序列化”这个关键词,然后它就返回了很多关于序列化的命名。 + +![](pictures/Codelf.png) + +并且,Codelf 还提供了 VS code 插件,看这个评价,看来大家还是很喜欢这款命名工具的。 + +![](pictures/vscode-codelf.png) + +## 总结 + +Guide 制作了一个涵盖上面所有重要内容的思维导图,便于小伙伴们日后查阅。 + +![](pictures/naming-mindmap.png) + +## 其他推荐阅读 + +1. 《阿里巴巴 Java 开发手册》 +2. 《Clean Code》 +3. Google Java 代码指南:https://google.github.io/styleguide/javaguide.html#s5.1-identifier-name + + diff --git a/docs/system-design/pictures/Codelf.png b/docs/system-design/pictures/Codelf.png new file mode 100644 index 00000000..2f030785 Binary files /dev/null and b/docs/system-design/pictures/Codelf.png differ diff --git a/docs/system-design/pictures/Session-Based-Authentication-flow.png b/docs/system-design/pictures/Session-Based-Authentication-flow.png new file mode 100644 index 00000000..4a21c3d4 Binary files /dev/null and b/docs/system-design/pictures/Session-Based-Authentication-flow.png differ diff --git a/docs/system-design/pictures/Token-Based-Authentication.png b/docs/system-design/pictures/Token-Based-Authentication.png new file mode 100644 index 00000000..ad30fd37 Binary files /dev/null and b/docs/system-design/pictures/Token-Based-Authentication.png differ diff --git a/docs/system-design/pictures/authentication.png b/docs/system-design/pictures/authentication.png new file mode 100644 index 00000000..66f12050 Binary files /dev/null and b/docs/system-design/pictures/authentication.png differ diff --git a/docs/system-design/pictures/authorization.png b/docs/system-design/pictures/authorization.png new file mode 100644 index 00000000..9f3afc0f Binary files /dev/null and b/docs/system-design/pictures/authorization.png differ diff --git a/docs/system-design/pictures/cookie-sessionId.png b/docs/system-design/pictures/cookie-sessionId.png new file mode 100644 index 00000000..f3940106 Binary files /dev/null and b/docs/system-design/pictures/cookie-sessionId.png differ diff --git a/docs/system-design/pictures/dubbo-naming.png b/docs/system-design/pictures/dubbo-naming.png new file mode 100644 index 00000000..2081bb88 Binary files /dev/null and b/docs/system-design/pictures/dubbo-naming.png differ diff --git a/docs/system-design/pictures/marting-naming.png b/docs/system-design/pictures/marting-naming.png new file mode 100644 index 00000000..5a797c43 Binary files /dev/null and b/docs/system-design/pictures/marting-naming.png differ diff --git a/docs/system-design/pictures/naming-mindmap.png b/docs/system-design/pictures/naming-mindmap.png new file mode 100644 index 00000000..131f7888 Binary files /dev/null and b/docs/system-design/pictures/naming-mindmap.png differ diff --git a/docs/system-design/pictures/session-cookie-intro.png b/docs/system-design/pictures/session-cookie-intro.png new file mode 100644 index 00000000..b4ef47ac Binary files /dev/null and b/docs/system-design/pictures/session-cookie-intro.png differ diff --git a/docs/system-design/pictures/vscode-codelf.png b/docs/system-design/pictures/vscode-codelf.png new file mode 100644 index 00000000..96b64502 Binary files /dev/null and b/docs/system-design/pictures/vscode-codelf.png differ diff --git a/docs/system-design/pictures/微信支付-fnglfdlgdfj.png b/docs/system-design/pictures/微信支付-fnglfdlgdfj.png new file mode 100644 index 00000000..b3c75a97 Binary files /dev/null and b/docs/system-design/pictures/微信支付-fnglfdlgdfj.png differ diff --git a/docs/system-design/restful-api.md b/docs/system-design/restful-api.md new file mode 100644 index 00000000..af4fbb43 --- /dev/null +++ b/docs/system-design/restful-api.md @@ -0,0 +1,141 @@ +![bd4442aed16acafc54c7943d34abff0edadfa74c](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/bd4442aed16acafc54c7943d34abff0edadfa74c.png) + +大家好我是 Guide 哥!这是我的第 **210** 篇优质原创!这篇文章主要分享了后端程序员必备的 RestFul API 相关的知识。 + +RESTful API 是每个程序员都应该了解并掌握的基本知识,我们在开发过程中设计 API 的时候也应该至少要满足 RESTful API 的最基本的要求(比如接口中尽量使用名词,使用 POST 请求创建资源,DELETE 请求删除资源等等,示例:`GET /notes/id`:获取某个指定 id 的笔记的信息)。 + +如果你看 RESTful API 相关的文章的话一般都比较晦涩难懂,包括我下面的文章也会提到一些概念性的东西。但是,实际上我们平时开发用到的 RESTful API 的知识非常简单也很容易概括!举个例子,如果我给你下面两个 url 你是不是立马能知道它们是干什么的!这就是 RESTful API 的强大之处! + +**RESTful API 可以你看到 url + http method 就知道这个 url 是干什么的,让你看到了 http 状态码(status code)就知道请求结果如何。** + +``` +GET /classes:列出所有班级 +POST /classes:新建一个班级 +``` + +下面的内容只是介绍了我觉得关于 RESTful API 比较重要的一些东西,欢迎补充。 + +### 一、重要概念 + +REST,即 **REpresentational State Transfer** 的缩写。这个词组的翻译过来就是"表现层状态转化"。这样理解起来甚是晦涩,实际上 REST 的全称是 **Resource Representational State Transfe** ,直白地翻译过来就是 **“资源”在网络传输中以某种“表现形式”进行“状态转移”** 。如果还是不能继续理解,请继续往下看,相信下面的讲解一定能让你理解到底啥是 REST 。 + +我们分别对上面涉及到的概念进行解读,以便加深理解,不过实际上你不需要搞懂下面这些概念,也能看懂我下一部分要介绍到的内容。不过,为了更好地能跟别人扯扯 “RESTful API”我建议你还是要好好理解一下! + +- **资源(Resource)** :我们可以把真实的对象数据称为资源。一个资源既可以是一个集合,也可以是单个个体。比如我们的班级 classes 是代表一个集合形式的资源,而特定的 class 代表单个个体资源。每一种资源都有特定的 URI(统一资源定位符)与之对应,如果我们需要获取这个资源,访问这个 URI 就可以了,比如获取特定的班级:`/class/12`。另外,资源也可以包含子资源,比如 `/classes/classId/teachers`:列出某个指定班级的所有老师的信息 +- **表现形式(Representational)**:"资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式比如 json,xml,image,txt 等等叫做它的"表现层/表现形式"。 +- **状态转移(State Transfer)** :大家第一眼看到这个词语一定会很懵逼?内心 BB:这尼玛是啥啊? 大白话来说 REST 中的状态转移更多地描述的服务器端资源的状态,比如你通过增删改查(通过 HTTP 动词实现)引起资源状态的改变。ps:互联网通信协议 HTTP 协议,是一个无状态协议,所有的资源状态都保存在服务器端。 + +综合上面的解释,我们总结一下什么是 RESTful 架构: + +1. 每一个 URI 代表一种资源; +2. 客户端和服务器之间,传递这种资源的某种表现形式比如 json,xml,image,txt 等等; +3. 客户端通过特定的 HTTP 动词,对服务器端资源进行操作,实现"表现层状态转化"。 + +### 二、REST 接口规范 + +#### 1、动作 + +- GET :请求从服务器获取特定资源。举个例子:`GET /classes`(获取所有班级) +- POST :在服务器上创建一个新的资源。举个例子:`POST /classes`(创建班级) +- PUT :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:`PUT /classes/12`(更新编号为 12 的班级) +- DELETE :从服务器删除特定的资源。举个例子:`DELETE /classes/12`(删除编号为 12 的班级) +- PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。 + +#### 2、路径(接口命名) + +路径又称"终点"(endpoint),表示 API 的具体网址。实际开发中常见的规范如下: + +1. **网址中不能有动词,只能有名词,API 中的名词也应该使用复数。** 因为 REST 中的资源往往和数据库中的表对应,而数据库中的表都是同种记录的"集合"(collection)。**如果 API 调用并不涉及资源(如计算,翻译等操作)的话,可以用动词。** 比如:`GET /calculate?param1=11¶m2=33` +2. 不用大写字母,建议用中杠 - 不用下杠 \_ 比如邀请码写成 `invitation-code`而不是 ~~invitation_code~~ + +Talk is cheap!来举个实际的例子来说明一下吧!现在有这样一个 API 提供班级(class)的信息,还包括班级中的学生和教师的信息,则它的路径应该设计成下面这样。 + +**接口尽量使用名词,禁止使用动词。** 下面是一些例子: + +``` +GET /classes:列出所有班级 +POST /classes:新建一个班级 +GET /classes/classId:获取某个指定班级的信息 +PUT /classes/classId:更新某个指定班级的信息(一般倾向整体更新) +PATCH /classes/classId:更新某个指定班级的信息(一般倾向部分更新) +DELETE /classes/classId:删除某个班级 +GET /classes/classId/teachers:列出某个指定班级的所有老师的信息 +GET /classes/classId/students:列出某个指定班级的所有学生的信息 +DELETE classes/classId/teachers/ID:删除某个指定班级下的指定的老师的信息 +``` + +反例: + +``` +/getAllclasses +/createNewclass +/deleteAllActiveclasses +``` + +理清资源的层次结构,比如业务针对的范围是学校,那么学校会是一级资源:`/schools`,老师: `/schools/teachers`,学生: `/schools/students` 就是二级资源。 + +#### 3、过滤信息(Filtering) + +如果我们在查询的时候需要添加特定条件的话,建议使用 url 参数的形式。比如我们要查询 state 状态为 active 并且 name 为 guidegege 的班级: + +``` +GET /classes?state=active&name=guidegege +``` + +比如我们要实现分页查询: + +``` +GET /classes?page=1&size=10 //指定第1页,每页10个数据 +``` + +#### 4、状态码(Status Codes) + +**状态码范围:** + +| 2xx:成功 | 3xx:重定向 | 4xx:客户端错误 | 5xx:服务器错误 | +| --------- | -------------- | ---------------- | --------------- | +| 200 成功 | 301 永久重定向 | 400 错误请求 | 500 服务器错误 | +| 201 创建 | 304 资源未修改 | 401 未授权 | 502 网关错误 | +| | | 403 禁止访问 | 504 网关超时 | +| | | 404 未找到 | | +| | | 405 请求方法不对 | | + + +### 三 HATEOAS + +> **RESTful 的极致是 hateoas ,但是这个基本不会在实际项目中用到。** + +上面是 RESTful API 最基本的东西,也是我们平时开发过程中最容易实践到的。实际上,RESTful API 最好做到 Hypermedia,即返回结果中提供链接,连向其他 API 方法,使得用户不查文档,也知道下一步应该做什么。 + +比如,当用户向 api.example.com 的根目录发出请求,会得到这样一个文档。 + +```javascript +{"link": { + "rel": "collection https://www.example.com/classes", + "href": "https://api.example.com/classes", + "title": "List of classes", + "type": "application/vnd.yourformat+json" +}} +``` + +上面代码表示,文档中有一个 link 属性,用户读取这个属性就知道下一步该调用什么 API 了。rel 表示这个 API 与当前网址的关系(collection 关系,并给出该 collection 的网址),href 表示 API 的路径,title 表示 API 的标题,type 表示返回类型 Hypermedia API 的设计被称为[HATEOAS](http://en.wikipedia.org/wiki/HATEOAS)。 + +在 Spring 中有一个叫做 HATEOAS 的 API 库,通过它我们可以更轻松的创建除符合 HATEOAS 设计的 API。 + +### 文章推荐 + +**RESTful API 介绍:** + +- [RESTful API Tutorial](https://RESTfulapi.net/) +- [RESTful API 最佳指南](http://www.ruanyifeng.com/blog/2014/05/RESTful_api.html)(阮一峰,这篇文章大部分内容来源) +- [[译] RESTful API 设计最佳实践](https://juejin.im/entry/59e460c951882542f578f2f0) +- [那些年,我们一起误解过的 REST](https://segmentfault.com/a/1190000016313947) +- [Testing RESTful Services in Java: Best Practices](https://phauer.com/2016/testing-RESTful-services-java-best-practices/) + +**Spring 中使用 HATEOAS:** + +- [在 Spring Boot 中使用 HATEOAS](a) +- [Building REST services with Spring](https://spring.io/guides/tutorials/classmarks/) (Spring 官网 ) +- [An Intro to Spring HATEOAS](https://www.baeldung.com/spring-hateoas-tutorial) (by [baeldung](https://www.baeldung.com/author/baeldung/)) +- [spring-hateoas-examples](https://github.com/spring-projects/spring-hateoas-examples/tree/master/hypermedia) +- [Spring HATEOAS](https://spring.io/projects/spring-hateoas#learn) (Spring 官网 ) \ No newline at end of file diff --git a/docs/system-design/website-architecture/分布式.md b/docs/system-design/website-architecture/分布式.md index d5632f5b..52a9d9f6 100644 --- a/docs/system-design/website-architecture/分布式.md +++ b/docs/system-design/website-architecture/分布式.md @@ -1,37 +1,39 @@ - - ### 一 分布式系统的经典基础理论 - - [分布式系统的经典基础理论](https://blog.csdn.net/qq_34337272/article/details/80444032) +### 一 分布式系统的经典基础理论 - 本文主要是简单的介绍了三个常见的概念: **分布式系统设计理念** 、 **CAP定理** 、 **BASE理论** ,关于分布式系统的还有很多很多东西。 - ![分布式系统的经典基础理论总结](https://user-gold-cdn.xitu.io/2018/5/24/1639234237ec9805?w=791&h=466&f=png&s=55908) +[分布式系统的经典基础理论](https://blog.csdn.net/qq_34337272/article/details/80444032) +本文主要是简单的介绍了三个常见的概念: **分布式系统设计理念** 、 **CAP定理** 、 **BASE理论** ,关于分布式系统的还有很多很多东西。 - - ### 二 分布式事务 - 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。 - * [深入理解分布式事务](http://www.codeceo.com/article/distributed-transaction.html) - * [分布式事务?No, 最终一致性](https://zhuanlan.zhihu.com/p/25933039) - * [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) +![分布式系统的经典基础理论总结](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/1639234237ec9805.png) + +### 二 分布式事务 + +分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。 +- [深入理解分布式事务](http://www.codeceo.com/article/distributed-transaction.html) +- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) - - ### 三 分布式系统一致性 - [分布式服务化系统一致性的“最佳实干”](https://www.jianshu.com/p/1156151e20c8) +### 三 分布式系统一致性 - - ### 四 一致性协议/算法 - 早在1898年就诞生了著名的 **Paxos经典算法** (**Zookeeper就采用了Paxos算法的近亲兄弟Zab算法**),但由于Paxos算法非常难以理解、实现、排错。所以不断有人尝试简化这一算法,直到2013年才有了重大突破:斯坦福的Diego Ongaro、John Ousterhout以易懂性为目标设计了新的一致性算法—— **Raft算法** ,并发布了对应的论文《In Search of an Understandable Consensus Algorithm》,到现在有十多种语言实现的Raft算法框架,较为出名的有以Go语言实现的Etcd,它的功能类似于Zookeeper,但采用了更为主流的Rest接口。 - * [图解 Paxos 一致性协议](https://mp.weixin.qq.com/s?__biz=MzI0NDI0MTgyOA==&mid=2652037784&idx=1&sn=d8c4f31a9cfb49ee91d05bb374e5cdd5&chksm=f2868653c5f10f45fc4a64d15a5f4163c3e66c00ed2ad334fa93edb46671f42db6752001f6c0#rd) - * [图解分布式协议-RAFT](http://ifeve.com/raft/) - * [Zookeeper ZAB 协议分析](https://dbaplus.cn/news-141-1875-1.html) +[分布式服务化系统一致性的“最佳实干”](https://www.jianshu.com/p/1156151e20c8) -- ### 五 分布式存储 +### 四 一致性协议/算法 - **分布式存储系统将数据分散存储在多台独立的设备上**。传统的网络存储系统采用集中的存储服务器存放所有数据,存储服务器成为系统性能的瓶颈,也是可靠性和安全性的焦点,不能满足大规模存储应用的需要。分布式网络存储系统采用可扩展的系统结构,利用多台存储服务器分担存储负荷,利用位置服务器定位存储信息,它不但提高了系统的可靠性、可用性和存取效率,还易于扩展。 - - * [分布式存储系统概要](http://witchiman.top/2017/05/05/distributed-system/) - -- ### 六 分布式计算 +早在1900年就诞生了著名的 **Paxos经典算法** (**Zookeeper就采用了Paxos算法的近亲兄弟Zab算法**),但由于Paxos算法非常难以理解、实现、排错。所以不断有人尝试简化这一算法,直到2013年才有了重大突破:斯坦福的Diego Ongaro、John Ousterhout以易懂性为目标设计了新的一致性算法—— **Raft算法** ,并发布了对应的论文《In Search of an Understandable Consensus Algorithm》,到现在有十多种语言实现的Raft算法框架,较为出名的有以Go语言实现的Etcd,它的功能类似于Zookeeper,但采用了更为主流的Rest接口。 + +* [图解 Paxos 一致性协议](https://mp.weixin.qq.com/s?__biz=MzI0NDI0MTgyOA==&mid=2652037784&idx=1&sn=d8c4f31a9cfb49ee91d05bb374e5cdd5&chksm=f2868653c5f10f45fc4a64d15a5f4163c3e66c00ed2ad334fa93edb46671f42db6752001f6c0#rd) +* [图解分布式协议-RAFT](http://ifeve.com/raft/) +* [Zookeeper ZAB 协议分析](https://dbaplus.cn/news-141-1875-1.html) + +### 五 分布式存储 + +**分布式存储系统将数据分散存储在多台独立的设备上**。传统的网络存储系统采用集中的存储服务器存放所有数据,存储服务器成为系统性能的瓶颈,也是可靠性和安全性的焦点,不能满足大规模存储应用的需要。分布式网络存储系统采用可扩展的系统结构,利用多台存储服务器分担存储负荷,利用位置服务器定位存储信息,它不但提高了系统的可靠性、可用性和存取效率,还易于扩展。 + +* [分布式存储系统概要](http://witchiman.top/2017/05/05/distributed-system/) + +### 六 分布式计算 + +**所谓分布式计算是一门计算机科学,它研究如何把一个需要非常巨大的计算能力才能解决的问题分成许多小的部分,然后把这些部分分配给许多计算机进行处理,最后把这些计算结果综合起来得到最终的结果。** +分布式网络存储技术是将数据分散的存储于多台独立的机器设备上。分布式网络存储系统采用可扩展的系统结构,利用多台存储服务器分担存储负荷,利用位置服务器定位存储信息,不但解决了传统集中式存储系统中单存储服务器的瓶颈问题,还提高了系统的可靠性、可用性和扩展性。 + +* [关于分布式计算的一些概念](https://blog.csdn.net/qq_34337272/article/details/80549020) - **所谓分布式计算是一门计算机科学,它研究如何把一个需要非常巨大的计算能力才能解决的问题分成许多小的部分,然后把这些部分分配给许多计算机进行处理,最后把这些计算结果综合起来得到最终的结果。** - 分布式网络存储技术是将数据分散的存储于多台独立的机器设备上。分布式网络存储系统采用可扩展的系统结构,利用多台存储服务器分担存储负荷,利用位置服务器定位存储信息,不但解决了传统集中式存储系统中单存储服务器的瓶颈问题,还提高了系统的可靠性、可用性和扩展性。 - - * [关于分布式计算的一些概念](https://blog.csdn.net/qq_34337272/article/details/80549020) - - \ No newline at end of file diff --git a/docs/system-design/设计模式.md b/docs/system-design/设计模式.md index 6af52e41..c7a86716 100644 --- a/docs/system-design/设计模式.md +++ b/docs/system-design/设计模式.md @@ -35,7 +35,7 @@ ### 常见结构型模式详解 - **适配器模式:** - - [深入理解适配器模式——加个“适配器”以便于复用](https://segmentfault.com/a/1190000011856448) + - [深入理解适配器模式——加个“适配器”以便于复用](https://segmentfault.com/a/1190000011856448) https://blog.csdn.net/carson_ho/article/details/54910430 - [适配器模式原理及实例介绍-IBM](https://www.ibm.com/developerworks/cn/java/j-lo-adapter-pattern/index.html) - **桥接模式:** [设计模式笔记16:桥接模式(Bridge Pattern)](https://blog.csdn.net/yangzl2008/article/details/7670996) - **组合模式:** [大话设计模式—组合模式](https://blog.csdn.net/lmb55/article/details/51039781) @@ -68,7 +68,7 @@ - [责任链模式实现的三种方式](https://www.cnblogs.com/lizo/p/7503862.html) - **命令模式:** 在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。 - **解释器模式:** -- **迭代器模式:** +- **迭代器模式:** - **中介者模式:** - **备忘录模式:** - **观察者模式:** diff --git a/docs/tools/Docker-Image.md b/docs/tools/Docker-Image.md index 43df839c..c7270439 100644 --- a/docs/tools/Docker-Image.md +++ b/docs/tools/Docker-Image.md @@ -214,7 +214,7 @@ docker search mysql ### 3.2 search 子命令 -命令行输入 `docker search--help`, 输出如下: +命令行输入 `docker search --help`, 输出如下: ``` Usage: docker search [OPTIONS] TERM diff --git a/docs/tools/Docker.md b/docs/tools/Docker.md index dbf00dce..2aca6d1f 100644 --- a/docs/tools/Docker.md +++ b/docs/tools/Docker.md @@ -172,7 +172,7 @@ Docker 设计时,就充分利用 **Union FS**的技术,将其设计为 **分 比如我们想要搜索自己想要的镜像: -![利用Docker Hub 搜索镜像](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/Screen Shot 2019-11-04 at 8.21.39 PM.png) +![利用Docker Hub 搜索镜像](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/Screen%20Shot%202019-11-04%20at%208.21.39%20PM.png) 在 Docker Hub 的搜索结果中,有几项关键的信息有助于我们选择合适的镜像: diff --git a/docs/tools/Git.md b/docs/tools/Git.md index d6dc1de7..78ea2805 100644 --- a/docs/tools/Git.md +++ b/docs/tools/Git.md @@ -152,7 +152,7 @@ Git 有三种状态,你的文件可能处于其中之一: 主体部分可以是很少的几行,来加入更多的细节来解释提交,最好是能给出一些相关的背景或者解释这个提交能修复和解决什么问题。 主体部分当然也可以有几段,但是一定要注意换行和句子不要太长。因为这样在使用 "git log" 的时候会有缩进比较好看。 - + 提交的标题行描述应该尽量的清晰和尽量的一句话概括。这样就方便相关的 Git 日志查看工具显示和其他人的阅读。 ### 推送改动到远程仓库 @@ -260,9 +260,13 @@ git branch -d feature_x git push origin ``` +## 推荐 +**在线演示学习工具:** -## 推荐阅读 +「补充,来自[issue729](https://github.com/Snailclimb/JavaGuide/issues/729)」Learn Git Branching https://oschina.gitee.io/learn-git-branching/ 。该网站可以方便的演示基本的git操作,讲解得明明白白。每一个基本命令的作用和结果。 + +**推荐阅读:** - [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html) - [图解Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html) diff --git a/docs/tools/github/github-star-ranking.md b/docs/tools/github/github-star-ranking.md index e68ffe9c..fa0c42ee 100644 --- a/docs/tools/github/github-star-ranking.md +++ b/docs/tools/github/github-star-ranking.md @@ -1,14 +1,4 @@ -## 题外话 -先来点题外话吧!如果想看正文的话可以直接看滑到下面正文。 - -来三亚旅行也有几天了,总体感觉很不错,后天就要返航回家了。偶尔出来散散心真的挺不错,放松一下自己的心情,感受一下大自然。个人感觉冬天的时候来三亚度假还是很不错的选择,但是不要 1 月份的时候过来(差不多就过年那会儿),那时候属于大旺季,各种东西特别是住宿都贵很多。而且,那时候的机票也很贵。很多人觉得来三亚会花很多钱,实际上你不是在大旺季来的话,花不了太多钱。我和我女朋友在这边玩的几天住的酒店都还不错(干净最重要!),价格差不多都在 200元左右,有一天去西岛和天涯海角那边住的全海景房间也才要 200多,不过过年那会儿可能会达到 1000+。 - -现在是晚上 7 点多,刚从外面玩耍完回来。女朋友拿着我的手机拼着图片,我一个只能玩玩电脑。这篇文章很早就想写了,毕竟不费什么事,所以逞着晚上有空写一下。 - -如果有读者想看去三亚拍的美照包括我和我女朋友的合照,可以在评论区扣个 “想看”,我可以整篇推文分享一下。 - -## 正文​ > 下面的 10 个项目还是很推荐的!JS 的项目占比挺大,其他基本都是文档/学习类型的仓库。 diff --git a/docs/update-history.md b/docs/update-history.md new file mode 100644 index 00000000..030772a2 --- /dev/null +++ b/docs/update-history.md @@ -0,0 +1,12 @@ +**2020-03-07** + +[refer](https://github.com/Snailclimb/JavaGuide/commit/9e6424ffd63cd6664a03dd84ec1a9dc32cf3b2e5) + +将面试指南部分的文章移除,避免JavaGuide因为内容的膨胀而过于臃肿。移除部分目前是通过 PDF 文档的形式来分享,详见[JavaGuide面试突击版](./javaguide面试突击版.md)。 + +**2020-02-29** + +[refer](https://github.com/Snailclimb/JavaGuide/commit/e86ee6761dc5fe03f410711b28bc3a119e9ff731) + +对整体知识体系结构进行了调整和完善。 + diff --git a/index.html b/index.html index 83d0a7d7..f154126a 100644 --- a/index.html +++ b/index.html @@ -1,34 +1,47 @@ + JavaGuide - + +

+ + @@ -40,5 +53,10 @@ --> + + + + - + + \ No newline at end of file diff --git a/media/pictures/database/B+树.png b/media/pictures/database/B+树.png new file mode 100644 index 00000000..4261da2a Binary files /dev/null and b/media/pictures/database/B+树.png differ diff --git a/media/pictures/database/B+树二级索引(辅助索引).png b/media/pictures/database/B+树二级索引(辅助索引).png new file mode 100644 index 00000000..8ffc0776 Binary files /dev/null and b/media/pictures/database/B+树二级索引(辅助索引).png differ diff --git a/media/pictures/database/B+树索引.png b/media/pictures/database/B+树索引.png new file mode 100644 index 00000000..ddbf2fc1 Binary files /dev/null and b/media/pictures/database/B+树索引.png differ diff --git a/media/pictures/database/B+树覆盖索引.png b/media/pictures/database/B+树覆盖索引.png new file mode 100644 index 00000000..2bb74ea3 Binary files /dev/null and b/media/pictures/database/B+树覆盖索引.png differ diff --git a/media/pictures/database/Mysql索引文件截图.png b/media/pictures/database/Mysql索引文件截图.png new file mode 100644 index 00000000..1c77319e Binary files /dev/null and b/media/pictures/database/Mysql索引文件截图.png differ diff --git a/media/pictures/database/联合索引(多列索引).png b/media/pictures/database/联合索引(多列索引).png new file mode 100644 index 00000000..bdc31184 Binary files /dev/null and b/media/pictures/database/联合索引(多列索引).png differ diff --git a/media/pictures/database/联合索引之查询条件生效.png b/media/pictures/database/联合索引之查询条件生效.png new file mode 100644 index 00000000..e88d5436 Binary files /dev/null and b/media/pictures/database/联合索引之查询条件生效.png differ diff --git a/media/pictures/java/linux_io/BIO原理.png b/media/pictures/java/linux_io/BIO原理.png new file mode 100644 index 00000000..6e67cadd Binary files /dev/null and b/media/pictures/java/linux_io/BIO原理.png differ diff --git a/media/pictures/java/linux_io/IO多路复用原理.png b/media/pictures/java/linux_io/IO多路复用原理.png new file mode 100644 index 00000000..b0363574 Binary files /dev/null and b/media/pictures/java/linux_io/IO多路复用原理.png differ diff --git a/media/pictures/java/linux_io/NIO原理.png b/media/pictures/java/linux_io/NIO原理.png new file mode 100644 index 00000000..d3bc63b3 Binary files /dev/null and b/media/pictures/java/linux_io/NIO原理.png differ diff --git a/media/pictures/java/linux_io/信号驱动IO原理.png b/media/pictures/java/linux_io/信号驱动IO原理.png new file mode 100644 index 00000000..1df98b20 Binary files /dev/null and b/media/pictures/java/linux_io/信号驱动IO原理.png differ diff --git a/media/pictures/java/linux_io/异步IO原理.png b/media/pictures/java/linux_io/异步IO原理.png new file mode 100644 index 00000000..098c9b7e Binary files /dev/null and b/media/pictures/java/linux_io/异步IO原理.png differ diff --git a/media/pictures/java/linux_io/用户态与内核态.png b/media/pictures/java/linux_io/用户态与内核态.png new file mode 100644 index 00000000..aa0dafc2 Binary files /dev/null and b/media/pictures/java/linux_io/用户态与内核态.png differ diff --git a/media/pictures/java/my-lru-cache/ConcurrentLinkedQueue-Diagram.png b/media/pictures/java/my-lru-cache/ConcurrentLinkedQueue-Diagram.png new file mode 100644 index 00000000..26b68b1f Binary files /dev/null and b/media/pictures/java/my-lru-cache/ConcurrentLinkedQueue-Diagram.png differ diff --git a/media/pictures/java/my-lru-cache/MyLRUCachePut.png b/media/pictures/java/my-lru-cache/MyLRUCachePut.png new file mode 100644 index 00000000..2ffc0b7b Binary files /dev/null and b/media/pictures/java/my-lru-cache/MyLRUCachePut.png differ diff --git a/media/pictures/java/my-lru-cache/ScheduledThreadPoolExecutor-diagram.png b/media/pictures/java/my-lru-cache/ScheduledThreadPoolExecutor-diagram.png new file mode 100644 index 00000000..00a11dc7 Binary files /dev/null and b/media/pictures/java/my-lru-cache/ScheduledThreadPoolExecutor-diagram.png differ diff --git a/media/pictures/jvm/java_jvm_compose_garbage_collector.png b/media/pictures/jvm/java_jvm_compose_garbage_collector.png new file mode 100644 index 00000000..5f8729c5 Binary files /dev/null and b/media/pictures/jvm/java_jvm_compose_garbage_collector.png differ diff --git a/media/pictures/jvm/java_jvm_garbage_collector_parameters.png b/media/pictures/jvm/java_jvm_garbage_collector_parameters.png new file mode 100644 index 00000000..6116bfb6 Binary files /dev/null and b/media/pictures/jvm/java_jvm_garbage_collector_parameters.png differ diff --git a/media/pictures/jvm/java_jvm_heap_parameters.png b/media/pictures/jvm/java_jvm_heap_parameters.png new file mode 100644 index 00000000..06d906c8 Binary files /dev/null and b/media/pictures/jvm/java_jvm_heap_parameters.png differ diff --git a/media/pictures/jvm/java_jvm_suggest_parameters.png b/media/pictures/jvm/java_jvm_suggest_parameters.png new file mode 100644 index 00000000..55ca4f42 Binary files /dev/null and b/media/pictures/jvm/java_jvm_suggest_parameters.png differ diff --git a/media/pictures/kafka/消费者设计概要2.png b/media/pictures/kafka/消费者设计概要2.png index 0c7383f9..5ea1f290 100644 Binary files a/media/pictures/kafka/消费者设计概要2.png and b/media/pictures/kafka/消费者设计概要2.png differ diff --git a/media/pictures/kafka/消费者设计概要3.png b/media/pictures/kafka/消费者设计概要3.png index 741302da..0c7383f9 100644 Binary files a/media/pictures/kafka/消费者设计概要3.png and b/media/pictures/kafka/消费者设计概要3.png differ diff --git a/media/pictures/linux/Debian10下IDEA的Markdown预渲染解决后.png b/media/pictures/linux/Debian10下IDEA的Markdown预渲染解决后.png new file mode 100644 index 00000000..303f3ee9 Binary files /dev/null and b/media/pictures/linux/Debian10下IDEA的Markdown预渲染解决后.png differ diff --git a/media/pictures/linux/Debian10下IDEA的Markdown预渲染问题.png b/media/pictures/linux/Debian10下IDEA的Markdown预渲染问题.png new file mode 100644 index 00000000..aaa25e62 Binary files /dev/null and b/media/pictures/linux/Debian10下IDEA的Markdown预渲染问题.png differ diff --git a/media/pictures/linux/Fcitx候选框定位问题.png b/media/pictures/linux/Fcitx候选框定位问题.png new file mode 100644 index 00000000..22219303 Binary files /dev/null and b/media/pictures/linux/Fcitx候选框定位问题.png differ diff --git a/media/pictures/linux/Linux目录.png b/media/pictures/linux/Linux目录.png new file mode 100644 index 00000000..a7c05810 Binary files /dev/null and b/media/pictures/linux/Linux目录.png differ diff --git a/media/pictures/linux/我的电脑配置.png b/media/pictures/linux/我的电脑配置.png new file mode 100644 index 00000000..a07c78de Binary files /dev/null and b/media/pictures/linux/我的电脑配置.png differ diff --git a/media/pictures/linux/文件inode信息.png b/media/pictures/linux/文件inode信息.png new file mode 100644 index 00000000..b47551e8 Binary files /dev/null and b/media/pictures/linux/文件inode信息.png differ diff --git a/media/pictures/linux/用户态与内核态.png b/media/pictures/linux/用户态与内核态.png new file mode 100644 index 00000000..aa0dafc2 Binary files /dev/null and b/media/pictures/linux/用户态与内核态.png differ diff --git a/media/pictures/linux/目录文件.png b/media/pictures/linux/目录文件.png new file mode 100644 index 00000000..52e93978 Binary files /dev/null and b/media/pictures/linux/目录文件.png differ diff --git a/media/pictures/linux/软链接和硬链接.png b/media/pictures/linux/软链接和硬链接.png new file mode 100644 index 00000000..214d436f Binary files /dev/null and b/media/pictures/linux/软链接和硬链接.png differ diff --git a/media/pictures/logo.png b/media/pictures/logo.png new file mode 100644 index 00000000..239f9a89 Binary files /dev/null and b/media/pictures/logo.png differ diff --git a/media/pictures/rostyslav-savchyn-5joK905gcGc-unsplash.jpg b/media/pictures/rostyslav-savchyn-5joK905gcGc-unsplash.jpg deleted file mode 100755 index e945cde5..00000000 Binary files a/media/pictures/rostyslav-savchyn-5joK905gcGc-unsplash.jpg and /dev/null differ diff --git a/media/sponsor/WechatIMG143.jpeg b/media/sponsor/WechatIMG143.jpeg deleted file mode 100644 index 5e1b53b4..00000000 Binary files a/media/sponsor/WechatIMG143.jpeg and /dev/null differ diff --git a/media/sponsor/kaikeba.png b/media/sponsor/kaikeba.png new file mode 100644 index 00000000..9a5ce487 Binary files /dev/null and b/media/sponsor/kaikeba.png differ diff --git a/media/sponsor/lagou-new.jpeg b/media/sponsor/lagou-new.jpeg new file mode 100644 index 00000000..a2acd797 Binary files /dev/null and b/media/sponsor/lagou-new.jpeg differ diff --git a/media/sponsor/wangyi.png b/media/sponsor/wangyi.png new file mode 100644 index 00000000..eca0bd90 Binary files /dev/null and b/media/sponsor/wangyi.png differ diff --git a/media/sponsor/xiangxue.png b/media/sponsor/xiangxue.png new file mode 100644 index 00000000..16591c0a Binary files /dev/null and b/media/sponsor/xiangxue.png differ diff --git a/sw.js b/sw.js new file mode 100644 index 00000000..cf6295c4 --- /dev/null +++ b/sw.js @@ -0,0 +1,83 @@ +/* =========================================================== + * docsify sw.js + * =========================================================== + * Copyright 2016 @huxpro + * Licensed under Apache 2.0 + * Register service worker. + * ========================================================== */ + +const RUNTIME = 'docsify' +const HOSTNAME_WHITELIST = [ + self.location.hostname, + 'fonts.gstatic.com', + 'fonts.googleapis.com', + 'unpkg.com' +] + +// The Util Function to hack URLs of intercepted requests +const getFixedUrl = (req) => { + var now = Date.now() + var url = new URL(req.url) + + // 1. fixed http URL + // Just keep syncing with location.protocol + // fetch(httpURL) belongs to active mixed content. + // And fetch(httpRequest) is not supported yet. + url.protocol = self.location.protocol + + // 2. add query for caching-busting. + // Github Pages served with Cache-Control: max-age=600 + // max-age on mutable content is error-prone, with SW life of bugs can even extend. + // Until cache mode of Fetch API landed, we have to workaround cache-busting with query string. + // Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190 + if (url.hostname === self.location.hostname) { + url.search += (url.search ? '&' : '?') + 'cache-bust=' + now + } + return url.href +} + +/** + * @Lifecycle Activate + * New one activated when old isnt being used. + * + * waitUntil(): activating ====> activated + */ +self.addEventListener('activate', event => { + event.waitUntil(self.clients.claim()) +}) + +/** + * @Functional Fetch + * All network requests are being intercepted here. + * + * void respondWith(Promise r) + */ +self.addEventListener('fetch', event => { + // Skip some of cross-origin requests, like those for Google Analytics. + if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) { + // Stale-while-revalidate + // similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale + // Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1 + const cached = caches.match(event.request) + const fixedUrl = getFixedUrl(event.request) + const fetched = fetch(fixedUrl, { cache: 'no-store' }) + const fetchedCopy = fetched.then(resp => resp.clone()) + + // Call respondWith() with whatever we get first. + // If the fetch fails (e.g disconnected), wait for the cache. + // If there’s nothing in cache, wait for the fetch. + // If neither yields a response, return offline pages. + event.respondWith( + Promise.race([fetched.catch(_ => cached), cached]) + .then(resp => resp || fetched) + .catch(_ => { /* eat any errors */ }) + ) + + // Update the cache with the version we fetched (only for ok status) + event.waitUntil( + Promise.all([fetchedCopy, caches.open(RUNTIME)]) + .then(([response, cache]) => response.ok && cache.put(event.request, response)) + .catch(_ => { /* eat any errors */ }) + ) + } +}) \ No newline at end of file diff --git a/其他/开源项目源码阅读指南.md b/其他/开源项目源码阅读指南.md new file mode 100644 index 00000000..b4ec3258 --- /dev/null +++ b/其他/开源项目源码阅读指南.md @@ -0,0 +1,90 @@ +# 前言 +作为一个程序员,阅读大牛们优秀的开源项目源码是一个提升个人编程能力、扩展思维的重要途径。在实际工作中,相信并不是所有人接手的项目代码都很优雅和优秀,而且很大可能因为历史遗留、赶进度等原因,导致代码冗余、模块耦合严重、扩展性差和兼容性差等等, 这就有可能导致在工作中无法使个人能力得到很好的提高,并且会导致个人的思维和眼界有所局限。 + +其实一种想法的实现往往是多种的,而欠缺能力的人往往采用简单粗暴的方式,另一方面,而有能力的人总能使用优雅的方式,尽可能考虑各种可能的需求变动、适应各种使用途径和场景、想到未来扩展的方式来实现。 + +优秀的开源项目正是这种有能力的人用优雅方式实现想法的结晶!所以,阅读优秀的开源项目对个人编程的思考方式、知识扩展都是非常非常有帮助的。 + +作为经常阅读别人的优秀开源项目的人,想给大家分享下我的阅读经验,希望能对大家有所帮助。 + +# 步骤 +## 寻找驱动力 +当你开始阅读开源项目首先你得有目的性,工作需要?个人学习?这都是很好的驱动力。 + +没驱动力是很难坚持的,特别是开源项目涉及到很多你不怎么了解的知识点,很容易会觉得枯燥、晦涩。毕竟阅读别人的代码并不是一件快乐的事情,我们很难去完成理解代码作者当时的思路和想法,这个过程是很痛苦的。但如果你有目标、有意图地去阅读,就能在一定程度上减少这痛苦。每天给自己打下鸡血,未来的你等下会为这份坚持感到骄傲! +## 浏览官方文档,对开源项目的功能、架构有大概的印象 +好了,有了驱动力,先别急,看看官方文档,看看这个项目能完成什么事情和不能完成什么事情,还有官方对这个项目的定位。例如 Replugin 写得满满的十几页的 wiki ,官方定位: + +RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案。 +完整的:让插件运行起来“像单品那样”,支持大部分特性 +稳定的:如此灵活完整的情况下,其框架崩溃率仅为业内很低的“万分之一” +适合全面使用的:其目的是让应用内的“所有功能皆为插件” +占坑类:以稳定为前提的Manifest占坑思路 +插件化方案:基于Android原生API和语言来开发,充分利用原生特性 + +可以看到,wiki很详细地介绍了Replugin定位和优点,这时相信对技术有追求的人都会冒出一个疑问:“他们如何做到的!?” 这又大大激起了你的好奇心,让你更有动力坚持下去。 + +很多人急功近利,马上就开始源码阅读之旅了,包括我。但经过多个项目源码的阅读的我,会告诉你,别急!我们还需要知道它怎么用。 +## 在工作中或实践中使用开源项目 +本节讲的不是怎么使用,这官网文档肯定会有说明的,而是讲为什么使用。一个东西你连使用都不会,就想去了解他的原理?就像你开车都不会,就去了解刹车怎么把车停下来。而事实是你开车都不会,你可能都分不清刹车、离合和油门是哪个跟哪个,这就去了解原理,往往会迷失方向。 + +所以,阅读前先使用吧。但我不建议在实际工作项目中立刻使用,因为你原理都不清楚,有问题不好排查,会影响线上用户,这就很糟糕了。我建议的是看官方demo,然后在自己的个人练手项目中使用。当对项目的使用有一定地理解了,ok,可以走下一步了。 +## 网上搜索针对该开源项目进行分析的优秀文章 +一个优秀的开源项目总是有很多人阅读并分析,然后整理写出总结文章。既然前人都帮我们分析好了,我们为什么不站在前人的肩膀上继续往上爬,这样就省了从脚到肩膀的力气了。但要注意我的字眼,是“优秀”的文章!现在很多人都写博客,很多都是潦潦而谈,只能说是笔记,而非总结。 + +像 Replugin 这样一个“巨型”的开源项目,老实说,对我这种菜鸡来说,很多知识点都只是略知一二,例如多进程通信、gradle编译脚本等,在实际工作中很少接触的难免会觉得难懂。另外,官方文档往往不会对实现细节讲得很细,这时,看前人的分析就很有必要了。这样可以让你对项目的实现有一定地了解,当你自己看时,你能很快懂得作者这样做的意图。 + +当然,如果你不想看别人的分析总结也未必不可,可能在自己阅读过程中多点磕磕碰碰,但你总不能跳过下一步! +## 对开源项目提出自己的疑问 +前面做了这么多准备,你总会产生疑问吧。什么?没有!好吧,这开源项目对你来说太简单,已经不值得你一读了。带着疑问去阅读是我认为最高效的阅读方式,当你有了目的,而不至于在阅读过程中迷失了方向,并且在阅读过程中针对性的看。对一个开源项目的疑问一般可以从以下方向提出: + +这块功能为什么这么做?有什么好处? +有没有另外一种实现方式? +我缺少哪些知识会阻碍我看源码(需要去补)? +例如我在阅读 Replugin 之前提出了几个疑问: + +如何做到一处hook?借助gradle? +查找坑位策略?如何替换真正的启动组件? +为什么需要声明这么多坑位? +为什么不用注入Service? +好了,当你有了好奇心、驱动力、目的,你已经准备好了。但开始阅读前还有一件事情先搞定:编译源码。 + +## 把开源项目下载到本地,并导入IDE,方便调试、测试 +工欲善其事,必先利其器。没有一个好的调试环境怎么能顺心地看源码。但幸亏GitHub让我们能简单地把源码download或clone下来,很多情况都是直接用IDE打开项目就搞定了。但也有像 Replugin 一样的,分为多个项目,每个项目都是单独编译的,这样我们就无法只打开一个窗口来调试,很不爽。这时就需要点导入技巧来搞定了。 +## 带着疑问阅读源码 +战争打响,在充满迷雾的大海中,我方对敌人的方位还不甚了解,但不怕,我们的指北针 —— 疑问 —— 会带领我们直达敌方腹地,我们终会揭开它的露出庐山真面目。 + +开源项目往往是庞大而复杂的,我们在阅读过程中真的非常容易会纠结于细节,而导致阅读混乱,迷失了方向,这对阅读的动力打击很沉重的,往往会使人放弃。 + +而有了疑问就不同了,你知道自己为何要看,你会思考,会有自己的目的,不拘泥于细节实现,能准确地找到源码的核心实现。 + +对于纠结细节是很多人在阅读源码犯的错误,有些细节我们根本不需要去搞清楚它怎么做的,知道它做什么就可以了。一些具体的实现可以放到当你使用过程中遇到问题,或者对该具体实现产生另一个疑问时才去深究,也就是说,还是带着疑问阅读代码。因为一个开源项目往往是多个优秀的人花了很多时间写出来的结晶,你想在短时间内把它完成消化,是不科学的。我们专注于最感兴趣的、最有参考价值和最核心的部分就可以了。 +## 阅读源码过程中多添加注释、多做笔记 +我得承认,我的记忆力不好,而我也不信我的记忆。好记性不如烂笔头,记忆终将遗忘,但所做的笔记除非被销毁,否则永远都会在那里,等着你去翻阅回顾。 + +我们把整个项目都下载下来了,首先当然是在阅读源码过程中添加下自己的注释了,写下自己的理解、疑惑,或者标记下值得借鉴参考的实现等等。另外,我们还需要做些简单的总结笔记。可以纸质或者网上很多的笔记类应用。对于我这种无法直视自己的手写字的,更倾向于用笔记类应用,这也是我推荐大家用的,多端同步,不能再省心。 + +##做阅读总结,吸收和再创造 +当你对开源项目阅读到一定程度了,对该项目有了深刻的理解,并有了自己的见解,你是不是有话要说?别憋着了,讲出来吧!跟大家分享!写篇博客总结下阅读经验、心得和成长等等,既能加深自己的印象,又能帮助到他人,何乐而不为呢?! + +阅读开源项目我们最终的目的是把其涉及到的知识点和设计实现思路吸收,并且转化为自己的功力。这个转化不是说你阅读完了就转化成功了,往往阅读是不够的,你还需要实践。 + +例如喜欢打球的我深知看NBA球星在球场上各种变向戏耍对手,对我的过人能力几乎没任何帮助,只是让我知道:“原来还能这么做呀!” 我还得自己去球场一招一式的练习,反复练习,或者我根据我的身体条件,做些简单的变种,直到这招转化为我的肌肉记忆,我才能在比赛中自然而然地使用出来。 + +所以我提倡再创造。所谓再创造不是让你重复造轮子,而是能根据自己的工作需求,把开源项目应用到工作中。这里的应用不一定是直接引用开源项目来使用,我是不建议这么做的,因为开源项目往往考虑全面,考虑到非常多的情况的,而你项目根本不存在这样的情况,这就是浪费。所以我建议的是:根据自己工作的需求,把开源项目的核心实现抽取出来,转化为能满足自己需求的库来使用。 + +而这个抽取的过程就是吸收的过程。在这个过程你遇到的问题并解决,会使你对开源项目有更深刻的理解。这个过程如果你对开源项目的某个实现不太认同,可以尝试改为自己的实现,这就是吸收。 + +# 总结 +非常感谢看到这里的童鞋,毕竟这些经验谈没什么干货,能耐心读到这里真的非常感谢!我们来总结一波阅读源码的步骤: + +寻找驱动力 +浏览官方文档,对开源项目的功能、架构有大概的印象 +在工作中或实践中使用开源项目 +网上搜索针对该开源项目进行分析的优秀文章 +对开源项目提出自己的疑问 +把开源项目下载到本地,并导入IDE,方便调试、测试 +带着疑问阅读源码 +阅读源码过程中多添加注释、多做笔记 +做阅读总结,吸收和再创造 +以上步骤有些可以根据实际情况跳过,程序员都是聪明人,总也会随机应变~ \ No newline at end of file