先定一個全文的基調,本文只是自己對鎖相關東西的一點想法,寫下來整理下,前文寫了點對鎖的理解,后文貼了點自己嘗試的無鎖數據結構,不是特別嚴謹,只是我的一點嘗試和探索。
just my humble opinion, for reference only
在并發程序中,保證訪問的有序性是非常重要的,內存模型也規范了代碼來訪問共享變量的順序,本文不太涉及虛擬機的一些實現,只是在應用層面分析下各自的有點與取舍。
在Java中,最簡單的保證訪問順序的就是用鎖了
Synchronized關鍵字
Synchronized這個關鍵字使用是最簡單的,可以加在普通方法名上相當于用this來加鎖,用在靜態方法名上相當于this.class來加鎖,也可以單獨加在代碼塊上自己制定某個對象來加鎖,這樣的例子網上數不勝數,我也不就在這多展開了,底層實現是虛擬機在字節碼上價了個montior,拋開底層,從應用層講的話,就是對對象的對象頭通過CAS重寫,簡單說下這個過程,就是每個對象都有一部分叫做“Mark Word”的空間,一般是32位,上面有著對象的類信息指針、年齡(用于GC)、hash值、以及相關鎖的信息,synchronized關鍵字其實就是每個線程走到這后去改對象頭里面鎖的信息,改成自己的了就是獲取到鎖了,改幾次都改失敗了,那么就得等著掛起了,這里我只是粗略的描述了下過程,大家可以網上搜搜synchronized底層實現,包括JVM優化的偏向鎖輕量級鎖等,都值得深究。
顯示鎖
Java中的顯示鎖最多的就是ReentrantLock了,在我的上一篇文章中分析AQS時花了很多筆墨分析其實現與特性,有問題的讀者可以點鏈接閱讀我前一篇博文。
Synchronized VS ReentrantLock
對于這兩者的比較,相同點很簡單,都是為了保證對于某些非線程安全的區域的順序訪問,不同點的話,我從功能和實現去比較下兩者
功能
synchronized這個關鍵字呢,這樣說,就是他一旦開始等這個鎖,他就會認為自己有朝一日定能拿到執行權,無比自信,然而ReentrantLock就不這樣,他并不那么堅定的認為自己一定能拿到鎖,他會試探,就有了tryLock方法,他會給自己留退路,就有了lockInterruptly方法,他不像synchronizd那樣執拗,所以tryLock方法還有帶超時機制的版本。除此之外,ReetrantLock還有綁定多個Condition功能,以及公平非公平模式,其實還有些api,可以看看源碼里面返回boolean值的方法,在這不多說啦,這就是他們在功能上的差異。實現
在文章開頭已經說到了這兩者實現的差異,一個是由JVM去實現,加個monitor保證訪問時的排他性,一個是基于AQS實現的排他類工具,也因為兩者出生的不同,synchronized未來有更多的優化余地,現在兩者性能上幾乎也沒差距,官方建議是除非你需要ReentrantLock的功能,不然就用synchronized關鍵字,最后選哪個還是看你自己咯。
Lock-Free 無鎖
說完鎖,說說無鎖把,無鎖的核心就是CompareAndSet(CAS)了,并發包里面以CAS為核心實現的工具類也特別多,可以說整個Atomic包都是把,但是CAS算法也不是萬能的,在極大的并發程度下性能是不如鎖的,這也很好理解,一群人占著僅有的資源重復做一些事,這些事又注定只有少部分人能成功,大部分的人效率自然低,Atomic包中的AtomicLong就是以CAS去實現加減操作,但是新版本的JDK中又多了LongAdder類,這是為什么?原因就是為了盡量避免前面提到的那種“大部分人都失敗少部分人成功”的現象,其思路就是分散熱點域,比如a要加1,AtomicLong就是先取出a的值,再用a+1的值去覆蓋,不斷嘗試直至成功,LongAdder的思路就是把a 分為多個數值,比如a-3, 2 ,1三個部分,然后選中一部分,做AtomicLong做的事,最后全部累加起來,但是因為a被分為多個部分,多個線程執行操作時可以很好的分散開來,不會集中在一個地方不斷嘗試,這樣做正確性也是毋庸置疑的,畢竟整體+1和部分+1肯定是相等的。
Atomic包有很多這樣的類,都可以研究研究,而對于無鎖數據結構的編寫,那就是一些experts要研究的東西了,在這里我寫兩個玩具demo,讓大家體會下。
Lock Free Queue
無鎖的隊列自己寫起來也不是太難,因為出隊入隊只在頭尾節點,所以只要處理下頭尾節點就可以,無鎖隊列的問題在于我在尾節點插入時,可能此時此刻有同樣的操作在另一個線程進行,所以我拿到的尾節點可能是過期的,所以要通過CAS嘗試。
我們拿尾節點插入來展示,頭節點讀者可以對照實現
public class LockFreeQueue<V> {
private class Node<V> {//內部節點
V value = null;
AtomicReference<Node<V>> next = null;
Node(V value, Node next) {
this.value = value;
this.next = new AtomicReference<>(next);
}
}
private AtomicReference<Node<V>> head = null;
private AtomicReference<Node<V>> tail = null;
public LockFreeQueue(){
Node<V> dummy = new Node<V>(null,null);
head = new AtomicReference<>(dummy);
tail = new AtomicReference<>(dummy);
}
public boolean enQueue(V value) {
Node<V> newNode = new Node<V>(value,null);
boolean success=false;
while(!success){//插入的時候可能有其他節點搶先入隊,tail會失效
Node<V> oldTail = tail.get();
AtomicReference<Node<V>> nextNode = oldTail.next;
success=nextNode.compareAndSet(null,newNode);
if(success) return true;
}
return false;
}
}
Lock Free List
隊列還是挺好理解的,下面我貼下我寫了大半天的無鎖單鏈表,不保證可伸縮性,簡單測試了下,因為單鏈表插入會有這樣的問題,比如A->B->C->D的鏈表,我想在BC間插一個X,當我拿到B時,按理說我應該連成B->X->C這樣的,但是有可能我拿到B的時候,C被人刪了,但是我不知道,我還是按B->X->C連的話,鏈表就被我弄斷了,同理,如果此時此刻B被人刪了,還這么連,鏈表一樣會斷,同理的同理,如果這時候有人在BC中已經差了個Y,鏈表已經是B->Y->C了,我還按B->X->C連,Y就丟了,所以要顧及的情況很多,remove方法也有如上所述的問題,網上對于無鎖鏈表的實現不多,我是參考這篇論文實現的,這篇論文將刪除分為兩步,即先標記(邏輯刪除),再實際刪除,我找了下JDK的工具類,認為AtomicMarkableReference這個類。這個類內部有個Pair(和C++中的pair差不多)就是一個二元組,第一個是持有的對象,第二個boolean標記值,符合要求,下面我貼一下代碼。
public class ConcurrentLinkedList<T> {
private class Node<T> {
public Node(T value, AtomicMarkableReference<Node<T>> next) {
this.value = value;
this.next = next;
}
T value;
AtomicMarkableReference<Node<T>> next;
}
AtomicMarkableReference<Node<T>> head
= new AtomicMarkableReference<>(new Node<>(null, null), false);
//其實這里還有邏輯分支要處理,就是尾端插入時node.next.isMarked()會
//有null異常,但因為知識演示核心思路,所以就略去了(因為水平有限,
//代碼會寫的很丑,有心的讀者可以自己實現,讓我學習學習)
public boolean insert(T after, T value) {
boolean success = false;
while (!success) {
for (Node<T> node = head.getReference(); node != null && !node.next.isMarked(); node = node.next.getReference()) {
if (node.value.equals(after) && !node.next.isMarked()) {
Node<T> nextNode = node.next.getReference();
Node<T> insertNode = new Node<>(value, new AtomicMarkableReference<>(nextNode, false));
success = node.next.compareAndSet(nextNode, insertNode, false, false);
if (success) return true;
} else if (node.next == null) {//如果已經是最后一個結點,還沒有匹配到就返回false
return false;
}
}
}
return false;
}
public boolean remove(T aim) {
boolean success = false;
while (!success) {
for (Node<T> predecessor = head.getReference(); predecessor != null && !predecessor.next.isMarked();
predecessor = predecessor.next.getReference()) {
AtomicMarkableReference<Node<T>> target = predecessor.next; //要刪除的目標節點
AtomicMarkableReference<Node<T>> successor = target.getReference().next;//目標節點的后繼結點
if (target.getReference().equals(aim) && !successor.isMarked()) {
while (!target.attemptMark(target.getReference(), true)) {}//標記已經刪除,即邏輯刪除
success = predecessor.next.compareAndSet(target.getReference(), successor.getReference(), true, false);//這里是物理刪除
if (success) return true;
} else if (successor == null)
return false;//如果目標節點的后繼結點已經是null了,說明已經到鏈表結尾了,
}
}
return false;
}
}
我自己實現的代碼已經貼出來了,就當成偽代碼看吧!自己也花了很多時間,我手動構造了點數據沒出現什么問題,但是我不知道更加復雜的場景會不會出現問題,這只是我給出的思路,not solution,just my opinion,網上我也沒找到很好的關于無鎖鏈表的實現,只有一些論文,給出了思路,當然思路和實現細節還是有差距,我貼出了我的看法,如果有讀者有更好的實現希望能放出來也讓我學習學習。
我寫這篇文章不是希望用這么點篇幅就能把很多鎖和無鎖的細節都描述的淋漓盡致甚至庖丁解牛,我更多的目的只是希望能作為一篇“雜談”談談我對無鎖和鎖的看法,給讀者提供一些我的理解,幫助讀者在自己的“深究之路上”提供更多的墊腳石,可能很多細節還是比較粗糙,歡迎吐槽。
最后再聲明下Just my humble opinion,I'm open to discussion
參考論文:(那個插圖我很早保存的,實在記不起哪里來的了,如有侵權,望通知,必刪)