前言
最近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:我来了