前言
最近java的concurrent庫學習的熱火朝天,有以 悲觀鎖 爲代表,AQS運用的如火純青的LinkedBlockingQueue,ArrayBlockingQueue。也有 樂觀鎖 爲代表,把 CAS+自旋 演示的出神入化的ConcurrentLinkedQueue。當然還有像ReentrantReadWriteLock,它的 獨佔鎖+共享鎖 實際運用,是教科書典範。
總之一句話,Doug Lea是佩服的五體投地。
最近開始學習CyclicBarrier,學着學着就感覺機會來了,CyclicBarrier主要用了ReentrantLock實現。被ReentrantLock鎖住的代碼一次只能由一個線程進入,我們完全可以改成 自旋+CAS 的方案,讓線程儘可能少的掛起,改造成一個無鎖版CyclicBarrier。
CyclicBarrier
CyclicBarrier 是什麼? 例如五個人(線程)要開會,但是要等到五個人都到了才行,而且開完會後可以重新等待五個人,循環往復的過程,難理解的話網上demo一堆,可以看看。
改造前,我們先看看CyclicBarrier的代碼,CyclicBarrier有這幾個重要的變量需要先了解:
-
parties: 同一批線程的個數,構造器就決定好了。
-
Generation: Generation描述着CyclicBarrier的更新換代。在CyclicBarrier中,同一批線程屬於同一代。當有parties個線程到達barrier,generation就會被更新換代。是決定線程到底是等待掛起還是喚醒返回的重要依據。
-
ReentrantLock and Condition: 線程掛起喚醒的重要工具,不必多說。
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
//lock鎖上線程
lock.lock();
try {
//分代標記
final Generation g = generation;
......
//一個線程到了,總數-1
int index = --count;
// 如果這一批線程都到了,可以觸發一個Runnable任務
if (index == 0) {
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
//喚醒所有等待線程,並更新generation
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
//如果不是這一批倒數第一個線程,都先掛起再說。
for (;;) {
.....
trip.await();
//被喚醒後比對是不是改朝換代了(generation變了),如果是,你就可以返回了。
if (g != generation)
return index;
....
}
} finally {
//解鎖
lock.unlock();
}
}
nextGeneration方法也很重要,倒數第一個線程喚醒的操作就在這裏了。
private void nextGeneration() {
// 喚醒這一批所有線程,大家都在等你,你肯定要叫醒大家
trip.signalAll();
// 改朝換代了,count要重新賦值,generation要重新賦值個對象。
count = parties;
generation = new Generation();
}
如何改造
之前看過ConcurrentLinkedQueue的源碼,可以總結一下 自旋+CAS 的編碼特點:
- 每一次自旋,cas代碼可以保證原子操作(只有一個線程能成功更改共享變量),保證操作準確性,但如果有兩個cas操作,就不能一前一後 順序執行,因爲這不是原子操作,在多線程環境下不保證操作準確性。
- 拋棄鎖思想,樹立每行代碼之間都不存在前後能夠關聯,數據能夠依賴的思想(因爲很可能就被其它線程改了), 儘可能多的考慮多線程情況,多一些if else判斷可能的衝突情況,如果一次自旋週期內發現 走不到cas代碼,或者感覺可能會出現衝突,就需要continue繼續自旋,直到走到cas代碼執行成功。
下面就是我的無鎖版CyclicBarrier
import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.LockSupport;
/**
* @program:
* @description:
* @author: qian.pan
* @create: 2019/07/11 17:09
**/
public class CyclicBarrierNoLock {
private final ConcurrentLinkedQueue<Thread> concurrentLinkedQueue;
private final int parties;
private final sun.misc.Unsafe UNSAFE;
private final long countOffset;
private volatile int count;
private volatile Object generation;
private final Runnable barrierCommand;
public CyclicBarrierNoLock(int count) throws NoSuchFieldException, IllegalAccessException {
this(count, null);
}
public CyclicBarrierNoLock(int count, Runnable barrierCommand) throws NoSuchFieldException, IllegalAccessException {
this.count = parties = count;
this.concurrentLinkedQueue = new ConcurrentLinkedQueue();
generation = new Object();
//獲取 Unsafe 內部的私有的實例化單例對象
Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
//無視權限
field.setAccessible(true);
UNSAFE = (sun.misc.Unsafe) field.get(null);
Class<?> k = CyclicBarrierNoLock.class;
countOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("count"));
this.barrierCommand = barrierCommand;
}
public int await() {
int index;
head:
while (true) {
final Object g = generation;
//防止count扣成負數
if (count < 0) {
continue head;
}
//cas更新不成功,重新自旋
if (!casDecrement()) {
continue head;
}
index = count;
//cas操作,如果 count 能從0 重新設置成 parties,說明這一批線程已經到位,執行command命令,generation更新,然後喚醒睡眠線程。
if (resetCount()) {
final Runnable command = barrierCommand;
if (command != null)
command.run();
generation = new Object();
signalAll();
return 0;
}
while (true) {
//加入掛起隊列,掛起線程。
concurrentLinkedQueue.offer(Thread.currentThread());
LockSupport.park();
//發現更新換代,返回
if (g != generation) {
return index;
}
}
}
}
private void signalAll() {
while (!concurrentLinkedQueue.isEmpty()) {
LockSupport.unpark(concurrentLinkedQueue.poll());
}
}
private final boolean casDecrement() {
return UNSAFE.compareAndSwapInt(this, countOffset, count, count - 1);
}
private final boolean resetCount() {
return UNSAFE.compareAndSwapInt(this, countOffset, 0, parties);
}
}
上面代碼看起來不像純自旋無阻塞,因爲還是利用了ConcurrentLinkedQueue來掛起暫時不用的線程,但是如果是純自旋等待的話 會無謂消耗cpu資源,我們只需要砍掉一些因爲ReentrantLock引起的阻塞豈可。
測試
我們貼上測試代碼測試我們的無鎖CyclicBarrierNoLock能否正常使用
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CyclicBarrierTest {
private static CyclicBarrierNoLock cyclicBarrier;
public static void main(String[] args) {
int num = 5;
try {
cyclicBarrier = new CyclicBarrierNoLock(num, () -> log.info("人到齊了"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
for (int i = 0; i < num; i++) {
loop(num);
}
}
private static void loop(int num) {
for (int i = 0; i < num; i++) {
Thread thread = new Thread(() -> {
try {
Thread.sleep((long) (Math.random() * 2000));
} catch (InterruptedException e) {
}
int index = cyclicBarrier.await();
log.info(Thread.currentThread().getId() + ":" + index + ":我來了");
});
thread.setDaemon(false);
thread.start();
}
}
}
測試了下沒什麼問題,至於爲什麼會先報 “人到齊了”,後纔有各線程的"我來了",是因爲代碼是先執行Command命令,後喚醒各掛起線程,CyclicBarrier代碼也一樣,試了下用CyclicBarrier測試也是這種輸出,不影響結果。
17:34:18.261 [Thread-19] INFO CyclicBarrierTest - 人到齊了
17:34:18.265 [Thread-16] INFO CyclicBarrierTest - 27:4:我來了
17:34:18.265 [Thread-3] INFO CyclicBarrierTest - 14:3:我來了
17:34:18.265 [Thread-23] INFO CyclicBarrierTest - 34:2:我來了
17:34:18.265 [Thread-4] INFO CyclicBarrierTest - 15:1:我來了
17:34:18.265 [Thread-19] INFO CyclicBarrierTest - 30:0:我來了
17:34:19.040 [Thread-21] INFO CyclicBarrierTest - 人到齊了
17:34:19.040 [Thread-1] INFO CyclicBarrierTest - 12:4:我來了
17:34:19.040 [Thread-12] INFO CyclicBarrierTest - 23:3:我來了
17:34:19.041 [Thread-6] INFO CyclicBarrierTest - 17:2:我來了
17:34:19.042 [Thread-9] INFO CyclicBarrierTest - 20:1:我來了
17:34:19.042 [Thread-21] INFO CyclicBarrierTest - 32:0:我來了