JUC并发编程
一、基础概念知识
1.什么是JUC?
JUC是java中java.util.concurrent这个包的缩写,JUC包可以用来更好的支持高并发任务。
2.进程与线程(可以看os的笔记)
3.异步与同步
从调用方角度来讲,如果:
并发是指同一时间段内多个线程同时访问同一个资源,如春运抢票、电商秒杀。
- A需要等待B的结果返回,才能继续运行,那么A、B就是同步的,不是说AB同步执行的意思!!!;
- A不需要等待B的结果返回,就能继续运行就是A、B就是异步的。
并发是指同一时间段内多个线程同时访问同一个资源,如春运抢票、电商秒杀。
并行是指同一时刻多个线程同时工作。
【tips】并发宏观上是多个线程同时执行,而微观上这多个线程还是顺序执行的;并行是微观宏观都是同时执行的。
二、Java线程
1.创建和运行线程
(1)方法一:通过创建Thread的子类
1)创建继承类,重写run方法
2)创建继承类对象,运行线程
结果:
(2)方法一的优化:使用匿名内部类
我们知道匿名内部类的本质就是继承了或实现了某个类的子类对象。所以使用匿名内部类来创建新线程,可以省去创建继承类这一步。
(3)方法二:Runnable接口+Thread
通过Thread的源码可以看到,还由一个构造方法可以传入一个Runnable来创建Thread。这种方法可以将线程和要执行的任务分开。将要执行的任务写在Runnable中,再用Runnable创建Thread。
注意Runnable也是个接口,所以需要用他的实现类,因此也推荐用匿名内部类来创建Runnable的实现类对象。
(4)lambda简化
只包含一个抽象方法的接口叫函数式接口(FunctionalInterface)。
对于函数式接口的匿名内部类可以用lambda表达式来简化。
(5)使用Runnable的好处
- 方法一由于继承了Thread,就不能再继承别的类了;而通过Runnable接口的实现类实现多线程,可以继承别的类,避免了Java只能单继承的局限性;
- 用 Runnable 更容易与线程池等高级 API 配合。
(6)方法三:FutureTask接口+Thread
-
FutureTask是Runnable的实现类,它能够接收 Callable 类型的参数,可以用来处理有返回结果的情况。
-
FutureTask的原理
主线程执行过程中可以单开一个子线程去完成某个任务,完成后子线程返回任务结果给主线程,主线程再继续执行。
(7)Callable与Runnable
Callable接口类似于Runnable。
- 是否有返回值:Callable可以用来处理有返回结果的情况,而Runnable不会返回结果;
- 是否会抛出异常:Callable如果无法计算结果,会抛出异常,而Runnable无法抛出经过检查的异常;
- 最终实现的方法不同:Callable通过call()实现,Runnable通过run()实现。
(8)方法四:线程池——见 九、
2.查看进程和线程的方法
(1)Windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
-
在cmd中执行:
-
tasklist:查看所有进程
-
taskkill:根据PID杀死进程
-
tasklist:查看所有进程
(2)Linux
- ps -ef:查看所有进程
- ps -ef | grep 关键字:查找指定关键字的进程
- ps -fT -p <PID>:查看某个进程(PID)的所有线程
- kill PID:杀死进程
-
top:动态查看所有进程信息,按大写 H 切换是否显示线程
- top -H -p <PID>:动态查看某个进程(PID)的所有线程
(3)Java
- jps:查看所有 Java 进程
- jstack <PID>:查看某个 Java 进程(PID)的所有线程状态
-
jconsole:来查看某个 Java 进程中线程的运行情况(图形界面)
3.线程的运行原理
(1)栈与栈帧
栈指的是Java Virtual Machine Stack(Java 虚拟机栈),也就是我们说的JVM栈。JVM 中由堆、栈、方法区组成,其中的栈内存是给线程用的,每个线程启动后,虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
【举例】以debug方式执行主方法,一运行时先进行类加载,jvm将字节码文件放入方法区,并为主方法分配一个栈区,相应地为main方法产生一个栈帧,同理运行到method1和2时,也会产生响应的栈帧(如下图)。
(2)线程的上下文切换
线程的上下文切换指的是从一个线程切换到另一个线程。
-
发生线程上下文切换的情况:
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
-
线程上下文切换时的开销:
- 当线程发生上下文切换时,需要由操作系统保存当前线程的状态,包括程序计数器(下一条要执行的指令)、栈帧信息(如局部变量、操作数栈、返回地址等),并恢复另一个线程的状态。
- 因此频繁发生线程切换会影响系统性能。
4.线程API
(1)常用方法
方法 | 说明 |
start() |
启动一个新线程。
start 方法只是让线程进入就绪状态,是否运行还要等CPU调度。
每个线程对象的 start方法只能调用一次。
|
run() |
线程运行后会调用run方法。
|
join()
join(long time)
|
等待线程运行结束。
哪个线程调用了join()就是等待哪个线程运行结束。
也可以设置一个等待时间time,等待 time 毫秒后没等到就不等了。
|
static sleep(long time) |
让当前执行的线程让出 cpu 的时间片给其它线程,休眠 time 毫秒。
sleep后进入TIMED_WAITING状态。
|
static yield() |
告诉线程调度器,让出当前线程对 CPU的使用。
主要是为了测试和调试。
|
getId() |
获取线程长整型的 id。 |
getName()
|
获取线程名
|
setName(String name) |
修改线程名 |
getPriority() |
获取线程优先级。 |
setPriority(int priority) |
修改线程优先级。
java中规定线程优先级是 1~10 的整数,较大的优先级能提高该线程被 CPU 调度的几率,不是说优先级高的一定会被先调用。
默认是5。
|
getState() |
获取线程状态 Java 中线程状态是用 6 个 enum 表示的,分别为:
|
interrupt() |
打断线程。
如果被打断的线程正在 sleep,wait,join ,会导致被打断的线程抛出 InterruptedException 异常,因此会清除打断标记(即通过isInterrupted方法判断会显示打断标记为false);
如果打断正在运行的线程,则会设置打断标记,但是正在运行的线程不会停下来,他可以通过判断自己有没有被设置打断标记来决定要不要停止运行。
park 的线程被打断,也会设置打断标记。
|
static interrupted() |
判断当前线程是否被打断。 获取打断标记并清除打断标记。 |
isInterrupted() |
判断当前线程是否被打断。 获取打断标记但是不会清除打断标记。 |
isAlive() |
判断线程是否存活(是否运行完毕)。 |
static currentThread() |
获取当前正在执行的线程。 |
(2)start与run的比较
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程;
- 使用 start 是启动新的线程,通过新线程间接执行 run 中的代码。
(3)join的应用
-
对于下面这段代码,打印的 r 会是什么?
——刚开始我以为会是10,但是错了:根据控制台的结果来看还是0。这是因为主线程main和 t1 是并发运行的,main不需要等待 t1 把10赋值给 r 就已经把 r=0 打印出来了。
-
如果我就想看到打印出来的 r 是10,就需要让main等待t1执行完后再打印,这里就要用到 join() 了:在main中用t1.join(),表示main要等待 t1 运行结束。
(4)sleep的应用——防止CPU占用100%
在项目上线后,可能需要一直接收请求。这时就会用到while(true)死循环,而在单核CPU下,这种死循环会导致CPU占用率达到100%,我们该如何避免这种情况呢?
-
sleep()或yield()——让线程短暂地让出cpu:
sleep一般适用于不需要加锁实现同步的场景。
- 使用wait()或条件变量也可以实现类似的功能,但是需要加锁,并配合相应的唤醒操作,一般适用于需要进行同步的场景。
(5)interrupt的应用——两阶段终止模式
我们如果想终止一个线程,不应该直接stop(),也不能直接System.exit()。前者可能导致线程持有的共享资源无法释放,导致死锁;后者是直接将整个程序退出了。
我们希望终止一个线程时可以让该线程先“料理后事”,比如上面的释放共享资源,然后再终止。这就可以用到interrupt()了。
【场景模拟】实现一个监控系统健康状态的后台线程,它可以每隔两秒对系统的健康状态进行记录。当然我们不想监控了,也可以来终止它。
-
两阶段终止模式的分析
(6)interrupt——打断park线程
park线程是通过调用LockSupport中的park()方法使线程停下来,interrupt也可以打断处在park状态中的线程,让线程继续执行下去。
park()有个特点是,如果线程的打断标记为true,那么park将不起作用,因此要想使用park()要想将打断标记清除(置为false)。
5.主线程与子线程、用户线程与守护线程
(1)概念
对于一个多线程进程,我们可以简单地分成主线程和子线程。每个进程只有一个主线程(main方法)。
子线程中可以简单地分为守护线程和用户线程(非守护线程)。
(2)各种线程的作用
-
主线程:当 Java 程序启动时,一个线程会立刻运行,该线程通常叫做程序的主线程(main thread), 即 main 方法对应的线程,它是程序开始时就执行的。
- 主线程是产生其他子线程的线程。
- 主线程不一定是最后完成执行的线程,子线程可能在它结束之后还在运行。
-
子线程:在主线程中创建并启动的线程,一般称之为子线程。
- 用户线程:用户线程通常用来异步处理一些业务或逻辑。主线程结束,用户线程可以继续运行。
- 守护线程:为主线程提供一些通用服务,比如垃圾回收。主线程结束,守护线程没有了守护的对象,所以守护线程也就结束了。
三、Lock接口
1.多线程编程的一般步骤
(1)创建一个共享资源类;
(2)创建多个线程来使用资源。
2.Lock接口的实现类
(1)ReentrantLock(可重入锁)
可重入锁指的是某个线程已经获得了某个锁,可以再次获取锁。因此可重入锁可以避免出现死锁。
与之对应的叫不可重入锁(自旋锁):当一个线程尝试去获取某一把锁的时候,如果这个锁已经被另外一个线程占有了,那么该线程就无法获取这把锁,该线程会等待,间隔一段时间后再次尝试获取。这种采用循环加锁,等待锁释放的机制就称为自旋锁(spinlock)。
(2)ReentrantReadWriteLock.ReadLock(读写锁——读锁)
(3)ReentrantReadWriteLock.WriteLock(读写锁——写锁)
四、线程间的通信
1.虚假唤醒
(1)什么是虚假唤醒?
多线程环境下,当有多个线程执行了wait()方法,就需要其他线程执行notify()或者notifyAll()方法去唤醒它们。而wait()的特点是在哪睡着(等待)就在哪被唤醒,所以假如多个线程都被唤醒了,但是其实只有其中一部分是有用的唤醒操作,其余被唤醒都是做的无用功。那么对于不应该被唤醒的线程而言,便是虚假唤醒。
(2)解决方案
在while循环中使用wait()方法。
2.线程间的定制化通信
定制化通信就是让多个线程按我们想要的顺序执行。
一般通过加一个标志位flag来实现。
五、集合的线程安全问题
1.ConcurrentModificationException——并发修改异常
当多个线程对集合进行修改(如add新元素)时,可能会造成并发修改异常。
2.解决方案
(1)Vector
Vector与ArrayList唯一的不同就是Vector的方法都用synchronization关键字修饰了,所以vector的方法都是同步的。
【tips】这是在jdk1.0就有的解决集合线程安全问题的方案,现在一般不用他了。
(2)Collections
Collections工具类中提供了返回支持线程同步的集合。
在创建集合对象时用Collections工具类来创建:
//线程不安全的list List<Integer> list=new ArrayList<>(); //支持同步的线程安全list List<Integer> list=Collections.synchronizedList(new ArrayList<>());
【tips】现在也不推荐用了。
(3)★CopyOnWriteArrayList——写时复制
CopyOnWriteArrayList是JUC中提供的一种解决集合线程安全问题的类。他是通过在线程需要修改list时,将原list复制一份,在复制品里写,写完再将原list覆盖。
(4)HashSet和HashMap的线程安全问题及解决方案
- HashSet解决方案——CopyOnWriteArraySet类
-
HashMap解决方案——ConcurrentHashMap类
六、多线程锁
1.公平锁与非公平锁
- 公平锁:雨露均沾。效率相对较低。
- 非公平锁:可能导致事情都让一个线程干了,其他线程被饿死的情况。但是执行效率高。
比如我们在创建一个ReentrantLock时,就可以通过传入参数fair来决定创建的锁是公平锁还是非公平锁。
【TODO源码还得看】
2.可重入锁
可重入锁指的是某个线程已经获得了某个锁,可以再次获取锁。因此可重入锁可以避免出现死锁。
synchronized和Lock接口的锁都是可重入锁。
与之对应的叫不可重入锁(自旋锁):当一个线程尝试去获取某一把锁的时候,如果这个锁已经被另外一个线程占有了,那么该线程就无法获取这把锁,该线程会等待,间隔一段时间后再次尝试获取。这种采用循环加锁,等待锁释放的机制就称为自旋锁(spinlock)。
3.死锁
(1)手写死锁代码
首先创建两个锁对象,然后两个线程各自先获取一个锁,然后互相再获取对方已经获取到的锁,此时就会出现死锁。
public class DeadlockDemo { public static void main(String[] args) { //o1,o2相当于两个资源 Object o1 = new Object(); Object o2 = new Object(); //线程A拥有o2,想要获取o1 Thread thread01 = new Thread(() -> { synchronized (o2) { //获取到了o2 try { //当前线程休眠1秒,保证另外一个线程已经获取到o1 TimeUnit.MILLISECONDS.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //想要获取o1 synchronized (o1) { System.out.println("A-ok"); } } },"A"); //线程B拥有o1,想要获取o2 Thread thread02 = new Thread(() -> { synchronized (o1) { try { TimeUnit.MILLISECONDS.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2) { System.out.println("B-ok"); } } },"B"); thread01.start(); thread02.start(); } }
(2)如何验证死锁
如何验证程序是出现了死锁而不是因为其他原因(比如死循环、微服务调用失败)导致运行不下去呢?
——在命令行界面(Terminal)中输入命令:
-
jps -l:类似Linux中的ps -ef命令,可以查看当前程序的所有进程。利用该命令找到当前进程:
-
jstack pid:jvm中自带的堆栈跟踪工具。如果有死锁会在最后有以下提示信息:
4.读写锁
(1)读写锁体系结构
(2)读写锁的使用
-
创建读写锁ReadWriteLock对象
-
加/解读锁
rwLock.readLock().lock()/unlock();
-
加/解写锁
rwLock.writeLock().lock()/unlock();
(3)读写锁的降级
读写锁降级指的是写锁降级成为读锁。锁降级是指保持当前拥有的写锁的同时,再获取到读锁,然后再释放写锁的过程。
-
为什么需要锁降级?
主要是为了保证数据的可见性。考虑一种场景:线程A修改完后还想查看刚才写的数据:
-- 没有锁降级:线程A修改完后会直接释放写锁,假设此时线程B获取了写锁并修改了数据,那么线程A再读到的数据就不是自己修改了的数据。
-- 加了锁降级:线程A修改完释放写锁后紧接着获取读锁,那么线程B将会被阻塞,直到线程A读完数据并释放读锁后,线程B才能获取写锁。
七、JUC中的辅助类
1.CountDownLatch——减少计数
CountDownLatch 类可以设置一个计数器。
CountDownLatch 主要有两个方法——await()和countDown()。
CountDownLatch 主要有两个方法——await()和countDown()。
- 当线程调用 await 方法时,该线程会阻塞,直到计数器的值 ≤ 0,该线程才会被唤醒。
- 线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)。
2.CyclicBarrier——循环栅栏
CyclicBarrier就是循环阻塞的意思。在使用中,CyclicBarrier 构造方法第一个参数是目标障碍数,每次执行CyclicBarrier一次障碍数会加一,如果达到了目标障碍数才会执行cyclicBarrier.await0之后的语句。第二个参数barrierAction可以理解为达到目标障碍数后要做的事情。
可以将CyclicBarrier 理解为加1操作。
3.Semaphore——信号量
这个信号量就是操作系统中学的那个信号量,用来实现多线程互斥访问共享资源。
使用构造方法创建信号量对象时,可以用permits参数规定共享资源的数量。
- acquire():调用该方法的线程请求获取一个信号量(一个共享资源),获取不到将阻塞等待,获取到才能执行。
- release():调用该方法的线程释放信号量(可以通过参数释放多个信号量)。像unlock()一样,一般放在finally块里。
八、线程池阻塞队列(BlockingQueue)
1.什么是阻塞队列?
阻塞队列是一个支持两个附加操作的队列:
- 在队列为空时,获取元素的线程会阻塞等待,直到队列中有元素;
- 当队列满时,存储元素的线程会阻塞等待,直到队列中有空位。
阻塞队列的好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,这一过程由BlockingQueue来完成。
2.阻塞队列的体系结构
3.分类
主要有3种类型的BlockingQueue:
(1)有界队列
-
ArrayBlockingQueue:基于数组的有界阻塞队列。
在ArrayBlockingQueue 内部,维护了一个定长数组,以便缓存队列中的数据对象。 -
LinkedBlockingQueue:基于链表的有界阻塞队列。
同 ArrayBlockingQueue 类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue 可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。
LinkedBlockingQueue 能够高效地处理并发数据,因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
(2)无界队列
-
DelayQueue:基于优先级队列实现的延迟无界阻塞队列。
DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。 -
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。 - LinkedTransferQueue:基于链表的无界阻塞队列。
-
LinkedBlockingDeque:基于链表的双向阻塞队列。
可以看做无界队列,但也可以设置容量限制,作为有界队列。
(3)同步队列
-
SynchronousQueue——不存储元素的阻塞队列。
如果不希望任务在队列中等待,而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。也就是说SynchronousQueue是一种线程间的移交机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。
4.常用方法
九、线程池(ThreadPool)
1.概述
线程池是一种线程使用模式。
线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这样可以避免在处理短时间任务时创建与销毁线程的代价。
线程池不仅能够保证内核的充分利用,还能防止过分调度。
2.优点
- 降低资源消耗:通过重复利用已创建的线程来降低线程创建和销毁造成的开销。
- 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性:如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以对线程进行统一的分配、调优和监控。
3.分类
(1)Executors.newFixedThreadPool(int)——一池N线程
创建一个可复用的固定线程数的线程池,以共享的无界队列方式来运行这些线程。
- 线程池中的线程处于一定的范围(固定线程数),可以很好的控制线程的并发量;
- 线程可以被重复使用,在显示关闭之前,都将一直存在;
- 超出线程池上限的线程被提交时候需要在队列中等待。
-
//一池五线程 ExecutorService threadPool = Executors.newFixedThreadPool(5);
(2)Executors.newSingleThreadExecutor——一池一线程
-
ExecutorService threadPool = Executors.newSingleThreadExecutor();
(3)Executors.newCachedThreadPool——线程池可扩容
-
ExecutorService threadPool = Executors.newCachedThreadPool();
(4)Executors.newScheduledThreadPool——适用于执行延时或者周期性任务
-
ExecutorService threadPool = Executors.newScheduledThreadPool();
4.线程池的使用
(1)创建一个线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
(2)将任务提交给线程池
将任务提交给线程池可以用execute()或者submit()。
- execute():直接执行任务,无法获取执行结果。
- submit():执行任务,并且可以获取执行的结果。
threadPool.execute(Runnable runnable);(3)
5.线程池创建的底层原理
上面三种线程池创建的底层原理都是调用了ThreadPoolExecutor()。
(1)ThreadPoolExecutor()的7个参数
(2)工作流程及拒绝策略
-
假设常驻线程=2、最大线程数量=5、阻塞队列=3。现在不断有线程来调用线程池中的线程:
- 线程池一创建,线程池里就会有2个线程(常驻线程)等待任务到来;
- 线程1、2调用两个常驻线程;
- 线程3、4、5想调用时常驻线程正在被调用,那么3、4、5就进入阻塞队列等待;如果常驻线程空闲了,就去阻塞队列中取任务来执行;
- 线程6、7、8也想调用线程池中的线程,那么线程池就会再创建三个线程(达到最大线程数5了);当这3个新创建的非常驻线程空闲的时间超过了线程存活时间(keepAliveTime),这三个非常驻线程就会被释放。
- 后续线程9、10...还想调用调用线程池中的线程,线程池就会根据拒绝策略拒绝这些线程的调用。
-
拒绝策略:
5.自定义线程池
实际开发中一般不用Executors创建线程池,根据实际需求通过ThreadPoolExecutor自定义线程池。
(1)为什么要自定义线程池?
(2)通过ThreadPoolExecutor自定义线程池
十、Fork/Join分支合并框架
1.简介
Fork/Join 可以将一个大任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。
Fork/Join 框架要完成两件事情:
- Fork:把一个复杂任务进行拆分;
- Join:把分拆任务的结果进行合并。
子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。
2.Java API
在Java的Fork/Join框架中,使用两个类完成上述操作。
(1)分支合并池——ForkJoinPool
与线程池的功能类似。
(2)ForkJoinTask与RecursiveTask
RecursiveTask继承了ForkJoinPool,继承后可以实现递归调用的任务,即自己可以调用自己。
十一、异步回调CompletableFuture
1.简介
在 Java 8 中,新增加了一个类——CompletableFuture,它提供了 Future 的扩展功能,可以简化异步编程的复杂性,可以通过回调的方式处理计算结果,并且提供了转换和组合 CompletableFuture 的方法。
CompletableFuture 和 FutureTask 同属于 Future 接口的实现类,Future最大的特点就是可以获取到异步执行的结果。
CompletableFuture 和 FutureTask 同属于 Future 接口的实现类,Future最大的特点就是可以获取到异步执行的结果。
2.使用
(1)创建异步对象——runAsync和supplyAsync
有两种方法创建异步对象,runAsync 都是没有返回结果的,supplyAsync 都是可以获取返回结果的。另外第二个参数(Executor)可以传入自定义的线程池,不传就用默认的线程池。
-
用runAsync创建:
-
用supplyAsync创建:
调用异步对象的get方法获取结果:
(2)完后回调与异常感知——whenComplete()
如果我们相等上面的任务完成后,再做某件事情,可以用完成时回调方法。
完成时回调方法有3种:
完成时回调方法有3种:
【tips】whenComplete()需要传一个BiConsumer(T,U),T是当前任务执行后的结果,U是执行过程中出现的异常,没有异常就是null。
-
whenComplete():等待当前任务执行完后再执行whenComplete()中的任务,且由执行当前任务的线程继续执行 whenComplete 的任务。
【结果】
-
whenCompleteAsync():与whenComplete()唯一的不同是,执行当前任务的线程与执行whenComplete中任务的线程不是同一个线程,二者是异步的(Async)。
【tips】如果是使用相同的线程池,也可能会被同一个线程选中执行。
-
exceptionally():处理执行过程中的异常。whenComplete()只能感知异常,不能针对异常做出处理,而exceptionally()可以对异常做出处理。
【结果】
(3)最终处理——handle()
上面的whenComplete()只能拿到当前执行的结果,无法对这个结果进行修改(即没有返回值),而handle()可以对结果进行处理并返回处理后的结果(即有返回值)。
-
比如我们可以在有结果没异常时,将结果*2后返回新结果;在没结果有异常时返回一个0:
【无异常的结果】int i = 10 / 4;
【有异常的结果】
(4)线程串行化方法——thenApply/Accept/Run()
线程串行化:比如有两个线程A和B,我们现在想让B在A执行完成得到结果后,拿着A的结果再执行。这就叫线程串行化。
【tips】加了Async的跟前面的一样,前后两次任务由异步线程执行。
-
thenRun():只要当前任务执行完成,就开始执行 thenRun中的任务,不需要前面的结果。
【结果】
-
thenAccepy():接收当前任务的处理结果,并进行消费,无返回结果。
【结果】
-
thenApply():既可以接收当前任务的结果,进行消费处理,又可以将处理后的新结果进行返回。
【结果】
(5)两任务组合——都完成后再执行——thenCombine/AcceptBoth()、runAfterBoth()
-
runAfterBoth():组合两个 future,不需要(也不能)获取 future 的结果,只需两个 future 都处理完任务后,再处理runAfterBoth中的任务,无返回值。
【结果】
-
thenAcceptBoth():组合两个 future,获取两个 future 任务的执行结果,然后处理thenAcceptBoth中的任务,无返回值。
【结果】
-
thenCombine():组合两个 future,获取两个 future 的执行结果,并返回thenCombine处理的新结果。
【结果】
(6)两任务组合——任意一个完成就执行——applyToEither()、acceptEither()、runAfterEither()
-
runAfterEither():两个任务有一个执行完成,不需要获取 future 的结果,就可以处理任务,无返回值。
-
acceptEither():两个任务有一个执行完成,获取该任务的返回值,处理任务,无返回值。
-
applyToEither():两个任务有一个执行完成,获取该任务的返回值,处理任务并返回新的结果。
(7)多任务组合——allOf()、anyOf()
-
allOf():等待所有任务完成再执行新任务;
【结果】
-
anyOf():只要有一个任务完成就可以执行新任务,有返回值,返回值是任务完成的那个结果。
【结果】