–ConcurrentHashMap的弱一致性
–SynchronousQueue的弱一致性
–Exchanger的弱一致性
–Linux核心無鎖隊列的弱一致性
–總結
經過前面一系列的源碼分析,我們基本覆蓋了JUC包的所有組件。在這諸多組件中,我們總是不斷看到一個如影隨行的東西:CAS。
相當鎖來講,它的原子粒度更小,只是作用在一個基本變數上面(比如一個Integer, Long, 或者Reference),而不像Lock那樣,全域加鎖,因此它的並發度更大。
但任何事情總是有2面,在帶來高並發度的同時,也帶來了另一個問題:“弱一致性”。
因為“弱一致性”的存在,極大的增加了我們的編碼難度。在前面ConcurrentHashMap,Exchanger, SynchronousQueue的分析中,我們都在一定程度上,感受都了“弱一致性”所帶來的編碼複雜度。
本文試圖對前面所講到的諸多“弱一致性問題“進行一個全面梳理,同時分析一下Linux核心的一個“地道的“真正的無鎖隊列,以及它所面對的弱一致性問題。
希望最終可以讓大家對”弱一致性“有一個深刻理解,這也就能更好的理解,為什麼”弱一致性“給編碼帶來了諸多複雜性。 ConcurrentHashMap的弱一致性 case1: clear函數的弱一致性
clear執行完畢,map中仍然還有元素存在。
原因: 因為是每個segment分別加鎖,clear下一個segment的時候,上一個segment的鎖已經釋放,此時其他線程可以再往裡面放元素。 case2: put進去的元素,get出來為null
原因:因為tab[index] = new HashEntry case 3: put進去的元素,get出來為空白
原因:因為get不加鎖,在put執行 count = c 那行代碼之前,雖然元素放進去了,但因為沒有happen before約束,可能get不到。(具體代碼此處就不再次列出,參見前面ConcurrentHashMap源碼分析) SynchronousQueue的弱一致性
在前面我們知道,TransferQueue/TransferStak都是基於一個單向鏈表實現的。多線程訪問這個鏈表的時候,並沒有加鎖,只是對head/tail進行了CAS訪問。因此就造成了以下的”弱一致性“問題:
//TransferQueue.transfer Object transfer(Object e, boolean timed, long nanos) { QNode s = null; // constructed/reused as needed boolean isData = (e != null); for (;;) { QNode t = tail; QNode h = head; if (t == null || h == null) continue; if (h == t || t.isData == isData) { QNode tn = t.next; if (t != tail) //上面明明賦值的t = tail,此處卻出現了t != tail。這是因為有其他線程在對tail做CAS操作。因此造成inconsist read continue; 。。。}
Exchanger的弱一致性
private Object doExchange(Object item, boolean timed, long nanos) { Node me = new Node(item); int index = hashIndex(); int fails = 0; for (;;) { Object y; Slot slot = arena[index]; if (slot == null) createSlot(index); else if ((y = slot.get()) != null && slot.compareAndSet(y, null)) { //slot裡面有人等待交換,則把slot清空,Node拿出來,倆人在Node裡面互動。把Slot讓給後面的人,做互動地點 Node you = (Node)y; if (you.compareAndSet(null, item)) {//slot清空了,正要準備在Node裡面交換呢,卻被別的線程搶了,導致you.compareAndSet失敗。可謂螳螂撲蟬,黃雀在後 LockSupport.unpark(you.waiter); return you.item; } } 。。。 }
上述問題之所以會發生,就是因為slot.comareAndSet和you.compareAndSet,各自分別都是原子的,但合在一起,卻不是原子的。 Linux核心無鎖隊列的弱一致性
在Linux核心中,有一個基於RingBuffer做的完全無鎖的隊列kfifo,連CAS都沒有用。當然,它有個前提:只能1讀1寫。
源碼連結如下:
https://github.com/opennetworklinux/linux-3.8.13/blob/master/kernel/kfifo.c
struct kfifo { unsigned char *buffer; /* the buffer holding the data */ unsigned int size; /* the size of the allocated buffer */ unsigned int in; /* data is added at offset (in % size) */ unsigned int out; /* data is extracted from off. (out % size) */ spinlock_t *lock; /* protects concurrent modifications */ };
入隊的時候,操作變數in;出隊的時候,操作變數out。
unsigned int __kfifo_in(struct __kfifo *fifo, const void *buf, unsigned int len){ unsigned int l; l = kfifo_unused(fifo); if (len > l) len = l; kfifo_copy_in(fifo, buf, len, fifo->in); fifo->in += len; //入隊,in指標前移 return len;}unsigned int __kfifo_out(struct __kfifo *fifo, void *buf, unsigned int len){ len = __kfifo_out_peek(fifo, buf, len); fifo->out += len; //出對, out指標前移 return len;}static inline unsigned int kfifo_unused(struct __kfifo *fifo){ return (fifo->mask + 1) - (fifo->in - fifo->out); //判斷未用的空間}unsigned int __kfifo_out_peek(struct __kfifo *fifo, void *buf, unsigned int len){ unsigned int l; l = fifo->in - fifo->out; if (len > l) len = l; kfifo_copy_out(fifo, buf, len, fifo->out); return len;}
從上面可以看出,無論是int, out,還是判斷隊列是否為空白/為滿,都沒有加鎖,也沒有CAS,至所以能做到這點,是因為2個前提:
(1)只有1讀1寫,一個操作in,一個操作out
(2)弱一致性:放的時候,隊列沒有滿,可能會判斷成已滿;取的時候,隊列不為空白,但判斷成已空。但沒有關係,生產者/消費者本來就是迴圈調用的,本次取不到,迴圈回來重試的時候,可能就取到了。
當然,關於kfifo,還有其他一些小技巧在裡面,此次不再詳述。大家可以參加下面的文章
http://blog.csdn.net/linyt/article/details/5764312 總結
在上面,我們列舉了諸多“弱一致性”的例子,總結下來:
(1)至所以會出現這個問題,就是因為不加鎖,或者鎖的粒度太細(CAS)。沒辦法像Lock那樣,可以有臨界區,”大面積“的加鎖,實現多個操作合在一起的原子操作。
(2)這個問題的解決辦法,通常就是”迴圈重試“。因為inconsistent read,那就再讀一次。