auto commit
This commit is contained in:
parent
eee1684f20
commit
2d9238aac7
118
notes/Java 并发.md
118
notes/Java 并发.md
@ -16,16 +16,21 @@
|
||||
* [2.1 synchronized](#21-synchronized)
|
||||
* [2.2 Lock](#22-lock)
|
||||
* [2.3 BlockingQueue](#23-blockingqueue)
|
||||
* [线程状态](#线程状态)
|
||||
* [线程状态转换](#线程状态转换)
|
||||
* [结束线程](#结束线程)
|
||||
* [1. 阻塞](#1-阻塞)
|
||||
* [2. 中断](#2-中断)
|
||||
* [原子性](#原子性)
|
||||
* [volatile](#volatile)
|
||||
* [1. 内存可见性](#1-内存可见性)
|
||||
* [2. 禁止指令重排](#2-禁止指令重排)
|
||||
* [Java 内存模型与线程](#java-内存模型与线程)
|
||||
* [1. 硬件的效率与一致性](#1-硬件的效率与一致性)
|
||||
* [2. Java 内存模型](#2-java-内存模型)
|
||||
* [2.1 主内存与工作内存](#21-主内存与工作内存)
|
||||
* [2.2 内存间交互操作](#22-内存间交互操作)
|
||||
* [2.3 内存模型三大特性](#23-内存模型三大特性)
|
||||
* [2.3.1 原子性](#231-原子性)
|
||||
* [2.3.2 可见性](#232-可见性)
|
||||
* [2.3.3 有序性](#233-有序性)
|
||||
* [3. 未完待续](#3-未完待续)
|
||||
* [多线程开发良好的实践](#多线程开发良好的实践)
|
||||
* [未完待续](#未完待续)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@ -40,7 +45,6 @@
|
||||
|
||||
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
|
||||
|
||||
|
||||
## 1. 实现 Runnable 接口
|
||||
|
||||
需要实现 run() 方法
|
||||
@ -337,18 +341,30 @@ Producer3 is consuming product made by Consumer3...
|
||||
Producer4 is consuming product made by Consumer4...
|
||||
```
|
||||
|
||||
# 线程状态
|
||||
# 线程状态转换
|
||||
|
||||
JDK 从 1.5 开始在 Thread 类中增添了 State 枚举,包含以下六种状态:
|
||||
1. NEW(新建):创建后尚未启动的线程。
|
||||
2. RUNNABLE(运行):处于此状态的线程有可能正在执行,也有可能正在等待着 CPU 为它分配执行时间。
|
||||
3. BLOCKED(阻塞):阻塞与等待的区别是,阻塞在等待着获取到一个排它锁,这个时间将在另一个线程放弃这个锁的时候发生;而等待则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
|
||||
4. Waiting(无限期等待):处于这种状态的进行不会被分配 CPU 执行时间,它们要等待其它线程显示地唤醒。以下方法会让线程进入这种状态:
|
||||
5. TIMED_WAITING(限期等待):处于这种状态的线程也不会被分配 CPU 执行时间,不过无序等待其它线程显示地唤醒,在一定时间之后它们会由系统自动唤醒。
|
||||
6. TERMINATED(死亡)
|
||||
|
||||
1. **NEW** (新建)
|
||||
2. **RUNNABLE** (当线程正在运行或者已经就绪正等待 CPU 时间片)
|
||||
3. **BLOCKED** (阻塞,线程在等待获取对象同步锁)
|
||||
4. **Waiting** (调用不带超时的 wait() 或 join())
|
||||
5. **TIMED_WAITING** (调用 sleep()、带超时的 wait() 或者 join())
|
||||
6. **TERMINATED** (死亡)
|
||||
以下方法会让线程陷入无限期的等待状态:
|
||||
|
||||
<div align="center"> <img src="../pics//19f2c9ef-6739-4a95-8e9d-aa3f7654e028.jpg"/> </div><br>
|
||||
- 没有设置 Timeout 参数的 Object.wait() 方法
|
||||
- 没有设置 Timeout 参数的 Thread.join() 方法
|
||||
- LockSupport.park() 方法
|
||||
|
||||
以下方法会让线程进入限期等待状体:
|
||||
|
||||
- Thread.sleep()
|
||||
- 设置了 Timeout 参数的 Object.wait() 方法
|
||||
- 设置了 Timeout 参数的 Thread.join() 方法
|
||||
- LockSupport.parkNanos() 方法
|
||||
- LockSupport.parkUntil() 方法
|
||||
|
||||
<div align="center"> <img src="../pics//38b894a7-525e-4204-80de-ecc1acc52c46.jpg"/> </div><br>
|
||||
|
||||
# 结束线程
|
||||
|
||||
@ -387,11 +403,50 @@ interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来
|
||||
|
||||
interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。
|
||||
|
||||
# 原子性
|
||||
# Java 内存模型与线程
|
||||
|
||||
对于除 long 和 double 之外的基本类型变量的读写,可以看成是具有原子性的,以不可分割的步骤操作内存。
|
||||
## 1. 硬件的效率与一致性
|
||||
|
||||
JVM 将 64 位变量(long 和 double)的读写当做两个分离的 32 位操作来执行,在两个操作之间可能会发生上下文切换,因此不具有原子性。可以使用 **volatile** 关键字来定义 long 和 double 变量,从而获得原子性。
|
||||
对处理器上的寄存器进行读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
|
||||
|
||||
每个处理器都有一个高速缓存,但是所有处理器共用一个主内存,因此高速缓存引入了一个新问题:缓存一致性。当多个处理器的运算都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。缓存不一致问题通常需要使用一些协议来解决。
|
||||
|
||||
<div align="center"> <img src="../pics//352dd00d-d1bb-4134-845d-16a75bcb0e02.jpg"/> </div><br>
|
||||
|
||||
除了增加高速缓存之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此,如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java 虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化。
|
||||
|
||||
## 2. Java 内存模型
|
||||
|
||||
Java 虚拟机规范中试图定义一种 Java 内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。在此之前,主流程序语言(如 C/C++等)直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错,因此在某些场景就必须针对不同的平台来编写程序。
|
||||
|
||||
### 2.1 主内存与工作内存
|
||||
|
||||
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。
|
||||
|
||||
Java 内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图所示。
|
||||
|
||||
<div align="center"> <img src="../pics//b02a5492-5dcf-4a69-9b5b-c2298b2cb81c.jpg"/> </div><br>
|
||||
|
||||
### 2.2 内存间交互操作
|
||||
|
||||
Java 内存模型定义了 8 种操作来完成工作内存与主内存之间的交互:一个变量从主内存拷贝到工作内存、从工作内存同步回主内存。虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
|
||||
|
||||
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
|
||||
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
|
||||
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
|
||||
- load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
|
||||
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
|
||||
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
|
||||
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
|
||||
- write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
|
||||
|
||||
### 2.3 内存模型三大特性
|
||||
|
||||
#### 2.3.1 原子性
|
||||
|
||||
除了 long 和 double 之外的基本数据类型的访问读写是具备原子性的。
|
||||
|
||||
Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行,即虚拟机可以不保证 64 位数据类型的 load、store、read 和 write 这 4 个操作的原子性。但是目前各种平台下的商用虚拟机几乎都选择把 64 位数据的读写操作作为原子操作来对待。
|
||||
|
||||
**AtomicInteger、AtomicLong、AtomicReference** 等特殊的原子性变量类提供了下面形式的原子性条件更新语句,使得比较和更新这两个操作能够不可分割地执行。
|
||||
|
||||
@ -409,27 +464,25 @@ public int next() {
|
||||
}
|
||||
```
|
||||
|
||||
原子性具有很多复杂问题,应当尽量使用同步而不是原子性。
|
||||
如果应用场景需要一个更大范围的原子性保证,Java 内存模型还提供了 lock 和 unlock 操作来满足这种需求,尽管虚拟机未把 lock 和 unlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式地使用这两个操作,这两个字节码指令反映到 Java 代码中就是同步块——synchronized 关键字,因此在 synchronized 块之间的操作也具备原子性。
|
||||
|
||||
# volatile
|
||||
#### 2.3.2 可见性
|
||||
|
||||
保证了内存可见性和禁止指令重排,没法保证原子性。
|
||||
可见性是指当一个线程修改了共享变量的值,其他线程能立即得知这个修改。
|
||||
|
||||
## 1. 内存可见性
|
||||
Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是 volatile 变量都是如此,普通变量与 volatile 变量的区别是,volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
|
||||
|
||||
普通共享变量被修改之后,什么时候被写入主存是不确定的。
|
||||
除了 volatile 之外,Java 还有两个关键字能实现可见性,即 synchronized 和 final。同步块的可见性是由“对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)”这条规则获得的,而 final 关键字的可见性是指:被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值。
|
||||
|
||||
volatile 关键字会保证每次修改共享变量之后该值会立即更新到内存中,并且在读取时会从内存中读取值。
|
||||
#### 2.3.3 有序性
|
||||
|
||||
synchronized 和 Lock 也能够保证内存可见性。它们能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。不过只有对共享变量的 set() 和 get() 方法都加上 synchronized 才能保证可见性,如果只有 set() 方法加了 synchronized,那么 get() 方法并不能保证会从内存中读取最新的数据。
|
||||
本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指线程内表现为串行的语义,后半句是指指令重排和工作内存和主内存存在同步延迟的现象。
|
||||
|
||||
## 2. 禁止指令重排
|
||||
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 关键字本身就包含了禁止指令重排序的语义,而 synchronized 则是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。
|
||||
|
||||
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
|
||||
synchronized 关键字在需要这 3 种特性的时候都可以作为其中一种的解决方案,看起来很“万能”。的确,大部分的并发控制操作都能使用 synchronized 来完成。synchronized 的“万能”也间接造就了它被程序员滥用的局面,越“万能”的并发控制,通常会伴随着越大的性能影响。
|
||||
|
||||
volatile 关键字通过添加内存屏障的方式来进制指令重排,即重排序时不能把后面的指令放到内存屏障之前。
|
||||
|
||||
可以通过 synchronized 和 Lock 来保证有序性,它们保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
|
||||
## 3. 未完待续
|
||||
|
||||
# 多线程开发良好的实践
|
||||
|
||||
@ -441,11 +494,10 @@ volatile 关键字通过添加内存屏障的方式来进制指令重排,即
|
||||
- 考虑使用线程池
|
||||
- 最低限度的使用同步和锁,缩小临界区。因此相对于同步方法,同步块会更好。
|
||||
|
||||
# 未完待续
|
||||
|
||||
# 参考资料
|
||||
|
||||
- Java 编程思想
|
||||
- 深入理解 Java 虚拟机
|
||||
- [Java 线程面试题 Top 50](http://www.importnew.com/12773.html)
|
||||
- [Java 面试专题 - 多线程 & 并发编程 ](https://www.jianshu.com/p/e0c8d3dced8a)
|
||||
- [可重入内置锁](https://github.com/francistao/LearningNotes/blob/master/Part2/JavaConcurrent/%E5%8F%AF%E9%87%8D%E5%85%A5%E5%86%85%E7%BD%AE%E9%94%81.md)
|
||||
|
BIN
pics/352dd00d-d1bb-4134-845d-16a75bcb0e02.jpg
Normal file
BIN
pics/352dd00d-d1bb-4134-845d-16a75bcb0e02.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
BIN
pics/38b894a7-525e-4204-80de-ecc1acc52c46.jpg
Normal file
BIN
pics/38b894a7-525e-4204-80de-ecc1acc52c46.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
BIN
pics/b02a5492-5dcf-4a69-9b5b-c2298b2cb81c.jpg
Normal file
BIN
pics/b02a5492-5dcf-4a69-9b5b-c2298b2cb81c.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
Loading…
x
Reference in New Issue
Block a user