一、概述
- 能保证修饰变量的可见性、有序性,但不能保证原子性。
二、分析
1、不能保证原子性
public class Thread17 {
private static volatile Integer INIT_VALUE = 0;
private static Integer MAX_LIMIT = 500;
public static void main(String[] args) {
test2();
}
public static void test2() {
new Thread(new Runnable() {
public void run() {
while(INIT_VALUE < MAX_LIMIT) {
++INIT_VALUE;
System.out.println("read : " + INIT_VALUE);
}
}
},"read").start();
try {
Thread.sleep(10);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(new Runnable() {
public void run() {
while (INIT_VALUE < MAX_LIMIT) {
++INIT_VALUE;
System.out.println("update : " + INIT_VALUE);
}
}
},"update").start();
}
...
read : 378
read : 379
read : 380
read : 381
read : 382
read : 383
read : 384
update : 384
update : 386
update : 387
read : 385
read : 389
read : 390
update : 388
read : 391
...
- 当INIT_VALUE用volatile修饰,或不用volatile修饰都会出现两个相同的值如:384
- 这是因为++INIT_VALUE实际上是由三部分组成,这三部在多线程下的组合并不是原子性:
- 1、从主内存中获取INIT_VALUE的值。
- 2、进行运算。
- 3、重新赋值给INIT_VALUE变量。
- 如:当INIT_VALUE为383的时候,read线程运行完2之后,还没执行3。
- 线程cpu的执行权被切换到udpate线程,然后执行++INIT_VALUE操作,读取到的INIT_VALUE依然为383,所以执行完++INIT_VALUE之后输出的值为384。
- 当cpu的执行权再次切换到read线程的时候,继续执行第3步骤,输出的值也为384,所以看到输出了两次384。
- 证明了volatile并不能保证原子性。
2、可见性已在上一篇文章中已介绍:https://blog.youkuaiyun.com/oJueQiang123456/article/details/100867699
3、有序性,先看一段代码:
public class VolatileTest {
private boolean isInit = false;
public VolatileTest() {
// 1、初始化程序
init();
// 2、设置为初始化完成
isInit = true;
}
public void init() {
}
public void update() {
if(isInit) {
// 执行业务代码
}
}
}
逻辑顺序是:初始化程序、设置isInit为已初始化,执行update业务,看起来逻辑没有问题,在单线程下确实没问题,但在多线程下就会出现问题,如:因为jvm会对程序进行重排序(具体重排序规则查看网络资料),重排序之后的代码可能为:
public VolatileTest() {
// 2、设置为初始化完成
isInit = true;
// 1、初始化程序
init();
}
jvm会保证最终一致性,执行完构造方法之后,当为单线程时,调用update业务不会有任务问题,但在多线程下重排序之后下:
- 当执行完2之后:isInit = true,但程序还未初始化。
- cpu执行权切换到其它线程去执行update业务的时候程序还未初始化,导致程序有问题
所以可以在变量isInit中添加volatile关键字修饰来保证程序的有序性:这样在isInit被修改之前,一定会保证之前的代码全部执行完成了,这样就不会不会出现重排导致的问题了,具体的指令重排还有很多内容,比如happens-before原则,内存屏障等等
private volatile boolean isInit = false;