图解java.util.concurrent并发包源码系列——深入理解Semaphore、CountDownLatch、CyclicBarrier并发编程三剑客

Semaphore

Semaphore是信号量,其实就是一种资源有限的共享锁。共享锁不会被一个线程独占,大家都可以获取。为什么又说是资源有限呢?那是因为它不是可以被无效的获取的,我们需要给定一个初始值作为最大的可被获取数,当锁被获取完以后,后面过来的线程就只能阻塞等待了。

当我们有一些业务场景,是资源有限但又不是独占的时候,可以使用Semaphore。

Semaphore的例子和使用

比如在Hystrix里面有个信号量隔离的机制,可以为每个远程调用的服务接口分配一定的信号量,然后每个线程发起远程调用时,必须先获取到信号量。获取到信号量的线程就可以发起远程调用,获取不到信号量的线程则要阻塞等待。而每个服务接口都有它自己的信号量,不同服务接口间是相互隔离的,因此即便自己的信号量全部获取完了,也不会影响到对方。

在这里插入图片描述

再比如我们可以通过Semaphore做接口限流,我们可以通过Semaphore实现一个限流器,每次有请求到来时,都要获取一个信号量,获取到了才放行,获取不到则不让请求继续往下走。

在这里插入图片描述

当我们要获取信号量时,我们可以调用Semaphore的acquire(),使用完毕后可以调用Semaphore的release()方法归还回去。

在这里插入图片描述

Semaphore源码

Semaphore构造方法和内部结构

Semaphore和ReentrantLock一样,有一个Sync抽象内部类,然后Semaphore可以实现公平模式和非公平模式,对应的Sync实现类就是FairSync和NonfairSync

在这里插入图片描述

Semaphore构造方法:

    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

Semaphore的构造方法接收两个参数,int permits是限制的资源大小,fair表示是否公平模式。如果是公平模式内部的Sync对象就是FairSync类型,如果是非公平模式,内部的Sync对象就是NonfairSync类型。

        FairSync(int permits) {
            super(permits);
        }

        NonfairSync(int permits) {
            super(permits);
        }

无论是FairSync还是NonfairSync,构造方法都会调用父类Sync的构造方法。

Sync构造方法:

        Sync(int permits) {
            setState(permits);
        }

Sync的构造方法调用AQS的setState变量,用state存储资源限制数。

在这里插入图片描述

acquire()方法

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

acquire方法调用sync.acquireSharedInterruptibly(1),acquireSharedInterruptibly是AQS定义的一个模板方法,会调用到继承AQS的子类的tryAcquireShared方法,然后就会进入到NonfairSync的tryAcquireShared方法或者是FairSync的tryAcquireShared方法。

NonfairSync#tryAcquireShared

        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }

NonfairSync的tryAcquireShared方法调用nonfairTryAcquireShared方法,进入到Sync的nonfairTryAcquireShared方法。

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

逻辑很简单,就看当前state减去要获取的数量acquires后,是否小于0。如果小于0,不会更新state,返回计算结果remaining,AQS里面规定tryAcquireShared方法返回负数表示资源不足,那么当前线程就要阻塞等待。如果不小于0,那么会通过CAS更新state变量,更新成功则返回更新后的state(大于等于0),更新不成功则继续自旋。

在这里插入图片描述
FairSync#tryAcquireShared:

        protected int tryAcquireShared(int acquires) {
            for (;;) {
            	// hasQueuedPredecessors():队列中是否有线程排队?
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }

FairSync的tryAcquireShared方法调用了hasQueuedPredecessors()方法判断当前AQS的队列中是否有线程排队,如果有线程排队,那么当前线程也要去排队。

如果没有线程排队,那么当前线程可以尝试获取锁,下面的逻辑就跟 nonfairTryAcquireShared方法中的逻辑一样了。

在这里插入图片描述

release()方法

    public void release() {
        sync.releaseShared(1);
    }

Semaphore的release方法调用sync.releaseShared(1),进入AQS的模板方法releaseShared中,然后AQS会调用子类的tryReleaseShared方法,就会进到Sync的tryReleaseShared方法内部。

Sync#tryReleaseShared:

        protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current)
                	// state加上释放数量releases后,反而少于原来的state的,说明溢出成负数了,抛一个异常
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }

释放资源的逻辑就没有分公平非公平了,都是进入到父类Sync的tryReleaseShared。里面有个溢出的处理,不过那不是重点。重点就是for ( ; ; ) + compareAndSetState(current, next),还是自旋加CAS。

在这里插入图片描述

CountDownLatch

CountDownLatch的作用和使用

CountDownLatch的主要作用就要可以实现一个线程等待一批线程完成某些操作或任务后,在继续往下执行的功能。

比如在分布式环境下保证写入到主节点的数据全部同步到从节点,再返回客户端写入成功。那么这种场景就可以用CountDownLatch。

在这里插入图片描述

我们使用CountDownLatch前,要创建一个CountDownLatch对象,CountDownLatch的构造方法需要我们传递一个int类型的count参数,这个count可以理解为主线程需要等待的线程数。比如上面的主从同步中,需要等待三个从节点同步完成,那么这里的CountDownLatch的构造参数可以传3。

然后当主线程调用CountDownLatch的await()方法,就会被阻塞。直到count被扣减为0,则会解阻塞。

执行任务的子线程可以在执行完任务之后,调用CountDownLatch的countDown()方法,把count减1。

在这里插入图片描述

CountDownLatch源码

CountDownLatch的构造方法

    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

创建了一个Sync内部类,这个Sync内部类也继承了AQS,而且这个Sync内部类不是一个抽象类,是可以new的,原因是因为CountDownLatch不需要分什么公平非公平模式。

        Sync(int count) {
            setState(count);
        }

同样是调用AQS提供的setState方法,把count设置到state。

在这里插入图片描述

await()方法

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

await() 方法调用了 sync.acquireSharedInterruptibly(1),进入到AQS的模板方法acquireSharedInterruptibly中,acquireSharedInterruptibly方法会调用子类的tryAcquireShared方法,然后就进入到Sync的tryAcquireShared方法内部。

Sync#tryAcquireShared:

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

这里逻辑就是,只要state不为0,那么就返回-1,那么当前线程就要在AQS的队列中阻塞等待了。只要state被countDown到0了,这里才会返回1,返回1的话当前线程就不用阻塞。

在这里插入图片描述

countDown()方法

    public void countDown() {
        sync.releaseShared(1);
    }

countDown方法直接调用的时sync的releaseShared方法,进入到AQS的模板方法releaseShared中,然后releaseShared方法会调用子类实现的的tryReleaseShared方法,进入到Sync的tryReleaseShared方法内部。

Sync#tryReleaseShared:

        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                // state减1,然后通过CAS更新到state
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

Sync的tryReleaseShared就是对state进行减1,然后用通过CAS更新回去,更新成功则退出自旋,更新不成功则自旋重试。

在这里插入图片描述

CyclicBarrier

CyclicBarrier的作用

CyclicBarrier的功能可以实现一批线程执行到某个位置的时候阻塞等待直到大家都到达后才往下进行。有点像赛马,赛马的时候需要等待所有的马就位,才可以开跑。这里也是等待所有的线程执行到某个位置,大家才能继续往下执行,否则就要阻塞在哪里等人到齐。

在这里插入图片描述

CyclicBarrier的作用与CountDownLatch是非常相似的。但是有一点区别就是CountDownLatch是一次性的,用完了就报废,每次来都要new一个;而CyclicBarrier是可以循环复用的。

我们使用CyclicBarrier,只需要创建一个CyclicBarrier对象,然后调用await()方法即可。

CyclicBarrier源码

CyclicBarrier成员变量与构造方法

public class CyclicBarrier {

	private static class Generation {
        boolean broken = false;
    }

    private final ReentrantLock lock = new ReentrantLock();

    private final Condition trip = lock.newCondition();

    private final int parties;

    private final Runnable barrierCommand;

	private Generation generation = new Generation();

    private int count;

	// 。。。。。。省略下面的代码
}

看到CyclicBarrier的成员变量,我们就可以猜出CyclicBarrier的功能就是靠ReentrantLock和Condition实现的。每次调用await方法,会先获取ReentrantLock锁,然后在对state进行减1,然后释放锁到Condition中等待,直到最后一个线程到来,唤醒Condition中的所有线程。

然后CyclicBarrier有两个int类型的成员变量(parties和count),肯定是一个用于计数,一个用于恢复。

Runnable barrierCommand则是当所有线程到达之后,会马上执行的一个任务。

CyclicBarrier还有一个Generation的内部类,里面就一个boolean类型的变量broken,用于表示栅栏有没有被破坏,线程被中断、超时等都会导致栅栏被破坏(也就是generation的broken置为true)。broken为true后,调用await方法会抛异常。

在这里插入图片描述

构造方法:

    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }

构造方法没什么重要逻辑,就是简单的赋值操作。

await()方法

    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe);
        }
    }

await() 方法就是直接调用 dowait(false, 0L),其他的不用看。

CyclicBarrier#dowait:

    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();
            }

			// count减1
            int index = --count;
            // 最后一个线程到来,会把count减成0,会进这个分支
            if (index == 0) {
                boolean ranAction = false;
                try {
                	// 运行barrierCommand任务
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    // 唤醒所有线程,重置栅栏
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

			// 非最后一个线程,都会进这里
            for (;;) {
                try {
                	// 7、在Condition中等待 
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                    	// 被打断了,破坏栅栏
                        breakBarrier();
                        throw ie;
                    } else {
                        Thread.currentThread().interrupt();
                    }
                }
				
				// 栅栏已被破坏,抛异常
                if (g.broken)
                    throw new BrokenBarrierException();

				// generation被更新了,说明最后一个线程到来重置了栅栏,那么可以开跑了
                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                	// 等待超时,破坏栅栏
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
        	// 释放锁
            lock.unlock();
        }
    }

dowait方法有点长,但是大体逻辑还是比较清晰。

  1. 首先要获取锁。
  2. 然后判断栅栏释放已被破坏,如果是的话就不用往下执行了,抛出一个异常。
  3. 判断当前线程是否已被中断,如果是的话就破坏栅栏,然后抛出一个中断异常。
  4. 然后就是count减1后看看是否等于0,如果等于0的话说明当前线程是最后一个到来的线程,需要执行barrierCommand任务,唤醒Condition中等待的所有线程,重置栅栏。
  5. 如果count减1后不是0,那说明当前线程不是最后一个到来的线程,那就自旋调Condition的await方法进行等待。中间如果线程被中断或者等待超时也会破坏栅栏,如果栅栏被破坏,也会抛异常。如果发现generation被更新了,说明最后一个线程到来重置了栅栏,那么可以开跑了。
  6. 最后释放锁

在这里插入图片描述

breakBarrier()
    private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }

breakBarrier() 方法用于破坏栅栏,里面的逻辑就是把generation的broken设置为true,然后唤醒所有等待的线程。

nextGeneration()
    private void nextGeneration() {
        trip.signalAll();
        count = parties;
        generation = new Generation();
    }

nextGeneration()的作用是唤醒所有等待的线程,重置count变量,new一个新的Generation覆盖老的Generation对象。

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值