深入理解volatile

#JMM Java 内存模型
Java的内存模型指定了Java虚拟机如何与计算机的内存进行工作
image.png
Java内存模型决定了一个线程对共享变量的写入何时对其他线程可见,Java内存模型定义了线程和主内存之间的抽象关系,具体如下:
1、共享变量存储在主内存中,每个线程都可以访问。
2、每个线程都有私有的工作内存。
3、工作内存只存储该线程对共享变量的副本。
4、线程不能直接操作主内存,只有先操作了工作内存之后才能写入内存。
假设主内存的共享变量为0,线程1和线程2分享拥有共享变量X的副本,假设线程1此时将工作内存中的X修改为1,同时刷新到主内存中,当线程2想要去使用副本X的时候,就会发现该变量已经失效了,必须到主内存中再次获取然后存入自己的工作内容中,这一点和CPU与CPU Cache之间的关系非常类似。
image.png
当同一个数据被分别存储到了计算机的各个内存区域时,就会导致多个线程在各自的工作内存中看到的可能不一样。后面会讲到Java语言中如何保证不通线程对某个共享变量的可见性。

#多线程可见性例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.util.concurrent.TimeUnit;
/**
* Created by chiyuanjia on 2019/7/17.
*/
public class VolatileFoo {

//init_value 的最大值
final static int MAX = 5 ;

//init_value 的初始值
static int init_value = 0;

public static void main(String[] args) {
//启动一个Reader线程 ,当发现local_value 和 init_value 不同时,则输出 init_value 被修改的信息
new Thread(new Runnable() {
public void run() {
int localValue = init_value;
while (localValue < MAX){
if(init_value != localValue){
System.out.printf("The init_value is updated to [%d]\n",init_value);
//对localValue 进行重新赋值
localValue = init_value;
}
}
}
},"Reader").start();

//启动Updater线程,主要用于对init_value的修改,当local_value>=5的时候则退出生命周期
new Thread(new Runnable() {
public void run() {
int localValue = init_value;
while(localValue < MAX){
//修改init_value
System.out.printf("The init_value will be changed to [%d]\n",++localValue);
init_value = localValue;
try {
//短暂休眠 目的是为了使Reader线程来得及输出变化内容
TimeUnit.SECONDS.sleep(2);
}catch (Exception e){
e.printStackTrace();
}
}
}
}).start();

}
}

大家先猜一下,运行结果是怎么样的?可能会大失所望
运行结果如下:

1
2
3
4
5
6
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/bin/java -Dvisualvm.id=83453442187216 -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -Didea.launcher.port=7534 "-Didea.launcher.bin.path=/Applications/IntelliJ IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar:/Users/bruce/2dfire/workspace/jvmstudy/target/classes:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar" com.intellij.rt.execution.application.AppMain com.just.study.jvm.concurrent.VolatileFoo
The init_value will be changed to [1]
The init_value will be changed to [2]
The init_value will be changed to [3]
The init_value will be changed to [4]
The init_value will be changed to [5]

通过控制台的输出我们发现:Reader线程压根就没有感知到init_value的变化、并且进入了死循环线程没有退出
我们对代码做一个调整,将init_value变量设置为volatile:

1
2
//init_value 的初始值
static volatile int init_value = 0;

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/bin/java -Dvisualvm.id=83688650193139 -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -Didea.launcher.port=7535 "-Didea.launcher.bin.path=/Applications/IntelliJ IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar:/Users/bruce/2dfire/workspace/jvmstudy/target/classes:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar" com.intellij.rt.execution.application.AppMain com.just.study.jvm.concurrent.VolatileFoo
The init_value will be changed to [1]
The init_value is updated to [1]
The init_value will be changed to [2]
The init_value is updated to [2]
The init_value will be changed to [3]
The init_value is updated to [3]
The init_value will be changed to [4]
The init_value is updated to [4]
The init_value will be changed to [5]
The init_value is updated to [5]

Process finished with exit code 0

为啥会发生这样的改变、后面会慢慢讲到、这里是因为volatile可以保证多线程环境下的可见性、还有volatile的变量是先被写后再被读(后续会讲到)。

#CPU缓存模型和缓存一致性
CPU在速度上的发展要快与内存在速度上的发展,由于两边速度严重的不等,所以为了增加吞吐量,缩小CPU和内存的速度差,建立了CPU Cache模型,就是大家所熟知的L1、L2、L3 CPU高速缓存。CPU Cache又由很多个Cache Line构成,Cache Line可以认为是CPU Cache中最小的缓存单元。
image.png

image.png

程序运行过程中,会将运算锁需要的数据从内存复制一份到CPU Cache中,然后进行读取和写入,当运算结束之后,在将CPU Cache中的最新数据刷新到内存中,这样通过CPU Cache在中间做交互,提高了CPU的吞吐能力。
image.png

CPU Cache虽然提高了CPU的吞吐能力,同时也带来了一个问题:缓存不一致的问题,比如i++这个操作,运行的过程如下;
1、读取主内存的i到CPU Cache中。
2、对i进行加一的操作。
3、将结果写回到CPU Cache中。
4、将数据刷新到主内存中。
i++在单线程的情况下不会有任何问题,但在多线程的情况下就会有问题,每个线程都有自己的工作内存(对于于CPU的Cache),变量i会在多个线程的本地内存中都存在一个副本。如果同时有两个线程执行i++操作,假设i的初始值为0,每一个线程都从主内存获取i的值存入CPU Cache中,然后经过计算在写入主内存中,很有可能i在经过了两次自增之后结果还是1,这就是典型的缓存不一致问题。
主要有两种解决方法:
1、通过总线加锁。
2、通过缓存一致性协议。
第一种是悲观的实现方式,CPU和其他组件的通信都是通过总线来进行,会有阻塞,效率低下。

第二种:
image.png
在缓存一致性中最为出名的是Intel的MESI协议,MESI协议保证了每一个缓存汇中使用的是共享变量副本都是一致的,大概意思就是当CPU在操作Cache中的数据时,如果发现该变量是一个共享变量,也就是说在其他的CPU Cache中也存在一个副本,那么进行如下操作:
1、读取操作,不做任何处理,只是将Cache中的数据读取到寄存器。
2、写入操作,发出信号通知其他CPU将该变量的Cache line置为无效状态,其他CPU在进行该变量读取的时候不得不到主内存中再次获取。

#并发编程三大特性:原子性、可见性、有序性

##原子性
原子性是值指在一次的操作或者多次操作中,要么所有的操作全部得到执行,要么所有的操作都不执行。i++ 是由三个原子操作组成get i, i+1 ,set i = x,但是i++就不是原子性操作。volatile不保证原子性,synchronized保证原子性,JUC的原子性类型保证原子性,例如:AtomicInteger,通过volatile和CAS来实现。

##可见性
可见性是指当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的新值。例如上面我们的例子Reader线程会将init_value从内存缓存到CPU Cache中,也就是从主内存缓存到线程的工作内存中,Updater线程对init_value的修改对Reader线程是不可见的。

##有序性
有序性就是程序代码在执行过程中的先后顺序,Java在编译器以及运行期的优化,会产生指令重排序,导致了代码的执行顺序不一定是编写代码时的顺序,指令重排序是在不影响运行结果的情况下进行重排序,对于单线程来说指令重排序不会有问题。例如:

1
2
3
4
int x = 10;
int y = 0;
x++;
y=20;

但是在多线程的情况下,如果有序性得不到保证,那么很有可能就会出现问题,例如如下代码:

1
2
3
4
5
6
7
8
9
private boolean initialized = false;
private Context context = null;
public Context load(){
if(!initialized){
context = loadContext();
initialized = true;
}
return context;
}

在单线程情况,这段代码重排序,录入把 initialized = true;放到 context = loadContext();调换位置,不会有问题,但是如果多线程情况下第二个线程在调用load方法后可能会得到一个null。

#JMM如何保证原子性、可见性、有序性
JVM采用内存模型的机制来屏蔽哥哥平台与操作系统之间内存访问的差异,以实现让Java程序在各种平台下达到一致的内存访问效果。比如C语言中的整型变量,在某些平台下占用了两个字节的内存,在某些平台下则占用了四个字节的内存,Java则在任何平台下,int类型就是四个字节,这就是一直内存访问效果。

##JMM与原子性
在Java语言中,对基本数据类型的变量读取赋值操作都是原子性的,对引用类型的变量读取和赋值的操作也是原子性的。
1、x=10 原子操作
2、y=x 非原子操作【两个原子操作合在一起就不是原子操作】
1)执行线程从主内存中读取x的值(如果在工作内存就直接从工作内存获取)
2)在执行线程的工作内存中修改y的值为x,然后将y的值写入主内存之中。
3、y++ 自增操作 不是原子的,因为包含三个原子操作:
1)执行线程从主内存中读取y的值(如果y已存在于执行线程的工作内存中,则直接获取),然后将其存入当前线程的工作内存中。
2)在执行线程工作内存中为y执行加1的操作。
3)将y的值写入主内存。
结论:
a、多个原子性操作在一起就不在是原子性操作了。
b、简单的读取和赋值操作是原子性操作,将一个变量赋给另外一个变量的操作不是原子性操作。
c、Java内存模型只保证了基本读取和赋值的原子性操作,其他的均不保证,如果先更要使得某些代码片段具备原子性,需使用关键字synchronized,或者JUC中的lock。原子封装类:AtomicInteger等。
总结:volatile不具备保证原子性的语义

#JMM与可见性
在多线程的环境下,如果某个线程首次读取共享变量,首先到主内存获取该变量,然后存入工作内存中,以后只需要在工作内存中读取该变量即可。同样如果对该变量执行了修改的操作,则先将新值写入工作内存中,然后在刷新至主内存中。但是什么时候最新的值会被刷新至主内存是不太确定的,这就解释了为什么没有加volatile关键字的时候VolatileFoo中的Reader线程始终无法获取到init_value最新的变化。
Java提供三种方式来保证可见性:
1)使用关键字volatile,共享资源的读操作直接在内存中进行。写操作是先写工作内存,然后立刻刷新到主内存中。
2)synchronized保证可见性,synchronized关键字能够保证同一时刻只有一个线程获得锁,然后执行同步方法,并且还会确保在锁释放之前,会将对变量的修改刷新到主内存中。
3)通过JUC提供的显示锁lock也能够保证可见性,Lock的lock防范能够保证在同一时刻只有一个线程获得锁然后执行同步方法,并且会确保在锁释放之前会将对变量的修改刷新到主内存当中。

注:1、JVM禁用JIT即时编译器的后多线程环境下共享变量也具有可见性
例如下面代码,如果添加JVM参数 -server -Djava.compiler=NONE 或者 -Xint 多线程环境下flag共享变量就具有可见性
2、System.out.print() 输出流会加锁,也具有可见性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Created by chiyuanjia on 2019/7/26.
*/
public class Zuo {

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
print();
}
}).start();
TimeUnit.SECONDS.sleep(2);
flag = false;
System.out.println("flag set to false");
}

private static void print() {
while (flag) {
}
}
}

#JMM与有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,在单线程的情况下,重排序不会有问题,但是多线程的情况下,会影响程序的正确运行。
Java提供了三种保证有序性的方式:

  • 使用关键字volatile。
  • synchronized关键字。
  • 使用显示Lock。
    后两者是采用同步。

Java内存的天生有一些有序性规则-Happens-before原则。如果两个操作无法从happens-before推导出来,那么他们就无法保证有序性。

  • 程序次序规则:在一个线程内,代码按照编写时的次序执行,编写在后面的操作发生于编写在前面的操作之后,虚拟机还是会对程序代码的指令进行重排序,只要确保在一个线程内最终的结果和代码顺序执行的结果一致即可。
  • 锁定规则:一个unlock操作要先行发生在对同一个锁的lock操作。
  • volatile变量规则:对一个变量的写操作要早与对这个变量之后的读操作。意思是一个变量volatile,一个线程对它进行读,一个线程对它进行写,写操作一定是先行发生于读操作。
  • 传递规则:如果操作A先于操作B,而操作B又先于操作C,则A先于操作C。
  • 线程启动规则:Thread对象的start()方法先行于线程的任何动作。
  • 线程中断规则:对线程执行interrupt()方法肯定要优先于捕捉到中断信号,意
    思是如果线程收到了中断信号,那么在此之前势必要有interrupt()。
  • 线程的终结规则:线程中所有的操作都要先行发生于线程的终止检测,意识是线程的任务执行,逻辑单元执行肯定要发生于线程死亡之前。
  • 对象的终结规则:一个对象初始化的完成先行发生于finalize()方法之前,意思是先生后死。
    总结:volatile关键字保证有序性
    #volatile关键字深入解析
    volatile具有两个语义:
  • 保证了不同线程之间对共享变量操作时的可见性,也就是说当一个线程修改volatile修改的变量,另外一个线程会立即看到最新的值。
  • 禁止对指令进行重排序操作。
    (1)理解volatile保证可见性:
    VolatileFoo例子,Updater线程对init_value变量的每一次更改都会使得Reader线程能够看到(happens-before规则中,第三条volatile变量规则:对一个变量的写操作要早于对这个变量之后的读操作),步骤:
  1. Reader线程从主内存获取init_value的值为0,并且将其缓存到本地工作内存中。
  2. Updater线程将init_value的值在本地工作内存中修改为1,然后立即刷新至主内存中。
  3. Reader线程在本地工作内存中的init_value失效。(反映到硬件上就是CPU Cache 的 Cache Line失效)
  4. 由于Reader线程的工作内存中的init_value失效,因此需要从主内存中从新读取init_value的值。
    (2)理解volatile保证有序性
    volatile关键字对有序性的保证比较粗暴,直接静止JVM和处理器对volatile关键字修改的指令重排序,但是对volatile前后无依赖关系的指令则可以随便怎么排序。
    (3)理解volatile不保证原子性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45

    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.atomic.AtomicInteger;

    /**
    * Created by chiyuanjia on 2019/7/21.
    * //没次的运行结果不一样,具体原因是 i++ 不是一个原子操作,i++操作分三步:
    * 1、从主内存中获取i的值,然后魂村至线程工作内存中。
    * 2、在线程工作内存中为进行加1的操作。
    * 3、将i的最新值写入主内存中。
    * 上面三个操作单独的每一个操作都是原子性操作,但是合起来就不是原子性操作了。
    */
    public class VolatileTest {

    //使用volatile修改共享资源i
    private static volatile int i = 0;
    //private static AtomicInteger i = new AtomicInteger(0);
    //10个线程
    private static final CountDownLatch latch = new CountDownLatch(10);

    private static void inc(){
    i++;
    //i.addAndGet(1);
    }

    public static void main(String[] args) throws InterruptedException {

    for (int i = 0; i< 10;i++){
    new Thread(new Runnable() {
    public void run() {

    for (int x = 0; x < 1000; x++){
    inc();
    }
    //使计算器减1
    latch.countDown();
    }
    }).start();
    }
    //等待所有的线程完成工作
    latch.await();
    System.out.println(i);

    }
    }

运行结果:

1
2
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/bin/java -Dvisualvm.id=89098433865570 -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -Didea.launcher.port=7533 "-Didea.launcher.bin.path=/Applications/IntelliJ IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar:/Users/bruce/2dfire/workspace/jvmstudy/target/classes:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar" com.intellij.rt.execution.application.AppMain com.just.study.jvm.concurrent.VolatileTest
9656

上面代码创建了10个线程,每个线程执行1000次对共享变量i的自增操作,但是最终结果可能不是10000,因为这段代码的 i++ 操作其实是3个原子操作合起来的,3个原子操作合起来就不是原子操作了。

#volatile的原理和实现机制
下面为OpenJDK下的unsafe.cpp源码,会发现被volatile装饰的变量存在于一个”lock”的前缀,源码如下:
image.png

#volatile的使用场景
虽然volatile有部分synchronized关键字的语义,但是volatile不可能完全替代synchronized关键字,因为volatile关键字不具备原子性操作语义,我们在使用volatile关键字的时候也是充分利用它的可见性以及有序性(防止重排序)特点。

  1. 开关控制-利用可见性的特点
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    * Created by chiyuanjia on 2019/7/25.
    */
    public class ThreadCloseable extends Thread {

    //volatile 关键字保证了started线程的可见性
    private volatile boolean started = true;

    @Override
    public void run() {

    while (started) {
    //do work
    System.out.println("I am working");
    }

    }

    public void shutdown() {
    this.started = false;
    }
    }

2.状态标记顺序性

1
2
3
4
5
6
7
8
9
10
11
12
//阻止重排序
private volatile boolean initialized = false;
private Context context;
public Context load() {
if(!initialized){
context = loadContext();
//如果这里的initialized变量不是volatile的,那么指令重排序后
//假设 initialized = true;重排到context = loadContext();之前多线程访问情况下就会出现问题
initialized = true;
}
return context;
}

3.单例模式的double-check也利用了volatile的有序性

#volatile和synchronized对比
(1)使用上的区别

  • volatile关键字只能用于修改实例变量或者类变量,不能用于修改方法以及方法参数和局部变量、常量等。
  • synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者语句块。
  • volatile修饰的变量可以为null,synchronized关键字同步块的monitor对象不能为null。
    (2)对原子性的保证
  • volatile无法保证原子性。
  • 由于synchronized是一种排他的机制,因此被synchronized关键字修饰的同步代码是无法被中途打断的,因此其能够保证代码的原子性。
    (3)对可见性的保证
  • 两者均可以保证资源在多线程间的可见性,但是实现机制完全不同。
  • synchronized借助于JVM指令monitor enter 和 monitor exit对通过排他的方式使得同步代码串行化,在monitor exit时所有共享资源都会被刷新到主内存中。
  • 相比较于synchronized关键字volatile使用机器指令(偏硬件)“lock;”的方式迫使其他线程工作内存中的数据失效,需要到主内存中进行再次加载。
    (4)对有序性的保证
  • volatile关键字禁止JVM编译器以及处理器对其进行重排序。
  • 虽然synchronized关键字所修饰的同步方法也可以保证顺序性,但是这种顺序性是以程序的串行化执行换来的,在synchronized关键字所修饰的代码中代码指令也会发生指令重排序的情况,比如:
    1
    2
    3
    4
    5
    6
    synchronized(this){
    int x = 10;
    int y =20;
    x++;
    y = y+1;
    }

x和y谁先定义谁最先进行运算,对结果没有影响。达到了最终的输出结果和代码编写顺序的一致性。
(5)其他

  • volatile不会使线程陷入阻塞。
  • synchronized会使线程进入阻塞状态。