auto commit
This commit is contained in:
parent
4982bea3f4
commit
1bbc3cd9f5
161
notes/Java 并发.md
161
notes/Java 并发.md
|
@ -20,17 +20,24 @@
|
|||
* [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-有序性)
|
||||
* [2.4 先行发生原则](#24-先行发生原则)
|
||||
* [3. 未完待续](#3-未完待续)
|
||||
* [3. 主内存与工作内存](#3-主内存与工作内存)
|
||||
* [4. 内存间交互操作](#4-内存间交互操作)
|
||||
* [5. 内存模型三大特性](#5-内存模型三大特性)
|
||||
* [5.1 原子性](#51-原子性)
|
||||
* [5.2 可见性](#52-可见性)
|
||||
* [5.3 有序性](#53-有序性)
|
||||
* [6. 先行发生原则](#6-先行发生原则)
|
||||
* [线程安全](#线程安全)
|
||||
* [1. Java 语言中的线程安全](#1-java-语言中的线程安全)
|
||||
* [1.1 不可变](#11-不可变)
|
||||
* [1.2 绝对线程安全](#12-绝对线程安全)
|
||||
* [1.3 相对线程安全](#13-相对线程安全)
|
||||
* [1.4 线程兼容](#14-线程兼容)
|
||||
* [1.5 线程对立](#15-线程对立)
|
||||
* [2. 线程安全的实现方法](#2-线程安全的实现方法)
|
||||
* [多线程开发良好的实践](#多线程开发良好的实践)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
@ -404,7 +411,7 @@ interrupted() 方法在检查完中断状态之后会清除中断状态,这样
|
|||
- LockSupport.parkNanos() 方法
|
||||
- LockSupport.parkUntil() 方法
|
||||
|
||||
# Java 内存模型与线程
|
||||
# 内存模型
|
||||
|
||||
## 1. 硬件的效率与一致性
|
||||
|
||||
|
@ -420,7 +427,7 @@ interrupted() 方法在检查完中断状态之后会清除中断状态,这样
|
|||
|
||||
Java 虚拟机规范中试图定义一种 Java 内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。在此之前,主流程序语言(如 C/C++等)直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错,因此在某些场景就必须针对不同的平台来编写程序。
|
||||
|
||||
### 2.1 主内存与工作内存
|
||||
## 3. 主内存与工作内存
|
||||
|
||||
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。
|
||||
|
||||
|
@ -428,7 +435,7 @@ Java 内存模型规定了所有的变量都存储在主内存(Main Memory)
|
|||
|
||||
<div align="center"> <img src="../pics//b02a5492-5dcf-4a69-9b5b-c2298b2cb81c.jpg"/> </div><br>
|
||||
|
||||
### 2.2 内存间交互操作
|
||||
## 4. 内存间交互操作
|
||||
|
||||
Java 内存模型定义了 8 种操作来完成工作内存与主内存之间的交互:一个变量从主内存拷贝到工作内存、从工作内存同步回主内存。虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
|
||||
|
||||
|
@ -441,9 +448,9 @@ Java 内存模型定义了 8 种操作来完成工作内存与主内存之间的
|
|||
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
|
||||
- write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
|
||||
|
||||
### 2.3 内存模型三大特性
|
||||
## 5. 内存模型三大特性
|
||||
|
||||
#### 2.3.1 原子性
|
||||
### 5.1 原子性
|
||||
|
||||
除了 long 和 double 之外的基本数据类型的访问读写是具备原子性的。
|
||||
|
||||
|
@ -467,7 +474,7 @@ public int next() {
|
|||
|
||||
如果应用场景需要一个更大范围的原子性保证,Java 内存模型还提供了 lock 和 unlock 操作来满足这种需求,尽管虚拟机未把 lock 和 unlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式地使用这两个操作,这两个字节码指令反映到 Java 代码中就是同步块——synchronized 关键字,因此在 synchronized 块之间的操作也具备原子性。
|
||||
|
||||
#### 2.3.2 可见性
|
||||
### 5.2 可见性
|
||||
|
||||
可见性是指当一个线程修改了共享变量的值,其他线程能立即得知这个修改。
|
||||
|
||||
|
@ -475,7 +482,7 @@ Java 内存模型是通过在变量修改后将新值同步回主内存,在变
|
|||
|
||||
除了 volatile 之外,Java 还有两个关键字能实现可见性,即 synchronized 和 final。同步块的可见性是由“对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)”这条规则获得的,而 final 关键字的可见性是指:被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值。
|
||||
|
||||
#### 2.3.3 有序性
|
||||
### 5.3 有序性
|
||||
|
||||
本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指线程内表现为串行的语义,后半句是指指令重排和工作内存和主内存存在同步延迟的现象。
|
||||
|
||||
|
@ -483,7 +490,7 @@ Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之
|
|||
|
||||
synchronized 关键字在需要这 3 种特性的时候都可以作为其中一种的解决方案,看起来很“万能”。的确,大部分的并发控制操作都能使用 synchronized 来完成。synchronized 的“万能”也间接造就了它被程序员滥用的局面,越“万能”的并发控制,通常会伴随着越大的性能影响。
|
||||
|
||||
### 2.4 先行发生原则
|
||||
## 6. 先行发生原则
|
||||
|
||||
如果 Java 内存模型中所有的有序性都只靠 volatile 和 synchronized 来完成,那么有一些操作将会变得很繁琐,但是我们在编写 Java 并发代码的时候并没有感觉到这一点,这是因为 Java 语言中有一个“先行发生”(Happen-Before) 的原则。这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依据。依靠这个原则,我们可以通过几条规则一次性地解决并发环境下两个操作之间是否可能存在冲突的所有问题。
|
||||
|
||||
|
@ -506,7 +513,7 @@ k = 2;
|
|||
- 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
|
||||
- volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
|
||||
- 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法先行发生于此线程的每一个动作。
|
||||
- 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
|
||||
- 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值等手段检测到线程已经终止执行。
|
||||
- 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测到是否有中断发生。
|
||||
- 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
|
||||
- 传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。
|
||||
|
@ -538,7 +545,123 @@ int j = 2;
|
|||
|
||||
上面两个例子综合起来证明了一个结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。
|
||||
|
||||
## 3. 未完待续
|
||||
# 线程安全
|
||||
|
||||
《Java Concurrency In Practice》的作者 Brian Goetz 对“线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的”。
|
||||
|
||||
这个定义比较严谨,它要求线程安全的代码都必须具备一个特征:代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己采取任何措施来保证多线程的正确调用。这点听起来简单,但其实并不容易做到,在大多数场景中,我们都会将这个定义弱化一些,如果把“调用这个对象的行为”限定为“单次调用”,这个定义的其他描述也能够成立的话,我们就可以称它是线程安全了,为什么要弱化这个定义,现在暂且放下,稍后再详细探讨。
|
||||
|
||||
## 1. Java 语言中的线程安全
|
||||
|
||||
我们这里讨论的线程安全,就限定于多个线程之间存在共享数据访问这个前提,因为如果一段代码根本不会与其他线程共享数据,那么从线程安全的角度来看,程序是串行执行还是多线程执行对它来说是完全没有区别的。
|
||||
|
||||
为了更加深入地理解线程安全,在这里我们可以不把线程安全当做一个非真即假的二元排他选项来看待,按照线程安全的“安全程度”由强至弱来排序,我们可以将 Java 语言中各种操作共享的数据分为以下 5 类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
|
||||
|
||||
### 1.1 不可变
|
||||
|
||||
在 Java 语言中(特指 JDK 1.5 以后,即 Java 内存模型被修正之后的 Java 语言),不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施,只要一个不可变的对象被正确地构建出来(没有发生 this 引用逃逸的情况),那其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最简单和最纯粹的。
|
||||
|
||||
Java 语言中,如果共享数据是一个基本数据类型,那么只要在定义时使用 final 关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行,不妨想一想 java.lang.String 类的对象,它是一个典型的不可变对象,我们调用它的 substring()、replace() 和 concat() 这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
|
||||
|
||||
保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为 final,这样在构造函数结束之后,它就是不可变的。
|
||||
|
||||
在 Java API 中符合不可变要求的类型,除了上面提到的 String 之外,常用的还有枚举类型,以及 java.lang.Number 的部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型;但同为 Number 的子类型的原子类 AtomicInteger 和 AtomicLong 则并非不可变的。
|
||||
|
||||
### 1.2 绝对线程安全
|
||||
|
||||
绝对的线程安全完全满足 Brian Goetz 给出的线程安全的定义,这个定义其实是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”通常需要付出很大的,甚至有时候是不切实际的代价。在 Java API 中标注自己是线程安全的类,大多数都不是绝对的线程安全。我们可以通过 Java API 中一个不是“绝对线程安全”的线程安全类来看看这里的“绝对”是什么意思。
|
||||
|
||||
如果说 java.util.Vector 是一个线程安全的容器,相信所有的 Java 程序员对此都不会有异议,因为它的 add()、get() 和 size() 这类方法都是被 synchronized 修饰的,尽管这样效率很低,但确实是安全的。但是,即使它所有的方法都被修饰成同步,也不意味着调用它的时候永远都不再需要同步手段了。
|
||||
|
||||
```java
|
||||
private static Vector<Integer> vector = new Vector<Integer>();
|
||||
|
||||
public static void main(String[] args) {
|
||||
while (true) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
vector.add(i);
|
||||
}
|
||||
|
||||
Thread removeThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (int i = 0; i < vector.size(); i++) {
|
||||
vector.remove(i);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Thread printThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (int i = 0; i < vector.size(); i++) {
|
||||
System.out.println((vector.get(i)));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
removeThread.start();
|
||||
printThread.start();
|
||||
|
||||
//不要同时产生过多的线程,否则会导致操作系统假死
|
||||
while (Thread.activeCount() > 20);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
Exception in thread"Thread-132"java.lang.ArrayIndexOutOfBoundsException:
|
||||
Array index out of range:17
|
||||
at java.util.Vector.remove(Vector.java:777)
|
||||
at org.fenixsoft.mulithread.VectorTest$1.run(VectorTest.java:21)
|
||||
at java.lang.Thread.run(Thread.java:662)
|
||||
```
|
||||
|
||||
很明显,尽管这里使用到的 Vector 的 get()、remove() 和 size() 方法都是同步的,但是在多线程的环境中,如果不在方法调用端做额外的同步措施的话,使用这段代码仍然是不安全的,因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号 i 已经不再可用的话,再用 i 访问数组就会抛出一个 ArrayIndexOutOfBoundsException。如果要保证这段代码能正确执行下去,我们不得不把 removeThread 和 printThread 的定义改成如下所示的样子:
|
||||
|
||||
```
|
||||
Thread removeThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (vector) {
|
||||
for (int i = 0; i < vector.size(); i++) {
|
||||
vector.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Thread printThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (vector) {
|
||||
for (int i = 0; i < vector.size(); i++) {
|
||||
System.out.println((vector.get(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 1.3 相对线程安全
|
||||
|
||||
相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
|
||||
|
||||
在Java语言中,大部分的线程安全类都属于这种类型,例如Vector、HashTable、Collections的synchronizedCollection() 方法包装的集合等。
|
||||
|
||||
### 1.4 线程兼容
|
||||
|
||||
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API中大部分的类都是属于线程兼容的,如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。
|
||||
|
||||
### 1.5 线程对立
|
||||
|
||||
线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于Java语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
|
||||
|
||||
一个线程对立的例子是Thread类的suspend()和resume()方法,如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都是存在死锁风险的,如果suspend()中断的线程就是即将要执行resume()的那个线程,那就肯定要产生死锁了。也正是由于这个原因,suspend()和resume()方法已经被JDK声明废弃(@Deprecated)了。常见的线程对立的操作还有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。
|
||||
|
||||
## 2. 线程安全的实现方法
|
||||
|
||||
|
||||
|
||||
# 多线程开发良好的实践
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user