深入理解volatile

JMM Java 內(nèi)存模型

Java的內(nèi)存模型指定了Java虛擬機(jī)如何與計(jì)算機(jī)的內(nèi)存進(jìn)行工作


image.png

Java內(nèi)存模型決定了一個(gè)線程對(duì)共享變量的寫(xiě)入何時(shí)對(duì)其他線程可見(jiàn),Java內(nèi)存模型定義了線程和主內(nèi)存之間的抽象關(guān)系,具體如下:
1、共享變量存儲(chǔ)在主內(nèi)存中,每個(gè)線程都可以訪問(wèn)。
2、每個(gè)線程都有私有的工作內(nèi)存。
3、工作內(nèi)存只存儲(chǔ)該線程對(duì)共享變量的副本。
4、線程不能直接操作主內(nèi)存,只有先操作了工作內(nèi)存之后才能寫(xiě)入內(nèi)存。
假設(shè)主內(nèi)存的共享變量為0,線程1和線程2分享?yè)碛泄蚕碜兞縓的副本,假設(shè)線程1此時(shí)將工作內(nèi)存中的X修改為1,同時(shí)刷新到主內(nèi)存中,當(dāng)線程2想要去使用副本X的時(shí)候,就會(huì)發(fā)現(xiàn)該變量已經(jīng)失效了,必須到主內(nèi)存中再次獲取然后存入自己的工作內(nèi)容中,這一點(diǎn)和CPU與CPU Cache之間的關(guān)系非常類(lèi)似。


image.png

當(dāng)同一個(gè)數(shù)據(jù)被分別存儲(chǔ)到了計(jì)算機(jī)的各個(gè)內(nèi)存區(qū)域時(shí),就會(huì)導(dǎo)致多個(gè)線程在各自的工作內(nèi)存中看到的可能不一樣。后面會(huì)講到Java語(yǔ)言中如何保證不通線程對(duì)某個(gè)共享變量的可見(jiàn)性。

多線程可見(jiàn)性例子

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) {
        //啟動(dòng)一個(gè)Reader線程 ,當(dāng)發(fā)現(xiàn)local_value 和 init_value 不同時(shí),則輸出 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);
                        //對(duì)localValue 進(jìn)行重新賦值
                        localValue = init_value;
                    }
                }
            }
        },"Reader").start();

        //啟動(dòng)Updater線程,主要用于對(duì)init_value的修改,當(dāng)local_value>=5的時(shí)候則退出生命周期
        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線程來(lái)得及輸出變化內(nèi)容
                        TimeUnit.SECONDS.sleep(2);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }).start();

    }
}

大家先猜一下,運(yùn)行結(jié)果是怎么樣的?可能會(huì)大失所望
運(yùn)行結(jié)果如下:

/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]

通過(guò)控制臺(tái)的輸出我們發(fā)現(xiàn):Reader線程壓根就沒(méi)有感知到init_value的變化、并且進(jìn)入了死循環(huán)線程沒(méi)有退出
我們對(duì)代碼做一個(gè)調(diào)整,將init_value變量設(shè)置為volatile:

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

運(yùn)行結(jié)果如下:

/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

為啥會(huì)發(fā)生這樣的改變、后面會(huì)慢慢講到、這里是因?yàn)関olatile可以保證多線程環(huán)境下的可見(jiàn)性、還有volatile的變量是先被寫(xiě)后再被讀(后續(xù)會(huì)講到)。

CPU緩存模型和緩存一致性

CPU在速度上的發(fā)展要快與內(nèi)存在速度上的發(fā)展,由于兩邊速度嚴(yán)重的不等,所以為了增加吞吐量,縮小CPU和內(nèi)存的速度差,建立了CPU Cache模型,就是大家所熟知的L1、L2、L3 CPU高速緩存。CPU Cache又由很多個(gè)Cache Line構(gòu)成,Cache Line可以認(rèn)為是CPU Cache中最小的緩存單元。


image.png
image.png

程序運(yùn)行過(guò)程中,會(huì)將運(yùn)算鎖需要的數(shù)據(jù)從內(nèi)存復(fù)制一份到CPU Cache中,然后進(jìn)行讀取和寫(xiě)入,當(dāng)運(yùn)算結(jié)束之后,在將CPU Cache中的最新數(shù)據(jù)刷新到內(nèi)存中,這樣通過(guò)CPU Cache在中間做交互,提高了CPU的吞吐能力。


image.png

CPU Cache雖然提高了CPU的吞吐能力,同時(shí)也帶來(lái)了一個(gè)問(wèn)題:緩存不一致的問(wèn)題,比如i++這個(gè)操作,運(yùn)行的過(guò)程如下;
1、讀取主內(nèi)存的i到CPU Cache中。
2、對(duì)i進(jìn)行加一的操作。
3、將結(jié)果寫(xiě)回到CPU Cache中。
4、將數(shù)據(jù)刷新到主內(nèi)存中。
i++在單線程的情況下不會(huì)有任何問(wèn)題,但在多線程的情況下就會(huì)有問(wèn)題,每個(gè)線程都有自己的工作內(nèi)存(對(duì)于于CPU的Cache),變量i會(huì)在多個(gè)線程的本地內(nèi)存中都存在一個(gè)副本。如果同時(shí)有兩個(gè)線程執(zhí)行i++操作,假設(shè)i的初始值為0,每一個(gè)線程都從主內(nèi)存獲取i的值存入CPU Cache中,然后經(jīng)過(guò)計(jì)算在寫(xiě)入主內(nèi)存中,很有可能i在經(jīng)過(guò)了兩次自增之后結(jié)果還是1,這就是典型的緩存不一致問(wèn)題。
主要有兩種解決方法:
1、通過(guò)總線加鎖。
2、通過(guò)緩存一致性協(xié)議。
第一種是悲觀的實(shí)現(xiàn)方式,CPU和其他組件的通信都是通過(guò)總線來(lái)進(jìn)行,會(huì)有阻塞,效率低下。

第二種:


image.png

在緩存一致性中最為出名的是Intel的MESI協(xié)議,MESI協(xié)議保證了每一個(gè)緩存匯中使用的是共享變量副本都是一致的,大概意思就是當(dāng)CPU在操作Cache中的數(shù)據(jù)時(shí),如果發(fā)現(xiàn)該變量是一個(gè)共享變量,也就是說(shuō)在其他的CPU Cache中也存在一個(gè)副本,那么進(jìn)行如下操作:
1、讀取操作,不做任何處理,只是將Cache中的數(shù)據(jù)讀取到寄存器。
2、寫(xiě)入操作,發(fā)出信號(hào)通知其他CPU將該變量的Cache line置為無(wú)效狀態(tài),其他CPU在進(jìn)行該變量讀取的時(shí)候不得不到主內(nèi)存中再次獲取。

并發(fā)編程三大特性:原子性、可見(jiàn)性、有序性

原子性

原子性是值指在一次的操作或者多次操作中,要么所有的操作全部得到執(zhí)行,要么所有的操作都不執(zhí)行。i++ 是由三個(gè)原子操作組成get i, i+1 ,set i = x,但是i++就不是原子性操作。volatile不保證原子性,synchronized保證原子性,JUC的原子性類(lèi)型保證原子性,例如:AtomicInteger,通過(guò)volatile和CAS來(lái)實(shí)現(xiàn)。

可見(jiàn)性

可見(jiàn)性是指當(dāng)一個(gè)線程對(duì)共享變量進(jìn)行了修改,那么另外的線程可以立即看到修改后的新值。例如上面我們的例子Reader線程會(huì)將init_value從內(nèi)存緩存到CPU Cache中,也就是從主內(nèi)存緩存到線程的工作內(nèi)存中,Updater線程對(duì)init_value的修改對(duì)Reader線程是不可見(jiàn)的。

有序性

有序性就是程序代碼在執(zhí)行過(guò)程中的先后順序,Java在編譯器以及運(yùn)行期的優(yōu)化,會(huì)產(chǎn)生指令重排序,導(dǎo)致了代碼的執(zhí)行順序不一定是編寫(xiě)代碼時(shí)的順序,指令重排序是在不影響運(yùn)行結(jié)果的情況下進(jìn)行重排序,對(duì)于單線程來(lái)說(shuō)指令重排序不會(huì)有問(wèn)題。例如:

int x = 10;
int y = 0;
x++;
y=20;

但是在多線程的情況下,如果有序性得不到保證,那么很有可能就會(huì)出現(xiàn)問(wèn)題,例如如下代碼:

private boolean initialized = false;
private Context context = null;
public Context load(){
     if(!initialized){
        context = loadContext();
        initialized = true;
     }
     return context;
}

在單線程情況,這段代碼重排序,錄入把 initialized = true;放到 context = loadContext();調(diào)換位置,不會(huì)有問(wèn)題,但是如果多線程情況下第二個(gè)線程在調(diào)用load方法后可能會(huì)得到一個(gè)null。

JMM如何保證原子性、可見(jiàn)性、有序性

JVM采用內(nèi)存模型的機(jī)制來(lái)屏蔽哥哥平臺(tái)與操作系統(tǒng)之間內(nèi)存訪問(wèn)的差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下達(dá)到一致的內(nèi)存訪問(wèn)效果。比如C語(yǔ)言中的整型變量,在某些平臺(tái)下占用了兩個(gè)字節(jié)的內(nèi)存,在某些平臺(tái)下則占用了四個(gè)字節(jié)的內(nèi)存,Java則在任何平臺(tái)下,int類(lèi)型就是四個(gè)字節(jié),這就是一直內(nèi)存訪問(wèn)效果。

JMM與原子性

在Java語(yǔ)言中,對(duì)基本數(shù)據(jù)類(lèi)型的變量讀取賦值操作都是原子性的,對(duì)引用類(lèi)型的變量讀取和賦值的操作也是原子性的。
1、x=10 原子操作
2、y=x 非原子操作【兩個(gè)原子操作合在一起就不是原子操作】
1)執(zhí)行線程從主內(nèi)存中讀取x的值(如果在工作內(nèi)存就直接從工作內(nèi)存獲取)
2)在執(zhí)行線程的工作內(nèi)存中修改y的值為x,然后將y的值寫(xiě)入主內(nèi)存之中。
3、y++ 自增操作 不是原子的,因?yàn)榘齻€(gè)原子操作:
1)執(zhí)行線程從主內(nèi)存中讀取y的值(如果y已存在于執(zhí)行線程的工作內(nèi)存中,則直接獲?。?,然后將其存入當(dāng)前線程的工作內(nèi)存中。
2)在執(zhí)行線程工作內(nèi)存中為y執(zhí)行加1的操作。
3)將y的值寫(xiě)入主內(nèi)存。
結(jié)論:
a、多個(gè)原子性操作在一起就不在是原子性操作了。
b、簡(jiǎn)單的讀取和賦值操作是原子性操作,將一個(gè)變量賦給另外一個(gè)變量的操作不是原子性操作。
c、Java內(nèi)存模型只保證了基本讀取和賦值的原子性操作,其他的均不保證,如果先更要使得某些代碼片段具備原子性,需使用關(guān)鍵字synchronized,或者JUC中的lock。原子封裝類(lèi):AtomicInteger等。
總結(jié):volatile不具備保證原子性的語(yǔ)義

JMM與可見(jiàn)性

在多線程的環(huán)境下,如果某個(gè)線程首次讀取共享變量,首先到主內(nèi)存獲取該變量,然后存入工作內(nèi)存中,以后只需要在工作內(nèi)存中讀取該變量即可。同樣如果對(duì)該變量執(zhí)行了修改的操作,則先將新值寫(xiě)入工作內(nèi)存中,然后在刷新至主內(nèi)存中。但是什么時(shí)候最新的值會(huì)被刷新至主內(nèi)存是不太確定的,這就解釋了為什么沒(méi)有加volatile關(guān)鍵字的時(shí)候VolatileFoo中的Reader線程始終無(wú)法獲取到init_value最新的變化。
Java提供三種方式來(lái)保證可見(jiàn)性:
1)使用關(guān)鍵字volatile,共享資源的讀操作直接在內(nèi)存中進(jìn)行。寫(xiě)操作是先寫(xiě)工作內(nèi)存,然后立刻刷新到主內(nèi)存中。
2)synchronized保證可見(jiàn)性,synchronized關(guān)鍵字能夠保證同一時(shí)刻只有一個(gè)線程獲得鎖,然后執(zhí)行同步方法,并且還會(huì)確保在鎖釋放之前,會(huì)將對(duì)變量的修改刷新到主內(nèi)存中。
3)通過(guò)JUC提供的顯示鎖lock也能夠保證可見(jiàn)性,Lock的lock防范能夠保證在同一時(shí)刻只有一個(gè)線程獲得鎖然后執(zhí)行同步方法,并且會(huì)確保在鎖釋放之前會(huì)將對(duì)變量的修改刷新到主內(nèi)存當(dāng)中。

注:1、JVM禁用JIT即時(shí)編譯器的后多線程環(huán)境下共享變量也具有可見(jiàn)性
例如下面代碼,如果添加JVM參數(shù) -server -Djava.compiler=NONE 或者 -Xint 多線程環(huán)境下flag共享變量就具有可見(jiàn)性
2、System.out.print() 輸出流會(huì)加鎖,也具有可見(jiàn)性。

/**
 * 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內(nèi)存模型中,允許編譯器和處理器對(duì)指令進(jìn)行重排序,在單線程的情況下,重排序不會(huì)有問(wèn)題,但是多線程的情況下,會(huì)影響程序的正確運(yùn)行。
Java提供了三種保證有序性的方式:

  • 使用關(guān)鍵字volatile。
  • synchronized關(guān)鍵字。
  • 使用顯示Lock。
    后兩者是采用同步。

Java內(nèi)存的天生有一些有序性規(guī)則-Happens-before原則。如果兩個(gè)操作無(wú)法從happens-before推導(dǎo)出來(lái),那么他們就無(wú)法保證有序性。

  • 程序次序規(guī)則:在一個(gè)線程內(nèi),代碼按照編寫(xiě)時(shí)的次序執(zhí)行,編寫(xiě)在后面的操作發(fā)生于編寫(xiě)在前面的操作之后,虛擬機(jī)還是會(huì)對(duì)程序代碼的指令進(jìn)行重排序,只要確保在一個(gè)線程內(nèi)最終的結(jié)果和代碼順序執(zhí)行的結(jié)果一致即可。
  • 鎖定規(guī)則:一個(gè)unlock操作要先行發(fā)生在對(duì)同一個(gè)鎖的lock操作。
  • volatile變量規(guī)則:對(duì)一個(gè)變量的寫(xiě)操作要早與對(duì)這個(gè)變量之后的讀操作。意思是一個(gè)變量volatile,一個(gè)線程對(duì)它進(jìn)行讀,一個(gè)線程對(duì)它進(jìn)行寫(xiě),寫(xiě)操作一定是先行發(fā)生于讀操作。
  • 傳遞規(guī)則:如果操作A先于操作B,而操作B又先于操作C,則A先于操作C。
  • 線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行于線程的任何動(dòng)作。
  • 線程中斷規(guī)則:對(duì)線程執(zhí)行interrupt()方法肯定要優(yōu)先于捕捉到中斷信號(hào),意
    思是如果線程收到了中斷信號(hào),那么在此之前勢(shì)必要有interrupt()。
  • 線程的終結(jié)規(guī)則:線程中所有的操作都要先行發(fā)生于線程的終止檢測(cè),意識(shí)是線程的任務(wù)執(zhí)行,邏輯單元執(zhí)行肯定要發(fā)生于線程死亡之前。
  • 對(duì)象的終結(jié)規(guī)則:一個(gè)對(duì)象初始化的完成先行發(fā)生于finalize()方法之前,意思是先生后死。
    總結(jié):volatile關(guān)鍵字保證有序性

volatile關(guān)鍵字深入解析

volatile具有兩個(gè)語(yǔ)義:

  • 保證了不同線程之間對(duì)共享變量操作時(shí)的可見(jiàn)性,也就是說(shuō)當(dāng)一個(gè)線程修改volatile修改的變量,另外一個(gè)線程會(huì)立即看到最新的值。
  • 禁止對(duì)指令進(jìn)行重排序操作。
    (1)理解volatile保證可見(jiàn)性:
    VolatileFoo例子,Updater線程對(duì)init_value變量的每一次更改都會(huì)使得Reader線程能夠看到(happens-before規(guī)則中,第三條volatile變量規(guī)則:對(duì)一個(gè)變量的寫(xiě)操作要早于對(duì)這個(gè)變量之后的讀操作),步驟:
  1. Reader線程從主內(nèi)存獲取init_value的值為0,并且將其緩存到本地工作內(nèi)存中。
  2. Updater線程將init_value的值在本地工作內(nèi)存中修改為1,然后立即刷新至主內(nèi)存中。
  3. Reader線程在本地工作內(nèi)存中的init_value失效。(反映到硬件上就是CPU Cache 的 Cache Line失效)
  4. 由于Reader線程的工作內(nèi)存中的init_value失效,因此需要從主內(nèi)存中從新讀取init_value的值。
    (2)理解volatile保證有序性
    volatile關(guān)鍵字對(duì)有序性的保證比較粗暴,直接靜止JVM和處理器對(duì)volatile關(guān)鍵字修改的指令重排序,但是對(duì)volatile前后無(wú)依賴關(guān)系的指令則可以隨便怎么排序。
    (3)理解volatile不保證原子性

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

/**
 * Created by chiyuanjia on 2019/7/21.
 * //沒(méi)次的運(yùn)行結(jié)果不一樣,具體原因是 i++ 不是一個(gè)原子操作,i++操作分三步:
 *    1、從主內(nèi)存中獲取i的值,然后魂村至線程工作內(nèi)存中。
 *    2、在線程工作內(nèi)存中為進(jìn)行加1的操作。
 *    3、將i的最新值寫(xiě)入主內(nèi)存中。
 *    上面三個(gè)操作單獨(dú)的每一個(gè)操作都是原子性操作,但是合起來(lái)就不是原子性操作了。
 */
public class VolatileTest {

    //使用volatile修改共享資源i
    private static volatile  int i = 0;
    //private static AtomicInteger i = new AtomicInteger(0);
    //10個(gè)線程
    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();
                    }
                    //使計(jì)算器減1
                    latch.countDown();
                }
            }).start();
        }
        //等待所有的線程完成工作
        latch.await();
        System.out.println(i);

    }
}

運(yùn)行結(jié)果:

/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

上面代碼創(chuàng)建了10個(gè)線程,每個(gè)線程執(zhí)行1000次對(duì)共享變量i的自增操作,但是最終結(jié)果可能不是10000,因?yàn)檫@段代碼的 i++ 操作其實(shí)是3個(gè)原子操作合起來(lái)的,3個(gè)原子操作合起來(lái)就不是原子操作了。

volatile的原理和實(shí)現(xiàn)機(jī)制

下面為OpenJDK下的unsafe.cpp源碼,會(huì)發(fā)現(xiàn)被volatile裝飾的變量存在于一個(gè)"lock"的前綴,源碼如下:


image.png

volatile的使用場(chǎng)景

雖然volatile有部分synchronized關(guān)鍵字的語(yǔ)義,但是volatile不可能完全替代synchronized關(guān)鍵字,因?yàn)関olatile關(guān)鍵字不具備原子性操作語(yǔ)義,我們?cè)谑褂胿olatile關(guān)鍵字的時(shí)候也是充分利用它的可見(jiàn)性以及有序性(防止重排序)特點(diǎn)。

  1. 開(kāi)關(guān)控制-利用可見(jiàn)性的特點(diǎn)
/**
 * Created by chiyuanjia on 2019/7/25.
 */
public class ThreadCloseable extends Thread {

    //volatile 關(guān)鍵字保證了started線程的可見(jiàn)性
    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.狀態(tài)標(biāo)記順序性

    //阻止重排序
    private volatile boolean initialized = false;
    private Context context;
    public Context load() {
        if(!initialized){
            context = loadContext();
            //如果這里的initialized變量不是volatile的,那么指令重排序后
            //假設(shè) initialized = true;重排到context = loadContext();之前多線程訪問(wèn)情況下就會(huì)出現(xiàn)問(wèn)題
            initialized = true;  
        }
        return context;
    }

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

volatile和synchronized對(duì)比

(1)使用上的區(qū)別

  • volatile關(guān)鍵字只能用于修改實(shí)例變量或者類(lèi)變量,不能用于修改方法以及方法參數(shù)和局部變量、常量等。
  • synchronized關(guān)鍵字不能用于對(duì)變量的修飾,只能用于修飾方法或者語(yǔ)句塊。
  • volatile修飾的變量可以為null,synchronized關(guān)鍵字同步塊的monitor對(duì)象不能為null。
    (2)對(duì)原子性的保證
  • volatile無(wú)法保證原子性。
  • 由于synchronized是一種排他的機(jī)制,因此被synchronized關(guān)鍵字修飾的同步代碼是無(wú)法被中途打斷的,因此其能夠保證代碼的原子性。
    (3)對(duì)可見(jiàn)性的保證
  • 兩者均可以保證資源在多線程間的可見(jiàn)性,但是實(shí)現(xiàn)機(jī)制完全不同。
  • synchronized借助于JVM指令monitor enter 和 monitor exit對(duì)通過(guò)排他的方式使得同步代碼串行化,在monitor exit時(shí)所有共享資源都會(huì)被刷新到主內(nèi)存中。
  • 相比較于synchronized關(guān)鍵字volatile使用機(jī)器指令(偏硬件)“l(fā)ock;”的方式迫使其他線程工作內(nèi)存中的數(shù)據(jù)失效,需要到主內(nèi)存中進(jìn)行再次加載。
    (4)對(duì)有序性的保證
  • volatile關(guān)鍵字禁止JVM編譯器以及處理器對(duì)其進(jìn)行重排序。
    +雖然synchronized關(guān)鍵字所修飾的同步方法也可以保證順序性,但是這種順序性是以程序的串行化執(zhí)行換來(lái)的,在synchronized關(guān)鍵字所修飾的代碼中代碼指令也會(huì)發(fā)生指令重排序的情況,比如:
synchronized(this){
     int x = 10;
     int y =20;
     x++;
     y = y+1;
}

x和y誰(shuí)先定義誰(shuí)最先進(jìn)行運(yùn)算,對(duì)結(jié)果沒(méi)有影響。達(dá)到了最終的輸出結(jié)果和代碼編寫(xiě)順序的一致性。
(5)其他

  • volatile不會(huì)使線程陷入阻塞。
  • synchronized會(huì)使線程進(jìn)入阻塞狀態(tài)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容