文章目录
- 一、JMM内存模型
- 1、什么是JMM
- (1)参考资料
- 2、竞态条件(Race Condition)
- (1)实例
- 3、同步动作(Synchronization Order)
- (1)实例
- (2)实例:SO 并不是阻止多线程切换
- (3)实例:volatile 只用了一半算 SO 吗?
- ① 实例1
- ② 实例2
- ③ 实例3
- 4、Happens-Before
- (1)具体规则
- (2)案例
- 5、因果律(Causality)
- 6、安全发布
- 二、内存屏障
- 1、LoadLoad
- 2、LoadStore
- 3、StoreStore
- 4、StoreLoad(*)
- 5、Acquire
- 6、Release
- 7、小总结
- 三、volatile
- 1、volatile的本质
- 2、可见性(visibility)
- (1)分析所有可能性
- (2)解决方案
- 3、有序性: 共享变量部分被volatile修饰:partial ordering
- (1)实例:重排序导致不可预料的结果
- (2)解决方案:volatile修饰y
- (3)分析:volatile 修饰 x - 行不行
- (4)总结
- 4、有序性:共享变量全部被volatile修饰:total ordering
- (1)实例:重排序导致不可预料的结果
- (2)volatile 仅修饰 y - 不符合最后写最先读
- (3)volatile 修饰 x 和 y - 不符合最后写最先读
- 四、Synchronized
- 1、Synchronized的本质
- 2、monitorenter 与 monitorexit 工作原理
- 3、Synchronized内存屏障
- 4、Synchronized正确使用
- 5、Synchronized的优化
- 6、无锁 vs 有锁
- 五、VarHandle
一、JMM内存模型
1、什么是JMM
A memory model describes, given a program and an execution trace of that program, whether the execution trace is a legal execution of the program. A high level, informal overview of the memory model shows it to be a set of rules for when writes by one thread are visible to another thread.
简单理解JMM内存模型:多线程下,共享变量的读写顺序是头等大事,内存模型就是多线程下对共享变量的一组读写规则
。
也就是说,JMM主要关注共享变量是否在线程间同步
、代码可能得执行顺序
。也就是我们常说的,可见性与指令重排序的问题。
需要关注的操作就有两种:Load、Store:
Load就是从缓存读取到寄存器中,如果一级缓存中没有,就会层层读取二级、三级缓存,最后才是Memory。
Store 就是从寄存器运算结果写入缓存,不会直接写入 Memory,当 Cache line 将被 eject 时,会writeback 到 Memory。
(1)参考资料
Java Language Specification Chapter 17. Threads and Locks
JSR-133: JavaTM Memory Model and Thread Specification
Doug Lea’s JSR-133 cookbook
Sutter’s Mill atomic Weapons: The C++ Memory Model and Modern Hardware
Paul E. Mckenney’s Is Parallel Programming Hard, And, If So, What Can You Do About It?Appendix C - Why Memory Barriers?
jcstress
Aleksey Shipilёv’s Java Memory Model Pragmatics (transcript)
Java Concurrency in Practice
The Art of Multiprocessor Programming
2、竞态条件(Race Condition)
在多线程下,没有依赖关系的代码,在执行共享变量读写操作(至少有一个线程写)时,并不能保证以编写顺序(Program Order)执行,这称为发生了竞态条件
(Race Condition)。
(1)实例
例如:有共享变量 x,线程 1 执行
r.r1 = y;
r.r2 = x;
线程 2 执行:
x = 1;
y = 1;
最终的结果可能是 r11 而 r20(指令重排序的结果):
y = 1;
r.r1 = y;
r.r2 = x;
x = 1;
有同学这个时候会有疑问了,既然产生的结果有可能不是我们预期的,为什么还要进行指令重排序呢?答案就是竞争是为了更好的性能 -Data Race Free
。
3、同步动作(Synchronization Order)
若要保证多线程下,每个线程的执行顺序(Synchronization Order)按编写顺序(Program Order)执行,那么必须使用 Synchronization Actions
来保证,这些 SA 有:
- lock,unlock - synchronized, ReentrantLock
- volatile 方式读写变量 - 保证可见性,防止重排序
- VarHandle 方式读写变量(比volatile更轻量,jdk9新增)
Synchronization Order 也称之为
Total Order
(1)实例
例如:用 volatile 修饰共享变量 y,线程 1 执行
r.r1 = y;
r.r2 = x;
线程 2 执行:
x = 1;
y = 1;
最终的结果就不可能是 r11 而 r20。
(2)实例:SO 并不是阻止多线程切换
错误的认识,线程 1 执行:
synchronized(LOCK) {
r1 = x; //1 处
r2 = x; //2 处
}
线程 2 执行:
synchronized(LOCK) {
x = 1
}
并不是说 //1 与 //2 处之间不能切换到线程 2,只是即使切换到了线程 2,因为线程 2 不能拿到 LOCK 锁导致被阻塞,执行权又会轮到线程 1。
(3)实例:volatile 只用了一半算 SO 吗?
① 实例1
// 初始化:
int x;
volatile int y;
// 代码执行:
x = 10; //1 处
y = 20; //2 处
此时 //1 处代码绝不会重排到 //2 处之后(只写了 volatile 变量)
② 实例2
int x;
volatile int y;
执行下面的测试用例:
@Actor // 线程1
public void a1(II_Result r) {
y = 1; //1 处
r.r2 = x; //2 处
}
@Actor // 线程2
public void a2(II_Result r) {
x = 1; //3 处
r.r1 = y; //4 处
}
//1 //2 处的顺序可以保证(只写了 volatile 变量),但 //3 //4 处的顺序却不能保证(只读了 volatile 变量),仍会出现 r1r20 的问题。
③ 实例3
有时会很迷惑人,例如下面的例子
@Actor // 线程1
public void a1(II_Result r) {
r.r2 = x; //1 处
y = 1; //2 处
}
@Actor // 线程2
public void a2(II_Result r) {
r.r1 = y; //3 处
x = 1; //4 处
}
这回 //1 //2 (只写了 volatile 变量)//3 //4 处(只读了 volatile 变量)的顺序均能保证了,绝不会出现r1r21 的情况。
此外将用例 2 中两个变量均用 volatile 修饰就不会出现 r1r20 的问题,因此也把全部都用 volatile 修饰称为total order
,部分变量用 volatile 修饰称为 partial order
并不是说 partial order 不能用,只是,正确使用需要学明白后面的原理
4、Happens-Before
happens-before规则——理解happens-before规则
Happens-Before 保证了 线程切换
时代码的顺序和可见性。
若是变量读写时发生线程切换(例如,线程 1 写入 x,切换至线程 2,线程 2 读取 x)在这些边界的处理上如果有action1 先于 action 2 发生,那么代码可以按确定的顺序执行,这称之为 Happens-Before Order 规则。
Happens-Before Order 也称之为
Partial Order
用公式表达为:
含义为:如果 action1 先于 action2 发生,那么 action1 之前的共享变量的修改对于 action2 可见,且代码按 PO(编写)顺序执行。
(1)具体规则
其中 Tn 代表线程,而 x 未加说明,是普通共享变量,使用 volatile 会单独说明:
1)线程的启动和运行边界
start()规则:如果线程 A 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的ThreadB.start()操作 happens-before 于线程 B 中的任意操作。
2)线程的结束和 join 边界
join()规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。
3)线程的打断和得知打断边界
程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
4)unlock 与 lock 边界
监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
5)volatile write 与 volatile read 边界
volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个volatile 域的读。
6)传递性
传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
(2)案例
// 永不终止的循环 - 可见性问题
public class TestInfinityLoop {
volatile static boolean stop = false; //停止标记 可以使用volatile实现共享变量的可见性
static final Object lock = new Object(); // 可以使用lock、synchronized 等锁实现可见性
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true; // volatile 的写
});
System.out.println("start " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
t.start();
t.join(); // 可以使用join,来实现共享变量的可见性
foo();
}
private static void foo() {
while (true) {
boolean b = stop; // volatile 的读
if (b) {
break;
}
}
System.out.println("end " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
}
}
5、因果律(Causality)
Causality 即因果律:代码之间如存在依赖关系
,即使没有加 SA(加锁) 操作,代码的执行顺序也是可以预见的。
什么是依赖关系?
@JCStressTest
@Outcome(id = {"0, 0"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
@Outcome(expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
@State
public static class Case5 {
int x;
int y;
@Actor // 线程1
public void a1(II_Result r) {
r.r1 = x;
y = r.r1;
}
@Actor // 线程2
public void a2(II_Result r) {
r.r2 = y;
x = r.r2;
}
}
x 的值来自于 y,y 的值来自于 x,而二者的初始值都是 0,因此没有可能有其他结果。
6、安全发布
若要安全构造对象,并将其共享使用,需要用 final 或 volatile 修饰其成员变量,并避免 this 溢出情况
。
静态成员变量可以安全地发布
例如:
class Holder {
int x1;
volatile int x2;
public Holder(int v) {
x1 = v;
x2 = v;
}
}
需要将它作为全局使用:
Holder f;
两个线程,一个创建,一个使用:
@Actor // 线程1
public void a1() {
f = new Holder(1);
}
@Actor // 线程2
void a2(I_Result r) {
Holder o = this.f;
if (o != null) {
r.r1 = o.x2 + o.x1; // 0
} else {
r.r1 = ‐1; // ‐1
}
}
new Holder(1) 的过程并不是一个原子的操作, 可能会看到未构造完整的对象。
如果想要得到预期的结果,可以在Holder f 加上volatile,或者在Holder中的x2 加上volatile。
注意:在Holder中的x1加上volatile仍然无法解决以上的问题。
二、内存屏障
共有四种内存屏障,具体实现与 CPU 架构相关,不必钻研太深,只需知道它们的效果。
1、LoadLoad
防止 B 的 Load(读) 重排到 A 的 Load(读) 之前。
// 相当于,read(B)不能排到read(A)上面去
read(A)
LoadLoad // (↓) 向下箭头 下面的上不去
read(B)
2、LoadStore
load(A)
LoadStore // (↓) 向下箭头 下面的上不去
Store(B)
防止 B 的 Store(写) 被重排到 A 的 Load(读) 之前
3、StoreStore
A = x
StoreStore
B = true
Store(A)
StoreStore // (↑) 向上箭头 上面的下不去
Store(B)
防止 A 的 Store 被重排到 B 的 Store 之后
意义:在 B 修改为 true 之前,其它线程别想看到 A 的修改。有点类似于 sql 中更新后,commit 之前,其它事务不能看到这些更新(B 的赋值会触发 commit 并撤除屏障)
4、StoreLoad(*)
StoreLoad发生在线程切换时 才有效,Store能够让线程1所有写入都同步到内存,线程2的Load操作能够读到内存中最新的数据。
Store(A) // 线程1
StoreLoad// Store(↑)不能跑下面 + Load(↓)不能跑上面
Load(B) // 线程2
意义:屏障前的改动都同步到主存,屏障后的 Load 获取主存最新数据。
防止屏障前所有的写操作,被重排序到屏障后的任何的读操作,可以认为此 store -> load 是连续的。
有点类似于 git 中先 commit,再远程 poll,而且这个动作是原子的
5、Acquire
Acquire 表示LoadLoad + LoadStore的组合。
load(x)
LoadLoad + LoadStore // (↓) 向下箭头 下面的上不去
store(z, 10)
load(y)
6、Release
Release表示StoreStore + LoadStore的组合
load(x)
store(x, 10)
StoreStore + LoadStore // (↑) 向上箭头 上面的下不去
store(y, 10)
7、小总结
LoadLoad + LoadStore = Acquire 即让同一线程内读操作之后的读写上不去,第一个 Load 能读到主存最新值
。
LoadStore + StoreStore = Release 即让同一线程内写操作之前的读写下不来,后一个 Store 能将改动都写入主存
。
StoreLoad 最为特殊,还能用在线程切换时,对变量的写操作 + 读操作做同步,只要是对同一变量先写后读,那么屏障就能生效
。
三、volatile
1、volatile的本质
volatile的本质其实就是不同的内存屏障的组合。
事实上对 volatile 而言 Store-Load,与 LoadLoad 屏障最为有用,简化起见以后的分析省略部分其他屏障。
volatile保证了:
- 单一变量的赋值原子性(以前32位操作系统,无法保证long、double等数据的原子性赋值)。
- 控制了可能的执行路径:线程内按屏障有序,线程切换时按 HB(Happens-Before) 有序。
- 可见性:线程切换时若发生了 写 ->读 则变量可见,顺带影响普通变量可见。
2、可见性(visibility)
/**
* 案例1: 测试对同一个变量的多次读操作是否连贯
*/
@JCStressTest
@Outcome(id = {"3, 3, 3", "0, 0, 0"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
@Outcome(id = {"0, 3, 3", "0, 0, 3"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
@Outcome(id = "0, 3, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
@State
public static class Case1 {
static class Foo {
int x = 0;
}
Foo p = new Foo();
Foo q = p;
@Actor // 线程1
public void actor1(III_Result r) {
r.r1 = p.x;
r.r2 = q.x;
r.r3 = p.x;
}
@Actor // 线程2
public void actor2() {
p.x = 3;
}
}
(1)分析所有可能性
初始:
case1:
case 2:
case 3:
case 4:
意外情况:
(2)解决方案
使用 volatile 修饰 x 即可,给 x 上加入 volatile,会阻止编译器对代码的优化,并加入的 StoreLoad 屏障会保证红色线程的写入,对后续蓝色线程的读取可见。
3、有序性: 共享变量部分被volatile修饰:partial ordering
(1)实例:重排序导致不可预料的结果
@JCStressTest
@Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
@State
public static class Case1 {
int x;
int y;
@Actor
public void actor1() {
x = 1;
y = 1;
}
@Actor
public void actor2(II_Result r) {
r.r1 = y;
r.r2 = x;
}
}
意外情况,发生了指令重排序,导致不可预料的结果:
(2)解决方案:volatile修饰y
如果 y=1 先发生,那么前面的 Store 屏障会阻止 x=1 排下去,而后面的 Load 屏障会阻止后续的两个读操作排上来,结果为 r1r21:
如果 r.r1=y 先发生,结果为 r1r20:
x=1 与 r.r2=x 的顺序无法保证:
x=1 和 y=1 的位置被固定了,结果都是 r10, r21 ,不会出现 case 4 的 r20, r11 情况。
(3)分析:volatile 修饰 x - 行不行
r.r1=y 可以越过 LoadLoad 屏障
y=1 可以越过 Store 等屏障
有可能导致r20, r11 情况。
(4)总结
部分共享变量用volatile修饰时(partial ordering),volatile 写要用来收官(放在最后),volatile 读要用来开篇(放在最前面)
。
4、有序性:共享变量全部被volatile修饰:total ordering
(1)实例:重排序导致不可预料的结果
public class TestOrderingTotal {
/**
* 案例1: 演示 total ordering 存在的问题
*/
@JCStressTest
@Outcome(id = {"1, 0", "0, 1", "1, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
@State
public static class Case1 {
int x;
int y;
@Actor
public void a1(II_Result r) {
y = 1;
r.r2 = x;
}
@Actor
public void a2(II_Result r) {
x = 1;
r.r1 = y;
}
}
/**
* 案例2: 演示单个 volatile 变量不能解决 total ordering 的情况
*/
@JCStressTest
@Outcome(id = {"1, 0", "0, 1", "1, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
@State
public static class Case2 {
int x;
volatile int y;
@Actor
public void a1(II_Result r) {
y = 1;
r.r2 = x;
}
@Actor
public void a2(II_Result r) {
x = 1;
r.r1 = y;
}
}
/**
* 案例3: 演示单个 volatile 变量能解决 total ordering 的情况
*/
@JCStressTest
@Outcome(id = {"0, 0", "0, 1", "1, 0"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
@Outcome(id = "1, 1", expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
@State
public static class Case3 {
int x;
volatile int y;
@Actor
public void a1(II_Result r) {
r.r2 = x;
y = 1;
}
@Actor
public void a2(II_Result r) {
r.r1 = y;
x = 1;
}
}
/**
* 案例4: 演示两个 volatile 变量可以解决 total ordering 存在的问题
*/
@JCStressTest
@Outcome(id = {"1, 0", "0, 1", "1, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
@Outcome(id = "0, 0", expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
@State
public static class Case4 {
volatile int x;
volatile int y;
@Actor
public void a1(II_Result r) {
y = 1;
r.r2 = x;
}
@Actor
public void a2(II_Result r) {
x = 1;
r.r1 = y;
}
}
}
意外情况,发生了指令重排序,导致不可预料的结果:
(2)volatile 仅修饰 y - 不符合最后写最先读
检查最后结果是否可能 r1 r2 都为 0:x=1 可能排下去
(3)volatile 修饰 x 和 y - 不符合最后写最先读
都是 volatile,意味着它们线程内的次序固定,是可以解决指令重排序的问题。
四、Synchronized
1、Synchronized的本质
Synchronized本质是通过两个指令:monitorenter 与 monitorexit 来实现的。
public class TestSync {
static final Object LOCK = new Object();
static int count;
public static void main(String[] args) {
synchronized (LOCK) {
count++;
}
}
}
以上代码我们通过javap -c -v TestSync.class查看,发现有monitorenter和monitorexit:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #7 // Field LOCK:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: getstatic #13 // Field count:I
9: iconst_1
10: iadd
11: putstatic #13 // Field count:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
2、monitorenter 与 monitorexit 工作原理
我们都知道synchronized是对象锁,对锁住的代码块或者方法,编译器会自动加上monitorenter 与 monitorexit,并且在抛异常的时候也会自动调用monitorexit。
当调用monitorenter时,会获取操作系统的Monitor锁(C++实现)。
比如说Thread1 执行了monitorenter,会先根据要锁住的对象(Java对象),会用这个Java对象(锁对象)的对象头来记录Monitor锁的地址,从而创建Monitor锁,然后把Monitor锁的地址记录在对象头中。这样Thread就会获取到Monitor锁。
获取锁的过程:
(1)首先会检查Monitor的Owner属性,看这个锁是否有主人,如果Owner为null,说明该锁是空闲的,Thread1就可以成为该Monitor的Owner。
(2)此时Thread2执行monitorenter指令去加锁,根据锁对象的对象头获取Monitor的地址,然后找到Monitor锁,发现Owner并不是空,首先会做几次自旋的尝试
,如果自旋期间Thread1释放锁正好Thread2获取锁,如果自旋期间Thread1未释放锁,Thread2会存放到Monitor的EntryList队列进行阻塞等待
,并且Thread2的状态也会从运行状态变成阻塞状态
。
(3)当Thread1执行了monitorexit之后,会把Owner释放掉,唤醒EntryList中阻塞的线程,阻塞的Thread2会拿到Monitor。
3、Synchronized内存屏障
注意!在Synchronized同步代码块里面的代码,是会发生重排序的。
4、Synchronized正确使用
public class TestSynchronized {
/**
* 案例1: 未正确使用 synchronized 情况分析
*/
@JCStressTest
@Outcome(id = {"0, 0", "1, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
@State
public static class Case1 {
int X = 0;
int Y = 0;
final Object LOCK = new Object();
@Actor
public void actor1() {
X = 1;
synchronized (LOCK) {
Y = 1;
}
}
@Actor
public void actor2(II_Result r) {
synchronized (LOCK) {
r.r1 = Y;
}
r.r2 = X;
}
}
/**
* 案例2: 未正确使用 synchronized 情况分析
*/
@JCStressTest
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
@Outcome(expect = Expect.ACCEPTABLE_INTERESTING, desc = "ACCEPTABLE_INTERESTING")
@State
public static class Case2 {
int A;
int B;
@Actor
public void t1() {
final Object lock = new Object();
synchronized (lock) {
A = 1;
B = 1;
}
}
@Actor
public void t2(II_Result r) {
final Object lock = new Object();
synchronized (lock) {
r.r1 = A;
r.r2 = B;
}
}
}
}
5、Synchronized的优化
synchronized 的实现原理以及锁升级详解
重量级锁:当有竞争时,仍会向系统申请 Monitor 互斥锁。
轻量级锁:如果线程加锁、解锁时间上刚好是错开的,这时候就可以使用轻量级锁,只是使用 cas
尝试将对象头替换为该线程的锁记录地址,如果 cas 失败,会锁重入或触发重量级锁升级。
偏向锁(默认):
打个比方,轻量级锁就好比用课本占座,线程每次占座前还得比较一下,课本是不是自己的(cas),频繁 cas 性能也会受到影响;
而偏向锁就好比座位上已经刻好了线程的名字,线程【专用】这个座位,比 cas 更为轻量;
但是一旦其他线程访问偏向对象,那么比较麻烦,需要把座位上的名字擦去,这称之为偏向锁撤销,锁也升级为轻量级锁;
偏向锁撤销也属于昂贵的操作,怎么减少呢,JVM 会记录这一类对象被撤销的次数,如果超过了 20 这个阈值,下次新线程访问偏向对象时,就不用撤销了,而是刻上新线程的名字,这称为重偏向;
如果撤销次数进一步增加,超过 40 这个阈值,JVM 会认为这一类对象不适合采用偏向锁,会对它们禁用偏向锁,下次新建对象会直接加轻量级锁。
6、无锁 vs 有锁
synchronized 更为重量,申请锁、锁重入都要发起系统调用,频繁调用性能会受影响;
synchronized 如果无法获取锁时,线程会陷入阻塞,引起的线程上下文切换成本高;
虽然做了一系列优化,但轻量级锁、偏向锁都是针对无数据竞争场景的;
如果数据的原子操作时间较长,仍应该让线程阻塞,无锁适合的是短频快的共享数据修改操作主要用于计数器、停止标记、或是阻塞前的有限尝试。
五、VarHandle
VarHandle:Java9中保证变量读写可见性、有序性、原子性利器