Java并发 -- CompletableFuture
泡茶烧水
123456789101112131415161718192021222324CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> { System.out.println("T1: 洗水壶"); sleep(1, TimeUnit.SECONDS); System.out.println("T1: 烧开水"); sleep(15, TimeUnit.SECONDS);});CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> { System.out.println("T2: 洗茶壶"); sleep(1, TimeUnit.SECONDS); System.out.println("T2: 洗茶杯"); sleep(2, TimeUnit.SECON ...
Java并发 -- Future
ThreadPoolExecutor12345678910// 无法获取任务的执行结果public void execute(Runnable command);// 可以获取任务的执行结果// Runnable接口的run()方法没有返回值,返回的Future只能断言任务是否已经结束,类似于Thread.join()public Future<?> submit(Runnable task)// Callable接口的call()方法有返回值,可以通过调用返回的Future对象的get()方法获取任务的执行结果public <T> Future<T> submit(Callable<T> task);// 返回的Future对象f,f.get()的返回值就是传给submit方法的参数resultpublic <T> Future<T> submit(Runnable task, T result)
Future1234567891011// 取消任务boolean cancel(boolean mayInterruptIfRunn ...
Java并发 -- 线程池
创建线程
创建普通对象,只是在JVM的堆里分配一块内存而已
创建线程,需要调用操作系统内核的API,然后操作系统需要为线程分配一系列资源,成本很高
线程是一个重量级对象,应该避免频繁创建和销毁,采用线程池方案
一般的池化资源12345678910111213141516// 假设Java线程池采用一般意义上池化资源的设计方法class ThreadPool { // 获取空闲线程 Thread acquire() { } // 释放线程 void release(Thread t) { }}// 期望的使用ThreadPool pool;Thread T1 = pool.acquire();// 传入Runnable对象T1.execute(() -> { // 具体业务逻辑});
生产者-消费者模式业界线程池的设计,普遍采用生产者-消费者模式,线程池的使用方是生产者,线程池本身是消费者
123456789101112131415161718192021222324252 ...
Java并发 -- 原子类
add12345678private long count = 0;public void add() { int idx = 0; while (idx++ < 10_000) { count += 1; }}
add()方法不是线程安全的,主要原因是count的可见性和count+=1的原子性
可见性的问题可以用volatile来解决,原子性的问题一般采用互斥锁来解决
无锁方案12345678private AtomicLong atomicCount = new AtomicLong(0);public void atomicAdd() { int idx = 0; while (idx++ < 10_000) { atomicCount.getAndIncrement(); }}
无锁方案相对于互斥锁方案,最大的好处是_性能_
互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身会消耗性能
拿不到锁的线程会进入 ...
Java并发 -- 并发容器
同步容器
Java 1.5之前提供的同步容器虽然也能保证线程安全,但性能很差
Java中的容器主要分为四大类,分别为List、Map、Set和Queue,并不是所有的Java容器都是线程安全的
将非线程安全的容器变成线程安全的容器的简单方案:synchronized
把非线程安全的容器封装在对象内部,然后控制好访问路径即可
线程安全的ArrayList12345678910111213141516171819public class SafeArrayList<T> { private List<T> list = new ArrayList<>(); public synchronized T get(int idx) { return list.get(idx); } public synchronized void add(int idx, T t) { list.add(idx, t); } public synchronized boolea ...
Java并发 -- CountDownLatch + CyclicBarrier
对账系统
1234567891011// 存在未对账订单while (existUnreconciledOrders()) { // 查询未对账订单 pOrder = getPOrder(); // 查询派送订单 dOrder = getDOrder(); // 执行对账操作 Order diff = check(pOrder, dOrder); // 将差异写入差异库 save(diff);}
性能瓶颈
getPOrder()和getDOrder()最为耗时,并且两个操作没有先后顺序的依赖,可以并行处理
简单并行 - join
123456789101112131415161718192021222324// 存在未对账订单// 存在未对账订单while (existUnreconciledOrders()) { // 查询未对账订单 Thread t1 = new Thread(() -> { pOrder = getPOrder(); }); t1. ...
Java并发 -- StampedLock
StampedLock VS ReadWriteLock
StampedLock同样适用于读多写少的场景,性能比ReadWriteLock好
ReadWriteLock支持两种模式:写锁、读锁
StampedLock支持三种模式:写锁、悲观读锁、_乐观读_(关键)
StampedLock的写锁、悲观读锁的语义和ReadWriteLock的写锁、读锁的语义非常类似
允许多个线程同时获取悲观读锁,只允许一个线程获取写锁,写锁和悲观读锁是互斥的
但StampedLock里的写锁和悲观读锁加锁成功之后,都会返回一个stamp,然后解锁的时候需要传入这个stmap
12345678910111213141516171819202122232425public class StampedLockExample { private final StampedLock stampedLock = new StampedLock(); @Test // 悲观读锁 public void pessimisticReadLockTest() { long st ...
Java并发 -- ReadWriteLock
读多写少
理论上,利用管程和信号量可以解决所有并发问题,但JUC提供了很多工具类,_细分场景优化性能,提升易用性_
针对读多写少的并发场景,JUC提供了读写锁,即ReadWriteLock
读写锁
读写锁是一种广泛使用的通用技术,并非Java所特有
所有读写锁都遵守3条基本原则
允许多个线程同时读共享变量 – 与互斥锁的重要区别
只允许一个线程写共享变量
如果一个写线程正常执行写操作,此时禁止读线程读取共享变量
缓存1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253public class Cache<K, V> { private final Map<K, V> map = new HashMap<>(); private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 读锁 private fi ...
Java并发 -- Semaphore
历史
信号量是由计算机科学家Dijkstra在1965年提出,在之后的15年,信号量一直都是并发编程领域的终结者
直到1980年管程被提出来,才有了第二选择,目前所有支持并发编程的语言都支持信号量机制
信号量模型
在信号量模型里,计数器和等待队列对外都是透明的,只能通过信号量模型提供的三个方法来访问它们,即init/down/up
init():设置计数器的初始值
down():计数器的值减1,如果此时计数器的值小于0,则当前线程将阻塞,否则当前线程可以继续执行
up():计数器加1,如果此时计数器的值大于或等于0,则唤醒等待队列中的一个线程,并将其从等待队列中移除
init/down/up都是原子性的,这个原子性由信号量模型的实现方保证
在JUC中,信号量模型由java.util.concurrent.Semaphore实现,Semaphore能够保证这三个方法的原子性操作
在信号量模型里,down/up这两个操作最早被称为P操作和V操作,因此信号量模型也被称为_PV原语_
在JUC中,down和up对应的是acquire和release
...
Java并发 -- Condition
Condition
Condition实现了管程模型中的_条件变量_
Java内置的管程(synchronized)只有一个条件变量,而Lock&Condition实现的管程支持多个条件变量
在很多并发场景下,支持多个条件变量能够让并发程序的可读性更好,也更容易实现
阻塞队列1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253// 下列三对操作的语义是相同的// Condition.await() Object.wait()// Condition.signal() Object.notify()// Condition.signalAll() Object.notifyAll()public class BlockedQueue<T> { private static final int MAX_SIZE = 10; // 可重入锁 private final Lock lock = ...