第3章| Java内存模型——03同步原语

volatile的特性

tips: 为好理解volatile特性,可将对volatile变量的单个读/写看成是使用同一个锁对这些单个读/写操作做了同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
示例代码:
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变量的读
}
}

当多线程调用上面的3个方法时,上述方法等同于下面实现
class VolatileFeaturesExample1 {
long vl = 0L; // 64位的long型普通变量

public synchronized void set(long l) {//对单个的普通变量的写用同一个锁同步
vl = l;
}

public void getAndIncrement() { //普通方法调用
long temp = get(); //调用已同步的读方法
temp += 1L; //普通写操作
set(temp); //调用已同步的写方法
}

public synchronized long get() { //对单个的普通变量的读用同一个锁同步
return vl;
}
}

锁的happens-before规则保证释放锁和获取锁的两个线程之间内存可见,意味着对一个volatile变量的读,总能看到对这个变量最后的写入。

volatile变量自身具有两个特性:

  1. 可见性:一个volatile变量的读,总能看到对这个变量最后的写入;
  2. 原子性: 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

volatile写-读建立的happens-before关系

从jdk1.5开始,volatile变量的读-写可以实现线程之间的通信
从内存语义角度来说,volatile的写-读与锁的释放-获取具有相同的内存效果(写->释放,读->获取)。

volatile写-读的内存语义

写-内存语义当写一个volatile变量时,JMM会把该线程对应的本地内存中共享变量值刷新到主内存
读-内存语义当读一个volatile变量时,JMM会把该线程本地内存置为无效。线程接下来将从主内存中读取共享变量

volatile内存语义的实现

volatile重排序规则表

是否能重排序 第二个操作
第一个操作 普通读/写 volatile读 volatile写
普通读/写 NO
volatile读 NO NO NO
volatile写 NO NO
  1. 为实现volatile的内存语义,编译器生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。保守策略的JMM内存屏障插入策略:
  • 在每个volatile写操作前面插入一个StoreStore屏障;
  • 在每个volatile写操作后面插入一个StoreLoad屏障;
  • 在每个volatile读操作后面插入一个LoadLoad屏障;
  • 在每个volatile读操作后面插入一个LoadStore屏障;

保守策略下,volatile写插入内存屏障后生成的指令序列示意图如下:
指令序列示意图-写

保守策略下,volatile读插入内存屏障后生成的指令序列示意图如下:
指令序列示意图-读

锁的内存语义

锁是Java并发编程中最重要的同步机制,它除了让临界区互斥执行外,还可以让释放锁的线程向获取锁的另一个线程发送消息。

锁内存语义的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
示例代码
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,该框架使用一个整形的volatile变量来维护同步状态,具体ReentrantLock类图如下:
 ReentrantLock的类图

ReentrantLock分为公平锁和非公平锁,内存语义如下:

  • 公平锁和非公平锁释放时,最后都要写一个volatile变量state;
  • 公平锁获取时,首先会去读取volatile变量;
  • 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读/写的内存语义。

同样对reentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少由以下两种方式:

  1. 利用volatile变量的读-写所有具有的内存语义;
  2. 利用CAS所附带的volatile读和volatile写的内存语义。

concurrent包的实现

Java线程之间的通信由以下4种方式

  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包的实现示意图如下:
concurrent包的实现示意图

final域的内存语义

final域的重排序规则

对于final域,编译器和处理器遵守以下两个重排序规则:

  1. 在构造函数内,对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这个操作之间不能重排序;
  2. 初次读一个包含final域的对象的引用,与随后读这个final域的引用,这个操作之间不能重排序
码哥 wechat
欢迎关注个人订阅号:「码上行动GO」