CountDownLatch详细分析,如何理解CountDownLatch,java并发编程中使用CountDownLatch,什么是JUC包中的CountDownLatch

一、初步理解JUC中的CountDownLatch

1、减法计数器。可以用来倒计时,当两个线程同时执行时,如果要确保一个线程优先执行(是优先执行,
不是优先处理完),可以使用计数器,当计数器清零的时候,再让另一个线程执行。比如:下面如何确
保 Thread 的线程比主线程优先输出?

public class Test {
	public static void main(String[] args) {
		//创建一个 CountDownLatch
		CountDownLatch countDownLatch = new CountDownLatch(100); //参数:倒计时数字
		
		new Thread(()->{
			for (int i = 0; i < 100; i++) {
				System.out.println("+++++++++++++++Thread");
				countDownLatch.countDown();
			}
		}).start();
		
		try {
			countDownLatch.await(); //当倒计时走完之后,我就唤醒其它线程
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		for (int i = 0; i < 100; i++) {
			System.out.println("main-----------------");
		}
	}
}
输出结果:无论运行多少次,肯定是 Thread 先输出

注意: new CountDownLatch(100)、countDown()、await() 必须配合起来使用,创建对象的时候赋的值是多少,coutDown() 就必须执行多少次,否则计数器是没有清零的,计数器就不会停止,其他线程也无法唤醒,程序就会一直卡在那。所以必须保证计数器清零(确保清零:coutDown() 的调用次数必须大于构造函数的参数值)

二、深入理解JUC中的CountDownLatch

减法计数器。可以用来倒计时,当两个线程同时执行时,如果要确保一个线程优先执行(是优先执行,不是优先处理完),就可以使用计数器,当计数器清零的时候,再让另一个线程执行。

await()方法阻塞

1、await() 方法是CountDownLatch类的一个核心方法,它的作用是阻塞调用它的线程

2、await() 并不是唤醒线程的操作,而是阻塞线程的一种机制。当一个线程调用 countDownLatch.await() 时,该线程会进入等待状态。只有当 countDown() 减到 0 时,所有在 await() 上等待的线程才会被唤醒,继续往下执行。

案例:假设有一个比赛,10 名运动员在起跑线等待发令枪声响起(所有运动员必须同时开始跑步)。他们跑到终点后,统计最终的成绩。

public class Test {
    public static void main(String[] args) {
        //创建两个CountDownLatch:
        //模拟发令枪(让所有运动员同时起跑)
        CountDownLatch readyToStart = new CountDownLatch(1);
        //统计所有运动员到达终点(让主线程等待所有运动员跑完)
        CountDownLatch raceFinished = new CountDownLatch(10);

        //创建线程池用于执行任务
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        //创建10个运动员
        for (int i = 0; i < 10; i++) {
            //运动员编号
            int athleteNumber = i + 1;
            executorService.submit(() -> {
                try {
                    System.out.println("运动员" + athleteNumber + "准备好了,等待发令枪...");
                    //所有运动员准备好后等待发令枪
                    readyToStart.await(); // 阻塞在这里,等待发令枪(主线程调用countDown())
                    System.out.println("运动员" + athleteNumber + "开始跑步!");

                    // 模拟运动员跑步的过程
                    Thread.sleep((long) (Math.random() * 1000)); // 跑步时间随机

                    System.out.println("运动员" + athleteNumber + "到达终点!");

                    //跑到终点后调用countDown,减少计数器
                    raceFinished.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        try {
            // 让主线程等待几秒模拟运动员准备
            Thread.sleep(2000);

            System.out.println("所有运动员准备好了,发令枪响!");

            // 发令枪响,所有运动员同时起跑
            readyToStart.countDown(); // 将readyToStart的计数器减到0,所有await()的线程开始执行

            // 主线程等待所有运动员到达终点
            raceFinished.await(); // 阻塞主线程,直到raceFinished计数器为0
            System.out.println("所有运动员都到达终点,比赛结束!");

            // 关闭线程池
            executorService.shutdown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果如下:
运动员1准备好了,等待发令枪…
运动员7准备好了,等待发令枪…
运动员8准备好了,等待发令枪…
运动员6准备好了,等待发令枪…
运动员5准备好了,等待发令枪…
运动员4准备好了,等待发令枪…
运动员3准备好了,等待发令枪…
运动员2准备好了,等待发令枪…
运动员9准备好了,等待发令枪…
运动员10准备好了,等待发令枪…
所有运动员准备好了,发令枪响!
运动员7开始跑步!
运动员8开始跑步!
运动员5开始跑步!
运动员9开始跑步!
运动员2开始跑步!
运动员1开始跑步!
运动员3开始跑步!
运动员4开始跑步!
运动员6开始跑步!
运动员10开始跑步!
运动员5到达终点!
运动员2到达终点!
运动员6到达终点!
运动员10到达终点!
运动员9到达终点!
运动员8到达终点!
运动员7到达终点!
运动员4到达终点!
运动员3到达终点!
运动员1到达终点!
所有运动员都到达终点,比赛结束!

具体的过程如下图所示:

理解之后,下面是多线程模拟高并发的代码,这个是一个常见的高并发场景测试代码框架:

/**
 * 多线程模拟高并发
 */
@Slf4j
public class concurrenceTest {

    public static void concurrentTest() {
        //创建一个原子整形类,0是初始值:所有的线程将同时对这个atomicInteger进行操作。
        final AtomicInteger atomicInteger = new AtomicInteger(0);

        //创建2个计数器:
        //当所有都准备好了,再一起执行,模拟多并发,保证并发量
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        //保证所有现场执行完之后再打印atomicInteger的值
        CountDownLatch countDownLatch1 = new CountDownLatch(1000);

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        try {
            for (int i = 0; i < 1000; i++) {
                //线程池会调度这些任务,并发执行
                executorService.submit(() -> {
                    try {
                        //一直阻塞当前线程,直到提交1000个任务(都准备好了)
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        log.error(e.getMessage(), e);
                    }
                    //每个线程的任务:自增1000
                    for (int j = 0; j < 1000; j++) {
                        //下面的代码可以用业务代码替换(这里只是模拟)
                        atomicInteger.incrementAndGet();
                    }
                    //一个任务完成了,计数器-1
                    countDownLatch1.countDown();
                });
                //确保1000个任务创建完成
                countDownLatch.countDown();
            }
            //等待所有线程的任务都执行完,然后主线程执行下面的代码
            countDownLatch1.await();
            log.info("atomicInteger的值为:{}", atomicInteger.get());
            executorService.shutdown();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    public static void main(String[] args) {
        concurrentTest();
    }
}

输出结果:1000 * 1000 = 1,000,000

问题:AtomicInteger是线程安全的,为什么还是1000*1000,而不是1000?

解析:

  • AtomicInteger 是线程安全的。在多线程环境中,AtomicInteger 能够保证所有线程在修改它的值时不会发生竞争条件,从而确保数据的一致性。
  • 并不是因为有 final 修饰才是这样的。代码中的 AtomicInteger 是在方法外部初始化的,并且传递给了所有线程,所以每个线程操作的是同一个 atomicInteger,不是各自独立的实例。
  • 变量共享:在 Java 中,如果一个对象(比如 AtomicInteger)被定义在方法的外部作用域,并传递给多个线程,即使它不是 final,这些线程依然会引用同一个对象的内存地址。因此,所有线程都会操作同一个 AtomicInteger 实例。
  • final 关键字的作用是确保变量不能被重新赋值,而不是控制对象是否在线程间共享。

至此,文章内容结束,主页还有更多干货哦!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小学鸡!

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

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

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

打赏作者

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

抵扣说明:

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

余额充值