java并发编程(二)并发工具类

一,并发包种的锁

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 信号量模型

在这里插入图片描述

  1. init():设置计数器的初始值。
  2. down():计数器的值减 1;如果此时计数器的值小于 0,则当前线程将被阻塞,否则当前线程可以继续执行。
  3. 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 读写锁规则

读写锁都遵守以下三条基本原则:

  1. 允许多个线程同时读共享变量;
  2. 只允许一个线程写共享变量;
  3. 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
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;
在这里插入图片描述

  1. List
    list相关只有CopyOnWriteArrayList,写操作会复制当前数组操作复制数组,读操作遍历原数组;
    使用场景:写少读多且允许读写数据短暂不一致;
  2. Map
    ConcurrentHashMap: key无序
    ConcurrentSkipListMap:key有序
    ConcurrentSkipListMap:key里面的 SkipList 本身就是一种数据结构,翻译为“调表”,插入、删除、查询操作平均的时间复杂度是 O(log n);
    在这里插入图片描述
  3. set
    CopyOnWriteArraySet:场景功能类似CopyOnWriteArrayList
    ConcurrentSkipListSet:场景功能类似ConcurrentSkipListMap
  4. Queue
    Java 并发包里阻塞队列都用 Blocking 关键字标识,单端队列使用 Queue 标识,双端队列使用 Deque 标识。
    1. 单端阻塞队列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和 DelayQueue。
    2. 双端阻塞队列:其实现是 LinkedBlockingDeque。
    3. 单端非阻塞队列:其实现是 ConcurrentLinkedQueue。
    4. 双端非阻塞队列:其实现是 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中的线程池以及使用

  1. submit和execute的区别
    submit方法参数丰富,可以传参Runnable和Callable,支持返回值。
    execute不支持返回值,参数单一
    submit可以进行异常处理
  2. 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;
    }
}
  1. 注意事项
    当线程池中无可用线程,且阻塞队列已满,那么此时就会触发拒绝策略。对于采用何种策略,具体要看执行的任务重要程度。如果是一些不重要任务,可以选择直接丢弃。但是如果为重要任务,可以采用降级处理,例如将任务信息插入数据库或者消息队列,启用一个专门用作补偿的线程池去进行补偿。所谓降级就是在服务无法正常提供功能的情况下,采取的补救措施。具体采用何种降级手段,这也是要看具体场景。

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);
  1. 提交 Runnable 任务 submit(Runnable task) :这个方法的参数是一个 Runnable 接口,Runnable 接口的 run() 方法是没有返回值的,所以 submit(Runnable task) 这个方法返回的 Future 仅可以用来断言任务已经结束了,类似于 Thread.join()。
  2. 提交 Callable 任务 submit(Callable task):这个方法的参数是一个 Callable 接口,它只有一个 call() 方法,并且这个方法是有返回值的,所以这个方法返回的 Future 对象可以通过调用其 get() 方法来获取任务的执行结果。
  3. 提交 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的优势:

  1. 无需手工维护线程,没有繁琐的手工维护线程的工作,给任务分配线程的工作也不需要我们关注;
  2. 语义更清晰,例如 f3 = f1.thenCombine(f2, ()->{}) 能够清晰地表述“任务 3 要等待任务 1 和任务 2 都完成后才能开始”;
  3. 代码更简练并且专注于业务逻辑,几乎所有代码都是业务逻辑相关的。
    在这里插入图片描述
//任务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接口,此接口方法丰富,有大量描述任务时序关系的接口;
任务时序关系:串行关系、并行关系、汇聚关系等;
在这里插入图片描述

  1. 描述串行关系
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);
  1. 描述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并发编程实战

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值