前言:
最近看到有人說可以使用 CAS + volatile 實現同步程式碼塊。
心想,確實是可以實現的呀!因為 AbstractQueuedSynchronizer(簡稱 AQS)內部就是透過 CAS + volatile(修飾同步標誌位state) 實現的同步程式碼塊。
並且ReentrantLock就是基於AQS原理來實現同步程式碼塊的;ReentrantLock原始碼學習和了解AQS原理可以參考:帶你探索ReentrantLock原始碼的快樂
今天,咱們就透過 CAS + volatile 實現一個 迷你版的AQS ;透過這個迷你版的AQS可以使大家對AQS原理更加清晰。
本文主線:
CAS操作和volatile簡述CAS + volatile = 同步程式碼塊(程式碼實現)CAS操作和volatile簡述:通過了解CAS操作和volatile來聊聊為什麼使用它們實現同步程式碼塊。
CAS操作:
CAS是什麼?
CAS是compare and swap的縮寫,從字面上理解就是比較並更新;主要是透過 處理器的指令 來保證操作的原子性 。
CAS 操作包含三個運算元:
記憶體位置(V)預期原值(A)更新值(B)簡單來說:從記憶體位置V上取到儲存的值,將值和預期值A進行比較,如果值和預期值A的結果相等,那麼我們就把新值B更新到記憶體位置V上,如果不相等,那麼就重複上述操作直到成功為止。
例如:JDK中的 unsafe 類中的 compareAndSwapInt 方法:
unsafe.compareAndSwapInt(this, stateOffset, expect, update);
stateOffset 變數值在記憶體中存放的位置;expect 期望值;update 更新值;
CAS的優點:
CAS是一種無鎖化程式設計,是一種非阻塞的輕量級的樂觀鎖;相比於synchronized阻塞式的重量級的悲觀鎖來說,效能會好很多 。
但是注意:synchronized關鍵字在不斷地最佳化下(鎖升級最佳化等),效能也變得十分的好。
volatile 關鍵字:
volatile是什麼?
volatile是java虛擬機器提供的一種輕量級同步機制。
volatile的作用:
可以保證被volatile修飾的變數的讀寫具有原子性,不保證複合操作(i++操作等)的原子性;禁止指令重排序;被volatile修飾的的變數修改後,可以馬上被其它執行緒感知到,保證可見性;通過了解CAS操作和volatile關鍵字後,才可以更加清晰地理解下面實現的同步程式碼的demo程式。
CAS + volatile = 同步程式碼塊總述同步程式碼塊的實現原理:
使用 volatile 關鍵字修飾一個int型別的同步標誌位state,初始值為0;加鎖/釋放鎖時使用CAS操作對同步標誌位state進行更新; 加鎖成功,同步標誌位值為 1,加鎖狀態; 釋放鎖成功,同步標誌位值為0,初始狀態;加鎖實現:
加鎖流程圖:
加鎖程式碼:
** * 加鎖,非公平方式獲取鎖 */public final void lock() { while (true) { // CAS操作更新同步標誌位 if (compareAndSetState(0, 1)) { // 將獨佔鎖的擁有者設定為當前執行緒 exclusiveOwnerThread = Thread.currentThread(); System.out.println(Thread.currentThread() + " lock success ! set lock owner is current thread . " + "state:" + state); try { // 睡眠一小會,模擬更加好的效果 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 跳出迴圈 break; } else { // TODO 如果同步標誌位是1,並且鎖的擁有者是當前執行緒的話,則可以設定重入,但本方法暫未實現 if (1 == state && Thread.currentThread() == exclusiveOwnerThread) { // 進行設定重入鎖 } System.out.println(Thread.currentThread() + " lock fail ! If the owner of the lock is the current thread," + " the reentrant lock needs to be set;else Adds the current thread to the blocking queue ."); // 將執行緒阻塞,並將其放入阻塞列表 parkThreadList.add(Thread.currentThread()); LockSupport.park(this); // 執行緒被喚醒後會執行此處,並且繼續執行此 while 迴圈 System.out.println(Thread.currentThread() + " The currently blocking thread is awakened !"); } }}
鎖釋放實現:
釋放鎖流程圖:
釋放鎖程式碼:
/** * 釋放鎖 * * @return */public final boolean unlock() { // 判斷鎖的擁有者是否為當前執行緒 if (Thread.currentThread() != exclusiveOwnerThread) { throw new IllegalMonitorStateException("Lock release failed ! The owner of the lock is not " + "the current thread."); } // 將同步標誌位設定為0,初始未加鎖狀態 state = 0; // 將獨佔鎖的擁有者設定為 null exclusiveOwnerThread = null; System.out.println(Thread.currentThread() + " Release the lock successfully, and then wake up " + "the thread node in the blocking queue ! state:" + state); if (parkThreadList.size() > 0) { // 從阻塞列表中獲取阻塞的執行緒 Thread thread = parkThreadList.get(0); // 喚醒阻塞的執行緒 LockSupport.unpark(thread); // 將喚醒的執行緒從阻塞列表中移除 parkThreadList.remove(0); } return true;}
完整程式碼如下:
測試程式碼:
例如上面測試程式啟動了10個執行緒同時執行同步程式碼塊,可能此時只有執行緒 thread-2 獲取到了鎖,其餘執行緒由於沒有獲取到鎖被阻塞進入到了阻塞列表中;
當獲取鎖的執行緒釋放了鎖後,會喚醒阻塞列表中的執行緒,並且是按照進入列表的順序被喚醒;此時被喚醒的執行緒會再次去嘗試獲取鎖,如果此時有新執行緒同時嘗試獲取鎖,那麼此時也存在競爭了,這就是非公平方式搶佔鎖(不會按照申請鎖的順序獲取鎖)。
擴充套件:
上面的程式碼中沒有實現執行緒自旋操作,下面看看該怎麼實現呢?
首先說說為什麼需要自旋操作:
因為在某些場景下,同步資源的鎖定時間很短,如果沒有獲取到鎖的執行緒,為了這點時間就進行阻塞的話,就有些得不償失了;因為進入阻塞時會進行執行緒上下文的切換,這個消耗是很大的;
使執行緒進行自旋的話就很大可能會避免阻塞時的執行緒上下文切換的消耗;並且一般情況下都會設定一個執行緒自旋的次數,超過這個次數後,執行緒還未獲取到鎖的話,也要將其阻塞了,防止執行緒一直自旋下去白白浪費CPU資源。
程式碼如下: