JUC是
java.util.concurrent
的简称,当中有我们需要熟知的常见类,此文为你带来相关知识
前面我们学习了3种创建线程的方法,分别是:
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法
- 创建线程池,利用线程池创建线程
JUC中为我们提供了一个新的创建线程的方法
1. Callable接口
Callable 是⼀个interface.相当于把线程封装了⼀个"返回值".方便程序猿借助多线程的方式计算结果.
那么既然已经有了Runnable接口可以创建线程任务,为何还要Callable接口呢??
我们从一个案例来探究一下吧!
示例: 创建线程计算1+2+3+…+100,不使用Callable版本
- 创建⼀个类Result,包含⼀个sum表示最终结果,lock表示线程同步使用的锁对象.
- main方法中先创建Result实例,然后创建⼀个线程t.在线程内部计算1+2+3+…+1000.
- 主线程同时使用wait等待线程t计算结束.(注意,如果执行到wait之前,线程t已经计算完了,就不
必等待了). - 当线程t计算完毕后,通过notify唤醒主线程,主线程再打印结果.
java">class Result{//累加和int sum=0;// 锁对象public Object lock = new Object();
}
public class TestRunnable {public static void main(String[] args) {Result result=new Result();Thread t=new Thread(){@Overridepublic void run() {int sum = 0;for (int i = 1; i <= 100; i++) {// 执行累加操作sum += i;}// 为结果赋值result.sum = sum;// 唤醒等待的线程synchronized (result.lock) {// 检查累加是否执行完成while (result.sum == 0) {// 没有累加完成,等待结果try {result.lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 打印结果System.out.println(result.sum);result.lock.notify();}}};t.start();}
}
示例: 创建线程计算1+2+3+…+100,使用Callable版本
- 创建⼀个匿名内部类,实现Callable接⼝.Callable带有泛型参数.泛型参数表示返回值的类型.
- 重写Callable的call方法,完成累加的过程.直接通过返回值返回计算结果.
- 把callable实例使用FutureTask包装⼀下.
- 创建线程,线程的构造方法传入FutureTask.此时新线程就会执行FutureTask内部的Callable的call
方法,完成计算.计算结果就放到了FutureTask对象中. - 在主线程中调用
futureTask.get()
能够阻塞等待新线程计算完毕.并获取到FutureTask中的结果.
java">import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class TestCallable {public static void main(String[] args) throws Exception {//实现Callable接口,重写call()方法Callable <Integer> callable= new Callable<Integer>() {@Overridepublic Integer call() throws Exception {// 执行累加操作int sum=0;for (int i = 0; i <=100; i++) {sum=sum+i;}return sum;}};//Callable要配合FutureTask一起使用,FutureTask用来获取Callable的执行结果FutureTask<Integer> futureTask=new FutureTask<>(callable);// FutureTask当做构造参数传入到Thread构造方法中Thread thread=new Thread(futureTask);thread.start();try {// 等待结果, 的时间可能被中断,会抛出InterruptedExceptionInteger result = futureTask.get();// 打印结果System.out.println("执行结果是:" + result);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();// 打印异常信息System.out.println("打印日志:" + e.getMessage());}}
}
我们发现Callable接口可以返回一个值,并让FutureTask等待结果,不需要借助辅助类,并且不需要使用锁,减少了CPU的消耗
2. Callable和Runnable的差异
我们通过源码来探究其差异:
总结
- Callable要实现call(),且有返回值,Runnable要实现的run()但没有返回值
- Callable的call()可以抛出异常,Runnable的run()不能抛出异常
- Callable配合FutrueTask一起使用,通过futureTask,get()方法获取call()的结果
- 两都是描述线程任务的接口
3. ReentrantLock
可重入互斥锁.和synchronized定位类似,都是用来实现互斥效果,保证线程安全.
ReentrantLock 也是可重⼊锁. Reentrant这个单词的原意就是"可重入"
3.1 ReentrantLock的用法
- lock(): 加锁,如果获取不到锁就死等.
- trylock(超时时间):加锁,如果获取不到锁,等待⼀定的时间之后就放弃加锁.
- unlock(): 解锁
源码:
java">lock.lock();
try {
// 业务逻辑代码
} finally {
// 如果业务代码执行到一半抛出了异常,那么会导致释放锁的代码无法执行,所以使用finally
lock.unlock()
}
3.2 ReentrantLock 和synchronized 的区别
- synchronized是⼀个关键字,是JVM内部实现的(大概率是基于C++实现).ReentrantLock是标准
库的⼀个类,在JVM外实现的(基于Java实现). - synchronized使用时不需要⼿动释放锁.ReentrantLock使用时需要⼿动释放.使用起来更灵活,但
是也容易遗漏unlock. - synchronized在申请锁失败时,会死等.ReentrantLock可以通过trylock的方式等待⼀段时间就放弃.
- synchronized是非公平锁,ReentrantLock默认是非公平锁.可以通过构造方法传入⼀个true开启公平锁模式.实现的过程中有会一个队列来组织排队的线程
- 更强大的唤醒机制.synchronized是通过Object的wait/notify实现等待-唤醒.每次唤醒的是⼀个随机等待的线程.ReentrantLock搭配
Condition类
实现等待-唤醒,可以更精确控制唤醒某个指定的线程.
java">ReentrantLock lock=new ReentrantLock(true);
// 条件1
Condition male= lock.newCondition();
// 条件2
Condition female= lock.newCondition();
// 根据不同的条件进行阻塞等待
male.await();
male.signal(); // 唤醒相应队列中的一个线程
male.signalAll(); // 唤醒相应队列中的所有线程// 根据不同的条件进行阻塞等待
female.await();
female.signal(); // 唤醒相应队列中的一个线程
female.signalAll(); // 唤醒相应队列中的所有线程
- 读写锁
java">//创建一个读写锁
ReentrantReadWriteLock readwriteLock=new ReentrantReadWriteLock();
//获取读锁
ReentrantReadWriteLock.ReadLock readLock=readwriteLock.readLock();
//获取写锁
ReentrantReadWriteLock.ReadLock writeLock=readwriteLock.readLock();
3.3 如何选择使用哪个锁?
- 锁竞争不激烈的时候,使用synchronized,效率更高,自动释放更方便.
- 锁竞争激烈的时候,使用ReentrantLock,搭配trylock更灵活控制加锁的行为,而不是死等.
- 如果需要使用公平锁,使用ReentrantLock.
4. 信号量Semaphore
信号量:用来表示"可用资源的个数".本质上就是⼀个计数器.
理解信号量
可以把信号量想象成是饭店的座位牌:当前有座位5个.表示有5个可用资源.
当有人进去的时候,就相当于申请⼀个可用资源,可⽤座位就-1(这个称为信号量的P操作)
当有人出来的时候,就相当于释放⼀个可用资源,可⽤座位就+1(这个称为信号量的V操作)
如果计数器的值已经为0了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源.
Semaphore的PV操作中的加减计数器操作都是原⼦的,可以在多线程环境下直接使用.
示例: 模拟吃饭的场景
java">// 初始化一个信号量的对象, 指定系统可用资源的数量, 相当于一个饭店有5个餐位Semaphore semaphore=new Semaphore(5);Runnable runnable=new Thread(){@Overridepublic void run() {//1.尝试申请资源,寻找座位吃饭System.out.println(Thread.currentThread().getName()+"尝试申请资源(寻找座位吃饭)");try {semaphore.acquire();} catch (InterruptedException e) {e.printStackTrace();}//2.获取到了资源System.out.println(Thread.currentThread().getName()+"获取到了资源(拥有了座位)");//3.利用资源处理业务try {//休眠一会,模拟业务处理消耗时间System.out.println(Thread.currentThread().getName()+"吃饭ing......");TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//4.释放资源semaphore.release();System.out.println(Thread.currentThread().getName()+"释放了资源(吃完饭走人)");}};//创建20个线程,模拟20个人吃饭场景for (int i = 0; i < 20; i++) {//创建线程并指定任务Thread thread=new Thread(runnable);//启动线程thread.start();}}
说明:
- acquire用于申请资源
- release用于释放资源
5. CountDownLatch
同时等待N个任务执行结束.
好像跑步⽐赛,10个选⼿依次就位,哨声响才同时出发;所有选⼿都通过终点,才能公布成绩。
示例: 模拟跑步比赛的场景
java">//指定参赛选手的个数(线程数)CountDownLatch countDownLatch=new CountDownLatch(10);System.out.println("各就各位,预备...");for (int i = 0; i < 10; i++) {Thread player = new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + "开跑.");// 模拟比赛过程, 休眠2秒TimeUnit.SECONDS.sleep(2);System.out.println(Thread.currentThread().getName() + "到达.");// 标记选手已达到终点,让countDownLatch的计数减1, 当计数到0时,表示所有的选手都到达终点,比赛结束countDownLatch.countDown();} catch (InterruptedException e) {e.printStackTrace();}}, "player" + i);// 启动线程player.start();}TimeUnit.MILLISECONDS.sleep(10);System.out.println("===== 比赛进行中 =====");// 等待比赛结束countDownLatch.await();// 颁奖System.out.println("比赛结束, 进行颁奖");
图解:
说明:
- countDownLatch的计数减1,当计数到0时,表示所有的选手都到达终点,比赛结束
- await负责等待计算器为0
应用场景:
一个大的文件,可以分割成好多块,一个线程去下载一个小块,当所有的线程都执行完下载任务,再把各个小块的内容拼成一个完整的文件 (迅雷)