Java 多线程编程笔记
一、多线程概述
1.1 什么是多线程
- Multi‑threading:一个程序内同时运行多个线程,并发执行不同任务。
- 线程:CPU 调度的最小单位。比进程更轻量(创建/切换开销小)。
1.2 多线程的优势
- 提高持续响应性(UI 不卡顿)
- 充分利用多核 CPU
- 改善用户体验
- 提高系统吞吐量
1.3 多线程面临的问题
| 问题 | 说明 |
|---|
| 线程安全 | 数据竞争、共享资源并发访问、原子性被破坏 |
| 死锁 | 线程相互等待资源,程序永久阻塞 |
| 内存一致性 | 缓存、指令重排、可见性问题 |
| 调试困难 | 并发 Bug 不可复现、时序依赖、执行路径复杂 |
二、Java 多线程模型
- JVM 线程模型:线程映射(通常 1:1 映射到 OS 线程)
- 线程调度:由操作系统调度器决定(抢占式 + 时间片轮转)
- Java 内存模型(JMM):定义变量的可见性、有序性、原子性规则
三、线程创建方式(4 种)
| 方式 | 实现 | 特点 |
|---|
| 继承 Thread 类 | class MyThread extends Thread { run() } | 简单,但受单继承限制 |
| 实现 Runnable | class MyRunnable implements Runnable { run() } | 避免单继承,可共享任务实例 |
| 实现 Callable | class MyCallable implements Callable<T> { call() } | 有返回值、可抛异常 |
| 线程池 | Executors.newFixedThreadPool(n) | 生产首选,资源复用 |
示例(Lambda 简化):
new Thread(() -> System.out.println("run")).start();
3.1 Callable + Future 工作流程(“点菜模型”)
- 你点菜 → 提交
Callable 任务 - 厨师做菜 → 线程池异步执行
- 服务员给你叫号器 → 返回
Future 对象 - 你玩手机 → 主线程不阻塞
- 想吃饭了,去取餐 → 调用
future.get() - 没做好就等(阻塞),做好了直接拿走
Future<Integer> future = executor.submit(() -> 100);
Integer result = future.get(); // 阻塞直到结果返回
四、线程基础操作 & 常用方法
4.1 核心方法清单
| 方法 | 作用 | 状态变化 |
|---|
start() | 启动线程 | NEW → RUNNABLE |
Thread.sleep(millis) | 休眠 | RUNNABLE → TIMED_WAITING |
join() | 等待该线程结束 | RUNNABLE → WAITING |
join(timeout) | 等待超时 | RUNNABLE → TIMED_WAITING |
yield() | 让出 CPU(仍在就绪队列) | RUNNABLE(不改变状态) |
Thread.currentThread() | 获取当前线程 | - |
setName() / getName() | 设置/获取线程名 | - |
setDaemon(true) | 设为守护线程 | 必须在 start() 前调用 |
4.2 线程属性
- 优先级:
setPriority(1~10),仅建议,不保证顺序。 - 守护线程:JVM 退出时自动终止(如 GC 线程)。
4.3 异常处理(重点)
- 普通
try-catch 抓不住子线程里的异常。 - 使用
UncaughtExceptionHandler:
Thread thread = new Thread(() -> { throw new RuntimeException("崩了"); });
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("线程 " + t.getName() + " 异常:" + e.getMessage());
});
thread.start();
五、线程生命周期(完整 6 种状态)
NEW → RUNNABLE → BLOCKED / WAITING / TIMED_WAITING → TERMINATED
| 状态 | 触发条件 |
|---|
| NEW | new Thread(),尚未 start() |
| RUNNABLE | start() 后,等待 CPU 调度 |
| BLOCKED | 进入 synchronized 但锁被占用 |
| WAITING | wait()、join() 无超时、LockSupport.park() |
| TIMED_WAITING | sleep(time)、wait(time)、join(time) |
| TERMINATED | run() 执行完毕或异常退出 |
六、线程调度机制
- 抢占式调度:高优先级可抢占低优先级(但不保证)。
- 时间片轮转:同优先级线程轮流获得时间片。
- 公平调度:JVM 尽量避免线程饥饿。
七、线程监控工具
| 工具 | 用途 |
|---|
jstack | 命令行生成线程堆栈快照(定位死锁) |
jconsole | 图形化监控(线程、内存、类加载) |
VisualVM | 功能最强大的可视化工具 |
八、线程间协作(wait / notify / notifyAll)
- 必须在
synchronized 块内调用,否则 IllegalMonitorStateException。 wait():释放锁,进入 WAITING,等待 notify/notifyAll。notify():随机唤醒一个等待线程。notifyAll():唤醒所有等待线程。
synchronized (lock) {
while (条件不满足) lock.wait();
// 条件满足,执行操作
lock.notifyAll();
}
九、同步机制详解(从简单到复杂)
9.1 synchronized(内置锁,无脑首选)
- 自动加锁/解锁,不会忘记释放。
- 锁某个对象 → 最小粒度,性能最好。
- 锁当前对象
this → 整个方法串行。 - 锁 Class 对象 → 全局锁。
// 同步代码块(推荐)
private final Object lock = new Object();
public void deduct() {
synchronized(lock) { /* 临界区 */ }
}
// 同步方法
public synchronized void add() { count++; }
// 静态同步方法(类锁)
public static synchronized void globalMethod()
9.2 ReentrantLock(显式锁,更灵活)
- 需要手动
lock() / unlock(),必须放在 finally 中。 高级功能:
tryLock(timeout):超时放弃,避免死锁。lockInterruptibly():可中断抢锁。new ReentrantLock(true):公平锁(排队不插队)。- 多
Condition:精准唤醒指定线程。
private final Lock lock = new ReentrantLock();
public void update() {
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
9.3 volatile(轻量级同步)
- 只保证:可见性 + 禁止指令重排。
- 不保证:原子性(不能用于
count++)。 - 适用场景:布尔状态标记、单例双重检查中的
instance。
private volatile boolean stop = false;
public void run() {
while (!stop) { /* 执行任务 */ }
}
public void stopTask() { stop = true; }
9.4 原子类(无锁,CAS)
AtomicInteger、AtomicLong、AtomicReference 等。- 高并发计数用
LongAdder(分段累加,性能更高)。
private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 原子+1
9.5 线程安全集合(必记两个)
ConcurrentHashMap:分段锁 + CAS,高并发 Map。CopyOnWriteArrayList:读多写少场景,写时复制数组,不阻塞读。
Map<String, Integer> map = new ConcurrentHashMap<>();
List<String> list = new CopyOnWriteArrayList<>();
9.6 ThreadLocal(线程隔离)
- 每个线程拥有自己的变量副本,不竞争。
- 必须
remove(),否则内存泄漏(尤其在线程池中)。
private ThreadLocal<String> userLocal = new ThreadLocal<>();
userLocal.set("张三");
String user = userLocal.get();
userLocal.remove(); // 用完必须清
9.7 同步机制选择总结(背诵)
| 场景 | 推荐方案 |
|---|
| 简单逻辑同步(库存、判断) | synchronized |
| 需要超时、中断、公平锁 | ReentrantLock |
| 线程开关、状态标记 | volatile |
| 计数、累加、ID 生成 | AtomicInteger / LongAdder |
| 多线程操作 Map / List | ConcurrentHashMap / CopyOnWriteArrayList |
| 单例中存线程私有数据 | ThreadLocal |
禁忌:
- ❌ 不要用
volatile 做 i++ - ❌ 不要在单例 Bean 中写普通成员变量(若多线程修改)
- ❌ 不要用
HashMap / ArrayList 做并发写入 - ❌ 不要只锁一半逻辑(如只锁修改,不锁判断)
十、死锁
10.1 死锁的四个必要条件(缺一不可)
- 互斥:资源只能被一个线程占用。
- 请求与保持:拿着自己的资源,还请求别人的资源。
- 不可剥夺:资源不能被强行抢走。
- 循环等待:线程间形成等待闭环。
10.2 避免死锁的方法
- 破坏任意一个条件(如规定锁的顺序、使用
tryLock 超时放弃)。
十一、优雅停止线程
- 废弃:
stop()、suspend()、resume()(不安全)。 推荐方式:
- 中断机制:
thread.interrupt() 设置中断标志,线程内检查 Thread.currentThread().isInterrupted() 自行停止。 - 标志位:
volatile boolean running,线程定期检查。
// 标志位方式
public class MyTask implements Runnable {
private volatile boolean running = true;
public void stop() { running = false; }
public void run() {
while (running) { /* 任务 */ }
// 清理资源
}
}
十二、线程间通信(5 种实战方式)
| 方式 | 说明 | 常用场景 |
|---|
共享变量(volatile / 原子类) | 一个线程改,另一个读 | 状态标记、计数器 |
wait()/notify() | 配合 synchronized | 生产者‑消费者 |
Condition(配合 ReentrantLock) | 多路等待,精准唤醒 | 复杂协调 |
Semaphore | 控制同时访问的线程数 | 限流、连接池 |
CountDownLatch / CyclicBarrier | 线程协调工具 | 主等子、互相等待 |
12.1 Semaphore 示例
Semaphore semaphore = new Semaphore(3); // 最多3个线程同时访问
semaphore.acquire(); // 获取许可(阻塞)
semaphore.release(); // 释放许可
12.2 CountDownLatch(倒计时门栓)
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行任务
latch.countDown();
}).start();
}
latch.await(); // 主线程等待3个任务完成
12.3 CyclicBarrier(循环栅栏)
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("三人齐了,出发"));
for (int i = 0; i < 3; i++) {
new Thread(() -> {
barrier.await(); // 互相等待
}).start();
}
十三、线程池(企业开发核心)
13.1 为什么需要线程池
- 资源复用(避免频繁创建/销毁线程)
- 响应速度快(任务来了直接执行)
- 可管理(控制并发数、监控)
13.2 ThreadPoolExecutor 7 大核心参数(必背)
new ThreadPoolExecutor(
corePoolSize, // 核心线程数(常驻)
maximumPoolSize, // 最大线程数(核心+临时)
keepAliveTime, // 临时线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂(一般用默认)
RejectedExecutionHandler handler // 拒绝策略
);
13.3 执行流程(5 步,必背)
- 提交任务 → 核心线程未满 → 新建核心线程执行
- 核心线程满 → 任务加入阻塞队列
- 队列满 → 新建临时线程执行
- 总线程数 = 最大线程数 → 执行拒绝策略
- 临时线程空闲超时 → 销毁(回到核心线程数)
13.4 4 种拒绝策略(背)
| 策略 | 行为 | 比喻 |
|---|
AbortPolicy(默认) | 抛 RejectedExecutionException | 直接赶走客人,还吵架 |
CallerRunsPolicy | 调用者线程自己执行任务 | 让老板自己服务客人 |
DiscardPolicy | 悄悄丢弃任务,不报错 | 悄悄赶走客人,不吭声 |
DiscardOldestPolicy | 丢弃队列中最老的任务,然后重提 | 赶走排队最久的,让新人进 |
13.5 Executors 提供的 4 种常用线程池(开发规范禁止使用,需手动创建)
| 工厂方法 | 特点 | 适用场景 |
|---|
newFixedThreadPool(n) | 固定线程数,无界队列 | 重负载服务器 |
newCachedThreadPool() | 动态线程数,空闲60s回收 | 大量短期异步任务 |
newSingleThreadExecutor() | 单线程,顺序执行 | 任务需顺序处理 |
newScheduledThreadPool(n) | 支持定时/延迟执行 | 定时任务 |
规范:不要用 Executors 创建,因为 FixedThreadPool 使用无界队列可能 OOM,CachedThreadPool 可能创建过多线程。必须使用 new ThreadPoolExecutor 自定义。
13.6 线程数设置公式(背)
- CPU 密集型(计算为主):线程数 = CPU 核心数 + 1
- IO 密集型(网络、磁盘):线程数 = CPU 核心数 × 2 (或更多)
十四、定时任务线程池(ScheduledThreadPoolExecutor)
14.1 三种定时任务类型
| 方法 | 作用 | 特点 |
|---|
schedule(Runnable, delay, unit) | 延迟执行一次 | - |
scheduleAtFixedRate(...) | 固定频率执行 | 不关心上次任务是否完成,可能叠加 |
scheduleWithFixedDelay(...) | 固定延迟执行 | 必须等上次完成,再等延迟时间 |
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
scheduler.scheduleAtFixedRate(() -> System.out.println("ping"), 0, 2, TimeUnit.SECONDS);
14.2 对比 Timer 的优势
- 多线程,任务互不干扰(Timer 单线程,一个任务异常会影响全部)
- 支持线程池复用
十五、线程池优雅关闭(餐厅下班逻辑)
- 挂出「停止接客」牌子 →
shutdown()(不再接收新任务) - 让客人慢慢吃完 →
awaitTermination(timeout, unit) 等待现有任务完成 - 等了很久还没吃完 → 直接清场 →
shutdownNow()(强制中断所有任务)
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
十六、性能调优与监控
16.1 关键指标
- 活跃线程数:< 最大线程数的 80%
- 队列长度:< 队列容量的 70%
- 任务完成数:持续增长,增长停滞说明处理异常
- 拒绝任务数:应为 0,频繁拒绝需优化配置
16.2 监控工具
- JMX(
ThreadPoolExecutor 暴露的 getActiveCount() 等) - Micrometer(配合 Prometheus + Grafana)
jstack 检查死锁
十七、快速记忆口诀
- 创建线程四种法:继承、实现、Callable、池。
- 同步锁的选择:简单用 syn,灵活用 Re,开关用 volatile,计数用原子类。
- 线程池七参数:核心最大时单队工拒。
- 关闭优雅三步走:shutdown → 等待 → shutdownNow。
- 死锁四条件:互斥请求不剥夺,循环等待一把锁。
评论