Java常用并发工具类

同步工具类存在的意义

管程或者信号量可以解决所有的并发问题,那么同步工具类存在的意义是什么呢?
两个字:方便。
针对不同的并发场景,使用对应的工具类可以快速完成业务开发。

1. 锁工具

1.1 ReadWriteLock

  • 读锁
  • 写锁

1.1.1 使用场景

  1. 允许多个线程同时读共享变量

  2. 只允许一个线程写共享变量

  3. 如果一个线程正在执行写操作,此时禁止读线程读共享变量

其实就是读锁共享锁,写锁互斥锁。除了同时读以外,其余场景均阻塞。

×
× ×

1.1.2 例子

class Cache<K, V> {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock writeLock = lock.writeLock();
    private final Lock readLock = lock.readLock();
    private final Map<K, V> cache = new HashMap<>();

    // 只有1个线程可以修改值
    public void put(K key, V val) {
        writeLock.lock();
        try {
            cache.put(key, val);
        } finally {
            writeLock.unlock();
        }
    }

    // 多个线程可同时获取值
    public V get(K key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }
}

1.1.3 读写锁的降级

ReadWriteLock不支持读锁升级为写锁
锁的降级指的是写锁降级为读锁,具体操作是在写锁还未释放时就申请读锁。
锁降级预防是当前线程刚写完数据,还没进行数据处理,便被其他线程再次修改数据且不感知的情况。
锁降级例子如下, 假设Cache作为旁路缓存使用,采用数据懒加载的模式。

class Cache<K, V> {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock writeLock = lock.writeLock();
    private final Lock readLock = lock.readLock();
    private final Map<K, V> cache = new HashMap<>();

    public V get(K key) {
        V val;
        readLock.lock();
        try {
            val = cache.get(key);
        } finally {
            readLock.unlock();
        }
        if (val != null) {
            return val;
        }
        writeLock.lock();
        val = cache.get(key);
        if (val == null) {  // 二次检查,避免多个线程都阻塞在写锁且再次进行数据库读取
            try {
                // 从数据库获取数据data, 假设有值
                cache.put(key, val);
                // 释放写锁前获取读锁,锁降级
                readLock.lock();
            } finally {
                writeLock.unlock();
            }
        }
        try {
            // 使用数据val
        } finally {
            readLock.unlock(); // 完全释放锁,此时其他线程可以修改缓存值
        }
        return val;
    }
}

1.2. StampedLock

  • 写锁
  • 读锁
  • 乐观读

1.2.1 使用场景

  • 写锁和读锁除了新增变量long stamp外,语义类似ReadWriteLock
  • 重要的是,StampedLock的乐观读,允许一个线程获取写锁。故性能比ReadWriteLock更好。
    乐观读tryOptimisticRead()需要配合validate(long stamp)验证乐观读到验证期间的这批数据(比如可以同时读多个变量)是否改变。
    有点类似于MySQL的MVCC,重点是保证读的那批数据是保证一致性的,验证成功后便可继续处理,不用管处理期间值的变化。但若是乐观读期间数据变化了,便需要加乐观读锁。

1.2.2 使用例子

  • 读模板
final StampedLock sl = new StampedLock();

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

1.2.3 锁的降级与升级

StampedLock 支持锁的降级(tryConvertToReadLock()方法)和升级(tryConvertToWriteLock()方法)

1.3. CountDownLatch

假如泡茶有三个步骤:

  1. 烧水1min
  2. 洗杯子2min
  3. 泡茶1min

可以发现只有泡茶依赖烧水和洗杯子,而烧水和洗杯子之间毫无关联。
假如串行地泡茶,那么需要4分钟。如果可以一个人烧水,一个人洗杯子,那么只要3分钟。

引入线程池的话,若是仅利用管程实现需要加入计数器,且为了计数器的并发安全,要么引入原子类主线程while循环等待,要么加锁实际成了串行,都不是个好方案。利用CountDownLatch可以很方便完成。

@Setter
public class Test {

    private CountDownLatch downLatch;

    public void water() {
        try {
            System.out.println(LocalDateTime.now() + " 开始烧水");
            Thread.sleep(1000);
            System.out.println(LocalDateTime.now() + " 结束烧水");
            downLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void cup() {
        try {
            System.out.println(LocalDateTime.now() + " 开始洗杯子");
            Thread.sleep(2000);
            System.out.println(LocalDateTime.now() + " 结束洗杯子");
            downLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void tea() {
        try {
            System.out.println(LocalDateTime.now() + " 等待泡茶");
            downLatch.await();  // 等待烧水和洗杯子结束
            System.out.println(LocalDateTime.now() + " 结束泡茶\n" );
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        ExecutorService executors = Executors.newFixedThreadPool(2);
        //for (int i = 0; i < 4; i++) {  //泡茶4批次注释
            test.setDownLatch(new CountDownLatch(2));
            executors.submit(test::water);
            executors.submit(test::cup);
            test.tea();
        //}
    }
}

打印如下

2023-12-01T00:31:07.440 等待泡茶
2023-12-01T00:31:07.440 开始洗杯子
2023-12-01T00:31:07.440 开始烧水
2023-12-01T00:31:08.445 结束烧水
2023-12-01T00:31:09.445 结束洗杯子
2023-12-01T00:31:10.446 结束泡茶

1.4. CyclicBarrier

假如我们现在需要泡4批次的茶,即将CountDownLatch中注释的for循环启用。
其实稍微想想就可以发现泡茶期间的时间被浪费了,因为完全可以直接进行第2批次的烧水和洗杯子,如下图所示。

image

那么为了泡茶不阻塞,所以需要将泡茶也丢到子线程中执行,并且需要维持烧水/洗杯子这两步和泡茶间的依赖关系。

CyclicBarrier很轻松就能解决这个并发场景。

  • CyclicBarrier除了设置同步次数以外,还能设置次数为0时的回调函数。

  • CyclicBarrier会自动重置同步次数。

@Setter
public class Test {

    private CyclicBarrier cyclicBarrier;

    public void water() {
        try {
            System.out.println(LocalDateTime.now() + " 开始烧水");
            Thread.sleep(1000);
            System.out.println(LocalDateTime.now() + " 结束烧水");
            cyclicBarrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

    public void cup() {
        try {
            System.out.println(LocalDateTime.now() + " 开始洗杯子");
            Thread.sleep(2000);
            System.out.println(LocalDateTime.now() + " 结束洗杯子");
            cyclicBarrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

    public void tea() {
        try {
            System.out.println(LocalDateTime.now() + " 等待泡茶");
            Thread.sleep(1000);
            System.out.println(LocalDateTime.now() + " 结束泡茶" );
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService teaExecutor = Executors.newFixedThreadPool(1); // 泡茶线程池
        ExecutorService executors = Executors.newFixedThreadPool(2); // 烧水/洗杯子
        Test test = new Test();
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> teaExecutor.execute(test::tea)); // 回调函数默认使用主线程,利用线程池异步执行
        test.setCyclicBarrier(cyclicBarrier);
        for (int i = 0; i < 4; i++) {
            executors.submit(test::water);
            executors.submit(test::cup);
        }
    }
}

打印如下, 可以看到泡茶4批次用时为9s,和预期符合。

2023-12-01T01:23:47.279 开始烧水
2023-12-01T01:23:47.279 开始洗杯子
2023-12-01T01:23:48.284 结束烧水
2023-12-01T01:23:49.284 结束洗杯子
2023-12-01T01:23:49.287 等待泡茶
2023-12-01T01:23:49.287 开始烧水
2023-12-01T01:23:49.287 开始洗杯子
2023-12-01T01:23:50.287 结束泡茶
2023-12-01T01:23:50.289 结束烧水
2023-12-01T01:23:51.288 结束洗杯子
2023-12-01T01:23:51.288 开始烧水
2023-12-01T01:23:51.288 开始洗杯子
2023-12-01T01:23:51.288 等待泡茶
2023-12-01T01:23:52.293 结束泡茶
2023-12-01T01:23:52.293 结束烧水
2023-12-01T01:23:53.293 结束洗杯子
2023-12-01T01:23:53.293 开始烧水
2023-12-01T01:23:53.293 等待泡茶
2023-12-01T01:23:53.293 开始洗杯子
2023-12-01T01:23:54.300 结束烧水
2023-12-01T01:23:54.300 结束泡茶
2023-12-01T01:23:55.298 结束洗杯子
2023-12-01T01:23:55.299 等待泡茶
2023-12-01T01:23:56.304 结束泡茶

2. 容器工具

2.1 同步容器(Synchronized Collections)

同步容器性能差,因为就是管程的基础实现(一个共享变量,访问入口直接加锁)。
同步容器都是在public接口上加个synchronized实现,串行执行,并发度低。

举例一些常见的同步容器。

  • List
    • java.util.Vector
    • java.util.Stack
    • Java.util.Collections.synchronizedList(List list)
  • Set
    • Java.util.Collections.synchronizedSet(Set s)
  • Map
    • java.util.HashTable
    • java.util.Collections.synchronizeMap(Map<K,V> m)

2.2 并发容器(Concurrent Collections)

Jdk1.5引入了java.util.concurrent包,专为并发场景设计。

2.2.1 List

2.2.1.1 CopyOnWriteArrayList

List中唯一的并发容器类,看名字可知写时复制。这套思想在redis执行bgsave也有使用。

image

2.2.2 Map

2.2.2.1 ConcurrentHashMap

  • Jdk1.7
    Jdk1.7使用分段锁机制,结构:Segment + HashEntry ,加锁:ReentrantLock。
    每段(Segment )通过继承 ReentrantLock 来进行加锁,其实就类似于一个HashTable。通过二次映射的分段式加锁,来提升并发度。
    并发级别(concurrencyLevel)默认16,其实就是有16个段,最多可以支持16个并发写。这个值只能在初始化时指定。

  • Jdk1.8
    结构同HashMap类似:数组 + 链表 + 红黑树,加锁:CAS + volatile + synchronized

    • 去掉了分段锁机制,使用一个包含键值对的Node数组来存储信息。锁粒度降低到了首节点。
    • 解决哈希冲突的链表过长时会转为红黑树,提供O(log n)时间复杂度查询
    • 通过CAS操作进行无锁扩容,使用volatile实现扩容可见性
      transient volatile Node<K,V>[] table;
      
    • 读操作全程不需要加锁,仅利用volatile实现可见性;利用CAS操作进行增删改,故不加锁也不存在两个线程同时读取写入的竞态条件
      static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        //可以看到这些都用了volatile修饰
        volatile V val;
        volatile Node<K,V> next;
      
      get源码如下。
      1. 首先计算hash值,定位到该table索引位置,如果是首节点符合就返回
      2. 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
      3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
        public V get(Object key) {
        	Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        	int h = spread(key.hashCode()); //计算hash
        	if ((tab = table) != null && (n = tab.length) > 0 &&
        		(e = tabAt(tab, (n - 1) & h)) != null) {//读取首节点的Node元素
        		if ((eh = e.hash) == h) { //如果该节点就是首节点就返回
        			if ((ek = e.key) == key || (ek != null && key.equals(ek)))
        				return e.val;
        		}
        		//hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
        		//eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
        		//eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
        		//eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
        		else if (eh < 0)
        			return (p = e.find(h, key)) != null ? p.val : null;
        		while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
        			if (e.hash == h &&
        				((ek = e.key) == key || (ek != null && key.equals(ek))))
        				return e.val;
        		}
        	}
        	return null;
        }
      

2.2.2.2 ConcurrentSkipListMap

跳表,redis中sorted set类型底层实现也有使用。时间复杂度O(log n)。
ConcurrentSkipListMap不仅线程安全,键值对也是有序的。查询效率相对ConcurrentHashMap低点。

2.2.3 Set

2.2.3.1 CopyOnWriteArraySet

原理同CopyOnWriteArrayList。

2.2.3.1 ConcurrentSkipListSet

原理同ConcurrentSkipListMap。

2.2.4 Queue

  • 阻塞队列都用 Blocking 关键字标识
  • 单端队列(队尾入队,队首出队)使用 Queue 标识
  • 双端队列使用 Deque 标识。Deque接口/类都继承自Queue接口/类。

2.2.4.1 单端阻塞队列

  • ArrayBlockingQueue
    内部数组实现
  • LinkedBlockingQueue
    内部链表实现
  • SynchronousQueue
    无队列,生产者线程的入队操作必须等待消费者线程的出队操作
  • LinkedTransferQueue
    融合 LinkedBlockingQueue 和 SynchronousQueue 的功能,性能比 LinkedBlockingQueue 更好
  • PriorityBlockingQueue
    按照优先级出队
  • DelayQueue
    支持延时出队,按照延时时间排序出队

2.2.4.2 双端阻塞队列

LinkedBlockingDeque

2.2.4.3 单端非阻塞队列

ConcurrentLinkedQueue

2.2.4.4 双端非阻塞队列

ConcurrentLinkedDeque

3. 原子类工具

3.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)

3.2 原子化的对象引用类型

AtomicReference/AtomicStampedReference/AtomicMarkableReference,其中AtomicStampedReference可以解决 ABA 问题,AtomicMarkableReference用于表示引用值是否已逻辑删除。

AtomicStampedReference类更新的时候会带上stamp值,通过CAS更新

boolean compareAndSet(
  V expectedReference,
  V newReference,
  int expectedStamp,
  int newStamp)

AtomicMarkableReference通过Mark值来标记是否已删除。
如果一个线程想要删除这份数据,但同时另一个线程正在使用这份数据,那么这个线程可能会在数据被删除前先读取一份副本,然后在其上执行一些操作。在这个过程中,如果原始数据被标记为已删除,那么正在使用数据的线程可能会发现这个标记,从而避免使用已经删除的数据。

boolean compareAndSet(
  V expectedReference,
  V newReference,
  boolean expectedMark,
  boolean newMark)

3.3 原子化数组

tomicIntegerArray、AtomicLongArray、 AtomicReferenceArray,比起原子类基础类型主要是多了个索引参数。

3.4 原子化对象属性更新器

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

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

需要注意的是,对象属性必须是 volatile 类型的,只有这样才能保证可见性;如果对象属性不是 volatile 类型的,newUpdater() 方法会抛出 IllegalArgumentException 这个运行时异常。
newUpdater() 的方法参数只有类的信息,没有对象的引用,而更新对象的属性,一定需要对象的引用,那这个参数是在哪里传入的呢?是在原子操作的方法参数中传入的。例如 compareAndSet() 这个原子操作,相比原子化的基本数据类型多了一个对象引用 obj。
原子化对象属性更新器相关的方法,相比原子化的基本数据类型仅仅是多了对象引用参数.

3.5 原子化的累加器

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

4. 异步协同工具

异步协同可以分为4种:

  • 简单依赖
  • 串行/集合编排
  • 批量执行
  • 分治执行
    重新画个图汇总下,这图不显示

4.1. Future接口

4.1.1 Future接口

// 取消任务
boolean cancel(boolean mayInterruptIfRunning);
// 判断任务是否已取消
boolean isCancelled();
// 判断任务是否已结束
boolean isDone();
// 获得任务执行结果
get();
// 获得任务执行结果,支持超时
get(long timeout, TimeUnit unit);

两个get方法都是阻塞式地等待结果返回。

4.1.2 FutureTask类

FutureTask实现了RunnableFuture接口,拥有Runnable接口及Future接口的特性。

public interface RunnableFuture<V> extends Runnable, Future<V> { ... }

进阶版泡茶流程。

好,现在可以泡茶了。可以看到泡茶依赖于烧开水/洗茶壶/洗茶杯/拿茶叶,而烧开水又依赖于洗水壶。烧开水的时间较久,
所以可以两个线程,线程A执行洗水壶->烧开水->泡茶,线程B执行洗茶壶->洗茶杯->拿茶叶
利用FutureTask可以直接start/join原始实现,这里使用线程池利用Future即可。

        ExecutorService service = Executors.newFixedThreadPool(2);
        Future<String> futureB = service.submit(() -> {
            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 "龙井";
        });
        Future<String> futureA = service.submit(() -> {
            System.out.println("T1:洗水壶...");
            TimeUnit.SECONDS.sleep(1);

            System.out.println("T1:烧开水...");
            TimeUnit.SECONDS.sleep(15);
            String tf = futureB.get();

            System.out.println("T1:泡茶...");
            return "上茶:" + tf;
        });
        System.out.println(futureA.get());
        service.shutdown();

4.2. CompletableFuture接口

CompletableFuture支持异步任务的编排。

  • 无需手工维护线程关系,如上个例子中的futureA还需要显式等待futureB.get()完成。
  • 语义更清晰,可以专注于业务代码实现

这里将泡茶独立成一个单独任务。

        CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> {
            try {
                System.out.println("T1:洗水壶...");
                TimeUnit.SECONDS.sleep(1);
                System.out.println("T1:烧开水...");
                TimeUnit.SECONDS.sleep(15);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
            try {
                System.out.println("T2:洗茶壶...");
                TimeUnit.SECONDS.sleep(1);

                System.out.println("T2:洗茶杯...");
                TimeUnit.SECONDS.sleep(2);

                System.out.println("T2:拿茶叶...");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "龙井";
        });

        CompletableFuture<String> f3 = f1.thenCombine(f2, (__, f2Result) -> "上茶:" + f2Result); // f1无返回值用__代替

        System.out.println(f3.get());
默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数。生产代码建议自定义线程池使用。

观察实现的接口,可以发现异步等待及结果返回都是通过Future实现,那么异步任务编排肯定就是通过CompletionStage<T>接口实现的了。

public class CompletableFuture<T> implements Future, CompletionStage<T> { ... }

4.2.1 CompletionStage接口

  • 不带Async系列的方法: 沿用上一个执行任务所使用的线程池进行处理
  • 带Async系列的方法
    • 不指定线程池:使用默认ForkJoinPool线程池
    • 指定线程池: 使用指定线程池

4.2.1.1 串行编排

// thenApply系列有参有返回
CompletionStage<R> thenApply(fn);
CompletionStage<R> thenApplyAsync(fn);
// thenAccept系列有参无返回
CompletionStage<Void> thenAccept(consumer);
CompletionStage<Void> thenAcceptAsync(consumer);
// thenRun系列无参无返回
CompletionStage<Void> thenRun(action);
CompletionStage<Void> thenRunAsync(action);
// thenCompose系列会新创建子流程,结果等同thenApply系列方法
CompletionStage<R> thenCompose(fn);
CompletionStage<R> thenComposeAsync(fn);

thenApply的使用例子如下。

CompletableFuture<String> f0 = CompletableFuture
    .supplyAsync(() -> "Hello World")
    .thenApply(s -> s + " QQ")
    .thenApply(String::toUpperCase);
System.out.println(f0.get()); // HELLO WORLD QQ

4.2.1.2 汇聚编排

AND关系

// thenCombine系列有参有返回
CompletionStage<R> thenCombine(other, fn);
CompletionStage<R> thenCombineAsync(other, fn);
// thenAcceptBoth系列有参无返回
CompletionStage<Void> thenAcceptBoth(other, consumer);
CompletionStage<Void> thenAcceptBothAsync(other, consumer);
// runAfterBoth系列无参无返回
CompletionStage<Void> runAfterBoth(other, action);
CompletionStage<Void> runAfterBothAsync(other, action);

OR关系

// applyToEither系列有参有返回
CompletionStage applyToEither(other, fn);
CompletionStage applyToEitherAsync(other, fn);
// acceptEither系列有参无返回
CompletionStage acceptEither(other, consumer);
CompletionStage acceptEitherAsync(other, consumer);
// runAfterEither系列无参无返回
CompletionStage runAfterEither(other, action);
CompletionStage runAfterEitherAsync(other, action);

4.2.1.3 异常处理编排

// 类似try{}catch{}中的 catch{}
CompletionStage exceptionally(fn);
// 类似try{}finally{}中的 finally{},支持返回结果
CompletionStage<R> handle(fn);
CompletionStage<R> handleAsync(fn);
// 类似try{}finally{}中的 finally{},不支持返回结果
CompletionStage<R> whenComplete(consumer);
CompletionStage<R> whenCompleteAsync(consumer);

4.3. CompletionService

小明要做一个询价应用,这个应用需要从三个电商询价,然后保存在自己的数据库里。如何优化?

// 向电商S1询价,并保存
r1 = getPriceByS1();
save(r1);
// 向电商S2询价,并保存
r2 = getPriceByS2();
save(r2);
// 向电商S3询价,并保存
r3 = getPriceByS3();
save(r3);

首先考虑异步化询价。

// 创建线程池
ExecutorService executor =
  Executors.newFixedThreadPool(3);
// 异步向电商S1询价
Future<Integer> f1 = 
  executor.submit(
    ()->getPriceByS1());
// 异步向电商S2询价
Future<Integer> f2 = 
  executor.submit(
    ()->getPriceByS2());
// 异步向电商S3询价
Future<Integer> f3 = 
  executor.submit(
    ()->getPriceByS3());
    
// 获取电商S1报价并保存
r=f1.get();
executor.execute(()->save(r));
  
// 获取电商S2报价并保存
r=f2.get();
executor.execute(()->save(r));
  
// 获取电商S3报价并保存  
r=f3.get();
executor.execute(()->save(r));

虽然异步了,但是如果f1.get()和后续依然有阻塞关系。再次优化。

// 创建阻塞队列
BlockingQueue<Integer> bq = new LinkedBlockingQueue<>();
//电商S1报价异步进入阻塞队列  
executor.execute(()-> bq.put(f1.get()));
//电商S2报价异步进入阻塞队列  
executor.execute(()-> bq.put(f2.get()));
//电商S3报价异步进入阻塞队列  
executor.execute(()-> bq.put(f3.get()));
//异步保存所有报价  
for (int i=0; i<3; i++) {
  Integer r = bq.take();
  executor.execute(()->save(r));
}  

这样可以实现先返回的报价先入库。

CompletionService便是这样的一个并发工具类,内部通过一个阻塞队列实现优先返回的结果先出队列。但是队列保存的是Future对象,而非具体结果。
构造函数如下。

ExecutorCompletionService(Executor executor); \\ 默认阻塞队列为无界的 LinkedBlockingQueue
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;

利用CompletionService优化后的代码如下。

// 创建线程池
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));
}

4.4. Fork/Join

Fork/Join 计算框架主要包含两部分,一部分是分治任务的线程池 ForkJoinPool,另一部分是分治任务 ForkJoinTask。
类似于 ThreadPoolExecutor 和 Runnable 的关系,都可以理解为提交任务到线程池,只不过分治任务有自己独特类型 ForkJoinTask。

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

计算斐波那契数列的例子。

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

    static class Fibonacci extends RecursiveTask<Integer> {
        final int n;

        Fibonacci(int n) { // compute()方法没有入参,通过构造函数传参
            this.n = n;
        }

        protected Integer compute() {
            if (n <= 1) {
                return n;
            }
            Fibonacci f1 = new Fibonacci(n - 1);
            Fibonacci f2 = new Fibonacci(n - 2);
            //创建子任务
            f1.fork();
            f2.fork();
            //等待子任务结果,并合并结果
            return f2.join() + f1.join(); // 子任务的join必须倒序,类似出栈
        }
    }
posted @ 2023-11-30 15:25  kiper  阅读(25)  评论(0编辑  收藏  举报