一,并发包种的锁
1.Lock和Condition
1.1Lock
1. lock和synchronized 的区别
synchronized不需要手动加锁,不会出现死锁,异常自动释放锁;
Lock手动加锁解锁,提供丰富的方法,异常不会自动释放锁,使用不当容易死锁;
Lock可以非阻塞获取锁;
synchronized当一个线程获取锁后,其它线程阻塞,不能解决“破坏锁不可抢占方案”;
Lock提供了3中解决方案:
1. 能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
2. 支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
3. 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
2. lock怎么解决可见性问题
synchronized 的解锁 Happens-Before 于后续对这个锁的加锁,解决可见性问题;
Lock怎么解决可见性呢?它是利用了 volatile 相关的 Happens-Before 规则;ReentrantLock,内部持有一个 volatile 的成员变量 state,加锁和解锁都会读写volitale修饰的state变量;
顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock();
volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作;
传递性规则:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() 操作。
结合代码来看:
class X {
private final Lock rtl =
new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value+=1;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
class SampleLock {
volatile int state;
// 加锁
lock() {
// 省略代码无数
state = 1;
}
// 解锁
unlock() {
// 省略代码无数
state = 0;
}
}
3. 公平锁和非公平锁
在公平锁中,每一次的tryAcquire都会检查CLH队列中是否仍有前驱的元素,如果仍然有那么继续等待,通过这种方式来保证先来先服务的原则;
而非公平锁,首先是检查并设置锁的状态,这种方式会出现即使队列中有等待的线程,但是新的线程仍然会与排队线程中的对头线程竞争(但是排队的线程是先来先服务的),所以新的线程可能会抢占已经在排队的线程的锁,这样就无法保证先来先服务,但是已经等待的线程们是仍然保证先来先服务的。
公平锁能保证先来先服务,非公平锁不能保证先来先服务;
主要有两个相关的构造方法:
//无参构造函数:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//根据公平策略参数创建锁
public ReentrantLock(boolean fair){
sync = fair ? new FairSync()
: new NonfairSync();
}
1.2 Condition
Lock解决并发中的互斥,而Condition解决同步问题;Condition 实现了管程模型里面的条件变量,不同于java默认管程,java SDK包中管程可以有多个条件变量;
同步和异步的区别到底是什么呢?
通俗点来讲就是调用方是否需要等待结果,如果需要等待结果,就是同步;如果不需要等待结果,就是异步。
如何利用两个条件变量快速实现阻塞队列呢?
public class BlockedQueue<T>{
final Lock lock =
new ReentrantLock();
// 条件变量:队列不满
final Condition notFull =
lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty =
lock.newCondition();
// 入队
void enq(T x) {
lock.lock();
try {
while (队列已满){
// 等待队列不满
notFull.await();
}
// 省略入队操作...
//入队后,通知可出队
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void deq(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
//出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}
2.信号量Semaphore
2.1 信号量模型
- init():设置计数器的初始值。
- down():计数器的值减 1;如果此时计数器的值小于 0,则当前线程将被阻塞,否则当前线程可以继续执行。
- up():计数器的值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。
init()、down() 和 up() 三个方法都是原子性的。
class Semaphore{
// 计数器
int count;
// 等待队列
Queue queue;
// 初始化操作
Semaphore(int c){
this.count=c;
}
//
void down(){
this.count--;
if(this.count<0){
//将当前线程插入等待队列
//阻塞当前线程
}
}
void up(){
this.count++;
if(this.count<=0) {
//移除等待队列中的某个线程T
//唤醒线程T
}
}
}
2.2 信号量实现一个限流器
Java SDK 里面提供了 Lock,为啥还要提供一个 Semaphore ?
Semaphore 可以允许多个线程访问一个临界区。
我们工作中遇到的各种池化资源,例如连接池、对象池、线程池等等,就可以用信号量来实现。
我们把计数器的值设置成对象池里对象的个数 N,就能完美解决对象池的限流问题了。
class ObjPool<T, R> {
final List<T> pool;
// 用信号量实现限流器
final Semaphore sem;
// 构造函数
ObjPool(int size, T t){
pool = new Vector<T>(){};
for(int i=0; i<size; i++){
pool.add(t);
}
sem = new Semaphore(size);
}
// 利用对象池的对象,调用func
R exec(Function<T,R> func) {
T t = null;
sem.acquire();
try {
t = pool.remove(0);
return func.apply(t);
} finally {
pool.add(t);
sem.release();
}
}
}
// 创建对象池
ObjPool<Long, String> pool =
new ObjPool<Long, String>(10, 2);
// 通过对象池获取t,之后执行
pool.exec(t -> {
System.out.println(t);
return t.toString();
});
3.ReadWriteLock与StampedLock
3.1 ReadWriteLock
3.1.1 读写锁规则
读写锁都遵守以下三条基本原则:
- 允许多个线程同时读共享变量;
- 只允许一个线程写共享变量;
- 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
3.1.2读写锁实现缓存
class CachedData {
Object data;
volatile boolean cacheValid;
final ReadWriteLock rwl =
new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
//写锁
final Lock w = rwl.writeLock();
void processCachedData() {
// 获取读锁
r.lock();
if (!cacheValid) {
// 释放读锁,因为不允许读锁的升级
r.unlock();
// 获取写锁
w.lock();
try {
// 再次检查状态
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 释放写锁前,降级为读锁
// 降级是可以的
r.lock(); ①
} finally {
// 释放写锁
w.unlock();
}
}
// 此处仍然持有读锁
try {use(data);}
finally {r.unlock();}
}
}
获取写锁的前提是读锁和写锁均未被占用
获取读锁的前提是没有其他线程占用写锁
申请写锁时不中断其他线程申请读锁
这个缓存虽然解决了缓存的初始化问题,但是没有解决缓存数据与源头数据的同步问题,这里的数据同步指的是保证缓存数据和源头数据的一致性。
解决方案:
1.超时机制,缓存增加超时时间;
2.通过近实时地解析 binlog 来识别数据是否发生了变化,如果发生了变化就将最新的数据推送给缓存;
3.数据库和缓存的双写。
3.2 StampedLock
StampedLock 支持三种模式,分别是:写锁、悲观读锁和乐观读。
StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。
StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。
class Point {
private int x, y;
final StampedLock sl =
new StampedLock();
//计算到原点的距离
int distanceFromOrigin() {
// 乐观读
long stamp =
sl.tryOptimisticRead();
// 读入局部变量,
// 读的过程数据可能被修改
int curX = x, curY = y;
//判断执行读操作期间,
//是否存在写操作,如果存在,
//则sl.validate返回false
if (!sl.validate(stamp)){
// 升级为悲观读锁
stamp = sl.readLock();
try {
curX = x;
curY = y;
} finally {
//释放悲观读锁
sl.unlockRead(stamp);
}
}
return Math.sqrt(
curX * curX + curY * curY);
}
}
如果执行乐观读操作的期间,存在写操作,会把乐观读升级为悲观读锁。
使用场景以及注意事项:
对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代 ReadWriteLock,但是StampedLock 的功能仅仅是 ReadWriteLock 的子集;
StampedLock 不支持重入;
StampedLock 的悲观读锁、写锁都不支持条件变量
使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁writeLockInterruptibly()。
二,其他并发包知识
CountDownLatch 和 CyclicBarrier的区别:
CountDownLatch 主要用来解决一个线程等待多个线程的场景; CountDownLatch的计数器是不能循环利用的,也就是说一旦计数器减到 0,再有线程调用 await(),该线程会直接通过。
CyclicBarrier 是一组线程之间互相等待; CyclicBarrier 的计数器是可以循环利用的,而且具备自动重置的功能,一旦计数器减到 0会自动重置到你设置的初始值
CyclicBarrier 还可以设置回调函数,可以说是功能丰富。
1.并发容器
Java 中的容器主要可以分为四个大类,分别是 List、Map、Set 和 Queue;
- List
list相关只有CopyOnWriteArrayList,写操作会复制当前数组操作复制数组,读操作遍历原数组;
使用场景:写少读多且允许读写数据短暂不一致; - Map
ConcurrentHashMap: key无序
ConcurrentSkipListMap:key有序
ConcurrentSkipListMap:key里面的 SkipList 本身就是一种数据结构,翻译为“调表”,插入、删除、查询操作平均的时间复杂度是 O(log n);
- set
CopyOnWriteArraySet:场景功能类似CopyOnWriteArrayList
ConcurrentSkipListSet:场景功能类似ConcurrentSkipListMap - Queue
Java 并发包里阻塞队列都用 Blocking 关键字标识,单端队列使用 Queue 标识,双端队列使用 Deque 标识。- 单端阻塞队列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和 DelayQueue。
- 双端阻塞队列:其实现是 LinkedBlockingDeque。
- 单端非阻塞队列:其实现是 ConcurrentLinkedQueue。
- 双端非阻塞队列:其实现是 ConcurrentLinkedDeque。
2.无锁操作
2.1原理
无锁方案基本是基于CAS+自旋操作来完成的。
class SimulatedCAS{
volatile int count;
// 实现count+=1
addOne(){
do {
newValue = count+1; //①
}while(count !=
cas(count,newValue) //②
}
// 模拟实现CAS,仅用来帮助理解
synchronized int cas(
int expect, int newValue){
// 读目前count的值
int curValue = count;
// 比较目前count值是否==期望值
if(curValue == expect){
// 如果是,则更新count的值
count= newValue;
}
// 返回写入前的值
return curValue;
}
}
2.2 原子类
1. 原子化的基本数据类型
getAndIncrement() //原子化i++
getAndDecrement() //原子化的i--
incrementAndGet() //原子化的++i
decrementAndGet() //原子化的--i
//当前值+=delta,返回+=前的值
getAndAdd(delta)
//当前值+=delta,返回+=后的值
addAndGet(delta)
//CAS操作,返回是否成功
compareAndSet(expect, update)
//以下四个方法
//新值可以通过传入func函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)
2. 原子化的对象引用类型
主要有AtomicReference、AtomicStampedReference 和 AtomicMarkableReference。
AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题。
3. 原子化数组
AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray
4. 原子化对象属性更新器
相关实现有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性,这三个方法都是利用反射机制实现的。
对象属性必须是 volatile 类型的,只有这样才能保证可见性;
5. 原子化的累加器
DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。
三,线程池相关
1.线程池的创建
1.1为什么创建线程池?
创建一个线程不像创建对象那样在 内存里开辟一块空间,需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,成本较高。
所以,为了避免线程频繁创建和销毁,引入了线程池,节约成本。
1.2 线程池原理
线程池使用 生产者-消费者模式,其中阻塞队列充当两者的桥梁,
生产者调用值调用submit()或execute()提交任务Task到阻塞队列,
消费者是线程池内部操作循环或取阻塞队列中的Task。
原理简略demo可以参考以下代码:
//简化的线程池,仅用来说明工作原理
class MyThreadPool{
//利用阻塞队列实现生产者-消费者模式
BlockingQueue<Runnable> workQueue;
//保存内部工作线程
List<WorkerThread> threads
= new ArrayList<>();
// 构造方法
MyThreadPool(int poolSize,
BlockingQueue<Runnable> workQueue){
this.workQueue = workQueue;
// 创建工作线程
for(int idx=0; idx<poolSize; idx++){
WorkerThread work = new WorkerThread();
work.start();
threads.add(work);
}
}
// 提交任务
void execute(Runnable command){
workQueue.put(command);
}
// 工作线程负责消费任务,并执行任务
class WorkerThread extends Thread{
public void run() {
//循环取任务并执行
while(true){ ①
Runnable task = workQueue.take();
task.run();
}
}
}
}
/** 下面是使用示例 **/
// 创建有界阻塞队列
BlockingQueue<Runnable> workQueue =
new LinkedBlockingQueue<>(2);
// 创建线程池
MyThreadPool pool = new MyThreadPool(
10, workQueue);
// 提交任务
pool.execute(()->{
System.out.println("hello");
});
1.3 java中的线程池以及使用
- submit和execute的区别
submit方法参数丰富,可以传参Runnable和Callable,支持返回值。
execute不支持返回值,参数单一
submit可以进行异常处理 - TheadPoolExecutor
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
创建线程池时最多可设置7个参数;
corePoolSize:核心线程数,也是线程池最少活跃线程数;
maximumPoolSize:最大线程数;
keepAliveTime:线程最大空闲时间;
unit:时间单位;
workQueue:阻塞队列,生产者与消费者之间的桥梁;
threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
handler:拒绝策略,已经提供四种策略,当然也可以自定义策略。
CallerRunsPolicy:提交任务的线程自己去执行该任务。
AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。DiscardPolicy:直接丢弃任务,没有任何异常抛出。
DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
3. 为什么不推荐Executor创建线程?
Executors 提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列。
4. 如何给线程池的线程指定名字?
class ReNameThreadFactory implements ThreadFactory {
/**
* 线程池编号(static修饰)(容器里面所有线程池的数量)
*/
private static final AtomicInteger POOLNUMBER = new AtomicInteger(1);
/**
* 线程编号(当前线程池线程的数量)
*/
private final AtomicInteger threadNumber = new AtomicInteger(1);
/**
* 线程组
*/
private final ThreadGroup group;
/**
* 业务名称前缀
*/
private final String namePrefix;
/**
* 重写线程名称(获取线程池编号,线程编号,线程组)
*
* @param prefix 你需要指定的业务名称
*/
public ReNameThreadFactory(@NonNull String prefix) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
//组装线程前缀
namePrefix = prefix + "-poolNumber:" + POOLNUMBER.getAndIncrement() + "-threadNumber:";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
//方便dump的时候排查(重写线程名称)
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
- 注意事项
当线程池中无可用线程,且阻塞队列已满,那么此时就会触发拒绝策略。对于采用何种策略,具体要看执行的任务重要程度。如果是一些不重要任务,可以选择直接丢弃。但是如果为重要任务,可以采用降级处理,例如将任务信息插入数据库或者消息队列,启用一个专门用作补偿的线程池去进行补偿。所谓降级就是在服务无法正常提供功能的情况下,采取的补救措施。具体采用何种降级手段,这也是要看具体场景。
2.Future及FutureTask
2.1 如何获取线程异步执行结果?
使用submit()方法可返回线程执行结果
// 提交Runnable任务
Future<?>
submit(Runnable task);
// 提交Callable任务
<T> Future<T>
submit(Callable<T> task);
// 提交Runnable任务及结果引用
<T> Future<T>
submit(Runnable task, T result);
- 提交 Runnable 任务 submit(Runnable task) :这个方法的参数是一个 Runnable 接口,Runnable 接口的 run() 方法是没有返回值的,所以 submit(Runnable task) 这个方法返回的 Future 仅可以用来断言任务已经结束了,类似于 Thread.join()。
- 提交 Callable 任务 submit(Callable task):这个方法的参数是一个 Callable 接口,它只有一个 call() 方法,并且这个方法是有返回值的,所以这个方法返回的 Future 对象可以通过调用其 get() 方法来获取任务的执行结果。
- 提交 Runnable 任务及结果引用 submit(Runnable task, T result):这个方法很有意思,假设这个方法返回的 Future 对象是 f,f.get() 的返回值就是传给 submit() 方法的参数 result。这个方法该怎么用呢?下面这段示例代码展示了它的经典用法。需要你注意的是 Runnable 接口的实现类 Task 声明了一个有参构造函数 Task(Result r) ,创建 Task 对象的时候传入了 result 对象,这样就能在类 Task 的 run() 方法中对 result 进行各种操作了。result 相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据。
ExecutorService executor
= Executors.newFixedThreadPool(1);
// 创建Result对象r
Result r = new Result();
r.setAAA(a);
// 提交任务
Future<Result> future =
executor.submit(new Task(r), r);
Result fr = future.get();
// 下面等式成立
fr === r;
fr.getAAA() === a;
fr.getXXX() === x
class Task implements Runnable{
Result r;
//通过构造函数传入result
Task(Result r){
this.r = r;
}
void run() {
//可以操作result
a = r.getAAA();
r.setXXX(x);
}
}
2.2 Future接口
相关方法:
// 取消任务
boolean cancel(
boolean mayInterruptIfRunning);
// 判断任务是否已取消
boolean isCancelled();
// 判断任务是否已结束
boolean isDone();
// 获得任务执行结果
get();
// 获得任务执行结果,支持超时
get(long timeout, TimeUnit unit);
2.3 FutureTask工具类
FutureTask实现了Runnable和Future接口。所以它能够当作任务提交给线程池,又能获取执行结果;
主要有两个够构造是方法:
FutureTask(Callable<V> callable);
FutureTask(Runnable runnable, V result);
3.CompletableFuture
3.1 CompletableFuture实现烧水泡茶
异步化,是并行方案得以实施的基础,更深入地讲其实就是:利用多线程优化性能这个核心方案得以实施的基础。
CompletableFuture的优势:
- 无需手工维护线程,没有繁琐的手工维护线程的工作,给任务分配线程的工作也不需要我们关注;
- 语义更清晰,例如 f3 = f1.thenCombine(f2, ()->{}) 能够清晰地表述“任务 3 要等待任务 1 和任务 2 都完成后才能开始”;
- 代码更简练并且专注于业务逻辑,几乎所有代码都是业务逻辑相关的。
//任务1:洗水壶->烧开水
CompletableFuture<Void> f1 =
CompletableFuture.runAsync(()->{
System.out.println("T1:洗水壶...");
sleep(1, TimeUnit.SECONDS);
System.out.println("T1:烧开水...");
sleep(15, TimeUnit.SECONDS);
});
//任务2:洗茶壶->洗茶杯->拿茶叶
CompletableFuture<String> f2 =
CompletableFuture.supplyAsync(()->{
System.out.println("T2:洗茶壶...");
sleep(1, TimeUnit.SECONDS);
System.out.println("T2:洗茶杯...");
sleep(2, TimeUnit.SECONDS);
System.out.println("T2:拿茶叶...");
sleep(1, TimeUnit.SECONDS);
return "龙井";
});
//任务3:任务1和任务2完成后执行:泡茶
CompletableFuture<String> f3 =
f1.thenCombine(f2, (__, tf)->{
System.out.println("T1:拿到茶叶:" + tf);
System.out.println("T1:泡茶...");
return "上茶:" + tf;
});
//等待任务3执行结果
System.out.println(f3.join());
void sleep(int t, TimeUnit u) {
try {
u.sleep(t);
}catch(InterruptedException e){}
}
// 一次执行结果:
T1:洗水壶...
T2:洗茶壶...
T1:烧开水...
T2:洗茶杯...
T2:拿茶叶...
T1:拿到茶叶:龙井
T1:泡茶...
上茶:龙井
3.2CompletableFuture对象
创建CompletableFuture的集中方式:
/使用默认线程池
static CompletableFuture<Void>
runAsync(Runnable runnable)
static <U> CompletableFuture<U>
supplyAsync(Supplier<U> supplier)
//可以指定线程池
static CompletableFuture<Void>
runAsync(Runnable runnable, Executor executor)
static <U> CompletableFuture<U>
supplyAsync(Supplier<U> supplier, Executor executor)
Runnable 无返回值,Supplier有返回值
默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数。
如果所有 CompletableFuture 共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议你要根据不同的业务类型创建不同的线程池,以避免互相干扰。
3.3 CompletionStage接口
CompletableFuture实现了CompletionStage接口,此接口方法丰富,有大量描述任务时序关系的接口;
任务时序关系:串行关系、并行关系、汇聚关系等;
- 描述串行关系
CompletionStage<R> thenApply(fn);
CompletionStage<R> thenApplyAsync(fn);
CompletionStage<Void> thenAccept(consumer);
CompletionStage<Void> thenAcceptAsync(consumer);
CompletionStage<Void> thenRun(action);
CompletionStage<Void> thenRunAsync(action);
CompletionStage<R> thenCompose(fn);
CompletionStage<R> thenComposeAsync(fn);
thenApply、thenAccept、thenRun 和 thenCompose 四个接口。
thenApply:fn 的类型是接口 Function<T,R>,对应CompletionStage中的
R apply(T t) ,既能接收参数也支持返回值;
thenAccept:fn 的类型是接口consumer 的类型是接口Consumer,对应CompletionStage中的void accept(T t),支持参数,但却不支持回值
thenRun :action 的参数是 Runnable,不支持参数,不支持回值
thenCompose :这个系列的方法会新创建出一个子流程,最终结果和 thenApply 系列是相同的。
2. 描述AND汇聚关系
CompletionStage<R> thenCombine(other, fn);
CompletionStage<R> thenCombineAsync(other, fn);
CompletionStage<Void> thenAcceptBoth(other, consumer);
CompletionStage<Void> thenAcceptBothAsync(other, consumer);
CompletionStage<Void> runAfterBoth(other, action);
CompletionStage<Void> runAfterBothAsync(other, action);
- 描述OR汇聚关系
CompletionStage applyToEither(other, fn);
CompletionStage applyToEitherAsync(other, fn);
CompletionStage acceptEither(other, consumer);
CompletionStage acceptEitherAsync(other, consumer);
CompletionStage runAfterEither(other, action);
CompletionStage runAfterEitherAsync(other, action);
3.4 异步编程中异常处理
CompletionStage exceptionally(fn);
CompletionStage<R> whenComplete(consumer);
CompletionStage<R> whenCompleteAsync(consumer);
CompletionStage<R> handle(fn);
CompletionStage<R> handleAsync(fn);
exceptionally() 方法来处理异常,exceptionally() 的使用非常类似于 try{}catch{}中的 catch{},
whenComplete() 和 handle() 系列方法就类似于 try{}finally{}中的 finally{}
whenComplete() 和 handle() 的区别在于 whenComplete() 不支持返回结果,而 handle() 是支持返回结果的。
4. CompletionService批量异步编程
4.1 CompletionService对象创建
主要有两个构造方法:
ExecutorCompletionService(Executor executor);
ExecutorCompletionService(Executor executor, BlockingQueue> completionQueue)
方法说明:
Future<V> submit(Callable<V> task);
Future<V> submit(Runnable task, V result);
Future<V> take()
throws InterruptedException;
Future<V> poll();
Future<V> poll(long timeout, TimeUnit unit)
throws InterruptedException;
take()、poll() 都是从阻塞队列中获取并移除一个元素;
它们的区别在于如果阻塞队列是空的,那么调用 take() 方法的线程会被阻塞,而 poll() 方法会返回 null 值。
poll(long timeout, TimeUnit unit) 方法支持以超时的方式获取并移除阻塞队列头部的一个元素,如果等待了 timeout unit 时间,阻塞队列还是空的,那么该方法会返回 null 值。
当需要批量提交异步任务的时候建议你使用 CompletionService。CompletionService 将线程池 Executor 和阻塞队列 BlockingQueue 的功能融合在了一起,能够让批量异步任务的管理更简单。
除此之外,CompletionService 能够**让异步任务的执行结果有序化,先执行完的先进入阻塞队列,**利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待,同时还可以快速实现诸如 Forking Cluster 这样的需求。
1. 对于简单的并行任务,你可以通过“线程池 +Future”的方案来解决;
2. 如果任务之间有聚合关系,无论是 AND 聚合还是 OR 聚合,都可以通过 CompletableFuture 来解决;
3. 而批量的并行任务,则可以通过 CompletionService 来解决。
参考:
极客时间——Java并发编程实战