Java并發(fā)機制的底層實現(xiàn)原理

????Java代碼在編譯后會變成Java字節(jié)碼,字節(jié)碼被類加載器加載到JVM里,JVM執(zhí)行字節(jié)碼,最終需要轉(zhuǎn)化為匯編指令在CPU上執(zhí)行,Java中所使用的并發(fā)機制依賴于JVM的實現(xiàn)和CPU的指令。
1. volatile的應用
????在多線程并發(fā)程序中synchronized,它在多處理器中開發(fā)中保證了共享變量的”可見性“??梢娦缘囊馑际钱斠粋€程序修改了一個共享變量時,另外一個線程能莊到這個修改的值。如果volatile變量修飾符使用恰當?shù)脑?,它比synchronized的使用和執(zhí)行成本更低,因為它不會引起線程上下文的切換和調(diào)度。文本將深入分析在硬件層面上Intel處理器是如何實現(xiàn)volatile的,通過深入分析幫助我們正確地使用volatile變量。
????我們先從了解volatile的定義開始。
????1.volatile的定義和實現(xiàn)原理
????Java語言規(guī)范第3版中對volatile的定義如下:Java編輯語言允許線程訪問共享變量,為了確保共享變量共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量。Java語言提供了volatile,在某些情況下比鎖更加方便。如果一個字段被聲明成volatile,Java線程內(nèi)存模型確保所有線程看到這個變量的值是一致的。
????volatile是如何保證可見性的呢?
????有volatile變量修飾的共享變量進行寫操作的時候,多多出一行一行匯編代碼,Lock前綴的指令在多核處理器下引發(fā)了兩件事情。
????1)將當前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。
????2)這個寫回內(nèi)在的操作會使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效。
????為了提高處理速度,處理器不直接和內(nèi)存進行通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進行操作,但操作完不知道何時會寫到內(nèi)存。如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發(fā)送一條Lock前綴的指令,將這個變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。但是,就算寫回到內(nèi)存,如果其他處理器緩存的值還是舊的,再執(zhí)行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的緩存是一致的,就會實現(xiàn)緩存一致性協(xié)議,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當處理器發(fā)現(xiàn)自己緩存行對應的內(nèi)存地址被修改,就會將當前處理器的緩存行設(shè)置成無效狀態(tài),當處理器對這個數(shù)據(jù)進行修改操作的時候,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。
????具體講解一下volatile的兩條實現(xiàn)原則。
????1)Lock前綴指令會引起處理器緩存回寫到內(nèi)存。Lock前綴指令導致在執(zhí)行指令期間,聲言處理器的LOCK#信號。在多處理器環(huán)境中,LOCK#信號確保在聲言該信號期間,處理器可以獨占任何共享內(nèi)存。但是,在最近的處理器里,LOCK#信號一般不鎖總線,而是鎖緩存,畢竟鎖總線的開銷比較大。
????2)一個處理器的緩存回寫到內(nèi)存會導致其他處理器的緩存無效。IA-32處理器和Intel 64處理器使用MESI(修改、獨占、共享、無效)控制協(xié)議去維護內(nèi)部緩存和其他處理器緩存的一致性。在多核處理器系統(tǒng)中進行操作的時候,IA-32和Intel 64處理器能嗅探其他處理器訪問系統(tǒng)內(nèi)存和它們的內(nèi)部緩存。
????2.volatile的使用優(yōu)化
????著名的Java并發(fā)編輯大師Doug lea在JDK7的并發(fā)包里新增一個隊列集合類Linked-TransferQueue,它在使用volatile變量時,用一種追加字節(jié)的方式來優(yōu)化隊列出隊和入隊的性能。
????追加字節(jié)能性能性能?這種方式看起來很神奇,但如果深入理解處理器架構(gòu)就能理解其中的奧秘。LinkedTransferQueue它使用一個內(nèi)部類類型來定義隊列的頭節(jié)點(head)和尾節(jié)點(tail),而這個內(nèi)部類PaddedAtomicReference相對于父類AtomicReference只做了一件事情,就是將共享變量追加到64字節(jié)。
????為什么追加64字節(jié)能夠提高并發(fā)編輯的效率呢?因為對于Intel 酷睿 I7、酷睿、Atom和NetBurst,以及Core Solo和Pentium M處理器的L1、L2或L3緩存的高速緩存行是64個字節(jié)寬,不支持部分填充緩存行,這意味著,如果隊列的頭節(jié)點和尾節(jié)點都不足64個字節(jié)寬的話,處理器會將它們都讀到同一個高速緩存中,在多處理器下每個處理器下每個處理器都會緩存同樣的頭、尾節(jié)點,當一個處理器試圖修改頭節(jié)點時,會將整個緩存行鎖定,那么在緩存一致性機制的作用下,會導致其他處理器不能訪問自己高速緩存中的尾節(jié)點,所以在多處理器的情況下將會嚴重影響到隊列的入隊和出險效率。Doug lea使用追加到64字節(jié)的方式來填滿高速緩沖區(qū)的緩存行,避免頭節(jié)點和尾節(jié)點加載到同一個緩存行,使頭、尾節(jié)點在修改時不會互相鎖定。
????那么是不是在使用volatile時都應該追加到64字節(jié)呢?不是的,在以下兩種場景下,就不應該使用該方式。
???? 緩存行非64字節(jié)寬的處理器。如P6系統(tǒng)列奔騰處理器,它們的L1和L2高速緩存行是32個字節(jié)寬。
???? 共享變量不會被頻繁地寫。因為使用追加字節(jié)的方式需要處理器讀取更多的字節(jié)到高速緩沖區(qū),這本身就會帶來一定的性能消耗,如果共享變量不被頻繁寫的話,鎖的幾率也非常小,就沒必要通過追加字節(jié)的方式來避免相互鎖定。
????不過這種追加的方式在Java 7下可能不生效,因為Java 7 變得更加智能,它會淘汰或重新排列無用字段,需要使用其他追加字節(jié)的方式。除了volatile, Java并發(fā)編輯中應用較多的是synchronized。

2. synchronized的實現(xiàn)原理與應用
????在多線程并發(fā)編輯 中synchronized一直都是元老級角色,很多人都稱呼它為重量級鎖。但隨著Java SE 1.6對synchronized進行了各種優(yōu)化之后,有些情況下它就并不那么重了。
???? 對于普通同步方法,鎖是當前實例對象。
????對于靜態(tài)同步方法,鎖是當前類的Class對象。
????對于同步方法塊,鎖是Synchronized括號里配置的對象。
????當一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖。那么鎖到底存在哪里呢?鎖里面會存儲什么信息呢?
????從JVM規(guī)范中可以看到Synchonized在JVM里的實現(xiàn)原理,JVM基于進入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步,者的實現(xiàn)細節(jié)不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現(xiàn)的。而方法同步是使用另外一種方式實現(xiàn)的,細節(jié)在JVM規(guī)范里并沒有說明。但是,方法的同步同樣可以使用這兩個指令來實現(xiàn)。
????monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結(jié)束處和異常處,JVM要保證每個monitorenter和monitorexit與之配對。任何對象都有一個monitor與之關(guān)聯(lián),當且一個monitor被持有后,它將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權(quán),即嘗試獲取對象的鎖。
2.1 Java對象頭
????synchronized用的鎖是存在Java對象頭里的。如果對象是數(shù)組類型,則虛擬機用3個字寬(Word)存儲對象頭,如果對象是非數(shù)組類型,則用2字寬存儲對象頭。在32位虛擬機中1字寬等于4字節(jié),即32bit。

對象頭.png

2. 鎖的升級與對比
????Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了”偏向鎖“和”輕量級鎖“,在Java SE1.6中,鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài),這幾個狀態(tài)隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
????1.偏向鎖
????HotSpot的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲取,為了讓純種獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊并獲取鎖時會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經(jīng)獲得到了鎖。如果測試失敗,則需要再測試一下Mark Word偏向鎖的標識是否設(shè)置成1(表示當前是偏向鎖):如果沒有設(shè)置,則使用CAS競爭鎖;如果設(shè)置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
????(1)偏向鎖的撤銷
????偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執(zhí)行的字節(jié)碼)。它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否仍然活著,擁有偏向鎖的棧會被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程,要么恢復到無鎖或者標記對象不適合作為偏向鎖,最后喚醒暫停的線程。

偏向鎖的獲得和撤銷流程.jpg

????(2)關(guān)閉偏向鎖
????偏向鎖在Java 6 和 Java 7里是默認啟用的,但是它在應用程序啟動幾秒鐘后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲: -XX:BiasedLockingStarupDelay=0。如果你確定應用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認進入輕量級鎖狀態(tài)。
????2.2輕量級鎖
????(1)輕量級鎖加鎖
????線程在執(zhí)行同步塊之前,JVM會先在當前線程的棧楨中創(chuàng)建中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
????(2)輕量級鎖解鎖
????輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。
輕量級鎖及膨脹流程圖.jpg

????因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態(tài)。當鎖處于這個狀態(tài)下,其他線程試圖獲取鎖時,都會被阻塞,當持有鎖的線程釋放鎖之后喚醒這些線程,被喚醒的線程會進行新一輪的奪鎖之爭。
????3.鎖的優(yōu)缺點對比
鎖對比.jpg

3.原子操作的實現(xiàn)原理
????原子(atomic)本意是”不能被進一步分割的最小粒子“,而原子操作(atomic operation)意為”不可被中斷的一個或一系列操作“。在多處理器上實現(xiàn)原子操作就變得有點復雜。
????1.術(shù)語定義
????在了解原子操作前,先要了解一下相關(guān)的術(shù)語。

CPU術(shù)語定義.jpg

????2.處理器如何實現(xiàn)原子操作
????32位IA-32處理器使用基于對緩存加鎖或總線加鎖的方式來實現(xiàn)多處理器之間的原子操作。首先處理器會自動保證基本的內(nèi)存操作的原子性。處理器保證從系統(tǒng)內(nèi)存中讀取或者寫入一個字節(jié)是原子的,意思是當一個處理器讀取一個字節(jié)時,其他處理器不能訪問這個字節(jié)的內(nèi)存地址。Pentium 6 和最新的處理器能自動保證單處理器對同一個緩存行里進行16/32/64位的操作是原子的,但是復雜的內(nèi)存操作處理器是不能自動保證其原子性的,比如跨總線寬度、跨多個緩存行和跨頁表的訪問。但是,處理器提供總線鎖定和緩存鎖定兩個機制來保證復雜內(nèi)存操作的原子性。
????(1)使用總線鎖保證原子性
????第一個機制是通過總線鎖保證原子性。如果多個處理器同時對共享變量進行讀寫操作(i++就是經(jīng)典的讀改寫操作),那么共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完成之后共享變量的值會和期望的不一致。
????(2)使用緩存鎖保證原子性
????第二個機制是通過緩存鎖來保證原子性。在同一時刻,我們只需保證對某個內(nèi)存地址的操作是原子性即可,但總線鎖定把CPU和內(nèi)存的通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內(nèi)存地址的數(shù)據(jù),所以總線鎖定的開銷比較大,目前處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優(yōu)化。
????頻繁使用的內(nèi)存會緩存在處理器的L1、L2和L3高速緩存里,那么原子操作就可以直接在處理器內(nèi)部緩存中進行,并不需要聲明總線鎖,在Pentium 6 和目前的處理器中可以使用”緩存鎖定“的方式來實現(xiàn)復雜的原子性。所謂”緩存鎖定“是指內(nèi)存區(qū)域如果被緩存在處理器的緩存行中,并且在Lock操作期間被鎖定,那么當它執(zhí)行鎖操作回寫到內(nèi)存中,處理器不在總線上聲言LOCK¥信號,而是修改內(nèi)部的內(nèi)存地址,并允許它的緩存一致性機制來保證操作的原子性,因為緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內(nèi)存區(qū)哉數(shù)據(jù),當其他處理器回寫已被鎖定的緩存行的數(shù)據(jù)時,會被緩存行無效。
????但是有兩種情況下處理器不會使用緩存鎖定。
????第一種情況是:當操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部,或操作的數(shù)據(jù)跨多個緩存行(cache line)時,則處理器會調(diào)用總線鎖定。
????第二種情況是:有些處理器不支持緩存鎖定。對于Intel 486和Pentium處理器,就算鎖定的內(nèi)存區(qū)域在處理器的緩存行中也會調(diào)用總線鎖定。
????針對以上兩個機制,我們通過Intel處理器提供了很多Lock前綴的指令來實現(xiàn)。
????3.Java如何實現(xiàn)原子操作
????在Java中可以通過循環(huán) CAS的方式來實現(xiàn)原子操作。
????(1)使用循環(huán)CAS實現(xiàn)原子操作
????JVM中的CAS操作正是利用了處理器提供的CMPXCHG指令實現(xiàn)的。自旋CAS實現(xiàn)的基本思路就是瑝進行CAS操作直到成功為止。

package com.hexy.java8;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger atomicI = new AtomicInteger(0);
    private int i=0;

    public static void main(String[] args) {
        final Counter cas = new Counter();
        List<Thread> ts = new ArrayList<>(600);
        long start = System.currentTimeMillis();
        for(int j=0;j<100;j++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i=0;i<10000;i++){
                        cas.count();
                        cas.safeCount();
                    }
                }
            });
            ts.add(t);
        }
        for(Thread t : ts){
            t.start();
        }
        // 等待所有線程執(zhí)行完成
        for(Thread t : ts){
            try{
                t.join();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        System.out.println(cas.i);
        System.out.println(cas.atomicI.get());
        System.out.println(System.currentTimeMillis() - start);
    }

    // 使用CAS實現(xiàn)線程安全計數(shù)器
    private void safeCount(){
        for(;;){
            int i = atomicI.get();
            boolean suc = atomicI.compareAndSet(i, ++i);
            if(suc){
                break;
            }
        }
    }

    //非線程安全計數(shù)器
    private void count(){
        i++;
    }

}

????從Java 1.5開始,JDK的并發(fā)包里提供了一些類來支持原子操作,如AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值)。這些原子包裝類還提供了有用的工具方法,比如以原子的方式將當前值自增1和自減1.
????(2)CAS實現(xiàn)原子操作的三大問題
????在Java并發(fā)包中有一并發(fā)框架也使用了自旋CAS的方式來實現(xiàn)原子操作,比如LinkedTransferQueue類的Xfer方法。CAS雖然很高效地解決了原子操作,但是CAS仍然存在三大問題。ABA問題,循環(huán)時間長開銷大,以及只能保證一個共享變量的原子操作。
????1)ABA問題。因為CAS需要在操作值的時候,檢查值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加1,那么A→B→A就會變成1A→2B→3A。從Java1.5開始,JDK的Atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查當前引用是否等于預期引用,并且檢查當前標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值設(shè)置為給定的更新值。
????2)循環(huán)時間長開銷大。 自旋CAS如果長時間不成功,會給CPU帶來非常大的執(zhí)行開銷。如果JVM能支持處理器提供的pause指令,那么效率會有一定的提升。pause指令有兩個作用:第一,它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會消耗過多的執(zhí)行資源,延遲的時間取決于具體實現(xiàn)的版本,在一些處理器上延遲時間是零;第二,它可以避免在退出循環(huán)的時候因內(nèi)存順序沖突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU的執(zhí)行效率。
????3)只能保證一個共享變量的原子操作。當對一個共享變量執(zhí)行操作時,我們可以使用循環(huán)CAS的方式來保證原子操作,但是對多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,這個時候就可以用鎖。還有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作。比如,有兩個共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij。從Java1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象里來進行CAS操作。
????(3)使用鎖機制實現(xiàn)原子操作
????鎖機制保證了只有獲得鎖的線程才能操作鎖定的內(nèi)存區(qū)域。JVM內(nèi)部實現(xiàn)了很多種鎖機制,有偏向鎖、輕量級鎖和互斥鎖。有意思的是除了偏向鎖,JVM實現(xiàn)鎖的方式都用了循環(huán)CAS,即當一個線程想進入同步塊的時候,使用CAS的方式來獲取鎖,當它退出同步塊的時候使用循環(huán)CAS釋放鎖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

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