深入JVM-鎖與並發


一、鎖在Java虛擬機中的實現與優化

1.1 偏向鎖

偏向鎖是JDK 1.6 提出的一種鎖優化方式。其核心思想是,如果程序沒有競爭,則取消之前已經取得鎖的線程同步操作。也就說,若某一鎖被線程獲取后,便進入偏向模式,當線程再次請求這個鎖時,無需進行相關的同步操作,從而節省了操作時間。如果在此之前有其他線程進行了鎖請求,則鎖退出偏向模式。在JVM中使用-XX:+UseBiasedLocking可以設置啟用偏向鎖。

當鎖對象處於偏向模式時,對象頭會記錄獲取鎖的線程

[JavaThread* | epoch | age | 1 | 01]

這樣,當該線程再次嘗試獲得鎖時,通過Mark Word的線程信息就可以判斷當前線程是否持有偏向鎖。

偏向鎖在鎖競爭激烈的場合沒有太強的優化效果,因為大量的競爭會導致持有鎖的線程不停地切換,鎖也很難一直保持在偏向模式,此時,使用鎖偏向不僅得不到性能的優化,反而有可能降低系統性能。

1.2 輕量級鎖

如果偏向鎖失敗,Java虛擬機會讓線程申請輕量級鎖。輕量級鎖在虛擬機內部,使用一個稱謂BasicObjectLock的對象實現,這個對象內部由一個BasicLock對象和一個持有該鎖的Java對象指針組成。BasicObjectLock對象放置在Java棧的棧幀中。在BasicLock對象內部還維護者displaced_header字段,他用於備份對象頭部的Mark Word。

當一個線程持有一個對象的鎖時,對象頭部Mark Word如下:

[ptr       | 00]  locked

末尾兩位比特為00,整個Mark Word為指向BasicLock對象的指針。由於BasicObjectLock對象在線程棧中,因此該指針必然指向持有該鎖的線程棧空間。當需要判斷某一線程是否持有該對象鎖時,也只需簡單的判斷對象頭的指針是否在當前線程的棧地址范圍即可。同時,BasicLock對象的displaced_header字段,備份了元對象的Mark Word內存。BasicObjectLock對象的obj字段則指向該對象。

1.3 鎖膨脹

當輕量級鎖失敗,虛擬機就會使用重量級鎖。在使用重量級鎖時,對象的Mark Word如下:

[ptr        | 10] monitor

末尾的2比特標記位被置為10。整個Mark Word表示指向monitor對象的指針。

1.4 自旋鎖

鎖膨脹后,進入ObjectMonitor的enter(),線程很可能會在操作系統層面被掛起,這樣線程上下文切換的性能損失就比較大。因此,在鎖膨脹后,虛擬機會做最后的爭取,希望線程可以盡快進入臨界區而避免被操作系統掛起。一種較為有效的手段就是使用自旋鎖。

自旋鎖可以使線程在沒有取得鎖時,不被掛起,而轉而去執行一個空循環(即所謂的自旋),在若干個空循環后,線程如果可以獲得鎖,則繼續執行。若線程依然不能獲得鎖,才會被掛起。

使用自旋鎖后,線程被掛起的幾率相對減少,線程執行的連貫性相對加強。因此,對於那些鎖競爭不是很激烈,鎖占用時間很短的並發線程,具有一定的積極意義,但對於鎖競爭激烈,單線程鎖占用時間長的並發程序,自旋鎖在自旋等待后,往往依然無法獲得對應的鎖,不僅僅白白浪費了CPU時間,最終還是免不了執行被掛起的操作,反而浪費了系統資源。

在JDK 1.6 中,Java虛擬機提供-XX:+UseSpinning參數來開啟自旋鎖,使用-XX:PreBlockSpin參數來設置自旋鎖的等待次數。

在JDK 1.7中,自旋鎖的參數被取消,虛擬機不再支持由用戶配置自旋鎖。自旋鎖總是會執行,自旋次數也由虛擬機自行調整。

1.5 鎖消除

鎖消除是Java虛擬機在JIT編譯時,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖。通過鎖消除,可以節省毫無意義的請求鎖時間。

二、鎖在應用層的優化思路

2.1 減少鎖持有時間

public Matcher matcher(CharSequence input) {
    if (!compiled) {
        synchronized(this) {
            if (!compiled)
                compile();
        }
    }
    Matcher m = new Matcher(this, input);
    return m;
}

2.2 減小鎖粒度

典型的場景就是ConcurrentHashMap類的實現。ConcurrentHashMap將整個HshMap分成若干段(Segment),每個段都是一個子HashMap。

如果需要在ConcurrentHashMap中增加一個新的表項,並不是將整個HashMap加鎖,而是首先根據hashcode得到該表項應該被存放到哪個段中,然后對該段加鎖,並完成put()操作。在多線程環境中,如果多個線程同時進行put()操作,只要被加入的表項不存放在同一個段中,則線程間可以做到真正的並行。

2.3 鎖分離

鎖分離是減小鎖粒度的一個特例。他依據應用程序的功能特點,將一個獨占鎖分成多個鎖。一個典型的案例就是java.util.concurrent.LinkedBlockingQueue的實現。

在LinkedBlockingQueue的實現中,take()函數和put()函數分別實現了從隊列中取得數據和往隊列中zeng增加數據的功能。雖然兩個函數都對當前隊列進行了修改操作,但由於LinkedBlockingQueue是基於鏈表的,因此,兩個操作分別作用於隊列的前端和尾端,從理論上來說,兩者並不沖突。

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

2.4 鎖粗化

通常情況下,為了保證多線程間的有效並發,會要求每個線程持有鎖的時間盡量短,即在使用完公共資源后,應該立即釋放鎖。只有這樣,等待在這個鎖上的其他線程才能盡早的獲得資源執行任務。但是,凡事都有一個度,如果對同一個鎖不停地進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於性能的優化。

為此,虛擬機在遇到一連串連續的對同一鎖不斷進行請求和釋放的操作時,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步次數。這個操作叫做鎖的粗化。

三、無鎖

可以使用yi'z一種稱為非阻塞同步的方法,這種方法不需要使用“鎖”(因此稱之為無鎖),但是依然能確保數據和程序在高並發環境下,保持多線程間的一致性。

3.1 理解CAS

CAS算法的過程是這樣:它包含3個參數CAS(V,E,N)。V表示要更新的變量,E表示預期值,N表示新值。僅當V值=E值時,才會將V的值設為N,如果V值和E值不同,則說明已經有其他線程做了更新,則當前線程什么都不做。最后,CAS返回當前V的真實值。

3.2 原子操作

為了能讓CAS操作被Java應用程序充分使用,在JDK的java.util.concurrent.atomic包下,有一組使用無鎖算法實現的原子操作類,主要有AtomicInteger、AtomicIntegerArray、AtomicLong、AtomicLongArray和AtomicReference等。他們分別封裝了對整數、整數數組、長整型、長整型數組和普通對象的多線程安全操作。

3.3 LongAdder

在JDK 1.8中引入了LongAdder。結合減小鎖粒度與ConcurrentHashMap的實現,我們可以想到一種對傳統AtomicInteger等原子類的改進思路。雖然在CAS操作中沒有鎖,但是像減小鎖粒度這種分離熱點的思路依然可以使用。一種可行的方案就是仿造ConcurrentHashMap,將熱點數據分離。比如,可以將AtomicInteger的內部核心數據value分離成一個數組,每個線程訪問時,通過哈希等算法映射到其中一個數字進行計數,而最終的技術結果,則為這個數組的求和累加。其中,熱點數據value被分離成多個單元cell,每個cell獨自維護內部的值,當前對象的實際值由所有的cell累計合成,這樣,熱點就進行了有效的分離,提高了並行度。LongAdder正是使用了這種思想。

四、理解Java內存模型

  • 原子性
  • 有序性
  • 可見性
  • Happens-Before原則


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM