一般情况下大家在网上看到关于volatile关键字的解释有如下两点
1. 内存可见性
2. 防止指令重排序
对于1.内存可见性,通常都会贴上如下代码,来证明没有volatile关键字修饰的变量被线程A修改后,线程B未感知到
public class ThreadDemo extends Thread {
private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("进入了run");
while (isRunning) {
}
System.out.println("结束");
}
public static void main(String[] args) {
try {
ThreadDemo t = new ThreadDemo();
t.start();
Thread.sleep(100);
t.setRunning(false);
System.out.println("isRunning设置成false");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果
isRunning设置为false,未被感知到,线程一直没有停止。
此时给isRunning增加volatile修饰
private volatile boolean isRunning = true;
程序可以正常结束
但是当你自己手写这块代码时,你有时候会发现情况并不会这样,比如这样
@Override
public void run() {
System.out.println("进入了run");
while (isRunning) {
System.out.println("===");
}
System.out.println("结束");
}
我在while代码里添加了一个System.out.println("==="), 我就想看一下进程一直在里边转,一直在打印输出东西,特此强调,此时的isRunning仍未修饰volatile,完整代码是这样的
public class ThreadDemo extends Thread {
private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("进入了run");
while (isRunning) {
System.out.println("===");
}
System.out.println("结束");
}
public static void main(String[] args) {
try {
ThreadDemo t = new ThreadDemo();
t.start();
Thread.sleep(100);
t.setRunning(false);
System.out.println("isRunning设置成false");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果看
???怎么回事,线程感知到了isRunning的变化,并终止了循环体,结束了线程,就因为在循环体里加了一个 System.out.print()。
(PS: 这个现象在网上几乎所有关于volatile的关键字分析案例中都进行了规避处理,包括马士兵的视频教程,即使他在while代码块里写了个System.out.print()发现了案例不正常,他也选择把代码注释掉然后继续讲课)
打开System.out.print()源码,发现里面有synchronized代码块,继而发现直接添加synchronized代码块也可以复现这个问题,既然synchronized可以复现,那尝试时候会发现Lock和AQS系的锁都可以复现。那我们知道AQS的原理是Unsafe类,但是通过尝试发现并不是所有用到Unsafe类的都会有这样的情况,比如Atomic系的对象创建并不会复现该问题,但是Atomic系对象的增减是可以复现的。synchronized和Unsafe自旋底层原理都是lock cmpxchg,原子类底层原理是lock addl,volatile原理也是lock,所以是否跟lock有关,而lock是一个内核态操作,这中间是否与线程上下文切换有关,我们知道Thread.sleep和Thread.yield一个是线程阻塞,一个是让出CPU资源,两个操作都会产生CPU内核态切换。
可重现该现象的代码有如下这些方式:
- System.out.print() (方法中使用了synchronized锁)
- synchronized(){}
- new AbstractQueuedSynchronizer(){} (包括用到AQS系的子类Sync的API锁,如Lock系的ReentrantLock、ReentrantReadWriteLock,Semaphore,CountDownLatch等)
- 原子类的数据变化
- new File("/xx")
- Thread.sleep()
- Thread.yield()
- 其他未探索发现的方式
仔细分析一下上述代码的特征:
- synchronized底层需要内核态切换执行lock cmpxchg操作
- AQS系的Unsafe自旋一样会用到lock操作,
- 原子类实现原理还是Unsafe自旋,
- Thread.sleep和yeild,自然不用说,线程阻塞和让出CPU使用权一定会产生线程切换即产生CPU中断。
- File类也会涉及到Unsafe自旋操作
结论:可能的原因是因为上述几种方式均出现了线程上下文切换即产生了CPU中断,线程切换之后共享变量会被同步更新到工作内存中,所以会出现未添加volatile关键字,修改仍可见的现象。