在并发操作中,当需要当前线程c等待另一线程a结束后在运行的话,我们首先想到的是join方法,在c线程运行中调用a.join(),该方法会使当前线程阻塞于a,直到线程a运行结束,JVM调用a.notifyAll()方法唤醒z。
在Java1.5之后,并发包提供的CountDownLatch也可以实现join功能,并且更为强大。
一、使用方法
下面放出一个简单的Demo:
public static void main(String[]args){ CountDownLatch c = new CountDownLatch(2); Thread a = new Thread(new Runnable(){ @Override public void run() { try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("a"); c.countDown(); } }); Thread b = new Thread(new Runnable(){ @Override public void run() { try { Thread.currentThread().sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("b"); c.countDown(); } }); a.start(); b.start(); try { c.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("main"); } /* a b main */
CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。每当调用CountDownLatch的countDown方法,N就会减1。CountDownLatch的await方法会阻塞当前线程,知道N变为0。由于countDown方法可以用在任何地方,所以既可以是N个线程, 也可以是1个线程N个步骤。在使用多线程时,只需要把这个CountDownLatch的引用传入线程即可。此外,CountDownLatch也允许多个线程调用await方法,同时阻塞多个线程。
还有一点要注意的是,CountDownLatch的计数器无法被重置。
二、实现原理
同读写锁一样,CountDownLatch本质上也是一个共享锁。它允许一个或多个线程等待其他线程。它的实验原理也是同读写锁一样,通过队列同步器(AbstractQueuedSynchronizer AQS)实现的:
构造方法:
public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); }
构造器传入的计数count被用来构造一个Sync(AQS)对象。Sync的构造函数如下:
Sync(int count) { setState(count); //设置同步状态 }
这里,同步器的state就是一个锁状态,当state大于0时,锁被占用,阻塞的线程就不能被唤醒。
阻塞方法:
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }这里调用了同步器的共享式获取同步状态,如果当前线程未获取同步,则会进入同步队列等待。并且此方法响应中断。
同步器中该方法源码如下:
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); //响应中断 if (tryAcquireShared(arg) < 0) //尝试获取共享锁 doAcquireSharedInterruptibly(arg); //尝试失败 线程进入阻塞 }
该方法为模板方法,关键在于重写的tryAcquireShared方法,如下:
protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }
尝试获取共享锁,当锁计数器==0,即锁是可以获取的,则返回1,获取锁成功,直接退出acquireSharedInterruptibly方法。否则返回-1,进入doAcquireSharedInterruptibly方法阻塞当前线程。doAcquireSharedInterruptibly方法源码如下:
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); //创建当前线程的Node节点,且标记为共享锁,将锁加入CLH队列末尾 boolean failed = true; try { for (;;) { final Node p = node.predecessor(); //获取前继节点 if (p == head) { int r = tryAcquireShared(arg); //尝试获取锁 if (r >= 0) { //如果成功 setHeadAndPropagate(node, r); //设置为头结点 p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && //如果不是头结点,则继续等待 parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
countDown方法
public void countDown() { sync.releaseShared(1); }实际上调用队列同步器的释放锁方法:
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false;
首先会尝试直接释放共享锁,成功则直接返回,否则调用doReleaseShared();
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; //状态-1 if (compareAndSetState(c, nextc)) return nextc == 0; } } }
三、总结
CountDownLatch是通过共享锁实现的。在创建CountDownLatch中时,会传递一个int类型参数count,该参数是队列同步器中的state状态值,表示共享锁最多能被count线程同时获取。当某线程调用该CountDownLatch的await()方法时,该线程会等待共享锁可用时,才能获取共享锁。而共享锁的可用条件就是state等于0。只有每次调用CountDownLatch的countDown方法,状态值才会减1。