1. 程式人生 > >java同步鎖優化方案學習筆記(偏向鎖,輕量級鎖,自旋鎖,重量級鎖)

java同步鎖優化方案學習筆記(偏向鎖,輕量級鎖,自旋鎖,重量級鎖)

目錄

一,概述

二,CAS

一,概述

什麼是java的鎖?

為什麼java要有鎖?

java的鎖為什麼需要優化?

怎麼優化的?

1,java中使用synchronized關鍵字來實現同步功能,被synchronized修飾的方法或是程式碼塊,在多執行緒的情況下不能同時執行,只能挨個執行,以避免一些多執行緒併發的問題,這是java同步語句本身存在的意義。

2,在JDK1.5之前,事情就是像前面說的那麼簡單,當執行緒A和執行緒B同時執行到同步程式碼塊時,他們會爭搶同步鎖,假設執行緒A搶到了鎖,那麼執行緒B就會從執行狀態(Running)變成阻塞(Blocked)狀態,直到執行緒A退出同步程式碼塊,執行緒B才能獲得鎖,從阻塞狀態變成執行狀態,進入程式碼塊開始執行。

3,到JDK1.6,java的開發者們不再滿足於這種簡單的執行-阻塞-執行模式了,因為這麼在作業系統中切換執行緒的上下文的確挺慢,於是他們搞了一套優化的方案,也就是引入偏向鎖,輕量級鎖,自旋鎖和重量級鎖等概念,來提高同步鎖的效率。從此,synchronized還是那個synchronized,用法還是那個用法,但是JVM不一定在拿不到鎖的時候就直接阻塞執行緒了,而是有了一套更快一點的方案。

4,當鎖的競爭很少或者基本沒有時,JVM使用偏向鎖來處理同步鎖,這基本就算是沒加鎖。鎖競爭越激烈的場景,JVM會把鎖的處理方案會按照偏向鎖,輕量級鎖,自旋鎖,重量級鎖的順序不斷升級(或者叫鎖的膨脹),這些鎖的方案會消耗越來越多的資源,鎖的效率也越來越低,所以JVM能用前面的方案就不會用後面的方案。

各個鎖的具體介紹在後面。

先了解幾個基本概念

二,CAS

CAS的全稱是Compare-And-Swap,CAS演算法的基本思想是這樣的:當我們要改變一個變數的值時,先判斷變數的值是否和某個預期值相同,如果相同則修改,如果不同則不修改。這個演算法可以保證在多執行緒同時修改某個變數時,不會產生執行緒安全問題。

java中的java.util.concurrent.atomic包下的類很多都實現了這種演算法,他們使用compareAndSet()方法來實現CAS,而且往往這個compareAndSet()方法對JVM來說都是原子操作,很安全。

三,Mark Word和物件頭

java的物件由三部分組成:物件頭,例項資料,填充位元組。

非陣列物件的物件頭由兩部分組成:指向類的指標,Mark Word。

陣列物件的物件頭由三部分組成,比非陣列物件多了塊用於記錄陣列長度。

Mark Word用於記錄物件的HashCode和鎖資訊等,在32位JVM中的Mark Word長度為32bit,在64位JVM中的Mark Word長度為64bit。

Mark Word的最後2bit是鎖標誌位,代表當前物件處於哪種鎖狀態,當Mark Word處於不同的鎖狀態時,Mark Word記錄的資訊也有所不同。

32位JVM中不同鎖狀態的Mark Word記錄的資訊如下表:

鎖狀態

25bit

4bit

1bit

2bit

23bit

2bit

是否偏向鎖

鎖標誌位

無鎖

物件的HashCode

分代年齡

0

01

偏向鎖

執行緒ID

Epoch

分代年齡

1

01

輕量級鎖

指向棧中鎖記錄的指標

00

重量級鎖

指向重量級鎖的指標

10

GC標記

11

可以看到無鎖和偏向鎖的鎖標誌位都是01,只是在前面的1bit區分了這是無鎖狀態還是偏向鎖狀態。

隨著鎖的升級,Mark Word裡面的資料就會按照上表不斷變化,JVM也會按照Mark Word裡面的資訊來判斷物件鎖處於什麼狀態。

關於物件頭和Mark Word的詳細介紹見下面的連線:

下面單獨介紹各個鎖

四,偏向鎖 Baised Lock

偏向二字就是字面上的意思,說的是第一個試圖獲取鎖的執行緒,JVM會把鎖物件的Mark  Word從無鎖狀態變成偏向鎖狀態,並把執行緒id記在鎖物件的Mark Word中,當這個執行緒以後還想要獲取這個鎖時,JVM發現這個鎖物件處於偏向鎖狀態,而且執行緒id就是這個執行緒自己,就直接讓他通過,不用再進行爭搶鎖的操作了,省了CAS操作的時間。

當然這個特權只有第一個獲取鎖的執行緒才能擁有,這也就是偏向二字的意思。

如果有第二個執行緒想要來爭搶鎖,JVM發現鎖物件處於偏向鎖狀態,而且執行緒id是另外一個執行緒,新執行緒會使用CAS操作試圖爭搶物件鎖,如果成功,Word Mark中的執行緒id就會替換為新執行緒的id,如果失敗,這個偏向鎖就會升級為輕量級鎖,同樣也是改鎖物件的Mark  Word。

由此可見,偏向鎖是一種用快取空間換時間的方案,在鎖競爭不是很激烈的情況下會很有用,如果競爭比較激烈,JVM先使用偏向鎖然後又不斷進行鎖升級,鎖的效率會下降。

啟用偏向鎖的方式:

-XX:+UseBiasedLocking

-XX:BiasedLockingStartupDelay=0

關閉偏向鎖的方式:

-XX:-UseBiasedLocking

五,輕量級鎖

如果物件鎖升級為輕量級鎖,JVM會在當前執行緒的執行緒棧中開闢一塊單獨的空間叫鎖記錄(Lock Record),鎖記錄由兩部分組成,分別是Displaced hdr和Owner。

JVM會把鎖物件的Mark Word複製進去,然後把在物件Mark Word中儲存指向鎖記錄的指標,並在鎖記錄的Owner中儲存指向Mark Word的指標。這兩個儲存操作都是CAS操作。

如果儲存成功,則表示當前執行緒獲得該輕量級鎖,修改鎖物件的Mark Word鎖標誌位為00。

如果儲存失敗,JVM就檢查鎖物件的Mark Word是否已經儲存了指向當前執行緒的指標,如果有則說明當前執行緒已經獲得了這個鎖,可以繼續執行。如果沒有指向當前執行緒的指標,則代表搶鎖失敗。

當前執行緒搶鎖失敗後會用自旋鎖重試搶鎖,如果一直失敗,當前鎖會升級為重量級鎖,執行緒會被阻塞,鎖物件的Mark Word標誌位也會改為10。

六,自旋鎖 SpinLock

spin在英文中用於描述紡紗的紗輪瘋狂自轉的樣子,瞧這名字起的,一看就很耗CPU。

自旋鎖其實並不屬於鎖的狀態,從Mark Word的說明可以看到,並沒有一個鎖狀態叫自旋鎖。所謂自旋其實指的就是自己重試,當執行緒搶鎖失敗後,重試幾次,要是搶到鎖了就繼續,要是搶不到就阻塞執行緒。說白了還是為了儘量不要阻塞執行緒。

由此可見,自旋鎖是是比較消耗CPU的,因為要不斷的迴圈重試,不會釋放CPU資源。另外,加鎖時間普遍較短的場景非常適合自旋鎖,可以極大提高鎖的效率。

在JDK1.6之前,自旋鎖可以用引數來確定是否啟用,以及自旋的次數:

-XX:+UseSpinning 啟用自旋鎖

-XX:PreBlockSpin=10 自旋次數10次

而從JDK1.7開始,自旋鎖預設啟用,而且JVM有了一套確認自旋次數和自旋週期的方案:

1,如果平均負載小於CPUs則一直自旋。

2,如果有超過(CPUs/2)個執行緒正在自旋,則後來執行緒直接阻塞。

3,如果正在自旋的執行緒發現Owner發生了變化則延遲自旋時間(自旋計數)或進入阻塞。

4,如果CPU處於節電模式則停止自旋。

5,自旋時間的最壞情況是CPU的儲存延遲(CPU A儲存了一個數據,到CPU B得知這個資料之間的時間差)。

6,自旋時會適當放棄執行緒優先順序之間的差異。

java程式碼可以實現類似自旋鎖的功能,網上有很多,我貼一個jetty中寫的自旋鎖的例子:

import java.util.concurrent.atomic.AtomicReference;
public class SpinLock
{
    private final AtomicReference<Thread> _lock = new AtomicReference<>(null);
    private final Lock _unlock = new Lock();
   
    public Lock lock()
    {
        Thread thread = Thread.currentThread();
        while(true)
        {
            if (!_lock.compareAndSet(null,thread))
            {
                if (_lock.get()==thread)
                    throw new IllegalStateException("SpinLock is not reentrant");
                continue;
            }
            return _unlock;
        }
    }
   
    public boolean isLocked()
    {
        return _lock.get()!=null;
    }
   
    public boolean isLockedThread()
    {
        return _lock.get()==Thread.currentThread();
    }
   
    public class Lock implements AutoCloseable
    {
        @Override
        public void close()
        {
            _lock.set(null);
        }
    }
}

主要是思路就是用AtomicReference類的compareAndSet()方法,這個方法是原子操作,通過不斷迴圈來重試獲得鎖。

七,重量級鎖

重量級鎖就是java最原始的同步鎖,搶不到鎖的執行緒就會被阻塞,在等待池中等待啟用。

這種鎖不是公平鎖,來的早的執行緒不一定優先啟用。

關於執行緒的等待池,JVM也有一套完整的執行方案。

八,在應用層提高鎖效率的方案

上面所說的鎖的優化方案都是在JVM內部的,由JVM自己搞定。在應用層,開發者也可以採取一些措施,提高鎖的效率。

1,減少鎖的持有時間

指的是不需要同步執行的程式碼,不要放在同步程式碼塊中。同步塊中程式碼減少,鎖的持續時間短,鎖的效能會有所提高。

2,減小鎖粒度

指的是把資源分批使用不同的鎖,不同批次的資源的操作互不影響。

比如ConturrentHashMap類,把map分成多段,每段一個鎖,不在一段的資料可以同時修改。

3,鎖分離

把關係不大的操作使用不同的鎖,使這些操作互不影響。

比如LinkedBlockingQueue類,從佇列頭獲取資料的take()方法和從佇列末尾新增資料的put()方法分別使用不同的鎖,兩者互不影響。

4,鎖粗化

指的是當虛擬機器需要連續對同一把鎖進行加鎖和釋放時,儘量改成只使用一次鎖。

比如連續多個synchronized語句塊,或迴圈中的synchronized語句塊,用的是同一個物件作為鎖,那還不如直接用一個synchronized語句塊把他們都包含起來。

5,棄用synchronized關鍵字

不使用synchronized關鍵字,可以自己編寫程式碼實現類似偏向鎖、自旋鎖的功能,減少因為同步鎖而帶來的效率損耗。比如上文中jetty自己實現的自旋鎖。

以上。