目录
前言:
上一部分我们学习了顺序一致性内存模型和volatile的内存语义,这一部分开始我们来学习锁的内存语义和final域的内存语义
1. 锁的内存语义
1.1锁的释放-获取 happens-before
锁是java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一锁的线程发送消息。
下面还是来看代码
public class MonitorTest01 {
int a = 0;
public void writer() {//1
a = 1; //2
} //3
public void reader() {//4
int i = a; //5
...
} //6
}
假设有两个线程A和B,A线程执行write方法,B线程执行reader方法,根据happens-before规则,会出现三种happens-before情况
- 根据次序规则:1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6。
- 根据监视器规则:3 happens before 4
- 根据happens-before的传递性,2 happens-before 5
因此,A线程在释放锁之前所有可见的共享变量,在B线程获取同一个锁之后将立即对B线程可见。
1.2 锁的释放和获取的内存语义
当线程释放锁的时候,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中,如图所示
线程获取锁时,JMM会把该线程对应的本地内存置为无效。使得被监视器保护的临界区代码必须从主存中读取共享变量。如图所示锁获取的状态示意
可以看出锁获取和volatile读有相同的语义,锁释放和volatile写有相同的语义。对锁释放和锁获取的总结如下
- 线程A释放锁,实际上是线程A通知接下来要获取这个锁的线程:我对共享变量做了修改
- 线程B获得锁,实际上是线程B接收了之前某个线程发出的释放这个锁之前对共享变量做了修改的通知
- 线程A释放锁,线程B随后获得锁,实质上是线程A通过主内存向线程B发送消息
1.3 锁内存语义的实现
下面我们借助ReentrantLock可重入锁来分析锁内存语义的实现,在开始之前我们先来了解一下什么是ReentrantLock,ReentrantLock是java中和synchronized齐名的锁,ReentrantLock锁和synchronized一样属于可重入锁,ReentrantLock可以通过构造函数来设置公平锁或者非公平锁,而synchronized只能实现非公平锁(公平锁就是先等待的线程先获得锁)。ReentrantLock需要通过显式的lock()和unlock()来加锁和解锁。ReentrantLock基于jdk实现,jdk中有一个同步器框架AbtractQueuedSynchronized(AQS),AQS使用一个整形的volatile变量来维护同步状态。ReentrantLock可以相应中断,ReentrantLock提供了一个Condition类,用来实现分组唤醒需要唤醒的线程们,而Synchronized只能随机唤醒一个线程,或者唤醒全部线程。
ReentrantLock的简单类图如下
先分析公平锁, 使用公平锁时,加锁方法lock()实际调用的方法是tryAcquire(int acquires),下面是tryAcquire的源代码
/**
* The synchronization state.
*/
private volatile int state;
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();//获取锁的开始,先读取volatile变量state
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;
}
公平锁的解锁方法unlock(),实际调用的方法是tryRelease(int releases),tryRelease方法的源代码如下
/**
* The synchronization state.
*/
private volatile int state;
/**
* Sets the value of synchronization state.
* This operation has memory semantics of a {@code volatile} write.
* @param newState the new state value
*/
protected final void setState(int newState) {
state = newState;
}
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;
}
我们再来看看非公平锁的内存语义实现,非公平锁的释放和公平锁一样,所以只看获取锁的lock()方法,在非公平锁时调用加锁方法lock(),实际调用的方法是AQS中的compareAndSetState(int expect, int update),compareAndSetState方法的源代码如下
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
可以看到调用了unsafe类下的CAS原子操作方法,unsafe类中的compateAndSwapInt是个本地方法,jdk文档中对这个方法的说明是这样的:如果当前值等于预期值,则以原子方式将同步状设置为给定的更新值。此操作有volatile读和写的语义。
由于本地方法调用的是c++的本地方法库,这里就不展开叙述了,现在对公平锁和非公平锁的内存语义做个总结
- 公平锁和非公平锁释放时,最后都要写一个volatile变量state
- 公平锁加锁时,会先获取volatile变量state
- 非公平锁加锁时,会先用cas更新volatile变量,这个操作同时具有volatile读和volatile写的语义
1.4 concurrent包的实现
由于java的CAS同时具有volatile读和volatile写的内存语义,所以java线程之间通信有4种方式
- A线程写volatile变量,B线程随后读
- A线程写volatile变量,B线程随后用CAS更新这个变量
- A线程用CAS更新一个volatile变量,B线程随后用CAS更新这个变量
- A线程用CAS更新一个volatile变量,B线程随后读这个变量
concurrent保重的代码都有一个通用的实现方式,就是先声明共享变量为volatile,然后用CAS原子更新来实现线程之间同步,同时以volatile读/写和CAS的内存语义来实现线程间的通信。
2. final域的内存语义
2.1 final域的重排序规则
对于final域,编译器和处理器要遵守两个重排序规则。
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,两个操作之间不能重排序。
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
下面用一个例子来说明这两个规则
public class Test {
int i;//普通变量
final int j;//final变量
static Test obj;
public Test() {
i = 1;
j = 2;//写final域
}
public static void writer() { //写线程A执行
obj = new Test();
}
public static void reader() { //读线程B执行
Test object = obj; //读对象引用
int a = obj.i; //读普通域
int b = obj.j; //读final域
}
}
1. 写final域的重排序规则
写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现主要由两部分组成,首先,JMM禁止编译器把final域的写重排序到构造函数之外。然后编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。
2. 读final域的重排序规则
读final域的重排序规则是在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM会禁止这两个操作之间的重排序。编译器会在读final域操作的前面插入一个LoadLoad屏障。
总结
本部分我们一起学习了锁的内存语义和final域的内存语义,下一部分我们将继续学习双重检查与延迟初始化的内容。