最近一直在看《Java并发编程的艺术》这本书,看了后有种感觉,网上关于JVM与JUC的绝大部分文章、资料的源头都出自这本书以及《深入理解Java虚拟机》。作为一个Javaer,我应该好好多撸几遍这两本书且多做笔记。下面开始吧~
先看一段代码:
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo, "Thread-A").start();
while (true) {
if (threadDemo.isFlag()) {
System.out.println(Thread.currentThread().getName() + "=============");
break;
}
}
}
}
class ThreadDemo implements Runnable {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag = " + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
输出是什么?为什么?
发现flag的值已经被改为true了,但是Main线程中的while循环并未结束,这是什么原因导致的?
该问题被称为 内存可见性问题 ,即多个线程操作共享数据时,线程之间不能看到共享变量被修改后的
值,出现的原因就涉及到JVM内存模型(JMM)了:
Java中,所有的共享变量(实例域、静态域和数组)都存在堆内存中,因为堆内存在线程之间是共享的,所以这些变量被称为共享变量,JVM定义了线程与主内存(这里可以理解为就是堆内存)之间的一种抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地工作内存(也可以理解为缓存,主要是为了解决效率问题),本地内存中存储了该线程以读/写共享变量的副本
关于CPU操作主内存、本地工作内存可以类比一下CPU、高速缓存、内存之间的关系
线程A与线程B之间要通信(相互更改共享变量的值后让对方知道)的话,必须经历下面2个步骤:
- 线程A把本地工作内存A中更新过的共享变量刷新到主内存中去
- 线程B到主内存中去读取线程A之前已经更新过的共享变量
上述问题原因分析:
- 一开始线程Thread-A与main线程之间都有共享变量副本值flag = false
- 线程Thread-A将本地工作内存flag值改为flag = true,此时并不会立即更新到主内存中去
- main运行时,一直是用的是最开始从主内存中获取的flag = false,保存在本地工作内存中的, 所以while循环一直未能break
解决办法:
Java提供了volatile关键字,如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
有volatile变量修饰的共享变量进行写操作的时候,JVM执行字节码转化为汇编指令代码时,会多出lock前缀的一行汇编代码,Lock前缀的指令在多和多核处理器下会引发两件事情:
-
将当前处理器缓存行(本地工作内存)的数据写回到系统内存(主内存) 对应:当写一个volatile变量时,JMM会把该线程对应的本地工作内存中的共享变量值刷新到主内中。
-
这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效 对应:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内中读取共享变量。
volatile有许多特性:
-
可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量的最后的写入。什么意思?即当一个线程修改了变量的值,新的值会立即同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。
-
原子性。对任意单个volatile变量的读写具有原子性,但类似于volatile++这种复合操作则不具有原子性
为什么volatile可以有这样的特性?(先大概了解下,后面可能会详细学习记录happens-before原则)
先行发生原则(happens-before),从JDK1.5开始,Java使用新的JSR-133内存模型,用happens-before原则来阐述线程之间的可见性,在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系
与程序员密切相关的happens-before规则如下:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C ,那么A happens-before C。
了解完上述知识点后,再回到原来的问题,将flag用volatile修饰再运行验证下结论
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo, "Thread-A").start();
while (true) {
if (threadDemo.isFlag()) {
System.out.println(Thread.currentThread().getName() + "=============");
break;
}
}
}
}
class ThreadDemo implements Runnable {
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag = " + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}