03. 并发编程 - 并发工具类

52 阅读3分钟

并发编程——并发工具类

0.1. 内容概要

  1. CountDownLatch
  2. CyclicBarrier
  3. Semaphore
  4. Exchanger
  5. 总结

0.2. 学习目标

  • 理解常用并发工具类的概念、工作原理
  • 能够根据需求正确使用并发工具类

1. CountDownLatch

CountDownLatch-领队带三个小伙伴出游.gif

1.1. 概念和作用

倒计时门闩;倒计时计数器;发令枪;

一个同步辅助类,它允许一个或多个线程等待,直到在其他线程中执行的一组操作完成为止。

  • 特点

    无法重置计数

  • 使用场景

    等待/通知;倒计时计数;

  • 基本操作

    await():使当前线程等待,直到计数器被扣减为0

    countDown():扣减计数器,每次扣减1

1.2. 注意事项

  1. countDown之后,子线程依然在执行,并未被阻塞

  2. 若计数器未能完全释放,则main线程一直等待(可以尝试使用await带超时参数的重载方法)

  3. 计数器 >= 线程数,一个线程可以倒计时多次

1.3. 扩展:底层实现机制

共享锁:也叫做读锁,

​ 所有加锁的线程只能读取锁,不能写入、修改等操作,

​ 每次获取锁,得到的锁状态都是固定值,

​ 其它线程也可以对该锁对象进行加锁(但是只能加共享锁,而非互斥锁)

加锁过程:LockSupport.park(this) 释放锁过程:LockSupport.unpark(s.thread)

1.4. 示例代码

1.4.1. 需求一:

领队带领10个小伙伴一起出游,模拟实现签到功能

1.4.1. 思路分析:

​ 1.使用main线程模拟领队

​ 2.定义总数为10的计数器,代表10个小伙伴

​ 3.每个人签到时,计数器扣减1

​ 4.所有人都签到完成,即计数器扣减为0时,出发,否则,领队将一直等待

1.4.1. 步骤分析:

​ 1.定义计数器CountDownLatch对象,计数参数为10

​ 2.启动10个线程,模拟10个小伙伴

​ 3.每个小伙伴都执行相同的任务:出发-签到-上车

​ 在执行签到动作时,进行计数器的扣减

1.4.1. 代码实现:

String[] actions = {"打瞌睡", "听歌", "玩游戏", "吃早餐", "闲聊"};
// 1.定义计数器CountDownLatch对象,计数参数为10
CountDownLatch count = new CountDownLatch(10);
Runnable runnable = () -> {
    Random random = new Random();
    try {
        int waitSeconds = random.nextInt(5); // 随机休眠时间
        Thread.sleep(waitSeconds * 1000);
        String name = Thread.currentThread().getName();
        System.out.println("小伙伴" + name + "磨磨蹭蹭了" + waitSeconds + "秒");
		// 3.在执行签到动作时,进行计数器的扣减
        count.countDown(); // 小伙伴上车了
        System.out.println("小伙伴" + name + "已上车,开始" + actions[waitSeconds]);

        if (Integer.parseInt(name) == 5) {
            count.countDown();
            System.err.println("小伙伴" + name + "又签到了一次");
        }

    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};
System.out.println("小伙们已出发。。。开始计时");
long start = System.currentTimeMillis();
// 2.启动10个线程,模拟10个小伙伴
for (int i = 0; i < 9; i++) {
    new Thread(runnable, "" + i).start();
}

try {
    count.await(); // 领队等待所有小伙伴上车
    // count.await(7000, TimeUnit.MILLISECONDS); // 领队等待所有小伙伴上车
} catch (InterruptedException e) {
    e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("所有小伙伴都已到达,共耗时:"+ (end - start) / 1000.0 +"秒");

1.4.1. 输出结果:

小伙们已出发。。。开始计时
小伙伴8磨磨蹭蹭了0秒
小伙伴2磨磨蹭蹭了0秒
小伙伴4磨磨蹭蹭了0秒
小伙伴4已上车,开始打瞌睡
小伙伴8已上车,开始打瞌睡
小伙伴2已上车,开始打瞌睡
小伙伴1磨磨蹭蹭了2秒
小伙伴7磨磨蹭蹭了2秒
小伙伴1已上车,开始玩游戏
小伙伴7已上车,开始玩游戏
小伙伴3磨磨蹭蹭了3秒
小伙伴6磨磨蹭蹭了3秒
小伙伴0磨磨蹭蹭了3秒
小伙伴6已上车,开始吃早餐
小伙伴3已上车,开始吃早餐
小伙伴0已上车,开始吃早餐
小伙伴5磨磨蹭蹭了4秒
小伙伴5已上车,开始闲聊
小伙伴5又签到了一次
所有小伙伴都已到达,共耗时:4.031秒

1.4.2. 需求二:

15个线程并发计数后,合并总数,在主线程中打印结果

1.4.2. 思路分析:

​ 1.定义计数器CountDownLatch对象

​ 2.所有子线程启动之后,主线程应该立即进入等待

​ 3.子线程完成任务之后,计数器对象减 1

1.4.2. 代码实现:

public static AtomicInteger number = new AtomicInteger(0);
public static void main(String[] args) {
    // 1.定义计数器CountDownLatch对象
    CountDownLatch count = new CountDownLatch(15);
    Runnable runnable = () -> {
        for (int i = 0; i < 1000; i++){
            number.incrementAndGet(); // number = ++number
        }
        System.out.println(Thread.currentThread().getName() + ":" + number.get());
        // 3.子线程完成任务之后,计数器对象减 1
        count.countDown();
    };
    for (int i = 0; i < 15; i++) {
        new Thread(runnable).start();
    }

    // 2.所有子线程启动之后,主线程应该立即进入等待
    try {
        count.await(); // 加强版join()
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    //        try {
    //            Thread.sleep(3000);
    //        } catch (InterruptedException e) {
    //            e.printStackTrace();
    //        }
    System.out.println("最终结果" + number.get());
}

1.4.2. 输出结果:

Thread-12:4018
Thread-6:7810
Thread-4:14039
Thread-7:9378
Thread-0:3000
Thread-11:10686
Thread-14:5810
Thread-8:11000
Thread-3:6810
Thread-9:15000
Thread-5:14194
Thread-10:8234
Thread-2:2074
Thread-1:1518
Thread-13:14680
最终结果15000

2. CyclicBarrier

CyclicBarrier-每个站点进行一次集合.gif

2.1. 概念和作用

循环屏障;

一个同步辅助类,它允许一组线程全部互相等待以到达一个公共的障碍点。

  • 特点

    释放等待线程后可以重新使用

  • 使用场景

    执行一组固定大小、且彼此偶尔需要相互等待的线程;

  • 基本操作

    await():使当前线程等待,直到计数器被扣减为0后,会自动唤醒

2.2. 扩展:底层实现机制

  1. 使用可重入锁ReentrantLock + Condition对象实现同步:

    让线程在同一个条件对象上等待:因为没有扣减到0

  2. 定义计数器count,每当线程执行await方法,计数器减1

  3. 当计数器减为0时,唤醒所有在此条件对象上等待的线程

    ​ 唤醒之后,重置屏障,以便下次使用

  4. 关于超时和异常、线程中断:

    ​ 若线程超时,未能到达屏障,则重置/销毁屏障

    ​ 程序发生异常,则重置/销毁屏障

    ​ 唤醒所有线程

2.3. 示例代码

2.3.1. 需求:

3个小伙伴一起出游。路线:故宫——奥林匹克公园——香山公园。于某日某时某刻在第一站“故宫”集合,然后自由玩耍;出发去下一站之前再次集合,直到游玩结束。

2.3.1. 思路分析:

  1. 分别用三个线程模拟三个小伙伴;
  2. 定义集合点,先到达的小伙伴等待;
  3. 所有小伙伴集合完毕,之后再出发去下一站;

2.3.1. 步骤分析:

  1. 定义CyclicBarrier对象:代表集合点

    约定集合的小伙伴的个数:3

  2. 在需要的集合的地方,调用await方法进行等待

    await()方法会自动计数

    计数扣减为0时,会自动唤醒所有线程

2.3.1. 代码实现:

// 三个旅游站点
String[] imperialPalace = {"天安门", "延禧宫", "慈宁宫"};
String[] olympic = {"鸟巢", "水立方", "森林公园"};
String[] fragrantHills = {"香山东门", "香山寺", "见心斋", "碧云寺"};
String[][] places = {imperialPalace, olympic, fragrantHills};

// 1.定义CyclicBarrier对象:代表集合点
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
    System.err.println("所有小伙伴集合完成");
});
Runnable play = () -> {
    String name = Thread.currentThread().getName();
    try {
        for (int placeNumber = 0; placeNumber < places.length; placeNumber++) {
            // 出发
            System.out.println("小伙伴们已出发。。。正在前往第" + (placeNumber + 1) + "个站点:"
                               + Arrays.toString(places[placeNumber]));

            // 游玩
            String[] place = places[placeNumber]; // 景点数组
            Random random = new Random();
            for (int i = 0; i < place.length; i++) {
                int index = random.nextInt(place.length); // 随机游玩的景点的索引
                Thread.sleep((index + 1) * 1000); // 线程休眠,在此地游玩时间
                System.out.println("小伙伴"+ name +"在"+ place[index] +"玩了" + index + "秒");
            }
            System.out.println("小伙伴"+ name +"正在前往第"+ (placeNumber + 1) +"个集合点");
            // 集合
            cyclicBarrier.await(); // 所有线程在此处阻塞
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    // 结束
    System.out.println("小伙伴"+ name +"游玩结束,今天很开心");
};

// 三个小伙伴
for (int i = 0; i < 3; i ++) {
    new Thread(play).start();
}

2.3.1. 输出结果:

小伙伴们已出发。。。正在前往第1个站点:[天安门, 延禧宫, 慈宁宫]
小伙伴们已出发。。。正在前往第1个站点:[天安门, 延禧宫, 慈宁宫]
小伙伴们已出发。。。正在前往第1个站点:[天安门, 延禧宫, 慈宁宫]
小伙伴Thread-1在延禧宫玩了1秒
小伙伴Thread-2在慈宁宫玩了2秒
小伙伴Thread-0在慈宁宫玩了2秒
小伙伴Thread-2在延禧宫玩了1秒
小伙伴Thread-1在慈宁宫玩了2秒
小伙伴Thread-0在慈宁宫玩了2秒
小伙伴Thread-1在延禧宫玩了1秒
小伙伴Thread-1正在前往第1个集合点
小伙伴Thread-2在慈宁宫玩了2秒
小伙伴Thread-2正在前往第1个集合点
小伙伴Thread-0在慈宁宫玩了2秒
小伙伴Thread-0正在前往第1个集合点
所有小伙伴集合完成
小伙伴们已出发。。。正在前往第2个站点:[鸟巢, 水立方, 森林公园]
小伙伴们已出发。。。正在前往第2个站点:[鸟巢, 水立方, 森林公园]
小伙伴们已出发。。。正在前往第2个站点:[鸟巢, 水立方, 森林公园]
小伙伴Thread-0在水立方玩了1秒
小伙伴Thread-2在水立方玩了1秒
小伙伴Thread-1在森林公园玩了2秒
小伙伴Thread-2在鸟巢玩了0秒
小伙伴Thread-1在鸟巢玩了0秒
小伙伴Thread-0在水立方玩了1秒
小伙伴Thread-2在水立方玩了1秒
小伙伴Thread-2正在前往第2个集合点
小伙伴Thread-0在水立方玩了1秒
小伙伴Thread-0正在前往第2个集合点
小伙伴Thread-1在森林公园玩了2秒
小伙伴Thread-1正在前往第2个集合点
所有小伙伴集合完成
小伙伴们已出发。。。正在前往第3个站点:[香山东门, 香山寺, 见心斋, 碧云寺]
小伙伴们已出发。。。正在前往第3个站点:[香山东门, 香山寺, 见心斋, 碧云寺]
小伙伴们已出发。。。正在前往第3个站点:[香山东门, 香山寺, 见心斋, 碧云寺]
小伙伴Thread-2在香山寺玩了1秒
小伙伴Thread-1在香山寺玩了1秒
小伙伴Thread-2在香山东门玩了0秒
小伙伴Thread-0在碧云寺玩了3秒
小伙伴Thread-1在香山寺玩了1秒
小伙伴Thread-0在香山东门玩了0秒
小伙伴Thread-2在香山寺玩了1秒
小伙伴Thread-1在香山寺玩了1秒
小伙伴Thread-2在香山东门玩了0秒
小伙伴Thread-2正在前往第3个集合点
小伙伴Thread-0在香山寺玩了1秒
小伙伴Thread-0在香山东门玩了0秒
小伙伴Thread-0正在前往第3个集合点
小伙伴Thread-1在见心斋玩了2秒
小伙伴Thread-1正在前往第3个集合点
所有小伙伴集合完成
小伙伴Thread-0游玩结束,今天很开心
小伙伴Thread-1游玩结束,今天很开心
小伙伴Thread-2游玩结束,今天很开心

2.4. CountDownLatch和CyclicBarrier的区别

CountDownLatch和CyclicBarrier的区别.png

3. Semaphore

Semaphore-停车场管理员管理车位.gif

3.1. 概念和作用

信号量,信号灯;

它允许线程集等待,直到被允许继续运行为止。一个信号量管理多个许可(permit),线程必须请求许可,否则等待。

  • 特点

    控制访问多个共享资源的计数器

  • 使用场景

    通常用于限制访问资源的线程总数;

    流量控制;

  • 基本操作

    acquire():请求许可

    release():释放许可

3.2. 注意事项

  1. 锁是由其它线程释放的,而不是主线程(车场管理员)

  2. 获取许可和释放许可的不一定是同一个线程

    第一个线程释放,第二个线程请求获取

  3. 线程可以申请多个许可,也可以释放多个许可

  4. 许可初始化数量为1时,可以当作普通的互斥锁来使用

3.3. 扩展:底层实现机制

公平锁和非公平锁

共享锁

3.4. 示例代码

3.4.1. 需求:

假设停车场有3个车位,且都没有停车。先后到来5辆车,管理员只能允许3辆车停入车位。直到车辆陆陆续续从车场离开,有了空余车位,后来的车才能停入车位。

3.4.1. 思路分析:

  1. 车场管理员就是一个Semaphore,控制车辆访问车位
  2. 车辆入场,必须向管理员申请许可(permit)
  3. 车辆出场,需要向管理员释放许可(permit)

3.4.1. 实现步骤:

  1. 定义车场管理员对象Semaphore,管理3个车位

    ​ Semaphore semaphore = new Semaphore(3);

  2. 车辆入场,申请许可:

    ​ acquire()

  3. 车辆出场,释放许可:

    ​ release()

3.4.1. 代码实现:

// 1.定义车场管理员对象Semaphore,管理3个车位
Semaphore parkingAdmin = new Semaphore(3);
Runnable carRunnable = () -> {
    try {
        String name = Thread.currentThread().getName();
        // 2.车辆入场,申请许可:
        parkingAdmin.acquire();

        // 停车入位
        Random random = new Random();
        int seconds = random.nextInt(3) + 1;
        Thread.sleep(seconds * 1000);
        System.out.println(name + "停车入位,耗时"+ seconds +"秒");

        // 3.车辆出场,释放许可
        seconds = random.nextInt(15) + 1;
        Thread.sleep(seconds * 1000);
        parkingAdmin.release();
        System.err.println(name + "车辆离场,停车"+ seconds +"秒");

    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};
for (int i = 0; i < 5; i++) {
    new Thread(carRunnable).start();
}

3.4.1. 输出结果:

Thread-0停车入位,耗时1秒
Thread-1停车入位,耗时2秒
Thread-2停车入位,耗时3秒
Thread-0车辆离场,停车8秒
Thread-4停车入位,耗时3秒
Thread-1车辆离场,停车13秒
Thread-3停车入位,耗时2秒
Thread-2车辆离场,停车14秒
Thread-4车辆离场,停车7秒
Thread-3车辆离场,停车10秒

4. Exchanger

Exchanger的工作流程.gif

4.1. 概念和作用

交换器;

允许两个线程在要交换的对象准备好时交换对象 。

  • 特点

    在成对出现的线程间交换数据

  • 使用场景

    一个线程向实例添加数据,

    另一个线程从实例清除数据

  • 基本操作

    exchange(V):尝试交换指定对象。直到配对的线程执行到此处之前,此线程一直等待

4.2. 注意事项

  1. Exchanger交换器的交换槽,只支持两两交换

4.3. 示例代码

4.3.1. 需求:

两个特务头子约定在某时某地交换身份

4.3.1. 思路分析:

  1. 使用两个线程来分别模拟两个特务
  2. 定义交换点(同步点),先到达的特务等待
  3. 后来的特务与先到的特务交换身份

4.3.1. 实现步骤:

  1. 创建交换器对象:

    ​ Exchanger<V> exchanger = new Exchanger();

  2. 在指定位置交换身份:

    ​ exchange(V)

4.3.1. 代码实现:

// 1.创建交换器对象
Exchanger<UserInfo> exchanger = new Exchanger<>();
Runnable runnable = () -> {
    try {
        // 前往交换地点
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + "正在前往交换地点....");

        // 到达交换点,等待
        UserInfo user = new UserInfo(18, threadName);
        System.out.println(threadName + "已到达交换点");

        // 进行交换
        user = exchanger.exchange(user); // 在第二个线程到达此处之前,第一个线程一直等待
        System.out.println("交易完成,交易前我是"+ threadName +",交换后的信息是:" + user);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};
new Thread(runnable, "特务头子1").start();
new Thread(runnable, "特务头子2").start();

4.3.1. 输出结果:

特务头子2正在前往交换地点....
特务头子1正在前往交换地点....
特务头子2已到达交换点
特务头子1已到达交换点
交易完成,交易前我是特务头子2,交换后的信息是:UserInfo{age=18, name='特务头子1'}
交易完成,交易前我是特务头子1,交换后的信息是:UserInfo{age=18, name='特务头子2'}