- 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决办法
-
使用
volatile
(易变关键字) -
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
2-2 可见性vs原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况
注意:
synchronized
语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。
- 但缺点是
synchronized
是属于重量级操作,性能相对更低。
- 如果在前面示例的死循环中加入
System.out.println()
会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?
进入`println`源码,可以看出加了`synchronized`,保证了每次`run`变量都会从主存中获取
public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
3-1 诡异的结果
看下面一个栗子:
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
看到这里可能聪明的小伙伴会想到有下面三种情况:
情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
但其实还有可能为0哦! 😲
有可能还是:线程 2 执行 ready=true ,切换到线程1 ,进入if分支,相加为0,在切回线程 2 执行 num=2
这种现象就是指令重排
3-2 解决方法
volatile
修饰的变量,可以禁用指令重排
@JCStressTest
@Outcome(id = {“1”, “4”}, expect = Expect.ACCEPTABLE, desc = “ok”)
@Outcome(id = “0”, expect = Expect.ACCEPTABLE_INTERESTING, desc = “!!!”)
@State
public class ConcurrencyTest {
int num = 0;
volatile boolean ready = false;//可以禁用指令重排
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
3-3 有序性理解
同一线程内,JVM会在不影响正确性的前提下,可以调整语句的执行顺序,看看下面的代码:
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = …; // 较为耗时的操作
j = …;
可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时, 既可以是
i = …; // 较为耗时的操作
j = …;
也可以是
j = …;
i = …; // 较为耗时的操作
这种特性称之为指令重排,多线程下指令重排会影响正确性
3-4 happens-before
happens-before 规定了对共享变量写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
- 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m) {
x = 10;
}
},“t1”).start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},“t2”).start()
- 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(()->{
x = 10;
},“t1”).start();
new Thread(()->{
System.out.println(x);
},“t2”).start();
- 线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},“t2”).start();
- 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用
t1.isAlive()
或t1.join()
等待它结束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},“t1”);
t1.start();
t1.join();
System.out.println(x);
- 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通 过
t2.interrupted
或t2.isInterrupted
)
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);//10
break;
}
}
},“t2”);
t2.start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 10;
t2.interrupt();
},“t1”).start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);//10
}
-
对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
-
具有传递性,如果
x hb-> y
并且y hb-> z
那么有x hb-> z
volatile static int x;
static int y;
new Thread(()->{
y = 10;
x = 20;//写屏障,y也会同步到主存
},“t1”).start();
new Thread(()->{
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
},“t2”).start();
以上变量都是指共享变量即成员变量或静态资源变量
volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
-
对 volatile 变量的写指令后会加入写屏障
-
对 volatile 变量的读指令前会加入读屏障
4-1 如何保证可见性
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
最后
学习视频:
大厂面试真题:
volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
-
对 volatile 变量的写指令后会加入写屏障
-
对 volatile 变量的读指令前会加入读屏障
4-1 如何保证可见性
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
最后
学习视频:
[外链图片转存中…(img-LFpBrXxj-1714158809495)]
大厂面试真题:
[外链图片转存中…(img-DSnIWUtd-1714158809495)]