Java并发之CountDownLatch

20 篇文章 4 订阅
13 篇文章 0 订阅

Java并发之CountDownLatch

目录

Java并发之CountDownLatch

1、什么是CountDownLatch

2、CountDownLatch如何工作的

3、方法说明

4、实例:一个线程等待其他线程运算结果,其他线程不需要阻塞等待

6、源码分析:

7、CountDownLatch内部实现:

7.1 await内部实现流程:

7.2 countDown内部实现流程:

7.3 countDown 与CyclicBarrier的区别:

8、AbstractQueuedSynchronizer (简称AQS)

9、CountDownLatch应用场景:


1、什么是CountDownLatch


同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
CountDownLatch是java的JUC并发包里的一个工具类,可以理解为一个倒计时器,主要是用来控制多个线程之间的通信。 
比如有一个主线程A,它要等待其他4个子线程执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

闭锁是一种同步工具类,可以延迟线程的进度,直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能够通过,当达到结束状态时,这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。
闭锁可以确保某些活动直到其他活动都完成之后才继续执行。

CountDownLatch是一种灵活的闭锁实现,它允许一个或多个线程等待一组事件的产生。
闭锁状态包括一个计数器,该计数器初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生了,而await方法会一直阻塞直到计数器为0,或者等待中的线程中断,或者等待超时。

2、CountDownLatch如何工作的


正如Java文档所描述的那样,CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。
CountDownLatch是在Java1.5被引入的,跟它一起被引入的并发工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue,它们都存在于java.util.concurrent包下。

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。
每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,
然后在闭锁上等待的线程就可以恢复执行任务。

3、方法说明

// 构造器,必须指定一个大于零的计数
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

// 构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。
// 这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。

// 计数-1
public void countDown() {
    sync.releaseShared(1);
}
// 获取计数
public long getCount() {
    return sync.getCount();
}

// 其他线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。
// 这种通知机制是通过CountDownLatch.countDown()方法来完成的,每调用一次这个方法,在构造函数中初始化的count值就减1.所以当N个线程都调用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。

// 线程阻塞,直到计数为0的时候唤醒;可以响应线程中断退出阻塞
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
// 线程阻塞一段时间,如果计数依然不是0,则返回false;否则返回true
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

 

必须有线程中显示的调用了countDown()计数-1方法;必须有线程显示调用了 await()方法(没有这个就没有必要使用CountDownLatch了)
由于await()方法会阻塞到计数为0,如果在代码逻辑中某个线程漏掉了计数-1,导致最终计数一直大于0,直接导致死锁了
鉴于上面一点,更多的推荐 await(long, TimeUnit)来替代直接使用await()方法,至少不会造成阻塞死只能重启的情况。

当线程调用了 await()实现阻塞同步,则这个线程就等待这个计数器变为0,当这个计数器变为0时,这个线程继续自己下面的工作。

4、实例:一个线程等待其他线程运算结果,其他线程不需要阻塞等待


1、多线程实现累加,多个线程不需要同时开始,一个线程等待其他线程的运算结果。此时的其他线程可以去做其他事情了,不需要阻塞等待在这了。注意和CyclicBarrier区分,参考对比更容易理解。

/**
 * @author powerful
 * @dec 模拟多线程运算,异步分段计算,线程1实现 1加到1000,线程2实现 1000加到2000,线程3实现 2000加到5000,线程4实现
 *      线程1、2、3计算结果的和。 这个和上个例子不同,不需要统一的开始时间,只要线程1和2完成运算,线程3才能运算。
 *      
 *      线程3 : 线程3开始运算
		线程2 : 线程2开始运算
		线程1 : 线程1开始运算
		线程4 : 开始等待结果
		线程3 : 线程3运算结束: 10498500
		线程2 : 线程2运算结束: 1499500
		线程1 : 线程1运算结束: 499500
		线程4 : 开始计算总和
		线程4 : 线程4运算结束: 12497500
 * 
 */

public class CountDownLatchMultithreadOperation {
	private CountDownLatch countDownLatch;
	private int Num1 = 1, Num2 = 1000, Num3 = 2000, Num4 = 5000;
	private volatile int tmpRes1, tmpRes2, tmpRes3;

	// 计算开始数值到结束数值的和
	private int add(int startNum, int endNum) {
		int sum = 0;
		for (int i = startNum; i < endNum; i++) {
			sum += i;
		}
		return sum;
	}

	// 统计所有的和的值
	private int totalSum(int a, int b, int c) {
		return a + b + c;
	}

	public void calculate() {
		countDownLatch = new CountDownLatch(3);// 有3个需要处理计算的线程,第4个线程等待前3个线程运算结果

		Thread thread1 = new Thread(() -> {
			try {
				// 确保线程3先与1,2执行,由于countDownLatch计数不为0而阻塞
				System.out.println(Thread.currentThread().getName() + " : 线程1开始运算");
				Thread.sleep((long) (Math.random() * 10000));
				tmpRes1 = add(Num1, Num2);
				System.out.println(Thread.currentThread().getName() + " : 线程1运算结束: " + tmpRes1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				countDownLatch.countDown();
			}
		}, "线程1");

		Thread thread2 = new Thread(() -> {
			try {
				// 确保线程3先与1,2执行,由于countDownLatch计数不为0而阻塞
				System.out.println(Thread.currentThread().getName() + " : 线程2开始运算");
				Thread.sleep(1000);
				tmpRes2 = add(Num2, Num3);
				System.out.println(Thread.currentThread().getName() + " : 线程2运算结束: " + tmpRes2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				countDownLatch.countDown();
			}
		}, "线程2");

		Thread thread3 = new Thread(() -> {
			try {
				// 确保线程3先与1,2执行,由于countDownLatch计数不为0而阻塞
				System.out.println(Thread.currentThread().getName() + " : 线程3开始运算");
				Thread.sleep(100);
				tmpRes3 = add(Num3, Num4);
				System.out.println(Thread.currentThread().getName() + " : 线程3运算结束: " + tmpRes3);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				countDownLatch.countDown();
			}
		}, "线程3");

		Thread thread4 = new Thread(() -> {
			try {
				System.out.println(Thread.currentThread().getName() + " : 开始等待结果");
				countDownLatch.await();
				System.out.println(Thread.currentThread().getName() + " : 开始计算总和");
				int ans = totalSum(tmpRes1, tmpRes2, tmpRes3);
				System.out.println(Thread.currentThread().getName() + " : 线程4运算结束: " + ans);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}, "线程4");

		thread3.start();
		thread4.start();
		thread1.start();
		thread2.start();

	}

	public static void main(String[] args) throws InterruptedException {
		CountDownLatchMultithreadOperation demo = new CountDownLatchMultithreadOperation();
		demo.calculate();

		Thread.sleep(10000);
	}
}


2、多个线程同时开始,等所有线程都结束,主线程结束。

/**
 * @author powerful
 * @dec 模拟了百米赛跑,10名选手已经准备就绪,只等裁判一声令下。当所有人都到达终点时,比赛结束。
 */

public class PlayersTest {
	public static void main(String[] args) throws InterruptedException {
		// 开始的倒数锁,开始的信号,没有开始都在等待,当开始信号发出(执行countDown方法),
		final CountDownLatch begin = new CountDownLatch(1);// 控制统一开始,信号发出即开始。
		// 结束的倒数锁,所有的人跑完,所有的线程结束,执行完成。
		final CountDownLatch end = new CountDownLatch(10);// 控制统一结束,所有人跑完结束。
		// 模拟出10个线程代表10个参赛选手
		for (int index = 0; index < 10; index++) {
			final int num = index + 1;
			new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						// 如果当前计数为零,则此方法立即返回。
						begin.await();// 等待开始命令
						Thread.sleep((long) (Math.random() * 10000));
						System.out.println("No." + num + " arrived");
					} catch (InterruptedException ignored) {
					} finally {
						end.countDown(); // 选手到达终点时,end就减一
					}
				}
			}).start();
		}
		
		System.out.println("Game Start");
		begin.countDown(); // begin减一,开始游戏
		
		System.out.println("players is running!");
		
		end.await(); // 等待end变为0,即所有选手到达终点
		System.out.println("Game Over");
	}
}

6、源码分析:

// 核心代码:
CountDownLatch latch = new CountDownLatch(1);
latch.await();
latch.countDown();

其中构造函数的参数是计数器的值; 
await()方法是用来阻塞线程,直到计数器的值为0 ;
countDown()方法是执行计数器-1操作。

1、构造函数的代码

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


首先if判断传入的count是否<0,如果小于0直接抛异常。 
然后new一个类Sync。

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

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

        int getCount() {
            return getState();
        }
    //尝试获取共享锁
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -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;
            }
        }
    }


可以看到Sync是一个内部类,继承了AQS,AQS是一个同步器,之后我们会详细讲。 
其中有几个核心点:

变量 state是父类AQS里面的变量,在这里的语义是计数器的值;
getState()方法也是父类AQS里的方法,很简单,就是获取state的值;
tryAcquireShared和tryReleaseShared也是父类AQS里面的方法,在这里CountDownLatch对他们进行了重写。

2、了解了CountDownLatch的构造函数之后,我们再来看它的核心代码,首先是await()。

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


可以看到,其实是通过内部类Sync调用了父类AQS的acquireSharedInterruptibly()方法。

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
    //判断线程是否是中断状态
        if (Thread.interrupted())
            throw new InterruptedException();
    //尝试获取state的值
        if (tryAcquireShared(arg) < 0)//step1
            doAcquireSharedInterruptibly(arg);//step2,获取共享锁失败
    }


tryAcquireShared(arg)这个方法就是我们刚才在Sync内看到的重写父类AQS的方法,意思就是判断是否getState() == 0,
如果state为0,返回1,则step1处不进入if体内acquireSharedInterruptibly(int arg)方法执行完毕。
若state!=0,则返回-1,进入if体内step2处。

下面我们来看doAcquireSharedInterruptibly方法:

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
    //step1、把当前线程封装为共享类型的Node,加入队列尾部
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
        //step2、获取当前node的前一个元素
                final Node p = node.predecessor();
        //step3、如果前一个元素是队首
                if (p == head) {
            //step4、再次调用tryAcquireShared()方法,判断state的值是否为0
                    int r = tryAcquireShared(arg);
            //step5、如果state的值==0
                    if (r >= 0) {
            //step6、设置当前node为队首,并尝试释放共享锁
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
        //step7、是否可以安心挂起当前线程,是就挂起;并且判断当前线程是否中断
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
    //step8、如果出现异常,failed没有更新为false,则把当前node从队列中取消
            if (failed)
                cancelAcquire(node);
        }
    }


按照代码中的注释,我们可以大概了解该方法的内容,下面我们来仔细看下其中调用的一些方法是干什么的。 
1、首先看addWaiter()

//step1
private Node addWaiter(Node mode) {
    //把当前线程封装为node
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
    //获取当前队列的队尾tail,并赋值给pred
        Node pred = tail;
    //如果pred!=null,即当前队尾不为null
        if (pred != null) {
    //把当前队尾tail,变成当前node的前继节点
            node.prev = pred;
        //cas更新当前node为新的队尾
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
    //如果队尾为空,走enq方法
        enq(node);//step1.1
        return node;
    }

-----------------------------------------------------------------
//step1.1
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
        //如果队尾tail为null,初始化队列
            if (t == null) { // Must initialize
        //cas设置一个新的空node为队首
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
        //cas把当前node设置为新队尾,把前队尾设置成当前node的前继节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }


2、接下来我们在来看setHeadAndPropagate()方法,看其内部实现

//step6
private void setHeadAndPropagate(Node node, int propagate) {
    //获取队首head
        Node h = head; // Record old head for check below
    //设置当前node为队首,并取消node所关联的线程
        setHead(node);
    //
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
        //如果当前node的后继节点为null或者是shared类型的
            if (s == null || s.isShared())
        //释放锁,唤醒下一个线程
                doReleaseShared();//step6.1
        }
    }
--------------------------------------------------------------------
//step6.1
private void doReleaseShared() {
        for (;;) {
        //找到头节点
            Node h = head;
            if (h != null && h != tail) {
        //获取头节点状态
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
            //唤醒head节点的next节点
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }


3、接下来我们来看countDown()方法。

public void countDown() {
        sync.releaseShared(1);
    }
可以看到调用的是父类AQS的releaseShared 方法

public final boolean releaseShared(int arg) {
    //state-1
        if (tryReleaseShared(arg)) {//step1
        //唤醒等待线程,内部调用的是LockSupport.unpark方法
            doReleaseShared();//step2
            return true;
        }
        return false;
    }
------------------------------------------------------------------
//step1
protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
        //获取当前state的值
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
        //cas操作来进行原子减1
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }


7、CountDownLatch内部实现:

CountDownLatch主要是通过计数器state来控制是否可以执行其他操作,如果不能就通过LockSupport.park()方法挂起线程,直到其他线程执行完毕后唤醒它。
下面我们通过一个简单的图来帮助我们理解一下:        
        

7.1 await内部实现流程:

判断state计数是否为0,是,则放过执行后面的代码;
大于0,则表示需要阻塞等待计数为0
当前线程封装Node对象,进入阻塞队列
然后就是循环尝试获取锁,直到成功(即state为0)后出队,继续执行线程后续代码。

7.2 countDown内部实现流程:


尝试释放锁tryReleaseShared,实现计数-1
若计数已经小于0,则直接返回false
否则执行计数(AQS的state)减一
若减完之后,state==0,表示没有线程占用锁,即释放成功,然后就需要唤醒被阻塞的线程了
释放并唤醒阻塞线程 doReleaseShared
如果队列为空,即表示没有线程被阻塞(也就是说没有线程调用了 CountDownLatch#wait()方法),直接退出
头结点如果为SIGNAL, 则依次唤醒头结点下个节点上关联的线程,并出队。

7.3 countDown 与CyclicBarrier的区别:

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同。
CountDownLatch一般用于某个线程等待若干个其他线程执行完任务之后,它才执行。
CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。
闭锁用于等待事件,而栅栏用于等待其他线程。

8、AbstractQueuedSynchronizer (简称AQS)


 AQS是一个用于构建锁和同步容器的框架。
 事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。
 AQS解决了在实现同步容器时设计的大量细节问题

AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。
其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus

private transient volatile Node head;

private transient volatile Node tail;

private volatile int state;

static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    /** waitStatus value to indicate thread has cancelled */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    static final int PROPAGATE = -3;

    //取值为 CANCELLED, SIGNAL, CONDITION, PROPAGATE 之一
    volatile int waitStatus;

    volatile Node prev;

    volatile Node next;

    // Link to next node waiting on condition, 
    // or the special value SHARED
    volatile Thread thread;

    Node nextWaiter;
}

9、CountDownLatch应用场景:


电商的详情页,由众多的数据拼装组成,如可以分成一下几个模块:
交易的收发货地址,销量
商品的基本信息(标题,图文详情之类的)
推荐的商品列表
评价的内容
....
上面的几个模块信息,都是从不同的服务获取信息,且彼此没啥关联;所以为了提高响应,完全可以做成并发获取数据,如

线程1获取交易相关数据
线程2获取商品基本信息
线程3获取推荐的信息
线程4获取评价信息
....
但是最终拼装数据并返回给前端,需要等到上面的所有信息都获取完毕之后,才能返回,这个场景就非常的适合 CountDownLatch来做了。

当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。
在这个需求中,要实现主线程等待所有线程完成sheet的解析操作。


参考博文:
https://blog.csdn.net/qq_38293564/article/details/80557355 
http://blog.itmyhome.com/2017/07/java-concurrent-countdownlatch

 

每天努力一点,每天都在进步。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

powerfuler

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值