【多线程】JUC中的常见组件
🥰🥰🥰来都来了,不妨点个关注叭!
👉博客主页:欢迎各位大佬!👈
JUC 是 java.util.concurrent
这个包的简写,里面存放了 Java 并发框架为协调并发任务所提供的一些工具,本文将介绍 JUC 中常见的工具类!
文章目录
1. Callable 接口
1.1 Callable 接口是什么
Callable 的用法非常类似于 Runnable 接口用法,Runnable 是描述了一个任务,一个线程需要做什么,Runnable 通过 run 方法描述,返回类型为 void,但是在实际情况中,很多时候希望任务是要有返回值,即有一个具体的结果产出
Callable 接口是一个函数式接口, 只有一个方法 call()
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
其中,类型参数 V 是 call() 方法返回值的类型,如 Callable 这样写,表示的是一个最终返回 Integer 对象
下面具体通过一个例子理解
1.2 Callable 的用法
描述:使用 Callable 创建线程,用这个线程计算 1+2+3+…+1000
1)创建一个匿名内部类,并实现 Callable 接口,Callable 是带有泛型参数的,其泛型参数就表示返回值的类型,这个案例,泛型参数使用 Integer,重写 Callable 的 call() 方法,完成 1 到 1000 的累加过程,并返回其结果,如下:
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;
}
};
因为 Callable 是一个函数式接口,也也可以使用 lambda 表达式的方式来定义,如下:
Callable<Integer> callable = ()->{
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
};
2) 创建好 callable 任务后,需要一个线程来启动(注意!!! 这里并不是在构造方法中直接将 callable 传入),这里需要先通过 FutureTask 包装一下,再将 FutureTask 传入 Thread 的构造方法中
创建线程 t,在线程 t 的构造方法中传入 FutureTask,此时 t 就会执行 FutureTask 内部的 Callable 的 call() 方法,完成计算,最终的计算结果会存在 futureTask 中
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
FutureTask
执行 Callable 的一种方法是使用 FutureTask,它实现了 Future 和 Runnable 接口,因此,可以构造一个线程来运行这个任务,在 Thread 构造方法里传入 FutureTask 对象,此时线程会执行 FutureTask 内部的 Callable 的 call() 方法,完成计算,最终计算的结果就会存在 futureTask 里,通过 get() 获取到值
Future
Future 接口代表一个异步计算的结果,可以在后台线程中进行计算,而不会阻塞当前线程,其中的 get() 方法可以获取这个结果,并且它的调用会阻塞,直到计算完成,类似于 join() 方法的效果,如果运行该计算的线程被中断, get() 方法将抛出 InterruptedExpection,如果计算已完成,可以通过 get 方法立即返回结果
// 计算结果
futureTask.get()
举一个栗子来理解 FutureTask:
小万去吃饭,排队买饭,拿到了小票,当餐点好了后,后厨就开始做这个饭了,同时前台也会给小万一张小票,做好了凭这个小票去领取,这个小票就相当于是这里的 FutureTask,这也意味着后面如果小万想知道自己的饭做好没,凭着这个小票去查看是否做好(不得不说,一切都源于生活呀~)
下面用图解说明:
完整代码如下:
public class Demo3 {
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 t = new Thread(futureTask);
t.start();
System.out.println(futureTask.get());
}
}
运行结果如下:
1.3 Runnable 和 Callable 接口的区别
Runnable 核心代码:
public interface Runnable {
void run();
}
Callable 核心代码:
public interface Callable {
V call() throws Exception;
}
- 返回值:Runnable 没有返回值, Callable 有返回值,其返回值通过 FutureTask 对象获取
- 异常处理:Runnable 的 run() 方法不能抛出检查异常,而 Callable 的 call() 方法可以抛出异常,因此,Callable 允许异常向上抛,可以在外部捕获异常
✨ 创建线程的方式
可以看到,上述又有一种创建线程的方式:Callable 的方式
具体创建线程的方式,可回顾前期内容:☔️ 创建线程到底是多少种方法?
2. ReentrantLock 类
2.1 ReentrantLock 是什么
synchronized 是关键字,基于代码块的方式来控制加锁解锁的,ReentrantLock 则是提供了 lock 和 unlock 独立的方法,来进行加锁和解锁。
有些小伙伴就有疑问了,手动 lock 和 unlock 很容易造成加了锁但忘记 unlock ,忘记释放锁的情况,这个角度来看, ReentrantLock 似乎不实用呀~ 其实不是,虽然在大部分情况下,使用 synchronized 就足够使用了,但是 ReentrantLock 也是一个重要的补充!
可以从三个方面来看:
- synchronized 只是加锁和解锁,加锁的时候,如果发现锁被占用了,只能阻塞等待,而 ReentrantLock 还提供一个 tryLock 方法,如果加锁成功,没啥特殊的,如果加锁失败,不会阻塞,直接返回 false,这会让程序猿更加灵活决定接下来应该怎么做~
- synchronized 是一个非公平锁(简单回顾以下,非公平锁就是概率相等,不遵循先来后到), ReentrantLock 提供了公平和非公平两种工作模式,默认是非公平锁,如果在构造方法中传入 true 就是开启了公平锁
- synchronized 搭配 wait 和 notify 使用,进行线程等待,如果多个线程 wait 同一个对象,notify 是随机唤醒一个,ReentrantLock 则是搭配 Condition 这个类,这个类也能起到等待通知的作用,可以功能更加强大
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
2.2 ReentrantLock 的用法
- lock():加锁,如果获取不到锁,就死等
- trylock(超时时间):加锁,如果获取不到锁,等待一定时间之后就放弃加锁,返回 false
- unlock():解锁
- lockInterruptibly():能够中断等待锁的线程机制
public class Demo1 {
public static void main(String[] args) {
// ReentrantLock 加锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
//业务代码
} finally {
lock.unlock();
}
}
}
2.3 ReentrantLock 的实现原理
ReentrantLock 是 Lock 接口的一个实现,它提供了与 synchronized 关键字类似的锁功能,是 synchronized 关键字的补充, 它更加灵活~
下面通过代码具体理解:
class Demo2 {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 释放锁
}
}
public int getCount() {
return count;
}
}
- increment 方法先上锁,尝试增加 count 的值,在完成操作后释放锁,保证 count++ 的操作是线程安全的
- ReentrantLock 是可重入的独占锁,可重入表示当前线程获取锁后再次获取不会被阻塞,独占表示只能有一个线程获取该锁,其它想获取该锁的线程会被阻塞
- ReentrantLock 内部会通过一个计数器来跟踪锁的持有次数,当线程调用 lock() 方法获取锁的时候,锁没有被其它线程占用,则获取到锁,如果被其它线程持有,当前线程根据锁的公平性( ReentrantLock可以是公平锁也可以是非公平锁),可能会被加入等待队列中
- 线程首次获取锁,计数器值变为 1,如果同一线程再次获取锁,计数器增加+1,每释放一次锁,计数器-1
- 当线程调用 unlock() 方法的时候,ReentrantLock 会将持有的锁计数-1,当计数达到 0 ,则释放锁,并唤醒等待队列中的线程来竞争锁
具体流程图如下:
2.4 ReentrantLock 如何实现公平锁/非公平锁
1)ReentrantLock 默认构造方法是非公平锁 NonfairSync
2)ReentrantLock 在构造方法中传入true创建 公平锁 FairSync
ReentrantLock lock = new ReentrantLock(true);
// true:公平锁,false:非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
(NonfairSync 和 FairSync 两个类都是继承Sync,Sync 也是继承自 AQS)
补充:AQS 是什么?
AQS定义:是一套多线程访问共享资源的同步器框架,是 Java 并发包 java.util.concurrent的核心框架类,如 ReentrantLock、Semaphore、CountDownLatch 等同步类都依赖它。AQS 通过一个 int 类型的 volatile 变量来表示同步状态,并通过内置的 FIFO 队列来实现线程的阻塞和唤醒,这个 FIFO 队列就是 CLH 队列的变体,它是一个虚拟的双向队列,不存在队列实例,仅存在结点之间的关联关系
AQS思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态;如果被请求的共享资源被占用,则需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制就是基于 CLH 队列来实现的,它将暂时获取不到锁的线程封装成一个节点加入到队列中,等待锁的分配。
2.5 synchronized 和 ReentrantLock 的区别
synchronized 和 ReentrantLock 之间的联系是,它们都可以用来实现同步~
- 类型:synchronized 是一个关键字,是 JVM 内部实现的,大概率是基于 C++ 实现,而 ReentrantLock 是标准库的一个类,是在 JVM 外实现的,是 Lock 接口的一个实现,基于 Java 实现的;
- 使用方式:synchronized 可以直接在方法上加锁,也可以在代码块上加锁,而 ReentrantLock 必须手动声明来加锁和释放锁
- 是否需要手动释放锁:synchronized 使用时不需要手动释放锁,ReentrantLock 使用时需要手动释放,使用起来更灵活,但是也容易忘记写 unlock,导致忘记释放锁;
- 申请锁后的处理方式:synchronized 在申请锁失败时会死等,ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃
- 锁类型:synchronized 是非公平锁, ReentrantLock 默认是非公平锁,但可以通过构造方法传入一个 true 开启公平锁模式
2.6 并发量大的情况下,使用 synchronized 还是 ReentrantLock?
在并发量特别高的情况下,ReentrantLock 的性能可能要优于 synchronized
上述总结了 synchronized 和 ReentrantLock 的区别,基于以上特性,可以分析一下原因:
- ReentrantLock 提供了超时和公平锁等特性,更加灵活,更好应对复杂的并发场景
- ReentrantLock 允许更细粒度的锁控制,可以有效减少锁竞争,减少开销
- ReentrantLock 支持条件变量 Condition,可以实现比 synchronized 更加复杂的线程间通信机制
💛💛💛本期内容回顾💛💛💛
✨✨✨本期内容到此结束啦~
原文地址:https://blog.csdn.net/m0_61814277/article/details/143447879
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!