手撕同步工具类源码(一)

在了解了 AQS 再来学习下,它在 Java 源码中的应用。先从同步工具类开始吧;关于同步工具类如何使用,有一篇很好的文章可以给你:点击这里


CountDownLatch

允许一个或多个线程等待,直到在其它线程中执行的一组操作完成。

在了解了 AQS 之后,我们知道它的独占锁式和共享锁模式。所以,我们第一步需要弄明白 ,CountDownLatch 使用的是什么模式?参考它的用法知道,它需要一个或多个线程阻塞,并在计数降为 0 后,唤醒所有阻塞的线程,所以,它是一个共享锁模式。

AQS 也提到过,大多数使用它的地方都不是直接继承,而是维护一个内部的私有类来继承,在这里也一样:

	private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
		// 初始化计数,使用 AQS 中的 state
        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }
		/**
		* 使用共享锁模式需要实现的方法,在这里,state 为 0 表示获取到锁,如果获取锁失败,将入 CLH 队		  * 列并阻塞线程;
		*/
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
		/**
		* 递减 state
		*/
        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;
            }
        }
    }

CountDownLatch 有着以下几个方法,由于每个方法的源码比较简单,这里就不在列出:

  1. 构造函数:构造函数接受一个 int ,用于创建上面的 Sync 对象,这是 state 初始的值;更改 state 的值必须使用父类的方法,为了线程安全;

  2. await:该方法调用了 AQSacquireSharedInterruptibly 方法,熟悉 AQS 的朋友都知道,该方法会回调子类实现的 tryAcquireShared 方法(也就是上面的这个方法)。如果这个方法返回的数小于 0,则将入队列,并阻塞,然后等待被唤醒;

  3. countDwon:该方法调用了 AQSreleaseShared 方法,每次状态值减一,该方法会回调上面的 tryReleaseShared 方法。如果该方法返回 true,将去唤醒队列中阻塞的线程。可以看见,当状态值为已经 0 ,将直接返回 false,只有在状态值第一次等于 0 时,才会返回 true,这时候就会尝试去唤醒队列中的阻塞线程。

    注意:并不是等于 0 就直接去唤醒,而是只有在第一次等于 0 才会去这样做,也就是后续再调用这个方法并不会再去队列上唤醒线程了,尽管这个队列上没有线程。

  4. getCount:返回状态值,在这里的语义可以理解为:等待线程还需再等待多少其它线程完成操作就可以执行。

总结:state 值很重要,只应该通过 AQS 提供的方法来操作它。锁与 state 值只存在一种表示关系,你可以理解为 state 为 0 才表示获得锁,也可以理解为 state 为 1 才表示获得锁,这取决于你自己。只要明白 tryAcquireShared 方法返回一个大于 0 的锁就是锁获取成功,返回小于 1 的数,表示获取锁失败,并且会阻塞等待,其它的完全取决于你的想象力。


CyclicBarrier

允许一组线程彼此等待到达一个共同的障碍点

CyclicBarrier 有一个代的思想,它指的是当一组线程均到达了屏障,则完成了一代,然后开始下一代;但也有特殊情况,那就是在各线程均到达屏障点以后要执行的那个任务失败了,则将损坏当前代(屏障被打破),其它还未获得执行许可的线程在阻塞超时以后,将得到当前代已经被损坏的信息,并抛出 BrokenBarrierException 异常。

CyclicBarrier 底层是通过 ReentrantLock ,并结合 Condition 来实现的,主要的代码是以下这个方法:

	private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            // 代表当前代
            final Generation g = generation;
			// 检查当前代是否已经损坏
            if (g.broken)
                throw new BrokenBarrierException();
			// 如果当前线程已经被中断,则直接打破屏障
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            int index = --count;
            // 代表所有线程均到达屏障
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    // 所有线程到达屏障后,可以执行的任务
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    // 正常开始下一代,唤醒所有阻塞线程,重置计数和代
                    nextGeneration();
                    return 0;
                } finally {
                    // 屏障任务未正常执行完成,将打破屏障,所有阻塞的线程被唤醒,然后,将以异常结束
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        // 到达超时时间也不能结束,需要被唤醒
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }
				// 阻塞结束后,需要检验的各种条件
                if (g.broken)
                    throw new BrokenBarrierException();
				// 当前代已经结束,已正常开始下一代
                if (g != generation)
                    return index;
				
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

从上面可以知道,如果在屏障处等待的线程被中断或者超时,将打破屏障(如果还处在当前代的话),所有在已到达屏障处的线程将被唤醒,并抛出 BrokenBarrierException;还未到达屏障处的线程在到达以后,将立即得到该异常。

CyclicBarrier 的方法,都是基于 ReentrantLock 的加锁和解锁来操作的,所以,在调用其它的方法时,也需要注意锁的竞争;

总结

在屏障处等待的一个线程,有三种结束状态:

  1. 正常的结束:此时,循环屏障已经开启下一代,可重复使用;
  2. 被中断,捕获到中断异常:这表明屏障已经被打破,其它在屏障处等待的线程将捕获到 BrokenBarrierException 异常。
  3. 捕获到 BrokenBarrierException 异常:这可能是由于屏障任务执行失败,其它线程被中断。

想要重用循环屏障,需要调用 reset 方法,不过,参考该方法的实现发现,该方法会先去打破屏障,再开始下一代。该方法的文档介绍说并不适用于重置因某种原因破损的屏障,此时,推荐重新创建循环屏障。


推荐博文


手撕 AQS 之独占锁模式

手撕AQS之共享锁模式

手撕AQS之Condition


我与风来


认认真真学习,做思想的产出者,而不是文字的搬运工。错误之处,还望指出!

小新


如果觉得这篇文章对你有所帮助,动动小手,点点赞,这将使我能够为你带来更好的文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值