概念
volatile关键字是由JVM提供的最轻量级的同步机制,它能保证内存可见性和防止指令重排序。
JMM(JAVA内存模型)常见概念
- 原子性:保证指令不会受到上下文切换的影响
- 有序性:保证指令不会受到CPU并行优化的影响
- 可见性:保证指令不会受到CPU缓存的影响
可见性
多核CPU,由于CPU速度远大于内存速度,故在CPU和内存之间,存在缓存,可以一定程度降低两者之间的差距。但也因此出现了主存和缓存不一致的问题,这个问题我们称为可见性问题。
@Slf4j
public class Test {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
}
}, "t1").start();
Thread.sleep(1000);
log.info("stop");
flag = false;
}
}
- 期望结果:在主线程休眠结束后,由于flag变为false,while循环会结束。
- 实际结果:t1仍旧处于死循环中,并未因为flag变为false而终止。
- 原因分析:因为t1除了最初从主存中读取了flag的值之后,后续循环一直都读的缓存的值,当主线程更改了flag的值,并未通知t1缓存做出更改,导致循环未退出。
- 解决方案:在flag前加上volatile关键字。因为加上volatile关键字后,当线程修改volatile所修饰的变量时,会直接将缓存中的值写入主存中,同时通知其他缓存更新主存中的数据,故当flag发生变化的同时,t1就监测到其变化,进而终止循环。
指令重排序
如下代码,CPU在保证最终结果一直的情况下,回对程序执行顺序进行优化,可能会先执行j = 2再执行i = 1;
int i = 1;
int j = 2;
说到指令重排序就不得不说到经典的dcl(double check locking)问题了
public class Singleton {
private Singleton(){};
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
- 问题分析:上面代码是经典的dcl代码,但由于有指令重排序问题,可能会出现new Singleton()初始化时,先给INSTANCE绑定了对象地址,但对象还没来得及赋值的情况,而此时另一个线程拿到了这个未赋值的对象时,就会出问题。
- 解决方法:给INSTANCE增加volatile修饰符
- 原理分析:对volatile修饰的属性的读操作,会在读之前加上读屏障(防止读之后的代码跑到前面),对volatile修饰的属性的写操作,会在写操作之后加一个写屏障(防止写之前的操作跑后面去)。构造方法是在赋值前,故不会出现刚刚所说的结果。
happens-before规则
happens-before仅仅要求前一个操作的执行结果对后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。
synchronized
@Slf4j
public class Test {
private static int count = 0;
private static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
count++;
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
log.info("count:{}", count);
}
}, "t2");
t1.start();
t2.start();
}
}
volatile
@Slf4j
public class Test {
private static volatile int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
count++;
}, "t1");
Thread t2 = new Thread(() -> {
log.info("count:{}", count);
}, "t2");
t1.start();
t2.start();
}
}
Thread.start()方法
- 调用start()方法前,需要用到的变量都是读可见
- 同理,线程运行结束时,线程内做出的修改对其他线程读可见
@Slf4j
public class Test {
public static void main(String[] args) {
int count = 10;
new Thread(() -> {
log.info("count = {}", count);
}, "t1").start();
}
}
Thread.internupt()方法
线程被打断后,打断前的变量修改可见
@Slf4j
public class Test {
private static int count = 10;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
log.info("count = {}", count);
break;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
count = 20;
t1.interrupt();
}, "t2");
t1.start();
t2.start();
while (!t1.isInterrupted()) {
Thread.yield();
}
log.info("count = {}", count);
}
}
传递性
由于x被volatile修饰,x的修改是可见的,而y在x写操作之前,由于传递性y也是可见的
@Slf4j
public class Test {
private static volatile int x = 0;
private static int y = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
y = 10;
x = 10;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
Thread t2 = new Thread(() -> {
log.info("x = {}, y = {}", x, y);
}, "t2");
t1.start();
t2.start();
}
}