一、構建自定義的同步工具
1. 內置的條件隊列
條件隊列就如同烤面包機上的面包已好的鈴聲。如果你正在聽著它, 當面包烤好后你可以立即注意到, 并且放下手頭的事情開始品嘗面包, 如果你沒有聽見它, 你會錯過通知消息, 但是回到廚房后還是看到面包的狀態, 如果已經烤完, 就取面包, 如果未烤完, 就再次監聽鈴聲。
條件隊列中的元素是一個個正在等待相關條件的線程。每一個對象都可以作為一個條件隊列,并且Object的wait,notify和notifyAll構成了內部條件隊列的API。
wait:wait是等待的意思,調用wait會自動釋放鎖,并請求系統掛起當前線程,從而使其他線程能夠獲得這個鎖
notify:發出通知,解除阻塞條件,JVM會從這個條件隊列上等待的多個線程選擇一個來喚醒
notifyAll:發出通知,解除阻塞條件,JVM會喚醒所有在這個條件隊列上等待的線程
條件謂語:線程等待的條件
條件等待中存在一個很重要的三元關系:synchronized,wait和一個條件謂語。
條件變量由一個鎖保護,檢查條件謂語時必須先持有鎖,調用wait和notifyAll所在方法的對象必須是同一個對象。
用下面這個代碼來解析:
public synchronized V take() throws InterruptedException{
while(isEmpty()){
wait();
}
V v = get();
notifyAll();
return v;
}
在這塊代碼中,isEmpty就是take()方法的條件謂語,A線程如果判斷為空,將會調用wait,阻塞A線程,直到其它線程B操作使isEmpty為false,接著B調用notifyAll,釋放鎖后,喚醒線程A。
notify和notifyAll的區別:
大多數情況下,應該優先選擇notifyAll。假如線程A在條件隊列上等待條件謂語PA,線程B在同一個條件隊列上等待條件謂語PB,假如線程C將PB變為真,且調用notify,JVM將從眾多的等待線程選擇其中A來喚醒,但是A看到PA仍然為false,于是繼續等待,然而線程B本可以開始執行,卻沒有被喚醒。
只有滿足一下兩個條件時,才能用單一的notify而不是notifyAll:
1. 所有等待線程的類型都相同
2. 單進單出:在條件變量上的每次通知,最多只能喚醒一個線程來執行
如果有10個線程在條件隊列中等待, 調用notifyAll會喚醒每一個線程, 讓它們去競爭鎖, 然后它們中的大多數或者全部又回到休眠狀態, 這意味著每一個激活單一線程執行的事件, 都會帶來大量的上下文切換, 和大量競爭鎖的請求
2. Condition對象
內置的條件隊列有一些缺陷,每一個內置鎖都只能由一個相關聯的條件隊列。如果想要編寫一個帶有多個條件謂語的并發對象,可以使用Lock和Condition。
一個Condition和一個單獨的Lock相關聯, 調用Lock.newCondition()方法, 可以創建一個Condition。每個Lock可以有任意數量的Condition對象. wait, notify, notifyAll在Condition中都有對應的:await, signal, signalAll, 而且一定要使用后者!
示例:
public class ConditionBoundedBuffer<T> {
private static final int BUFFER_SIZE = 2;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final T[] items = (T[]) new Object[BUFFER_SIZE];
private int tail, head, count;
public void put(T x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); //
}
items[tail] = x;
if (++tail == items.length) {
tail = 0;
}
count++;
notEmpty.signal(); //
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();//
}
T x = items[head];
items[head] = null;
if (++head == items.length) {
head = 0;
}
count--;
notFull.signal(); //
return x;
} finally {
lock.unlock();
}
}
}
3.AbstractQueuedSynchronizer(AQS)
同步的實現都是基于AbstractQueuedSynchronizer(AQS),AQS是一個用于構建鎖和同步器的框架,例如ReentrantLock,Semaphore,CountDownLatch等都是基于AQS構建的。
AQS構建的容器中,最基本的就是獲取操作和釋放操作,對于CountDownLatch,獲取意味著等待并直到閉鎖到達結束狀態,對于FutureTask,獲取意味著等待直到任務已經完成。
AQS負責同步容器類中的狀態,它管理了一個整數狀態信息,可以通過getState,setState以及compareAndSetState來設置和獲取。例如ReentrantLock用它來表示線程已經重復獲取該鎖的次數,Semaphore用它來表示剩余的許可數量,FutureTask用它來表示任務的狀態(尚未開始,正在運行,已完成以及以取消)
AQS獲取操作可能是獨占的, 就像ReentrantLock一樣, 也可能是非獨占的, 就像Semaphore和CountDownLatch一樣, 這取決于不同的Synchronizer
下面使用AQS來實現一個閉鎖:(事實上,同步容器就是這樣做的)
public class OneShotLatch {
private final Sync sync = new Sync();
private class Sync extends AbstractQueuedSynchronizer {
@Override
protected int tryAcquireShared(int arg) {
return (getState() == 1) ? 1 : -1;
}
@Override
protected boolean tryReleaseShared(int arg) {
setState(1);
return true;
}
}
public void signal() {
sync.releaseShared(0);
}
public void await() throws InterruptedException {
sync.acquireInterruptibly(0);
}
}
代碼中通過AQS管理閉鎖的狀態:關閉0, 打開1。await方法調用AQS的acqurieSharedInterruptibly, 后者隨后請求OneShotLatch中的tryAcquireShared方法必須返回一個值來表明操作能否繼續執行. 如果閉鎖已經事先打開, tryAcquireShared會返回成功, 并允許線程通過; 否則他會返回一個值, 表明獲取請求的嘗試失敗. acquireSharedInterruptibly方法處理失敗的方式, 是把線程植入一個隊列中, 該隊列中的元素都是等待中的線程, signal調用releaseShared, 進而導致tryReleaseShared被調用. tryReleasseShared的實現無條件地把閉鎖的狀態設置為打開, 通過返回值表明Synchronizer處于完全釋放的狀態
4.java.util.concurrent同步類中的AQS
java.util.concurrent同步類都沒有直接擴展AQS,而是將他們的功能委托給AQS子類實現。
java.util.concurrent中基于AQS開發的類有:ReentrantLock,Semaphore,ReentrantReadWriteLock,CountDownLatch,SynchronousQueue和FutureTask等。
二、原子變量和非阻塞同步機制
非阻塞算法被廣泛用于操作系統和JVM中實現線程/進程調度機制,垃圾回收機制以及鎖和其它并發數據結構。
1. 比較并交換(CAS)
概念:
1. 比較并交換有三個操作數: 內存位置V,進行比較的值A,新值B
2. 當且僅當V的值等于舊值A時, CAS才會用新值B原子化地更新V的值, 否則它什么都不會做。
3. 無論位置V的值是否等于A,CAS都會返回V的真實值。CAS的意思是: 我認為V的值應該是A, 如果是, 那么將B值賦值給V, 若不是, 則不修改, 并告訴我V的舊值為多少。
4. CAS是一項樂觀技術: 它抱著成功的希望進行更新, 并且如果另一個線程在上次檢查后更新了該變量, 它能夠發現錯誤
5. 當多個線程試圖使用CAS同時更新相同的變量時, 只有一個線程會更新變量的值,而其他的都會失敗。然而,失敗的線程不會被掛起, 他們會被告知這次賽跑失利, 但是允許重試。由于一個線程不會競爭CAS時不會被阻塞,因此它可以決定是否重試。
CAS的典型使用模式:
首先從V中讀取值A,根據A計算值B,然后通過CAS以原子操作將V的值A變為B。
一個模擬的CAS:
public class SimulateCAS {
private int value;
public synchronized int get() {
return value;
}
public synchronized int compareAndSwap(int expectedValue, int newValue) {
int oldValue = value;
if (expectedValue == oldValue) {
value = newValue;
}
return oldValue; //無論能否修改,都返回舊值
}
public synchronized boolean compareAndSet(int expectedValue, int newValue) {
return (expectedValue == compareAndSwap(expectedValue, newValue));
}
}
基于CAS實現的線程安全的計數器:
public class CasCounter {
private SimulateCAS value = new SimulateCAS();
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;
}
}
2.非阻塞算法
如果線程在持有鎖時由于阻塞IO,內存頁缺失或其他延遲導致推遲執行,那么很可能所有的線程都不能繼續執行下去。
非阻塞算法:如果在某種算法中,一個線程的失敗或者掛起不會導致其他線程也失敗或掛起,那么這種算法就被稱為非阻塞算法。
無鎖算法:如果在算法的每一個步驟都存在某個線程能夠執行下去,那么這種算法稱為無鎖算法。
無阻塞,無鎖算法:如果算法中僅將CAS用于協調線程之間的操作,并且能夠正確地實現,那么它是一種無阻塞,無鎖算法。
上面基于CAS實現的線程安全的計數器就是無阻塞算法實現
基于非阻塞的Stack實現:
public class ConcurrentStack<E> {
class Node<E> {
E item;
Node<E> next;
public Node(E item) {
this.item = item;
}
}
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));
}
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));
return oldHead.item;
}
}
基于非阻塞的鏈表:
public class LinkedQueue<E> {
static class Node<E> {
final E item;
final AtomicReference<Node<E>> next;
public Node() {
this(null, null);
}
public Node(E item, Node<E> next) {
this.item = item;
this.next = new AtomicReference<Node<E>>(next);
}
}
private final Node<E> dummy = new Node<E>();
private final AtomicReference<Node<E>> head = new AtomicReference<Node<E>>(dummy);
private final AtomicReference<Node<E>> tail = new AtomicReference<Node<E>>(dummy);
public boolean put(E item) {
Node<E> newNode = new Node<E>(item, null);
while (true) {
Node<E> curTailNode = tail.get();
Node<E> tailNextNode = curTailNode.next.get();
if (curTailNode == tail.get()) {
if (tailNextNode == null) {
// 更新尾節點下一個節點
if (curTailNode.next.compareAndSet(null, newNode)) {
// 更新成功, 將尾節點指向下一個節點
tail.compareAndSet(curTailNode, newNode);
return true;
}
} else {
// 在更新過程中, 發現尾節點的下一個節點被更新了, 將尾節點指向下一個節點
tail.compareAndSet(curTailNode, tailNextNode);
}
}
}
}
public static void main(String[] args) {
final LinkedQueue<String> queue = new LinkedQueue<String>();
new Thread(new Runnable() {
@Override
public void run() {
queue.put("item1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
queue.put("item2");
}
}).start();
}
}