CyclicBarrier自旋改造

前言

最近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:我来了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值