Concurrent总结
线程和进程
进程 —> QQ, 360
线程 —> 进程中的程序, 每一个进程至少有一个线程
多核CPU中进程可以并发执行
CUP同一时刻只能运行一个进程, 不同的线程来抢占CPU时间片
并发编程领域, 提醒性能本质就是提升硬件的利用率 —> 提升I/O和CPU的利用率
Volatile
可见性和有序性问题:
package com.cdqf.concurrent;
public class Test01 {
public static void main(String[] args) {
new Thread(() -> System.out.println(SingleTon.getInstance())).start();
new Thread(() -> System.out.println(SingleTon.getInstance())).start();
}
}
class SingleTon {
private static volatile SingleTon singleTon;
private SingleTon() {
}
public static SingleTon getInstance() {
if (singleTon == null) {
// 线程A 抢占到锁, B等待, 当线程A运行到33行时释放锁, 线程B拿到锁
// 这时候可能会出现A线程还未完成第三步的情况, 线程B进入代码块后判断singleTon为空
// 就会造成内存可见性问题
synchronized (SingleTon.class) {
if (singleTon == null) {
// 这里new SingleTong()其实分为三步
// 1. 堆内分配空间和地址
// 2. 将地址赋予对象
// 3. 初始化这个类
// 我们所期望的正确顺序为 1 3 2 但是由于JMM重排序的存在
// 可能会造成123的情况 这就是指令重排序(有序性)
singleTon = new SingleTon();
}
}
}
return singleTon;
}
}
以上可见性和有序性问题可以使用volatile解决
可见性:
当使用了volatile的共享变量被一个线程修改了以后, volatile语句会强迫正在使用此共享变量的其他线程所拿到的共享变量失效, 然后重新读取
有序性:
volatile指令会使用内存屏障屏蔽指令重排
原子性问题:
package com.cdqf.concurrent;
public class Test02 {
private volatile static int i = 0;
public static void main(String[] args) {
for (int i1 = 0; i1 < 20; i1++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 我们所期望的i值最后为20000, 但结果却小于20000 造成这一结果的原因是
// 我们拆解i++ 这一步骤 1. 从主存中读取i值 2. i++ 3. 将值写到主存中
// 但是由于volatile语句的作用, 可能在A线程执行到第二步的时候, B线程已经执行完毕了
// 这时候会强制要求A线程重新读取i的值, 造成i值的一次丢失
i++;
}
}).start();
while (Thread.activeCount() > 2) {
Thread.yield();
}
}
System.out.println(i);
}
}
解决办法:
加锁:
package com.cdqf.concurrent;
public class Test02 {
private static volatile int i = 0;
private static final Object lock = new Object();
public static void main(String[] args) {
for (int j = 0; j < 20; j++) {
new Thread(() -> {
synchronized (lock) {
for (int k = 0; k < 1000; k++) {
i++;
}
}
}).start();
}
while (Thread.activeCount() > 2) Thread.yield();
System.out.println(i);
}
}
Synchronized锁
使用场景:
1.代码块
2.静态方法, 锁class
3.普通方法, 锁this
特点:
1.不可以被打断
2.synchronized锁的释放时机为出现异常jvm自动释放或同步代码块执行完毕释放
3.可重入锁 (可以对一个对象重复加锁)
Synchronized(对象在内存中的布局)三大模块:
对象头:
syn(A) :
Mark Word: 00空闲 01使用状态标识 标记A是否被使用
class metadata address: 指向class的指针
每个对象都有一个Monitor对象: 管程
entryList: 当前准备抢占对象锁的线程集合
owner: 拥有当前monitor的线程
count: 如果锁被持有 count+1 , 锁被释放 count-1
waitSet: 当我们调用wait()方法时, 线程进入waitSet中等待nofity/nofityAll方法唤醒
管程流程: 先读取count 和 owner 然后entryList其中某个线程继续进入
monitorEnter(): 如果能进入monitor就代表管程中已经有owner了, 指向同步代码块的开始位置
monitorExit(): monitor释放方法, 指向同步代码块的出去位置
无锁-------偏向锁(偏向某一个线程)------轻量级锁((交替执行)----重量级锁(竞争非常激烈)
实例数据:
对齐填充:
lock锁
API:
-
lock():获得锁,如果锁被占用则等待。
-
lockInterruptibly():获得锁,但优先响应中断。(留着)
-
tryLock():尝试获得锁,如果成功,则返回true;失败返回false。此方法不等待,立即返回。
-
tryLock(long time,TimeUnit):在上面的方法上加上时间
-
unlock():释放锁
-
Condition newCondition()
lock/unlock:
package com.cdqf.concurrent;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test03 implements Runnable {
Lock lock = new ReentrantLock();
private static int i;
@Override
public void run() {
try {
lock.lock();
for (int i1 = 0; i1 < 10000; i1++) {
i++;
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Test03 myThread = new Test03();
for (int i1 = 0; i1 < 3; i1++) {
new Thread(myThread).start();
}
while (Thread.activeCount() > 2) Thread.yield();
System.out.println(i);
}
}
trylock:
package com.cdqf.concurrent;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test03 implements Runnable {
Lock lock = new ReentrantLock();
private static int i;
@Override
public void run() {
if (lock.tryLock()) {
try {
for (int i1 = 0; i1 < 10000; i1++) {
i++;
}
} finally {
lock.unlock();
}
} else {
System.out.println("未能拿到锁的线程:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
Test03 myThread = new Test03();
for (int i1 = 0; i1 < 3; i1++) {
new Thread(myThread).start();
}
while (Thread.activeCount() > 2) Thread.yield();
System.out.println(i);
}
}
trylock重载:
package com.cdqf.concurrent;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test03 implements Runnable {
Lock lock = new ReentrantLock();
private static int i;
@Override
public void run() {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
Thread.sleep(1000);
System.out.println("拿到锁的线程为:" + Thread.currentThread().getName());
for (int i1 = 0; i1 < 10000; i1++) {
i++;
}
} finally {
lock.unlock();
}
} else {
System.out.println("未能拿到锁的线程:" + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Test03 myThread = new Test03();
for (int i1 = 0; i1 < 3; i1++) {
new Thread(myThread).start();
}
while (Thread.activeCount() > 2) Thread.yield();
System.out.println(i);
}
}
lockInterruptibly:
package cn.cdqf.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest2 implements Runnable{
Lock lock = new ReentrantLock();
@Override
public void run() {
try {
lock.lockInterruptibly();
for (; ; ) {
System.out.println(Thread.currentThread().getName() + "抢到锁");
//Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()+"被打断了");
}finally {
//一定要保证释放锁
lock.unlock();
}
// lock.lock();
}
public static void main(String[] args) throws InterruptedException {
LockTest2 lockTest = new LockTest2();
Thread thread = new Thread(lockTest);
thread.start();
Thread.sleep(2000);
Thread thread1 = new Thread(lockTest);
thread1.start();
//如果thread1能获得锁 就继续运行 如果不能获得锁 我自己打断等待
thread1.interrupt();
//thread.interrupt();
}
}
读写锁 ReadWriteLock
读锁:共享锁
写锁:互斥/排他锁
package com.cdqf.concurrent;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test04 implements Runnable {
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
@Override
public void run() {
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
readLock.lock();
//writeLock.lock();
for (int i = 0; i < 2; i++) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程:" + Thread.currentThread().getName() );
}
readLock.unlock();
//writeLock.unlock();
}
public static void main(String[] args) {
Test04 test04 = new Test04();
new Thread(test04).start();
new Thread(test04).start();
}
}
Semaphore 信号流
package com.cdqf.concurrent;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
public class Test05 implements Runnable {
// 放入了十个许可
Semaphore semaphore = new Semaphore(10);
AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
if (semaphore.tryAcquire()) {
try {
// 获得一个许可, 进入方法
// semaphore.acquire();
System.out.println("当前获得线程的个数为:" + atomicInteger.incrementAndGet());
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可
semaphore.release();
}
} else {
System.out.println("服务器正忙, 请稍后再试");
}
}
public static void main(String[] args) {
Test05 test05 = new Test05();
for (int i = 0; i < 100; i++) {
new Thread(test05).start();
}
}
}
运行结果:
当前获得线程的个数为:1
当前获得线程的个数为:3
当前获得线程的个数为:2
当前获得线程的个数为:4
当前获得线程的个数为:5
当前获得线程的个数为:6
当前获得线程的个数为:7
当前获得线程的个数为:8
当前获得线程的个数为:9
当前获得线程的个数为:10
服务器正忙, 请稍后再试
服务器正忙, 请稍后再试
服务器正忙, 请稍后再试
服务器正忙, 请稍后再试...
CountDownLatch
package com.cdqf.concurrent;
import java.util.concurrent.CountDownLatch;
public class Test06 {
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
System.out.println("正在执行:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
latch.countDown();
System.out.println("执行完毕:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println("正在执行:" + Thread.currentThread().getName());
latch.countDown();
System.out.println("执行完毕:" + Thread.currentThread().getName());
}).start();
try {
latch.await();
System.out.println("所有线程执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
CyclicBarrier
package com.cdqf.concurrent;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;
public class Test07 {
public static void main(String[] args) {
Writer writer = new Writer();
for (int i = 0; i < 4; i++)
new Thread(writer).start();
}
static class Writer implements Runnable {
private AtomicInteger atomicInteger = new AtomicInteger(0);
public static CyclicBarrier cyclicBarrier = new CyclicBarrier(4);
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "正在写入数据...");
try {
Thread.sleep(atomicInteger.incrementAndGet() * 2000); //以睡眠来模拟写入数据操作
System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕,等待其他线程写入完毕");
//会阻塞。。。直到 4 个线程都走到这儿
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("所有线程写入完毕,继续处理其他任务...");
}
}
}
CAS(CompareAndSwap) 与 AtomicInteger相关
当业务需求十个线程同时执行
for (int i = 0; i<1000 ;i++) {}
使用CAS会有以下三个核心数据:
V: 地址偏移量 —> 当i从0到1的时候 我们可以把偏移量看做是1
A: 我们的预期值, 从主存中拿到的i的值
B: 改变后的值, 从0到1这一步中我们可以把预期值看做是1
只有当地址偏移量未改变的情况下, 即 V = A 我们才会进行下一步操作 i ++, 否则的话CAS进行自旋(无限循环) 因为cas检测到在我修改数值之前我的地址偏移量产生了变化, 所以重新获取预期值
无限循环导致的时间开销大:
只能保证一个变量的原子性:
ABA问题: AtomicStampedReference类解决ABA问题(核心为标记)
线程池
Executors: executor顶级接口工具类
newCachedThreadPool: 无限大的可缓存线程池, 当执行第二个任务时第一个任务已经完成的情况下, 会复用第一个任务的线程而不会创建新线程
newFixedThreadPool: 定长线程池, 控制线程最大并发数, 超出的线程会在队列中等待
newScheduledThreadPool: 定长线程池, 支持定时以及周期性的执行任务
newSingleThreadExecutor: 单线程化的线程池, 保证所有任务按照指定的顺序执行
线程池核心设置: new ThreadPoolExecutor()
corePoolSize: 核心线程数, 表示线程池存在的最小线程数, 不会被回收
maximumPoolSize: 线程池最大线程数, 表示该线程池最多同时存在的线程数量
keepAliveTime & unit: 空闲的时间和单位, 表示超过该空闲时间的线程就会被回收
workQueue: 工作队列
threadFactory: 可以自定义创建线程, 例如给线程自定义名字等
handler:
可以通过该参数自定义任务的拒接策略, 如果线程池中所有的线程都在运行并且工作队列也满载, 那么在此时提交任务线程池就会拒绝接受.
拒绝策略:
CallerRunPolicy: 提交任务的线程会自己去执行任务
AbortPolicy: 默认的拒绝策略, throws RejectedExecutionException
DiscardPolicy: 丢弃任务, 并且不抛出异常
DiscardOldestPolicy: 丢弃最老的工作队列中的任务并把新任务添加进队列
关闭方法:
shutdown(): 不会立即终止线程池, 会等待所有任务执行完毕, 并且不会再接受新任务
shutdownNow(): 立即终止线程池, 并尝试打断正在执行的任务, 清空队列, 返回尚未执行的任务
Callable
线程创建的第三种方式, 存在的意义为, 因为Run方法没有参数, 没有返回值, 不能抛出异常, 所以我们需要另一种方法去监控线程中的异常状态等信息
package com.cdqf.concurrent;
import java.util.concurrent.*;
public class Test08 implements Callable<String> {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
try {
String s = executorService.submit(new Test08()).get();
System.out.println(s);
} catch (InterruptedException | ExecutionException e) {
System.out.println("抛出异常: " + e.getMessage());
}
executorService.shutdown();
}
@Override
public String call() {
System.out.println("callable线程方法运行中");
// if (1 == 1) throw new RuntimeException("1=1异常");
return "这是一个Callable返回值";
}
}
Future
package com.cdqf.concurrent;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* FutureTask类
*/
public class Test09 implements Callable {
@Override
public Object call() throws Exception {
System.out.println("callable线程执行中");
return "callable返回值";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<String>(new Test09()){
@Override
protected void done() {
System.out.println("callable任务执行结束");
}
};
futureTask.run();
String s = futureTask.get();
System.out.println(s);
}
}
CompletableFuture类
CompletableFuture 默认使用ForkJoinPool()线程池, 但是也可以自定义配置
runAsync(Runable) 无返回值
runAsycn(Runable, Executor) 无返回值
package com.cdqf.concurrent;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* CompletableFuture类 runAsync方法
*/
public class Test10 {
public static void main(String[] args) {
// 新建线程池
ExecutorService cDqf_ = Executors.newCachedThreadPool(r -> {
Thread thread = new Thread(r);
thread.setName("CDqf_");
return thread;
});
// CompletableFuture runAsync方法实现
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {
System.out.println("当前执行的线程是 : " + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 放入自定义线程池
, cDqf_);
// 线程执行异步任务
voidCompletableFuture.join();
System.out.println("异步任务执行结束");
cDqf_.shutdown();
}
}
supplyAsync(Supplier supplier)
supplyAsync(Supplier supplier, Executor)
thenApply(Function<?, ?> function) 跟supplyAsync使用同一个线程运行
thenApply(Function<?, ?> function, Executor)
thenApplyAsync(Function<?, ?> function)
thenApplyAsync(Function<?, ?> function, Executor)
package com.cdqf.concurrent;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* CompletableFuture类 supplyAsync方法 thenApply方法
*/
public class Test11 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 使用自定义FixedThreadPool定长线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
CompletableFuture<String> ret = CompletableFuture.supplyAsync(() -> {
System.out.println("当前执行方法的线程是 : " + Thread.currentThread().getName());
// supplyAsync方法相比runAsync方法的特点之一是他有返回值, 而且是一个泛型约束的返回值
return "supplyAsync方法的返回值";
// 可以使用自定义线程池或默认线程池
// thenApply(Function(有参数, 有返回值)) s 代表的是上一个方法的返回值
}, executorService).thenApply((s) -> {
System.out.println("上一个方法的返回值是:" + s);
return s.toUpperCase();
}).thenApplyAsync((s) -> {
System.out.println("最终返回值为:" + s);
return s.substring(0, 11);
});
// 这里使用get方法之后就会自定加载线程任务, 而不用使用join方法再将线程任务插入主线程
System.out.println(ret.get());
// 关闭线程池
executorService.shutdown();
}
}
thenAccept()
thenRun()
package com.cdqf.concurrent;
import java.util.concurrent.CompletableFuture;
/**
* CompletableFuture类 thenAccept方法
*/
public class Test12 {
public static void main(String[] args) {
CompletableFuture<Void> stringCompletableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "线程运行中");
return "supplyAsync返回值";
// thenAccept会直接消耗掉类对象, 再在下方获取对象会得到空值
}).thenAccept(System.out::println).thenRun(() -> System.out.println("thenRun方法执行"));
}
}
thenCompose() 组装多个有关联关系的异步任务
package com.cdqf.concurrent;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* completableFuture类 thenCompose方法 thenCombine方法
*/
public class Test13 {
private static CompletableFuture<String> firstStep() {
return CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "线程任务开始");
return "First Step";
});
}
private static CompletableFuture<String> secondStep(Object s) {
return CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "线程任务开始");
System.out.println("完成前置条件:" + s);
return "Second step";
});
}
// 将第一步和第二步组装起来
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> stringCompletableFuture = firstStep().thenCompose(Test13::secondStep);
System.out.println(stringCompletableFuture.get());
}
}
thenCombine() 组装两个没有参数关联的异步任务
package com.cdqf.concurrent;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* CompletableFuture类 thenCombine方法
*/
public class Test14 {
private static CompletableFuture<String> first() throws InterruptedException {
Thread.sleep(2000);
System.out.println("first down");
return CompletableFuture.supplyAsync(() -> "first");
}
private static CompletableFuture<String> second() throws InterruptedException {
Thread.sleep(1000);
System.out.println("second down");
return CompletableFuture.supplyAsync(() -> "second");
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> stringCompletableFuture = first().thenCombine(second(), (ret1, ret2) -> {
System.out.println("完成: " + ret1);
System.out.println("完成: " + ret2);
return "mission success";
});
System.out.println(stringCompletableFuture.get());
}
}
allOf 等待多个异步任务执行成功
package com.cdqf.concurrent;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* CompletableFuture类 allOf方法
*/
public class Test15 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Void> first = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("first down");
});
CompletableFuture<Void> second = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("second down");
});
CompletableFuture<Void> third = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("third down");
});
// 以上方法是没有先后顺序的
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(first, second, third);
// 同步所有的方法
voidCompletableFuture.get();
System.out.println("all down");
}
}
anyOf() 只要有一个任务成功, 即步入下一步, 不等待其他任务
package com.cdqf.concurrent;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* CompletableFuture类 anyOf方法
*/
public class Test16 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Void> first =
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("first down");
});
CompletableFuture<String> second = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("second down");
return "second key";
});
CompletableFuture<Void> third =
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("third down");
});
CompletableFuture<Object> objectCompletableFuture = CompletableFuture.anyOf(first, second, third);
objectCompletableFuture.join();
System.out.println(objectCompletableFuture.get());
}
}
exceptionally() 异常处理方法
package com.cdqf.concurrent;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* ExceptionHandler
*/
public class Test17 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> p = CompletableFuture.supplyAsync(() -> {
System.out.println("first");
return "first down";
}).thenApply((s) -> {
System.out.println(s + " second going...");
return "second down";
}).thenApply((s) -> {
System.out.println(s);
if (1==1) throw new RuntimeException();
return "finish";
// 因为出现异常, 下面的代码不会运行
}).thenApply((s) -> {
System.out.println(s);
return "Project done";
});
System.out.println(p.get());
}
private static void exceptionally() {
CompletableFuture<String> exceptionally = CompletableFuture.supplyAsync(() -> {
System.out.println("first");
return "first";
}).thenApply((s) -> {
System.out.println("second " + s);
return "second";
}).thenApply((s) -> {
System.out.println("finish " + s);
if (1 == 1) throw new RuntimeException("1=1 异常");
return "down";
}).exceptionally((e) -> {
// 因为使用了exceptionally方法, 所以可以捕获到异常
return "exception";
});
String join = exceptionally.join();
System.out.println("finally:" + join);
}
}