diff --git a/README.md b/README.md
index 5f0ed162a09..8a89d065d1f 100644
--- a/README.md
+++ b/README.md
@@ -171,7 +171,6 @@
1. [操作系统常见问题总结!](docs/operating-system/basis.md)
2. [后端程序员必备的 Linux 基础知识](docs/operating-system/linux.md)
3. [Shell 编程入门](docs/operating-system/Shell.md)
-4. [Linux 性能分析工具合集](docs/operating-system/Linux性能分析工具合集.md)
## 数据结构与算法
@@ -229,13 +228,13 @@
**知识点/面试题:**
-1. **[Spring 常见问题总结](docs/system-design/framework/spring/SpringInterviewQuestions.md)**
+1. **[Spring 常见问题总结](docs/system-design/framework/spring/Spring常见问题总结.md)**
2. **[SpringBoot 指南/常见面试题总结](https://github.com/Snailclimb/springboot-guide)**
**重要知识点详解:**
1. **[Spring/Spring 常用注解总结!安排!](./docs/system-design/framework/spring/SpringBoot+Spring常用注解总结.md)**
-2. **[Spring 事务总结](docs/system-design/framework/spring/spring-transaction.md)**
+2. **[Spring 事务总结](docs/system-design/framework/spring/Spring事务总结.md)**
3. [Spring 中都用到了那些设计模式?](docs/system-design/framework/spring/Spring-Design-Patterns.md)
#### MyBatis
@@ -279,8 +278,8 @@ RPC 让调用远程服务调用像调用本地方法那样简单。
网关主要用于请求转发、安全认证、协议转换、容灾。
-1. [为什么要网关?你知道有哪些常见的网关系统?](docs/system-design/micro-service/api-gateway-intro.md)
-2. [如何设计一个亿级网关(API Gateway)?](docs/system-design/micro-service/API网关.md)
+1. [为什么要网关?你知道有哪些常见的网关系统?](docs/system-design/distributed-system/api-gateway/为什么要网站有哪些常见的网站系统.md)
+2. [如何设计一个亿级网关(API Gateway)?](docs/system-design/distributed-system/api-gateway/如何设计一个亿级网关.md)
#### 分布式 id
@@ -290,9 +289,9 @@ RPC 让调用远程服务调用像调用本地方法那样简单。
> 前两篇文章可能有内容重合部分,推荐都看一遍。
-1. [【入门】ZooKeeper 相关概念总结](docs/system-design/framework/zookeeper/zookeeper-intro.md)
-2. [【进阶】ZooKeeper 相关概念总结](docs/system-design/framework/zookeeper/zookeeper-plus.md)
-3. [【实战】ZooKeeper 实战](docs/system-design/framework/zookeeper/zookeeper-in-action.md)
+1. [【入门】ZooKeeper 相关概念总结](docs/system-design/distributed-system/zookeeper/zookeeper-intro.md)
+2. [【进阶】ZooKeeper 相关概念总结](docs/system-design/distributed-system/zookeeper/zookeeper-plus.md)
+3. [【实战】ZooKeeper 实战](docs/system-design/distributed-system/zookeeper/zookeeper-in-action.md)
### 微服务
@@ -335,7 +334,7 @@ RPC 让调用远程服务调用像调用本地方法那样简单。
高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的 。
-相关阅读: **《[如何设计一个高可用系统?要考虑哪些地方?](docs/system-design/high-availability/如何设计一个高可用系统?要考虑哪些地方?.md)》** 。
+相关阅读: **《[如何设计一个高可用系统?要考虑哪些地方?](docs/system-design/high-availability/如何设计一个高可用系统要考虑哪些地方.md)》** 。
#### CAP 理论
@@ -347,24 +346,40 @@ CAP 也就是 Consistency(一致性)、Availability(可用性)、Partiti
**BASE** 是 **Basically Available(基本可用)** 、**Soft-state(软状态)** 和 **Eventually Consistent(最终一致性)** 三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。
+关于 CAP 的详细解读请看:[《BASE理论解读》](docs/system-design/high-availability/BASE理论.md)。
+
#### 限流
+限流是从用户访问压力的角度来考虑如何应对系统故障。
+
限流为了对服务端的接口接受请求的频率进行限制,防止服务挂掉。比如某一接口的请求限制为 100 个每秒, 对超过限制的请求放弃处理或者放到队列中等待处理。限流可以有效应对突发请求过多。相关阅读:[限流算法有哪些?](docs/system-design/high-availability/limit-request.md)
#### 降级
-限流是从用户访问压力的角度来考虑如何应对故障,降级是从系统功能优先级的角度考虑如何应对故障
+降级是从系统功能优先级的角度考虑如何应对系统故障。
-服务降级指的是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。
+服务降级指的是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。
#### 熔断
-熔断和降级是两个比较容易混淆的概念,因为单纯从名字上看好像都有禁止某个功能的意思,但其实内在含义是不同的,原因在于降级的目的是应对系统自身的故障,而熔断的目的是应对依赖的外部系统故障的情况。
+熔断和降级是两个比较容易混淆的概念,两者的含义并不相同。
+
+降级的目的在于应对系统自身的故障,而熔断的目的在于应对当前系统依赖的外部系统或者第三方系统的故障。
#### 排队
另类的一种限流,类比于现实世界的排队。玩过英雄联盟的小伙伴应该有体会,每次一有活动,就要经历一波排队才能进入游戏。
+#### 集群
+
+相同的服务部署多份,避免单点故障。
+
+#### 超时和重试机制
+
+**一旦用户的请求超过某个时间得不到响应就结束此次请求并抛出异常。** 如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法在处理请求。
+
+另外,重试的次数一般设为 3 次,再多次的重试没有好处,反而会加重服务器压力(部分场景使用失败重试机制会不太适合)。
+
### 大型网站架构
- [8 张图读懂大型网站技术架构](docs/system-design/website-architecture/8%20张图读懂大型网站技术架构.md)
diff --git "a/docs/dataStructures-algorithms/images/\346\210\221\347\232\204\347\254\254\344\270\200\346\234\254\347\256\227\346\263\225\344\271\246.jpeg" "b/docs/dataStructures-algorithms/images/\346\210\221\347\232\204\347\254\254\344\270\200\346\234\254\347\256\227\346\263\225\344\271\246.jpeg"
new file mode 100644
index 00000000000..221f53bc2ec
Binary files /dev/null and "b/docs/dataStructures-algorithms/images/\346\210\221\347\232\204\347\254\254\344\270\200\346\234\254\347\256\227\346\263\225\344\271\246.jpeg" differ
diff --git "a/docs/dataStructures-algorithms/images/\346\210\221\347\232\204\347\254\254\344\270\200\346\234\254\347\256\227\346\263\225\344\271\246.png" "b/docs/dataStructures-algorithms/images/\346\210\221\347\232\204\347\254\254\344\270\200\346\234\254\347\256\227\346\263\225\344\271\246.png"
deleted file mode 100644
index 17c0418e2c3..00000000000
Binary files "a/docs/dataStructures-algorithms/images/\346\210\221\347\232\204\347\254\254\344\270\200\346\234\254\347\256\227\346\263\225\344\271\246.png" and /dev/null differ
diff --git "a/docs/dataStructures-algorithms/images/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227.jpeg" "b/docs/dataStructures-algorithms/images/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227.jpeg"
new file mode 100644
index 00000000000..1f3e8eb8469
Binary files /dev/null and "b/docs/dataStructures-algorithms/images/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227.jpeg" differ
diff --git "a/docs/dataStructures-algorithms/images/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227.png" "b/docs/dataStructures-algorithms/images/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227.png"
deleted file mode 100644
index fa50afece6d..00000000000
Binary files "a/docs/dataStructures-algorithms/images/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227.png" and /dev/null differ
diff --git "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225-4.jpeg" "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225-4.jpeg"
new file mode 100644
index 00000000000..57af669b753
Binary files /dev/null and "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225-4.jpeg" differ
diff --git "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225-4.png" "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225-4.png"
deleted file mode 100644
index bbfd3b5c977..00000000000
Binary files "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225-4.png" and /dev/null differ
diff --git "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\233\276\350\247\243.jpeg" "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\233\276\350\247\243.jpeg"
new file mode 100644
index 00000000000..f59c5ccf0c3
Binary files /dev/null and "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\233\276\350\247\243.jpeg" differ
diff --git "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\233\276\350\247\243.png" "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\233\276\350\247\243.png"
deleted file mode 100644
index ce1edb0acc8..00000000000
Binary files "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\233\276\350\247\243.png" and /dev/null differ
diff --git "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\257\274\350\256\272.jpeg" "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\257\274\350\256\272.jpeg"
new file mode 100644
index 00000000000..f7b282be6aa
Binary files /dev/null and "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\257\274\350\256\272.jpeg" differ
diff --git "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\257\274\350\256\272.png" "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\257\274\350\256\272.png"
deleted file mode 100644
index fc1f52b5b4a..00000000000
Binary files "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\257\274\350\256\272.png" and /dev/null differ
diff --git "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\350\256\276\350\256\241\346\211\213\345\206\214.png" "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\350\256\276\350\256\241\346\211\213\345\206\214.png"
index 1fed6659ad8..ecf283346dc 100644
Binary files "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\350\256\276\350\256\241\346\211\213\345\206\214.png" and "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\350\256\276\350\256\241\346\211\213\345\206\214.png" differ
diff --git "a/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\344\271\213\347\276\216.jpeg" "b/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\344\271\213\347\276\216.jpeg"
new file mode 100644
index 00000000000..fad8cc7ca7a
Binary files /dev/null and "b/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\344\271\213\347\276\216.jpeg" differ
diff --git "a/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\344\271\213\347\276\216.png" "b/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\344\271\213\347\276\216.png"
deleted file mode 100644
index d137b6188ea..00000000000
Binary files "a/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\344\271\213\347\276\216.png" and /dev/null differ
diff --git "a/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\347\217\240\347\216\221.jpeg" "b/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\347\217\240\347\216\221.jpeg"
new file mode 100644
index 00000000000..79dddcc90c6
Binary files /dev/null and "b/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\347\217\240\347\216\221.jpeg" differ
diff --git "a/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\347\217\240\347\216\221.png" "b/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\347\217\240\347\216\221.png"
deleted file mode 100644
index 7695d88eb7d..00000000000
Binary files "a/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\347\217\240\347\216\221.png" and /dev/null differ
diff --git "a/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" "b/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md"
index 5af08c9d30d..699ddf51404 100644
--- "a/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md"
+++ "b/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md"
@@ -12,13 +12,13 @@
### 入门
-
+
**[我的第一本算法书](https://book.douban.com/subject/30357170/) (豆瓣评分 7.1,0.2K+人评价)**
一本不那么“专业”的算法书籍。和下面两本推荐的算法书籍都是比较通俗易懂,“不那么深入”的算法书籍。我个人非常推荐,配图和讲解都非常不错!
-
+
**[《算法图解》](https://book.douban.com/subject/26979890/)(豆瓣评分 8.4,1.5K+人评价)**
@@ -32,7 +32,7 @@
### 经典
-
+
**[《算法 第四版》](https://book.douban.com/subject/10432347/)(豆瓣评分 9.3,0.4K+人评价)**
@@ -42,7 +42,7 @@
> **下面这些书籍都是经典中的经典,但是阅读起来难度也比较大,不做太多阐述,神书就完事了!推荐先看 《算法》,然后再选下面的书籍进行进一步阅读。不需要都看,找一本好好看或者找某本书的某一个章节知识点好好看。**
-
+
**[编程珠玑](https://book.douban.com/subject/3227098/)(豆瓣评分 9.1,2K+人评价)**
@@ -50,15 +50,13 @@
很多人都说这本书不是教你具体的算法,而是教你一种编程的思考方式。这种思考方式不仅仅在编程领域适用,在其他同样适用。
-
-
-
+
**[《算法设计手册》](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+人评价)**
@@ -80,15 +78,13 @@
-
+
**[程序员代码面试指南:IT名企算法与数据结构题目最优解(第2版)](https://book.douban.com/subject/30422021/) (豆瓣评分 8.7,0.2K+人评价)**
题目相比于《剑指 offer》 来说要难很多,题目涵盖面相比于《剑指 offer》也更加全面。全书一共有将近300道真实出现过的经典代码面试题。
-
-
-
+
diff --git "a/docs/java/jvm/JVM\345\236\203\345\234\276\345\233\236\346\224\266.md" "b/docs/java/jvm/JVM\345\236\203\345\234\276\345\233\236\346\224\266.md"
index 5f001636c4a..f6ede78512e 100644
--- "a/docs/java/jvm/JVM\345\236\203\345\234\276\345\233\236\346\224\266.md"
+++ "b/docs/java/jvm/JVM\345\236\203\345\234\276\345\233\236\346\224\266.md"
@@ -332,7 +332,7 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
-
+
### 3.3 标记-整理算法
diff --git "a/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\345\237\272\347\241\200\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" "b/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\345\237\272\347\241\200\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md"
index e55612460b6..59c1484834a 100644
--- "a/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\345\237\272\347\241\200\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md"
+++ "b/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\345\237\272\347\241\200\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md"
@@ -1,1119 +1,305 @@
-点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
-
-
-
-
-
-
-- [Java 并发进阶常见面试题总结](#java-并发进阶常见面试题总结)
- - [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.1.说一说自己对于 synchronized 关键字的了解
-
-**`synchronized` 关键字解决的是多个线程之间访问资源的同步性,`synchronized`关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。**
-
-另外,在 Java 早期版本中,`synchronized` 属于 **重量级锁**,效率低下。
-
-**为什么呢?**
-
-因为监视器锁(monitor)是依赖于底层的操作系统的 `Mutex Lock` 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
-
-庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
-
-所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。
-
-### 1.2. 说说自己是怎么使用 synchronized 关键字
-
-**synchronized 关键字最主要的三种使用方式:**
-
-**1.修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得 **当前对象实例的锁**
+
-```java
-synchronized void method() {
- //业务代码
-}
-```
-
-**2.修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 **当前 class 的锁**。因为静态成员不属于任何一个实例对象,是类成员( _static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份_)。所以,如果一个线程 A 调用一个实例对象的非静态 `synchronized` 方法,而线程 B 需要调用这个实例对象所属类的静态 `synchronized` 方法,是允许的,不会发生互斥现象,**因为访问静态 `synchronized` 方法占用的锁是当前类的锁,而访问非静态 `synchronized` 方法占用的锁是当前实例对象锁**。
-
-```java
-synchronized void staic method() {
- //业务代码
-}
-```
-
-**3.修饰代码块** :指定加锁对象,对给定对象/类加锁。`synchronized(this|object)` 表示进入同步代码库前要获得**给定对象的锁**。`synchronized(类.class)` 表示进入同步代码前要获得 **当前 class 的锁**
-
-```java
-synchronized(this) {
- //业务代码
-}
-```
-
-**总结:**
-
-- `synchronized` 关键字加到 `static` 静态方法和 `synchronized(class)` 代码块上都是是给 Class 类上锁。
-- `synchronized` 关键字加到实例方法上是给对象实例上锁。
-- 尽量不要使用 `synchronized(String a)` 因为 JVM 中,字符串常量池具有缓存功能!
-
-下面我以一个常见的面试题为例讲解一下 `synchronized` 关键字的具体使用。
-
-面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”
-
-**双重校验锁实现对象单例(线程安全)**
-
-```java
-public class Singleton {
-
- private volatile static Singleton uniqueInstance;
-
- private Singleton() {
- }
-
- public static Singleton getUniqueInstance() {
- //先判断对象是否已经实例过,没有实例化过才进入加锁代码
- if (uniqueInstance == null) {
- //类对象加锁
- synchronized (Singleton.class) {
- if (uniqueInstance == null) {
- uniqueInstance = new Singleton();
- }
- }
- }
- return uniqueInstance;
- }
-}
-```
+- [Java 并发基础常见面试题总结](#java-并发基础常见面试题总结)
+ - [1. 什么是线程和进程?](#1-什么是线程和进程)
+ - [1.1. 何为进程?](#11-何为进程)
+ - [1.2. 何为线程?](#12-何为线程)
+ - [2. 请简要描述线程与进程的关系,区别及优缺点?](#2-请简要描述线程与进程的关系区别及优缺点)
+ - [2.1. 图解进程和线程的关系](#21-图解进程和线程的关系)
+ - [2.2. 程序计数器为什么是私有的?](#22-程序计数器为什么是私有的)
+ - [2.3. 虚拟机栈和本地方法栈为什么是私有的?](#23-虚拟机栈和本地方法栈为什么是私有的)
+ - [2.4. 一句话简单了解堆和方法区](#24-一句话简单了解堆和方法区)
+ - [3. 说说并发与并行的区别?](#3-说说并发与并行的区别)
+ - [4. 为什么要使用多线程呢?](#4-为什么要使用多线程呢)
+ - [5. 使用多线程可能带来什么问题?](#5-使用多线程可能带来什么问题)
+ - [6. 说说线程的生命周期和状态?](#6-说说线程的生命周期和状态)
+ - [7. 什么是上下文切换?](#7-什么是上下文切换)
+ - [8. 什么是线程死锁?如何避免死锁?](#8-什么是线程死锁如何避免死锁)
+ - [8.1. 认识线程死锁](#81-认识线程死锁)
+ - [8.2. 如何避免线程死锁?](#82-如何避免线程死锁)
+ - [9. 说说 sleep() 方法和 wait() 方法区别和共同点?](#9-说说-sleep-方法和-wait-方法区别和共同点)
+ - [10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#10-为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法)
+ - [公众号](#公众号)
-另外,需要注意 `uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要。
+
-`uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要的, `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行:
+# Java 并发基础常见面试题总结
-1. 为 `uniqueInstance` 分配内存空间
-2. 初始化 `uniqueInstance`
-3. 将 `uniqueInstance` 指向分配的内存地址
+## 1. 什么是线程和进程?
-但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 `getUniqueInstance`() 后发现 `uniqueInstance` 不为空,因此返回 `uniqueInstance`,但此时 `uniqueInstance` 还未被初始化。
+### 1.1. 何为进程?
-使用 `volatile` 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
+进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
-### 1.3. 构造方法可以使用 synchronized 关键字修饰么?
+在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
-先说结论:**构造方法不能使用 synchronized 关键字修饰。**
+如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。
-构造方法本身就属于线程安全的,不存在同步的构造方法一说。
+
-### 1.3. 讲一下 synchronized 关键字的底层原理
+### 1.2. 何为线程?
-**synchronized 关键字底层原理属于 JVM 层面。**
+线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
-#### 1.3.1. synchronized 同步语句块的情况
+Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。
```java
-public class SynchronizedDemo {
- public void method() {
- synchronized (this) {
- System.out.println("synchronized 代码块");
+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());
}
}
}
-
```
-通过 JDK 自带的 `javap` 命令查看 `SynchronizedDemo` 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。
-
-
-
-从上面我们可以看出:
-
-**`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。**
-
-当执行 `monitorenter` 指令时,线程试图获取锁也就是获取 **对象监视器 `monitor`** 的持有权。
-
-> 在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由[ObjectMonitor](https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/runtime/objectMonitor.cpp)实现的。每个对象中都内置了一个 `ObjectMonitor`对象。
->
-> 另外,**`wait/notify`等方法也依赖于`monitor`对象,这就是为什么只有在同步的块或者方法中才能调用`wait/notify`等方法,否则会抛出`java.lang.IllegalMonitorStateException`的异常的原因。**
-
-在执行`monitorenter`时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
-
-在执行 `monitorexit` 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
-
-#### 1.3.2. `synchronized` 修饰方法的的情况
-
-```java
-public class SynchronizedDemo2 {
- public synchronized void method() {
- System.out.println("synchronized 方法");
- }
-}
+上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):
```
-
-
-
-`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
-
-#### 1.3.3.总结
-
-`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。
-
-`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。
-
-**不过两者的本质都是对对象监视器 monitor 的获取。**
-
-### 1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
-
-JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
-
-锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
-
-关于这几种优化的详细信息可以查看下面这几篇文章:
-
-- [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.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
-
-`synchronized` 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 `synchronized` 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。`ReentrantLock` 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
-
-#### 1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能
-
-相比`synchronized`,`ReentrantLock`增加了一些高级功能。主要来说主要有三点:
-
-- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
-- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。
-- **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()`和`notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法。
-
-> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 Condition 接口默认提供的。而`synchronized`关键字就相当于整个 Lock 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而`Condition`实例的`signalAll()`方法 只会唤醒注册在该`Condition`实例中的所有等待线程。
-
-**如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。性能已不是选择标准**
-
-## 2. volatile 关键字
-
-我们先要从 **CPU 缓存模型** 说起!
-
-### 2.1. CPU 缓存模型
-
-**为什么要弄一个 CPU 高速缓存呢?**
-
-类比我们开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。 **CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。**
-
-我们甚至可以把 **内存可以看作外存的高速缓存**,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。
-
-总结:**CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。**
-
-为了更好地理解,我画了一个简单的 CPU Cache 示意图如下(实际上,现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache):
-
-
-
-**CPU Cache 的工作方式:**
-
-先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 **内存缓存不一致性的问题** !比如我执行一个 i++操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。
-
-**CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议或者其他手段来解决。**
-
-### 2.2. 讲一下 JMM(Java 内存模型)
-
-在 JDK1.2 之前,Java 的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。
-
-
-
-要解决这个问题,就需要把变量声明为**`volatile`**,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
-
-所以,**`volatile` 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。**
-
-
-
-### 2.3. 并发编程的三个重要特性
-
-1. **原子性** : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。`synchronized` 可以保证代码片段的原子性。
-2. **可见性** :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。`volatile` 关键字可以保证共享变量的可见性。
-3. **有序性** :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。`volatile` 关键字可以禁止指令进行重排序优化。
-
-### 2.4. 说说 synchronized 关键字和 volatile 关键字的区别
-
-`synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在!
-
-- **volatile 关键字**是线程同步的**轻量级实现**,所以**volatile 性能肯定比 synchronized 关键字要好**。但是**volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块**。
-- **volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。**
-- **volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。**
-
-## 3. ThreadLocal
-
-### 3.1. ThreadLocal 简介
-
-通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK 中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**
-
-**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。**
-
-再举个简单的例子:
-
-比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。
-
-### 3.2. ThreadLocal 示例
-
-相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。
-
-```java
-import java.text.SimpleDateFormat;
-import java.util.Random;
-
-public class ThreadLocalExample implements Runnable{
-
- // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
- private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
-
- public static void main(String[] args) throws InterruptedException {
- ThreadLocalExample obj = new ThreadLocalExample();
- for(int i=0 ; i<10; i++){
- Thread t = new Thread(obj, ""+i);
- Thread.sleep(new Random().nextInt(1000));
- t.start();
- }
- }
-
- @Override
- public void run() {
- System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
- try {
- Thread.sleep(new Random().nextInt(1000));
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- //formatter pattern is changed here by thread, but it won't reflect to other threads
- formatter.set(new SimpleDateFormat());
-
- System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
- }
-
-}
-
+[5] Attach Listener //添加事件
+[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
+[3] Finalizer //调用对象 finalize 方法的线程
+[2] Reference Handler //清除 reference 线程
+[1] main //main 线程,程序入口
```
-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
-```
+从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。
-从输出中可以看出,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");
- }
- };
-```
+## 2. 请简要描述线程与进程的关系,区别及优缺点?
-### 3.3. ThreadLocal 原理
+**从 JVM 角度说进程和线程之间的关系**
-从 `Thread`类源代码入手。
+### 2.1. 图解进程和线程的关系
-```java
-public class Thread implements Runnable {
- ......
-//与此线程有关的ThreadLocal值。由ThreadLocal类维护
-ThreadLocal.ThreadLocalMap threadLocals = null;
-
-//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
-ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
- ......
-}
-```
+下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md "《可能是把 Java 内存区域讲的最清楚的一篇文章》")
-从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是 null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set()`方法。
+
+

+
-`ThreadLocal`类的`set()`方法
+从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。
-```java
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
-```
+**总结:** **线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。**
-通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。
+下面是该知识点的扩展内容!
-**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为 key ,Object 对象为 value 的键值对。**
+下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢?
-```java
-ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
- ......
-}
-```
+### 2.2. 程序计数器为什么是私有的?
-比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。
+程序计数器主要有下面两个作用:
-
+1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
+2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
-`ThreadLocalMap`是`ThreadLocal`的静态内部类。
+需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
-
+所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。
-### 3.4. ThreadLocal 内存泄露问题
+### 2.3. 虚拟机栈和本地方法栈为什么是私有的?
-`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法
+- **虚拟机栈:** 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
+- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
-```java
- static class Entry extends WeakReference>> {
- /** The value associated with this ThreadLocal. */
- Object value;
+所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。
- Entry(ThreadLocal> k, Object v) {
- super(k);
- value = v;
- }
- }
-```
+### 2.4. 一句话简单了解堆和方法区
-**弱引用介绍:**
+堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
-> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
->
-> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
+## 3. 说说并发与并行的区别?
-## 4. 线程池
+- **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
+- **并行:** 单位时间内,多个任务同时执行。
-### 4.1. 为什么要用线程池?
+## 4. 为什么要使用多线程呢?
-> **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。**
+先从总体上来说:
-**线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。
+- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
+- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
-这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**:
+再深入到计算机底层来探讨:
-- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
-- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
+- **单核时代:** 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。
+- **多核时代:** 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。
-### 4.2. 实现 Runnable 接口和 Callable 接口的区别
+## 5. 使用多线程可能带来什么问题?
-`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)`)。
+## 6. 说说线程的生命周期和状态?
-`Runnable.java`
+Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。
-```java
-@FunctionalInterface
-public interface Runnable {
- /**
- * 被线程执行,没有返回值也无法抛出异常
- */
- public abstract void run();
-}
-```
+
-`Callable.java`
+线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):
-```java
-@FunctionalInterface
-public interface Callable {
- /**
- * 计算结果,或在无法这样做时抛出异常。
- * @return 计算得出的结果
- * @throws 如果无法计算结果,则抛出异常
- */
- V call() throws Exception;
-}
-```
+
-### 4.3. 执行 execute()方法和 submit()方法的区别是什么呢?
+> 订正(来自[issue736](https://github.com/Snailclimb/JavaGuide/issues/736)):原图中 wait到 runnable状态的转换中,`join`实际上是`Thread`类的方法,但这里写成了`Object`。
-1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;**
-2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
+由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。
-我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码:
+> 操作系统隐藏 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(运行中)** 状态 。
-```java
- public Future> submit(Runnable task) {
- if (task == null) throw new NullPointerException();
- RunnableFuture ftask = newTaskFor(task, null);
- execute(ftask);
- return ftask;
- }
-```
+
-上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。
+当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的`run()`方法之后将会进入到 **TERMINATED(终止)** 状态。
-```java
- protected RunnableFuture newTaskFor(Runnable runnable, T value) {
- return new FutureTask(runnable, value);
- }
-```
+## 7. 什么是上下文切换?
-我们再来看看`execute()`方法:
+多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
-```java
- public void execute(Runnable command) {
- ...
- }
-```
+概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。
-### 4.4. 如何创建线程池
+上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
-《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
+Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
-> Executors 返回线程池对象的弊端如下:
->
-> - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
-> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
+## 8. 什么是线程死锁?如何避免死锁?
-**方式一:通过构造方法实现**
-
-**方式二:通过 Executor 框架的工具类 Executors 来实现**
-我们可以创建三种类型的 ThreadPoolExecutor:
+### 8.1. 认识线程死锁
-- **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
-- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
-- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
+线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
-对应 Executors 工具类中的方法如图所示:
-
+如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
-### 4.5 ThreadPoolExecutor 类分析
+
-`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。
+下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):
```java
- /**
- * 用给定的初始参数创建一个新的ThreadPoolExecutor。
- */
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue workQueue,
- ThreadFactory threadFactory,
- RejectedExecutionHandler handler) {
- if (corePoolSize < 0 ||
- maximumPoolSize <= 0 ||
- maximumPoolSize < corePoolSize ||
- keepAliveTime < 0)
- throw new IllegalArgumentException();
- if (workQueue == null || threadFactory == null || handler == null)
- throw new NullPointerException();
- this.corePoolSize = corePoolSize;
- this.maximumPoolSize = maximumPoolSize;
- this.workQueue = workQueue;
- this.keepAliveTime = unit.toNanos(keepAliveTime);
- this.threadFactory = threadFactory;
- this.handler = handler;
- }
-```
-
-**下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。**
-
-#### 4.5.1 `ThreadPoolExecutor`构造函数重要参数分析
-
-**`ThreadPoolExecutor` 3 个最重要的参数:**
-
-- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。
-- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
-- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
-
-`ThreadPoolExecutor`其他常见参数:
-
-1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁;
-2. **`unit`** : `keepAliveTime` 参数的时间单位。
-3. **`threadFactory`** :executor 创建新线程的时候会用到。
-4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。
-
-#### 4.5.2 `ThreadPoolExecutor` 饱和策略
+public class DeadLockDemo {
+ private static Object resource1 = new Object();//资源 1
+ private static Object resource2 = new Object();//资源 2
-**`ThreadPoolExecutor` 饱和策略定义:**
-
-如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略:
-
-- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。
-- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
-- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。
-- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。
-
-举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)
-
-### 4.6 一个简单的线程池 Demo
-
-为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。
-
-首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。)
-
-`MyRunnable.java`
-
-```java
-import java.util.Date;
-
-/**
- * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
- * @author shuang.kou
- */
-public class MyRunnable implements Runnable {
-
- private String command;
-
- public MyRunnable(String s) {
- this.command = s;
- }
-
- @Override
- public void run() {
- System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
- processCommand();
- System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
- }
-
- private void processCommand() {
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public String toString() {
- return this.command;
- }
-}
-
-```
-
-编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。
-
-`ThreadPoolExecutorDemo.java`
-
-```java
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-public class ThreadPoolExecutorDemo {
-
- private static final int CORE_POOL_SIZE = 5;
- private static final int MAX_POOL_SIZE = 10;
- private static final int QUEUE_CAPACITY = 100;
- private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
-
- //使用阿里巴巴推荐的创建线程池的方式
- //通过ThreadPoolExecutor构造函数自定义参数创建
- ThreadPoolExecutor executor = new ThreadPoolExecutor(
- CORE_POOL_SIZE,
- MAX_POOL_SIZE,
- KEEP_ALIVE_TIME,
- TimeUnit.SECONDS,
- new ArrayBlockingQueue<>(QUEUE_CAPACITY),
- new ThreadPoolExecutor.CallerRunsPolicy());
-
- for (int i = 0; i < 10; i++) {
- //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
- Runnable worker = new MyRunnable("" + i);
- //执行Runnable
- executor.execute(worker);
- }
- //终止线程池
- executor.shutdown();
- while (!executor.isTerminated()) {
- }
- System.out.println("Finished all threads");
+ 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();
}
}
-
-```
-
-可以看到我们上面的代码指定了:
-
-1. `corePoolSize`: 核心线程数为 5。
-2. `maximumPoolSize` :最大线程数 10
-3. `keepAliveTime` : 等待时间为 1L。
-4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。
-5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100;
-6. `handler`:饱和策略为 `CallerRunsPolicy`。
-
-**Output:**
-
-```
-pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
-pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
-pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
-pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
-pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
-pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
-pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
-pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
-pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
-pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
-pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
-pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
-pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
-pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
-pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
-pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
-pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
-pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
-pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
-pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019
-
-```
-
-### 4.7 线程池原理分析
-
-承接 4.6 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会)
-
-现在,我们就分析上面的输出内容来简单分析一下线程池原理。
-
-**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.6 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码:
-
-```java
- // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
- private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
-
- private static int workerCountOf(int c) {
- return c & CAPACITY;
- }
-
- private final BlockingQueue workQueue;
-
- public void execute(Runnable command) {
- // 如果任务为null,则抛出异常。
- if (command == null)
- throw new NullPointerException();
- // ctl 中保存的线程池当前的一些状态信息
- int c = ctl.get();
-
- // 下面会涉及到 3 步 操作
- // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
- // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
- if (workerCountOf(c) < corePoolSize) {
- if (addWorker(command, true))
- return;
- c = ctl.get();
- }
- // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
- // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
- if (isRunning(c) && workQueue.offer(command)) {
- int recheck = ctl.get();
- // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
- if (!isRunning(recheck) && remove(command))
- reject(command);
- // 如果当前线程池为空就新创建一个线程并执行。
- else if (workerCountOf(recheck) == 0)
- addWorker(null, false);
- }
- //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
- //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
- else if (!addWorker(command, false))
- reject(command);
- }
-```
-
-通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。
-
-
-
-现在,让我们在回到 4.6 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢?
-
-没搞懂的话,也没关系,可以看看我的分析:
-
-> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。
-
-## 5. Atomic 原子类
-
-### 5.1. 介绍一下 Atomic 原子类
-
-Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
-
-所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
-
-并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
-
-
-
-### 5.2. JUC 包中的原子类是哪 4 类?
-
-**基本类型**
-
-使用原子的方式更新基本类型
-
-- AtomicInteger:整形原子类
-- AtomicLong:长整型原子类
-- AtomicBoolean:布尔型原子类
-
-**数组类型**
-
-使用原子的方式更新数组里的某个元素
-
-- AtomicIntegerArray:整形数组原子类
-- AtomicLongArray:长整形数组原子类
-- AtomicReferenceArray:引用类型数组原子类
-
-**引用类型**
-
-- AtomicReference:引用类型原子类
-- AtomicStampedReference:原子更新引用类型里的字段原子类
-- AtomicMarkableReference :原子更新带有标记位的引用类型
-
-**对象的属性修改类型**
-
-- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
-- AtomicLongFieldUpdater:原子更新长整形字段的更新器
-- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
-
-### 5.3. 讲讲 AtomicInteger 的使用
-
-**AtomicInteger 类常用方法**
-
-```java
-public final int get() //获取当前的值
-public final int getAndSet(int newValue)//获取当前的值,并设置新的值
-public final int getAndIncrement()//获取当前的值,并自增
-public final int getAndDecrement() //获取当前的值,并自减
-public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
-boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
-public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
```
-**AtomicInteger 类的使用示例**
-
-使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。
-
-```java
-class AtomicIntegerTest {
- private AtomicInteger count = new AtomicInteger();
- //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。
- public void increment() {
- count.incrementAndGet();
- }
-
- public int getCount() {
- return count.get();
- }
-}
+Output
```
-
-### 5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理
-
-AtomicInteger 线程安全原理简单分析
-
-AtomicInteger 类的部分源码:
-
-```java
- // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
- private static final Unsafe unsafe = Unsafe.getUnsafe();
- private static final long valueOffset;
-
- static {
- try {
- valueOffset = unsafe.objectFieldOffset
- (AtomicInteger.class.getDeclaredField("value"));
- } catch (Exception ex) { throw new Error(ex); }
- }
-
- private volatile int value;
+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
```
-AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
-
-CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
+线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
-关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg)
+学过操作系统的朋友都知道产生死锁必须具备以下四个条件:
-## 6. AQS
+1. 互斥条件:该资源任意一个时刻只由一个线程占用。
+2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
+3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
+4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
-### 6.1. AQS 介绍
+### 8.2. 如何避免线程死锁?
-AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。
+我上面说了产生死锁的四个必要条件,为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了。现在我们来挨个分析一下:
-
+1. **破坏互斥条件** :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
+2. **破坏请求与保持条件** :一次性申请所有的资源。
+3. **破坏不剥夺条件** :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
+4. **破坏循环等待条件** :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
-AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
-
-### 6.2. AQS 原理分析
-
-AQS 原理这部分参考了部分博客,在 5.2 节末尾放了链接。
-
-> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。
-
-下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。
-
-#### 6.2.1. AQS 原理概览
-
-**AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。**
-
-> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
-
-看个 AQS(AbstractQueuedSynchronizer)原理图:
-
-
-
-AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
-
-```java
-private volatile int state;//共享变量,使用volatile修饰保证线程可见性
-```
-
-状态信息通过 protected 类型的 getState,setState,compareAndSetState 进行操作
+我们对线程 2 的代码修改成下面这样就不会产生死锁了。
```java
-
-//返回同步状态的当前值
-protected final int getState() {
- return state;
-}
- // 设置同步状态的值
-protected final void setState(int newState) {
- state = newState;
-}
-//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
-protected final boolean compareAndSetState(int expect, int update) {
- return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
-}
+ 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();
```
-#### 6.2.2. AQS 对资源的共享方式
-
-**AQS 定义两种资源共享方式**
-
-- **Exclusive**(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁:
- - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
-- **Share**(共享):多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
-
-ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。
-
-不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。
-
-#### 6.2.3. AQS 底层使用了模板方法模式
-
-同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
-
-1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
-2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
-
-这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
-
-**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:**
-
-```java
-isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
-tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
-tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
-tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
-tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
+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
-默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
-
-以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
-
-再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。
-
-一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。
-
-推荐两篇 AQS 原理和相关源码分析的文章:
-
-- http://www.cnblogs.com/waterystone/p/4920797.html
-- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
-
-### 6.3. AQS 组件总结
-
-- **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
-- **CountDownLatch (倒计时器):** CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
-- **CyclicBarrier(循环栅栏):** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
-
-### 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");
- }
-
-}
+Process finished with exit code 0
```
-**有没有可以改进的地方呢?**
+我们分析一下上面的代码为什么避免了死锁的发生?
-可以使用 `CompletableFuture` 类来改进!Java8 的 `CompletableFuture` 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。
+线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
-```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. ");
-```
+## 9. 说说 sleep() 方法和 wait() 方法区别和共同点?
-上面的代码还可以接续优化,当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。
+- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。
+- 两者都可以暂停线程的执行。
+- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
+- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。
-```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()])
-);
+## 10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
-```
+这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
-## 7 Reference
+new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
-- 《深入理解 Java 虚拟机》
-- 《实战 Java 高并发程序设计》
-- 《Java 并发编程的艺术》
-- http://www.cnblogs.com/waterystone/p/4920797.html
-- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
--
+**总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。**
## 公众号
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
-**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取!
+**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取!
**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。

+
+
diff --git "a/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\350\277\233\351\230\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" "b/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\350\277\233\351\230\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md"
index 59c1484834a..e55612460b6 100644
--- "a/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\350\277\233\351\230\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md"
+++ "b/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\350\277\233\351\230\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md"
@@ -1,305 +1,1119 @@
-
+点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
+
+
+
+
+
+
+- [Java 并发进阶常见面试题总结](#java-并发进阶常见面试题总结)
+ - [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.1.说一说自己对于 synchronized 关键字的了解
+
+**`synchronized` 关键字解决的是多个线程之间访问资源的同步性,`synchronized`关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。**
+
+另外,在 Java 早期版本中,`synchronized` 属于 **重量级锁**,效率低下。
+
+**为什么呢?**
+
+因为监视器锁(monitor)是依赖于底层的操作系统的 `Mutex Lock` 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
+
+庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
+
+所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。
+
+### 1.2. 说说自己是怎么使用 synchronized 关键字
+
+**synchronized 关键字最主要的三种使用方式:**
+
+**1.修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得 **当前对象实例的锁**
-- [Java 并发基础常见面试题总结](#java-并发基础常见面试题总结)
- - [1. 什么是线程和进程?](#1-什么是线程和进程)
- - [1.1. 何为进程?](#11-何为进程)
- - [1.2. 何为线程?](#12-何为线程)
- - [2. 请简要描述线程与进程的关系,区别及优缺点?](#2-请简要描述线程与进程的关系区别及优缺点)
- - [2.1. 图解进程和线程的关系](#21-图解进程和线程的关系)
- - [2.2. 程序计数器为什么是私有的?](#22-程序计数器为什么是私有的)
- - [2.3. 虚拟机栈和本地方法栈为什么是私有的?](#23-虚拟机栈和本地方法栈为什么是私有的)
- - [2.4. 一句话简单了解堆和方法区](#24-一句话简单了解堆和方法区)
- - [3. 说说并发与并行的区别?](#3-说说并发与并行的区别)
- - [4. 为什么要使用多线程呢?](#4-为什么要使用多线程呢)
- - [5. 使用多线程可能带来什么问题?](#5-使用多线程可能带来什么问题)
- - [6. 说说线程的生命周期和状态?](#6-说说线程的生命周期和状态)
- - [7. 什么是上下文切换?](#7-什么是上下文切换)
- - [8. 什么是线程死锁?如何避免死锁?](#8-什么是线程死锁如何避免死锁)
- - [8.1. 认识线程死锁](#81-认识线程死锁)
- - [8.2. 如何避免线程死锁?](#82-如何避免线程死锁)
- - [9. 说说 sleep() 方法和 wait() 方法区别和共同点?](#9-说说-sleep-方法和-wait-方法区别和共同点)
- - [10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#10-为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法)
- - [公众号](#公众号)
+```java
+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` 关键字的具体使用。
+
+面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”
+
+**双重校验锁实现对象单例(线程安全)**
+
+```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` 关键字修饰也是很有必要。
-# Java 并发基础常见面试题总结
+`uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要的, `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行:
-## 1. 什么是线程和进程?
+1. 为 `uniqueInstance` 分配内存空间
+2. 初始化 `uniqueInstance`
+3. 将 `uniqueInstance` 指向分配的内存地址
-### 1.1. 何为进程?
+但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 `getUniqueInstance`() 后发现 `uniqueInstance` 不为空,因此返回 `uniqueInstance`,但此时 `uniqueInstance` 还未被初始化。
-进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
+使用 `volatile` 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
-在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
+### 1.3. 构造方法可以使用 synchronized 关键字修饰么?
-如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。
+先说结论:**构造方法不能使用 synchronized 关键字修饰。**
-
+构造方法本身就属于线程安全的,不存在同步的构造方法一说。
-### 1.2. 何为线程?
+### 1.3. 讲一下 synchronized 关键字的底层原理
-线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
+**synchronized 关键字底层原理属于 JVM 层面。**
-Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。
+#### 1.3.1. synchronized 同步语句块的情况
```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());
+public class SynchronizedDemo {
+ public void method() {
+ synchronized (this) {
+ System.out.println("synchronized 代码块");
}
}
}
+
```
-上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):
+通过 JDK 自带的 `javap` 命令查看 `SynchronizedDemo` 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。
+
+
+
+从上面我们可以看出:
+
+**`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。**
+
+当执行 `monitorenter` 指令时,线程试图获取锁也就是获取 **对象监视器 `monitor`** 的持有权。
+
+> 在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由[ObjectMonitor](https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/runtime/objectMonitor.cpp)实现的。每个对象中都内置了一个 `ObjectMonitor`对象。
+>
+> 另外,**`wait/notify`等方法也依赖于`monitor`对象,这就是为什么只有在同步的块或者方法中才能调用`wait/notify`等方法,否则会抛出`java.lang.IllegalMonitorStateException`的异常的原因。**
+
+在执行`monitorenter`时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
+
+在执行 `monitorexit` 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
+
+#### 1.3.2. `synchronized` 修饰方法的的情况
+
+```java
+public class SynchronizedDemo2 {
+ public synchronized void method() {
+ System.out.println("synchronized 方法");
+ }
+}
```
-[5] Attach Listener //添加事件
-[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
-[3] Finalizer //调用对象 finalize 方法的线程
-[2] Reference Handler //清除 reference 线程
-[1] main //main 线程,程序入口
+
+
+
+`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
+
+#### 1.3.3.总结
+
+`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。
+
+`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。
+
+**不过两者的本质都是对对象监视器 monitor 的获取。**
+
+### 1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
+
+JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
+
+锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
+
+关于这几种优化的详细信息可以查看下面这几篇文章:
+
+- [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.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
+
+`synchronized` 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 `synchronized` 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。`ReentrantLock` 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
+
+#### 1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能
+
+相比`synchronized`,`ReentrantLock`增加了一些高级功能。主要来说主要有三点:
+
+- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
+- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。
+- **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()`和`notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法。
+
+> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 Condition 接口默认提供的。而`synchronized`关键字就相当于整个 Lock 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而`Condition`实例的`signalAll()`方法 只会唤醒注册在该`Condition`实例中的所有等待线程。
+
+**如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。性能已不是选择标准**
+
+## 2. volatile 关键字
+
+我们先要从 **CPU 缓存模型** 说起!
+
+### 2.1. CPU 缓存模型
+
+**为什么要弄一个 CPU 高速缓存呢?**
+
+类比我们开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。 **CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。**
+
+我们甚至可以把 **内存可以看作外存的高速缓存**,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。
+
+总结:**CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。**
+
+为了更好地理解,我画了一个简单的 CPU Cache 示意图如下(实际上,现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache):
+
+
+
+**CPU Cache 的工作方式:**
+
+先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 **内存缓存不一致性的问题** !比如我执行一个 i++操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。
+
+**CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议或者其他手段来解决。**
+
+### 2.2. 讲一下 JMM(Java 内存模型)
+
+在 JDK1.2 之前,Java 的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。
+
+
+
+要解决这个问题,就需要把变量声明为**`volatile`**,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
+
+所以,**`volatile` 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。**
+
+
+
+### 2.3. 并发编程的三个重要特性
+
+1. **原子性** : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。`synchronized` 可以保证代码片段的原子性。
+2. **可见性** :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。`volatile` 关键字可以保证共享变量的可见性。
+3. **有序性** :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。`volatile` 关键字可以禁止指令进行重排序优化。
+
+### 2.4. 说说 synchronized 关键字和 volatile 关键字的区别
+
+`synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在!
+
+- **volatile 关键字**是线程同步的**轻量级实现**,所以**volatile 性能肯定比 synchronized 关键字要好**。但是**volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块**。
+- **volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。**
+- **volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。**
+
+## 3. ThreadLocal
+
+### 3.1. ThreadLocal 简介
+
+通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK 中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**
+
+**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。**
+
+再举个简单的例子:
+
+比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。
+
+### 3.2. ThreadLocal 示例
+
+相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。
+
+```java
+import java.text.SimpleDateFormat;
+import java.util.Random;
+
+public class ThreadLocalExample implements Runnable{
+
+ // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
+ private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
+
+ public static void main(String[] args) throws InterruptedException {
+ ThreadLocalExample obj = new ThreadLocalExample();
+ for(int i=0 ; i<10; i++){
+ Thread t = new Thread(obj, ""+i);
+ Thread.sleep(new Random().nextInt(1000));
+ t.start();
+ }
+ }
+
+ @Override
+ public void run() {
+ System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
+ try {
+ Thread.sleep(new Random().nextInt(1000));
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ //formatter pattern is changed here by thread, but it won't reflect to other threads
+ formatter.set(new SimpleDateFormat());
+
+ System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
+ }
+
+}
+
```
-从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。
+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
+```
-## 2. 请简要描述线程与进程的关系,区别及优缺点?
+从输出中可以看出,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");
+ }
+ };
+```
-**从 JVM 角度说进程和线程之间的关系**
+### 3.3. ThreadLocal 原理
-### 2.1. 图解进程和线程的关系
+从 `Thread`类源代码入手。
-下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md "《可能是把 Java 内存区域讲的最清楚的一篇文章》")
+```java
+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()`方法。
-从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。
+`ThreadLocal`类的`set()`方法
-**总结:** **线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。**
+```java
+ public void set(T value) {
+ Thread t = Thread.currentThread();
+ ThreadLocalMap map = getMap(t);
+ if (map != null)
+ map.set(this, value);
+ else
+ createMap(t, value);
+ }
+ ThreadLocalMap getMap(Thread t) {
+ return t.threadLocals;
+ }
+```
-下面是该知识点的扩展内容!
+通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。
-下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢?
+**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为 key ,Object 对象为 value 的键值对。**
-### 2.2. 程序计数器为什么是私有的?
+```java
+ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
+ ......
+}
+```
-程序计数器主要有下面两个作用:
+比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。
-1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
-2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
+
-需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
+`ThreadLocalMap`是`ThreadLocal`的静态内部类。
-所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。
+
-### 2.3. 虚拟机栈和本地方法栈为什么是私有的?
+### 3.4. ThreadLocal 内存泄露问题
-- **虚拟机栈:** 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
-- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
+`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;
-### 2.4. 一句话简单了解堆和方法区
+ Entry(ThreadLocal> k, Object v) {
+ super(k);
+ value = v;
+ }
+ }
+```
-堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
+**弱引用介绍:**
-## 3. 说说并发与并行的区别?
+> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
+>
+> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
-- **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
-- **并行:** 单位时间内,多个任务同时执行。
+## 4. 线程池
-## 4. 为什么要使用多线程呢?
+### 4.1. 为什么要用线程池?
-先从总体上来说:
+> **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。**
-- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
-- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
+**线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。
-再深入到计算机底层来探讨:
+这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**:
-- **单核时代:** 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。
-- **多核时代:** 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。
+- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
+- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
+- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
-## 5. 使用多线程可能带来什么问题?
+### 4.2. 实现 Runnable 接口和 Callable 接口的区别
-并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。
+`Runnable`自 Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。
-## 6. 说说线程的生命周期和状态?
+工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callable(Runnable task`)或 `Executors.callable(Runnable task,Object resule)`)。
-Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。
+`Runnable.java`
-
+```java
+@FunctionalInterface
+public interface Runnable {
+ /**
+ * 被线程执行,没有返回值也无法抛出异常
+ */
+ public abstract void run();
+}
+```
-线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):
+`Callable.java`
-
+```java
+@FunctionalInterface
+public interface Callable {
+ /**
+ * 计算结果,或在无法这样做时抛出异常。
+ * @return 计算得出的结果
+ * @throws 如果无法计算结果,则抛出异常
+ */
+ V call() throws Exception;
+}
+```
-> 订正(来自[issue736](https://github.com/Snailclimb/JavaGuide/issues/736)):原图中 wait到 runnable状态的转换中,`join`实际上是`Thread`类的方法,但这里写成了`Object`。
+### 4.3. 执行 execute()方法和 submit()方法的区别是什么呢?
-由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。
+1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;**
+2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
-> 操作系统隐藏 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(运行中)** 状态 。
+我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码:
-
+```java
+ public Future> submit(Runnable task) {
+ if (task == null) throw new NullPointerException();
+ RunnableFuture ftask = newTaskFor(task, null);
+ execute(ftask);
+ return ftask;
+ }
+```
-当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的`run()`方法之后将会进入到 **TERMINATED(终止)** 状态。
+上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。
-## 7. 什么是上下文切换?
+```java
+ protected RunnableFuture newTaskFor(Runnable runnable, T value) {
+ return new FutureTask(runnable, value);
+ }
+```
-多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
+我们再来看看`execute()`方法:
-概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。
+```java
+ public void execute(Runnable command) {
+ ...
+ }
+```
-上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
+### 4.4. 如何创建线程池
-Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
+《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
-## 8. 什么是线程死锁?如何避免死锁?
+> Executors 返回线程池对象的弊端如下:
+>
+> - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
+> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
-### 8.1. 认识线程死锁
+**方式一:通过构造方法实现**
+
+**方式二:通过 Executor 框架的工具类 Executors 来实现**
+我们可以创建三种类型的 ThreadPoolExecutor:
-线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
+- **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
+- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
+- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
-如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
+对应 Executors 工具类中的方法如图所示:
+
-
+### 4.5 ThreadPoolExecutor 类分析
-下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):
+`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。
```java
-public class DeadLockDemo {
- private static Object resource1 = new Object();//资源 1
- private static Object resource2 = new Object();//资源 2
+ /**
+ * 用给定的初始参数创建一个新的ThreadPoolExecutor。
+ */
+ public ThreadPoolExecutor(int corePoolSize,
+ int maximumPoolSize,
+ long keepAliveTime,
+ TimeUnit unit,
+ BlockingQueue workQueue,
+ ThreadFactory threadFactory,
+ RejectedExecutionHandler handler) {
+ if (corePoolSize < 0 ||
+ maximumPoolSize <= 0 ||
+ maximumPoolSize < corePoolSize ||
+ keepAliveTime < 0)
+ throw new IllegalArgumentException();
+ if (workQueue == null || threadFactory == null || handler == null)
+ throw new NullPointerException();
+ this.corePoolSize = corePoolSize;
+ this.maximumPoolSize = maximumPoolSize;
+ this.workQueue = workQueue;
+ this.keepAliveTime = unit.toNanos(keepAliveTime);
+ this.threadFactory = threadFactory;
+ this.handler = handler;
+ }
+```
+
+**下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。**
+
+#### 4.5.1 `ThreadPoolExecutor`构造函数重要参数分析
+
+**`ThreadPoolExecutor` 3 个最重要的参数:**
+
+- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。
+- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
+- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
+
+`ThreadPoolExecutor`其他常见参数:
+
+1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁;
+2. **`unit`** : `keepAliveTime` 参数的时间单位。
+3. **`threadFactory`** :executor 创建新线程的时候会用到。
+4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。
+
+#### 4.5.2 `ThreadPoolExecutor` 饱和策略
+**`ThreadPoolExecutor` 饱和策略定义:**
+
+如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略:
+
+- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。
+- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
+- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。
+- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。
+
+举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)
+
+### 4.6 一个简单的线程池 Demo
+
+为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。
+
+首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。)
+
+`MyRunnable.java`
+
+```java
+import java.util.Date;
+
+/**
+ * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
+ * @author shuang.kou
+ */
+public class MyRunnable implements Runnable {
+
+ private String command;
+
+ public MyRunnable(String s) {
+ this.command = s;
+ }
+
+ @Override
+ public void run() {
+ System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
+ processCommand();
+ System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
+ }
+
+ private void processCommand() {
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return this.command;
+ }
+}
+
+```
+
+编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。
+
+`ThreadPoolExecutorDemo.java`
+
+```java
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class ThreadPoolExecutorDemo {
+
+ private static final int CORE_POOL_SIZE = 5;
+ private static final int MAX_POOL_SIZE = 10;
+ private static final int QUEUE_CAPACITY = 100;
+ private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
- 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();
+
+ //使用阿里巴巴推荐的创建线程池的方式
+ //通过ThreadPoolExecutor构造函数自定义参数创建
+ ThreadPoolExecutor executor = new ThreadPoolExecutor(
+ CORE_POOL_SIZE,
+ MAX_POOL_SIZE,
+ KEEP_ALIVE_TIME,
+ TimeUnit.SECONDS,
+ new ArrayBlockingQueue<>(QUEUE_CAPACITY),
+ new ThreadPoolExecutor.CallerRunsPolicy());
+
+ for (int i = 0; i < 10; i++) {
+ //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
+ Runnable worker = new MyRunnable("" + i);
+ //执行Runnable
+ executor.execute(worker);
+ }
+ //终止线程池
+ executor.shutdown();
+ while (!executor.isTerminated()) {
+ }
+ System.out.println("Finished all threads");
}
}
+
```
-Output
+可以看到我们上面的代码指定了:
+
+1. `corePoolSize`: 核心线程数为 5。
+2. `maximumPoolSize` :最大线程数 10
+3. `keepAliveTime` : 等待时间为 1L。
+4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。
+5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100;
+6. `handler`:饱和策略为 `CallerRunsPolicy`。
+
+**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
+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
+
```
-线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
+### 4.7 线程池原理分析
+
+承接 4.6 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会)
+
+现在,我们就分析上面的输出内容来简单分析一下线程池原理。
+
+**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.6 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码:
+
+```java
+ // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
+ private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
+
+ private static int workerCountOf(int c) {
+ return c & CAPACITY;
+ }
+
+ private final BlockingQueue workQueue;
+
+ public void execute(Runnable command) {
+ // 如果任务为null,则抛出异常。
+ if (command == null)
+ throw new NullPointerException();
+ // ctl 中保存的线程池当前的一些状态信息
+ int c = ctl.get();
+
+ // 下面会涉及到 3 步 操作
+ // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
+ // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
+ if (workerCountOf(c) < corePoolSize) {
+ if (addWorker(command, true))
+ return;
+ c = ctl.get();
+ }
+ // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
+ // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
+ if (isRunning(c) && workQueue.offer(command)) {
+ int recheck = ctl.get();
+ // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
+ if (!isRunning(recheck) && remove(command))
+ reject(command);
+ // 如果当前线程池为空就新创建一个线程并执行。
+ else if (workerCountOf(recheck) == 0)
+ addWorker(null, false);
+ }
+ //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
+ //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
+ else if (!addWorker(command, false))
+ reject(command);
+ }
+```
+
+通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。
+
+
+
+现在,让我们在回到 4.6 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢?
+
+没搞懂的话,也没关系,可以看看我的分析:
+
+> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。
+
+## 5. Atomic 原子类
+
+### 5.1. 介绍一下 Atomic 原子类
-学过操作系统的朋友都知道产生死锁必须具备以下四个条件:
+Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
-1. 互斥条件:该资源任意一个时刻只由一个线程占用。
-2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
-3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
-4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
+所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
-### 8.2. 如何避免线程死锁?
+并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
-我上面说了产生死锁的四个必要条件,为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了。现在我们来挨个分析一下:
+
-1. **破坏互斥条件** :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
-2. **破坏请求与保持条件** :一次性申请所有的资源。
-3. **破坏不剥夺条件** :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
-4. **破坏循环等待条件** :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
+### 5.2. JUC 包中的原子类是哪 4 类?
-我们对线程 2 的代码修改成下面这样就不会产生死锁了。
+**基本类型**
+
+使用原子的方式更新基本类型
+
+- AtomicInteger:整形原子类
+- AtomicLong:长整型原子类
+- AtomicBoolean:布尔型原子类
+
+**数组类型**
+
+使用原子的方式更新数组里的某个元素
+
+- AtomicIntegerArray:整形数组原子类
+- AtomicLongArray:长整形数组原子类
+- AtomicReferenceArray:引用类型数组原子类
+
+**引用类型**
+
+- AtomicReference:引用类型原子类
+- AtomicStampedReference:原子更新引用类型里的字段原子类
+- AtomicMarkableReference :原子更新带有标记位的引用类型
+
+**对象的属性修改类型**
+
+- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
+- AtomicLongFieldUpdater:原子更新长整形字段的更新器
+- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
+
+### 5.3. 讲讲 AtomicInteger 的使用
+
+**AtomicInteger 类常用方法**
```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();
+public final int get() //获取当前的值
+public final int getAndSet(int newValue)//获取当前的值,并设置新的值
+public final int getAndIncrement()//获取当前的值,并自增
+public final int getAndDecrement() //获取当前的值,并自减
+public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
+boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
+public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
+```
+
+**AtomicInteger 类的使用示例**
+
+使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。
+
+```java
+class AtomicIntegerTest {
+ private AtomicInteger count = new AtomicInteger();
+ //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。
+ public void increment() {
+ count.incrementAndGet();
+ }
+
+ public int getCount() {
+ return count.get();
+ }
+}
+
+```
+
+### 5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理
+
+AtomicInteger 线程安全原理简单分析
+
+AtomicInteger 类的部分源码:
+
+```java
+ // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
+ private static final Unsafe unsafe = Unsafe.getUnsafe();
+ private static final long valueOffset;
+
+ static {
+ try {
+ valueOffset = unsafe.objectFieldOffset
+ (AtomicInteger.class.getDeclaredField("value"));
+ } catch (Exception ex) { throw new Error(ex); }
+ }
+
+ private volatile int value;
+```
+
+AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
+
+CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
+
+关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg)
+
+## 6. AQS
+
+### 6.1. AQS 介绍
+
+AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。
+
+
+
+AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
+
+### 6.2. AQS 原理分析
+
+AQS 原理这部分参考了部分博客,在 5.2 节末尾放了链接。
+
+> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。
+
+下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。
+
+#### 6.2.1. AQS 原理概览
+
+**AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。**
+
+> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
+
+看个 AQS(AbstractQueuedSynchronizer)原理图:
+
+
+
+AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
+
+```java
+private volatile int state;//共享变量,使用volatile修饰保证线程可见性
+```
+
+状态信息通过 protected 类型的 getState,setState,compareAndSetState 进行操作
+
+```java
+
+//返回同步状态的当前值
+protected final int getState() {
+ return state;
+}
+ // 设置同步状态的值
+protected final void setState(int newState) {
+ state = newState;
+}
+//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
+protected final boolean compareAndSetState(int expect, int update) {
+ return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
+}
```
-Output
+#### 6.2.2. AQS 对资源的共享方式
+
+**AQS 定义两种资源共享方式**
+
+- **Exclusive**(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁:
+ - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
+ - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
+- **Share**(共享):多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
+
+ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。
+
+不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。
+
+#### 6.2.3. AQS 底层使用了模板方法模式
+
+同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
+
+1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
+2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
+
+这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
+
+**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:**
+
+```java
+isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
+tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
+tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
+tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
+tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
```
-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
+默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
+
+以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
+
+再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。
+
+一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。
+
+推荐两篇 AQS 原理和相关源码分析的文章:
+
+- http://www.cnblogs.com/waterystone/p/4920797.html
+- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
+
+### 6.3. AQS 组件总结
+
+- **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
+- **CountDownLatch (倒计时器):** CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
+- **CyclicBarrier(循环栅栏):** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
+
+### 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");
+ }
+
+}
```
-我们分析一下上面的代码为什么避免了死锁的发生?
+**有没有可以改进的地方呢?**
-线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
+可以使用 `CompletableFuture` 类来改进!Java8 的 `CompletableFuture` 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。
-## 9. 说说 sleep() 方法和 wait() 方法区别和共同点?
+```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. ");
+```
-- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。
-- 两者都可以暂停线程的执行。
-- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
-- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。
+上面的代码还可以接续优化,当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。
-## 10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
+```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()])
+);
-这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
+```
-new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
+## 7 Reference
-**总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。**
+- 《深入理解 Java 虚拟机》
+- 《实战 Java 高并发程序设计》
+- 《Java 并发编程的艺术》
+- http://www.cnblogs.com/waterystone/p/4920797.html
+- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
+-
## 公众号
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
-**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取!
+**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取!
**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。

-
-
diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/CachedThreadPool-execute.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/CachedThreadPool-execute.png"
new file mode 100644
index 00000000000..8b2ede8ab54
Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/CachedThreadPool-execute.png" differ
diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executors\345\267\245\345\205\267\347\261\273.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executors\345\267\245\345\205\267\347\261\273.png"
new file mode 100644
index 00000000000..87658aa3f09
Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executors\345\267\245\345\205\267\347\261\273.png" differ
diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executor\346\241\206\346\236\266\347\232\204\344\275\277\347\224\250\347\244\272\346\204\217\345\233\276.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executor\346\241\206\346\236\266\347\232\204\344\275\277\347\224\250\347\244\272\346\204\217\345\233\276.png"
new file mode 100644
index 00000000000..5cc148dd79a
Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executor\346\241\206\346\236\266\347\232\204\344\275\277\347\224\250\347\244\272\346\204\217\345\233\276.png" differ
diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/FixedThreadPool.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/FixedThreadPool.png"
new file mode 100644
index 00000000000..fc1c7034fdf
Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/FixedThreadPool.png" differ
diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\211\247\350\241\214\345\221\250\346\234\237\344\273\273\345\212\241\346\255\245\351\252\244.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\211\247\350\241\214\345\221\250\346\234\237\344\273\273\345\212\241\346\255\245\351\252\244.png"
new file mode 100644
index 00000000000..c56521d283e
Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\211\247\350\241\214\345\221\250\346\234\237\344\273\273\345\212\241\346\255\245\351\252\244.png" differ
diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\234\272\345\210\266.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\234\272\345\210\266.png"
new file mode 100644
index 00000000000..bae0dc5b2dc
Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\234\272\345\210\266.png" differ
diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/SingleThreadExecutor.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/SingleThreadExecutor.png"
new file mode 100644
index 00000000000..c933674fad4
Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/SingleThreadExecutor.png" differ
diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png"
new file mode 100644
index 00000000000..30c298591bc
Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" differ
diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\344\273\273\345\212\241\347\232\204\346\211\247\350\241\214\347\233\270\345\205\263\346\216\245\345\217\243.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\344\273\273\345\212\241\347\232\204\346\211\247\350\241\214\347\233\270\345\205\263\346\216\245\345\217\243.png"
new file mode 100644
index 00000000000..6aebd60b591
Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\344\273\273\345\212\241\347\232\204\346\211\247\350\241\214\347\233\270\345\205\263\346\216\245\345\217\243.png" differ
diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\345\233\276\350\247\243\347\272\277\347\250\213\346\261\240\345\256\236\347\216\260\345\216\237\347\220\206.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\345\233\276\350\247\243\347\272\277\347\250\213\346\261\240\345\256\236\347\216\260\345\216\237\347\220\206.png"
new file mode 100644
index 00000000000..bc661944a0a
Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\345\233\276\350\247\243\347\272\277\347\250\213\346\261\240\345\256\236\347\216\260\345\216\237\347\220\206.png" differ
diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\347\272\277\347\250\213\346\261\240\345\220\204\344\270\252\345\217\202\346\225\260\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\347\272\277\347\250\213\346\261\240\345\220\204\344\270\252\345\217\202\346\225\260\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png"
new file mode 100644
index 00000000000..d609943bafe
Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\347\272\277\347\250\213\346\261\240\345\220\204\344\270\252\345\217\202\346\225\260\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" differ
diff --git "a/docs/java/multi-thread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" "b/docs/java/multi-thread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md"
index 54f4b650aa2..6a946dd0206 100644
--- "a/docs/java/multi-thread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md"
+++ "b/docs/java/multi-thread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md"
@@ -97,7 +97,7 @@ public class ScheduledThreadPoolExecutor
implements ScheduledExecutorService
```
-
+
#### 3) 异步计算的结果(`Future`)
@@ -107,7 +107,7 @@ public class ScheduledThreadPoolExecutor
### 2.3 Executor 框架的使用示意图
-
+
1. **主线程首先要创建实现 `Runnable` 或者 `Callable` 接口的任务对象。**
2. **把创建完成的实现 `Runnable`/`Callable`接口的 对象直接交给 `ExecutorService` 执行**: `ExecutorService.execute(Runnable command)`)或者也可以把 `Runnable` 对象或`Callable` 对象提交给 `ExecutorService` 执行(`ExecutorService.submit(Runnable task)`或 `ExecutorService.submit(Callable task)`)。
@@ -167,7 +167,7 @@ public class ScheduledThreadPoolExecutor
下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》):
-
+
**`ThreadPoolExecutor` 饱和策略定义:**
@@ -198,7 +198,7 @@ public class ScheduledThreadPoolExecutor
> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
**方式一:通过`ThreadPoolExecutor`构造函数实现(推荐)**
-
+
**方式二:通过 Executor 框架的工具类 Executors 来实现**
我们可以创建三种类型的 ThreadPoolExecutor:
@@ -207,7 +207,7 @@ public class ScheduledThreadPoolExecutor
- **CachedThreadPool**
对应 Executors 工具类中的方法如图所示:
-
+
## 四 (重要)ThreadPoolExecutor 使用示例
@@ -388,7 +388,7 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020
通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。
-
+
@@ -705,7 +705,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5
`FixedThreadPool` 的 `execute()` 方法运行示意图(该图片来源:《Java 并发编程的艺术》):
-
+
**上图说明:**
@@ -755,7 +755,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5
#### 5.2.2 执行任务过程介绍
**`SingleThreadExecutor` 的运行示意图(该图片来源:《Java 并发编程的艺术》):**
-
+
**上图说明;**
@@ -799,7 +799,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5
#### 5.3.2 执行任务过程介绍
**CachedThreadPool 的 execute()方法的执行示意图(该图片来源:《Java 并发编程的艺术》):**
-
+
**上图说明:**
@@ -830,7 +830,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5
### 6.2 运行机制
-
+
**`ScheduledThreadPoolExecutor` 的执行主要分为两大部分:**
@@ -845,7 +845,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5
### 6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤
-
+
1. 线程 1 从 `DelayQueue` 中获取已到期的 `ScheduledFutureTask(DelayQueue.take())`。到期任务是指 `ScheduledFutureTask`的 time 大于等于当前系统的时间;
2. 线程 1 执行这个 `ScheduledFutureTask`;
diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\346\200\273\347\273\223.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\346\200\273\347\273\223.md"
index 9a4a15c4bb7..7fa600055ea 100644
--- "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\346\200\273\347\273\223.md"
+++ "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\346\200\273\347\273\223.md"
@@ -1,8 +1,7 @@
本文是我在大二学习计算机网络期间整理的, 大部分内容都来自于谢希仁老师的《计算机网络》这本书。
-
-

-
+
+
@@ -37,9 +36,7 @@
## 1. 计算机网络概述
-
-

-
+
### 1.1. 基本术语
diff --git a/docs/operating-system/Linux_performance/image-20200604180850391.png b/docs/operating-system/Linux_performance/image-20200604180850391.png
deleted file mode 100755
index 20b6a534ddc..00000000000
Binary files a/docs/operating-system/Linux_performance/image-20200604180850391.png and /dev/null differ
diff --git a/docs/operating-system/Linux_performance/image-20200604180851790.png b/docs/operating-system/Linux_performance/image-20200604180851790.png
deleted file mode 100755
index 20b6a534ddc..00000000000
Binary files a/docs/operating-system/Linux_performance/image-20200604180851790.png and /dev/null differ
diff --git a/docs/operating-system/Linux_performance/image-20200604181133355.png b/docs/operating-system/Linux_performance/image-20200604181133355.png
deleted file mode 100755
index 47d74f36722..00000000000
Binary files a/docs/operating-system/Linux_performance/image-20200604181133355.png and /dev/null differ
diff --git a/docs/operating-system/Linux_performance/image-20200604203027136.png b/docs/operating-system/Linux_performance/image-20200604203027136.png
deleted file mode 100755
index 753a72f6031..00000000000
Binary files a/docs/operating-system/Linux_performance/image-20200604203027136.png and /dev/null differ
diff --git a/docs/operating-system/Linux_performance/image-20200605104607007.png b/docs/operating-system/Linux_performance/image-20200605104607007.png
deleted file mode 100755
index 95b1791fbd2..00000000000
Binary files a/docs/operating-system/Linux_performance/image-20200605104607007.png and /dev/null differ
diff --git a/docs/operating-system/Linux_performance/iostat.png b/docs/operating-system/Linux_performance/iostat.png
deleted file mode 100755
index 4e8c446fab0..00000000000
Binary files a/docs/operating-system/Linux_performance/iostat.png and /dev/null differ
diff --git a/docs/operating-system/Linux_performance/linux_xn.png b/docs/operating-system/Linux_performance/linux_xn.png
deleted file mode 100755
index 0eb28c0177f..00000000000
Binary files a/docs/operating-system/Linux_performance/linux_xn.png and /dev/null differ
diff --git a/docs/operating-system/Linux_performance/tcp_close.jpg b/docs/operating-system/Linux_performance/tcp_close.jpg
deleted file mode 100755
index f58c76cdc33..00000000000
Binary files a/docs/operating-system/Linux_performance/tcp_close.jpg and /dev/null differ
diff --git a/docs/operating-system/Linux_performance/tcp_conn.jpg b/docs/operating-system/Linux_performance/tcp_conn.jpg
deleted file mode 100755
index fae229dba30..00000000000
Binary files a/docs/operating-system/Linux_performance/tcp_conn.jpg and /dev/null differ
diff --git a/docs/operating-system/Linux_performance/tcpclose.png b/docs/operating-system/Linux_performance/tcpclose.png
deleted file mode 100755
index 80d174300c9..00000000000
Binary files a/docs/operating-system/Linux_performance/tcpclose.png and /dev/null differ
diff --git a/docs/operating-system/Linux_performance/tcpconn.png b/docs/operating-system/Linux_performance/tcpconn.png
deleted file mode 100755
index 1985847c3c4..00000000000
Binary files a/docs/operating-system/Linux_performance/tcpconn.png and /dev/null differ
diff --git "a/docs/operating-system/Linux\346\200\247\350\203\275\345\210\206\346\236\220\345\267\245\345\205\267\345\220\210\351\233\206.md" "b/docs/operating-system/Linux\346\200\247\350\203\275\345\210\206\346\236\220\345\267\245\345\205\267\345\220\210\351\233\206.md"
deleted file mode 100755
index 41e17b973c2..00000000000
--- "a/docs/operating-system/Linux\346\200\247\350\203\275\345\210\206\346\236\220\345\267\245\345\205\267\345\220\210\351\233\206.md"
+++ /dev/null
@@ -1,634 +0,0 @@
-# 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.性能分析工具合集
-
-
-
-### 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分钟的系统平均负载值
-
-
-
-第一项是当前时间,up 表示系统正在运行,6:47是系统启动的总时间,最后是系统的负载load信息
-
-w 同上,增加了具体登陆了那些用户及登陆时间。
-
-#### top
-
-常用来监控[Linux](http://lib.csdn.net/base/linux)的系统状况,比如cpu、内存的使用,显示系统上正在运行的进程。
-
-
-
-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工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数,单位是秒,第二个参数是采样的次数,如:
-
-
-
-每个参数的含义:
-
-**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使用情况
-
-
-
-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/system-design/authority-certification/basis-of-authority-certification.md b/docs/system-design/authority-certification/basis-of-authority-certification.md
index 86352a50d3f..b576a28cf36 100644
--- a/docs/system-design/authority-certification/basis-of-authority-certification.md
+++ b/docs/system-design/authority-certification/basis-of-authority-certification.md
@@ -105,7 +105,7 @@ public String readAllCookies(HttpServletRequest request) {
花了个图简单总结了一下Session认证涉及的一些东西。
-
+
另外,Spring Session提供了一种跨多个应用程序或实例管理用户会话信息的机制。如果想详细了解可以查看下面几篇很不错的文章:
@@ -192,7 +192,7 @@ OAuth 2.0 比较常用的场景就是第三方登录,当你的网站接入了
微信支付账户相关参数:
-
+
**推荐阅读:**
diff --git a/docs/system-design/authority-certification/images/basis-of-authority-certification/session-cookie-intro.jpeg b/docs/system-design/authority-certification/images/basis-of-authority-certification/session-cookie-intro.jpeg
new file mode 100644
index 00000000000..feca0114904
Binary files /dev/null and b/docs/system-design/authority-certification/images/basis-of-authority-certification/session-cookie-intro.jpeg differ
diff --git a/docs/system-design/authority-certification/images/basis-of-authority-certification/session-cookie-intro.png b/docs/system-design/authority-certification/images/basis-of-authority-certification/session-cookie-intro.png
deleted file mode 100644
index b4ef47acbd3..00000000000
Binary files a/docs/system-design/authority-certification/images/basis-of-authority-certification/session-cookie-intro.png and /dev/null differ
diff --git a/docs/system-design/micro-service/api-gateway-intro.md "b/docs/system-design/distributed-system/api-gateway/\344\270\272\344\273\200\344\271\210\350\246\201\347\275\221\347\253\231\346\234\211\345\223\252\344\272\233\345\270\270\350\247\201\347\232\204\347\275\221\347\253\231\347\263\273\347\273\237.md"
similarity index 100%
rename from docs/system-design/micro-service/api-gateway-intro.md
rename to "docs/system-design/distributed-system/api-gateway/\344\270\272\344\273\200\344\271\210\350\246\201\347\275\221\347\253\231\346\234\211\345\223\252\344\272\233\345\270\270\350\247\201\347\232\204\347\275\221\347\253\231\347\263\273\347\273\237.md"
diff --git "a/docs/system-design/micro-service/API\347\275\221\345\205\263.md" "b/docs/system-design/distributed-system/api-gateway/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\344\272\277\347\272\247\347\275\221\345\205\263.md"
similarity index 100%
rename from "docs/system-design/micro-service/API\347\275\221\345\205\263.md"
rename to "docs/system-design/distributed-system/api-gateway/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\344\272\277\347\272\247\347\275\221\345\205\263.md"
diff --git a/docs/system-design/framework/zookeeper/images/curator.png b/docs/system-design/distributed-system/zookeeper/images/curator.png
similarity index 100%
rename from docs/system-design/framework/zookeeper/images/curator.png
rename to docs/system-design/distributed-system/zookeeper/images/curator.png
diff --git "a/docs/system-design/framework/zookeeper/images/watche\346\234\272\345\210\266.png" "b/docs/system-design/distributed-system/zookeeper/images/watche\346\234\272\345\210\266.png"
similarity index 100%
rename from "docs/system-design/framework/zookeeper/images/watche\346\234\272\345\210\266.png"
rename to "docs/system-design/distributed-system/zookeeper/images/watche\346\234\272\345\210\266.png"
diff --git a/docs/system-design/framework/zookeeper/images/znode-structure.png b/docs/system-design/distributed-system/zookeeper/images/znode-structure.png
similarity index 100%
rename from docs/system-design/framework/zookeeper/images/znode-structure.png
rename to docs/system-design/distributed-system/zookeeper/images/znode-structure.png
diff --git "a/docs/system-design/framework/zookeeper/images/zookeeper\351\233\206\347\276\244.png" "b/docs/system-design/distributed-system/zookeeper/images/zookeeper\351\233\206\347\276\244.png"
similarity index 100%
rename from "docs/system-design/framework/zookeeper/images/zookeeper\351\233\206\347\276\244.png"
rename to "docs/system-design/distributed-system/zookeeper/images/zookeeper\351\233\206\347\276\244.png"
diff --git "a/docs/system-design/framework/zookeeper/images/zookeeper\351\233\206\347\276\244\344\270\255\347\232\204\350\247\222\350\211\262.png" "b/docs/system-design/distributed-system/zookeeper/images/zookeeper\351\233\206\347\276\244\344\270\255\347\232\204\350\247\222\350\211\262.png"
similarity index 100%
rename from "docs/system-design/framework/zookeeper/images/zookeeper\351\233\206\347\276\244\344\270\255\347\232\204\350\247\222\350\211\262.png"
rename to "docs/system-design/distributed-system/zookeeper/images/zookeeper\351\233\206\347\276\244\344\270\255\347\232\204\350\247\222\350\211\262.png"
diff --git "a/docs/system-design/framework/zookeeper/images/\350\277\236\346\216\245ZooKeeper\346\234\215\345\212\241.png" "b/docs/system-design/distributed-system/zookeeper/images/\350\277\236\346\216\245ZooKeeper\346\234\215\345\212\241.png"
similarity index 100%
rename from "docs/system-design/framework/zookeeper/images/\350\277\236\346\216\245ZooKeeper\346\234\215\345\212\241.png"
rename to "docs/system-design/distributed-system/zookeeper/images/\350\277\236\346\216\245ZooKeeper\346\234\215\345\212\241.png"
diff --git a/docs/system-design/framework/zookeeper/zookeeper-in-action.md b/docs/system-design/distributed-system/zookeeper/zookeeper-in-action.md
similarity index 100%
rename from docs/system-design/framework/zookeeper/zookeeper-in-action.md
rename to docs/system-design/distributed-system/zookeeper/zookeeper-in-action.md
diff --git a/docs/system-design/framework/zookeeper/zookeeper-intro.md b/docs/system-design/distributed-system/zookeeper/zookeeper-intro.md
similarity index 100%
rename from docs/system-design/framework/zookeeper/zookeeper-intro.md
rename to docs/system-design/distributed-system/zookeeper/zookeeper-intro.md
diff --git a/docs/system-design/framework/zookeeper/zookeeper-plus.md b/docs/system-design/distributed-system/zookeeper/zookeeper-plus.md
similarity index 100%
rename from docs/system-design/framework/zookeeper/zookeeper-plus.md
rename to docs/system-design/distributed-system/zookeeper/zookeeper-plus.md
diff --git a/docs/system-design/framework/spring/Spring-Design-Patterns.md b/docs/system-design/framework/spring/Spring-Design-Patterns.md
index b37e318b1d7..c5d56ff6d37 100644
--- a/docs/system-design/framework/spring/Spring-Design-Patterns.md
+++ b/docs/system-design/framework/spring/Spring-Design-Patterns.md
@@ -154,8 +154,6 @@ AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无
模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。
-
-
```java
public abstract class Template {
//这是我们的模板方法
diff --git a/docs/system-design/framework/spring/spring-transaction.md "b/docs/system-design/framework/spring/Spring\344\272\213\345\212\241\346\200\273\347\273\223.md"
similarity index 99%
rename from docs/system-design/framework/spring/spring-transaction.md
rename to "docs/system-design/framework/spring/Spring\344\272\213\345\212\241\346\200\273\347\273\223.md"
index f465cd0c951..1e82a2aae38 100644
--- a/docs/system-design/framework/spring/spring-transaction.md
+++ "b/docs/system-design/framework/spring/Spring\344\272\213\345\212\241\346\200\273\347\273\223.md"
@@ -179,7 +179,7 @@ public interface PlatformTransactionManager {
主要是因为要将事务管理行为抽象出来,然后不同的平台去实现它,这样我们可以保证提供给外部的行为不变,方便我们扩展。我前段时间分享过:**“为什么我们要用接口?”**
-
+
#### 3.2.2. TransactionDefinition:事务属性
diff --git a/docs/system-design/framework/spring/SpringInterviewQuestions.md "b/docs/system-design/framework/spring/Spring\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md"
similarity index 100%
rename from docs/system-design/framework/spring/SpringInterviewQuestions.md
rename to "docs/system-design/framework/spring/Spring\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md"
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
deleted file mode 100644
index d868e0515cb..00000000000
Binary files a/docs/system-design/framework/spring/images/spring-transaction/ed279f05-f5ad-443e-84e9-513a9e777139.png and /dev/null differ
diff --git "a/docs/system-design/framework/spring/images/spring-transaction/\346\216\245\345\217\243\344\275\277\347\224\250\345\216\237\345\233\240.png" "b/docs/system-design/framework/spring/images/spring-transaction/\346\216\245\345\217\243\344\275\277\347\224\250\345\216\237\345\233\240.png"
new file mode 100644
index 00000000000..eb7e6748411
Binary files /dev/null and "b/docs/system-design/framework/spring/images/spring-transaction/\346\216\245\345\217\243\344\275\277\347\224\250\345\216\237\345\233\240.png" differ
diff --git "a/docs/system-design/high-availability/BASE\347\220\206\350\256\272.md" "b/docs/system-design/high-availability/BASE\347\220\206\350\256\272.md"
index e69de29bb2d..770ffd62a90 100644
--- "a/docs/system-design/high-availability/BASE\347\220\206\350\256\272.md"
+++ "b/docs/system-design/high-availability/BASE\347\220\206\350\256\272.md"
@@ -0,0 +1,50 @@
+## 简介
+
+**BASE** 是 **Basically Available(基本可用)** 、**Soft-state(软状态)** 和 **Eventually Consistent(最终一致性)** 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。
+
+## BASE 理论的核心思想
+
+即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。
+
+**BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。**
+
+**为什么这样说呢?**
+
+CAP 理论这节我们也说过了:
+
+> 如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。因此,**如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。**
+
+因此,AP 方案只是在系统发生分区的时候放弃一致性,而不是永远放弃一致性。在分区故障恢复后,系统应该达到最终一致性。这一点其实就是 BASE 理论延伸的地方。
+
+## BASE 理论三要素
+
+
+
+### 1. 基本可用
+
+基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。
+
+**什么叫允许损失部分可用性呢?**
+
+- **响应时间上的损失**: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。
+- **系统功能上的损失**:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。
+
+### 2. 软状态
+
+软状态指允许系统中的数据存在中间状态(**CAP 理论中的数据不一致**),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
+
+### 3. 最终一致性
+
+最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
+
+> 分布式一致性的 3 种级别:
+>
+> 1. **强一致性** :系统写入了什么,读出来的就是什么。
+> 2. **弱一致性** :不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
+> 3. **最终一致性** :弱一致性的升级版。,系统会保证在一定时间内达到数据一致的状态,
+>
+> 业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。
+
+## 总结
+
+**ACID 是数据库事务完整性的理论,CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。**
\ No newline at end of file
diff --git "a/docs/system-design/high-availability/CAP\347\220\206\350\256\272.md" "b/docs/system-design/high-availability/CAP\347\220\206\350\256\272.md"
index 381cda57ed7..32993c3f80a 100644
--- "a/docs/system-design/high-availability/CAP\347\220\206\350\256\272.md"
+++ "b/docs/system-design/high-availability/CAP\347\220\206\350\256\272.md"
@@ -12,15 +12,21 @@ CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细
- **一致性(Consistence)** : 所有节点访问同一份最新的数据副本
- **可用性(Availability)**: 非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。
-- **分区容错性(Partition tolerance)** : 分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供服务。
+- **分区容错性(Partition tolerance)** : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
CAP 仅适用于原子读写的 NOSQL 场景中,并不适合数据库系统。现在的分布式系统具有更多特性比如扩展性、可用性等等,在进行系统设计和开发时,我们不应该仅仅局限在 CAP 问题上。
+**什么是网络分区?**
+
+> 分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫网络分区。
+
## 不是所谓的“3 选 2”
大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法,而且在 CAP 理论诞生 12 年之后,CAP 之父也在 2012 年重写了之前的论文。
> **当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。**
+>
+> 简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。
因此,**分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。**
@@ -38,7 +44,7 @@ CAP 仅适用于原子读写的 NOSQL 场景中,并不适合数据库系统。
注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。
-
+
常见的可以作为注册中心的组件有:ZooKeeper、Eureka、Nacos...。
diff --git a/docs/system-design/high-availability/images/cap/cap.png b/docs/system-design/high-availability/images/cap/cap.png
index 4ac6504d1a0..64ffddcace3 100644
Binary files a/docs/system-design/high-availability/images/cap/cap.png and b/docs/system-design/high-availability/images/cap/cap.png differ
diff --git a/docs/system-design/high-availability/images/cap/dubbo-architecture.png b/docs/system-design/high-availability/images/cap/dubbo-architecture.png
index 530fd53ff5d..e00cd474aac 100644
Binary files a/docs/system-design/high-availability/images/cap/dubbo-architecture.png and b/docs/system-design/high-availability/images/cap/dubbo-architecture.png differ
diff --git "a/docs/system-design/high-availability/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\357\274\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271\357\274\237.md" "b/docs/system-design/high-availability/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271.md"
similarity index 100%
rename from "docs/system-design/high-availability/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\357\274\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271\357\274\237.md"
rename to "docs/system-design/high-availability/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271.md"
diff --git a/index.html b/index.html
index 974e7e7ee83..6b0768a5feb 100644
--- a/index.html
+++ b/index.html
@@ -8,11 +8,16 @@
-
+
+
+
@@ -21,12 +26,12 @@
if (typeof navigator.serviceWorker !== 'undefined') {
navigator.serviceWorker.register('sw.js')
}
-
window.$docsify = {
name: 'JavaGuide',
repo: 'https://github.com/Snailclimb/JavaGuide',
- maxLevel: 3,//最大支持渲染的标题层级
- coverpage: true,//封面,_coverpage.md
+ maxLevel: 4,//最大支持渲染的标题层级
+ //封面,_coverpage.md
+ //coverpage: true,
auto2top: true,//切换页面后是否自动跳转到页面顶部
//ga: 'UA-138586553-1',
//logo: 'https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3logo-透明.png' ,
@@ -38,10 +43,16 @@
// 搜索标题的最大程级, 1 - 6
depth: 3,
},
+ // 字数统计
+ count: {
+ countable: true,
+ fontsize: '0.9em',
+ color: 'rgb(90,90,90)',
+ language: 'chinese'
+ },
plugins: [
EditOnGithubPlugin.create('https://github.com/Snailclimb/JavaGuide/blob/master/')
],
-
}
@@ -59,6 +70,8 @@
+
+