并发编程系列(十一)—深入理解基于共享锁的CountDownLatch

并发编程系列(十一)—深入理解基于共享锁的CountDownLatch_第1张图片

前言

大家好,牧码心今天给大家推荐一篇并发编程系列(十一)—深入理解基于共享锁的CountDownLatch的文章,希望对你有所帮助。内容如下:

  • CountDownLatch 概要
  • CountDownLatch 数据结构
  • CountDownLatch 使用方式
  • CountDownLatch 实现原理
  • 总结

CountDownLatch 概要

CountDownLatch是一个同步辅助器,在一组其他线程执行完成操作之前,允许一个或多个线程一直等待。它的作用有点类似于计数器,先设定一个计数初始值,当计数达到0时,将会触发一些事件。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执。它具体以下几个特点:

  1. 初始计数器的值调用构造函数new CountDownLatch(int count)就会创建,其中count表示初始的线程数量;
  2. 线程调用CountDownLatch的await()方法后就会进入阻塞。只要有其他线程每调用一次countDown() 方法,计数值就会减1,计数 器的值达到0时,所有阻塞的线程会唤醒;
  3. CountDownLatch的初始计数值一旦达到0,无法重置。如果需要重置,可以考虑使用CyclicBarrier,后续文章进行说明

CountDownLatch 使用示例

下面我们看一个模拟场景如何使用CountDownLatch

设计一个初始计数值为N的 CountDownLatch,作为一个完成信号点:使某个线程在其它N个线程完成某项操作之前一直等待。

public class CountDownLatchTest {

    private static int LATCH_COUNT=N;
    private static CountDownLatch countDownLatch;
    public static void main(String[] args) {
        try {
            countDownLatch=new CountDownLatch(LATCH_COUNT);
            for(int i=0;i<N;i++){
                // 创建子线程作业
                new SubThread("Thread"+i).start();
            }
            System.out.println(" 主线程开始等待!");
            countDownLatch.await();
            System.out.println(" 主线程完成!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    static class SubThread extends Thread{

        public SubThread(String name){
            super(name);
        }
        @Override
        public void run() {
            try {
                // 休眠2s,模拟业务
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()+" sleep 2s");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                // 将CountDownLatch的数值减1
                countDownLatch.countDown();
            }
        }
    }

}

CountDownLatch 数据结构

CountDownLatch 依赖共享锁实现,我们看下其UML类图和主要函数。

  • UML类图
    并发编程系列(十一)—深入理解基于共享锁的CountDownLatch_第2张图片
    说明: CountDownLatch 类结构比较简单。内部包含Sync对象,Sync又继承AQS,来实现共享锁。
  • 主要函数
// 初始化一个计数为count的CountDownLatch
CountDownLatch(int count)
// 使当前线程在计数为零之前一直等待,除非线程被中断。
void await()
// 使当前线程在计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
boolean await(long timeout, TimeUnit unit)
// 递减计数,如果计数到达零,则释放所有等待的线程。
void countDown()
// 返回当前计数。
long getCount()

CountDownLatch 实现原理

CountDownLatch 内部依赖AQS,基于共享锁实现,下面我们从初始化CountDownLatch ,阻塞操作await()和递减计数器countDown()来分析

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

调用CountDownLatch构造器初始化,并设置计数器的初始值为count,内部创建一个AQS子类。深入AQS子类创建:

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

可以看到初始的计数器值count为AQS中同步状态state的值。对于CountDownLatch而言,state表示的”锁计数器“。CountDownLatch中的getCount()也是调用AQS中的getState(),返回的state对象。

  • 阻塞操作await()
public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

阻塞操作await()实际调用的是AQS中的acquireSharedInterruptibly方法:

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
        // 若线程中断,抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 2.尝试获取共享锁,返回值小于0表示获取失败,加入等待队列,直到当前线程获取到共享锁(或被中断)才返回。
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

其中tryAcquireShared()方法尝试获取锁,由AQS子类实现如下:

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

说明:前面说到在CountDownLatch中,同步状态State表示CountDownLatch的计数器的初始值,则当State==0时,表示无锁状态,此时所有线程在await上等待的线程都可以继续执行。所以"锁计数器state=0",即锁是可获取状态,则返回1;否则,锁是不可获取状态,则返回-1。
若尝试获取锁失败后,会调用doAcquireSharedInterruptibly,实现如下:

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
		// 1、创建共享锁类型的节点,并放到CLH等待队列中末尾。
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
        	// 2、自旋阻塞线程或尝试获取锁
            for (;;) { 
            	 // 2.1获取上一个节点,若上一个节点是表头,则尝试获取该共享锁
                final Node p = node.predecessor();
                if (p == head) {
                	// 尝试获取锁,若大于0则表示获取成功
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                // 2.2 若上一个节点不是表头,当前线程一直等待,直到获取到共享锁
                // 如果线程在等待过程中被中断过,则再次中断该线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

说明:doAcquireSharedInterruptibly()方法作用是会使当前线程一直等待,直到当前线程获取到共享锁(或被中断)才返回。

  • countDown() 唤醒阻塞线程
    countDown()方法作用是将递减计数器的值,若计数器值变为0 则唤醒阻塞的线程。实现如下:
public void countDown() {
        sync.releaseShared(1);
    }

内部实际调用了AQS的releaseShared方法来尝试释放锁,实现如下:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

说明:releaseShared()作用是让当前线程释放它所持有的共享锁。它首先会通过tryReleaseShared()去尝试释放共享锁。尝试成功,则直接返回;尝试失败,则通过doReleaseShared()去释放共享锁。tryReleaseShared()实现如下:

	protected boolean tryReleaseShared(int releases) {
			// 采用自旋阻塞线程尝试释放锁
            for (;;) {
            	// 获取锁计数器state
                int c = getState();
                // 若state=0 不需要释放
                if (c == 0)
                    return false;
				// 否则锁计数器-1                    
                int nextc = c-1;
                // 通过CAS操作更新锁计数器值
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

总结

CountDownLatch内部通过共享锁实现。在创建CountDownLatch实例时,需要传递一个int型的参数:count,该参数为计数器的初始值,也可以理解为该共享锁可以获取的总次数。当某个线程调用await()方法,程序首先判断count的值是否为0,如果不会0的话则会一直等待直到为0为止。当其他线程调用countDown()方法时,则执行释放共享锁状态,使count值 – 1。当在创建CountDownLatch时初始化的count参数,必须要有count线程调用countDown方法才会使计数器count等于0,锁才会释放,前面等待的线程才会继续运行。注意CountDownLatch不能回滚重置。

你可能感兴趣的:(并发编程)