并发编程模型两个关键问题
两个关键问题是线程间如何通信以及线程间如何同步。线程间通信有两种:共享内存和消息传递,Java内存模型使用的是共享内存机制。
Java内存模型结构
在Java中,所有实例域、静态域和数组元素(三者都简称为共享变量)都存储在堆中,堆内存与线程之间共享。Java线程间通信由Java内存模型(JMM)控制,线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,存储着共享变量的副本。Java内存模型如下图所示。
线程A和线程B之间需要通信,必须经过两个步骤:1)线程A把本地内存A中更新过的共享变量刷新到主内存中去;2)线程B到主内存中去读取线程A之前已更新过的共享变量。
指令序列重排序
重排序分为3种:
(1)编译器优化重排序:编译器在不改变单线程程序语义前提下,可以重新安排语句执行顺序;
(2)指令级并行重排序:若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
(3)内存系统重排序。
重排序可能会导致多线程程序内存可见性问题。对于编译器,JMM编译器重排序规则会禁止特定类型的编译器重排序;对于处理器,JMM处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障,通过内存屏障指令来禁止特定类型的处理器重排序。
happens-before简介
happens-before顾名思义,就是一系列操作在另一系列操作之前发生,阐述内存可见性。happens-before类型:
(1)程序顺序规则,一个线程中每个操作happens-before于该线程任意后续操作;
(2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁;
(3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读;
(4)传递性:A happens-before B,B happens-before C,那么A happens-before C。
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行,仅仅要求前一个操作(执行的结果)对后一个操作是可见的。
重排序
数据依赖性:若两个操作访问同一个变量,且其中一个为写操作,此时两个操作之间存在数据依赖性。下面是数据依赖性三种类型。
上面的这些,只要改变重排序两个操作顺序,结果就可能不同。由于存在数据依赖性,因此,编译器和处理器不会对这些操作进行指令重排序。但这里说的只是单线程中指令不能重排序,若是多线程中,很可能第二个线程的第二条指令发生在第一个线程的第一个指令之前,此时,尽管有数据依赖,但指令还是被重排了。
as-if-serial语义
as-if-serial语义意思是不管如何重排序,单线程程序的执行结果都不能被改变。程序员感觉程序就是按照顺序执行的一般,但其实内部还是进行了重排序,只不过对于有依赖关系的指令顺序不能改变,因此,程序执行的结果就是准确唯一的。
重排序对多线程的影响
首先看个示例。
class ReorderExample { int a = 0; boolean flag = false; // 线程A public void write() { a = 1; // 1 flag = true; // 2 } // 线程B public void read() { if(flag) { //3 int i = a * a; // 4 } }}
由于操作1和操作2没有数据依赖性,而操作3和操作4也没有数据依赖性,因此可以对其进行重排序。首先看看操作1和操作2重排序会产生什么效果呢?如下图所示。线程A首先写变量flag为true,判断后执行a * a,而此时a为0,这里多线程语义被破坏。
而操作3和操作4会发生什么情况呢?如下图所示,操作3和操作4存在控制依赖性,执行线程B的处理器可以提前读取并计算a * a,然后把结果放置在重排序缓冲中,当操作3为真,就把该计算结果写入变量i中。这里的重排序也破坏了多线程语义。在单线程中,对存在控制依赖的操作重排序,不会改变结果;但在多线程中,很可能会改变结果。
顺序一致性
顺序一致性内存模型是个理论参考模型。
数据竞争和顺序一致性:当程序未正确同步时,会产生数据竞争。数据竞争即在一个线程中写一个变量,在另一个线程中读同一个变量,而且写和读没有通过同步来排序。若程序是正确同步的,程序的执行将具有顺序一致性。
顺序一致性内存模型:理想化的参考模型,为程序员提供了极强的内存可见性保证。一个线程中所有操作都必须按照程序顺序来执行;所有线程都只能看到一个单一的操作执行顺序,每个操作都必须原子执行且立刻对所有线程可见。宏观上,它就是对所有线程挨个串行化执行。
假设有两个线程A和B,并发执行。线程A操作顺序为A1->A2->A3,线程B操作顺序为B1->B2->B3。假设两个线程使用监视器锁来正确同步,执行顺序如下图所示。
现在假设两个线程没有进行同步,则操作顺序可能如下图所示。
未同步程序在顺序一致性模型中虽然整体上执行顺序无序,但所有线程都只看到一个一致性的整体执行顺序。如上图所示,就是B1->A1->A2->B2->A3->B3。之所以得到这个保证是因为顺序一致性内存模型中每个操作必须立即对任意线程可见。
但在JMM中没有这个保证,未同步程序不但在JMM中整体上无序,而且所有线程看到的操作顺序也可能不一致。
同步程序的顺序一致性效果:下面对上面的程序进行加锁,观察同步程序如何实现顺序一致性。
class SynchronizedExample { int a = 0; boolean flag = false; // 线程A public synchronized void write() { // 获取锁 a = 1; flag = true; } // 释放锁 // 线程B public synchronized void read() { // 获取锁 if(flag) { int i = a * a; } } // 释放锁}
下面是执行时序图。
顺序一致性模型中,所有程序按照程序顺序串行执行,而在JMM中,临界区的代码可以重排序,但JMM不允许临界区内代码逃逸到外面。虽然线程A临界区内做了重排序,但由于监视器互斥执行特性,线程B根本无法“观察”到线程A在临界区内的重排序。
未同步程序执行特性:对于未同步程序,JMM只提供最小安全性——线程读取到的值要么是之前某个程序写入的值要么是默认值(0,null,false)。JMM不保证未同步程序执行结果与该程序一致性模型中执行结果一致。因为若想要保证执行结果一致,需要禁止大量处理器和编译器优化。
未同步程序在JMM中执行时,整体上是无序的,其执行结果无法预知。未同步程序在顺序一致性模型中和JMM模型的差异如下:
(1)顺序一致性模型保证单线程内操作会按顺序执行,但JMM不能保证,例如同步的多线程程序在临界区做了指令重排;
(2)顺序一致性模型保证所有程序只能看到一致的操作执行顺序,但JMM不保证;
(3)JMM不保证对64位long型和double型变量的写操作原子性,而顺序一致性模型可以保证。
volatile内存语义
可见性:对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入,也即任意线程对此volatile变量的写之后其他任意线程都能感知到;
不具有原子性:对任意单个volatile变量单一操作具有原子性,但类似++这样的复合操作不具有原子性;
禁止指令重排。
volatile写和锁的释放有相同的内存语义,volatile读与锁的获取有相同的内存语义。volatile写-读的happens-before关系是写happens-before读,并且volatile写-读的内存语义就是通过主内存进行读写通信(可查看前面volatile文章详细介绍)。
volatile内存语义的实现是通过编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
在旧的内存模型中,volatile的写-读没有锁的释放-获取所具有的内存语义。为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。
锁的内存语义
锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一锁的线程发送消息。
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新回主内存;当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息;线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息;线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。锁内存语义的实现:下面借助ReentrantLock源代码,分析锁内存语义具体实现机制。ReentrantLock中,调用lock()加锁,unlock()释放锁。ReentrantLock实现依赖于AbstractQueuedSynchronizer(AQS,抽象队列同步器),使用一个整型volatile变量(state,状态)维护同步状态。公平锁加锁方法lock()调用轨迹:1)ReentrantLock:lock();2)FairSync:lock();3)AbstractQueuedSynchronizer:acquire(int arg);4)ReentrantLock:tryAcquire(int acquires)。第4步开始真正加锁,下面是该方法源代码,加锁方法首先读volatile变量。
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false;}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
调用解锁方法:1)ReentrantLock:unlock();2)AbstractQueuedSynchronizer:release(int arg);3)Sync:tryRelease(int releases)。第3步才真正解锁,下面是该方法源码。
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); // 释放锁最后写volatile变量state return free;}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // 释放锁最后写volatile变量state
return free;
}
公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。具体的ReentrantLock源码分析后面会专门开辟文章来讲解。java.util.concurrent包的实现:由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信具有以下四种方式:1)A线程写volatile变量,随后B线程读这个volatile变量;2)A线程写volatile变量,随后B线程用CAS更新这个volatile变量;3)A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量;4)A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。concurrent包下的类的通用实现形式:1)首先,声明共享变量为volatile;2)然后,使用CAS的原子条件更新来实现线程之间的同步;3)同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下所示。
final域内存语义
final域重排序规则:1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序;2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。样例:一个线程执行write()方法,一个线程执行read()方法。
public class FinalExample { int i; final int j; static FinalExample obj; public FinalExample() { i = 1; j = 2; } public static void write() { obj = new FinalExample(); } public static void read() { FinalExample object = obj; int a = object.i; int b = object.j; }}
public class FinalExample {
int i;
final int j;
static FinalExample obj;
public FinalExample() {
i = 1;
j = 2;
}
public static void write() {
obj = new FinalExample();
}
public static void read() {
FinalExample object = obj;
int a = object.i;
int b = object.j;
}
}
写final域重排序规则:写final域的重排序规则禁止把final域的写重排序到构造函数之外,包含两条:1)JMM禁止编译器把final域的写重排序到构造函数之外;2)编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障;这个屏障禁止处理器把final域的写重排序到构造函数之外。以下图为例,假设线程B读取对象引用与读对象成员域之间没有重排序。
写普通域的操作被编译器重排序到了构造函数之外,读线程B错误地读取了普通变量i初始化之前的值;而写final域的操作,被写final域的重排序规则“限定”在了构造函数之内,读线程B正确地读取了final变量初始化之后的值;写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。读final域的重排序规则:在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器),编译器会在读final域操作的前面插入一个LoadLoad屏障。下图是可能的一种执行顺序。
读对象的普通域的操作被处理器重排序到读对象引用之前,读普通域时,该域还没有被写线程A写入,这是一个错误的读取操作;而读final域的重排序规则会把读对象final域的操作“限定”在读对象引用之后,此时该final域已经被A线程初始化过了,这是一个正确的读取操作;读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。final域为引用类型:在构造函数内对一个final引用的对象的成员域
的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。final引用不能从构造函数内“逸出”。通过为final域增加写和读重排序规则,可以为Java程序员提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(指lock和volatile的使用)就可以保证任意线程 都能看到这个final域在构造函数中被初始化之后的值。