【Java学习笔记】多线程 Part 7 - CountDownLatch

本文详细介绍了Java并发工具类CountDownLatch的原理与使用,通过实例展示了其在多线程同步中的作用,包括构造方法、countDown()和await()的源码分析。对比了CountDownLatch与join的区别,指出CountDownLatch在灵活性和应用场景上的优势。
摘要由CSDN通过智能技术生成

一、CountDownLatch简述

  • CountDownLatch是JUC下的其中一个辅助类。

  • 当有某个事务需要在其他一些事务执完成之后再执行,我们就可以使用CountDownLatch来完成这个工作。

  • CountDownLatch可以简单的理解为计数器。或者我更喜欢理解为,在一开始就一次性上好几把锁,然后再一把把的拿掉。不过从底层来讲的话,CountDownLatch其实加的是共享锁。

二、代码示例

1. 基本应用

import java.util.concurrent.CountDownLatch;

public class TestCountDownLatch {
	
	public static void main(String[] args) throws InterruptedException {
		CountDownLatch countDownLatch = new CountDownLatch(8); //设置初始状态为8
		
		for (int i = 0; i < 8; i++) {
			new Thread(()->{
				System.out.println(Thread.currentThread().getName()+" done!");
				countDownLatch.countDown(); //拿掉一把锁
			}, "thread "+i).start();
		}
		
		countDownLatch.await(); //等待锁全释放
		System.out.println("all finish");
	}
	
}

输出:

thread 0 done!
thread 5 done!
thread 4 done!
thread 3 done!
thread 1 done!
thread 2 done!
thread 7 done!
thread 6 done!
all finish

可以看到,当8个线程全都执行完毕以后,程序才往下打印了"all finish"

2. 跑步比赛

模拟一下,跑步比赛时候的场景,运动员等待裁判员发号施令,枪响以后才能开始跑,等到大家全部跑完以后再统计分数

public class Competition {
	public static void main(String[] args) throws InterruptedException {
		CountDownLatch judge = new CountDownLatch(1); //1个裁判
		CountDownLatch athletes = new CountDownLatch(8); //8个运动员
		
		for (int i = 0; i < 8; i++) {
			new Thread(()->{
				try {
					//等裁判发号施令
					judge.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				
				System.out.println(Thread.currentThread().getName()+" finish");
				//跑完了
				athletes.countDown();
			}, "Runner "+i).start();
		}
		
		//比赛开始
		System.out.println("Bang!");
		judge.countDown();
		
		//等所有人跑完
		athletes.await();
		System.out.println("all finish");
		System.out.println("record scores");
	}
}

输出:

Bang!
Runner 0 finish
Runner 1 finish
Runner 4 finish
Runner 3 finish
Runner 2 finish
Runner 6 finish
Runner 7 finish
Runner 5 finish
all finish
record scores

可以看的,8个运动员线程等到judge的计数器归零以后才开始启动,等到8个线程都运行结束了,程序才继续往后执行

三、源码分析(基于Java 8)

1. new CountDownLatch()

进入CountDownLatch的构造方法,在源码中这是CountDownLatch唯一的一个构造方法,而且是有参的。而且一旦count传参进去之后,无法再更改。

//CountDownLatch唯一的有参构造方法
public CountDownLatch(int count) {
	//传进来的count代表上锁的个数,一般要等待多少线程就会上多少锁,如果参数小于0就报异常
    if (count < 0) throw new IllegalArgumentException("count < 0");
    //调用Sync的构造方法,Sync继承于AbstractQueuedSynchronizer
    this.sync = new Sync(count);
}

找到Sync的构造方法

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

进入setState,我们发现count的值最终被赋给了state

protected final void setState(int newState) {
    state = newState;
}

state正是AQS里的一个变量,代表锁状态

/**
 * The synchronization state.
 */
private volatile int state;

简单的说,执行new CountDownLatch(count)就是将锁状态赋值为count,即在一开始加了count把锁。

2. countDown()

进入countDown()

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

进入releaseShared

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

进入tryReleaseSharedCountDownLatch的实现,将锁状态减1,如果锁状态已经是0了就返回false

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

如果锁状态被减为0了,锁就自由了。由于是共享锁,这里就会继续调用doReleaseShared()尝试唤醒后一个节点。这是一个死循环,只有当head节点未发生改变的时候才会跳出循环。如果队列中第一个排队的节点被唤醒了,由于队列后移,head节点会发生改变,那此时循环就会继续。

/**
  * Release action for shared mode -- signals successor and ensures
  * propagation. (Note: For exclusive mode, release just amounts
  * to calling unparkSuccessor of head if it needs signal.)
  */
 private void doReleaseShared() {
     for (;;) {
         Node h = head;
         if (h != null && h != tail) {
             int ws = h.waitStatus;
             if (ws == Node.SIGNAL) {
             	 //CAS将head节点状态设为0,如果失败的话就循环再试一次
                 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) 
                     continue;            // loop to recheck cases
                 unparkSuccessor(h); //将head后一个节点唤醒
             }
             //如果head节点当前状态是0的话,就CAS将节点状态设为PROPAGATE也就是-3,如果失败的话就循环再试一次
             else if (ws == 0 &&
                      !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //
                 continue;                // loop on failed CAS
         }
         if (h == head)                   // loop if head changed
             break;
     }
 }

3. await()

进入await()方法,调用了Sync的方法

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

进入acquireSharedInterruptibly

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted()) //先判断有没有被中断
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0) //尝试获得共享锁
        doAcquireSharedInterruptibly(arg);
}

进入tryAcquireShared,找到CountDownLatch的实现

protected int tryAcquireShared(int acquires) {
	//如果锁状态为0返回1,否则返回-1
    return (getState() == 0) ? 1 : -1;
}

那我们假定锁状态为大于0,那tryAcquireShared就会返回-1,回到acquireSharedInterruptibly方法就会向下执行doAcquireSharedInterruptibly

/**
  * Acquires in shared interruptible mode.
  * @param arg the acquire argument
  */
 private void doAcquireSharedInterruptibly(int arg)
     throws InterruptedException {
     final Node node = addWaiter(Node.SHARED); //令当前节点入队,并且返回这个节点
     boolean failed = true;
     try {
         for (;;) {
             final Node p = node.predecessor(); //找到当前节点前一个节点,如果没有的话就返回null
             if (p == head) { //判断前一个节点是不是head,如果是的话或者根本就没有队列的话,往下走
                 int r = tryAcquireShared(arg); //这里自旋一次看看锁状态是不是自由,调用的就是之前的tryAcquireShared方法
                 if (r >= 0) { //锁状态自由r才会大于0
                     setHeadAndPropagate(node, r); //重新设置头结点,然后看后一个节点是不是共享模式,如果是的话也尝试唤醒
                     p.next = null; // help GC //把前一个节点移出队列
                     failed = false;
                     return;
                 }
             }
             if (shouldParkAfterFailedAcquire(p, node) && //把前一个节点状态置为Node.SIGNAL也就是-1
                 parkAndCheckInterrupt()) //在这里阻塞当前线程
                 throw new InterruptedException();
         }
     } finally {
         if (failed)
             cancelAcquire(node); //如果线程中断就会引起异常然后进入这里,将当前节点状态设为取消,之后会从队列中被移出
     }
 }

调用await(),如果当前线程尝试拿锁失败,那就会在parkAndCheckInterrupt()这里阻塞,被唤醒以后也是从这里继续向下执行,setHeadAndPropagate方法会把队列往后移,同时也会调用doReleaseShared()尝试唤醒后一个节点。

四、CountDownLatch 和 join 有区别吗

本篇给出的两个代码例子是可以用join实现相同功能的。但并不是所有CountDownLatch的场景都可以用join来取代。简单的说,CountDownLatch 比 join 其实更灵活,而且相比join,更推荐使用CountDownLatch。

  1. join必须等待线程执行结束,而CountDownLatch不需要
    join的源码实现其实就是在一直检查调用join的线程是否还存活,如果存活的话就让当前线程继续wait()等待下去;而CountDownLatch并不需要等待线程执行结束。我们可以把计数器减1的操作,也就是countDown(),放在代码的任何位置,比如某个事务的中间。那我们就可以实现类似这样的功能:让某一事务等待其它几个事务完成部分阶段之后,开始执行。

  2. 使用join必须得到线程引用,而CountDownLatch不需要
    CountDownLatch不属于任何一个线程,所以我们在给计数器减1的时候根本不用关心是哪个线程的执行代码块调用了countDown()。但是使用join的时候,我们必须知道那个线程的引用。从这个方面来看,使用CountDownLatch有时候就会减少很多麻烦。比如一个联机游戏要等待至少3个玩家进入房间才能开始游戏。那使用CountDownLatch的话就根本不需要去获取其它玩家的线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值