一。引文
1.1 SMP(Symmetric Multi-Processor)
對稱多處理器結構,指服務器中多個CPU對稱工作,每個CPU訪問內存地址所需時間相同。其主要特征是共享,包含對CPU,內存,I/O等進行共享。
SMP能夠保證內存一致性,但這些共享的資源很可能成為性能瓶頸,隨着CPU數量的增加,每個CPU都要訪問相同的內存資源,可能導致內存訪問沖突,
可能會導致CPU資源的浪費。常用的PC機就屬於這種。
1.2 NUMA(Non-Uniform Memory Access)
非一致存儲訪問,將CPU分為CPU模塊,每個CPU模塊由多個CPU組成,並且具有獨立的本地內存、I/O槽口等,模塊之間可以通過互聯模塊相互訪問,
訪問本地內存的速度將遠遠高於訪問遠地內存(系統內其它節點的內存)的速度,這也是非一致存儲訪問的由來。NUMA較好地解決SMP的擴展問題,
當CPU數量增加時,因為訪問遠地內存的延時遠遠超過本地內存,系統性能無法線性增加。
二。CLH
CLH(Craig, Landin, and Hagersten locks): 是一個自旋鎖,能確保無飢餓性,提供先來先服務的公平性。
CLH鎖也是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,它不斷輪詢前驅的狀態,如果發現前驅釋放了鎖就結束自旋。
當一個線程需要獲取鎖時:
1.創建一個的QNode,將其中的locked設置為true表示需要獲取鎖
2.線程對tail域調用getAndSet方法,使自己成為隊列的尾部,同時獲取一個指向其前趨結點的引用myPred
3.該線程就在前趨結點的locked字段上旋轉,直到前趨結點釋放鎖
4.當一個線程需要釋放鎖時,將當前結點的locked域設置為false,同時回收前趨結點
如下圖,線程A需要獲取鎖,其myNode域為true,tail指向線程A的結點,然后線程B也加入到線程A后面,tail指向線程B的結點。然后線程A和B
都在其myPred域上旋轉,一旦它的myPred結點的locked字段變為false,它就可以獲取鎖。明顯線程A的myPred locked域為false,此時線程A獲取
到了鎖。
三。CLH代碼示例
public class CLHLock implements Lock { AtomicReference<QNode> tail ; ThreadLocal<QNode> myPred; ThreadLocal<QNode> myNode; public CLHLock() { tail = new AtomicReference<QNode>(new QNode()); myNode = new ThreadLocal<QNode>() { protected QNode initialValue() { return new QNode(); } }; myPred = new ThreadLocal<QNode>() { protected QNode initialValue() { return null; } }; } @Override public void lock() { QNode qnode = myNode.get(); qnode.locked = true; QNode pred = tail.getAndSet(qnode); myPred.set(pred); while (pred.locked) { } } @Override public void unlock() { QNode qnode = myNode.get(); qnode.locked = false; myNode.set(myPred.get()); } }
四。CLH分析
CLH隊列鎖的優點是空間復雜度低(如果有n個線程,L個鎖,每個線程每次只獲取一個鎖,那么需要的存儲空間是O(L+n),n個線程有n個
myNode,L個鎖有L個tail),CLH的一種變體被應用在了JAVA並發框架中(AbstractQueuedSynchronizer.Node)。CLH在SMP系統結構下
該法是非常有效的。但在NUMA系統結構下,每個線程有自己的內存,如果前趨結點的內存位置比較遠,自旋判斷前趨結點的locked域,性能
將大打折扣,一種解決NUMA系統結構的思路是MCS隊列鎖。
五。MCS
MSC與CLH最大的不同並不是鏈表是顯示還是隱式,而是線程自旋的規則不同:CLH是在前趨結點的locked域上自旋等待,而MCS是在自己的
結點的locked域上自旋等待。正因為如此,它解決了CLH在NUMA系統架構中獲取locked域狀態內存過遠的問題。
MCS隊列鎖的具體實現如下:
a. 隊列初始化時沒有結點,tail=null
b. 線程A想要獲取鎖,於是將自己置於隊尾,由於它是第一個結點,它的locked域為false
c. 線程B和C相繼加入隊列,a->next=b,b->next=c。且B和C現在沒有獲取鎖,處於等待狀態,所以它們的locked域為true,
尾指針指向線程C對應的結點
d. 線程A釋放鎖后,順着它的next指針找到了線程B,並把B的locked域設置為false。這一動作會觸發線程B獲取鎖
六。代碼實現
public class MCSLock implements Lock { AtomicReference<QNode> tail; ThreadLocal<QNode> myNode; @Override public void lock() { tail = new AtomicReference<QNode>(new QNode()); QNode qnode = myNode.get(); QNode pred = tail.getAndSet(qnode); if (pred != null) { qnode.locked = true; pred.next = qnode; // wait until predecessor gives up the lock while (qnode.locked) { } } } @Override public void unlock() { QNode qnode = myNode.get(); if (qnode.next == null) { if (tail.compareAndSet(qnode, null)) return; // wait until predecessor fills in its next field while (qnode.next == null) { } } qnode.next.locked = false; qnode.next = null; } class QNode { boolean locked = false; QNode next = null; } }