【JavaEE】JUC 常见的类 -- 多线程篇(8)

news/2024/11/29 2:29:12/

JUC 常见的类

  • 1. Callable 接口
  • 2. ReentrantLock
  • 3. 原子类
  • 4. 线程池
  • 5. 信号量 Semaphore
  • 6. CountDownLatch

1. Callable 接口

  • Callable Interface 也是一种创建线程的方式
    • Runnable 能表示一个任务 (run方法) – 返回 void
    • Callable 也能表示一个任务(call方法) 返回一个具体的的值, 类型可以通过泛型参数来指定(Object)

代码示例: 创建线程计算 1 + 2 + 3 + … + 1000, 不使用 Callable 版本

  • 创建一个类 Result , 包含一个 sum 表示最终结果, lock 表示线程同步使用的锁对象.
  • main 方法中先创建 Result 实例, 然后创建一个线程 t. 在线程内部计算 1 + 2 + 3 + … + 1000.
  • 主线程同时使用 wait 等待线程 t 计算结束. (注意, 如果执行到 wait 之前, 线程 t 已经计算完了, 就不必等待了).
  • 当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果.
static class Result {public int sum = 0;public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {Result result = new Result();Thread t = new Thread() {@Overridepublic void run() {int sum = 0;for (int i = 1; i <= 1000; i++) {sum += i;}synchronized (result.lock) {result.sum = sum;result.lock.notify();}}};t.start();synchronized (result.lock) {while (result.sum == 0) {result.lock.wait();}System.out.println(result.sum);}
}

可以看到, 上述代码需要一个辅助类 Result, 还需要使用一系列的加锁和 wait notify 操作, 代码复杂, 容易出错.


代码示例: 创建线程计算 1 + 2 + 3 + … + 1000, 使用 Callable 版本

  • 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
  • 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
  • 把 callable 实例使用 FutureTask 包装一下.
  • 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
  • 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
// 使用 Callable 版本实现 1 累加到 100
public static void main(String[] args) throws ExecutionException, InterruptedException {// 1. 创建实现Callable接口的匿名内部类Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i <= 100; i++) {sum += i;}return sum;}};// 2. 创建futureTask类 -- 要来存储callable的返回值FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();// 3. 调用futureTask的get方法 -- 该方法是阻塞等待的Integer result = futureTask.get();System.out.println(result);
}
  • 可以看到, 使用 Callable 和 FutureTask 之后, 代码简化了很多, 也不必手动写线程同步代码了.

理解Callable

  • Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务, Runnable 描述的是不带返回值的任务.
  • Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
  • FutureTask 就可以负责这个等待结果出来的工作.

2. ReentrantLock

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.

ReentrantLock 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
  • unlock(): 解锁

在这里插入图片描述

ReentrantLock 和 synchronized 的区别:

  • synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.在这里插入图片描述
  • 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

如何选择使用哪个锁?

  • 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
  • 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
  • 如果需要使用公平锁, 使用 ReentrantLock.

3. 原子类

使用示例

在这里插入图片描述

原子类的应用场景

  1. 计数需求
    • 播放量, 点赞量…
    • 同一个视频, 有很多人都在同时的播放/点赞
  2. 统计效果
    • 统计出现错误的请求数目 – 使用原子类, 记录出错的请求数目 – 另外写一个监控服务器, 获取线上服务器的这些错误技术, 并且以曲线图的方式绘制到页面上 – 某次发布程序之后, 发现突然这里的错误数大幅度上升, 说明你这个版本代码大概率存在 bug
    • 统计收到请求的总数 (衡量服务器的压力)
    • 统计每个请求的响应事件 -> 平均的响应事件 (衡量服务器的运行效率)
    • 线上服务通过这些统计内容, 进行简单技术 -> 实现监控服务器

4. 线程池

  • 之前的文章里已经讲过了, 请跳转: 线程池

在这里插入图片描述

5. 信号量 Semaphore

信号量的概念

  • 信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.

在这里插入图片描述

Semaphore 使用示例

public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(4);int count = 0;// acquire方法 -- P操作 -- 计数器减一semaphore.acquire();System.out.println(count++);// release方法 -- V操作 -- 计数器加一semaphore.release();semaphore.acquire();System.out.println(count++);semaphore.acquire();System.out.println(count++);semaphore.acquire();System.out.println(count++);semaphore.acquire();System.out.println(count++);semaphore.acquire();System.out.println(count++);semaphore.acquire();System.out.println(count++);
}

运行结果如下;
在这里插入图片描述

6. CountDownLatch

  • 同时等待 N 个任务执行结束.

好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。

应用场景举例

  • 下载一个大文件, 将大文件分成几个小的文件, 分别让多个线程来执行相应的下载任务, 当所有线程完成任务的时候, 该大文件也就下载完成了;
  • CountDownLatch 就是用来等待所有线程完成任务的
  • 和 join不同的是, join表示执行任务的线程退出了; CountDownLatch 只是等线程完成任务, 线程只要告知CountDownLatch 我完成任务即可, 可以不用被销毁

使用举例

public static void main(String[] args) throws InterruptedException {// 构造方法中, 指定创建几个任务.CountDownLatch countDownLatch = new CountDownLatch(10);for (int i = 0; i < 10; i++) {int id = i + 1;Thread t = new Thread(() -> {System.out.println("线程" + id + "正在工作");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("线程" + id + "完成工作");// 每个任务执行结束这里, 调用一下方法// 把 10 个线程想象成短跑比赛的 10 个运动员. countDown 就是运动员撞线了.countDownLatch.countDown();// 假设线程不退出while (true);});t.start();}// 主线程如何知道上述所有的任务都完成了呢??// 难道要在主线程中调用 10 次 join 嘛?// 万一要是任务结束, 但是线程不需要结束, join 不就也不行了嘛?// 主线程中可以使用 countDownLatch 负责等待任务结束.// a => all 等待所有任务结束. 当调用 countDown 次数 < 初始设置的次数, await 就会阻塞.countDownLatch.await();System.out.println("多个线程的所有任务都执行完毕了");}

http://www.ppmy.cn/news/1168298.html

相关文章

后缀表达式求值

题目要求: 后缀表达式求值&#xff1a;建立一个操作数栈S。然后从左到右读表达式&#xff0c;如果读到操作数就将它压入栈S中&#xff0c;如果读到n元运算符(即需要参数个数为n的运算符)则取出由栈顶向下的n项操作数进行运算&#xff0c;再将运算的结果代替原栈顶的n项压入栈中…

【rust/树莓派】使用rppalembedded-graphics控制st7789 LCD屏幕

说在前面 树莓派版本&#xff1a;4bLCD模块&#xff1a;ST7789V2 240*280 LCD树莓派系统&#xff1a;Linux raspberrypi 5.15.76-v8 #1597 SMP aarch64 GNU/Linuxrust版本&#xff1a;rustc 1.73.0 模块详情 某雪的1.69inch LCD模块&#xff0c;包含杜邦线 准备工作 树莓派…

面试题-React(十七):如何使用RTK进行状态管理

Redux Toolkit&#xff08;RTK&#xff09; 是一个强大的工具集&#xff0c;旨在简化和改进Redux的使用。它提供了一组工具和约定&#xff0c;使Redux的配置和编写更加直观和高效。 一、Redux Toolkit简介 Redux Toolkit是一个由Redux官方团队开发和维护的库&#xff0c;旨在…

数据回归算法 | Python逻辑回归

逻辑回归是一种经典的机器学习算法,用于解决二分类问题。 它常被用于预测某个事件发生的概率,通过将输入特征映射到一个概率值来进行分类。 简单聊聊 逻辑回归就像一位智能的侦探,根据一些线索(输入特征)判断某件事情是否会发生。 它将这些线索组合起来,计算出一个关于…

【Java 进阶篇】深入理解 Bootstrap 导航条与分页条

Bootstrap 是一个强大的前端框架&#xff0c;为网页和应用程序开发提供了丰富的组件和工具。其中&#xff0c;导航条和分页条是两个常用的组件&#xff0c;用于创建网站的导航和分页功能。本篇博客将深入探讨 Bootstrap 导航条和分页条的使用&#xff0c;适用于那些希望提升网页…

YOLOv5算法改进(16)— 增加小目标检测层 | 四头检测机制(包括代码+添加步骤+网络结构图)

前言:Hello大家好,我是小哥谈。小目标检测层是指在目标检测任务中用于检测小尺寸目标的特定网络层。由于小目标具有较小的尺寸和低分辨率,它们往往更加难以检测和定位。YOLOv5算法的检测速度与精度较为平衡,但是对于小目标的检测效果不佳,根据一些论文,我们可以通过增加检…

SpringCloud 微服务全栈体系(一)

第一章 认识微服务 随着互联网行业的发展&#xff0c;对服务的要求也越来越高&#xff0c;服务架构也从单体架构逐渐演变为现在流行的微服务架构。这些架构之间有怎样的差别呢&#xff1f; 一、单体架构 单体架构&#xff1a;将业务的所有功能集中在一个项目中开发&#xff…

ant的Path-like结构

ant可以使用path和classpath结构指明路径。path和classpath可以包含内嵌的元素&#xff0c;类似下面的通用形式&#xff1a; <classpath><pathelement path"${classpath}"/><pathelement location"lib/helper.jar"/> </classpath>…