对比Thread.join()和CountDownLatch及原理解析
本文意在对比Thread.join()和CountDownLatch
一、引入
在main线程中启动100个线程,100个线程完成后,主线程打印“完成”,如何实现?
二、思考
关键点在于如何让主线程在其他100个线程执行完成之后再执行。
三、测试实验(JDK1.8)
1.使用join方式实现
1 public static void main(String[] args) throws InterruptedException { 2 for (int i = 0; i < 100; i++) { 3 Thread t = new Thread(()-> { 4 System.out.println("current thread name is " + Thread.currentThread().getName()); 5 System.out.println(Thread.currentThread().getId()); 6 //try { 7 //TimeUnit.SECONDS.sleep(2); 8 //} catch (InterruptedException e) { 9 //e.printStackTrace(); 10 //} 11 }); 12 t.start(); 13 //主线程中调用其他线程的join 14 t.join(); 15 } 16 System.out.println("main completed"); 17 }
输出结果(部分解图):可以看出等待100个线程都输出完成之后main线程才输出"main completed"。

2.使用CountDownLatch方式实现
1 public static void main(String[] args) throws InterruptedException { 2 CountDownLatch c = new CountDownLatch(100); 3 for (int i = 0; i < 100; i++) { 4 new Thread(() -> { 5 System.out.println("current thread name is " + Thread.currentThread().getName()); 6 c.countDown(); 7 System.out.println("i:" + c.getCount()); 8 }).start(); 9 } 10 c.await(); 11 System.out.println("main completed"); 12 }
输出结果(部分截图):同样地,在100个线程输出完成之后main线程才输出。

四、结论分析
1.join()和CountDownLatch都能实现让当前线程阻塞等待直到其他线程执行完毕。
2.从结果图来看,上述实验使用join()每个线程都是顺序执行的,而CountDownLatch并没有顺序执行;
实际上,如果将CountDownLatch中//System.out.println("i:" + c.getCount());这行代码取消注释之后,可以看到输出的count值完全是乱序的,并且和System.out.println("current thread name is " + Thread.currentThread().getName());输出的结果也不保证原子性,更有就是甚至可能在main线程输出之后才输出。

而join测试中,将//System.out.println("i:" + c.getCount());取消注释之后,完全是按照线程顺序依次执行完成且main线程最后执行。

3.对于第2条按顺序执行,是因为在for循环中调用了t.join()方法,也就是每一次循环体,线程启动了之后不管有没有执行完成主线程都需要阻塞等待,并且上一个线程都执行完毕了之后才会执行下一个线程,其实这也就印证了执行下一次循环时主线程需要等待线程执行完毕。
五、源码分析
1.Thread.join() -> join(0)
源码:
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } //如果传入的参数是0,一直阻塞直到isAlive==false,也就是线程正常执行完或者发生异常 if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
在当前线程调用了另外一个线程的join()时,实际上是调用了wait(),当线程正常执行完run方法中的代码时,JVM会调用exit()方法退出线程,而在exit()方法中调用了notifyAll(),唤醒处于等待状态的main线程。
2.CountDownLatch
源码:
//内部类,AQS框架 private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; //把传入的count作为状态值,Uses AQS state to represent count. Sync(int count) { setState(count); } //获取状态值 int getCount() { return getState(); } //尝试获取同步状态,当count(state)已经减为0时返回1 protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } //CAS自旋,尝试修改state的值,如果在尝试的过程中state为0,则直接返回false,不能再被修改;否则通过CAS的方式将state自减1 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; } } } private final Sync sync; //构造方法,用传入的count初始化Sync的同步状态值 public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); } //中断等待 public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); } //调用这个方法尝试使count(state同步状态)减1 public void countDown() { sync.releaseShared(1); }
使用CountDownLatch,每当线程调用countDown()时count减一(同步状态state),而主线程中调用await()时去判断state是否为0,如果为0表示所有的线程都已经完成,主线程可以继续运行,否则主线程只能继续阻塞等待。
六、结论
1.join()是Thread提供的方法,而CountDownLatch JUC包提供的并发类;
2.join()和CountDownLatch都能实现让当前线程阻塞等待直到其他线程执行完毕;
3.join()使用简单但是却很粗暴,而CountDownLatch粒度更具体,可以通过在run()中控制调用countDown()的位置控制粒度,而不需要等待线程run()中全部逻辑执行完之后才释放锁。所以在实际业务场景中,为了提高并发性能,通常会选用更加灵活的CountDownLatch实现;
4.虽然CountDownLatch更加灵活,但是也需要在使用时明确等待条件,否则main线程还是会await,造成不必要的性能开销。

浙公网安备 33010602011771号