自旋鎖
自旋鎖(spin lock)是一個典型的對臨界資源的互斥手段,自旋鎖是基于CAS原語的,所以它是輕量級的同步操作,它的名稱來源于它的特性。自旋鎖是指當一個線程嘗試獲取某個鎖時,如果該鎖已被其他線程占用,就一直循環檢測鎖是否被釋放,而不是進入線程掛起或睡眠狀態。由于自旋鎖只不進行線程狀態的改變(掛起線程),所以當線程競爭不激烈時,它的響應速度極快(因為避免了線程調度的上下文切換)。自旋鎖適用于鎖保護的臨界區很小的情況,線程競爭不激烈的場景下。如果線程之間競爭激烈或者臨界區的操作特別耗時,那么線程的自旋操作就會耗費大量的cpu資源,所以這種情況下性能就會下降明顯。
現代計算機CPU的處理結構
由于自旋鎖受不同的硬件處理器和架構的影響,我們來簡單了解一下相關的知識,重點關注SMP和NUMA。
(1)多線程
(2)多核心
(3)SMP SMP(Symmetric Multi-Processing),對稱多處理結構的簡稱,是指在一個計算機上匯集了一組處理器(多CPU),各CPU之間共享內存子系統以及總線結構。在這種技術的支持下,一個服務器系統可以同時運行多個處理器,并共享內存和其他的主機資源。像雙至強,也就是所說的二路,這是在對稱處理器系統中最常見的一種(至強MP可以支持到四路,AMD Opteron可以支持1-8路)。也有少數是16路的。但是一般來講,SMP結構的機器可擴展性較差,很難做到100個以上多處理器,常規的一般是8個到16個,不過這對于多數的用戶來說已經夠用了。在高性能服務器和工作站級主板架構中最為常見,像UNIX服務器可支持最多256個CPU的系統。
(4)NUMA
NUMA即非一致訪問分布共享存儲技術,它是由若干通過高速專用網絡連接起來的獨立節點構成的系統,各個節點可以是單個的CPU或是SMP系統。在NUMA中,Cache 的一致性有多種解決方案,一般采用硬件技術實現對cache的一致性維護,通常需要操作系統針對NUMA訪存不一致的特性(本地內存和遠端內存訪存延遲和帶寬的不同)進行特殊優化以提高效率,或采用特殊軟件編程方法提高效率。NUMA系統的例子。這里有3個SMP模塊用高速專用網絡聯起來,組成一個節點,每個節點可以有12個CPU。像Sequent的系統最多可以達到64個CPU甚至256個CPU。顯然,這是在SMP的基礎上,再用NUMA的技術加以擴展,是這兩種技術的結合。
(4)亂序執行
(4)分枝技術
(4)控制器
自旋鎖的分類及特點
關于自旋鎖的種類大體上有四種:
(1)簡單自旋鎖(非公平)
不能保證公平性
(2)基于票據的自旋鎖(公平)
雖然解決了公平性的問題,但是多處理器系統上,每個進程/線程占用的處理器都在讀寫同一個變量serviceNum ,每次讀寫操作都必須在多個處理器緩存之間進行緩存同步,這會導致繁重的系統總線和內存的流量,大大降低系統整體的性能
(3)CLH自旋鎖(公平)
作者:CLH:Craig,Landin and Hagersten。 鎖的名稱都來源于發明人的名字首字母
CLH自旋鎖是一種基于隱式鏈表(節點里面沒有next指針)的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,它不斷輪詢前驅的狀態,如果發現前驅釋放了鎖就結束自旋。CLH隊列鎖的優點是空間復雜度低(如果有n個線程,L個鎖,每個線程每次只獲取一個鎖,那么需要的存儲空間是O(L+n),n個線程有n個。myNode,L個鎖有L個tail),CLH的一種變體被應用在了JAVA并發框架中。 CLH在SMP系統結構下該法是非常有效的。但在NUMA系統結構下,每個線程有自己的內存,如果前趨結點的內存位置比較遠,自旋判斷前趨結點的locked域,性能將大打折扣
(4)MCS自旋鎖(公平)
作者:MCS:John Mellor-Crummey and Michael Scott。 鎖的名稱都來源于發明人的名字首字母
MCS Spinlock是一種基于顯式鏈表(節點里面擁有next指針)的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,由直接前驅負責通知其結束自旋(與CLH自旋鎖不同的地方,不在輪詢前驅的狀態,而是由前驅主動通知),從而極大地減少了不必要的處理器緩存同步的次數,降低了總線和內存的開銷。而MCS是在自己的結點的locked域上自旋等待。正因為如此,它解決了CLH在NUMA系統架構中獲取locked域狀態內存過遠的問題。
實現CLH和MCS自旋鎖
關于簡單的自旋鎖和基于票號的自旋鎖前面的文章已經介紹過,這里不再重復介紹。
首先看CLH自旋鎖的實現方式:
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 24px 0px; font: 400 14px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; padding: 16px; overflow: auto; background-color: rgb(247, 247, 247); border-radius: 3px; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; tab-size: 2; hyphens: none; color: rgb(51, 51, 51); letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package concurrent.spinlock;
import java.util.concurrent.atomic.AtomicReference;
/**
-
Created by qindongliang on 2018/8/5.
*/
public class CLHLock {class Node{
//false代表沒人占用鎖
volatile boolean locked=false;
}//指向最后加入的線程
final AtomicReference<Node> tail=new AtomicReference<>(new Node());//使用ThreadLocal保證每個線程副本內都有一個Node對象
final ThreadLocal<Node> current;public CLHLock(){
//初始化當前節點的node
current=new ThreadLocal<Node>(){
@Override
protected Node initialValue() {
return new Node();
}
};}
public void lock() throws InterruptedException {
//得到當前線程的Node節點 Node own=current.get(); //修改為true,代表當前線程需要獲取鎖 own.locked=true; //設置當前線程去注冊鎖,注意在多線程下環境下,這個 //方法仍然能保持原子性,,并返回上一次的加鎖節點(前驅節點) Node preNode=tail.getAndSet(own); //在前驅節點上自旋 while(preNode.locked){ System.out.println(Thread.currentThread().getName()+" 開始自旋.... "); Thread.sleep(2000); }
}
public void unlock(){
//當前線程如果釋放鎖,只要將占用狀態改為false即可 //因為其他的線程會輪詢自己,所以volatile布爾變量改變之后 //會保證下一個線程能立即看到變化,從而得到鎖 current.get().locked=false;
}
public static void main(String[] args) throws InterruptedException {
CLHLock lock=new CLHLock(); Runnable runnable=new Runnable() { @Override public void run() { try { lock.lock(); System.out.println(Thread.currentThread().getName()+" 獲得鎖 "); //前驅釋放,do own work Thread.sleep(5000); System.out.println(Thread.currentThread().getName()+" 釋放鎖 "); lock.unlock(); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread t1=new Thread(runnable,"線程1"); Thread t2=new Thread(runnable,"線程2"); Thread t3=new Thread(runnable,"線程3"); t1.start(); t2.start(); t3.start();
}
}</pre>
在三個線程下面的輸出結果:
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 24px 0px; font: 400 14px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; padding: 16px; overflow: auto; background-color: rgb(247, 247, 247); border-radius: 3px; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; tab-size: 2; hyphens: none; color: rgb(51, 51, 51); letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">線程1 獲得鎖
線程3 開始自旋....
線程2 開始自旋....
線程3 開始自旋....
線程2 開始自旋....
線程2 開始自旋....
線程3 開始自旋....
線程1 釋放鎖
線程2 獲得鎖
線程3 開始自旋....
線程3 開始自旋....
線程3 開始自旋....
線程2 釋放鎖
線程3 獲得鎖
線程3 釋放鎖</pre>
MCS鎖的實現方式:
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 24px 0px; font: 400 14px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; padding: 16px; overflow: auto; background-color: rgb(247, 247, 247); border-radius: 3px; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; tab-size: 2; hyphens: none; color: rgb(51, 51, 51); letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package concurrent.spinlock;
import java.util.concurrent.atomic.AtomicReference;
/**
-
Created by qindongliang on 2018/8/5.
*/
public class MCSLock {class Node{
volatile Node next;//后繼節點
//默認false
volatile boolean locked;
}//指向最后加入的線程
final AtomicReference<MCSLock.Node> tail=new AtomicReference<>(null);ThreadLocal<Node> current;
public MCSLock(){
//初始化當前節點的node
current=new ThreadLocal<MCSLock.Node>(){
@Override
protected MCSLock.Node initialValue() {
return new MCSLock.Node();
}
};
}public void lock() throws InterruptedException {
//獲取當前線程的Node Node own=current.get(); //獲取前驅節點 Node preNode=tail.getAndSet(own); //如果前驅節點不為null,說明有線程已經占用 if(preNode!=null){ //設置當前節點為需要占用狀態; own.locked=true; //把前面節點的next指向自己 preNode.next=own; //在自己的節點上自旋等待前驅通知 while(own.locked){ System.out.println(Thread.currentThread().getName()+" 開始自旋.... "); Thread.sleep(2000); } } System.out.println(Thread.currentThread().getName()+" 獲得了鎖.... ");
}
public void unlock(){
//獲取自己的節點
Node own=current.get();
//
if(own.next==null){
//判斷是不是自身是不是最后一個線程
if(tail.compareAndSet(own,null)){
//是的話就結束
return;
}//在判斷過程中,又有線程進來 while (own.next==null){ } } //本身解鎖,通知它的后繼節點可以工作了,不用再自旋了 own.next.locked=false; own.next=null;// for gc
}
public static void main(String[] args) {
MCSLock lock=new MCSLock(); Runnable runnable=new Runnable() { @Override public void run() { try { lock.lock(); System.out.println(Thread.currentThread().getName()+" 獲得鎖 "); //前驅釋放,do own work Thread.sleep(4000); System.out.println(Thread.currentThread().getName()+" 釋放鎖 "); lock.unlock(); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread t1=new Thread(runnable,"線程1"); Thread t2=new Thread(runnable,"線程2"); Thread t3=new Thread(runnable,"線程3"); t1.start(); t2.start(); t3.start();
}
}</pre>
輸出結果:
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 24px 0px; font: 400 14px/1.45 Consolas, "Liberation Mono", Menlo, Courier, monospace; padding: 16px; overflow: auto; background-color: rgb(247, 247, 247); border-radius: 3px; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; tab-size: 2; hyphens: none; color: rgb(51, 51, 51); letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">線程1 獲得了鎖....
線程2 開始自旋....
線程3 開始自旋....
線程1 獲得鎖
線程3 開始自旋....
線程2 開始自旋....
線程1 釋放鎖
線程3 開始自旋....
線程2 開始自旋....
線程3 獲得了鎖....
線程3 獲得鎖
線程2 開始自旋....
線程2 開始自旋....
線程2 開始自旋....
線程3 釋放鎖
線程2 獲得了鎖....
線程2 獲得鎖
線程2 釋放鎖</pre>
詳細介紹,在代碼的注釋里面寫的很清楚了,這里再給一張圖來幫助我們我們理解基于鏈表實現公平方式的過程:
CLH 對比 MCS
(1)從代碼實現來看,CLH比MCS要簡單得多。
(2)從自旋的條件來看,CLH是在前驅節點的屬性上自旋,而MCS是在本地屬性變量上自旋。
(3)從鏈表隊列來看,CLH的隊列是隱式的,CLHNode并不實際持有下一個節點;MCS的隊列是物理存在的。
(4)CLH鎖釋放時只需要改變自己的屬性,MCS鎖釋放則需要改變后繼節點的屬性。
(5)CLH適合CPU個數不多的計算機硬件架構上,MCS則適合擁有很多CPU的硬件架構上
(6)CLH和MCS實現的自旋鎖都是不可重入的
總結
本文主要介紹了目前主流的4種自旋鎖的特點和實現,此外眾所周知,AbstractQueuedSynchronizer是Java并發包的基石之一,而CLH鎖的原理和思想則是AbstractQueuedSynchronizer的基石之一,JDK里面的CLH鎖的是增強改進后的CLH鎖。理解清楚CLH鎖的原理和實現對后面學習和理解AbstractQueuedSynchronizer是非常重要的。最后文中所有的代碼已經上傳我的github上,感興趣的朋友可以研究學習。