Java并发工具类

并发编程领域,有两大核心问题:

  • 互斥:即同一时刻只允许一个线程访问共享资源;
  • 同步:即线程之间如何通信、协作。

这两大问题,管程都是能够解决的。
Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。

Lock

Java 语言本身提供的 synchronized 是管程的一种实现,既然 Java 从语言层面已经实现了管程了,那为什么还要在 SDK 里提供另外一种实现呢?

  • 解决死锁时,提出了一个破坏不可抢占条件方案,但是这个方案 synchronized 没有办法解决。原因是 synchronized
    申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。但我们希望的是:对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
    • 重新设计一把互斥锁去解决这个问题的三种方案

      1、能够响应中断。

      synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。

      2、支持超时。

      如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

      3、非阻塞地获取锁。

      如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

    • 三个方案就是“重复造轮子”的主要原因,体现在 API 上,就是 Lock 接口的三个方法 :

      支持中断的 API

      void lockInterruptibly()
      throws InterruptedException;

      支持超时的 API

      boolean tryLock(long time, TimeUnit unit)
      throws InterruptedException;

      支持非阻塞获取锁的 API

      boolean tryLock();

Lock 靠什么保证可见性?

它是利用了 volatile 相关的 Happens-Before 规则。Java SDK 里面的 ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值。

ReentrantLock 可重入锁,线程可以重复获取同一把锁。有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的公平策略,如果传入 true 就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。

  • 用锁的最佳实践

    1、永远只在更新对象的成员变量时加锁
    2、永远只在访问可变的成员变量时加锁
    3、永远不在调用其他对象的方法时加锁

Condition

synchronized 管程里只有一个条件变量,而 Lock&Condition 实现的管程是支持多个条件变量的,在很多并发场景下,支持多个条件变量能够让我们的并发程序可读性更好,实现起来也更容易。

Lock 和 Condition 实现的管程,线程等待和通知需要调用 await()、signal()、signalAll();

synchronized 实现的管程才能使用 wait()、notify()、notifyAll() ;

Semaphore(信号量)

Semaphore 可以允许多个线程访问一个临界区,当多个线程进入临界区时,如果需要访问共享变量就会存在并发问题,所以必须加锁,Semaphore 需要锁中锁。

信号量模型

信号量模型:一个计数器,一个等待队列,三个方法。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up(),三个方法都是原子性的,并且这个原子性是由信号量模型的实现方保证的。

在这里插入图片描述

1、init():设置计数器的初始值。
2、down():计数器的值减 1;如果此时计数器的值小于或者等于 0,则当前线程将被阻塞,否则当前线程可以继续执行。
3、up():计数器的值加 1;如果此时计数器的值大于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。

  • 在 Java SDK 并发包里,down() 和 up() 对应的则是 acquire() 和 release()。
  • 应用场景

    各种池化资源,例如连接池、对象池、线程池等等。
    init() 初始化指定数量的凭证,当多个线程同时访问,每个线程调用 acquire() 方法获取一个凭证,当凭证数量归零,获取不到凭证的线程将阻塞在等待队列中,持有凭证的线程调用 release() 归还凭证,同时唤醒一个阻塞队列中的线程,并将其移除阻塞队列。acquire() 与 release()成对使用

ReadWriteLock(读写锁接口)

读写锁类似于 ReentrantLock,也支持公平模式和非公平模式。读锁和写锁都实现了 java.util.concurrent.locks.Lock 接口,所以支持 lock() 方法、tryLock()
、lockInterruptibly() 等方法也都是支持的。但是有一点需要注意,那就是只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出
UnsupportedOperationException 异常。

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

1、允许多个线程同时读共享变量;
2、只允许一个线程写共享变量;
3、如果一个写线程正在执行写操作,此时禁止读线程读共享变量。

ReentrantReadWriteLock

  • 用 ReadWriteLock 快速实现一个通用的缓存工具类
class Cache<K, V> {
    final Map<K, V> m = new HashMap<>();
    final ReadWriteLock rwl = new ReentrantReadWriteLock();
    // 读锁
    final Lock r = rwl.readLock();
    // 写锁
    final Lock w = rwl.writeLock();


    V get(K key) {
        V v = null;
        // 读缓存
        r.lock();
        try {
            return m.get(key);
        } finally {
            r.unlock();
        }
        // 缓存中存在,返回
        if (v != null) {
            return v;
        }
        // 缓存中不存在,查询数据库
        w.lock();
        try {
            // 再次验证
            // 其他线程可能已经查询过数据库
            v = m.get(key);
            if (v == null) {
                // 查询数据库
//                v= 省略代码无数
                m.put(key, v);
            }
        } finally {
            w.unlock();
        }
        return v;
    }
}
  • ReentrantReadWriteLock 锁的升级是不允许的,但是锁的降级却是允许
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();
        }
    }
}

StampedLock

性能就比读写锁还要好。锁的升级会生成新的 stamp ,锁的申请和释放要成对出现

  • StampedLock 支持的三种锁模式
    分别是:写锁、悲观读锁和乐观读。其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。

    final StampedLock sl = 
      new StampedLock();
      
    // 获取 / 释放悲观读锁示意代码
    long stamp = sl.readLock();
    try {
      // 省略业务相关代码
    } finally {
      sl.unlockRead(stamp);
    }
     
    // 获取 / 释放写锁示意代码
    long stamp = sl.writeLock();
    try {
      // 省略业务相关代码
    } finally {
      sl.unlockWrite(stamp);
    }
    

StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是StampedLock 支持乐观读的方式。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。

class Point {
    private int x, y;
    final StampedLock sl = new StampedLock();

    // 计算到原点的距离  
    int distanceFromOrigin() {
        // 乐观读, tryOptimisticRead() 是无锁的
        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 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致CPU 飙升,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。 StampedLock 支持锁的降级(通过 tryConvertToReadLock() 方法实现)和升级(通过 tryConvertToWriteLock() 方法实现),但是建议慎重使用。

  • StampedLock 读模板:
final StampedLock sl = new StampedLock();
 
// 乐观读
long stamp = sl.tryOptimisticRead();
// 读入方法局部变量
......
// 校验 stamp
if (!sl.validate(stamp)){
  // 升级为悲观读锁
  stamp = sl.readLock();
  try {
    // 读入方法局部变量
    .....
  } finally {
    // 释放悲观读锁
    sl.unlockRead(stamp);
  }
}
// 使用方法局部变量执行业务操作
......
  • StampedLock 写模板:
long stamp = sl.writeLock();
try {
  // 写共享变量
  ......
} finally {
  sl.unlockWrite(stamp);
}

CountDownLatch、CyclicBarrier(线程同步工具类)

CountDownLatch 主要用来解决一个线程等待多个线程的场景,可以类比旅游团团长要等待所有的游客到齐才能去下一个景点;CountDownLatch 的计数器是不能循环利用的,也就是说一旦计数器减到 0,再有线程调用 await()
,该线程会直接通过。

CyclicBarrier 是一组线程之间互相等待,更像是几个驴友之间不离不弃。CyclicBarrier 的计数器是可以循环利用的,而且具备自动重置的功能,一旦计数器减到 0 会自动重置到你设置的初始值。CyclicBarrier还可以设置回调函数。

CountDownLatch

在这里插入图片描述

// 创建 2 个线程的线程池
Executor executor = Executors.newFixedThreadPool(2);
while(存在未对账订单){
  // 计数器初始化为 2
  CountDownLatch latch = new CountDownLatch(2);
  // 查询未对账订单
  executor.execute(()-> {
    pos = getPOrders();
    latch.countDown();
  });
  // 查询派送单
  executor.execute(()-> {
    dos = getDOrders();
    latch.countDown();
  });
  
  // 等待两个查询操作结束
  latch.await();
  
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);

CyclicBarrier

当看到回调函数的时候,一定问一问执行回调函数的线程是谁。

CyclicBarrier 是同步调用回调函数之后才唤醒等待的线程

优化,两次查询操作能够和对账操作并行,对账操作还依赖查询操作的结果。两次查询操作是生产者,对账操作是消费者。队列,来保存生产者生产的数据,而消费者则从这个队列消费数据。

设计了两个队列,并且两个队列的元素之间还有对应关系。好处是,对账操作可以每次从订单队列出一个元素,从派送单队列出一个元素,然后对这两个元素执行对账操作,这样数据一定不会乱掉。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4PzwVwkT-1664765031935)(pic/双队列示意图.png)]

线程 T1 和线程 T2 要互相等待,步调要一致;同时当线程 T1 和 T2 都生产完一条数据的时候,还要能够通知线程 T3 执行对账操作。
在这里插入图片描述

// 订单队列
Vector<P> pos;
// 派送单队列
Vector<D> dos;
// 执行回调的线程池 
Executor executor = Executors.newFixedThreadPool(1);
final CyclicBarrier barrier = new CyclicBarrier(2, ()->{
    executor.execute(()->check());
  });
  
void check(){
  P p = pos.remove(0);
  D d = dos.remove(0);
  // 执行对账操作
  diff = check(p, d);
  // 差异写入差异库
  save(diff);
}
  
void checkAll(){
  // 循环查询订单库;线程 T1 负责查询订单,当查出一条时,调用 barrier.await() 来将计数器减 1,同时等待计数器变成 0;
  Thread T1 = new Thread(()->{
    while(存在未对账订单){
      // 查询订单库
      pos.add(getPOrders());
      // 等待
      barrier.await();
    }
  });
  T1.start();  
  // 循环查询运单库;线程 T2 负责查询派送单,当查出一条时,也调用 barrier.await() 来将计数器减 1,同时等待计数器变成 0;
  Thread T2 = new Thread(()->{
    while(存在未对账订单){
      // 查询运单库
      dos.add(getDOrders());
      // 等待
      barrier.await();
    }
  });
  T2.start();
}
//当 T1 和 T2 都调用 barrier.await() 的时候,计数器会减到 0,此时 T1 和 T2 就可以执行下一条语句了,同时会调用 barrier 的回调函数来执行对账操作。

并发容器

  • 如何将非线程安全的容器变成线程安全的容器?
    把非线程安全的容器封装在对象内部,然后控制好访问路径就可以了。

    List list = Collections.synchronizedList(new ArrayList());
    Set set = Collections.synchronizedSet(new HashSet());
    Map map = Collections.synchronizedMap(new HashMap());

经过包装后线程安全容器,都是基于 synchronized 这个同步关键字实现的,所以也被称为同步容器。Java 提供的同步容器还有 Vector、Stack 和 Hashtable,这三个容器不是基于包装类实现的,但同样是基于
synchronized 实现的,对这三个容器的遍历,同样要加锁保证互斥。

在这里插入图片描述

List

在这里插入图片描述

CopyOnWriteArrayList,写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。

CopyOnWriteArrayList 内部维护了一个数组,成员变量 array 就指向这个内部数组,所有的读操作都是基于 array 进行的,迭代器 Iterator 遍历的就是 array 数组。

如果在遍历 array 的同时,还有一个写操作,CopyOnWriteArrayList 会将 array 复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将 array 指向这个新的数组。
读写是可以并行的,遍历操作一直都是基于原 array 执行,而写操作则是基于新 array 进行。

CopyOnWriteArrayList 需要注意的“坑”:

1、仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。
2、CopyOnWriteArrayList 迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。

Map

ConcurrentHashMap 的 key 是无序的
ConcurrentSkipListMap 的 key 是有序的

ConcurrentSkipListMap 里面的 SkipList 本身就是“跳表”。跳表插入、删除、查询操作平均的时间复杂度是 O(log n),理论上和并发线程数没有关系,所以在并发度非常高的情况下,若对
ConcurrentHashMap 的性能还不满意,可以尝试一下 ConcurrentSkipListMap。

在这里插入图片描述

Set

Set 接口的两个实现是 CopyOnWriteArraySet 和 ConcurrentSkipListSet

Queue

Java 并发包里阻塞队列都用 Blocking 标识,单端队列使用 Queue 标识,双端队列使用 Deque 标识。

阻塞与非阻塞,所谓阻塞指的是当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞。
单端与双端,单端指的是只能队尾入队,队首出队;而双端指的是队首队尾皆可入队出队。

使用队列时,需要格外注意队列是否支持有界。只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的,所以在使用其他无界队列时,一定要充分考虑是否存在导致 OOM 的隐患。

  • 单端阻塞队列
    ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和
    DelayQueue。内部一般会持有一个队列,这个队列可以是数组(其实现是 ArrayBlockingQueue)也可以是链表(其实现是 LinkedBlockingQueue);甚至还可以不持有队列(其实现是
    SynchronousQueue),此时生产者线程的入队操作必须等待消费者线程的出队操作。而 LinkedTransferQueue 融合 LinkedBlockingQueue 和 SynchronousQueue 的功能,性能比
    LinkedBlockingQueue 更好;PriorityBlockingQueue 支持按照优先级出队;DelayQueue 支持延时出队。

    在这里插入图片描述

  • 双端阻塞队列 其实现是 LinkedBlockingDeque

    在这里插入图片描述

  • 单端非阻塞队列:其实现是 ConcurrentLinkedQueue。

  • 双端非阻塞队列:其实现是 ConcurrentLinkedDeque。

原子类

无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。

无锁方案的实现原理

CPU 为了解决并发问题,提供了 CAS 指令(CAS,全称是 Compare And Swap,即“比较并交换”)。CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地址 A处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。
作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。
使用 CAS 来解决并发问题,一般都会伴随着自旋,循环尝试。在 CAS 方案中,有一个ABA的问题(加递增版本号解决)。

  • CAS 使用的经典范例
do {
  // 获取当前值
  oldV = xxxx;
  // 根据当前值计算新值
  newV = ...oldV...
}while(!compareAndSet(oldV,newV);

原子类概览

在这里插入图片描述

  1. 原子化的基本数据类型
    相关实现有 AtomicBoolean、AtomicInteger 和 AtomicLong

    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,利用它们可以实现对象引用的原子化更新。对象引用的更新需要重点关注 ABA
    问题,AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题。

  3. 原子化数组
    相关实现有 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,利用这些原子类,可以原子化地更新数组里面的每一个元素。

  4. 原子化对象属性更新器
    相关实现有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和
    AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性,这三个方法都是利用反射机制实现的,创建更新器的方法如下:

    public static <U> AtomicXXXFieldUpdater<U> newUpdater(Class<U> tclass,String fieldName)
    

    对象属性必须是 volatile 类型的,只有这样才能保证可见性;如果对象属性不是 volatile 类型的,newUpdater() 方法会抛出 IllegalArgumentException 这个运行时异常。

  5. 原子化的累加器
    DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。

Executor与线程池

共享线程池:有福同享就要有难同当;大多数更倾向于使用隔离的方案

Java 中创建对象,仅仅是在 JVM 的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,线程是一个重量级的对象,应该避免频繁创建和销毁。

一般意义上的池化资源,当需要资源的时候就调用 acquire() 方法来申请资源,用完之后就调用 release() 释放资源。

线程池

一种生产者 - 消费者模式,线程池的使用方是生产者,线程池本身是消费者。

// 简化的线程池,仅用来说明工作原理
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){ 
          //从队列头部移除元素,队列 null,该操作阻塞 
        Runnable task = workQueue.take();
        task.run();
      } 
    }
  }  
}

    /** 下面是使用示例 **/
// 创建有界阻塞队列
    BlockingQueue<Runnable> workQueue =
            new LinkedBlockingQueue<>(2);
    // 创建线程池  
    MyThreadPool pool = new MyThreadPool(
            10, workQueue);
// 提交任务  
pool.execute(()->{
        System.out.println("hello");
        });

Java 中的线程池

Java 提供的线程池相关的工具类中,最核心的是ThreadPoolExecutor,它强调的是 Executor,而不是一般意义上的池化资源。

ThreadPoolExecutor 的构造函数:

//ThreadPoolExecutor 的构造函数
ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler) 
  • corePoolSize:表示线程池保有的最小线程数。
  • maximumPoolSize:表示线程池创建的最大线程数。
  • keepAliveTime & unit:用来定义这个“一段时间”的参数。如果一个线程空闲了“一段时间”,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
  • workQueue:工作队列(慎用无界队列)
  • threadFactory:自定义如何创建线程,例如给线程指定一个有意义的名字。
  • handler:自定义任务的拒绝策略(慎用默认拒绝策略

1、AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
2、DiscardPolicy:直接丢弃任务,没有任何异常抛出。
3、CallerRunsPolicy:提交任务的线程自己去执行该任务。
4、DiscardOldestPolicy:丢弃最老的任务,把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

  • Java 在 1.6 版本还增加了 allowCoreThreadTimeOut(boolean value) 方法,它可以让所有线程都支持超时,这意味着如果很闲,就会将所有线程移除。

线程池要注意点

  • 使用有界队列
    ThreadPoolExecutor 的构造函数实在是有些复杂,所以 Java 并发包里提供了一个线程池的静态工厂类 Executors,利用 Executors 可以快速创建线程池。 但是 Executors
    提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。

  • 默认拒绝策略要慎重使用
    使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。

  • 异常处理(捕获所有异常并按需处理)
    通过 ThreadPoolExecutor 对象的 execute() 方法提交任务时,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止;不过,最致命的是任务虽然异常了,但是你却获取不到任何通知,这会让你误以为任务都执行得很正常。最稳妥和简单的方案还是捕获所有异常并按需处理。

      try {
      // 业务逻辑
      } catch (RuntimeException x) {
      // 按需处理
      } catch (Throwable x) {
      // 按需处理
      }
    

Future(获取任务执行结果)

Java 通过 ThreadPoolExecutor 提供的 3 个 submit() 方法和 1 个 FutureTask 工具类来支持获得任务执行结果的需求。

  • 3 个 submit()
// 提交 Runnable 任务
Future<?> submit(Runnable task);
// 提交 Callable 任务
<T> Future<T> submit(Callable<T> task);
// 提交 Runnable 任务及结果引用  
<T> Future<T> submit(Runnable task, T result);
  • Future 接口有 5 个方法:
    • 取消任务的方法 cancel()、判断任务是否已取消的方法 isCancelled()、判断任务是否已结束的方法 isDone()
    • 获得任务执行结果的 get() 和 get(timeout, unit),这两个方法都是阻塞式的
// 取消任务
boolean cancel(boolean mayInterruptIfRunning);
// 判断任务是否已取消  
boolean isCancelled();
// 判断任务是否已结束
boolean isDone();
// 获得任务执行结果
get();
// 获得任务执行结果,支持超时
get(long timeout, TimeUnit unit);

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);
  }
}
  • FutureTask 工具类
    FutureTask 实现了 Runnable 和 Future 接口,可以直接被 Thread 执行,也能用来获得任务的执行结果。

    构造函数:

FutureTask(Callable<V> callable);
FutureTask(Runnable runnable, V result);

在这里插入图片描述

// 创建任务 T2 的 FutureTask
FutureTask<String> ft2
  = new FutureTask<>(new T2Task());
// 创建任务 T1 的 FutureTask
FutureTask<String> ft1
  = new FutureTask<>(new T1Task(ft2));
// 线程 T1 执行任务 ft1
Thread T1 = new Thread(ft1);
T1.start();
// 线程 T2 执行任务 ft2
Thread T2 = new Thread(ft2);
T2.start();
// 等待线程 T1 执行结果
System.out.println(ft1.get());

// T1Task 需要执行的任务:
// 洗水壶、烧开水、泡茶
class T1Task implements Callable<String>{
  FutureTask<String> ft2;
  // T1 任务需要 T2 任务的 FutureTask
  T1Task(FutureTask<String> ft2){
    this.ft2 = ft2;
  }
  @Override
  String call() throws Exception {
    System.out.println("T1: 洗水壶...");
    TimeUnit.SECONDS.sleep(1);
    
    System.out.println("T1: 烧开水...");
    TimeUnit.SECONDS.sleep(15);
    // 获取 T2 线程的茶叶  
    String tf = ft2.get();
    System.out.println("T1: 拿到茶叶:"+tf);
 
    System.out.println("T1: 泡茶...");
    return " 上茶:" + tf;
  }
}


// T2Task 需要执行的任务:
// 洗茶壶、洗茶杯、拿茶叶
class T2Task implements Callable<String> {
  @Override
  String call() throws Exception {
    System.out.println("T2: 洗茶壶...");
    TimeUnit.SECONDS.sleep(1);
 
    System.out.println("T2: 洗茶杯...");
    TimeUnit.SECONDS.sleep(2);
 
    System.out.println("T2: 拿茶叶...");
    TimeUnit.SECONDS.sleep(1);
    return " 龙井 ";
  }
}

利用 Java 并发包提供的 Future 可以很容易获得异步任务的执行结果,无论异步任务是通过线程池 ThreadPoolExecutor
执行的,还是通过手工创建子线程来执行的。利用多线程可以快速将一些串行的任务并行化,从而提高性能;如果任务之间有依赖关系,比如当前任务依赖前一个任务的执行结果,这种问题基本上都可以用 Future 来解决。

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: 泡茶...
上茶: 龙井

创建 CompletableFuture 对象

创建 CompletableFuture 对象主要靠 4 个静态方法:

// 使用默认线程池
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 接口的 run() 方法没有返回值;
//Supplier 接口的 get() 方法是有返回值的 

默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option:
-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置ForkJoinPool 线程池的线程数)。如果所有 CompletableFuture共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议根据不同的业务类型创建不同的线程池,以避免互相干扰。

创建完 CompletableFuture 对象之后,会自动地异步执行 runnable.run() 方法或者 supplier.get()方法,对于一个异步操作,需要关注两个问题:一个是异步操作什么时候结束,另一个是如何获取异步操作的执行结果。CompletableFuture 类实现了 Future 接口,所以这两个问题都可以通过 Future接口来解决。另外,CompletableFuture 类还实现了 CompletionStage 接口。

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

    方法里参数 fn 的类型是接口 Function<T, R>,这个接口里与 CompletionStage 相关的方法是 R apply(T t),这个方法既能接收参数也支持返回值,所以 thenApply 系列方法返回的是CompletionStage。

    thenAccept

    方法里参数 consumer 的类型是接口Consumer,这个接口里与 CompletionStage 相关的方法是 void accept(T t),这个方法虽然支持参数,但却不支持返回值,所以 thenAccept 系列方法返回的是CompletionStage。

    thenRun

    方法里参数 action 的类型是接口 Runnable,所以 action 既不能接收参数也不支持返回值,所以 thenRun 系列方法返回的也是CompletionStage。

    thenCompose

    这个系列的方法会新创建出一个子流程,最终结果和 thenApply 系列是相同的。

    这些方法里面 Async 代表的是异步执行 fn、consumer 或者 action。

    //先通过 supplyAsync() 启动一个异步流程,任务①②③却是串行执行的,②依赖①的执行结果,③依赖②的执行结果。
    CompletableFuture<String> f0 = 
      CompletableFuture.supplyAsync(
        () -> "Hello World")      //①
      .thenApply(s -> s + " QQ")  //②
      .thenApply(String::toUpperCase);//③
     
    System.out.println(f0.join());
    // 输出结果
    HELLO WORLD QQ
    
  2. 描述 AND 汇聚关系
    CompletionStage 接口里面描述 AND 汇聚关系,主要是 thenCombine、thenAcceptBoth 和 runAfterBoth 系列的接口,这些接口的区别也是源自 fn、consumer、action
    这三个核心参数不同。

     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);
    
  3. 描述 OR 汇聚关系
    CompletionStage 接口里面描述 OR 汇聚关系,主要是 applyToEither、acceptEither 和 runAfterEither 系列的接口,这些接口的区别也是源自 fn、consumer、action
    这三个核心参数不同。

     CompletionStage applyToEither(other, fn);
     CompletionStage applyToEitherAsync(other, fn);
     CompletionStage acceptEither(other, consumer);
     CompletionStage acceptEitherAsync(other, consumer);
     CompletionStage runAfterEither(other, action);
     CompletionStage runAfterEitherAsync(other, action);
    
  4. 异常处理

    CompletionStage exceptionally(fn);
    CompletionStage<R> whenComplete(consumer);
    CompletionStage<R> whenCompleteAsync(consumer);
    CompletionStage<R> handle(fn);
    CompletionStage<R> handleAsync(fn);
    

    exceptionally() 的使用非常类似于 try{}catch{}中的 catch{},whenComplete() 和 handle() 系列方法就类似于 try{}finally{}中的
    finally{},无论是否发生异常都会执行 whenComplete() 中的回调函数 consumer 和 handle() 中的回调函数 fn。whenComplete() 和 handle() 的区别在于whenComplete() 不支持返回结果,而 handle() 是支持返回结果的。

CompletionService

CompletionService 的实现原理也是内部维护了一个阻塞队列,当任务执行结束就把任务的执行结果加入到阻塞队列中,不同的是 CompletionService 是把任务执行结果的 Future 对象加入到阻塞队列中。

  • CompletionService 接口的实现类是 ExecutorCompletionService,这个实现类的构造方法有两个,分别是:
    ExecutorCompletionService(Executor executor);
    ExecutorCompletionService(Executor executor, BlockingQueue<Future> completionQueue);


    两个构造方法都需要传入一个线程池,如果不指定 completionQueue,那么默认会使用无界的 LinkedBlockingQueue。任务执行结果的 Future 对象就是加入到 completionQueue 中。通过CompletionService 接口提供的 take() 方法获取一个 Future 对象,调用 Future 对象的 get() 方法就能返回操作的执行结果了。
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 创建 CompletionService
CompletionService<Integer> cs = new ExecutorCompletionService<>(executor);
// 异步向电商 S1 询价
cs.submit(()->getPriceByS1());
// 异步向电商 S2 询价
cs.submit(()->getPriceByS2());
// 异步向电商 S3 询价
cs.submit(()->getPriceByS3());
// 将询价结果异步保存到数据库
for (int i=0; i<3; i++) {
  Integer r = cs.take().get();
  executor.execute(()->save(r));
}
  • CompletionService 接口提供的方法
//提交任务  Callable  call() 方法获取结果
Future<V> submit(Callable<V> task);
//提交任务  result  结果
Future<V> submit(Runnable task, V result);

// 从阻塞队列中获取并移除一个元素,如果阻塞队列是空的,线程会被阻塞
Future<V> take() 
  throws InterruptedException;
  
// 从阻塞队列中获取并移除一个元素,如果阻塞队列是空的,返回 null 值
Future<V> poll();

// 支持以超时的方式获取并移除阻塞队列头部的一个元素,如果等待了 timeout unit 时间,阻塞队列还是空的,那么该方法会返回 null 值。
Future<V> poll(long timeout, TimeUnit unit) 
  throws InterruptedException;
  • 利用 CompletionService 实现 Dubbo 中的 Forking Cluster
    Dubbo 中有一种叫做Forking 的集群模式,这种集群模式下,支持并行地调用多个查询服务,只要有一个成功返回结果,整个服务就可以返回了。
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 创建 CompletionService
CompletionService<Integer> cs = new ExecutorCompletionService<>(executor);
// 用于保存 Future 对象
List<Future<Integer>> futures = new ArrayList<>(3);
// 提交异步任务,并保存 future 到 futures 
futures.add(cs.submit(()->geocoderByS1()));
futures.add(cs.submit(()->geocoderByS2()));
futures.add(cs.submit(()->geocoderByS3()));
// 获取最快返回的任务执行结果
Integer r = 0;
try {
  // 只要有一个成功返回,则 break
  for (int i = 0; i < 3; ++i) {
    r = cs.take().get();
    // 简单地通过判空来检查是否成功返回
    if (r != null) {
      break;
    }
  }
} finally {
  // 取消所有任务
  for(Future<Integer> f : futures)
    f.cancel(true);
}
// 返回结果
return r;

当需要批量提交异步任务的时候建议使用 CompletionService。CompletionService 将线程池 Executor 和阻塞队列 BlockingQueue
的功能融合在了一起,能够让批量异步任务的管理更简单。除此之外,CompletionService 能够让异步任务的执行结果有序化,先执行完的先进入阻塞队列,利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待,同时还可以快速实现诸如 Forking Cluster 这样的需求。

Fork/Join(并行计算框架)

对于简单的并行任务,可以通过“线程池 +Future”的方案来解决;
如果任务之间有聚合关系,无论是 AND 聚合还是 OR 聚合,都可以通过 CompletableFuture 来解决;
批量地并行任务,则可以通过 CompletionService 来解决。

并发编程可以分为三个层面的问题,分别是分工、协作和互斥

分治,指的是把一个复杂的问题分解成多个相似的子问题,然后再把子问题分解成更小的子问题,直到子问题简单到可以直接求解。

Fork/Join 的使用

Fork/Join 是一个并行计算的框架,主要就是用来支持分治任务模型的,这个计算框架里的Fork 对应的是分治任务模型里的任务分解,Join 对应的是结果合并。Fork/Join 计算框架主要包含两部分,一部分是分治任务的线程池ForkJoinPool,另一部分是分治任务 ForkJoinTask。

ForkJoinTask 是一个抽象类,它的方法有很多,最核心的是 fork() 方法和 join() 方法,其中 fork() 方法会异步地执行一个子任务,而 join()
方法则会阻塞当前线程来等待子任务的执行结果。ForkJoinTask 有两个子类RecursiveAction 和 RecursiveTask,它们都是用递归的方式来处理分治任务的。这两个子类都定义了抽象方法 compute(),不过区别是RecursiveAction 定义的 compute() 没有返回值,而 RecursiveTask 定义的 compute() 方法是有返回值的。这两个子类也是抽象类,在使用的时候,需要定义子类去扩展。


static void main(String[] args){
  // 创建分治任务线程池  
  ForkJoinPool fjp = new ForkJoinPool(4);
  // 创建分治任务
  Fibonacci fib = new Fibonacci(30);   
  // 启动分治任务  
  Integer result = fjp.invoke(fib);
  // 输出结果  
  System.out.println(result);
}

// 递归任务
static class Fibonacci extends RecursiveTask<Integer>{
  final int n;
  Fibonacci(int n){this.n = n;}
  protected Integer compute(){
    if (n <= 1)
      return n;
    Fibonacci f1 = new Fibonacci(n - 1);
    // 创建子任务  
    f1.fork();
    Fibonacci f2 = new Fibonacci(n - 2);
    // 等待子任务结果,并合并结果  
    return f2.compute() + f1.join();
  }
}

在这里插入图片描述

ForkJoinPool 内部有多个任务队列,当通过 ForkJoinPool 的 invoke() 或者 submit() 方法提交任务时,ForkJoinPool根据一定的路由规则把任务提交到一个任务队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的任务队列中。ForkJoinPool支持一种叫做“任务窃取”的机制,如果工作线程空闲了,那它可以“窃取”其他工作任务队列里的任务,ForkJoinPool中的任务队列采用的是双端队列,工作线程正常获取任务和“窃取任务”分别是从任务队列不同的端消费,这样能避免很多不必要的数据竞争。

//统计一个文件里面每个单词的数量
//用一个字符串数组 String[] fc 来模拟文件内容,fc 里面的元素与文件里面的行数据一一对应。关键的代码在 compute() 这个方法里面,这是一个递归方法,前半部分数据 fork 一个递归任务去处理(关键代码 mr1.fork()),后半部分数据则在当前任务中递归处理(mr2.compute())。
static void main(String[] args){
  String[] fc = {"hello world",
          "hello me",
          "hello fork",
          "hello join",
          "fork join in world"};
  // 创建 ForkJoin 线程池    
  ForkJoinPool fjp = new ForkJoinPool(3);
  // 创建任务    
  MR mr = new MR(fc, 0, fc.length);  
  // 启动任务    
  Map<String, Long> result = fjp.invoke(mr);
  // 输出结果    
  result.forEach((k, v)->System.out.println(k+":"+v));
}
//MR 模拟类
static class MR extends RecursiveTask<Map<String, Long>> {
  private String[] fc;
  private int start, end;
  // 构造函数
  MR(String[] fc, int fr, int to){
    this.fc = fc;
    this.start = fr;
    this.end = to;
  }
  @Override 
  protected Map<String, Long> compute(){
    if (end - start == 1) {
      return calc(fc[start]);
    } else {
      int mid = (start+end)/2;
      MR mr1 = new MR(fc, start, mid);
      mr1.fork();
      MR mr2 = new MR(fc, mid, end);
      // 计算子任务,并返回合并的结果    
      return merge(mr2.compute(),mr1.join());
    }
  }
  // 合并结果
  private Map<String, Long> merge(
      Map<String, Long> r1, 
      Map<String, Long> r2) {
    Map<String, Long> result = new HashMap<>();
    result.putAll(r1);
    // 合并结果
    r2.forEach((k, v) -> {
      Long c = result.get(k);
      if (c != null)
        result.put(k, c+v);
      else 
        result.put(k, v);
    });
    return result;
  }
  // 统计单词数量
  private Map<String, Long> calc(String line) {
    Map<String, Long> result = new HashMap<>();
    // 分割单词    
    String [] words = line.split("\\s+");
    // 统计单词数量    
    for (String w : words) {
      Long v = result.get(w);
      if (v != null) 
        result.put(w, v+1);
      else
        result.put(w, 1L);
    }
    return result;
  }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值