并发编程中, 线程之间如何通信及线程之间如何同步, 通信是指线程之间以何种机制来交换
信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
内存模型的抽象
java中,所有的实例域,静态域和数组元素存储在堆内存中. 局部变量,方法定义参数和异常处理参数定义栈内存中,他们不会有内存可见性问题,不受内存模型影响.
共享内存,只的是共享变量存储在主内存中,但是每一个线程都有一个私有的本地内存.
本地内存中存储了该线程 读/写 共享变量的副本.
A B 两个线程进行通信需要, A 更新本地缓存, A将共享变量刷新到主存中,
B 去主存中拉去 A 已经更新后的共享变量.
JMM (Java内存模型) 提供内存可见性保证.
源代码的重排序
执行程序时为提高性能,编译器会对指令做重排序
- 编译器优化重排序 2.指令级并行重排序 3. 内存系统重排序
2,3 属于处理器重排序,处理器重排序,JMM会要求Jav编译器在生成指令序列的时候,插入特定类型的内存屏障指令, 通过内存屏障指令来禁止特定类型的处理器重排序.
happens-before
从 JDK5 开始,java 使用新的 JSR -133 内存模型(本文除非特别说明,针对的都
是 JSR- 133 内存模型)。JSR-133 使用 happens-before 的概念来阐述操作之间
的内存可见性。在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那
么这两个操作之间必须要存在 happens-before 关系。这里提到的两个操作既可以
是在一个线程之内,也可以是在不同线程之间。
程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
监视器锁规则:对一个监视器的解锁,happens- before 于随后对这个监视器
的加锁。volatile 变量规则:对一个 volatile 域的写,happens- before 于任意后续对
这个 volatile 域的读。传递性:如果 A happens- before B,且 B happens- before C,那么 A
happens- before C。
volatile
我们把对 volatile 变量的单个读/写,看成是使用同一个锁对这些单
个读/写操作做了同步
class VolatileFeaturesExample {
volatile long vl = 0L;
//使用 volatile 声明 64 位的 long 型变量
public void set(long l) {
vl = l;
//单个 volatile 变量的写
}
public void getAndIncrement () {
vl++;
//复合(多个)volatile 变量的读/写
}
public long get() {
return vl;
//单个 volatile 变量的读
}
}
假设有多个线程分别调用上面程序的三个方法,实际变为:
class VolatileFeaturesExample {
long vl = 0L;
public synchronized void set(long l) {
// 64 位的 long 型普通变量
//对单个的普通变量的写用同一个
锁同步
vl = l;
}
public void getAndIncrement () {
long temp = get();
//普通方法调用
//调用已同步的读方法
temp += 1L; //普通写操作
set(temp); //调用已同步的写方法
}
public synchronized long get() {
//对单个的普通变量的读用同一个
锁同步
return vl;
}
}
volatile重排序
当第二个操作是 volatile写时, 不管第一个操作是什么,都不能重排序.
第一个操作是volatile读时,不管第二个操作是什么都不能重排序
第一个操作是volatile写,第二个操作是volatile读时,不能重排序
在每个 volatile 写操作的前面和后面插入一个 StoreStore 屏障。
主要防止: 禁止上面的普通写和下面的volatile 写重排序,防止上面的
volatile 写与下面可能有的 volatile读/写重排序。
锁
class Monitor Example {
int a = 0;
public synchronized void writer() {
a++;
}
//1
//2
//3
public synchronized void reader() {
int i = a;
//4
//5
......
}
//6
}
锁释放时: JMM 会把该线程对应的本地内存中的共享变量刷新到主内存
中。
获取锁时: JMM 会把该线程对应的本地内存置为无效。从而使得被监视器
保护的临界区代码必须要从主内存中去读取共享变量。
class ReentrantLockExample {
int a = 0;
ReentrantLock lock = new ReentrantLock();
public void writer() {
lock.lock();
//获取锁
try {
a++;
} finally {
lock.unlock(); //释放锁
}
}
public void reader () {
lock.lock();
//获取锁
try {
int i = a;
......
} finally {
lock.unlock(); //释放锁
}
}
}
在 ReentrantLock 中,调用 lock()方法获取锁;调用 unlock()方法释放锁。
ReentrantLock 的实现依赖于 java 同步器框架 AbstractQueuedSynchronizer
(本文简称之为 AQS)。AQS 使用一个整型的 volatile 变量(命名为 state)来维
护同步状态,马上我们会看到,这个 volatile 变量是 ReentrantLock 内存语义实现
的关键。
ReentrantLock 分为公平锁和非公平锁,我们首先分析公平锁。
加锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread. currentThread ();
int c = getState();
//获取锁的开始,首先读 volatile 变量 state
if (c == 0) {
if (isFirst(current) &&
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 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 变量后将立即变的对获取锁的
线程可见。
核心实现
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
compareAndSet() 方法调用简称为
CAS。JDK 文档对该方法的说明如下: 如果当前状态值等于预期值,则以原子方式将同步
状态设置为给定的更新值。
ReentrantLock 的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方
式:
利用 volatile 变量的写-读所具有的内存语义。
利用 CAS 所附带的 volatile 读和 volatile 写的内存语义。
concurrent 包的实现
Java 线程之间的通信现在有了下面四种方式:
- A 线程写 volatile 变量,随后 B 线程读这个 volatile 变量。
- A 线程写 volatile 变量,随后 B 线程用 CAS 更新这个 volatile 变量。
- A 线程用 CAS 更新一个 volatile 变量,随后 B 线程用 CAS 更新这个 volatile变量。
- A 线程用 CAS 更新一个 volatile 变量,随后 B 线程读这个 volatile 变量。
把这些特性整合在一起,就形成了整个 concurrent 包得以实现的基石。
分析 concurrent 包的源代码实现,会发现一个通用化的实现模式:
- 首先,声明共享变量为 volatile;
- 然后,使用 CAS 的原子条件更新来实现线程之间的同步;
- 同时,配合以 volatile 的
AQS,非阻塞数据结构和原子变量类
finall 实现
对于 final 域,编译器和处理器要遵守两个重排序规则:
- 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一
个引用变量,这两个操作之间不能重排序。 - 初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操
作之间不能重排序。
public class FinalExample {
int i; //普通变量
final int j; //final 变量
static FinalExample obj;
public void FinalExample () {
//构造函数
i = 1; //写普通域
j = 2; //写 final 域
}
public static void writer () {
//写线程 A 执行
obj = new FinalExample ();
}
public static void reader () {
//读线程 B 执行FinalExample object = obj;
//读对象引用
int a = object.i; //读普通域
int b = object.j; //读 final 域
}
}