【JUC合集-01】JUC并发编程复习总结

【JUC合集-01】JUC并发编程复习总结
目录JUCjava.util.concurrent并发编程总结一、核心认知JUC 到底是什么1.1 本质定位1.2 JUC 整体体系二、原子类无锁原子操作2.1 核心原理CAS2.2 常用原子类2.3 AtomicInteger 实战与源码解析2.4 LongAdder 为什么更快三、锁体系比 synchronized 更灵活3.1 Lock 接口总览3.2 ReentrantLock 重入锁尝试获取锁tryLock公平锁 vs 非公平锁3.3 ReentrantReadWriteLock 读写锁3.4 ReentrantLock 和 synchronized 对比四、三大常用同步工具类4.1 CountDownLatch倒计时门闩4.2 CyclicBarrier循环栅栏4.3 CountDownLatch vs CyclicBarrier4.4 Semaphore信号量五、线程池线程复用管理5.1 为什么要用线程池5.2 ThreadPoolExecutor 七大核心参数5.3 线程池执行流程5.4 四种拒绝策略5.5 Executors 自带线程池为什么不推荐5.6 自定义线程池最佳实践六、并发容器6.1 ConcurrentHashMap演进历程关键特性6.2 CopyOnWriteArrayList6.3 其他并发容器七、阻塞队列 BlockingQueue7.1 核心特性7.2 四组操作方法7.3 常用实现类对比八、底层基石AQS 抽象队列同步器8.1 什么是 AQS8.2 核心思想8.3 两种模式8.4 为什么 AQS 这么重要九、踩坑与实践1. 锁相关踩坑2. 线程池踩坑3. 并发容器踩坑4. 通用最佳实践十、复习速记JUCjava.util.concurrent并发编程总结基于 JDK 1.8 标准 API 整理覆盖核心工具、底层原理、实战用法与踩坑一、核心认知JUC 到底是什么1.1 本质定位JUC 就是java.util.concurrent包的简称是 JDK 官方提供的并发编程工具集。在 JUC 出现之前Java 做并发只能靠synchronized、wait()、notify()这些底层原语写起来麻烦、功能单一、容易出错。JUC 把并发开发中常用的锁、原子类、线程池、同步工具、并发容器、阻塞队列等能力全部封装好让开发者不用自己从零实现并发控制大大降低了并发编程的门槛。它解决的核心痛点synchronized是独占锁不支持读写分离、不支持尝试获取锁、不支持中断等待手动写wait/notify很容易写错导致假唤醒、死锁没有统一的线程池管理手动创建线程容易失控没有现成的并发容器手动写线程安全集合容易出问题没有同步工具类多线程协作逻辑要自己反复造轮子1.2 JUC 整体体系整个 JUC 包可以分成六大核心模块原子类java.util.concurrent.atomic包下基于 CAS 实现无锁原子操作锁体系locks包下Lock接口、重入锁、读写锁等基于 AQS 实现同步工具CountDownLatch、CyclicBarrier、Semaphore等用于多线程协作线程池Executor体系统一管理线程生命周期复用线程并发容器ConcurrentHashMap、CopyOnWriteArrayList等线程安全集合阻塞队列BlockingQueue体系用于生产者消费者场景、线程池任务队列二、原子类无锁原子操作2.1 核心原理CAS原子类能实现线程安全核心靠CASCompare And Swap比较并交换这是一种无锁并发方案。执行更新操作时先拿到旧值真正写入前比较当前内存值是不是和旧值一致一致就把新值写进去返回成功不一致就说明中间被别的线程改过了返回失败一般会自旋重试CAS 是 CPU 级别的原子指令全程没有加锁性能远高于锁特别适合并发量不极端高的热点数据更新。ABA 问题CAS 只看值有没有变不看值的变化过程。如果一个值从 A 改成 B又改回 ACAS 会认为它没变过但实际已经被修改过。解决办法是加版本号每次修改版本号加一比较的时候连版本号一起比JDK 提供了AtomicStampedReference就是干这个的。2.2 常用原子类类型代表类作用基本类型AtomicInteger、AtomicLong、AtomicBoolean原子更新基本类型引用类型AtomicReference、AtomicStampedReference原子更新对象引用带版本号解决 ABA数组类型AtomicIntegerArray、AtomicLongArray原子更新数组里的元素对象属性更新AtomicIntegerFieldUpdater原子更新对象的某个字段不用改原类累加器LongAdder、DoubleAdder高并发下累加性能比 AtomicLong 好适合统计计数2.3 AtomicInteger 实战与源码解析最常用的原子类用于原子计数比如接口访问量统计、分布式序号生成。public class AtomicIntegerDemo { // 初始值为0的原子整型 private static final AtomicInteger count new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { // 10个线程每个加1000次 for (int i 0; i 10; i) { new Thread(() - { for (int j 0; j 1000; j) { // 原子自增1等价于count但线程安全 count.incrementAndGet(); } }).start(); } Thread.sleep(2000); // 最终结果一定是10000不会出现并发丢失更新的问题 System.out.println(count.get()); } }底层源码拆解incrementAndGet// Unsafe是CAS的核心类直接操作内存所有原子类都靠它 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) 1; } // Unsafe里的核心方法自旋CAS public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { // 从内存中拿到当前值 var5 this.getIntVolatile(var1, var2); // CAS尝试更新成功就返回旧值失败就循环重试 } while(!this.compareAndSwapInt(var1, var2, var5, var5 var4)); return var5; }本质就是自旋 CAS拿值 → 尝试更新 → 失败就再拿再试直到成功为止。并发不高的时候很快冲突特别激烈的时候大量线程自旋会浪费 CPU。2.4 LongAdder 为什么更快高并发下大量线程同时 CAS 一个变量冲突严重很多线程空转重试性能会下降。LongAdder做了优化把一个值拆成多个Cell单元格每个线程去更新自己对应的 Cell最后求和的时候把所有 Cell 加起来。这样大大降低了冲突概率并发越高优势越明显。 代价是数据有短暂的弱一致性求和不一定完全精准适合统计计数、监控指标这种不需要绝对精确的场景。三、锁体系比 synchronized 更灵活3.1 Lock 接口总览Lock是 JUC 锁的顶层接口最常用的实现是ReentrantLock重入锁和ReentrantReadWriteLock读写锁。和synchronized相比Lock 的优势可以尝试获取锁拿不到就返回不会一直死等可以响应中断等待锁的过程中可以被打断支持公平锁和非公平锁切换支持读写分离读多写少场景性能更好可以绑定多个条件变量精确唤醒不同的线程3.2 ReentrantLock 重入锁和 synchronized 一样都是可重入的同一个线程可以多次获取同一把锁不会自己把自己锁住。基础用法public class ReentrantLockDemo { // 创建重入锁默认是非公平锁 private final Lock lock new ReentrantLock(); private int count 0; public void add() { // 加锁和synchronized进入代码块等价 lock.lock(); try { // 业务代码放try里 count; } finally { // 必须在finally里手动释放锁不然异常了锁就泄露了 lock.unlock(); } } }注意Lock 必须手动解锁而且一定要写在 finally 里不然代码抛异常了锁永远不会释放别的线程永远拿不到最终死锁。这是最容易犯的错。尝试获取锁tryLock这是最实用的特性避免线程无限阻塞public boolean tryAdd() { // 尝试获取锁等3秒还拿不到就放弃返回false try { if (lock.tryLock(3, TimeUnit.SECONDS)) { try { count; return true; } finally { lock.unlock(); } } } catch (InterruptedException e) { // 等待过程中被中断的处理 Thread.currentThread().interrupt(); } // 拿锁失败返回失败业务可以做降级处理 return false; }公平锁 vs 非公平锁非公平锁默认来抢锁的线程不管队列里有没有人在等直接先尝试抢抢不到再去排队。优点是吞吐量高缺点是可能出现线程饥饿有的线程一直抢不到。公平锁严格按先来后到排队谁等得久谁先拿。优点是公平不会饥饿缺点是吞吐量低大量线程切换开销大。// true就是公平锁false非公平 Lock fairLock new ReentrantLock(true);3.3 ReentrantReadWriteLock 读写锁现实中很多场景是读多写少普通锁不管读写都独占读的时候也不能并发读浪费性能。 读写锁把锁分成读锁和写锁读锁共享锁多个线程可以同时持有读锁并发读不互斥写锁独占锁写的时候不能有任何读或写规则读 - 读不互斥读 - 写互斥写 - 写互斥public class ReadWriteLockDemo { private final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); private final Lock readLock rwLock.readLock(); private final Lock writeLock rwLock.writeLock(); private String data; // 读操作加读锁多线程可以同时读 public String read() { readLock.lock(); try { return data; } finally { readLock.unlock(); } } // 写操作加写锁同一时间只能一个写 public void write(String newData) { writeLock.lock(); try { this.data newData; } finally { writeLock.unlock(); } } }适用场景读多写少比如配置缓存、元数据管理读的频率远高于写用读写锁性能提升非常明显。3.4 ReentrantLock 和 synchronized 对比对比项ReentrantLocksynchronized实现层面JDK 层面实现的 APIJVM 层面关键字底层靠 monitor锁释放必须手动 unlock一般放 finally自动释放代码块结束或异常都自动释放锁特性支持尝试获取、可中断、公平 / 非公平切换非公平不可中断不能尝试锁类型独占锁也有读写锁扩展独占锁只有一种条件变量支持多个 Condition精确唤醒只有一个 waitSet只能全部唤醒性能高并发下更好功能灵活低并发下够用JDK1.6 后优化了很多性能差距不大易用性容易写错忘记释放锁会死锁简单JVM 兜底总结普通同步场景用 synchronized 就够了简单不容易错需要高级特性尝试锁、可中断、公平锁、读写锁的时候再用 ReentrantLock。四、三大常用同步工具类4.1 CountDownLatch倒计时门闩作用一个线程等待多个线程完成任务后自己再继续执行。相当于发令枪所有人都到终点了裁判才宣布比赛结束。只能用一次计数减到 0 就不能再用了调用countDown()计数减 1调用await()阻塞等待计数归零代码示例等待所有子任务完成再汇总public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { // 初始化计数器数值为3代表要等3个线程完成 CountDownLatch latch new CountDownLatch(3); for (int i 0; i 3; i) { new Thread(() - { try { System.out.println(Thread.currentThread().getName() 执行任务); Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 任务完成计数减1必须放finally保证一定执行 latch.countDown(); } }).start(); } System.out.println(主线程等待所有子任务完成...); // 主线程阻塞直到计数变成0 latch.await(); System.out.println(所有任务完成主线程继续执行); } }典型场景并行计算多个线程分别算不同部分全部算完主线程汇总结果启动时等待多个资源初始化完成再对外提供服务。4.2 CyclicBarrier循环栅栏作用多个线程互相等待所有线程都到达栅栏位置后再一起继续执行。可以循环使用重置后能反复用。每个线程调用await()就到达栅栏阻塞等待最后一个线程到达后所有线程同时被唤醒继续执行可以设置一个栅栏动作最后一个线程到达时自动执行代码示例运动员到齐了一起起跑public class CyclicBarrierDemo { public static void main(String[] args) { // 等待3个线程全部到齐后执行后面的Runnable CyclicBarrier barrier new CyclicBarrier(3, () - { System.out.println(所有人都到齐了预备跑); }); for (int i 0; i 3; i) { new Thread(() - { try { System.out.println(Thread.currentThread().getName() 到达起跑线); // 到达栅栏等待其他人 barrier.await(); System.out.println(Thread.currentThread().getName() 开始跑); } catch (InterruptedException | BrokenBarrierException e) { Thread.currentThread().interrupt(); } }).start(); } } }4.3 CountDownLatch vs CyclicBarrier区别CountDownLatchCyclicBarrier等待关系一个线程等多个线程多个线程互相等能否复用只能用一次计数到 0 就废了可以循环使用重置后反复用计数方式调用 countDown 减 1调用 await 计数加 1到阈值释放额外动作没有内置回调支持设置栅栏动作最后一个线程到达时执行异常处理中断抛 InterruptedException栅栏破了抛 BrokenBarrierException4.4 Semaphore信号量作用控制同时访问某个资源的线程数量相当于限流。有 N 个许可线程拿一个许可才能执行执行完归还许可许可用完了其他线程就阻塞等。代码示例限流器最多 3 个线程同时执行public class SemaphoreDemo { public static void main(String[] args) { // 初始化3个许可最多3个线程同时执行 Semaphore semaphore new Semaphore(3); for (int i 0; i 10; i) { new Thread(() - { try { // 获取一个许可拿不到就阻塞 semaphore.acquire(); System.out.println(Thread.currentThread().getName() 获得许可开始执行); Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 释放许可 semaphore.release(); System.out.println(Thread.currentThread().getName() 释放许可); } }).start(); } } }典型场景接口限流、数据库连接池控制、资源访问并发数控制。五、线程池线程复用管理5.1 为什么要用线程池手动 new Thread 弊端很多每次创建销毁线程开销大频繁创建销毁性能差线程数量不受控高并发下创建几百上千个线程内存和 CPU 都扛不住不好管理没法统一监控、统计、调优线程池的核心思想就是线程复用提前创建好一批线程任务来了直接用线程执行执行完不销毁继续等下一个任务。既降低了创建销毁开销又能控制并发数量。5.2 ThreadPoolExecutor 七大核心参数这是面试必考点也是自定义线程池的基础。public ThreadPoolExecutor( int corePoolSize, // 1. 核心线程数 int maximumPoolSize, // 2. 最大线程数 long keepAliveTime, // 3. 空闲线程存活时间 TimeUnit unit, // 4. 存活时间单位 BlockingQueueRunnable workQueue, // 5. 任务队列 ThreadFactory threadFactory, // 6. 线程工厂 RejectedExecutionHandler handler // 7. 拒绝策略 )逐个解释核心线程数线程池长期保留的线程数即使空闲也不会销毁除非设置了允许核心线程超时。最大线程数线程池最多能创建的线程总数核心线程 非核心线程。空闲存活时间非核心线程空闲超过这个时间就会被销毁。核心线程默认不回收。时间单位配合存活时间使用秒、毫秒等。任务队列核心线程都忙的时候新任务放到队列里等待。线程工厂创建线程的工厂可以自定义线程名、是否守护线程、优先级等方便排查问题。拒绝策略线程和队列都满了新任务来的时候怎么处理。5.3 线程池执行流程新任务提交到线程池执行顺序是记忆顺序核心线程 → 队列 → 非核心线程 → 拒绝策略。很多人会搞错以为先开非核心线程再进队列实际是先进队列队列满了才开非核心。5.4 四种拒绝策略策略行为适用场景AbortPolicy默认直接抛出 RejectedExecutionException 异常大多数场景异常及时暴露CallerRunsPolicy由提交任务的主线程自己执行这个任务不能丢任务流量平稳的场景DiscardPolicy直接丢掉任务什么都不做不重要的任务丢了也无所谓DiscardOldestPolicy丢掉队列里最老的任务把新任务加进去最新任务最重要的场景5.5 Executors 自带线程池为什么不推荐JDK 提供了Executors工具类快速创建线程池但《阿里巴巴开发规范》明确禁止用它创建线程池原因FixedThreadPool 和 SingleThreadPool队列用的是无界的LinkedBlockingQueue任务多了会无限堆积内存溢出。CachedThreadPool最大线程数是 Integer.MAX_VALUE高并发下会创建无数线程系统卡死。ScheduledThreadPool同样是无界队列容易 OOM。生产环境必须用ThreadPoolExecutor手动指定参数根据业务场景设置合理的核心线程数、队列大小、最大线程数、拒绝策略。5.6 自定义线程池最佳实践public class ThreadPoolUtil { private static final ThreadPoolExecutor EXECUTOR; static { // 核心线程数根据业务来CPU密集型设为CPU核数IO密集型设为2*CPU核数 int coreSize Runtime.getRuntime().availableProcessors() * 2; EXECUTOR new ThreadPoolExecutor( coreSize, coreSize * 2, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(500), // 自定义线程工厂给线程起名字排查问题方便 new ThreadFactory() { private final AtomicInteger threadNum new AtomicInteger(1); Override public Thread newThread(Runnable r) { return new Thread(r, biz-pool- threadNum.getAndIncrement()); } }, // 自定义拒绝策略记录日志降级处理 new ThreadPoolExecutor.CallerRunsPolicy() ); // 允许核心线程超时回收长期空闲可以释放资源 EXECUTOR.allowCoreThreadTimeOut(true); } public static void execute(Runnable task) { EXECUTOR.execute(task); } }六、并发容器6.1 ConcurrentHashMap线程安全的 HashMap是并发场景下最常用的集合。演进历程JDK 1.7分段锁Segment把整个 Map 分成 16 段每段一把锁不同段之间可以并发写。缺点是分段粒度还是大同一段内还是串行并发度最高 16。JDK 1.8废弃分段锁改用CAS synchronized锁的粒度是每个数组元素每个桶。没有哈希冲突的时候用 CAS 放元素有冲突或者扩容的时候才加 synchronized 锁。粒度更细并发性能更好同时结构和普通 HashMap 保持一致都是数组 链表 红黑树。关键特性不允许 null 键和 null 值和 HashMap 不一样迭代器是弱一致性的遍历过程中数据修改不会抛并发修改异常但遍历的是某个时间点的快照不一定能看到最新修改size 计算是估算值不是绝对精确并发场景下没有绝对准确的 size6.2 CopyOnWriteArrayList写时复制的 ArrayList核心思想读操作完全不加锁直接读原数组性能很高写操作的时候复制一份新数组在新数组上修改改完把数组引用指向新数组写的时候加锁防止并发写复制多份优点读性能极高完全无锁适合读多写极少的场景缺点写的时候要复制数组内存开销大数据有延迟写的内容不能立刻被读到弱一致性适用场景配置列表、黑白名单、订阅关系读非常多写非常少允许短暂不一致的场景。6.3 其他并发容器ConcurrentSkipListMap线程安全的跳表实现有序对应 TreeMap 的并发版ConcurrentLinkedQueue无界非阻塞并发队列CAS 实现高并发性能好BlockingQueue阻塞队列下面单独讲七、阻塞队列 BlockingQueue7.1 核心特性阻塞队列是特殊的队列支持阻塞式的插入和移除队列满的时候插入元素的线程会阻塞直到队列有空位队列空的时候取出元素的线程会阻塞直到队列有元素天生适合生产者消费者模式也是线程池任务队列的核心。7.2 四组操作方法操作方式抛出异常返回特殊值阻塞等待超时等待插入add(e)offer(e)put(e)offer(e, time, unit)移除remove()poll()take()poll(time, unit)检查队首element()peek()--7.3 常用实现类对比实现类底层结构是否有界特点适用场景ArrayBlockingQueue数组有界创建时必须指定大小固定容量公平 / 非公平模式固定容量的生产者消费者、线程池队列LinkedBlockingQueue链表默认无界也可指定容量吞吐量比数组版高头尾两把锁大多数阻塞场景FixedThreadPool 用的就是它SynchronousQueue不存储元素直接传递容量为 0来了任务必须有线程接不然就阻塞CachedThreadPool 用的就是它直接递交给线程DelayQueue优先级队列无界元素必须实现 Delayed 接口到时间才能取出定时任务、延迟消息、超时订单关闭PriorityBlockingQueue优先级队列无界按优先级出队不是先进先出需要按优先级处理任务的场景八、底层基石AQS 抽象队列同步器8.1 什么是 AQSAQS 全称AbstractQueuedSynchronizer是 JUC 里几乎所有锁和同步工具的底层框架。ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock全都是基于 AQS 实现的。它把并发同步的通用逻辑全部抽出来了用一个volatile int state变量表示同步状态用一个CLH 变体的双向链表做等待队列抢不到锁的线程排到队列里提供了独占模式和共享模式两种同步方式子类只需要实现几个简单的方法就能轻松实现自己的同步器8.2 核心思想抢锁失败的线程不会一直自旋浪费 CPU而是封装成节点放到等待队列里挂起锁释放的时候再唤醒队列里的下一个线程。队列是双向链表每个节点保存线程引用、等待状态、前驱后继指针。入队新节点加到队尾用 CAS 保证原子性出队头节点释放锁后唤醒下一个节点的线程去抢锁8.3 两种模式独占模式同一时间只能有一个线程持有锁比如 ReentrantLock。代表方法acquire()、release()。共享模式多个线程可以同时持有锁比如 Semaphore、读写锁的读锁。代表方法acquireShared()、releaseShared()。8.4 为什么 AQS 这么重要它是 JUC 的骨架把所有同步器通用的「排队、阻塞、唤醒、状态管理」逻辑都封装好了上层工具不用重复实现。理解了 AQS再看各种锁和同步工具本质都是在操作 state 变量ReentrantLockstate0 表示锁空闲state0 表示被占用重入一次加 1CountDownLatchstate 初始是计数countDown 一次减 1减到 0 就释放所有等待线程Semaphorestate 是剩余许可数acquire 减 1release 加 1九、踩坑与实践1. 锁相关踩坑ReentrantLock 忘记 unlock必须放 finally不然异常了锁泄露最终死锁锁重入次数不匹配加了几次锁就要解几次锁多解会抛异常少解锁不释放读写锁写锁饥饿读特别多的时候写锁一直抢不到会饥饿。可以用公平模式缓解synchronized 锁范围太大 / 太小太大并发差太小不安全刚好包住共享变量操作就行2. 线程池踩坑用 Executors 创建线程池无界队列 / 无线程上限生产环境必炸一定要手动 new ThreadPoolExecutor核心线程数设置不合理CPU 密集型计算多设为 CPU 核数IO 密集型数据库、网络调用多设为 2*CPU 核数当然洛需要根据压测调整毕竟只是理论数据还得看实际情况线程池不关闭程序退出线程池还在导致进程关不掉。优雅停机要调用 shutdown ()任务异常吞掉execute 提交的任务抛异常会导致线程销毁最好自己在任务里 try-catch 处理3. 并发容器踩坑ConcurrentHashMap 不是万能的单个操作是原子的但多步组合操作不是比如先判断存在再更新这两步之间可能被别的线程改需要自己加锁或者用 compute 等原子方法CopyOnWrite 写太多性能差写一次复制一次数组写多了内存和性能都扛不住只适合读多写极少遍历集合时修改并发容器不会抛并发修改异常但不代表就可以随便边遍历边改结果是不可预期的4. 通用最佳实践优先用工具类不要自己手写 wait/notify很容易写错优先降低锁粒度能锁代码块就别锁整个方法能锁局部变量就别锁整个对象能无锁就不用锁原子类能解决的就别用锁并发场景下优先考虑最终一致不要强求强一致性能差很多给线程起有意义的名字排查问题的时候一眼就能认出是哪个池的线程避免死锁固定加锁顺序避免循环依赖设置锁超时时间尽量不要嵌套加锁十、复习速记JUC 六大模块原子类、锁、同步工具、线程池、并发容器、阻塞队列。原子类核心CAS 自旋CPU 级原子指令无锁并发高并发计数用 LongAdder 性能更好。锁体系ReentrantLock 灵活可中断、支持尝试锁读写锁适合读多写少synchronized 简单不易错普通场景够用。三大工具CountDownLatch 一等多一次性CyclicBarrier 多等一可复用Semaphore 限并发数量。线程池七大参数核心数、最大数、存活时间、单位、队列、线程工厂、拒绝策略执行顺序核心→队列→非核心→拒绝。ConcurrentHashMap1.8 用 CAS synchronized桶粒度锁数组 链表 红黑树比 1.7 分段锁性能好。AQS 基石state 状态变量 CLH 等待队列独占 / 共享两种模式是所有锁和同步工具的底层。核心原则优先用 JDK 现成工具不重复造轮子锁粒度尽量小线程池必须自定义参数。