Java内存模型的抽象结构
Java线程之间的通信由Java内存模型(JMM,Java Memory Model)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
这样的内存模型设计会产生可见性问题。可见性的意思是指当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。但在多线程环境下,很容易产生不可见的问题,例如下面代码:
public class VisibilityTest {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
while (true) {
if (thread.isFlag()) {
System.out.println("Yes!");
break;
}
}
}
}
class MyThread extends Thread{
private boolean flag = false;
public boolean isFlag() {
return flag;
}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Set flag to true!");
}
}
你会发现,上述代码永远不会输出Yes!
,这便是产生了内存可见性问题。有很多方式来保证内存可见性,volatile便是其中一种。
volatile的内存语义
volatile保证了变量的可见性,其内存语义如下:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。注意,是所有的共享变量,而非仅仅是volatile变量自身。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,然后从主内存中读取共享变量。
我们把上述代码中的共享变量用volatile修饰,再看运行结果如何:
public class VisibilityTest {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
while (true) {
if (thread.isFlag()) {
System.out.println("Yes!");
break;
}
}
}
}
class MyThread extends Thread{
private volatile boolean flag = false;
public boolean isFlag() {
return flag;
}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Set flag to true!");
}
}
运行结果:
Set flag to true!
Yes!
Process finished with exit code 0
如何正确使用volatile
相对于锁,volatile不会造成线程阻塞,也更易于阅读,但如果使用不当,volatile可能不会给你提供预想的线程安全,这主要是因为volatile对复合操作不具备原子特性。例如,a++
,实际上涉及a的读取、a的自增和a的写入三个操作,所以此时如果有多个线程执行a++
操作,不能够保证线程安全,请看下面的代码:
public class VolatileTest {
private static volatile int sum = 0;
private static CountDownLatch latch = new CountDownLatch(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
MyThread myThread = new MyThread();
myThread.start();
}
latch.await();
System.out.println(sum);
}
static class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 1000; i++) {
sum++;
}
latch.countDown();
}
}
}
上述代码的输出结果永远是一个小于等于10000的不确定的值。
每日学习笔记,写于2020-05-03 星期日