Java常用并发工具类
同步工具类存在的意义
管程或者信号量可以解决所有的并发问题,那么同步工具类存在的意义是什么呢?
两个字:方便。
针对不同的并发场景,使用对应的工具类可以快速完成业务开发。
1. 锁工具
1.1 ReadWriteLock
- 读锁
- 写锁
1.1.1 使用场景
-
允许多个线程同时读共享变量
-
只允许一个线程写共享变量
-
如果一个线程正在执行写操作,此时禁止读线程读共享变量
其实就是读锁共享锁,写锁互斥锁。除了同时读以外,其余场景均阻塞。
读 | 写 | |
---|---|---|
读 | √ | × |
写 | × | × |
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
假如泡茶有三个步骤:
- 烧水1min
- 洗杯子2min
- 泡茶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批次的烧水和洗杯子,如下图所示。
那么为了泡茶不阻塞,所以需要将泡茶也丢到子线程中执行,并且需要维持烧水/洗杯子这两步和泡茶间的依赖关系。
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)
- Java.util.Collections.synchronizedSet(Set
- 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也有使用。
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操作进行增删改,故不加锁也不存在两个线程同时读取写入的竞态条件
get源码如下。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; - 首先计算hash值,定位到该table索引位置,如果是首节点符合就返回
- 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回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必须倒序,类似出栈 } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律