基礎篇:詳解鎖原理,volatile+cas、synchronized的底層實現

cscw發表於2020-10-05

  • 隨著多程式多執行緒的出現,對共享資源(裝置,資料等)的競爭往往會導致資源的使用表現為隨機無序
  • 例如:一個執行緒想在控制檯輸出"I am fine",剛寫到"I am",就被另一執行緒搶佔控制檯輸出"naughty",導致結果是"I am naughty";對於資源的被搶佔使用,我們能怎麼辦呢?當然不是涼拌,可使用鎖進行同步管理,使得資源在加鎖期間,其他執行緒不可搶佔使用

1 鎖的分類

  • 悲觀鎖
    • 悲觀鎖,每次去請求資料的時候,都認為資料會被搶佔更新(悲觀的想法);所以每次運算元據時都要先加上鎖,其他執行緒修改資料時就要等待獲取鎖。適用於寫多讀少的場景,synchronized就是一種悲觀鎖
  • 樂觀鎖
    • 在請求資料時,覺得無人搶佔修改。等真正更新資料時,才判斷此期間別人有沒有修改過(預先讀出一個版本號或者更新時間戳,更新時判斷是否變化,沒變則期間無人修改);和悲觀鎖不同的是,期間資料允許其他執行緒修改
  • 自旋鎖
    • 一句話,魔力轉轉圈。當嘗試給資源加鎖卻被其他執行緒先鎖定時,不是阻塞等待而是迴圈再次加鎖
    • 在鎖常被短暫持有的場景下,執行緒阻塞掛起導致CPU上下文頻繁切換,這可用自旋鎖解決;但自旋期間它佔用CPU空轉,因此不適用長時間持有鎖的場景

2 synchronized底層原理

  • 程式碼使用synchronized加鎖,在編譯之後的位元組碼是怎樣的呢
public class Test {
    public static void main(String[] args){
        synchronized(Test.class){
            System.out.println("hello");
        }
    }
}

擷取部分位元組碼,如下

    4: monitorenter
    5: getstatic    #9    // Field java/lang/System.out:Ljava/io/PrintStream; 
    8: ldc           #15   // String hello
    10: invokevirtual #17  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    13: aload_1
    14: monitorexit

位元組碼出現了4: monitorenter和14: monitorexit兩個指令;字面理解就是監視進入,監視退出。可以理解為程式碼塊執行前的加鎖,和退出同步時的解鎖

  • 那monitorenter和monitorexit,又揹著我們幹了啥呢?
  • 執行monitorenter指令時,執行緒會為鎖物件關聯一個ObjectMonitor物件
objectMonitor.cpp
  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;   \\用來記錄獲取該鎖的執行緒數
    _waiters      = 0,
    _recursions   = 0;    \\鎖的重入次數
    _object       = NULL;
    _owner        = NULL;  \\當前持有ObjectMonitor的執行緒
    _WaitSet      = NULL;  \\wait()方法呼叫後的執行緒等待佇列
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; \\阻塞等待佇列
    FreeNext      = NULL ;
    _EntryList    = NULL ; \\synchronized 進來執行緒的排隊佇列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;  \\自旋計算
    OwnerIsThread = 0 ;
  }
  • 每個執行緒都有兩個ObjectMonitor物件列表,分別為free和used列表,如果當前free列表為空,執行緒將向全域性global list請求分配ObjectMonitor
  • ObjectMonitor的owner、WaitSet、Cxq、EntryList這幾個屬性比較關鍵。WaitSet、Cxq、EntryList的佇列元素是包裝執行緒後的物件-ObjectWaiter;而獲取owner的執行緒,既為獲得鎖的執行緒
  • monitorenter對應的執行方法
void ATTR ObjectMonitor::enter(TRAPS)  {
    ...
    //獲取鎖:cmpxchg_ptr原子操作,嘗試將_owner替換為自己,並返回舊值
    cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
    ...
    // 重複獲取鎖,次數加1,返回
    if (cur == Self) {
        _recursions ++ ;
        return ;
    }
    //首次獲取鎖情況處理
    if (Self->is_lock_owned ((address)cur)) {
        assert (_recursions == 0, "internal state error");
        _recursions = 1 ;
        _owner = Self ;
        OwnerIsThread = 1 ;
        return ;
    }
    ...
    //嘗試自旋獲取鎖
    if (Knob_SpinEarly && TrySpin (Self) > 0) {
    ...
  • monitorexit對應的執行方void ATTR ObjectMonitor::exit(TRAPS)...程式碼太長,就不貼了。主要是recursions減1、count減少1或者如果執行緒不再持有owner(非重入加鎖)則設定owner為null,退鎖的持有狀態,並喚醒Cxq佇列的執行緒

總結

  • 執行緒遇到synchronized同步時,先會進入EntryList佇列中,然後嘗試把owner變數設定為當前執行緒,同時monitor中的計數器count加1,即獲得物件鎖。否則通過嘗試自旋一定次數加鎖,失敗則進入Cxq佇列阻塞等待
  • 執行緒執行完畢將釋放持有的owner,owner變數恢復為null,count自減1,以便其他執行緒進入獲取鎖
  • synchronized修飾方法原理也是類似的。只不過沒用monitor指令,而是使用ACC_SYNCHRONIZED標識方法的同步
    public synchronized void lock(){
        System.out.println("world");
    }
....
  public synchronized void lock();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #20                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #26                 // String world
         5: invokevirtual #28                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

  • synchronized是可重入,非公平鎖,因為entryList的執行緒會先自旋嘗試加鎖,而不是加入cxq排隊等待,不公平

3 Object的wait和notify方法原理

  • wait,notify必須是持有當前物件鎖Monitor的執行緒才能呼叫 (物件鎖代指ObjectMonitor/Monitor,鎖物件代指Object)
  • 上面有說到,當在sychronized中鎖物件Object呼叫wait時會加入waitSet佇列,WaitSet的元素物件就是ObjectWaiter
class ObjectWaiter : public StackObj {
 public:
  enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;
  enum Sorted  { PREPEND, APPEND, SORTED } ;
  ObjectWaiter * volatile _next;
  ObjectWaiter * volatile _prev;
  Thread*       _thread;
  ParkEvent *   _event;
  volatile int  _notified ;
  volatile TStates TState ;
  Sorted        _Sorted ;           // List placement disposition
  bool          _active ;           // Contention monitoring is enabled
 public:
  ObjectWaiter(Thread* thread);
  void wait_reenter_begin(ObjectMonitor *mon);
  void wait_reenter_end(ObjectMonitor *mon);
};

呼叫物件鎖的wait()方法時,執行緒會被封裝成ObjectWaiter,最後使用park方法掛起

//objectMonitor.cpp
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS){
    ...
    //執行緒封裝成 ObjectWaiter物件
    ObjectWaiter node(Self);
    node.TState = ObjectWaiter::TS_WAIT ;
    ...
    //一系列判斷操作,當執行緒確實加入WaitSet時,則使用park方法掛起
    if (node._notified == 0) {
        if (millis <= 0) {
            Self->_ParkEvent->park () ;
        } else {
            ret = Self->_ParkEvent->park (millis) ;
        }
    }

而當物件鎖使用notify()時

  • 如果waitSet為空,則直接返回
  • waitSet不為空從waitSet獲取一個ObjectWaiter,然後根據不同的Policy加入到EntryList或通過Atomic::cmpxchg_ptr指令自旋操作加入cxq佇列或者直接unpark喚醒
void ObjectMonitor::notify(TRAPS){
    CHECK_OWNER();
    //waitSet為空,則直接返回
    if (_WaitSet == NULL) {
        TEVENT (Empty-Notify) ;
        return ;
    }
    ...
    //通過DequeueWaiter獲取_WaitSet列表中的第一個ObjectWaiter
    Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ;
    ObjectWaiter * iterator = DequeueWaiter() ;
    if (iterator != NULL) {
    ....
    if (Policy == 2) {      // prepend to cxq
         // prepend to cxq
         if (List == NULL) {
             iterator->_next = iterator->_prev = NULL ;
             _EntryList = iterator ;
         } else {
            iterator->TState = ObjectWaiter::TS_CXQ ;
            for (;;) {
                ObjectWaiter * Front = _cxq ;
                iterator->_next = Front ;
                if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {
                    break ;
                }
            }
         }
     }
  • Object的notifyAll方法則對應voidObjectMonitor::notifyAll(TRAPS),流程和notify類似。不過會通過for迴圈取出WaitSet的ObjectWaiter節點,再依次喚醒所有執行緒

4 jvm對synchronized的優化

  • 先介紹下32位JVM下JAVA物件頭的結構

  • 偏向鎖

    • 未加鎖的時候,鎖標誌為01,包含雜湊值、年齡分代和偏向鎖標誌位(0)
    • 施加偏向鎖時,雜湊值和一部分無用記憶體會轉化為鎖主人的執行緒資訊,以及加鎖時的時間戳epoch,此時鎖標誌位沒變,偏向鎖標誌改為1
    • 加鎖時先判斷當前執行緒id是否與MarkWord的執行緒id是否一致,一致則執行同步程式碼;不一致則檢查偏向標誌是否偏向,未偏向則使用CAS加鎖;未偏向CAS加鎖失敗存在偏向鎖會導致偏向鎖膨脹為輕量級鎖,或者重新偏向
    • 偏向鎖只有遇到其他執行緒競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,執行緒不會主動去釋放偏向鎖
  • 輕量級鎖

    • 當發生多個執行緒競爭時,偏向鎖會變為輕量級鎖,鎖標誌位為00
    • 獲得鎖的執行緒會先將偏向鎖撤銷(在安全點),並在棧楨中建立鎖記錄LockRecord,物件的MarkWord被複制到剛建立的LockRecord,然後CAS嘗試將記錄LockRecord的owner指向鎖物件,再將鎖物件的MarkWord指向鎖,加鎖成功
    • 如果CAS加鎖失敗,執行緒會自旋一定次數加鎖,再失敗則升級為重量級鎖
  • 重量級鎖

    • 重量級鎖就是上面介紹到synchronized使用監視器Monitor實現的鎖機制
    • 競爭執行緒激烈,鎖則繼續膨脹,變為重量級鎖,也是互斥鎖,鎖標誌位為10,MarkWord其餘內容被替換為一個指向物件鎖Monitor的指標
  • 自旋鎖

    • 減少不必要的CPU上下文切換;在輕量級鎖升級為重量級鎖時,就使用了自旋加鎖的方式
  • 鎖粗化

    • 多次加鎖操作在JVM內部也是種消耗,如果多個加鎖可以合併為一個鎖,就可減少不必要的開銷
Test.class
//編譯器會考慮將兩次加鎖合併
public void test(){
    synchronized(this){
        System.out.println("hello");   
    }
    synchronized(this){
        System.out.println("world");   
    }
}
  • 鎖消除
    • 刪除不必要的加鎖操作,如果變數是獨屬一個執行緒的棧變數,加不加鎖都是安全的,編譯器會嘗試消除鎖
    • 開啟鎖消除需要在JVM引數上設定-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
//StringBuffer的append操作會加上synchronized,
//但是變數buf不加鎖也安全的,編譯器會把鎖消除
public void test() {
    StringBuffer buf = new StringBuffer();
    buf.append("hello").append("world");
}
  • 其他鎖優化方法
    • 分段鎖,分段鎖也並非一種實際的鎖,而是一種思想;ConcurrentHashMap是學習分段鎖的最好實踐。主要是將大物件拆成小物件,然後對大物件的加鎖操作變成對小物件加鎖,增加了並行度

5 CAS的底層原理

  • volatile int i = 0; i++中,volatile型別的讀寫是原子同步的,但是i++卻不能保證同步性,我們該怎麼呢?
  • 可以使用synchronized加鎖;還有就是用CAS(比較並交換),使用樂觀鎖的思想同步,先判斷共享變數是否改變,沒有則更新。下面看看不同步版本的CAS
int expectedValue = 1;
public boolean compareAndSet(int newValue) {
    if(expectedValue == 1){
        expectedValue = newValue;
        return ture;
    }
    return false;
}

在jdk是有提供同步版的CAS解決方案,其中使用了UnSafe.java的底層方法

//UnSafe.java
    @HotSpotIntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset, int expected, int x) ..
    @HotSpotIntrinsicCandidate
    public final native int compareAndExchangeInt(Object o, long offset, int expected, int x)...

我們再來看看本地方法,Unsafe.cpp中的compareAndSwapInt

//unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

在Linux的x86,Atomic::cmpxchg方法的實現如下

/**
    1 __asm__表示彙編的開始;
    2 volatile表示禁止編譯器優化;//禁止指令重排
    3 LOCK_IF_MP是個行內函數,
      根據當前系統是否為多核處理器,
      決定是否為cmpxchg指令新增lock字首 //記憶體屏障
*/
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

到這一步,可以總結到:jdk提供的CAS機制,在彙編層級,會禁止變數兩側的指令優化,然後使用cmpxchg指令比較並更新變數值(原子性),如果是多核則使用lock鎖定(快取鎖、MESI)

6 CAS同步操作的問題

  • ABA問題
    • 執行緒X準備將變數的值從A改為B,然而這期間執行緒Y將變數的值從A改為C,然後再改為A;最後執行緒X檢測變數值是A,並置換為B。但實際上,A已經不再是原來的A了
    • 解決方法,是把變數定為唯一型別。值可以加上版本號,或者時間戳。如加上版本號,執行緒Y的修改變為A1->B2->A3,此時執行緒X再更新則可以判斷出A1不等於A3
  • 只能保證一個共享變數的原子操作
    • 只保證一個共享變數的原子操作,對多個共享變數同步時,迴圈CAS是無法保證操作的原子

7 基於volatile + CAS 實現同步鎖的原理

  • CAS只能同步一個變數的修改,我們又應該如何用它來鎖住程式碼塊呢?
  • 先說說實現鎖的要素
    • 1 同步程式碼塊同一時刻只能有一個執行緒能執行
    • 2 加鎖操作要happens-before同步程式碼塊裡的操作,而程式碼塊裡的操作要happens-before解鎖操作
    • 3 同步程式碼塊結束後相對其他執行緒其修改的變數是可見的 (記憶體可見性)
  • 要素1:可以利用CAS的原子性來實現,任意時刻只有一個執行緒能成功操作變數
    • 先設想CAS操作的共享變數是一個關聯程式碼塊的同步狀態變數,同步開始之前先CAS更新狀態變數為加鎖狀態,同步結束之後,再CAS狀態變數為無鎖狀態
    • 如果期間有第二個執行緒來加鎖,則會發現狀態變數為加鎖狀態,則放棄執行同步程式碼塊
  • 要素2:使用volatile修飾狀態變數,禁止指令重排
    • volatile保證同步程式碼裡的操作happens-before解鎖操作,而加鎖操作happens-before程式碼塊裡的操作
  • 要素3:還是用volatile,volatile變數寫指令前後會插入記憶體屏障
    • volatile修飾的狀態變數被CAS為無鎖狀態前,同步程式碼塊的髒資料就會被更新,被各個執行緒可見
//虛擬碼
volatile state = 0 ;   // 0-無鎖 1-加鎖;volatile禁止指令重排,加入記憶體屏障
...
if(cas(state, 0 , 1)){ // 1 加鎖成功,只有一個執行緒能成功加鎖
    ...                // 2 同步程式碼塊
    cas(state, 1, 0);  // 3 解鎖時2的操作具有可見性
}

8 LockSupport瞭解一下

  • LockSupport是基於Unsafe類,由JDK提供的執行緒操作工具類,主要作用就是掛起執行緒,喚醒執行緒。Unsafe.park,unpark操作時,會呼叫當前執行緒的變數parker代理執行。Parker程式碼
JavaThread* thread=JavaThread::thread_from_jni_environment(env);
...
thread->parker()->park(isAbsolute != 0, time);
class PlatformParker : public CHeapObj {
  protected:
    //互斥變數型別
    pthread_mutex_t _mutex [1] ; 
   //條件變數型別
    pthread_cond_t  _cond  [1] ;
    ...
}

class Parker : public os::PlatformParker {  
private:  
  volatile int _counter ;  
  ...  
public:  
  void park(bool isAbsolute, jlong time);  
  void unpark();  
  ...  
}
  • 在Linux系統下,用的POSIX執行緒庫pthread中的mutex(互斥量),condition來實現執行緒的掛起、喚醒
  • 注意點:當park時,counter變數被設定為0,當unpark時,這個變數被設定為1
  • unpark和park執行順序不同時,counter和cond的狀態變化如下
    • 先park後unpark; park:counter值不變,但會設定一個cond; unpark:counter先加1,檢查cond存在,counter減為0
    • 先unpark後park;park:counter變為1,但不設定cond;unpark:counter減為0(執行緒不會因為park掛起)
    • 先多次unpark;counter也只設定為為1

9 LockSupport.park和Object.wait區別

  • 兩種方式都有具有掛起的執行緒的能力
  • 執行緒在Object.wait之後必須等到Object.notify才能喚醒
  • LockSupport可以先unpark執行緒,等執行緒執行LockSupport.park是不會掛起的,可以繼續執行
  • 需要注意的是就算執行緒多次unpark;也只能讓執行緒第一次park是不會掛起

10 AbstractQueuedSynchronizer(AQS)

  • AQS其實就是基於volatile+cas實現的鎖模板;如果需要執行緒阻塞等待,喚醒機制,則使用LockSupport掛起、喚醒執行緒
//AbstractQueuedSynchronizer.java
public class AbstractQueuedSynchronizer{
    //執行緒節點
    static final class Node {
        ...
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        ...
    }    
    ....
    //head 等待佇列頭尾節點
    private transient volatile Node head;
    private transient volatile Node tail;
    // The synchronization state. 同步狀態
    private volatile int state;  
    ...
    //提供CAS操作,狀態具體的修改由子類實現
    protected final boolean compareAndSetState(int expect, int update) {
        return STATE.compareAndSet(this, expect, update);
    }
}
  • AQS內部維護一個同步佇列,元素就是包裝了執行緒的Node
  • 同步佇列中首節點是獲取到鎖的節點,它在釋放鎖的時會喚醒後繼節點,後繼節點獲取到鎖的時候,會把自己設為首節點
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
  • 執行緒會先嚐試獲取鎖,失敗則封裝成Node,CAS加入同步佇列的尾部。在加入同步佇列的尾部時,會判斷前驅節點是否是head結點,並嘗試加鎖(可能前驅節點剛好釋放鎖),否則執行緒進入阻塞等待

在AQS還存一個ConditionObject的內部類,它的使用機制和Object.wait、notify類似

//AbstractQueuedSynchronizer.java
public class ConditionObject implements Condition, java.io.Serializable {
    //條件佇列;Node 複用了AQS中定義的Node
    private transient Node firstWaiter;
    private transient Node lastWaiter;
    ...
  • 每個Condition物件內部包含一個Node元素的FIFO條件佇列
  • 當一個執行緒呼叫Condition.await()方法,那麼該執行緒將會釋放鎖、構造Node加入條件佇列並進入等待狀態
//類似Object.wait
public final void await() throws InterruptedException{
    ...
    Node node = addConditionWaiter(); //構造Node,加入條件佇列
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        //掛起執行緒
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //notify喚醒執行緒後,加入同步佇列繼續競爭鎖
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;

  • 呼叫Condition.signal時,獲取條件佇列的首節點,將其移動到同步佇列並且利用LockSupport喚醒節點中的執行緒。隨後繼續執行wait掛起前的狀態,呼叫acquireQueued(node, savedState)競爭同步狀態
    //類似Object.notify
    private void doSignal(Node first) {
        do {
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
        } while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
    }

  • volatile+cas機制保證了程式碼的同步性和可見性,而AQS封裝了執行緒阻塞等待掛起,解鎖喚醒其他執行緒的邏輯。AQS子類只需根據狀態變數,判斷是否可獲取鎖,是否釋放鎖成功即可
  • 繼承AQS需要選性重寫以下幾個介面
protected boolean tryAcquire(int arg);//嘗試獨佔性加鎖
protected boolean tryRelease(int arg);//對應tryAcquire釋放鎖
protected int tryAcquireShared(int arg);//嘗試共享性加鎖
protected boolean tryReleaseShared(int arg);//對應tryAcquireShared釋放鎖
protected boolean isHeldExclusively();//該執行緒是否正在獨佔資源,只有用到condition才需要取實現它

11 ReentrantLock的原理

  • ReentrantLock實現了Lock介面,並使用內部類Sync(Sync繼承AbstractQueuedSynchronizer)來實現同步操作
  • ReentrantLock內部類Sync
abstract static class Sync extends AbstractQueuedSynchronizer{
    .... 
    final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //直接CAS狀態加鎖,非公平操作
                if (compareAndSetState(0, acquires)) { 
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
    ...
    //重寫了tryRelease
    protected final boolean tryRelease(int releases) {
        c = state - releases; //改變同步狀態
        ...
        //修改volatile 修飾的狀態變數
        setState(c); 
        return free;
    }
}
  • Sync的子類NonfairSync和FairSync都重寫了tryAcquire方法
  • 其中NonfairSync的tryAcquire呼叫父類的nonfairTryAcquire方法, FairSync則自己重寫tryAcquire的邏輯。其中呼叫hasQueuedPredecessors()判斷是否有排隊Node,存在則返回false(false會導致當前執行緒排隊等待鎖)
    static final class NonfairSync extends Sync {
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    ....
    static final class FairSync extends Sync {
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&   
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
    ....    

12 AQS排他鎖的例項demo

public class TwinsLock implements Lock {

    private final Sync sync = new Sync(2);
    @Override
    public void lockInterruptibly() throws InterruptedException {  throw new RuntimeException(""); }
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {throw new RuntimeException("");}
    @Override
    public Condition newCondition() {  return sync.newCondition(); }
    @Override
    public void lock() {  sync.acquireShared(1); }
    @Override
    public void unlock() {  sync.releaseShared(1); } }
    @Override
    public boolean tryLock() { return sync.tryAcquireShared(1) > -1;  }
}

再來看看Sync的程式碼

class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            if (count <= 0) {
                throw new IllegalArgumentException("count must large than zero");
            }
            setState(count);
        }
        @Override
        public int tryAcquireShared(int reduceCount) {
            for (; ; ) {
                int current = getState(); 
                int newCount = current - reduceCount;
                if (newCount < 0 || compareAndSetState(current, newCount)) {
                    return newCount;
                }
            }
        }
        @Override
        public boolean tryReleaseShared(int returnCount) {
            for (; ; ) {
                int current = getState();
                int newCount = current + returnCount;
                if (compareAndSetState(current, newCount)) {
                    return true;
                }
            }
        }
        public Condition newCondition() {
            return new AbstractQueuedSynchronizer.ConditionObject();
        }
    }

13 使用鎖,能防止執行緒死迴圈嗎

  • 答案是不一定的;對於單個資源來說是可以做的;但是多個資源會存在死鎖的情況,例如執行緒A持有資源X,等待資源Y,而執行緒B持有資源Y,等待資源X
  • 有了鎖,可以對資源加狀態控制,但是我們還需要防止死鎖的產生,打破產生死鎖的四個條件之一就行
  • 1 資源不可重複被兩個及以上的使用者佔用
  • 2 使用者持有資源並等待其他資源
  • 3 資源不可被搶佔
  • 4 多個使用者形成等待對方資源的迴圈圈

14 ThreadLocal是否可保證資源的同步

  • 當使用ThreadLocal宣告變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本
  • 從上面的概念可知,ThreadLocal其實並不能保證變數的同步性,只是給每一個執行緒分配一個變數副本

關注公眾號,大家一起交流

參考文章

相關文章