Java同步工具类之CountDownLatch详解

CountDownLatch是一个同步工具类,用于线程间的等待。本文通过服务员与顾客的场景,展示了CountDownLatch如何实现线程等待。服务员在所有顾客到达前不会上菜,这对应于线程在计数器减至零前阻塞。CountDownLatch提供了await()和countDown()方法,前者使线程等待,后者减少计数。当计数为0时,所有等待的线程会被唤醒。此外,还介绍了内部类Sync如何基于AQS实现计数器的管理。

CountDownLatch(闭锁)

综述

CountDownLatch作为同步工具类中的一种。它的功能极其简单,它允许一个或多个线程等待(进入阻塞状态),直到其他线程完成操作。与CyclicBarrier不同,CountDownLatch是等待事件,而CyclicBarrier是等待线程。以下是我写的两个示例。

示例

服务员与顾客问题

在我们去餐厅吃饭时,服务员不会在人没来齐之前上菜, 所以服务员会等到所有用户来到餐厅后才开始上菜。换到Java中可以将服务员和顾客看成线程,借助CountDownLatch可以很方便的实现该功能。

public class CountDownLatchTest {

    public static void main(String[] args) {
        CountDownLatch cdl = new CountDownLatch(5);

        new Thread(new Waiter(cdl)).start();
        for (int i = 0; i < 5; i++) {
            new Thread(new Customer(i+"",cdl)).start();
        }
    }

    private static class Waiter implements Runnable{
        private final CountDownLatch countDownLatch;

        public Waiter(CountDownLatch countDownLatch){
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                System.out.println("服务员等待顾客");
                //服务员等待顾客
                countDownLatch.await();
                System.out.println("服务员开始上菜...");
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            }
        }
    }

    private static class Customer implements Runnable{
        private final String name;
        private final CountDownLatch countDownLatch;

        public Customer(String name,CountDownLatch countDownLatch){
            this.name = name;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
            	//模拟顾客到餐厅所需的时间
                TimeUnit.SECONDS.sleep(new Random().nextInt(10));
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            }
            //到达餐厅
            countDownLatch.countDown();
            System.out.println("顾客"+name+" 已到达餐厅");
        }
    }
}

运行结果如下:
在这里插入图片描述

由此可见,该测试运行成功,服务员成功在所有顾客到达之后才开始上菜。观察代码可以发现,除了我们CountDownLatch构造方法传入的参数5以外,我们仅仅只使用了CountDownLatch对象的两个方法。countDown()和await()。

详解

由以上示例可以发现,CountDownLatch对象中维护一个递减计数器,该计数器参数通过构造方法传入,通过调用countDown()方法来递减对象内部的计数器,直到计数器递减为0时,调用await()方法的线程将会被放行。在此之前,await()方法会让线程阻塞并能响应中断。

在这里插入图片描述
CountDownLatch的方法很少,暴露给外部类的方法只有await()以及带时间参数重载的await(),countDown()和getCount()。

void await()

将导致当前线程阻塞,直到CountDownLatch递减计数到零为止,除非该线程被中断。
如果当前count为零,则此方法立即返回。
如果当前count大于零,则出于线程调度目的,当前线程将被禁用,并且在发生以下两种情况之一之前,它处于休眠状态:

  • 由于countDown方法的调用,计数达到零。 或者
  • 其他一些线程中断当前线程。

如果当前线程:

  • 在进入此方法时已设置其中断状态; 或者
  • 在等待期间被打断,

然后抛出InterruptedException并清除当前线程的中断状态。

public void await() throws InterruptedException {
	//调用sync对象的能够响应中断的获取共享许可
	sync.acquireSharedInterruptibly(1);
}

boolean await(long timeout, TimeUnit unit)

除了拥有以上方法的功能外,在经过设置的时间之后,如果count仍然大于0,那么将返回false,否则立即返回true,也就是说,有线程调用此方法时,只会等待传入的时间,超时或者时间小于0,那么就立刻退出返回false。

public boolean await(long timeout, TimeUnit unit)
	throws InterruptedException {
	return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

void countDown()

当调用此方法时,count会尝试-1,如果-1之后count==0,那么将会重新调度所有等待的线程。如果count之前就等于0,那么调用此方法将会无事发生。

public void countDown() {
	//释放共享许可
	sync.releaseShared(1);
}

long getCount()

获取当前计数

public long getCount() {
	return sync.getCount();
}

由此可以发现,CountDownLatch的方法都与Sync的类有关,而Sync类是CountDownLatch的内部类,它继承自AbstractQueuedSynchronizer(AQS),AQS是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS轻易的构造出来,比如ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock等。


了解了AQS之后,再来看看CountDownLatch的内部类Sync

private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

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

        int getCount() {
            return getState();
        }

		//实现的AQS中共享的方式获取
        protected int tryAcquireShared(int acquires) {
        	//当count==0时返回值>1
            return (getState() == 0) ? 1 : -1;
        }
        
		//实现的AQS中共享的方式释放
        protected boolean tryReleaseShared(int releases) {
            //以CAS的方式来尝试释放
            for (;;) {
                int c = getState();
                //当count == 0直接返回false
                if (c == 0)
                    return false;
                //首先获取countDown之后的值
                int nextc = c-1;
                //当count的期望值是c时,将其进行countDown,防止多线程调用该方法时产生无效值
                if (compareAndSetState(c, nextc))
                	//当countDown之后的值为0时,返回ture
                    return nextc == 0;
            }
        }
    }

这里有个setState()和getState(),compareAndSetState方法,这是因为在AQS中管理着一个整数状态信息,该state以实现的同步器的不同有着不同的涵义,比如在CountDownLatch中,state用来表示当前的计数值,计数值为0时,将解除所有等待线程的阻塞。

	//AbstractQueuedSynchronizer部分源码
	/**
     * The synchronization state.
     */
    private volatile int state;

    /**
     * Returns the current value of synchronization state.
     * This operation has memory semantics of a {@code volatile} read.
     * @return current state value
     */
    protected final int getState() {
        return state;
    }

    /**
     * Sets the value of synchronization state.
     * This operation has memory semantics of a {@code volatile} write.
     * @param newState the new state value
     */
    protected final void setState(int newState) {
        state = newState;
    }

以下是countDown方法中调用的releaseShared

public final boolean releaseShared(int arg) {
	//这里的tryReleaseShared方法是CountDownLatch中实现的
    if (tryReleaseShared(arg)) {
    	//真正的释放操作,由AQS实现
        doReleaseShared();
        return true;
    }
    return false;
}

以下是await方法中调用的acquireSharedInterruptibly方法

public final void acquireSharedInterruptibly(int arg)
	   throws InterruptedException {
	//如果当前线程被设置为中断,那么抛出异常
	if (Thread.interrupted())
	    throw new InterruptedException();
	//这里的tryAcquireShared方法是CountDownLatch中实现的
	if (tryAcquireShared(arg) < 0)
		//真正的获取操作,由AQS实现
	    doAcquireSharedInterruptibly(arg);
}

总结

CountDownLatch是基于AQS实现的一个同步器,其内部的state被CountDownLatch当作计数器使用,并且使用共享的方式获取和释放state,await方法调用的acquireSharedInterruptibly使得能够响应中断,但是与CyclicBarrier栅栏不同的是,闭锁无法重置,也就是说每次使用闭锁,对象都需要重新建立。
未完待续…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值