文章目录
- 前言
- [1. Callable 接口](#1. Callable 接口)
-
- [1.1 回想创建线程方法](#1.1 回想创建线程方法)
- [2. ReentrantLock 可重入互斥锁](#2. ReentrantLock 可重入互斥锁)
- [3. Atomic 原子类](#3. Atomic 原子类)
- [4. 线程池](#4. 线程池)
- [5. Semaphore 信号量](#5. Semaphore 信号量)
- [6. CountDownLatch](#6. CountDownLatch)
- 总结
前言
本文主要讲解 JUC ---- java.util.concurrent 中的一些常见类. concurrent 就是并发的意思, 所以该类中放的都是一些多线程并发编程, 常常使用到的东西.
关注收藏, 开始学习吧🧐
1. Callable 接口
Callable interface 也是一种创建线程的方式, 相当于把线程封装了一个 "返回值". 方便程序员借助多线程的方式计算结果. 他与之前 Thread 使用 Runable 创建线程有些不同, 并且使用 Callable 不能直接作为 Thread 构造方法的参数.
- Runnable 能表示一个任务 (run 方法), 该方法返回的是一个空值.
- Callable 也能表示一个任务(call 方法), 该方法返回一个具体的值, 类型可以通过泛型参数来指定.
- 如果进行多线程操作, 如果只是关心多线程执行的过程, 那么使用 Runnable 即可. (比如线程池, 定时器, 都是使用 Runnable 去实现)
- 如果是关心多线程的计算结果, 使用 Callable 就更加合适. (创建一个线程, 让这个线程计算从1加到1000)
代码示例: 创建线程计算 1 + 2 + 3 + ... + 1000, 不使用 Callable 版本
- 创建一个类 Result , 包含一个 sum 表示最终结果, lock 表示线程同步使用的锁对象.
- main 方法中先创建 Result 实例, 然后创建一个线程 t. 在线程内部计算 1 + 2 + 3 + ... + 1000.
- 主线程同时使用 wait 等待线程 t 计算结束. (注意, 如果执行到 wait 之前, 线程 t 已经计算完了, 就不必等待了).
- 当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果.
java
public class ThreadDemo26 {
static class Result {
public int sum = 0;
public Object locker = new Object();
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread thread = new Thread() {
@Override
public void run() {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
synchronized (result.locker) {
result.sum = sum;
result.locker.notify();
}
}
};
thread.start();
synchronized (result.locker) {
while (result.sum == 0) {
result.locker.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 中的结果.
java
public class ThreadDemo27 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
int result = futureTask.get();
System.out.println(result);
}
}
可以看到, 使用 Callable 和 FutureTask 之后, 代码简化了很多, 也不必手动写线程同步代码了.
理解 Callable
- Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务, Runnable 描述的是不带返回值的任务.
- Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
- FutureTask 就可以负责这个等待结果出来的工作.
理解 FutureTask
- 想象自己去吃快餐. 当餐点好后, 后厨就开始做了. 同时前台会给你一张 "小票" . 这个小票就相当于是 FutureTask.
- 后面我们可以随时凭这张小票去查看自己的这份饭做出来了没.
1.1 回想创建线程方法
- 直接继承 Thread.
- 实现 Runnable.
- 使用 Lambda.
- 使用线程池.
- 使用 Callable.
2. ReentrantLock 可重入互斥锁
"Reentrant" 这个单词的原意就是 "可重入", 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全. 这个锁没有 synchronized 那么常用, 在使用上更接近 C++ 中的 mutex 锁.
ReentrantLock 的用法:
- lock(): 加锁, 如果获取不到锁就死等.
- trylock(等待时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
- unlock(): 解锁.
java
ReentrantLock lock = new ReentrantLock();
-----------------------------------------
lock.lock();
try {
// working
} finally {
lock.unlock()
}
ReentrantLock 和 synchronized 的区别:
- synchronized 是一个关键字, 是 JVM 内部实现的(基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
- synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
- synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃. 给加锁提供了更多的操作空间.
- synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
- 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
当然, ReentrantLock 也有很大的劣势
- 他的 unlock 操作容易被遗漏, 是非常致命的, 所以在使用时, 最好使用 finally 来执行 unlock.
所以在实际开发中, 用到锁一般优先考虑 synchronized. 在以下两个情况时, 可以优先考虑使用 ReentrantLock.
- 锁竞争激烈的时候, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
- 多线程开发时, 该环节需要使用公平锁.
3. Atomic 原子类
原子类, 我们在讲 CAS 部分时介绍过. 原子类内部用的是 CAS 实现, 所以性能要比加锁实现 i++ 高很多. 原子类主要有以下几个
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampedReference
那么在开发中, 原子类主要应用场景有哪些呢?
- 计数需求. 在我们逛视频软件时, 经常可以看到一些视频数据, 比如播放量, 点赞量, 投币量, 转发量, 收藏量等等. 同一个视频经常会有很多人都在同时进行这些操作. 很容易出现线程不安全的问题, 如果我们每个操作都进行上锁来避免不安全, 那么就会消耗很大的资源和时间, 此时如果我们采用原子类来进行计数, 就可以在无锁环境下实现线程安全.
- 统计效果 . 统计出现错误的请求数目, 收到的请求总数(以此简单衡量服务器的压力) , 统计每个请求的响应时间(衡量服务器的运行效率), 通过这些数据内容, 使用原子类来进行计数, 就可以实现一个监控服务器, 用来获取 / 统计 / 展示 / 报警发生错误的情况. 比如在某次发布程序后, 监控服务器显示错误大幅度上升, 说明这个新版本代码大概率存在 bug.
4. 线程池
- ExecutorService 和 Executors.
- ThreadPoolExecutor.
线程池在我之前的一篇文章中重点讲述过, 其用法在这里就不多做介绍了.
5. Semaphore 信号量
Semaphore 信号量, 也是并发编程中的一个重要组件, 在操作系统中也经常出现. 本质上就是一个计数器, 用来表示 "可用资源的个数". 描述的是, 当前该线程, 是否有 "临界资源" 可使用.
临界资源, 指多个线程 / 进程等并发执行的实体, 可以公共使用到的资源.
理解信号量
- 可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.
- 当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作),
acquire
申请. - 当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作),
release
释放. - 如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.
Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.
代码示例
- 创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源.
- acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
- 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果.
java
public class ThreadDemo28 {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("申请资源");
semaphore.acquire();
System.out.println("我获取到资源了");
Thread.sleep(2000);
System.out.println("释放资源");
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
for (int i = 0; i < 20; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
}
}
6. CountDownLatch
CountDownLatch 是用来针对特定场景的一个组件, 可以同时等待多个任务执行结束. 好像跑步比赛, 10个选手依次就位, 哨声响才同时出发. 所有选手都通过终点, 才能公布成绩.
代码示例
- 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
- 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
- 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
java
public class ThreadDemo29 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep((long) (Math.random() * 10000));
System.out.println("一名选手回来");
countDownLatch.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
for (int i = 0; i < 10; i++) {
new Thread(runnable).start();
}
countDownLatch.await();
System.out.println("game over");
}
}
总结
✨ 本文主要讲解了 JUC 中的一些常见类, 需要掌握 Callable 接口, ReentrantLock 锁, 原子类, 线程池, 信号量以及 CountDownLatch.
✨ 想了解更多的多线程知识, 可以收藏一下本人的多线程学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.
再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!