一、多线程基础:优缺点与核心代价
1. 核心优点
- 资源利用率更高:CPU 空闲时可调度其他线程执行,避免硬件资源浪费(如 IO 等待时 CPU 不闲置)。
- 程序设计更简洁:异步场景(如文件下载、接口调用)可通过多线程拆分任务,简化复杂逻辑。
- 程序响应更快:UI 界面、服务端程序可通过多线程避免主线程阻塞,提升用户/调用方体验。
2. 主要代价(易忽略细节)
- :需处理线程安全、同步、死锁、线程通信等问题,调试难度增加。
本文系统讲解了 Java 多线程与并发的核心知识,涵盖线程创建方式、线程安全判定原则、synchronized 同步机制、线程间通信(wait/notify)、死锁产生与避免、各类锁(ReentrantLock、读写锁、信号量)及阻塞队列的使用。重点阐述了线程池的核心结构与参数配置,以及 CAS 无锁编程原理与原子类应用,旨在帮助开发者理解并发模型,编写高效安全的并发代码。
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的业务逻辑(线程体)
}
}
// 启动线程:调用 start(),而非 run()
new MyThread().start();
class MyTask implements Runnable {
@Override
public void run() {
// 线程执行的业务逻辑(解耦:任务与线程分离)
}
}
// 启动线程:将任务传入 Thread 实例
new Thread(new MyTask()).start();
优先选择实现 Runnable 接口,原因如下:
调用 run() 方法而非 start():
直接调用 run() 只是普通方法调用(运行在当前线程),不会启动新线程;start() 才会触发 JVM 创建新线程,执行 run() 方法。
共享资源的'读 - 改 - 写'复合操作非原子性(如 count++,实际分为 3 步:读 count 值→自增→写回 count,多线程交错执行会导致数据错乱)。
核心判定原则:若一个资源(对象、文件、数据库连接等)的创建、使用、销毁,全程在同一个线程内完成,且不会逃逸到线程外部(即其他线程无法访问该资源),则该资源的使用是线程安全的。
对象成员变量(存储在堆内存,多线程可通过对象引用访问并修改,若未加同步,必存在线程安全问题)。
AtomicReference<String> ref = new AtomicReference<>("a");,ref 引用本身可被多线程修改(需用原子类保护),但引用指向的 String 对象本身不可变。synchronized 是 Java 原生的悲观锁,通过'互斥'保证临界区原子性,同时保证可见性(解锁前的修改对后续加锁线程可见)和可重入性。
| 作用范围 | 锁对象 | 实例代码 | 说明 |
|---|---|---|---|
| 实例方法同步 | 当前对象(this) | public synchronized void add(int value) { … } | 同一对象的多个同步实例方法,同一时刻仅一个线程可执行 |
| 静态方法同步 | 当前类的 Class 对象(全局唯一) | public static synchronized void add(int value) { … } | 同一类的所有静态同步方法,同一时刻仅一个线程可执行 |
| 实例方法中同步块 | 自定义对象(常用 this) | synchronized(this) { this.count += value; } | 缩小同步范围,仅保护临界区,提升性能 |
| 静态方法中同步块 | 当前类的 Class 对象 | synchronized(MyClass.class) { … } | 与静态同步方法锁对象一致,可灵活控制同步范围 |
线程间通信的核心是'协作'(如生产者 - 消费者模型),常用方式:共享对象通信、忙等待、wait/notify/notifyAll(推荐)。
通过共享对象的成员变量传递信号(需配合同步,避免竞态条件)。例如:线程 A 在同步块中设置 hasDataToProcess = true,线程 B 在同步块中读取该变量。
线程 B 循环等待信号,浪费 CPU 资源(空闲时也占用 CPU):
while(!sharedSignal.hasDataToProcess()) {
// 空循环,忙等待,浪费 CPU
}
必须在 synchronized 同步块/方法中调用,且调用对象必须是'当前持有锁的对象'(即同步块的锁对象)。
问:等待线程持有锁,会阻塞唤醒线程进入同步块吗? 答:不会。线程调用 wait() 后,会立即释放锁,允许其他线程(包括唤醒线程)获取锁并进入同步块;唤醒线程执行完同步块、释放锁后,被唤醒的线程才会竞争锁,成功后退出 wait()。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 丢失信号 | notify() 先于 wait() 执行,信号未保存,等待线程错过唤醒 | 用成员变量保存信号(如 boolean wasSignalled),唤醒时置为 true,等待时检查该变量 |
| 假唤醒 | 线程被唤醒但未收到有效信号(JVM 底层机制) | 用 while 循环检查信号(而非 if),即'自旋锁':while(!wasSignalled) { wait(); } |
| 意外唤醒 | 用常量字符串、全局对象作为锁,JVM 会复用该对象,导致跨实例唤醒 | 使用唯一锁对象(如 new Object()),避免使用""、Class 对象等全局共享对象 |
public class MyWaitNotify {
private final Object monitor = new Object(); // 唯一锁对象
private boolean wasSignalled = false; // 保存信号,避免丢失
// 等待信号
public void doWait() throws InterruptedException {
synchronized (monitor) {
while (!wasSignalled) { // while 循环,防止假唤醒
monitor.wait();
}
wasSignalled = false; // 清除信号,准备下次等待
}
}
// 发送信号
public void doNotify() {
synchronized (monitor) {
wasSignalled = true; // 保存信号
monitor.notify(); // 唤醒等待线程
}
}
}
两个或多个线程互相持有对方需要的锁,且永久阻塞,无法继续执行(线程'互相僵持')。
所有线程按相同顺序获取锁(如先锁 A、再锁 B),打破'循环等待'条件。
尝试获取锁时设置超时时间(如用 Lock.tryLock(long timeout, TimeUnit unit)),超时则释放已持有锁、回退,等待随机时间后重试,打破'持有并等待'条件。
适用于无法固定加锁顺序、超时不可行的场景:
用 Lock 锁替代 synchronized(synchronized 默认非公平),自定义公平锁或使用 JUC 的公平锁(如 ReentrantLock(true)):
核心是用'状态变量 + 同步'控制线程访问,示例(不可重入锁):
public class Lock {
private boolean isLocked = false; // 锁状态
// 获取锁(阻塞)
public synchronized void lock() throws InterruptedException {
while (isLocked) { // 自旋,防止假唤醒
wait();
}
isLocked = true; // 标记为锁定状态
}
// 释放锁
public synchronized void unlock() {
isLocked = false;
notify(); // 唤醒等待线程
}
}
同一线程可重复获取已持有的锁(synchronized、ReentrantLock 均支持可重入),示例:
public class ReentrantDemo {
// 两个同步方法,锁对象都是 this
public synchronized void outer() {
inner();
}
public synchronized void inner() {
/* 业务逻辑 */
}
}
需记录'持有锁的线程'和'重入次数',修改后的可重入锁示例:
public class ReentrantLock {
private boolean isLocked = false;
private Thread lockedBy = null; // 持有锁的线程
private int lockedCount = 0; // 重入次数
public synchronized void lock() throws InterruptedException {
Thread currentThread = Thread.currentThread();
// 若锁被占用,且不是当前线程持有,阻塞
while (isLocked && lockedBy != currentThread) {
wait();
}
isLocked = true;
lockedCount++;
lockedBy = currentThread;
}
public synchronized void unlock() {
// 仅持有锁的线程可释放
if (Thread.currentThread() != lockedBy) {
throw new IllegalMonitorStateException("未持有锁,无法释放");
}
lockedCount--; // 重入次数为 0 时,才释放锁
if (lockedCount == 0) {
isLocked = false;
lockedBy = null;
notify();
}
}
}
用 Lock 锁时,临界区可能抛出异常,需在 finally 中释放锁,避免锁泄露(锁永久被占用):
lock.lock();
try {
// 临界区(可能抛出异常)
} finally {
lock.unlock(); // 确保无论是否异常,都释放锁
}
适用于'读多写少'的场景(如缓存查询、配置读取),解决'读 - 读互斥'的性能问题,核心原则: 读 - 读共存、读 - 写互斥、写 - 写互斥。
public class ReadWriteLock {
private int readers = 0; // 读线程数量
private int writers = 0; // 写线程数量
private int writeRequests = 0; // 写请求数量(优先写)
// 获取读锁
public synchronized void lockRead() throws InterruptedException {
// 有写线程或写请求,阻塞(优先写)
while (writers > 0 || writeRequests > 0) {
wait();
}
readers++;
}
// 释放读锁
public synchronized void unlockRead() {
readers--;
notifyAll(); // 唤醒等待的写线程
}
// 获取写锁
public synchronized void lockWrite() throws InterruptedException {
writeRequests++; // 有读线程或写线程,阻塞
while (readers > 0 || writers > 0) {
wait();
}
writeRequests--;
writers++;
}
// 释放写锁
public synchronized void unlockWrite() {
writers--;
notifyAll(); // 唤醒所有等待的读/写线程
}
}
上述简单实现不可重入,会导致死锁(如持有写锁的线程再次请求写锁、持有读锁的线程再次请求读锁),需优化:
控制并发线程数量(如限流、连接池控制),可看作'可计数的锁',JUC 中已提供 java.util.concurrent.Semaphore,无需自定义。
// 上限 3 个线程同时执行
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可(阻塞,直到有空闲许可)
// 临界区(如接口调用、资源操作)
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
}).start();
}
当队列空时,获取元素(take())会阻塞;当队列满时,添加元素(put())会阻塞,无需手动处理线程同步(内部已实现)。
实现生产者 - 消费者模型(解耦生产者和消费者,平衡两者速度),线程池底层核心组件(缓存任务)。
public class BlockingQueue {
private final List<Object> queue = new LinkedList<>();
private final int limit; // 队列最大容量
public BlockingQueue(int limit) {
this.limit = limit;
}
// 入队(满则阻塞)
public synchronized void enqueue(Object item) throws InterruptedException {
while (queue.size() == limit) {
wait(); // 队列满,阻塞生产者
}
if (queue.size() == 0) {
notifyAll(); // 队列空,唤醒消费者
}
queue.add(item);
}
// 出队(空则阻塞)
public synchronized Object dequeue() throws InterruptedException {
while (queue.size() == 0) {
wait(); // 队列空,阻塞消费者
}
if (queue.size() == limit) {
notifyAll(); // 队列满,唤醒生产者
}
return queue.remove(0);
}
}
如 ArrayBlockingQueue(有界)、LinkedBlockingQueue(无界/有界)、SynchronousQueue(无缓冲),直接使用即可,无需自定义。
// 线程池核心类
public class ThreadPool {
private final BlockingQueue<Runnable> taskQueue; // 任务队列
private final List<PoolThread> threads; // 工作线程集合
private boolean isStopped = false; // 线程池状态
// 构造方法:指定线程数、队列最大容量
public ThreadPool(int threadCount, int maxTaskCount) {
taskQueue = new BlockingQueue<>(maxTaskCount);
threads = new ArrayList<>(threadCount);
// 初始化工作线程
for (int i = 0; i < threadCount; i++) {
threads.add(new PoolThread(taskQueue));
}
// 启动所有工作线程
for (PoolThread thread : threads) {
thread.start();
}
}
// 提交任务
public synchronized void execute(Runnable task) {
if (isStopped) {
throw new IllegalStateException("线程池已停止");
}
taskQueue.enqueue(task); // 任务入队
}
// 停止线程池
public synchronized void stop() {
isStopped = true; // 停止所有工作线程
for (PoolThread thread : threads) {
thread.toStop();
}
}
}
// 工作线程类
class PoolThread extends Thread {
private final BlockingQueue<Runnable> taskQueue;
private boolean isStopped = false;
public PoolThread(BlockingQueue<Runnable> queue) {
this.taskQueue = queue;
}
@Override
public void run() {
// 循环获取任务,直到线程池停止
while (!isStopped()) {
try {
// 从队列获取任务(空则阻塞)
Runnable task = taskQueue.dequeue();
task.run(); // 执行任务
} catch (Exception e) {
// 捕获异常,避免线程退出(线程池继续运行)
e.printStackTrace();
}
}
}
// 停止当前工作线程
public synchronized void toStop() {
isStopped = true;
this.interrupt(); // 打断阻塞在 dequeue() 的线程
}
public synchronized boolean isStopped() {
return isStopped;
}
}
使用 java.util.concurrent.ExecutorService,推荐手动创建 ThreadPoolExecutor(避免 Executors 工具类的 OOM 风险),核心参数:核心线程数、最大线程数、空闲线程存活时间、任务队列、拒绝策略。
CAS(Compare and Swap,比较并替换):一种无锁原子操作,底层由 CPU 指令(如 cmpxchg)保证原子性,核心逻辑:
无锁、无线程上下文切换开销、无死锁风险,性能优于 synchronized(高并发、低冲突场景)。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| ABA 问题 | V 的值从 A→B→A,CAS 认为未修改,导致错误替换 | 用 AtomicStampedReference(加版本号) |
| 自旋消耗 CPU | 高并发下,CAS 多次失败,循环重试消耗 CPU | 限制自旋次数,或搭配锁使用 |
| 只能保证单个变量原子性 | CAS 仅能操作单个变量,无法保证多个变量的复合操作原子性 | 用 AtomicReference 包装多个变量,或使用锁 |
锁、信号量、阻塞队列等同步器,底层设计逻辑一致,均包含 4 个核心部分:
| 类型 | 核心逻辑 | 优点 | 缺点 | 示例 |
|---|---|---|---|---|
| 阻塞算法 | 获取不到资源时,线程挂起,直到资源可用 | 实现简单,无 CPU 自旋消耗 | 线程切换开销大,可能死锁 | synchronized、Lock |
| 非阻塞算法 | 获取不到资源时,不挂起,直接返回或重试 | 无线程切换开销,无死锁 | 实现复杂,高冲突下自旋消耗 CPU | CAS、原子类 |
核心思想:乐观假设'无并发冲突',线程先拷贝共享资源、修改,再通过 CAS 将修改写回主内存,冲突则重试。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online