Java多线程详解
Java 多线程详解
本章位置:Java 核心基础
前置知识:面向对象、异常机制、集合框架、IO
学习目标:理解进程与线程的区别,掌握 Java 线程创建方式、生命周期、线程安全问题、同步锁、线程通信、线程池、Callable/Future、原子类与常见并发集合,并能够写出基础的并发程序。
一、什么是线程
在学习多线程之前,先区分:
进程
线程
二、进程
进程可以理解为:
一个正在运行的程序。
例如电脑中同时运行:
浏览器
IDEA
微信
音乐播放器
数据库
这些都可以看成不同进程。
每个进程通常拥有自己独立的:
内存空间
系统资源
运行环境
三、线程
线程可以理解为:
进程内部真正执行任务的执行单元。
一个进程中可以有:
一个线程
多个线程
例如浏览器中可能同时:
渲染页面
播放视频
处理网络请求
执行 JavaScript
这些任务可以由不同线程完成。
四、进程和线程的关系
可以简单理解:
进程
│
├─ 线程1
├─ 线程2
├─ 线程3
└─ ...
一个 Java 程序启动后:
至少会有一个 main 线程
五、什么是多线程
如果一个程序中:
同时存在多个线程执行任务
就属于:
多线程
例如:
主线程负责界面
后台线程下载文件
另一个线程读取网络消息
六、为什么需要多线程
多线程常用于:
同时处理多个任务
提高程序响应速度
利用多核 CPU
处理大量请求
执行后台任务
并发下载
并发计算
异步处理
七、并发和并行
这两个概念容易混。
并发 Concurrency
多个任务:
在一段时间内交替执行
例如单核 CPU:
任务 A 执行一点
任务 B 执行一点
任务 C 执行一点
切换很快,看起来像同时执行。
并行 Parallelism
多个任务:
在同一时刻真正同时执行
例如多核 CPU:
CPU 核心1执行 A
CPU 核心2执行 B
八、Java 程序默认就是多线程吗
简单 main 程序虽然我们通常只关注:
main 线程
但 JVM 内部还可能存在:
GC 线程
JIT 编译线程
其他 JVM 后台线程
所以 Java 运行环境本身通常就不是严格意义上的单线程。
九、Thread 类
Java 中:
java.lang.Thread
代表线程。
创建线程最经典的一种方式:
继承 Thread
十、方式一:继承 Thread
public class MyThread
extends Thread {
@Override
public void run() {
System.out.println(
"子线程执行"
);
}
}
启动:
MyThread thread =
new MyThread();
thread.start();
十一、run() 和 start() 的区别
这是多线程最重要的基础问题之一。
如果:
thread.run();
只是:
普通方法调用
通常还是由当前线程执行。
而:
thread.start();
表示:
真正启动新线程
JVM 会在合适时机调用:
run()
十二、不要直接调用 run() 当成启动线程
错误理解:
thread.run();
等于开启线程。
实际上:
不会创建新的执行线程
正确:
thread.start();
十三、线程运行顺序不可保证
例如:
public class Demo {
public static void main(
String[] args
) {
Thread t =
new Thread() {
@Override
public void run() {
System.out.println(
"子线程"
);
}
};
t.start();
System.out.println(
"主线程"
);
}
}
输出可能:
主线程
子线程
也可能:
子线程
主线程
因为线程调度由:
JVM
操作系统
CPU
共同决定。
十四、方式二:实现 Runnable
定义:
public class MyTask
implements Runnable {
@Override
public void run() {
System.out.println(
"任务执行"
);
}
}
创建线程:
Runnable task =
new MyTask();
Thread thread =
new Thread(task);
thread.start();
十五、为什么 Runnable 更灵活
Java 类只能:
单继承
如果类已经继承其他父类:
class MyTask extends Parent
就不能再:
extends Thread
但可以:
implements Runnable
所以一般设计上:
任务
和
线程
分离更灵活。
十六、Runnable Lambda
因为 Runnable 是:
函数式接口
可以写:
Thread thread =
new Thread(
() -> {
System.out.println(
"子线程执行"
);
}
);
thread.start();
十七、最简线程写法
new Thread(
() ->
System.out.println(
"Hello Thread"
)
).start();
适合:
简单演示
短任务
正式项目中复杂任务一般不要全部塞进匿名 Lambda。
十八、Thread.currentThread()
获取当前正在执行代码的线程:
Thread thread =
Thread.currentThread();
十九、获取线程名称
String name =
Thread.currentThread()
.getName();
例如:
System.out.println(
Thread.currentThread()
.getName()
);
main 方法中通常输出:
main
二十、设置线程名称
Thread thread =
new Thread(task);
thread.setName(
"下载线程"
);
或者:
Thread thread =
new Thread(
task,
"下载线程"
);
二十一、线程名称有什么用
实际排查并发问题时:
线程名非常重要
日志中如果出现:
pool-1-thread-3
或者:
order-worker-2
可以帮助定位:
哪个线程执行了什么任务
二十二、线程优先级
Java 线程有优先级:
1 ~ 10
常量:
Thread.MIN_PRIORITY
Thread.NORM_PRIORITY
Thread.MAX_PRIORITY
默认:
5
二十三、不要依赖线程优先级保证顺序
例如:
thread.setPriority(10);
并不意味着:
这个线程一定先执行
线程调度仍受操作系统影响。
所以:
不要使用优先级实现业务顺序控制。
二十四、sleep()
让当前线程:
暂停一段时间
例如:
Thread.sleep(1000);
单位:
毫秒
表示大约休眠:
1 秒
二十五、sleep() 会抛 InterruptedException
例如:
try {
Thread.sleep(1000);
} catch (
InterruptedException e
) {
Thread.currentThread()
.interrupt();
}
关于中断后面详细讲。
二十六、sleep() 不释放锁
这是重要区别。
如果线程已经进入:
synchronized
并持有锁,
调用:
Thread.sleep(...)
通常:
不会释放已经持有的锁
二十七、yield()
Thread.yield();
表示:
提示调度器
当前线程愿意让出 CPU
但只是:
提示
并不能保证其他线程一定执行。
实际业务很少依赖它。
二十八、join()
假设:
thread.start();
thread.join();
表示:
当前线程等待 thread 执行结束后再继续。
二十九、join() 示例
Thread thread =
new Thread(
() -> {
for (int i = 1;
i <= 3;
i++) {
System.out.println(
"子线程:"
+ i
);
}
}
);
thread.start();
thread.join();
System.out.println(
"主线程结束"
);
通常:
子线程执行完
主线程才继续
三十、线程生命周期
线程常见状态由:
Thread.State
表示:
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
三十一、NEW
线程对象刚创建:
Thread thread =
new Thread(task);
还没有:
start()
状态:
NEW
三十二、RUNNABLE
调用:
start()
之后进入:
RUNNABLE
Java 的 RUNNABLE 可以理解为:
可能正在运行
也可能等待 CPU 调度
三十三、BLOCKED
线程想进入:
synchronized
区域,
但锁被其他线程占用。
此时可能进入:
BLOCKED
三十四、WAITING
线程无限期等待某个事件。
例如:
wait()
join()
某些使用方式会进入:
WAITING
三十五、TIMED_WAITING
带时间限制的等待:
sleep(1000)
wait(1000)
join(1000)
可能进入:
TIMED_WAITING
三十六、TERMINATED
线程:
run() 执行结束
进入:
TERMINATED
线程结束后:
不能再次 start()
三十七、线程不能启动两次
错误:
Thread thread =
new Thread(task);
thread.start();
thread.start();
第二次会出现:
IllegalThreadStateException
一个 Thread 对象:
只能 start 一次
三十八、什么是线程安全问题
多个线程:
同时访问同一份可变数据
如果操作没有正确同步,
可能出现:
数据错误
结果丢失
状态不一致
这叫:
线程安全问题
三十九、经典卖票问题
假设:
100 张票
多个线程同时卖。
public class TicketTask
implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
System.out.println(
Thread
.currentThread()
.getName()
+ " 卖出第 "
+ ticket
+ " 张票"
);
ticket--;
}
}
}
创建多个线程共享:
TicketTask task =
new TicketTask();
new Thread(
task,
"窗口1"
).start();
new Thread(
task,
"窗口2"
).start();
四十、为什么 ticket-- 不安全
看起来:
ticket--;
只有一行。
但底层不是一个不可分割操作。
可以简单理解:
读取 ticket
计算 ticket - 1
写回 ticket
如果两个线程同时读取:
ticket = 100
都计算成:
99
最后可能:
少减了一次
这种问题叫:
竞态条件
Race Condition
四十一、临界区
多个线程访问共享可变数据的关键代码:
临界区
Critical Section
例如:
if (ticket > 0) {
ticket--;
}
需要保证:
同一时刻只有一个线程执行
四十二、synchronized
Java 提供:
synchronized
用于同步。
基本形式:
synchronized (锁对象) {
临界区代码;
}
四十三、同步代码块
卖票改造:
public class TicketTask
implements Runnable {
private int ticket = 100;
private final Object lock =
new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticket <= 0) {
break;
}
System.out.println(
Thread
.currentThread()
.getName()
+ " 卖出第 "
+ ticket
+ " 张票"
);
ticket--;
}
}
}
}
四十四、锁对象必须共享
错误:
synchronized (
new Object()
) {
}
如果每次都创建新的锁对象:
线程拿到的是不同锁
无法互斥。
所以:
需要保护同一份共享数据的线程,必须竞争同一把锁。
四十五、同步方法
方法可以直接使用:
synchronized
例如:
public synchronized
void sell() {
}
四十六、普通 synchronized 方法锁谁
实例同步方法:
public synchronized
void test() {
}
锁的是:
this
当前对象
等价理解:
synchronized (this) {
}
四十七、static synchronized 方法
例如:
public static synchronized
void test() {
}
锁的是:
当前 Class 对象
例如:
Demo.class
不是某一个实例对象。
四十八、synchronized 的作用
最主要:
互斥
即:
同一时刻只有一个线程进入临界区
同时还涉及:
内存可见性
后面继续讲。
四十九、原子性
一个操作:
要么完整执行
要么不执行
中间不会被其他线程看到半完成状态。
称为:
原子性
例如:
count++
不是原子操作。
五十、可见性
线程 A 修改:
共享变量
线程 B:
是否能及时看到
就是:
可见性
五十一、有序性
编译器和 CPU 为了优化:
可能调整某些指令执行顺序
只要单线程结果不变。
但在多线程中:
重排序
可能影响线程之间观察到的结果。
五十二、Java 内存模型
Java 对线程之间:
共享变量
可见性
重排序
同步规则
有一套规范:
Java Memory Model
简称:
JMM
初学阶段重点理解:
原子性
可见性
有序性
五十三、volatile
volatile 可以修饰:
共享变量
例如:
private volatile
boolean running = true;
主要作用:
保证一定程度的可见性
限制相关指令重排序
五十四、volatile 不保证复合操作原子性
例如:
private volatile
int count = 0;
然后:
count++;
仍然不是线程安全。
因为:
count++
是复合操作。
所以:
volatile ≠ 万能锁
五十五、volatile 常见场景
例如:
停止标志
状态标记
配置刷新标识
例如:
private volatile
boolean running = true;
线程:
while (running) {
}
其他线程:
running = false;
五十六、Lock 接口
除了:
synchronized
Java 还提供:
java.util.concurrent.locks.Lock
常见实现:
ReentrantLock
五十七、ReentrantLock
例如:
private final Lock lock =
new ReentrantLock();
使用:
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
五十八、为什么 unlock() 放 finally
如果:
临界区发生异常
仍然必须:
释放锁
否则其他线程可能永远无法获得锁。
所以标准模板:
lock.lock();
try {
} finally {
lock.unlock();
}
五十九、synchronized 和 Lock 基本区别
| 对比 | synchronized | Lock |
|---|---|---|
| 获取释放 | JVM 自动 | 手动 |
| 可中断获取 | 有限 | 支持 |
| 尝试获取 | 不方便 | tryLock() |
| 多条件队列 | 不方便 | Condition |
| 语法 | 简单 | 更灵活 |
普通同步:
优先 synchronized
复杂锁控制:
考虑 Lock
六十、可重入锁
synchronized 和 ReentrantLock 都属于:
可重入锁
意思:
同一个线程已经持有某把锁时,可以再次进入需要同一把锁的代码。
六十一、死锁
两个或多个线程:
互相等待对方持有的资源
导致:
谁也无法继续
叫:
死锁
Deadlock
六十二、经典死锁
线程 A:
先拿锁1
再等锁2
线程 B:
先拿锁2
再等锁1
结果:
A 等 B
B 等 A
永久卡住。
六十三、死锁示例
Object lockA =
new Object();
Object lockB =
new Object();
Thread t1 =
new Thread(
() -> {
synchronized (lockA) {
synchronized (lockB) {
}
}
}
);
Thread t2 =
new Thread(
() -> {
synchronized (lockB) {
synchronized (lockA) {
}
}
}
);
这段代码存在死锁风险。
六十四、避免死锁的思路
常见:
统一锁顺序
减少锁嵌套
缩小锁范围
避免长时间持锁
使用 tryLock()
设计清晰的锁策略
六十五、wait()
对象方法:
wait()
让当前线程:
释放当前对象锁
并进入等待
必须在:
synchronized
持有对应对象锁时调用。
六十六、notify()
notify()
唤醒:
等待该对象锁的某一个线程
六十七、notifyAll()
notifyAll()
唤醒:
等待该对象监视器的所有线程
实际代码中:
notifyAll()
往往比 notify() 更安全一些,
但具体仍要根据条件设计。
六十八、wait() 和 sleep() 区别
| 对比 | wait | sleep |
|---|---|---|
| 所属 | Object | Thread |
| 是否释放锁 | 会 | 不会 |
| 使用位置 | 同步区内 | 任意合适位置 |
| 目的 | 线程通信 | 暂停执行 |
六十九、生产者消费者模型
经典线程通信模型:
生产者
负责生产数据
消费者
负责消费数据
中间共享:
缓冲区
七十、生产者消费者基本问题
假设只有一个商品槽:
生产者生产后
消费者才能消费
消费者消费后
生产者才能继续生产
这需要:
等待
唤醒
七十一、wait/notify 伪代码
synchronized (lock) {
while (条件不满足) {
lock.wait();
}
// 修改共享状态
lock.notifyAll();
}
注意:
等待条件推荐使用 while
而不是 if
七十二、为什么 wait 条件用 while
线程被唤醒后:
不代表条件一定仍然满足
可能存在:
竞争
虚假唤醒
所以必须重新检查:
while (条件不满足) {
wait();
}
七十三、Condition
使用:
ReentrantLock
时可以创建:
Condition
例如:
Condition condition =
lock.newCondition();
对应:
await()
signal()
signalAll()
类似:
wait()
notify()
notifyAll()
但更灵活。
七十四、Callable
Runnable:
没有返回值
不能直接声明受检异常
如果任务需要:
返回结果
可以使用:
Callable<V>
七十五、Callable 示例
Callable<Integer> task =
() -> {
return 1 + 2;
};
返回类型:
Integer
七十六、FutureTask
Callable 不能直接传给:
new Thread(callable)
可以包装成:
FutureTask<Integer>
例如:
FutureTask<Integer> futureTask =
new FutureTask<>(
task
);
Thread thread =
new Thread(
futureTask
);
thread.start();
获取结果:
Integer result =
futureTask.get();
七十七、Future.get()
get():
如果任务还没完成
当前线程会等待
直到:
有结果
或任务异常
所以:
Future 并不等于自动非阻塞
七十八、Future
Future 表示:
一个未来可能完成的结果
常用:
get()
isDone()
cancel()
isCancelled()
七十九、为什么不能随便 new Thread()
如果每来一个任务都:
new Thread(...)
大量请求时可能产生:
几千
几万
甚至更多线程
导致:
内存占用高
线程切换成本高
系统不稳定
所以实际项目通常使用:
线程池
八十、线程池
线程池可以理解:
提前维护一组线程,任务到来时复用这些线程执行。
优点:
减少线程创建销毁成本
限制线程数量
方便管理任务
提高系统稳定性
八十一、ExecutorService
Java 提供:
ExecutorService
管理线程池。
例如:
ExecutorService pool =
Executors
.newFixedThreadPool(
4
);
提交任务:
pool.submit(task);
关闭:
pool.shutdown();
八十二、Executors 工厂方法
常见:
newFixedThreadPool()
newSingleThreadExecutor()
newCachedThreadPool()
newScheduledThreadPool()
八十三、newFixedThreadPool()
固定线程数量:
ExecutorService pool =
Executors
.newFixedThreadPool(
4
);
最多大致:
4 个工作线程
同时执行任务。
八十四、newSingleThreadExecutor()
Executors
.newSingleThreadExecutor();
只有:
一个工作线程
任务通常按照顺序执行。
八十五、newCachedThreadPool()
可以按需求创建线程,
空闲线程会复用。
但:
线程数量上限很大
高负载下可能产生过多线程。
生产项目要谨慎。
八十六、newScheduledThreadPool()
适合:
延迟任务
周期任务
例如:
ScheduledExecutorService pool =
Executors
.newScheduledThreadPool(
2
);
八十七、线程池为什么生产环境更常直接 ThreadPoolExecutor
虽然:
Executors
使用简单,
但它的一些工厂方法默认参数:
队列容量
最大线程数
可能不适合高并发生产环境。
所以真实项目中经常直接:
new ThreadPoolExecutor(...)
明确控制所有参数。
八十八、ThreadPoolExecutor 核心参数
构造器核心参数:
corePoolSize
maximumPoolSize
keepAliveTime
unit
workQueue
threadFactory
handler
八十九、corePoolSize
核心线程数
通常线程池会优先保持一定数量核心线程。
九十、maximumPoolSize
最大线程数
表示:
线程池允许创建的线程上限
九十一、keepAliveTime
非核心等空闲线程:
允许存活多久
超过时间后:
可能被回收
九十二、workQueue
任务队列。
常见:
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
不同队列会直接影响:
线程池行为
九十三、线程池执行流程简单理解
可以先记:
任务到来
↓
核心线程有空位
→ 创建/使用核心线程
否则
↓
尝试进入任务队列
队列满
↓
线程数未到最大值
→ 创建非核心线程
还无法处理
↓
执行拒绝策略
九十四、拒绝策略
常见:
AbortPolicy
CallerRunsPolicy
DiscardPolicy
DiscardOldestPolicy
九十五、AbortPolicy
无法接收任务时:
直接抛异常
通常是默认策略之一。
九十六、CallerRunsPolicy
线程池无法处理时:
让提交任务的线程自己执行
可以形成一定:
反压
但会拖慢调用线程。
九十七、DiscardPolicy
直接:
丢弃任务
不抛异常。
如果任务重要:
非常危险
九十八、DiscardOldestPolicy
丢弃队列中较旧任务,
再尝试提交新任务。
同样必须结合业务使用。
九十九、线程工厂 ThreadFactory
可以自定义线程:
名称
是否守护线程
异常处理器
例如:
ThreadFactory factory =
runnable -> {
Thread thread =
new Thread(
runnable
);
thread.setName(
"order-worker"
);
return thread;
};
一百、线程池一定要关闭
如果自己创建:
ExecutorService
程序结束时应根据生命周期调用:
shutdown()
或者:
shutdownNow()
否则线程池中的非守护线程可能阻止 JVM 正常退出。
一百零一、shutdown()
表示:
不再接收新任务
已经提交的任务继续执行
一百零二、shutdownNow()
尝试:
停止正在执行任务
并返回尚未开始的任务
但:
不保证任务一定立即停止
它主要通过:
interrupt
请求线程停止。
一百零三、线程中断
Java 中:
interrupt
不是强制“杀死线程”。
更准确地说:
向线程发出中断请求或设置中断状态。
一百零四、interrupt()
thread.interrupt();
表示:
请求中断该线程
线程需要:
配合处理中断
才能优雅停止。
一百零五、isInterrupted()
thread.isInterrupted();
判断:
线程中断标记
一百零六、Thread.interrupted()
Thread.interrupted();
检查当前线程中断状态,
并且:
通常会清除中断标志
所以和:
isInterrupted()
行为不同。
一百零七、InterruptedException
线程在:
sleep
wait
join
等阻塞操作时被中断,
通常会抛:
InterruptedException
一百零八、不要随便吞掉 InterruptedException
不推荐:
catch (
InterruptedException e
) {
}
更常见:
catch (
InterruptedException e
) {
Thread
.currentThread()
.interrupt();
return;
}
至少恢复:
中断状态
让上层知道线程已经收到中断请求。
一百零九、守护线程
设置:
thread.setDaemon(true);
表示:
守护线程
Daemon Thread
当 JVM 中只剩守护线程时:
JVM 可以退出
一百一十、守护线程常见用途
例如 JVM 内部:
GC
等后台线程。
不要把:
必须可靠完成的重要业务
只放进守护线程中。
因为 JVM 退出时:
守护线程可能直接结束
一百一十一、AtomicInteger
如果多个线程需要:
安全地 count++
可以使用:
AtomicInteger
一百一十二、AtomicInteger 示例
AtomicInteger count =
new AtomicInteger(0);
count.incrementAndGet();
等价业务含义:
安全地 +1 并返回新值
一百一十三、AtomicInteger 常用方法
get()
set()
incrementAndGet()
getAndIncrement()
decrementAndGet()
addAndGet()
compareAndSet()
一百一十四、CAS
很多原子类底层大量使用:
CAS
Compare And Set
思想:
只有当前值仍然等于预期值
才执行修改
否则:
失败
重新尝试
一百一十五、CAS 不是万能
CAS 适合:
简单原子状态
计数
无锁更新
但复杂业务仍可能需要:
锁
事务
同步结构
不能认为:
用了 AtomicInteger
整个业务就线程安全
一百一十六、LongAdder
高并发计数场景中:
LongAdder
可能比:
AtomicLong
竞争更小。
例如:
LongAdder adder =
new LongAdder();
adder.increment();
long result =
adder.sum();
适合:
高并发统计
一百一十七、线程安全集合
普通:
ArrayList
HashMap
HashSet
默认:
不是线程安全
Java 并发包提供:
ConcurrentHashMap
CopyOnWriteArrayList
BlockingQueue
等。
一百一十八、ConcurrentHashMap
适合:
多线程共享 Map
例如:
ConcurrentHashMap<
String,
Integer
> map =
new ConcurrentHashMap<>();
一百一十九、为什么不要简单用 HashMap 多线程写
多线程同时:
put()
remove()
resize()
可能导致:
数据竞争
结果不一致
因此共享修改需要:
同步
或并发集合
一百二十、CopyOnWriteArrayList
特点:
写操作时复制
读操作通常无需锁住整个结构
适合:
读多写少
例如:
监听器列表
配置订阅者
如果写操作很多:
复制成本会很高
一百二十一、BlockingQueue
阻塞队列是:
生产者消费者
非常重要的工具。
常见:
ArrayBlockingQueue
LinkedBlockingQueue
一百二十二、BlockingQueue 常用方法
put()
take()
offer()
poll()
其中:
put
队列满时等待
take
队列空时等待
可以大幅简化生产者消费者代码。
一百二十三、BlockingQueue 生产者
BlockingQueue<String> queue =
new ArrayBlockingQueue<>(
10
);
queue.put("任务1");
一百二十四、BlockingQueue 消费者
String task =
queue.take();
如果队列为空:
自动等待
不用自己手写:
wait/notify
一百二十五、CountDownLatch
用于:
等待多个任务全部完成
例如:
CountDownLatch latch =
new CountDownLatch(
3
);
三个线程结束后:
latch.countDown();
主线程:
latch.await();
等待计数变成 0。
一百二十六、CountDownLatch 使用场景
例如:
同时查询三个服务
三份数据全部返回后
再生成页面
可以使用:
CountDownLatch
协调。
一百二十七、Semaphore
Semaphore:
信号量
用于控制:
同一时刻最多允许多少个线程进入某区域
例如:
Semaphore semaphore =
new Semaphore(
3
);
最多允许:
3 个线程
同时获得许可。
一百二十八、Semaphore 使用
获取:
semaphore.acquire();
释放:
semaphore.release();
标准:
semaphore.acquire();
try {
// 使用有限资源
} finally {
semaphore.release();
}
一百二十九、CyclicBarrier 简单了解
CyclicBarrier:
让多个线程在某个位置互相等待
直到:
所有参与线程都到达
再一起继续。
与 CountDownLatch 不同:
CyclicBarrier 可以重复使用
一百三十、CompletableFuture
Java 8 提供:
CompletableFuture
用于更方便编写:
异步任务
任务组合
结果转换
异常处理
一百三十一、runAsync()
无返回值任务:
CompletableFuture<Void> future =
CompletableFuture
.runAsync(
() -> {
System.out.println(
"异步任务"
);
}
);
一百三十二、supplyAsync()
有返回值:
CompletableFuture<Integer> future =
CompletableFuture
.supplyAsync(
() -> 100
);
一百三十三、thenApply()
处理上一步结果:
CompletableFuture<String> future =
CompletableFuture
.supplyAsync(
() -> 100
)
.thenApply(
value ->
"结果:"
+ value
);
一百三十四、thenAccept()
消费结果:
future.thenAccept(
value ->
System.out.println(
value
)
);
一百三十五、thenCombine()
组合两个异步结果:
CompletableFuture<Integer> a =
CompletableFuture
.supplyAsync(
() -> 10
);
CompletableFuture<Integer> b =
CompletableFuture
.supplyAsync(
() -> 20
);
CompletableFuture<Integer> result =
a.thenCombine(
b,
Integer::sum
);
一百三十六、CompletableFuture 不要无脑使用默认线程池
如果调用:
supplyAsync(...)
不指定线程池,
通常会使用公共 ForkJoinPool。
实际 Web 项目中:
大量阻塞 IO
大量慢任务
如果全部扔到公共池,
可能相互影响。
生产项目通常考虑:
自定义 Executor
一百三十七、自定义 Executor 给 CompletableFuture
ExecutorService pool =
Executors
.newFixedThreadPool(
4
);
CompletableFuture<Integer> future =
CompletableFuture
.supplyAsync(
() -> 100,
pool
);
一百三十八、线程局部变量 ThreadLocal
ThreadLocal:
为每个线程提供独立变量副本
例如:
ThreadLocal<String> local =
new ThreadLocal<>();
线程 A:
local.set("A");
线程 B:
local.set("B");
互不直接干扰。
一百三十九、ThreadLocal 常见用途
例如:
用户上下文
请求上下文
数据库连接上下文
TraceId
事务资源
很多框架底层会使用。
一百四十、ThreadLocal 使用后注意 remove()
线程池中的线程:
会长期复用
如果 ThreadLocal 数据不清理:
可能导致脏数据
内存问题
上下文串线
所以常见:
try {
local.set(value);
// 业务
} finally {
local.remove();
}
一百四十一、线程池参数不能拍脑袋
线程池线程数应该结合:
CPU 核数
任务类型
是否 IO 密集
是否 CPU 密集
任务耗时
响应要求
机器资源
决定。
不能简单认为:
线程越多越快
一百四十二、CPU 密集型任务
例如:
复杂计算
图片压缩
加密
算法
线程数量通常不会远大于:
CPU 核数
否则线程切换过多。
一百四十三、IO 密集型任务
例如:
数据库查询
网络请求
文件读取
线程经常:
处于等待状态
可以允许比 CPU 核数更多的线程,
但也必须有限制。
一百四十四、线程过多的问题
过多线程会导致:
内存占用增加
上下文切换增加
调度开销增加
GC 压力
系统响应变慢
甚至 OOM
一百四十五、上下文切换
CPU 从:
线程 A
切换到:
线程 B
需要保存和恢复:
寄存器
程序计数器
线程状态
这不是免费的。
所以:
线程越多不等于性能越高
一百四十六、竞态条件示例:库存扣减
例如:
if (stock > 0) {
stock--;
}
多个线程同时执行可能:
超卖
这是电商系统中经典线程安全问题。
真正项目里还要结合:
数据库锁
乐观锁
Redis
分布式锁
事务
解决。
Java 本地 synchronized 只能保护:
当前 JVM 内的共享数据
一百四十七、本地锁和分布式锁区别
synchronized:
只在当前 JVM 进程内有效
如果系统部署:
服务器 A
服务器 B
它们的 JVM 不是同一个进程,
各自有自己的锁。
这时如果要保护共享资源:
需要数据库锁
Redis 分布式锁
ZooKeeper 等
后面分布式系统再学。
一百四十八、线程安全并不等于业务安全
例如:
AtomicInteger stock
即使 stock-- 是原子的,
业务可能还有:
创建订单
扣余额
写数据库
发送消息
这些操作需要整体一致性。
所以:
线程安全
只是并发正确性的一部分
一百四十九、常见错误:直接调用 run()
错误:
thread.run();
正确启动:
thread.start();
一百五十、常见错误:启动同一线程两次
thread.start();
thread.start();
会:
IllegalThreadStateException
一百五十一、常见错误:锁对象不共享
synchronized (
new Object()
) {
}
每次新锁:
无法保护共享数据
一百五十二、常见错误:锁范围过大
例如:
synchronized (lock) {
// 网络请求 5 秒
// 文件读取 10 秒
// 数据库慢查询
}
其他线程可能一直等待。
应该:
只保护真正共享的临界区
一百五十三、常见错误:锁范围过小
如果业务逻辑:
检查
+
修改
必须原子完成。
错误:
if (stock > 0) {
}
synchronized (lock) {
stock--;
}
检查已经在线程外执行,
仍可能超卖。
正确:
synchronized (lock) {
if (stock > 0) {
stock--;
}
}
一百五十四、常见错误:忘记 unlock()
使用 ReentrantLock:
lock.lock();
// 中间异常
lock.unlock();
如果异常:
unlock 永远执行不到
正确:
lock.lock();
try {
} finally {
lock.unlock();
}
一百五十五、常见错误:用 sleep() 做线程同步
例如:
Thread.sleep(1000);
然后假设:
另一个线程一定已经执行完
这是不可靠的。
如果需要等待线程结束:
join()
如果需要任务结果:
Future
CompletableFuture
一百五十六、常见错误:认为 volatile 能解决 count++
volatile int count;
count++;
仍然可能线程不安全。
可以考虑:
synchronized
AtomicInteger
一百五十七、常见错误:吞中断异常
catch (
InterruptedException e
) {
}
会让上层不知道线程已经收到中断请求。
推荐至少:
Thread
.currentThread()
.interrupt();
一百五十八、常见错误:线程池不关闭
程序测试中:
main 已经执行完
但程序还不退出
可能因为:
线程池仍有非守护线程存活
记得:
pool.shutdown();
一百五十九、常见错误:无限线程池
例如:
Executors
.newCachedThreadPool()
在高并发情况下:
可能创建大量线程
生产系统必须理解其风险。
一百六十、常见错误:无限任务队列
某些线程池:
队列容量近似无限
如果任务生产速度远大于消费:
任务大量堆积
最终内存压力巨大
因此生产环境通常需要:
有界队列
合理拒绝策略
监控
一百六十一、常见错误:HashMap 多线程修改
HashMap
不是并发集合。
多线程共享修改应考虑:
ConcurrentHashMap
或同步控制。
一百六十二、常见错误:ThreadLocal 忘记清理
线程池中:
线程反复复用
如果:
ThreadLocal.set()
后不:
remove()
可能造成:
上下文泄漏
内存泄漏风险
一百六十三、综合案例:多线程下载模拟
public class DownloadTask
implements Runnable {
private final String fileName;
public DownloadTask(
String fileName
) {
this.fileName =
fileName;
}
@Override
public void run() {
System.out.println(
Thread
.currentThread()
.getName()
+ " 开始下载 "
+ fileName
);
try {
Thread.sleep(1000);
} catch (
InterruptedException e
) {
Thread
.currentThread()
.interrupt();
return;
}
System.out.println(
fileName
+ " 下载完成"
);
}
}
使用:
new Thread(
new DownloadTask(
"a.zip"
)
).start();
new Thread(
new DownloadTask(
"b.zip"
)
).start();
一百六十四、综合案例:线程安全计数器
使用 synchronized:
public class Counter {
private int count;
public synchronized
void increment() {
count++;
}
public synchronized
int getCount() {
return count;
}
}
一百六十五、使用 AtomicInteger 计数
public class Counter {
private final AtomicInteger
count =
new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
一百六十六、综合案例:线程池执行任务
ExecutorService pool =
Executors
.newFixedThreadPool(
3
);
for (int i = 1;
i <= 10;
i++) {
int taskId = i;
pool.submit(
() -> {
System.out.println(
Thread
.currentThread()
.getName()
+ " 执行任务 "
+ taskId
);
}
);
}
pool.shutdown();
一百六十七、综合案例:Callable 求和
ExecutorService pool =
Executors
.newFixedThreadPool(
2
);
Future<Integer> future =
pool.submit(
() -> {
int sum = 0;
for (int i = 1;
i <= 100;
i++) {
sum += i;
}
return sum;
}
);
Integer result =
future.get();
System.out.println(
result
);
pool.shutdown();
一百六十八、综合案例:并行查询模拟
假设三个服务:
用户信息
订单信息
积分信息
可以异步:
CompletableFuture<String> userFuture =
CompletableFuture
.supplyAsync(
() -> "用户数据"
);
CompletableFuture<String> orderFuture =
CompletableFuture
.supplyAsync(
() -> "订单数据"
);
CompletableFuture<String> scoreFuture =
CompletableFuture
.supplyAsync(
() -> "积分数据"
);
等待全部:
CompletableFuture
.allOf(
userFuture,
orderFuture,
scoreFuture
)
.join();
一百六十九、综合案例:生产者消费者
推荐使用:
BlockingQueue
而不是手写 wait/notify。
BlockingQueue<String> queue =
new ArrayBlockingQueue<>(
5
);
生产者:
Thread producer =
new Thread(
() -> {
for (int i = 1;
i <= 10;
i++) {
try {
queue.put(
"任务"
+ i
);
} catch (
InterruptedException e
) {
Thread
.currentThread()
.interrupt();
return;
}
}
}
);
消费者:
Thread consumer =
new Thread(
() -> {
while (true) {
try {
String task =
queue.take();
System.out.println(
"消费:"
+ task
);
} catch (
InterruptedException e
) {
Thread
.currentThread()
.interrupt();
return;
}
}
}
);
一百七十、线程排查工具简单了解
后面开发中可以使用:
IDEA Debug
jstack
jconsole
VisualVM
Java Mission Control
排查:
死锁
线程阻塞
CPU 占用
线程数量
一百七十一、jstack 能看什么
可以看到:
线程状态
调用栈
锁等待
死锁信息
如果线上程序:
CPU 高
线程卡死
jstack 经常非常有用。
一百七十二、线程 Dump
线程快照:
Thread Dump
记录某一时刻:
所有线程状态
调用栈
锁信息
是排查 Java 并发问题的重要资料。
一百七十三、多线程知识结构
Java 多线程
│
├─ 基础
│ ├─ Thread
│ ├─ Runnable
│ ├─ Callable
│ └─ Future
│
├─ 生命周期
│ ├─ NEW
│ ├─ RUNNABLE
│ ├─ BLOCKED
│ ├─ WAITING
│ ├─ TIMED_WAITING
│ └─ TERMINATED
│
├─ 线程控制
│ ├─ sleep
│ ├─ join
│ ├─ interrupt
│ └─ daemon
│
├─ 线程安全
│ ├─ synchronized
│ ├─ volatile
│ ├─ Lock
│ ├─ AtomicInteger
│ └─ CAS
│
├─ 线程通信
│ ├─ wait
│ ├─ notify
│ ├─ Condition
│ └─ BlockingQueue
│
├─ 线程池
│ ├─ ExecutorService
│ ├─ ThreadPoolExecutor
│ └─ ScheduledExecutorService
│
└─ 并发工具
├─ ConcurrentHashMap
├─ CopyOnWriteArrayList
├─ CountDownLatch
├─ Semaphore
├─ CompletableFuture
└─ ThreadLocal
一百七十四、必须掌握的 Thread 方法
start()
run()
sleep()
join()
interrupt()
isInterrupted()
currentThread()
getName()
setName()
一百七十五、必须掌握的同步知识
什么是共享变量
什么是竞态条件
什么是临界区
synchronized 如何保护临界区
锁对象为什么必须共享
sleep 为什么不释放锁
wait 为什么会释放锁
volatile 能解决什么
volatile 不能解决什么
什么是死锁
一百七十六、必须掌握的线程池知识
至少能说明:
为什么使用线程池
corePoolSize
maximumPoolSize
workQueue
keepAliveTime
拒绝策略
shutdown
并理解:
生产环境不要无脑 new Thread
线程池也不能无脑使用默认参数
一百七十七、练习题 1:两个线程打印
创建两个线程:
线程 A 打印 1~10
线程 B 打印 A~J
观察输出顺序。
一百七十八、练习题 2:Runnable
不要继承 Thread,
使用:
Runnable
重新完成上一个练习。
一百七十九、练习题 3:卖票
三个窗口:
共同卖 100 张票
先写线程不安全版本,
观察问题。
然后使用:
synchronized
修复。
一百八十、练习题 4:AtomicInteger
创建:
10 个线程
每个线程执行:
10000 次 count++
分别使用:
普通 int
AtomicInteger
对比最终结果。
一百八十一、练习题 5:join
线程 A:
模拟下载文件
主线程必须等待下载完成后:
再执行解压
使用:
join()
一百八十二、练习题 6:线程池
创建固定:
4 个线程
的线程池,
提交:
20 个任务
观察线程复用。
一百八十三、练习题 7:Callable
使用 Callable 计算:
1 ~ 1000000
的和,
通过:
Future
获取结果。
一百八十四、练习题 8:BlockingQueue
实现:
生产者不断生产订单
消费者不断处理订单
队列容量:
5
一百八十五、练习题 9:CountDownLatch
创建:
3 个初始化线程
分别模拟:
加载配置
连接数据库
初始化缓存
主线程等待全部完成后:
启动系统
一百八十六、练习题 10:Semaphore
模拟停车场:
只有 3 个停车位
10 辆车同时尝试进入。
要求:
最多 3 辆同时停车
一百八十七、练习题 11:CompletableFuture
同时模拟:
查询用户
查询订单
查询积分
全部完成后拼接结果。
一百八十八、练习题 12:ThreadLocal
给每个请求线程设置:
requestId
业务执行结束后:
remove()
观察不同线程之间数据是否独立。
一百八十九、本章常见面试基础题
1. 进程和线程有什么区别?
2. start() 和 run() 有什么区别?
3. Runnable 和 Thread 有什么区别?
4. sleep() 和 wait() 有什么区别?
5. synchronized 锁的是什么?
6. volatile 能保证原子性吗?
7. 什么是死锁?
8. 什么是 CAS?
9. AtomicInteger 为什么线程安全?
10. HashMap 是线程安全的吗?
11. ConcurrentHashMap 用来解决什么问题?
12. 为什么需要线程池?
13. ThreadPoolExecutor 有哪些核心参数?
14. shutdown 和 shutdownNow 有什么区别?
15. 什么是 ThreadLocal?
16. 什么是 CompletableFuture?
一百九十、本章总结
Java 多线程真正要解决的不是:
“如何创建更多线程”
而是:
多个任务如何并发执行
共享数据如何保证正确
线程如何协调
线程数量如何受控
任务如何复用线程执行
异常和中断如何正确处理
学习顺序建议:
Thread
↓
Runnable
↓
线程生命周期
↓
sleep / join / interrupt
↓
线程安全问题
↓
synchronized
↓
volatile
↓
Lock
↓
wait / notify
↓
Callable / Future
↓
线程池
↓
Atomic
↓
并发集合
↓
BlockingQueue
↓
CountDownLatch / Semaphore
↓
CompletableFuture
↓
ThreadLocal
后面进入:
网络编程
Web 开发
Spring
Spring Boot
Redis
消息队列
分布式系统
时,
会发现很多底层问题都离不开:
线程
线程池
并发控制
异步任务
线程安全
所以多线程不是“只为了面试”的知识。
它是理解 Java 服务端程序如何真正运行的重要基础。