
文章目录
-
- 课程导言
- 第一部分:JUC概览------Java并发工具包全景
-
- [1.1 什么是JUC?](#1.1 什么是JUC?)
- [1.2 JUC五大核心组件](#1.2 JUC五大核心组件)
- [1.3 JUC的设计思想](#1.3 JUC的设计思想)
- [1.4 JUC与synchronized的关系](#1.4 JUC与synchronized的关系)
- 第二部分:Lock体系与AQS原理(第一课时重点)
-
- [2.1 Lock接口概述](#2.1 Lock接口概述)
- [2.2 ReentrantLock------可重入锁](#2.2 ReentrantLock——可重入锁)
-
- [2.2.1 基本用法](#2.2.1 基本用法)
- [2.2.2 公平锁与非公平锁](#2.2.2 公平锁与非公平锁)
- [2.2.3 可中断与限时获取](#2.2.3 可中断与限时获取)
- [2.2.4 synchronized与ReentrantLock对比](#2.2.4 synchronized与ReentrantLock对比)
- [2.3 ReentrantReadWriteLock------读写锁](#2.3 ReentrantReadWriteLock——读写锁)
-
- [2.3.1 概念与原理](#2.3.1 概念与原理)
- [2.3.2 使用示例](#2.3.2 使用示例)
- [2.3.3 锁降级](#2.3.3 锁降级)
- [2.4 AQS------JUC的基石](#2.4 AQS——JUC的基石)
-
- [2.4.1 什么是AQS?](#2.4.1 什么是AQS?)
- [2.4.2 AQS的核心组成](#2.4.2 AQS的核心组成)
- [2.4.3 AQS的设计模式](#2.4.3 AQS的设计模式)
- [2.4.4 AQS的工作流程](#2.4.4 AQS的工作流程)
- [2.5 LockSupport](#2.5 LockSupport)
- 第三部分:原子类与CAS机制(第一课时重点)
-
- [3.1 原子类概述](#3.1 原子类概述)
- [3.2 为什么需要原子类?](#3.2 为什么需要原子类?)
- [3.3 CAS原理详解](#3.3 CAS原理详解)
-
- [3.3.1 什么是CAS?](#3.3.1 什么是CAS?)
- [3.3.2 CAS在原子类中的应用](#3.3.2 CAS在原子类中的应用)
- [3.3.3 CAS的三大缺陷](#3.3.3 CAS的三大缺陷)
- [3.4 原子类使用示例](#3.4 原子类使用示例)
- 第四部分:并发工具类(第二课时重点)
-
- [4.1 CountDownLatch------倒计数器](#4.1 CountDownLatch——倒计数器)
-
- [4.1.1 概念与用途](#4.1.1 概念与用途)
- [4.1.2 核心方法](#4.1.2 核心方法)
- [4.1.3 使用示例](#4.1.3 使用示例)
- [4.2 CyclicBarrier------循环屏障](#4.2 CyclicBarrier——循环屏障)
-
- [4.2.1 概念与用途](#4.2.1 概念与用途)
- [4.2.2 与CountDownLatch的区别](#4.2.2 与CountDownLatch的区别)
- [4.2.3 使用示例](#4.2.3 使用示例)
- [4.3 Semaphore------信号量](#4.3 Semaphore——信号量)
-
- [4.3.1 概念与用途](#4.3.1 概念与用途)
- [4.3.2 核心方法](#4.3.2 核心方法)
- [4.3.3 使用示例](#4.3.3 使用示例)
- [4.3.4 二元信号量------可替代锁](#4.3.4 二元信号量——可替代锁)
- [4.4 Exchanger------交换器](#4.4 Exchanger——交换器)
-
- [4.4.1 概念与用途](#4.4.1 概念与用途)
- [4.4.2 使用示例](#4.4.2 使用示例)
- 第五部分:并发集合(第二课时重点)
-
- [5.1 线程安全的集合概述](#5.1 线程安全的集合概述)
- [5.2 ConcurrentHashMap------并发哈希表](#5.2 ConcurrentHashMap——并发哈希表)
-
- [5.2.1 为什么不用Hashtable?](#5.2.1 为什么不用Hashtable?)
- [5.2.2 JDK 1.7的实现:分段锁](#5.2.2 JDK 1.7的实现:分段锁)
- [5.2.3 JDK 1.8的演进:CAS + synchronized](#5.2.3 JDK 1.8的演进:CAS + synchronized)
- [5.2.4 扩容机制优化](#5.2.4 扩容机制优化)
- [5.3 CopyOnWriteArrayList------写时复制列表](#5.3 CopyOnWriteArrayList——写时复制列表)
-
- [5.3.1 原理](#5.3.1 原理)
- [5.3.2 适用场景](#5.3.2 适用场景)
- [5.4 BlockingQueue------阻塞队列](#5.4 BlockingQueue——阻塞队列)
-
- [5.4.1 概念](#5.4.1 概念)
- [5.4.2 常用实现类](#5.4.2 常用实现类)
- [5.4.3 生产者-消费者示例](#5.4.3 生产者-消费者示例)
- 第六部分:线程池(第二课时重点)
-
- [6.1 为什么需要线程池?](#6.1 为什么需要线程池?)
- [6.2 ThreadPoolExecutor核心参数](#6.2 ThreadPoolExecutor核心参数)
-
- [6.2.1 参数详解](#6.2.1 参数详解)
- [6.2.2 线程池工作流程](#6.2.2 线程池工作流程)
- [6.3 拒绝策略](#6.3 拒绝策略)
- [6.4 线程池创建方式](#6.4 线程池创建方式)
-
- [6.4.1 通过Executors工厂类](#6.4.1 通过Executors工厂类)
- [6.4.2 为什么不推荐Executors?](#6.4.2 为什么不推荐Executors?)
- [6.4.3 正确姿势:直接使用ThreadPoolExecutor](#6.4.3 正确姿势:直接使用ThreadPoolExecutor)
- [6.5 线程数配置策略](#6.5 线程数配置策略)
- [6.6 线程池监控](#6.6 线程池监控)
- [6.7 优雅关闭线程池](#6.7 优雅关闭线程池)
- 第七部分:JUC优化与实战(第二课时重点)
-
- [7.1 锁优化策略](#7.1 锁优化策略)
-
- [7.1.1 减少锁持有时间](#7.1.1 减少锁持有时间)
- [7.1.2 减小锁粒度](#7.1.2 减小锁粒度)
- [7.1.3 锁分离](#7.1.3 锁分离)
- [7.2 CAS优化](#7.2 CAS优化)
- [7.3 并发编程最佳实践](#7.3 并发编程最佳实践)
-
- [7.3.1 优先使用并发工具而非手动同步](#7.3.1 优先使用并发工具而非手动同步)
- [7.3.2 使用不可变对象](#7.3.2 使用不可变对象)
- [7.3.3 线程封闭](#7.3.3 线程封闭)
- [7.3.4 避免死锁](#7.3.4 避免死锁)
- [7.4 性能对比测试](#7.4 性能对比测试)
- [7.5 常见问题排查](#7.5 常见问题排查)
-
- [7.5.1 线程池队列积压](#7.5.1 线程池队列积压)
- [7.5.2 死锁](#7.5.2 死锁)
- [7.5.3 内存泄漏](#7.5.3 内存泄漏)
- 课程总结

课程导言
适用对象
本课程适合已经掌握Java多线程基础知识(如线程创建、synchronized、wait/notify),希望深入学习高并发编程的开发者。无论你是准备面试的求职者,还是希望提升系统性能的一线工程师,这门课程都将为你构建完整的JUC知识体系。
学习目标
通过两个课时的系统学习,你将能够:
- 掌握 JUC五大核心组件:locks、atomic、tools、collections、executor
- 理解 AQS抽象同步器的底层原理
- 熟练使用 ReentrantLock、Semaphore、CountDownLatch等工具类
- 深入剖析 ConcurrentHashMap的演进与实现
- 学会 线程池的参数配置与优化策略
- 掌握 CAS机制及其ABA问题的解决方案
课程安排
- 第一课时(约60分钟):JUC概览 + Lock体系 + AQS原理 + 原子类与CAS
- 第二课时(约60分钟):并发工具类 + 并发集合 + 线程池 + 优化与实战
第一部分:JUC概览------Java并发工具包全景
1.1 什么是JUC?
JUC是java.util.concurrent包的简称,是Java专门为支持高并发编程而设计的工具包。在JDK 1.5之后引入,包含了大量用于处理多线程编程的类和接口,可以有效减少竞争条件和死锁问题。
在JUC出现之前,Java的并发编程主要依赖synchronized关键字和wait/notify机制,功能有限且灵活性不足。JUC的诞生标志着Java并发编程进入了一个全新的时代------从"能用"走向"好用"。
1.2 JUC五大核心组件
JUC包的内容虽然丰富,但可以归纳为五大核心组件:
| 组件 | 描述 | 核心类/接口 |
|---|---|---|
| locks | 锁框架,提供更灵活的锁机制 | ReentrantLock, ReentrantReadWriteLock, LockSupport |
| atomic | 原子操作类,基于CAS实现 | AtomicInteger, AtomicLong, AtomicReference |
| tools | 并发工具类,解决特定同步场景 | CountDownLatch, CyclicBarrier, Semaphore |
| collections | 并发集合,线程安全的容器 | ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue |
| executor | 线程池框架,管理线程生命周期 | ThreadPoolExecutor, Executors, ScheduledExecutorService |
1.3 JUC的设计思想
JUC的核心设计思想可以概括为:
- 底层依赖 :声明共享变量为
volatile,保证可见性 - 中层工具:使用CAS进行原子条件更新,实现线程同步
- 上层实现:基于AQS(AbstractQueuedSynchronizer)构建各种同步器
这种分层设计使得JUC既保持了高性能,又具备了良好的扩展性。
1.4 JUC与synchronized的关系
很多人误以为JUC是用来取代synchronized的,其实不然。两者是互补关系:
- synchronized:简单易用,由JVM实现,适合锁竞争不激烈的场景
- JUC锁:功能丰富,由Java实现,适合需要高级锁特性的场景
在实际开发中,应根据具体需求选择合适的技术。
第二部分:Lock体系与AQS原理(第一课时重点)
2.1 Lock接口概述
Lock接口是JUC锁框架的顶层接口,定义了锁的基本操作:
java
public interface Lock {
void lock(); // 获取锁,获取不到就阻塞
void lockInterruptibly(); // 可中断地获取锁
boolean tryLock(); // 尝试获取锁,立即返回
boolean tryLock(long time, TimeUnit unit); // 限时尝试获取锁
void unlock(); // 释放锁
Condition newCondition(); // 创建条件变量
}
相比synchronized,Lock提供了三大优势:
- 可中断 :
lockInterruptibly()允许在等待锁时响应中断 - 可超时 :
tryLock(timeout)避免无限期等待 - 可公平:支持公平锁,按请求顺序获取锁
2.2 ReentrantLock------可重入锁
2.2.1 基本用法
ReentrantLock是Lock接口最经典的实现,使用方式如下:
java
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 必须在finally中释放!
}
}
}
核心要点:
lock()和unlock()必须成对出现- 解锁必须放在
finally块中,确保无论如何都能释放锁 - 不能在
try块中调用lock(),因为lock()本身可能抛出异常
2.2.2 公平锁与非公平锁
ReentrantLock的构造方法可以指定是否公平:
java
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
ReentrantLock defaultLock = new ReentrantLock(); // 默认非公平
- 公平锁:线程按照请求锁的先后顺序获取锁,避免饥饿
- 非公平锁:允许插队,性能更高,但可能导致某些线程永远获取不到锁
为什么默认非公平?因为非公平锁减少了线程挂起和唤醒的开销,在高并发下吞吐量更高。
2.2.3 可中断与限时获取
java
public void performTask() throws InterruptedException {
// 可中断获取锁
lock.lockInterruptibly();
try {
// 执行任务
} finally {
lock.unlock();
}
}
public boolean tryPerform() {
// 尝试在3秒内获取锁
try {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 成功获取锁
return true;
} finally {
lock.unlock();
}
}
return false; // 获取锁超时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
2.2.4 synchronized与ReentrantLock对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM关键字 | Java API |
| 锁释放 | 自动释放 | 手动解锁 |
| 可中断 | 不支持 | 支持lockInterruptibly() |
| 超时尝试 | 不支持 | 支持tryLock(timeout) |
| 公平性 | 非公平 | 可设置公平/非公平 |
| 条件变量 | 一个等待集 | 多个Condition |
选择建议:
- 锁竞争不激烈时,用
synchronized,代码简洁 - 需要公平锁、可中断、超时等待时,用
ReentrantLock
2.3 ReentrantReadWriteLock------读写锁
2.3.1 概念与原理
ReentrantReadWriteLock维护了一对锁:
- 读锁(ReadLock):共享锁,多个读线程可同时持有
- 写锁(WriteLock):独占锁,只有一个写线程能持有
读写锁遵循三条规则:
- 读-读不互斥:多个读线程可并发执行
- 读-写互斥:读时不能写,写时不能读
- 写-写互斥:多个写线程互斥
2.3.2 使用示例
java
public class Cache {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private Map<String, Object> data = new HashMap<>();
public Object get(String key) {
readLock.lock();
try {
return data.get(key);
} finally {
readLock.unlock();
}
}
public void put(String key, Object value) {
writeLock.lock();
try {
data.put(key, value);
} finally {
writeLock.unlock();
}
}
}
2.3.3 锁降级
读写锁支持锁降级:写锁可以降级为读锁,但读锁不能升级为写锁。
java
writeLock.lock();
try {
// 修改数据
data.put(key, value);
// 在释放写锁前获取读锁,实现降级
readLock.lock();
} finally {
writeLock.unlock(); // 释放写锁,但读锁还在
}
try {
// 此时以读锁执行后续操作
} finally {
readLock.unlock();
}
2.4 AQS------JUC的基石
2.4.1 什么是AQS?
AQS(AbstractQueuedSynchronizer) 是JUC的核心基础设施,ReentrantLock、Semaphore、CountDownLatch等同步器都基于它实现。
AQS的核心思想是:如果请求的共享资源空闲,则将当前线程设置为工作线程;否则将线程加入等待队列。
2.4.2 AQS的核心组成
AQS维护了两个关键元素:
-
volatile int state:同步状态
- 对于
ReentrantLock,state表示锁的持有次数(0表示未持有,≥1表示重入次数) - 对于
Semaphore,state表示剩余许可数量 - 对于
CountDownLatch,state表示计数器剩余值
- 对于
-
FIFO等待队列(CLH队列变体):存放获取资源失败的线程
2.4.3 AQS的设计模式
AQS采用模板方法模式,子类只需实现特定方法:
| 需要子类实现的方法 | 描述 |
|---|---|
tryAcquire(int) |
独占式获取资源 |
tryRelease(int) |
独占式释放资源 |
tryAcquireShared(int) |
共享式获取资源 |
tryReleaseShared(int) |
共享式释放资源 |
isHeldExclusively() |
是否独占模式 |
以ReentrantLock为例,它的Sync类继承AQS,实现了tryAcquire和tryRelease。
2.4.4 AQS的工作流程
线程请求资源 → tryAcquire()尝试获取 → 成功则直接执行
↓ 失败
加入等待队列
↓
前驱节点是头节点?→ 是 → 再次尝试获取 → 成功则出队执行
↓ 否 ↓ 失败
阻塞等待
↓
被唤醒后重复上述过程
2.5 LockSupport
LockSupport是JUC中的基础工具类,提供了线程阻塞和唤醒的原语:
java
// 阻塞当前线程
LockSupport.park();
// 唤醒指定线程
LockSupport.unpark(thread);
与wait/notify相比,LockSupport的优势在于:
- 不需要获取锁即可使用
- 先调用
unpark再调用park,线程不会阻塞 - 可以唤醒指定线程,更精确
第三部分:原子类与CAS机制(第一课时重点)
3.1 原子类概述
java.util.concurrent.atomic包提供了一系列原子操作类:
| 类型 | 原子类 |
|---|---|
| 基本类型 | AtomicInteger, AtomicLong, AtomicBoolean |
| 数组类型 | AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray |
| 引用类型 | AtomicReference, AtomicStampedReference, AtomicMarkableReference |
| 字段更新器 | AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater |
3.2 为什么需要原子类?
看一个经典问题:多个线程同时执行count++,最终结果可能小于预期。
java
public class UnsafeCounter {
private int count = 0;
public void increment() { count++; } // 非原子操作
}
count++不是原子操作,它分解为三步:读→改→写。使用synchronized可以解决,但性能较差。原子类提供了更高效的解决方案:
java
public class SafeCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }
}
3.3 CAS原理详解
3.3.1 什么是CAS?
CAS(Compare And Swap) 是一条CPU原子指令,包含三个操作数:
- 内存地址V
- 期望值A
- 新值B
核心逻辑:判断内存地址V当前的值是否等于A,如果相等就将V更新为B,整个操作是原子的。
用伪代码表示:
java
boolean compareAndSwap(V, A, B) {
if (V.get() == A) {
V.set(B);
return true;
}
return false;
}
3.3.2 CAS在原子类中的应用
以AtomicInteger的incrementAndGet()为例:
java
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// unsafe.getAndAddInt的实现(类似)
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2); // 获取当前值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // CAS重试
return var5;
}
核心就是自旋+CAS:不断尝试,直到成功为止。
3.3.3 CAS的三大缺陷
缺陷一:ABA问题
假设线程1读到变量值为A,此时线程2将A改为B再改回A,线程1进行CAS时发现仍是A,于是更新成功。但实际上变量已经被修改过。
解决方案 :使用AtomicStampedReference,通过版本号/时间戳解决。
java
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
int[] stamp = new int[1];
Integer value = ref.get(stamp); // 获取值和版本号
// CAS时同时检查值和版本号
ref.compareAndSet(100, 200, stamp[0], stamp[0] + 1);
缺陷二:循环时间长开销大
如果CAS长时间不成功,会一直自旋,给CPU带来很大开销。
解决方案:
pause指令,减少CPU消耗- 自适应自旋(JVM优化)
缺陷三:只能保证一个共享变量的原子操作
解决方案:
- 将多个变量合并为一个对象,用
AtomicReference包装 - 使用锁
3.4 原子类使用示例
java
// 计数器场景
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // +1
count.addAndGet(5); // +5
count.getAndSet(10); // 设置为10,返回旧值
// 布尔标志
AtomicBoolean flag = new AtomicBoolean(false);
flag.compareAndSet(false, true); // 原子性修改标志位
// 对象引用
AtomicReference<User> userRef = new AtomicReference<>();
User oldUser = userRef.getAndSet(newUser);
第四部分:并发工具类(第二课时重点)
4.1 CountDownLatch------倒计数器
4.1.1 概念与用途
CountDownLatch允许一个或多个线程等待,直到其他线程执行完一组操作。
生活类比 :跑步比赛,裁判需要等待所有运动员冲过终点才能宣布比赛结束。运动员就是线程,裁判就是CountDownLatch。
4.1.2 核心方法
| 方法 | 描述 |
|---|---|
CountDownLatch(int count) |
构造器,初始化计数器值 |
await() |
使当前线程等待,直到计数器归零 |
await(long timeout, TimeUnit unit) |
限时等待 |
countDown() |
计数器减1 |
4.1.3 使用示例
java
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int workerCount = 5;
CountDownLatch latch = new CountDownLatch(workerCount);
for (int i = 0; i < workerCount; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 开始工作");
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + " 工作完成");
latch.countDown(); // 计数器减1
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "工人-" + i).start();
}
// 主线程等待所有工人完成
latch.await();
System.out.println("所有工人完成,主线程继续");
}
}
典型应用场景:
- 多线程下载大文件,需要等待所有分片下载完成才合并
- 并行计算,等待所有子任务完成才汇总结果
4.2 CyclicBarrier------循环屏障
4.2.1 概念与用途
CyclicBarrier让一组线程互相等待,直到所有线程都到达某个公共屏障点才继续执行。
生活类比:朋友约好一起去吃饭,约定在餐厅门口集合。所有人都到齐了,才一起进去。这个餐厅门口就是屏障点。
4.2.2 与CountDownLatch的区别
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 重用性 | 不可重用,计数器归零后失效 | 可重用,通过reset()重置 |
| 参与方 | 等待方和倒计时方分离 | 所有线程互相等待 |
| 计数方式 | 线程调用countDown()减1 |
线程调用await()进入等待 |
4.2.3 使用示例
java
public class CyclicBarrierDemo {
public static void main(String[] args) {
int threadCount = 5;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("所有线程已到达屏障,优先执行此任务");
});
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 开始第一阶段");
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + " 到达屏障");
barrier.await(); // 等待其他线程
System.out.println(Thread.currentThread().getName() + " 开始第二阶段");
} catch (Exception e) {
e.printStackTrace();
}
}, "线程-" + i).start();
}
}
}
4.3 Semaphore------信号量
4.3.1 概念与用途
Semaphore是一个计数信号量,用于控制同时访问特定资源的线程数量。
生活类比:停车场门口的电子牌显示剩余车位。每进一辆车,剩余车位减1(P操作);每出一辆车,剩余车位加1(V操作)。没车位时,车辆只能等待。
4.3.2 核心方法
| 方法 | 描述 |
|---|---|
Semaphore(int permits) |
构造器,指定许可数量 |
acquire() |
获取许可,没有则阻塞(P操作) |
release() |
释放许可(V操作) |
tryAcquire() |
尝试获取许可,立即返回 |
4.3.3 使用示例
java
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3); // 最多3个线程同时访问
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 获取许可,开始工作");
Thread.sleep(1000); // 模拟工作
System.out.println(Thread.currentThread().getName() + " 释放许可");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程-" + i).start();
}
}
}
4.3.4 二元信号量------可替代锁
如果将信号量初始化为1,它就相当于一个互斥锁:
java
Semaphore mutex = new Semaphore(1);
mutex.acquire(); // lock
try {
// 临界区
} finally {
mutex.release(); // unlock
}
4.4 Exchanger------交换器
4.4.1 概念与用途
Exchanger用于两个线程交换数据。当两个线程都到达交换点时,它们会交换彼此的数据。
4.4.2 使用示例
java
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
try {
String data1 = "来自线程1的数据";
System.out.println("线程1准备交换: " + data1);
String data2 = exchanger.exchange(data1);
System.out.println("线程1收到: " + data2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程1").start();
new Thread(() -> {
try {
String data1 = "来自线程2的数据";
System.out.println("线程2准备交换: " + data1);
String data2 = exchanger.exchange(data1);
System.out.println("线程2收到: " + data2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程2").start();
}
}
第五部分:并发集合(第二课时重点)
5.1 线程安全的集合概述
Java中常见的集合大部分都不是线程安全的,如ArrayList、HashMap、HashSet等。在多线程环境下,需要使用线程安全的替代品。
| 非线程安全 | 线程安全(传统) | 线程安全(JUC) |
|---|---|---|
HashMap |
Hashtable |
ConcurrentHashMap |
ArrayList |
Vector |
CopyOnWriteArrayList |
HashSet |
- | CopyOnWriteArraySet |
TreeMap |
- | ConcurrentSkipListMap |
TreeSet |
- | ConcurrentSkipListSet |
Queue |
- | ConcurrentLinkedQueue |
5.2 ConcurrentHashMap------并发哈希表
5.2.1 为什么不用Hashtable?
传统的Hashtable虽然线程安全,但采用全局锁,所有操作都互斥,并发性能极差。而ConcurrentHashMap做了大量优化。
5.2.2 JDK 1.7的实现:分段锁
在JDK 1.7中,ConcurrentHashMap采用分段锁机制:
-
内部维护一个
Segment数组,默认16个 -
每个
Segment继承自ReentrantLock,守护一个HashEntry数组 -
不同线程操作不同
Segment可以并发执行ConcurrentHashMap
├── Segment 0 (锁) → HashEntry[] → 链表
├── Segment 1 (锁) → HashEntry[] → 链表
├── ...
└── Segment 15 (锁) → HashEntry[] → 链表
优点 :理论上支持16个线程并发写(不同Segment)
缺点:并发度受限于Segment数量
5.2.3 JDK 1.8的演进:CAS + synchronized
JDK 1.8对ConcurrentHashMap进行了重构,彻底抛弃了Segment:
- 数据结构与
HashMap一致:数组 + 链表 + 红黑树 - 线程安全机制:CAS + synchronized
- 并发粒度从Segment级别细化到桶级别
JDK 1.8的同步策略:
- 若散列桶为空:使用CAS乐观锁,无锁化插入
- 若散列桶不为空 :对桶的头节点加
synchronized,其他桶仍可并发访问
java
// JDK 1.8 putVal方法核心逻辑
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ...
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化,使用CAS保证线程安全
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 桶为空,CAS插入
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
} else {
// 桶不为空,对头节点加synchronized锁
synchronized (f) {
// 链表或红黑树操作
}
}
}
}
5.2.4 扩容机制优化
JDK 1.8的扩容采用多线程协助扩容机制:
- 将大数组的扩容拆分为多个小任务
- 每个线程负责搬运一部分元素到新数组
- 扩容期间,新老数组共存,查询需要同时查两个数组
这种"化整为零"的扩容方式,极大地减少了扩容对系统的影响。
5.3 CopyOnWriteArrayList------写时复制列表
5.3.1 原理
CopyOnWriteArrayList采用写时复制策略:
- 读操作无锁,直接读取原数组
- 写操作时,先复制一份新数组,在新数组上进行修改,最后将原数组引用指向新数组
java
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制
newElements[len] = e;
setArray(newElements); // 替换引用
return true;
} finally {
lock.unlock();
}
}
5.3.2 适用场景
优点:
- 读操作无锁,性能极高
- 读多写少的场景非常适合
缺点:
- 内存占用高:每次写都复制整个数组
- 数据弱一致性:写操作不能立即被读操作看到
典型应用:黑白名单、监听器列表、缓存等读多写少场景。
5.4 BlockingQueue------阻塞队列
5.4.1 概念
BlockingQueue是支持阻塞操作的队列,常用于生产者-消费者模式。
5.4.2 常用实现类
| 实现类 | 特点 |
|---|---|
ArrayBlockingQueue |
基于数组的有界队列,FIFO |
LinkedBlockingQueue |
基于链表的可选有界队列,FIFO |
PriorityBlockingQueue |
支持优先级的无界队列 |
DelayQueue |
延迟队列,元素到期才能取出 |
SynchronousQueue |
不存储元素,每个put必须等待take |
5.4.3 生产者-消费者示例
java
public class ProducerConsumerDemo {
private static final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
// 生产者
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
queue.put(i);
System.out.println("生产: " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 消费者
new Thread(() -> {
try {
while (true) {
Integer value = queue.take();
System.out.println("消费: " + value);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
第六部分:线程池(第二课时重点)
6.1 为什么需要线程池?
线程的创建和销毁是有成本的。频繁创建销毁线程会导致:
- 系统开销大(每次创建都需要系统调用)
- 资源管理混乱
- 系统稳定性下降
线程池的优势:
- 复用线程,减少创建销毁开销
- 控制并发数,避免资源耗尽
- 统一管理,方便监控和调优
6.2 ThreadPoolExecutor核心参数
ThreadPoolExecutor是线程池的核心实现,它有7个关键参数:
java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
6.2.1 参数详解
- corePoolSize:核心线程数,即使空闲也会保留
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数量
- keepAliveTime:非核心线程空闲存活时间,超过此时间会被回收
- workQueue:任务队列,存放等待执行的任务
- threadFactory:线程工厂,用于创建新线程(可自定义)
- handler:拒绝策略,当任务无法处理时的处理方式
6.2.2 线程池工作流程
提交任务 →
如果当前线程数 < corePoolSize → 创建新线程执行任务
否则 → 任务加入 workQueue
如果队列已满 → 创建新线程(但不超过 maximumPoolSize)
如果线程数已达 maximumPoolSize → 执行拒绝策略
6.3 拒绝策略
当任务无法被处理时,线程池有4种内置拒绝策略:
| 策略 | 行为 |
|---|---|
AbortPolicy |
抛出RejectedExecutionException(默认) |
CallerRunsPolicy |
由提交任务的线程自己执行 |
DiscardPolicy |
直接丢弃,不抛异常 |
DiscardOldestPolicy |
丢弃队列中最老的任务,重新提交 |
6.4 线程池创建方式
6.4.1 通过Executors工厂类
Executors提供了几种快捷创建方式:
java
// 固定线程数线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 可缓存线程池
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 单线程线程池
ExecutorService singlePool = Executors.newSingleThreadExecutor();
// 定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
6.4.2 为什么不推荐Executors?
阿里巴巴Java开发手册明确规定:线程池不允许使用Executors创建,而是通过ThreadPoolExecutor的方式。
原因在于Executors创建的线程池存在资源耗尽风险:
| 类型 | 问题 |
|---|---|
FixedThreadPool |
使用无界LinkedBlockingQueue,任务积压可能OOM |
CachedThreadPool |
最大线程数无限,创建过多线程可能OOM |
SingleThreadPool |
同样使用无界队列 |
6.4.3 正确姿势:直接使用ThreadPoolExecutor
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60, TimeUnit.SECONDS, // keepAliveTime
new LinkedBlockingQueue<>(100), // 有界队列,容量100
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
6.5 线程数配置策略
合理配置线程数需要根据任务类型决定:
CPU密集型任务
- 特征:大量计算,CPU使用率高
- 建议:
线程数 = CPU核心数 + 1 - 原因:避免过多线程导致频繁上下文切换
IO密集型任务
- 特征:大量等待(网络、磁盘),CPU使用率低
- 建议:
线程数 = CPU核心数 * 2 - 原因:线程在等待IO时,CPU可以调度其他线程执行
混合型任务
- 将任务拆分为CPU密集和IO密集两部分,或通过压测确定最优值
6.6 线程池监控
通过ThreadPoolExecutor提供的方法可以监控线程池状态:
java
int activeCount = executor.getActiveCount(); // 活跃线程数
long taskCount = executor.getTaskCount(); // 总任务数
long completedCount = executor.getCompletedTaskCount(); // 已完成任务数
int queueSize = executor.getQueue().size(); // 队列长度
6.7 优雅关闭线程池
java
// 1. 不再接受新任务
executor.shutdown();
try {
// 2. 等待现有任务完成(最多60秒)
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// 3. 强制停止
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
第七部分:JUC优化与实战(第二课时重点)
7.1 锁优化策略
7.1.1 减少锁持有时间
java
// 不好的做法:锁住整个方法
public synchronized void process() {
prepare(); // 准备阶段(无需锁)
doWork(); // 需要同步
cleanup(); // 清理阶段(无需锁)
}
// 好的做法:只锁必要部分
public void process() {
prepare(); // 无锁
synchronized(this) {
doWork(); // 需要同步
}
cleanup(); // 无锁
}
7.1.2 减小锁粒度
- 使用
ConcurrentHashMap代替Hashtable - 使用读写锁代替独占锁
- 使用分段锁或桶锁
7.1.3 锁分离
读写锁本质就是一种锁分离。还有更进一步的分离,如LinkedBlockingQueue的取锁和存锁分离。
7.2 CAS优化
- 使用
LongAdder替代AtomicLong:在高并发下,LongAdder通过分段累加减少CAS冲突 - 合理设置自旋次数:避免长时间自旋浪费CPU
7.3 并发编程最佳实践
7.3.1 优先使用并发工具而非手动同步
java
// 不好的做法:手动同步
Map<String, String> map = new HashMap<>();
synchronized(map) {
map.put(key, value);
}
// 好的做法:使用ConcurrentHashMap
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put(key, value);
7.3.2 使用不可变对象
不可变对象天然线程安全,无需同步。例如String、基本类型包装类等。
7.3.3 线程封闭
通过ThreadLocal将共享变量限制在单个线程内,避免同步:
java
ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd")
);
// 每个线程有自己的SimpleDateFormat实例,无需同步
7.3.4 避免死锁
遵循固定锁顺序原则:
java
// 按对象hashCode决定获取顺序,避免循环等待
public void transferMoney(Account from, Account to, int amount) {
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash < toHash) {
synchronized (from) {
synchronized (to) {
// 转账逻辑
}
}
} else if (fromHash > toHash) {
synchronized (to) {
synchronized (from) {
// 转账逻辑
}
}
} else {
// 如果hashCode相等,加一个额外锁
synchronized (lock) {
synchronized (from) {
synchronized (to) {
// 转账逻辑
}
}
}
}
}
7.4 性能对比测试
| 场景 | 建议方案 | 原因 |
|---|---|---|
| 简单计数器 | AtomicInteger |
无锁,性能最高 |
| 读多写少Map | ConcurrentHashMap |
读无锁,并发度高 |
| 读多写少List | CopyOnWriteArrayList |
读无锁,适合读远多于写 |
| 高并发写Map | ConcurrentHashMap |
桶锁,并发度高 |
| 生产者-消费者 | BlockingQueue |
内置阻塞机制,使用简单 |
| 定时任务 | ScheduledThreadPoolExecutor |
比Timer更可靠 |
7.5 常见问题排查
7.5.1 线程池队列积压
- 原因:消费者处理速度跟不上生产者
- 对策:增加消费者线程数,或优化处理逻辑
7.5.2 死锁
- 现象:程序卡死,jstack显示BLOCKED状态
- 排查:使用
jstack查看锁持有和等待关系 - 解决:确保锁获取顺序一致,使用
tryLock带超时
7.5.3 内存泄漏
- 现象:
ConcurrentHashMap无限增长 - 原因:没有移除机制,或
ThreadLocal使用后未remove - 解决:确保有移除策略,
ThreadLocal用完后调用remove()
课程总结
通过两个课时的系统学习,我们全面覆盖了JUC并发编程的五大核心组件:
- locks包 :
ReentrantLock、读写锁,基于AQS的灵活锁机制 - atomic包:原子类与CAS原理,无锁编程的基石
- tools包 :
CountDownLatch、CyclicBarrier、Semaphore等并发工具 - collections包 :
ConcurrentHashMap、CopyOnWriteArrayList等线程安全容器 - executor包:线程池框架,合理的资源管理
核心原理回顾
- AQS:JUC的基石,通过state和CLH队列实现同步器
- CAS:原子操作的基础,但需注意ABA问题
- 锁升级:从偏向锁到轻量级锁再到重量级锁的动态演进
- 分段锁/桶锁:ConcurrentHashMap的并发优化思路
实战要点
- 线程池参数配置:根据任务类型(CPU密集/IO密集)合理设置
- 锁优化:减少持有时间、减小粒度、读写分离
- 工具选择:根据场景选择最合适的并发工具
- 异常处理 :妥善处理
InterruptedException,保留线程中断状态