[Java高并发系列(3)]Java 中 CountDownLatch介绍 + 一道面试题
1 CountDownLatch介绍
1.1 CounttDownLatch有什么用
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助(类)
CountDownLatch 用给定的计数初始化。await( ) 方法阻塞,直到由于 countDown( ) 方法的而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的 await 调用立即返回。 这是一个一次性的现象 - 计数无法重置。 – 这是jdk文档的说法
说白了,可以把它看成是一个内部维护着一个count的计数器,只不过对这个计数器的操作都是原子操作,即同时只能有一个线程去操作这个计数器.
CountDownLatch通过构造函数传入一个初始计数值,调用者可以通过调用CounDownLatch对象的cutDown()方法,来使计数减1;如果调用对象上的await()方法,那么调用者就会一直阻塞在这里,直到别人通过cutDown方法. 当计数减到0时,才可以继续执行。
1.2 CountDownLatch使用例子
假如有10个线程, 我们希望它们能同时开始执行(分别完成一些工作), 并且希望当最后一个线程完成后, 回到主线程, 那么可以这样写:
public class TestCountDownLatch {
public static void main(String[] args) {
//用于使创建的所有线程阻塞, 等待CountDownLatchStart计数器减为0
CountDownLatch countDownLatchStart = new CountDownLatch(1);
//用于使主线程阻塞, 使得所有线程都完成后,才继续执行
CountDownLatch countDownLatchEnd = new CountDownLatch(10);
for(int i = 0 ; i< 10 ; i ++){
new Thread(new Runnable() {
@Override
public void run() {
try {
countDownLatchStart.await(); //每个工作线程先停在这, 等待主线程的CountDown
System.out.println(Thread.currentThread().getName() + " are ready!");
/* work */
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " are completed!");
countDownLatchEnd.countDown(); //每个线程工作完成后 ,将countDownLatchEnd减1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "work thread " + i).start();
}
System.out.println("1s later all will work");
try {
TimeUnit.SECONDS.sleep(1);
countDownLatchStart.countDown(); //start 减为0 , 所有线程同时执行
countDownLatchEnd.await(); //等待所有线程都减一,使得CountDownLatchEnd为0
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("all are completed!");
}
}
1s later all will work
work thread 0 are ready!
work thread 3 are ready!
work thread 6 are ready!
work thread 7 are ready!
work thread 8 are ready!
work thread 2 are ready!
work thread 1 are ready!
work thread 9 are ready!
work thread 4 are ready!
work thread 5 are ready!
work thread 0 are completed!
work thread 8 are completed!
work thread 4 are completed!
work thread 2 are completed!
work thread 6 are completed!
work thread 7 are completed!
work thread 3 are completed!
work thread 9 are completed!
work thread 5 are completed!
work thread 1 are completed!
all are completed!
1.3 CountDownLatch使用场景
- 确保某个计算在其需要的所有资源都被初始化之后才继续执行.
- 确保某个服务在其依赖的所有其他服务都已启动后才启动.
- 等待知道某个操作的所有者都就绪在继续执行。
2 一道面试题
下面这道题可以很好地将之前介绍过的synchronized关键字和CountDownLatch结合
实现一个容器,提供两个方法: add,size . add() 用于向容器中加入一个对象,getSize( )用于返回其对象个数
-
线程1添加10个元素到容器中,
-
线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
法1:可以使用sychronized和所对象的notify和wait方法完成
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class MyContainerT3 {
List list = new ArrayList();
public void add(Object o){list.add(o);}
public int getSize(){return list.size();}
public static void main(String[] args) {
MyContainerT3 myContainerT3 = new MyContainerT3();
Object lock = new Object();
/* monitor thread (thread 1)*/
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " started!" );
if (myContainerT3.getSize() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println( " has arrived 5 " );
System.out.println(Thread.currentThread().getName() + " end !" );
lock.notify();
}
}
}, "monitor thread").start();
/* add thread (thread 1)*/
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
System.out.println(Thread.currentThread().getName() + " started!" );
for (int i = 0; i< 10 ;i ++ ) {
myContainerT3.add(new Object());
System.out.println("object " + i + " has added ");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(myContainerT3.getSize() == 5){
lock.notify();
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}, "add thread").start();
}
}
- 先使用thread2 进行监听, 当不等于size不为5的时候就释放锁给thread 1执行.
- 在thread1中 判断到size为5了 , 唤醒 thread2继续执行.
- thread2 继续执行, 打印出size为5时的消息, 完成后notify()释放锁让thread 1 继续执行
- thread1 继续执行
法2: 用CountDownLatch 进行控制
/**
* 使用Latch(门闩)替代wait notify来进行通知
* 好处是通信方式简单,同时也可以指定等待时间
* 使用await和countdown方法替代wait和notify
* CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行
* 当不涉及同步,只是涉及线程通信的时候,用synchronized + wait/notify就显得太重了
* 这时应该考虑countdownlatch/cyclicbarrier/semaphore
*/
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class MyContainer5 {
// 添加volatile,使t2能够得到通知
volatile List lists = new ArrayList();
public void add(Object o) {lists.add(o);}
public int size() {return lists.size();}
public static void main(String[] args) {
MyContainer5 c = new MyContainer5();
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("t2启动");
if (c.size() != 5) {
try {
latch.await(); //等待着其他线程将count减为0
//也可以指定等待时间
//latch.await(5000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("t1启动");
for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add " + i);
if (c.size() == 5) {
// 打开门闩,让t2得以执行
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
}
}