7.Callable接口
创建线程的四种方式:
- 继承Thread
- 实现Runnable
- 实现Callable
- 线程池方式
Runnable 缺少的一项功能是,当线程 终止时(run完成时),我们无法使线程返回结果。为了支持此功能, Java 中提供了 Callable 接口。
Callable 接口的特点如下(重点)
- 为了实现 Runnable,需要实现不返回任何内容的 run()方法,而对于 Callable,需要实现在完成时返回结果的 call()方法。
- call()方法可以引发异常,而 run()则不能。
- 为实现 Callable 而必须重写 call 方法
- 不能直接替换 runnable,因为 Thread 类的构造方法根本没有 Callable
Runnable和Callable区别:
- 返回值
- 是否抛异常
- 实现方法名:run/call
7.1 Callable的方法
-
**public boolean cancel(boolean mayInterrupt)😗*用于停止任务。 ==如果尚未启动,它将停止任务。如果已启动,则仅在 mayInterrupt 为 true
时才会中断任务。==
-
public Object get()抛出 InterruptedException,ExecutionException: 用于获取任务的结果。
==如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果。 ==
-
public boolean isDone():如果任务完成,则返回 true,否则返回 false
可以看到 Callable 和 Future 做两件事-Callable 与 Runnable 类似,因为它封 装了要在另一个线程上运行的任务,而 Future 用于存储从另一个线程获得的结 果。实际上,future 也可以与 Runnable 一起使用。
要创建线程,需要 Runnable。为了获得结果,需要 future。
7.2 FutureTask
核心原理:
- 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些 作业交给 Future 对象在后台完成
- 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态
- 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去 获取结果。
- 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法
- 一旦计算完成,就不能再重新开始或取消计算
- get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常
- get 只计算一次,因此 get 方法放到最后
Thread并不能直接使用Callable进行构造:
所以这里使用FutureTask:他既实现了Runnable,又构造包含了Callable。
class MyThread3 implements Callable {
@Override
public Object call() throws Exception {
return 1024;
}
}
// FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread3());
FutureTask<Integer> futureTask2 = new FutureTask<>(()->1024);
FutureTask原理:未来任务
开启单线程去完成一个耗时长的支线任务,不影响主线任务。最后将这些任务都汇总起来,只汇总一次。
FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName() +" come in callable");
return 1024;
});
new Thread(futureTask2,"futureTask2").start();
while (!futureTask2.isDone()){
System.out.println(Thread.currentThread().getName()+" wait!");
}
System.out.println(futureTask2.get());
System.out.println(Thread.currentThread().getName()+" over!");
7.3 小结(重点)
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些 作业交给 Future 对象在后台完成, 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态
8 JUC辅助类
JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过 多时 Lock 锁的频繁操作。这三种辅助类为:
- CountDownLatch: 减少计数
- CyclicBarrier: 循环栅栏
- Semaphore: 信号灯
8.1 减少计数CountDownLatch
- CountDownlatch类:1构造2方法
- 构造方法 : CountDownLatch (int count):设置初始值
- 等待方法:await( ):当初始值为0时,可以执行一段逻辑,当初始值不为零是
- 计数方法:countDown():可以让初始值每次减1
- CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行 减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法 之后的语句。
- CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这 些线程会阻塞
- 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程 不会阻塞)
- 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行
- 场景: 6 个同学陆续离开教室后值班同学才可以关门。
//演示 CountDownLatch
public class CountDownLatchDemo {
//6个同学陆续离开教室之后,班长锁门
public static void main(String[] args) throws InterruptedException {
//创建CountDownLatch对象,设置初始值
CountDownLatch countDownLatch = new CountDownLatch(6);
//6个同学陆续离开教室之后
for(int i=1;i<=6;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号同学离开");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
//等待
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
}
}
8.2 循环栅栏CyclicBarrier
- 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的
程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环的 barrier。 - CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中 CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一 次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后 的语句。可以将 CyclicBarrier 理解为加 1 操作
- 方法:
- 构造方法:CyclicBarrier (int parties) / CyclicBarrier (int parties, Runnable barrierAction)
- Await():
- getNumberWaiting:
- getParties:
- isBroken:
- Reset:
- 场景: 集齐 7 颗龙珠就可以召唤神龙
//集齐7颗龙珠就可以召唤神龙
public class CyclicBarrierDemo {
//创建固定值
private static final int NUMBER = 7;
public static void main(String[] args) {
//创建CyclicBarrier
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER,()->{
System.out.println("收集7颗龙珠就可以召唤神龙");
});
//集齐七颗龙珠过程
for(int i=1;i<=7;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 星龙珠被收集");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
注意:如果循环的次数不是7,大于或小于7,在打印完之后,线程不会停止。
8.3 信号灯Semaphore
- 一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。
每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。 - Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线 程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方 法获得许可证,release 方法释放许可。
public class SemaphoreDemo {
public static void main(String[] args) {
//创建Semaphore,设置许可数量
Semaphore semaphore = new Semaphore(3);
//模拟6辆汽车
for (int i = 1; i <=6; i++) {
new Thread(()->{
try {
//抢占
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+" 抢到了车位");
//设置随机停车时间
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName()+" ------离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
// 执行结果:
1 抢到了车位
2 抢到了车位
3 抢到了车位
1 ------离开了车位
4 抢到了车位
4 ------离开了车位
5 抢到了车位
2 ------离开了车位
6 抢到了车位
5 ------离开了车位
3 ------离开了车位
6 ------离开了车位