本文分析并发辅助类 CountDownLatch,Semaphore,CyclicBarrier,Exchanger
CountDownLatch
同步辅助类 实现一个线程 等待其他1~N个线程执行完成,再继续执行其他代码。
构造方法
跟踪构造方法 依赖一个 内部类,这个内部类Sync 继承了AbstractQueuedSynchronizer
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
Sync(int count) {
setState(count);
}
CountDown依赖 AQS 实现。
构造方法设置state=n执行 await()进入等待,每次执行 countdown state=state-1 当state=0 执行唤醒,await()后面代码继续执行。
实现原理比较简单,依赖AQS共享锁实现,重写了tryReleaseShared 和tryAcquireShared。这里就不讲解了,贴出跟踪代码,很容易看明白。
countDown
public void countDown() {
sync.releaseShared(1);
}
AQS-
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
CountDownLatch-Sync-countDown
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
CountDownLatch-Sync
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
总结:CountDownLatch 依赖AQS 共享锁实现,实现比较简单,使用了内部类继承AQS的传统方式,重写了tryReleaseShared 和tryAcquireShared。
Semaphore
Semaphore:依赖AQS 共享锁的典型实现
CountDownLatch与Semaphore都依赖AQS共享锁实现,比较二者的实现,一定能总结出有意义的经验。
CountDownLatch
tryAcquireShared: state等于0返回1,不等于0返回-1 (1代表成功,-1代表失败)
tryReleaseShared: 每次执行 state减1,CAS更新state,如果失败循环重试,如果成功根据 state==0 返回true或false
Semaphore
tryAcquireShared: 每次执行 state减1 , 如果剩余值<0,返回负数,CAS更新state,如果失败循环重试,如果成功返回大于或等于0的数。 (大于等于0代表成功,负数代表失败)
tryReleaseShared: 每次执行 state加1 , next<curren抛出异常,CAS更新state,如果失败循环重试,如果成功返回true 。
CountDownLatch获得锁的条件是通行证达到固定条件。 Semaphore获得的锁的条件 是获取多个通行证中的一个。
CyclicBarrier
协同辅助类,可以使多个线程相互等待,直到到达了某个公共屏障点(common barrier point )。
实现逻辑是在指定数量的线程执行了await方法后。这些线程才会继续执行。
CountDownLatch和CyclicBarrier的区别:
- CountDownLatch的作用是允许N个线程等待其他指定数目的线程完成执行,这些指定数目的执行不会等待。而CyclicBarrier则是允许N个线程相互等待。
- CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier。
详细解释下第一点
假设有 A,B,C 三个任务,CountDownLatch 是C等待 A ,B执行完成后 再执行, A ,B不等待C执行,用于多个任务有先后顺序的场景。CyclicBarrier 是A,B,C 相互等待完成第一部分任务后执行后续任务,用于多个任务必须同时达到某种条件才能往下执行的场景。
类继承结构:没有继承类
构造方法分析:
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;// 需要相互等待的线程数
this.count = parties;//初始化count
this.barrierCommand = barrierAction;/
}
重要属性分析:
//处理阻塞用的锁
private final ReentrantLock lock = new ReentrantLock();
//用来处理等待和唤醒的 Condition
private final Condition trip = lock.newCondition();
//需要相互等待的线程数
private final int parties;
//达到 公共屏障要执行的动作
private final Runnable barrierCommand;
//代表一轮 barrier的状态
private Generation generation = new Generation();
//还需要等待的线程数。
private int count;
Generation :代表每执行一轮等待和释放的状态, 终止 状态改变,开始下一轮创建新的实例。
dowait
await 依赖dowait实现, dowait是CyclicBarrier核心方法。
barrierCommand:通过这个属性 实现在相互等待解除前一刻执行自定义的动作。
执行逻辑:
假设parties==3, 当执行 await的线程数小于2时,执行 Condition.await方法等待唤醒,当await的线程数等于3,执行Condititon.signalAll唤醒等待的线程。
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();//加锁,所有的使用CyclicBarrier的线程使用同一个锁,性能消耗大。
try {
final Generation g = generation;
if (g.broken)// 其他线程执行中被打断 执行了breakBarrier()后,g.broken==true
throw new BrokenBarrierException();
//打断处理
if (Thread.interrupted()) {//代码块A
// generation.broken = true;count = parties;trip.signalAll();
//更新generation状态,恢复count,唤醒所有的等待线程
breakBarrier();
throw new InterruptedException();
}
//记录还有多少没有执行 到此的线程
int index = --count;
if (index == 0) { // tripped // 执行的线程数都执行了,达到屏障条件,触发解除等待动作
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)//如果有需要执行的动作,do it
command.run();
ranAction = true;
nextGeneration();// 唤醒等待的线程,恢复count,生成新的 generation,。
return 0;
} finally {
if (!ranAction)
breakBarrier();// 这里只有 command.run() 发生异常 才有可能执行。
}
}
// loop until tripped, broken, interrupted, or timed out
//循环直到 解除等待,打断,或超时
for (;;) {
try {
if (!timed)
trip.await();// 非超时 处理,执行等待 直到被唤醒。
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos); // 超时等待 代码B
} catch (InterruptedException ie) {
//打断处理
if (g == generation && ! g.broken) {//正常情况下 此判断成立
breakBarrier();
throw ie;
} else { // 如果其他线程也被打断 执行了代码块A,就会执行到这里
//记录打断状态 方便后续执行
Thread.currentThread().interrupt();
}
}
if (g.broken)
//执行上面代码块中 catch,或 执行到此时其他线程被打断执行了代码块A
//都可能执行到此
throw new BrokenBarrierException();
if (g != generation)// 解除等待。
//参与的线程都没有被打断或执行异常情况,都会执行到此
return index;
if (timed && nanos <= 0L) {//执行代码B超时
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
要点分析:dowait()是await()的实现函数,它的作用就是让当前线程阻塞,直到“有parties个线程到达barrier” 或 “当前线程被中断” 或 “超时”这3者之一发生,当前线程才继续执行。当所有parties到达barrier(count=0),如果barrierCommand不为空,则执行barrierCommand。然后调用nextGeneration()进行换代操作。
在for(;;)自旋中。timed是用来表示当前是不是“超时等待”线程。如果不是,则通过trip.await()进行等待;否则,调用awaitNanos()进行超时等待
CyclicBarrier通过独占锁Reentrantlock和Condition配合实现。
Exchanger
同步辅助类,让两个线程可以交换数据。
关键技术点1:CacheLine填充
交换数据的场所就是Slot,每个要进行数据交换的线程在内部会用一个Node来表示。
Slot其实就是一个AtomicReference,其里面的q0, q1,..qd那些变量,都是多余的,不用的,起到了cache line填充的作用,避免了伪共享问题;
伪共享说明:假设一个类的两个相互独立的属性a和b在内存地址上是连续的(比如FIFO队列的头尾指针),那么它们通常会被加载到相同的cpu cache line里面。并发情况下,如果一个线程修改了a,会导致整个cache line失效(包括b),这时另一个线程来读b,就需要从内存里再次加载了,这种多线程频繁修改ab的情况下,虽然a和b看似独立,但它们会互相干扰,非常影响性能。
关键技术点2:锁分离
同ConcurrentHashMap类型,Exchange没有只定义一个slot,而是定义了一个slot的数组。这样在多线程调用exchange的时候,可以各自在不同的slot里面进行匹配
数据结构分析
Slot数组结构如上图所示
Slot和Note都继承了AtomicReference,维护value。 Slot中的value是Note,Node中的value 是要交换的数据。 Note中的waiter保存被阻塞的线程,item用于临时保存交换数据。
doExchange分析
doExchange处理步骤
- 根据线程id获取slot数组index,如果这个位置的slot是null,创建slot.
- 如果slot中的数据不为null,交换数据,解除等待交换线程的阻塞。
- 如果slot中的数据为null,占据当前slot,自旋转等待接其他线程交换数据,自旋结束还没得到数据,失效原index数据, index折半继续匹配。
- 当index==0 进入阻塞 等待唤醒,其他线程也会挪动到此,如果运气不好,最后所有线程都会在index==0位置交换数据。
注意Node有value和item两个属性, 当执行步骤3时 ,获取node.value, 执行步骤2时将node.item 赋值给value。
private Object doExchange(Object item, boolean timed, long nanos) {
Node me = new Node(item); //创建 占据节点
int index = hashIndex(); //计算当前index
int fails = 0; // 统计失败次数
for (;;) {
Object y; // 当前 slot内容
Slot slot = arena[index];
if (slot == null) //初始化当前slot
createSlot(index); //循环重读
else if ((y = slot.get()) != null && // 匹配上了,尝试交换数据,
slot.compareAndSet(y, null)) {//将slot 中的y取出,防止其他线程抢先
Node you = (Node)y;
if (you.compareAndSet(null, item)) {//交换 item
LockSupport.unpark(you.waiter);//解除阻塞
return you.item;//返回交换数据
} // 交换失败,说明 you被取消,continue重试
}
else if (y == null && // 没匹配上,占据当前slot.
slot.compareAndSet(null, me)) {
if (index == 0) // 当前index=0 阻塞
return timed ?
awaitNanos(me, slot, nanos) :
await(me, slot);
Object v = spinWait(me, slot); // 自旋等待匹配
if (v != CANCEL)//自旋中匹配上了
return v;//返回匹配
me = new Node(item); // 丢弃取消节点
int m = max.get();
if (m > (index >>>= 1)) // 折半查找
max.compareAndSet(m, m - 1); // Maybe shrink table
}
else if (++fails > 1) { // 失败次数多 调整 index
int m = max.get();
if (fails > 3 && m < FULL && max.compareAndSet(m, m + 1))
index = m + 1; // Grow on 3rd failed slot
else if (--index < 0)
index = m; // Circularly traverse
}
}
}
注意方法中的技巧,执行匹配和待匹配前,都要执行占位,匹配占位执行 slot.compareAndSet(y, null)
待匹配占位执行 slot.compareAndSet(null, me)
private static boolean tryCancel(Node node, Slot slot) {
if (!node.compareAndSet(null, CANCEL))//将Node的value从null替换为CANCEL
return false;
if (slot.get() == node) // pre-check to minimize contention
slot.compareAndSet(node, null);//将当前index对应Slot的value替换为null
return true;
}