多线程进阶——JUC的常见类
目录
一、Callable接口
(一)Callable带有泛型参数
(二)Callable中有带返回值的call()方法
(三)Callable实例要用FutureTask包装一下,再在线程构造方法中传入FutureTask。
(四)代码示例
二、ReentrantLock
synchronized和ReentrantLock之间的区别
三、信号量Semaphore
四、CountDownLatch
一、Callable接口
Callable是一个interfa,相当于把线程封装了一个“返回值”,方便程序员借助多线程的方式计算结果。
Callable 和 Runnable 相对,都是描述一个 "任务"。Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务。
Callable 通常需要搭配 FutureTask 来使用。FutureTask 用来保存 Callable 的返回结果。因为 Callable 往往是在另一个线程中执行的,啥时候执行完并不确定。
FutureTask 就可以负责这个等待结果出来的工作。
(一)Callable带有泛型参数
Callable带有泛型参数,泛型参数表示返回值的类型。
Callable<Integer> callable = new Callable<Integer>(){}
(二)Callable中有带返回值的call()方法
call()方法类似于Runnable中的void run()方法,有返回值。
public Integer call() throws Exception {}
(三)Callable实例要用FutureTask包装一下,再在线程构造方法中传入FutureTask。
想象去吃麻辣烫。当餐点好后,后厨就开始做了。同时前台会给你一张 "小票"。这个小票就是 FutureTask。后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没。
FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);
(四)代码示例
代码示例:创建线程计算1+2+3+...+1000,使用 Callable 版本
- 创建一个匿名内部类,实现 Callable 接口。Callable 带有泛型参数、泛型参数表示返回值的类型。
- 重写 Callable 的 call 方法,完成累加的过程。直接通过返回值返回计算结果。
- 把 callable 实例使用 FutureTask 包装一下。
- 创建线程,线程的构造方法传入 FutureTask。此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法,完成计算。计算结果就放到了 FutureTask 对象中。
- 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕。并获取到 FutureTask 中的结果。
代码:
public class Demo30 {public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable=new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum=0;for(int i=1;i<1001;i++){sum+=i;}return sum;}};FutureTask<Integer> futureTask=new FutureTask<>(callable);Thread t=new Thread(futureTask);t.start();int result=futureTask.get();System.out.println("sum:"+result);}
}
如果不使用Callable和FutureTask,则要考虑线程同步的问题,要等执行运算的线程结束后,再执行main线程里的输出结果。
二、ReentrantLock
可重入互斥锁
和 synchronized 定位类似,都是用来实现互斥效果,保证线程安全。
ReentrantLock 也是可重入锁。“Reentrant”这个单词的原意就是“可重入”。
ReentrantLock 的用法:
lock(): 加锁,如果获取不到锁就死等。
trylock(超时时间): 加锁,如果获取不到锁,等待一定的时间之后就放弃加锁。
unlock(): 解锁
ReentrantLock lock = new ReentrantLock();lock.lock();try {// working} finally {lock.unlock()}
synchronized和ReentrantLock之间的区别
- synchronized是关键字(内部实现是JVM内部通过C++实现的),ReentrantLock是标准库的类。
- synchronized通过代码块控制加锁解锁,ReentrantLock需要lock/unlock方法,但是要注意不要漏写unlock。
- ReentrantLock除了提供lock/unlock方法,还提供了trylock方法,trylock方法不会造成阻塞,加锁成功返回true,失败返回flase,调用者判定返回值,决定接下来如何做。
- ReentrantLock提供了公平锁的实现。
- ReentrantLock搭配的等待通知机制,是Condition类,相比于wait/notify来说功能更强大一些。
三、信号量Semaphore
信号量,用来表示"可用资源的个数"。本质上就是一个计数器。
创建初始值为1的信号量:
Semaphore semaphore = new Semaphore(1);
申请资源:
semaphore.acquire();
释放资源:
semaphore.release();
理解信号量
可以把信号量想象成是停车场的展示牌:当前有车位 100 个。表示有 100 个可用资源。
当有车开进去的时候,就相当于申请一个可用资源,可用车位就 -1(这个称为信号量的 P 操作)
当有车开出来的时候,就相当于释放一个可用资源,可用车位就 +1(这个称为信号量的 V 操作)
如果计数器的值已经为 0 了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源。
Semaphore 的 PV 操作中的加减计数器操作都是原子的,可以在多线程环境下直接使用。
信号量的一个特殊情况:初始值为1的信号量,取值要么是1要么是0(二元信号量)
等价于“锁”(普通的信号量,就相当于锁的更广泛推广),如果是普通的N的信号量,就可以限制同时有多少个线程来执行某个逻辑。
四、CountDownLatch
使用多线程,经常把一个大的任务拆分成多个子任务,使用多线程执行这些子任务,从而提高程序的效率。
如何衡量这些子任务都完成了呢?整个任务都完成了呢?
CountDownLatch表示同时等待 N 个任务执行结束。
好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。
构造 CountDownLatch 实例latch,初始化 10 表示有 10 个任务需要完成。
每个任务执行完毕,都调用 latch.countDown()。在 CountDownLatch 内部的计数器同时自减。
主程序中使用 latch.await() 阻塞等待所有任务执行完毕。相当于计数器为 0 了。
代码示例:
public class Demo31 {public static void main(String[] args) throws InterruptedException {Random random=new Random(10);CountDownLatch count=new CountDownLatch(10);Runnable runnable=new Runnable() {@Overridepublic void run() {int sum= random.nextInt();System.out.println(sum);count.countDown();}};//创建10个线程执行runnablefor(int i=0;i<10;i++){new Thread(runnable).start();}count.await();System.out.println("执行结束");}
}