一、并发基础核心概念
1.1 进程与线程
核心定义
- 进程 :程序的运行实例,负责加载指令、管理内存和 IO,是资源分配的最小单位(如记事本、浏览器运行时即为一个进程)。
- 线程 :进程内的指令流,是CPU 调度的最小单位,一个进程可包含多个线程(如浏览器的多个标签页对应多个线程)。
关键区别
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 独立性 | 相互独立 | 共享进程资源 |
| 通信成本 | 高(IPC / 网络协议) | 低(共享内存) |
| 上下文切换 | 成本高 | 成本低 |
1.2 并行与并发
核心定义(Rob Pike 经典描述)
- 并发(Concurrent) :同一时间应对多件事(单核 CPU 通过时间片切换实现,微观串行,宏观并行)。
- 并行(Parallel) :同一时间动手做多件事(多核 CPU 同时调度多个线程)。
生活案例
- 并发:家庭主妇独自做饭、打扫卫生、喂奶(轮流交替)。
- 并行:家庭主妇 + 3 个保姆,每人负责一件事(互不干扰)。
技术本质
- 单核 CPU:仅支持并发(线程轮流占用 CPU 时间片,切换速度快至人类无法感知)。
- 多核 CPU:支持并行(多个核心同时运行不同线程)。
2.3 并发的核心应用场景
- 异步调用:避免主线程阻塞(如视频格式转换、Tomcat 异步 Servlet、UI 程序后台任务)。
- 提高效率:多核 CPU 下并行计算(如 3 个耗时任务并行执行,总耗时取决于最长任务)。
- 资源统筹:合理分配线程执行任务(如烧水泡茶时,利用烧水时间洗茶壶、拿茶叶)。
二、Java 线程核心操作
2.1 线程创建的三种方式
方式 1:直接继承 Thread
java
运行
java
// 推荐指定线程名,便于调试
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("线程t1执行"); // 输出:19:19:00 [t1] c.ThreadStarter - 线程t1执行
}
};
t1.start(); // 启动线程(不可重复调用)
方式 2:Runnable 配合 Thread(解耦线程与任务)
java
// 1. 定义任务(线程要执行的逻辑)
Runnable task2 = () -> log.debug("线程t2执行"); // JDK8+ lambda简化
// 2. 创建线程(参数1:任务,参数2:线程名)
Thread t2 = new Thread(task2, "t2");
t2.start(); // 输出:19:19:00 [t2] c.ThreadStarter - 线程t2执行
方式 3:FutureTask 处理有返回值的任务
java
// 1. 创建带返回值的任务(Callable接口)
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("线程t3执行");
return 100; // 返回结果
});
// 2. 启动线程
new Thread(task3, "t3").start();
// 3. 主线程同步等待结果(阻塞)
Integer result = task3.get();
log.debug("线程t3返回结果:{}", result); // 输出:19:22:27 [main] c.ThreadStarter - 线程t3返回结果:100
三种方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Thread 继承 | 代码简洁 | 单继承限制 | 简单任务 |
| Runnable | 解耦线程与任务 | 无返回值 | 多线程共享任务 |
| FutureTask | 支持返回值 | 需处理异常 | 需获取任务结果(如并行计算) |
3.2 线程核心 API 详解
常用方法(重点)
| 方法名 | 功能 | 关键注意点 |
|---|---|---|
start() |
启动线程(进入就绪状态) | 仅能调用 1 次,重复调用抛异常 |
run() |
线程执行的任务逻辑 | 直接调用不会启动新线程(主线程执行) |
join() |
等待线程执行完毕 | 可指定超时时间(join(long n)) |
interrupt() |
打断线程 | 打断阻塞线程抛InterruptedException |
sleep(long n) |
休眠 n 毫秒 | 不释放锁,推荐用TimeUnit.sleep() |
yield() |
让出 CPU 使用权 | 仅为提示,调度器可忽略 |
getState() |
获取线程状态 | 返回 6 种状态(NEW/RUNNABLE 等) |
核心 API 实战
- start 与 run 的区别
java
// 错误:直接调用run(主线程执行)
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName()); // 输出main
}
};
t1.run();
// 正确:调用start(新线程执行)
t1.start(); // 输出t1
- join 等待线程完成
java
static int r = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1000); // 模拟耗时操作
r = 10;
});
t1.start();
t1.join(); // 等待t1执行完毕
log.debug("r的值:{}", r); // 输出10(而非初始0)
}
- interrupt 打断线程
java
// 打断阻塞线程(sleep)
Thread t1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("线程被打断");
return;
}
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt(); // 打断t1的sleep
3.3 线程状态(Java API 层面)
6 种状态及转换
| 状态 | 含义 | 转换场景 |
|---|---|---|
| NEW | 线程未启动 | 创建线程后未调用start() |
| RUNNABLE | 就绪 / 运行 / IO 阻塞 | 调用start()后 |
| BLOCKED | 等待锁 | 竞争synchronized锁失败 |
| WAITING | 无超时等待 | 调用wait()/join()/park() |
| TIMED_WAITING | 有超时等待 | 调用sleep(long)/wait(long) |
| TERMINATED | 线程执行完毕 | 任务执行完成或异常终止 |
状态转换流程图
java
NEW → RUNNABLE(start())
RUNNABLE ↔ BLOCKED(竞争synchronized锁)
RUNNABLE ↔ WAITING(wait()/park()等)
RUNNABLE ↔ TIMED_WAITING(sleep()/wait(long)等)
RUNNABLE → TERMINATED(任务完成)
3.4 主线程与守护线程
核心定义
- 守护线程:依赖非守护线程,当所有非守护线程结束,守护线程强制终止(无需执行完)。
- 非守护线程:默认线程类型(如主线程),需等待自身执行完毕。
实战示例:
java
log.debug("主线程启动");
Thread daemonThread = new Thread(() -> {
log.debug("守护线程启动");
Thread.sleep(2000); // 模拟耗时操作
log.debug("守护线程执行完毕"); // 不会输出(主线程1秒后结束)
}, "daemon");
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
Thread.sleep(1000);
log.debug("主线程执行完毕");
常见应用
- 垃圾回收器(GC)线程:守护线程(JVM 退出时无需等待 GC 完成)。
- Tomcat 的 Acceptor/Poller 线程:守护线程(Tomcat 关闭时无需等待当前请求处理)。
四、共享模型之管程(synchronized 与 Lock)
4.1 共享资源的线程安全问题
问题本质
多线程对共享资源进行读写操作时,指令交错导致结果不可预测(竞态条件)。
经典案例:静态变量自增自减:
java
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
// 线程1:自增5000次
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) counter++;
}, "t1");
// 线程2:自减5000次
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) counter--;
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("counter结果:{}", counter); // 结果可能为正数、负数、0
}
问题分析(字节码层面)
counter++对应的字节码指令(非原子操作):
getstatic:读取主存中counter的值到工作内存。iconst_1:准备常量 1。iadd:工作内存中自增。putstatic:将结果写回主存。
多线程下指令交错执行,导致最终结果错误。
4.2 synchronized 解决方案(对象锁)
核心原理
通过互斥锁保证同一时刻仅有一个线程进入临界区(读写共享资源的代码块),避免指令交错。
- 代码块形式(推荐,锁粒度可控)
java
static final Object lock = new Object(); // 锁对象(任意对象均可)
static int counter = 0;
// 临界区代码
synchronized (lock) {
counter++; // 原子操作
}
- 成员方法形式(锁当前对象 this)
java
public synchronized void increment() {
counter++;
}
- 静态方法形式(锁类对象 Class)
java
public static synchronized void decrement() {
counter--;
}
解决线程安全问题
java
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("counter结果:{}", counter); // 稳定输出0
}
关键注意点
- 锁对象必须唯一(多线程竞争同一把锁才有效)。
- 临界区代码应尽量精简(减少锁持有时间,提高并发度)。
synchronized自动释放锁(异常或代码执行完毕时)。
4.3 线程八锁(synchronized 锁对象面试题)
核心考点:判断synchronized锁住的对象
| 情况 | 代码示例 | 执行结果 | 结论 |
|---|---|---|---|
| 1 | 两个同步成员方法,同一对象调用 | 12 或 21 | 锁 this,同一对象互斥 |
| 2 | 同步成员方法 + 非同步方法,同一对象调用 | 3 先输出,再 12 或 21 | 非同步方法不参与锁竞争 |
| 3 | 两个同步静态方法,不同对象调用 | 1s 后 12 或 21 | 锁 Class 对象,全局互斥 |
| 4 | 同步成员方法 + 同步静态方法,同一对象调用 | 2 先输出,1s 后 1 | 锁对象不同(this vs Class),不互斥 |
4.4 Lock 接口(synchronized 增强版)
Lock 与 synchronized 对比
| 特性 | synchronized | Lock(ReentrantLock) |
|---|---|---|
| 可中断 | 否 | 是(lockInterruptibly()) |
| 超时获取 | 否 | 是(tryLock(long)) |
| 公平锁 | 否 | 是(构造函数指定true) |
| 多条件变量 | 否 | 是(newCondition()) |
| 手动释放 | 否(自动) | 是(必须在 finally 中unlock()) |
ReentrantLock 核心用法
java
// 1. 创建Lock对象(公平锁)
ReentrantLock lock = new ReentrantLock(true);
// 2. 条件变量(实现线程间通信)
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// 3. 加锁与释放锁(必须在finally中释放)
lock.lock();
try {
// 临界区代码
while (条件不满足) {
notEmpty.await(); // 等待
}
// 业务逻辑
notFull.signal(); // 唤醒其他线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
可中断锁实战
java
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("t1启动,尝试获取锁");
try {
lock.lockInterruptibly(); // 可被打断的锁
} catch (InterruptedException e) {
log.debug("t1获取锁时被打断");
return;
}
try {
log.debug("t1获取锁成功");
} finally {
lock.unlock();
}
}, "t1");
lock.lock(); // 主线程先获取锁
log.debug("主线程获取锁成功");
t1.start();
Thread.sleep(1000);
t1.interrupt(); // 打断t1的锁等待
log.debug("主线程执行打断");
lock.unlock();
4.5 线程间通信(wait/notify vs Condition)
1. Object 的 wait/notify(synchronized 配套)
java
static final Object lock = new Object();
static boolean hasCigarette = false;
public static void main(String[] args) {
// 等待线程
new Thread(() -> {
synchronized (lock) {
while (!hasCigarette) { // 避免虚假唤醒,用while而非if
try {
log.debug("没烟,等待...");
lock.wait(); // 释放锁,进入等待队列
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟了,开始干活");
}
}, "小南").start();
// 唤醒线程
new Thread(() -> {
synchronized (lock) {
hasCigarette = true;
log.debug("烟到了,唤醒等待线程");
lock.notifyAll(); // 唤醒所有等待线程
}
}, "送烟的").start();
}
2. Condition(Lock 配套,多条件精准唤醒)
java
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigarette = lock.newCondition(); // 等烟的条件
static Condition waitBreakfast = lock.newCondition(); // 等早餐的条件
static boolean hasCigarette = false;
static boolean hasBreakfast = false;
public static void main(String[] args) {
// 等烟的线程
new Thread(() -> {
lock.lock();
try {
while (!hasCigarette) {
waitCigarette.await();
}
log.debug("等到烟,干活");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
// 等早餐的线程
new Thread(() -> {
lock.lock();
try {
while (!hasBreakfast) {
waitBreakfast.await();
}
log.debug("等到早餐,干活");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
// 送早餐(精准唤醒等早餐的线程)
new Thread(() -> {
lock.lock();
try {
hasBreakfast = true;
waitBreakfast.signal();
} finally {
lock.unlock();
}
}).start();
}
4.6 活跃性问题(死锁 / 活锁 / 饥饿)
1. 死锁(最常见)
死锁条件
- 资源互斥
- 持有并等待
- 不可剥夺
- 循环等待
死锁示例
java
Object A = new Object();
Object B = new Object();
// 线程1:持有A,等待B
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("t1持有A,等待B");
Thread.sleep(1000);
synchronized (B) {
log.debug("t1持有A和B");
}
}
}, "t1");
// 线程2:持有B,等待A
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("t2持有B,等待A");
Thread.sleep(1000);
synchronized (A) {
log.debug("t2持有B和A");
}
}
}, "t2");
t1.start();
t2.start();
死锁排查
- 工具:jconsole(图形化)、jstack(命令行)。
- 命令:
jps获取 PID →jstack PID查看死锁信息。
死锁避免
- 按固定顺序获取锁(如先获取 A 再获取 B)。
- 超时获取锁(
tryLock(long))。 - 避免一次性获取多把锁。
2. 活锁与饥饿
- 活锁:线程互相修改对方结束条件,无法终止(如线程 1 减 count,线程 2 加 count,永远无法满足退出条件)。
- 饥饿:低优先级线程长期得不到 CPU 调度(如高优先级线程持续占用 CPU)。
五、共享模型之内存(JMM)
5.1 JMM 核心目标
解决多线程下的三大问题:
- 原子性:指令不可分割(避免线程切换导致指令交错)。
- 可见性:一个线程对共享变量的修改,其他线程能立即看到(避免 CPU 缓存导致的旧值读取)。
- 有序性:避免指令重排导致的执行顺序混乱。
5.2 可见性问题
问题示例(退不出的循环)
java
static boolean run = true; // 未加volatile
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
// 循环体为空
}
});
t.start();
Thread.sleep(1000);
run = false; // 主线程修改run,t线程看不到,无法退出
}
问题原因
- 线程 t 频繁读取
run,JIT 编译器将其缓存到 CPU 高速缓存,主线程修改后,t 线程仍读取缓存中的旧值。
解决方案:volatile 关键字
java
static volatile boolean run = true; // 加volatile
volatile 核心作用
- 强制线程读写变量时直接操作主存(不经过 CPU 缓存)。
- 禁止指令重排(保证有序性)。
注意:volatile 不保证原子性
java
static volatile int i = 0;
public static void main(String[] args) throws InterruptedException {
// 1000个线程,每个自增1000次
for (int j = 0; j < 1000; j++) {
new Thread(() -> {
for (int k = 0; k < 1000; k++) i++;
}).start();
}
Thread.sleep(2000);
log.debug("i的值:{}", i); // 结果小于1000000(原子性问题)
}
5.3 有序性问题(指令重排)
指令重排定义
JVM 在不影响单线程正确性的前提下,调整指令执行顺序(如i=1; j=2可能重排为j=2; i=1)。
多线程下的问题示例
java
int num = 0;
boolean ready = false;
// 线程1
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num; // 可能为0(指令重排导致)
} else {
r.r1 = 1;
}
}
// 线程2
public void actor2(I_Result r) {
num = 2;
ready = true; // 可能重排到num=2之前执行
}
解决方案
- 用
volatile修饰ready(禁止指令重排)。 - 用
synchronized/Lock(保证原子性的同时保证有序性)。
5.4 happens-before 规则(可见性 / 有序性保障)
JMM 定义的一套规则,保证线程间操作的可见性:
- 锁解锁 → 后续加锁可见(
synchronized/Lock)。 volatile写 → 后续volatile读可见。- 线程
start()前的写 → 线程内读可见。 - 线程结束前的写 → 其他线程
join()后读可见。 - 传递性:A→B 且 B→C,则 A→C。
六、共享模型之无锁(CAS)
6.1 CAS 核心原理
定义
Compare And Swap(比较并交换),基于乐观锁思想:
- 核心操作:
compareAndSet(预期值, 新值)。 - 逻辑:如果当前值等于预期值,则更新为新值(原子操作),否则重试。
底层实现
CPU 的lock cmpxchg指令(单核 / 多核均保证原子性),多核下通过总线锁定保证操作原子性。
CAS 实战(原子类 AtomicInteger)
java
public class AccountSafe implements Account {
private AtomicInteger balance;
public AccountSafe(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public void withdraw(Integer amount) {
while (true) {
int prev = balance.get(); // 获取当前值
int next = prev - amount; // 计算新值
// CAS更新:预期值prev,新值next
if (balance.compareAndSet(prev, next)) {
break;
}
}
// 简化写法:balance.addAndGet(-amount);
}
@Override
public Integer getBalance() {
return balance.get();
}
}
6.2 原子类家族
1. 基本类型原子类
AtomicInteger/AtomicLong/AtomicBoolean:解决基本类型的原子操作。- 核心方法:
getAndIncrement()(i++)、incrementAndGet()(++i)、compareAndSet()。
2. 引用类型原子类
AtomicReference:原子操作引用类型。AtomicStampedReference:解决 ABA 问题(加版本号)。AtomicMarkableReference:解决 ABA 问题(加标记,不关心修改次数)。
3. ABA 问题及解决
ABA 问题示例
java
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
// 线程1:A→B→A
new Thread(() -> {
String prev = ref.get();
log.debug("修改A→B");
ref.compareAndSet(prev, "B");
log.debug("修改B→A");
ref.compareAndSet("B", "A");
}, "t1").start();
Thread.sleep(1000);
// 线程2:A→C(无法感知A被修改过)
String prev = ref.get();
log.debug("修改A→C:{}", ref.compareAndSet(prev, "C")); // 输出true
}
解决方案:AtomicStampedReference
java
// 初始化:值A,版本号0
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
// 线程1:A→B→A(版本号从0→1→2)
new Thread(() -> {
int stamp = ref.getStamp(); // 获取当前版本号0
log.debug("当前版本:{}", stamp);
ref.compareAndSet("A", "B", stamp, stamp + 1); // 0→1
ref.compareAndSet("B", "A", 1, 2); // 1→2
}, "t1").start();
Thread.sleep(1000);
// 线程2:用版本号0修改A→C(失败)
int stamp = ref.getStamp(); // 此时版本号为2
log.debug("修改A→C:{}", ref.compareAndSet("A", "C", 0, 1)); // 输出false
}
6.3 原子累加器(LongAdder)
为什么比 AtomicLong 快?
AtomicLong:高并发下 CAS 重试频繁,效率低。LongAdder:分段累加(多个 Cell 单元),减少 CAS 竞争,最后汇总结果。
实战对比
java
// LongAdder累加(高并发高效)
LongAdder adder = new LongAdder();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
adder.increment();
}
}).start();
}
// AtomicLong累加(高并发低效)
AtomicLong atomicLong = new AtomicLong();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
atomicLong.incrementAndGet();
}
}).start();
}
七、共享模型之不可变
7.1 不可变类定义
- 类的状态不可修改(属性为
final)。 - 没有修改状态的方法(仅提供读操作)。
- 类声明为
final(避免子类破坏不可变性)。
经典示例:String 类
java
public final class String implements Serializable {
private final char value[]; // final属性,不可修改
// 无修改value的方法(replace/substring等方法返回新String)
}
7.2 不可变类的线程安全
不可变类天然线程安全(无状态修改,无需锁),例如:
String、Integer、Long等包装类。- Java 8 的
DateTimeFormatter(线程安全,替代SimpleDateFormat)。
实战:日期格式化(线程安全)
java
// DateTimeFormatter是不可变类,线程安全
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LocalDate date = dtf.parse("2024-01-01", LocalDate::from);
log.debug("{}", date);
}).start();
}
7.3 无状态设计
- 无状态类:没有成员变量(状态信息),天然线程安全。
- 应用场景:Servlet(避免设置成员变量)、工具类(如
StringUtils)。
八、JUC 核心工具实战
8.1 线程池(ThreadPoolExecutor)
线程池核心作用
- 复用线程(避免线程创建 / 销毁的开销)。
- 控制线程数量(防止资源耗尽)。
- 管理任务队列(缓冲任务)。
核心参数
java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(常驻线程)
int maximumPoolSize, // 最大线程数(核心+救急线程)
long keepAliveTime, // 救急线程空闲时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂(命名线程)
RejectedExecutionHandler handler // 拒绝策略
)
常见线程池(Executors 工具类)
| 线程池类型 | 核心参数 | 适用场景 |
|---|---|---|
| newFixedThreadPool | 核心 = 最大线程数,无救急线程 | 任务量固定,耗时较长 |
| newCachedThreadPool | 核心 = 0,最大 = Integer.MAX_VALUE | 任务密集,执行时间短 |
| newSingleThreadExecutor | 核心 = 最大 = 1 | 单线程串行执行任务 |
| newScheduledThreadPool | 核心线程数固定 | 定时 / 周期性任务 |
线程池工作流程
- 提交任务 → 核心线程未满 → 创建核心线程执行。
- 核心线程已满 → 任务队列未满 → 放入队列。
- 队列已满 → 未达最大线程数 → 创建救急线程执行。
- 达最大线程数 → 执行拒绝策略。
拒绝策略
| 策略 | 作用 |
|---|---|
| AbortPolicy | 抛RejectedExecutionException(默认) |
| CallerRunsPolicy | 调用者执行任务 |
| DiscardPolicy | 放弃当前任务 |
| DiscardOldestPolicy | 放弃队列中最早的任务 |
实战:自定义线程池
java
// 1. 创建线程池(核心2,最大5,队列容量10)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
5,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 2. 提交10个任务
for (int i = 0; i < 10; i++) {
int finalI = i;
pool.submit(() -> {
log.debug("执行任务{}", finalI);
Thread.sleep(1000);
});
}
// 3. 关闭线程池
pool.shutdown(); // 等待任务执行完毕后关闭
// pool.shutdownNow(); // 立即关闭,中断正在执行的任务
8.2 并发集合(java.util.concurrent)
常用并发集合对比
| 集合类型 | 线程安全实现 | 适用场景 |
|---|---|---|
| ConcurrentHashMap | CAS + 分段锁 | 高并发键值对存储 |
| CopyOnWriteArrayList | 写入时拷贝数组 | 读多写少(如配置列表) |
| BlockingQueue | 锁 + 条件变量 | 生产者 - 消费者模型 |
| ConcurrentLinkedQueue | CAS | 高并发队列(无阻塞) |
ConcurrentHashMap 实战(单词计数)
java
// 高并发下单词计数(线程安全)
ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();
List<String> words = Arrays.asList("a", "b", "a", "c", "b", "a");
// 多线程计数
for (String word : words) {
// 不存在则创建LongAdder,存在则自增
map.computeIfAbsent(word, k -> new LongAdder()).increment();
}
// 输出结果
map.forEach((k, v) -> log.debug("{}: {}", k, v.sum()));
BlockingQueue 实战(生产者 - 消费者)
java
// 阻塞队列(容量3)
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
// 生产者线程
new Thread(() -> {
try {
queue.put("任务1");
queue.put("任务2");
queue.put("任务3");
queue.put("任务4"); // 队列满,阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "生产者").start();
// 消费者线程
new Thread(() -> {
try {
Thread.sleep(1000);
log.debug("消费:{}", queue.take());
Thread.sleep(1000);
log.debug("消费:{}", queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "消费者").start();
8.3 同步工具类
1. Semaphore(信号量)
- 作用:限制同时访问共享资源的线程数(如限流)。
- 实战:3 个许可,10 个线程竞争。
java
Semaphore semaphore = new Semaphore(3); // 3个许可
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
log.debug("运行中");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
}, "t" + i).start();
}
2. CountDownLatch(倒计时器)
- 作用:等待多个线程完成后再执行主线程。
- 实战:等待 3 个线程完成。
java
CountDownLatch latch = new CountDownLatch(3);
// 3个工作线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
log.debug("线程执行完毕");
latch.countDown(); // 计数减1
}, "t" + i).start();
}
latch.await(); // 等待计数为0
log.debug("所有线程执行完毕,主线程继续");
3. CyclicBarrier(循环栅栏)
- 作用:等待线程达到计数后,同时继续执行(可重用)。
- 实战:3 个线程到达后,执行回调。
java
// 3个线程到达后,执行回调
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
log.debug("所有线程到达,执行回调");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
log.debug("线程到达栅栏");
try {
barrier.await(); // 等待其他线程
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
log.debug("线程继续执行");
}, "t" + i).start();
}
九、实战案例:烧水泡茶(统筹并发)
需求
用两个线程模拟烧水泡茶流程,优化时间(洗水壶 1min → 烧开水 15min → 泡茶;烧开水期间洗茶壶 1min、洗茶杯 2min、拿茶叶 1min)。
实现代码
java
public class MakeTea {
// 锁对象
static final Object lock = new Object();
// 标记:水是否烧开
static boolean isBoiled = false;
public static void main(String[] args) {
// 线程1:洗水壶→烧开水
new Thread(() -> {
synchronized (lock) {
log.debug("洗水壶(1min)");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("烧开水(15min)");
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isBoiled = true;
lock.notify(); // 唤醒线程2
}
}, "线程A").start();
// 线程2:等待水烧开→洗茶壶→洗茶杯→拿茶叶→泡茶
new Thread(() -> {
synchronized (lock) {
while (!isBoiled) {
try {
log.debug("等待水烧开...");
lock.wait(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("洗茶壶(1min)");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("洗茶杯(2min)");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("拿茶叶(1min)");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("泡茶");
}
}, "线程B").start();
}
}
执行结果(总耗时≈16min)
java
10:00:00 [线程A] c.MakeTea - 洗水壶(1min)
10:00:01 [线程A] c.MakeTea - 烧开水(15min)
10:00:01 [线程B] c.MakeTea - 等待水烧开...
10:00:16 [线程B] c.MakeTea - 洗茶壶(1min)
10:00:17 [线程B] c.MakeTea - 洗茶杯(2min)
10:00:19 [线程B] c.MakeTea - 拿茶叶(1min)
10:00:20 [线程B] c.MakeTea - 泡茶
十、总结与拓展
核心知识点梳理
- 基础层:进程 / 线程、并发 / 并行、线程状态与 API。
- 锁机制:synchronized(对象锁)、Lock(可中断 / 超时 / 公平锁)。
- 内存模型:JMM 三大特性(原子性 / 可见性 / 有序性)、volatile、happens-before。
- 无锁编程:CAS、原子类、ABA 问题解决。
- 工具层:线程池、并发集合、同步工具类(Semaphore/CountDownLatch 等)。
实际应用建议
- 优先使用 JUC 工具类(如线程池、ConcurrentHashMap),避免手动创建线程。
- 简单线程安全场景用
synchronized,复杂场景用Lock(如需要超时 / 中断)。 - 读多写少用
CopyOnWriteArrayList,高并发键值对用ConcurrentHashMap。 - 避免死锁:按固定顺序加锁、超时获取锁。
拓展学习方向
- 异步编程:CompletableFuture、Guava ListenableFuture。
- 反应式编程:Project Reactor、Spring WebFlux。
- 分布式并发:分布式锁(Redis/ZooKeeper)、分布式计数器。
- 性能优化:无锁编程、CAS 优化、CPU 缓存优化。