java並發編程(8)原子變量和非阻塞的同步機制
原子變量和非阻塞的同步機制
一、鎖的劣勢
1.在多線程下:鎖的掛起和恢復等過程存在著很大的開銷(及時現代的jvm會判斷何時使用掛起,何時自旋等待)
2.volatile:輕量級別的同步機制,但是不能用於構建原子復合操作
因此:需要有一種方式,在管理線程之間的競爭時有一種粒度更細的方式,類似與volatile的機制,同時還要支持原子更新操作
二、CAS
獨占鎖是一種悲觀的技術--它假設最壞的情況,所以每個線程是獨占的
而CAS比較並交換:compareAndSwap/Set(A,B):我們認為內存處值是A,如果是A,將其修改為B,否則不進行操作;返回內存處的原始值或是否修改成功
如:模擬CAS操作
//模擬的CAS public class SimulatedCAS { private int value; public synchronized int get() { return value; } //CAS操作 public synchronized int compareAndSwap(int expectedValue, int newValue) { int oldValue = value; if (oldValue == expectedValue) { value= newValue; } return oldValue; } public synchronized boolean compareAndSet(int expectedValue, int newValue) { return (expectedValue == compareAndSwap(expectedValue, newValue)); } } //典型使用場景 public class CasCounter { private SimulatedCAS value; public int getValue() {return value.get(); } public int increment() { int v; do { v = value.get(); } while { (v != value.compareAndSwap(v, v + 1)); } return v + 1; } }
JAVA提供了CAS的操作
原子狀態類:AtomicXXX的CAS方法
JAVA7/8:對Map的操作:putIfAbsent、computerIfAbsent、computerIfPresent.........
三、原子變量類
AtomicRefence原子更新對象,可以是自定義的對象;如:
public class CasNumberRange { private static class IntPair { // INVARIANT: lower <= upper final int lower; //將值定義為不可變域 final int upper; //將值定義為不可變域 public IntPair(int lower, int upper) { this.lower = lower; this.upper = upper; } } private final AtomicReference<IntPair> values = new AtomicReference<IntPair>(new IntPair(0, 0)); //封裝對象 public int getLower() { return values.get().lower; } public int getUpper() { return values.get().upper; } public void setLower(int i) { while (true) { IntPair oldv = values.get(); if (i > oldv.upper) { throw new IllegalArgumentException("Can‘t set lower to " + i + " > upper"); } IntPair newv = new IntPair(i, oldv.upper); //屬性為不可變域,則每次更新新建對象 if (values.compareAndSet(oldv, newv)) { //原子更新,如果在過程中有線程修改了,則其他線程不會更新成功,因為oldv與內存處值就不同了 return; } } } //同上 public void setUpper(int i) { while (true) { IntPair oldv = values.get(); if (i < oldv.lower) throw new IllegalArgumentException("Can‘t set upper to " + i + " < lower"); IntPair newv = new IntPair(oldv.lower, i); if (values.compareAndSet(oldv, newv)) return; } } }
性能問題:使用原子變量在中低並發(競爭)下,比使用鎖速度要快,一般情況下是比鎖速度快的
四、非阻塞算法
許多常見的數據結構中都可以使用非阻塞算法
非阻塞算法:在多線程中,工作是否成功有不確定性,需要循環執行,並通過CAS進行原子操作
1、上面的CasNumberRange
2、棧的非阻塞算法:只保存頭部指針,只有一個狀態
//棧實現的非阻塞算法:單向鏈表 public class ConcurrentStack <E> { AtomicReference<Node<E>> top = new AtomicReference<Node<E>>(); public void push(E item) { Node<E> newHead = new Node<E>(item); Node<E> oldHead; do { oldHead = top.get(); newHead.next = oldHead; } while (!top.compareAndSet(oldHead, newHead));//CAS操作:原子更新操作,循環判斷,非阻塞 } public E pop() { Node<E> oldHead; Node<E> newHead; do { oldHead = top.get(); if (oldHead == null) { return null; } newHead = oldHead.next; } while (!top.compareAndSet(oldHead, newHead));//CAS操作:原子更新操作,循環判斷,非阻塞 return oldHead.item; } private static class Node <E> { public final E item; public Node<E> next; public Node(E item) { this.item = item; } } }
3、鏈表的非阻塞算法:頭部和尾部的快速訪問,保存兩個狀態,更加復雜
public class LinkedQueue <E> { private static class Node <E> { final E item; final AtomicReference<LinkedQueue.Node<E>> next; public Node(E item, LinkedQueue.Node<E> next) { this.item = item; this.next = new AtomicReference<LinkedQueue.Node<E>>(next); } } private final LinkedQueue.Node<E> dummy = new LinkedQueue.Node<E>(null, null); private final AtomicReference<LinkedQueue.Node<E>> head = new AtomicReference<LinkedQueue.Node<E>>(dummy); private final AtomicReference<LinkedQueue.Node<E>> tail = new AtomicReference<LinkedQueue.Node<E>>(dummy); //保存尾節點 public boolean put(E item) { LinkedQueue.Node<E> newNode = new LinkedQueue.Node<E>(item, null); while (true) { LinkedQueue.Node<E> curTail = tail.get(); LinkedQueue.Node<E> tailNext = curTail.next.get(); if (curTail == tail.get()) { if (tailNext != null) { // 處於中間狀態,更新尾節點為當前尾節點的next tail.compareAndSet(curTail, tailNext); } else { // 將當前尾節點的next 設置為新節點:鏈表 if (curTail.next.compareAndSet(null, newNode)) { /** * 此處即為中間狀態,雖然在這裏進行了兩次原子操作,整體不是原子的,但是通過算法保證了安全: * 原因是處於中間狀態時,如果有其他線程進來操作,則上面那個if將執行; * 上面if的操作是來幫助當前線程完成更新尾節點操作,而當前線程的更新就會失敗返回,最終則是更新成功 */ // 鏈接成功,尾節點已經改變,則將當前尾節點,設置為新節點 tail.compareAndSet(curTail, newNode); return true; } } } } } }
3.原子域更新器
上面的邏輯,實現了鏈表的非阻塞算法,使用Node來保存頭結點和尾節點
在實際的ConcurrentLinkedQueue中使用的是基於反射的AtomicReferenceFiledUpdater來包裝Node
五、ABA問題
CAS操作中容易出現的問題:
判斷值是否為A,是的話就繼續更新操作換為B;
但是如果一個線程將值A改為C,然後又改回A,此時,原線程將判斷A=A成功執行更新操作;
如果把A改為C,然後又改回A的操作,也需要視為變化,則需要對算法進行優化
解決:添加版本號,每次更新操作都要更新版本號,即使值是一樣的
java並發編程(8)原子變量和非阻塞的同步機制