跳到主要内容Java 多线程与并发核心机制详解 | 极客日志Javajava算法
Java 多线程与并发核心机制详解
Java 多线程与并发涉及线程创建、安全控制及资源调度。核心包括继承 Thread 或实现 Runnable 接口启动线程,通过 synchronized 和 Lock 保证临界区原子性与可见性。竞态条件需通过锁机制解决,死锁避免依赖加锁顺序。线程通信使用 wait/notify 配合唯一锁对象。线程池优化资源管理,CAS 提供无锁高性能操作。读写锁提升读多写少场景性能,阻塞队列解耦生产消费。掌握这些机制是构建高并发系统的基础。
19510189250 浏览 Java 多线程与并发核心机制详解
一、多线程基础:优缺点与核心代价
1. 核心优点
- 资源利用率更高:CPU 空闲时可调度其他线程执行,避免硬件资源浪费(如 IO 等待时 CPU 不闲置)。
- 程序设计更简洁:异步场景(如文件下载、接口调用)可通过多线程拆分任务,简化复杂逻辑。
- 程序响应更快:UI 界面、服务端程序可通过多线程避免主线程阻塞,提升用户/调用方体验。
2. 主要代价(易忽略细节)
- 设计复杂度上升:需处理线程安全、同步、死锁、线程通信等问题,调试难度增加。
- 上下文切换开销:线程切换时需保存/恢复线程上下文(栈、寄存器等),消耗 CPU 资源(高频切换会严重影响性能)。
额外资源消耗:每个线程需占用栈内存(默认 1M 左右)、内核对象,过多线程会导致内存溢出(OOM)。二、线程创建与启动(关键细节补全)
1. 两种标准实现方式
方式 1:继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
}
}
new MyThread().start();
方式 2:实现 Runnable 接口(推荐)
class MyTask implements Runnable {
@Override
public void run() {
}
}
new Thread(new MyTask()).start();
2. 选型建议(笔记补充)
- 解耦:任务逻辑(Runnable)与线程控制(Thread)分离,便于复用、维护。
- 适配高级特性:线程池、Future 等 JUC 工具类仅支持 Runnable 接口,可降低后续扩展成本。
- 规避单继承限制:Java 是单继承语言,继承 Thread 会占用唯一的继承名额,影响类的扩展性。
3. 经典错误(必记)
调用 run() 方法而非 start():
直接调用 run() 只是普通方法调用(运行在当前线程),不会启动新线程;start() 才会触发 JVM 创建新线程,执行 run() 方法。
三、线程安全核心:竞态条件与临界区
1. 核心概念
- 竞态条件:多线程竞争同一共享资源时,执行结果依赖线程执行顺序(如多线程自增同一个变量,结果可能小于预期值)。
- 临界区:导致竞态条件的代码片段(即多个线程共享资源并可能修改资源的代码块)。
2. 本质原因
共享资源的'读 - 改 - 写'复合操作非原子性(如 count++,实际分为 3 步:读 count 值→自增→写回 count,多线程交错执行会导致数据错乱)。
四、线程安全判定:线程控制逃逸规则(重点)
核心判定原则:若一个资源(对象、文件、数据库连接等)的创建、使用、销毁,全程在同一个线程内完成,且不会逃逸到线程外部(即其他线程无法访问该资源),则该资源的使用是线程安全的。
1. 天生线程安全的场景
- 局部基本变量:存储在线程私有栈中,不共享,天然安全(如方法内的 int、boolean 变量)。
- 局部对象引用(无逃逸):对象本身在堆内存(共享区域),但引用仅存在于当前线程栈,且未通过返回值、参数传递等方式暴露给其他线程,线程安全。
2. 天生线程不安全的场景
对象成员变量(存储在堆内存,多线程可通过对象引用访问并修改,若未加同步,必存在线程安全问题)。
五、不可变性与线程安全(补全细节)
- 核心结论:只读共享的资源不会产生线程安全问题;不可变对象(状态一旦创建无法修改)天然线程安全(如 String、Integer 等包装类)。
- 关键提醒:不可变对象的引用不一定安全!例如:
AtomicReference<String> ref = new AtomicReference<>("a");,ref 引用本身可被多线程修改(需用原子类保护),但引用指向的 String 对象本身不可变。
六、synchronized 同步机制(核心,笔记细节补全)
synchronized 是 Java 原生的悲观锁,通过'互斥'保证临界区原子性,同时保证可见性(解锁前的修改对后续加锁线程可见)和可重入性。
1. 四种作用范围(附实例与锁对象)
| 作用范围 | 锁对象 | 实例代码 | 说明 |
|---|
| 实例方法同步 | 当前对象(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) { … } | 与静态同步方法锁对象一致,可灵活控制同步范围 |
2. 核心特性(必记)
- 可重入性:同一线程可重复获取同一把锁(如同步方法调用另一个同步方法,不会死锁)。
- 互斥性:同一时刻,仅一个线程能持有锁并进入临界区。
- 可见性:线程解锁前,对共享变量的修改会强制刷新到主内存,后续加锁线程会从主内存读取最新值。
- 非公平性:线程唤醒后会随机竞争锁,不保证请求顺序(默认非公平,性能更优)。
七、线程间通信(重点:wait/notify/notifyAll)
线程间通信的核心是'协作'(如生产者 - 消费者模型),常用方式:共享对象通信、忙等待、wait/notify/notifyAll(推荐)。
1. 共享对象通信(基础)
通过共享对象的成员变量传递信号(需配合同步,避免竞态条件)。例如:线程 A 在同步块中设置 hasDataToProcess = true,线程 B 在同步块中读取该变量。
2. 忙等待(不推荐)
线程 B 循环等待信号,浪费 CPU 资源(空闲时也占用 CPU):
while(!sharedSignal.hasDataToProcess()){
}
3. wait()、notify()、notifyAll()(核心,补全细节)
强制规则(违反抛 IllegalMonitorStateException)
必须在 synchronized 同步块/方法中调用,且调用对象必须是'当前持有锁的对象'(即同步块的锁对象)。
核心机制(易混淆点)
- wait():线程释放持有的锁,进入该锁的等待队列,变为非运行状态,直到被 notify()/notifyAll() 唤醒。
- notify():随机唤醒等待队列中的一个线程(唤醒后需重新竞争锁,才能进入临界区)。
- notifyAll():唤醒等待队列中的所有线程(所有线程竞争锁,依次进入临界区)。
关键疑问解答(笔记补充)
问:等待线程持有锁,会阻塞唤醒线程进入同步块吗?
答:不会。线程调用 wait() 后,会立即释放锁,允许其他线程(包括唤醒线程)获取锁并进入同步块;唤醒线程执行完同步块、释放锁后,被唤醒的线程才会竞争锁,成功后退出 wait()。
4. 常见问题与解决方案(必记)
| 问题 | 原因 | 解决方案 |
|---|
| 丢失信号 | 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) {
monitor.wait();
}
wasSignalled = false;
}
}
public void doNotify() {
synchronized (monitor) {
wasSignalled = true;
monitor.notify();
}
}
}
八、死锁(核心:原因与避免)
1. 定义
两个或多个线程互相持有对方需要的锁,且永久阻塞,无法继续执行(线程'互相僵持')。
2. 典型场景(必记)
- 基础场景:线程 1 锁 A→等 B,线程 2 锁 B→等 A,互相阻塞。
- 代码场景(TreeNode 示例):线程 1 调用 parent.addChild(child)(锁 parent),线程 2 调用 child.setParent(parent)(锁 child),后续线程 1 需锁 child、线程 2 需锁 parent,导致死锁。
- 数据库场景:多个事务更新相同记录,顺序相反(事务 1 锁记录 1→等记录 2,事务 2 锁记录 2→等记录 1)。
3. 死锁产生的 4 个必要条件(缺一不可)
- 互斥:锁资源只能被一个线程持有。
- 持有并等待:线程持有一个锁,同时等待另一个锁。
- 不可剥夺:线程持有锁时,不能被其他线程强制剥夺。
- 循环等待:多个线程形成'互相等待锁'的循环链。
4. 避免死锁的 3 种核心策略(重点)
策略 1:固定加锁顺序(最常用、最易实现)
所有线程按相同顺序获取锁(如先锁 A、再锁 B),打破'循环等待'条件。
策略 2:加锁时限
尝试获取锁时设置超时时间(如用 Lock.tryLock(long timeout, TimeUnit unit)),超时则释放已持有锁、回退,等待随机时间后重试,打破'持有并等待'条件。
策略 3:死锁检测
- 用数据结构(如 Map、有向图)记录线程持有/请求的锁。
- 线程请求锁失败时,遍历锁关系图,检测是否存在循环(死锁)。
- 检测到死锁后,释放所有锁、回退重试,或按优先级让部分线程回退。
九、饥饿与公平性
1. 核心概念
- 饥饿:线程长期得不到 CPU 运行时间或锁资源,最终'饥饿致死'(如低优先级线程被高优先级线程长期抢占 CPU)。
- 公平性:所有线程公平获取资源(按请求顺序获取),避免饥饿。
2. Java 中导致饥饿的 3 个原因
- 高优先级线程吞噬低优先级线程的 CPU 时间(Java 优先级 1-10,优先级高的线程被调度概率高)。
- 线程永久阻塞在同步块外(非公平锁下,新线程可能持续抢占锁,老线程一直等待)。
- 线程等待一个永久无法完成的对象(如等待一个不会被 notify() 的对象)。
3. 实现公平性的方案
用 Lock 锁替代 synchronized(synchronized 默认非公平),自定义公平锁或使用 JUC 的公平锁(如 ReentrantLock(true)):
- 公平锁会维护一个等待队列,解锁后仅唤醒队列头部的线程,保证请求顺序。
- 注意:公平锁性能低于非公平锁(需维护队列、切换线程),仅在需要避免饥饿时使用。
十、Java 中的锁(进阶,补全笔记细节)
1. 简单锁的实现(理解原理)
核心是用'状态变量 + 同步'控制线程访问,示例(不可重入锁):
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();
}
}
2. 锁的可重入性(重点)
定义
同一线程可重复获取已持有的锁(synchronized、ReentrantLock 均支持可重入),示例:
public class ReentrantDemo {
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--;
if (lockedCount == 0) {
isLocked = false;
lockedBy = null;
notify();
}
}
}
3. 关键注意点:finally 中调用 unlock()
用 Lock 锁时,临界区可能抛出异常,需在 finally 中释放锁,避免锁泄露(锁永久被占用):
lock.lock();
try {
} finally {
lock.unlock();
}
十一、读写锁(ReentrantReadWriteLock)
1. 核心场景
适用于'读多写少'的场景(如缓存查询、配置读取),解决'读 - 读互斥'的性能问题,核心原则:
读 - 读共存、读 - 写互斥、写 - 写互斥。
2. 简单实现(理解原理)
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();
}
}
3. 读写锁的可重入性(补全笔记)
上述简单实现不可重入,会导致死锁(如持有写锁的线程再次请求写锁、持有读锁的线程再次请求读锁),需优化:
- 读锁重入:用 Map 记录线程及读锁重入次数,已持有读锁的线程可再次获取读锁(无论是否有写请求)。
- 写锁重入:记录持有写锁的线程及重入次数,已持有写锁的线程可再次获取写锁。
- 锁降级:持有写锁的线程可直接获取读锁(无需释放写锁),避免写锁释放后被其他线程抢占。
十二、信号量(Semaphore)
1. 核心作用
控制并发线程数量(如限流、连接池控制),可看作'可计数的锁',JUC 中已提供 java.util.concurrent.Semaphore,无需自定义。
2. 核心特性
- 可计数:允许同时获取多个许可(如 permits=5,可同时有 5 个线程获取许可)。
- 有上限:可设置最大许可数(如上限 10,超过则阻塞)。
- 可当作锁:当 permits=1 时,等价于非公平锁(控制单个线程进入临界区)。
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();
}
十三、阻塞队列(BlockingQueue)
1. 核心区别(与普通队列)
当队列空时,获取元素(take())会阻塞;当队列满时,添加元素(put())会阻塞,无需手动处理线程同步(内部已实现)。
2. 核心作用
实现生产者 - 消费者模型(解耦生产者和消费者,平衡两者速度),线程池底层核心组件(缓存任务)。
3. 简单实现(理解原理)
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);
}
}
4. JUC 中的阻塞队列(常用)
如 ArrayBlockingQueue(有界)、LinkedBlockingQueue(无界/有界)、SynchronousQueue(无缓冲),直接使用即可,无需自定义。
十四、线程池(核心,补全笔记细节)
1. 核心价值
- 避免频繁创建/销毁线程的性能开销(线程创建需分配栈内存、内核对象)。
- 控制并发线程数量,防止线程过多导致 OOM 或 CPU 耗尽。
- 统一管理线程,便于监控、调度、复用(如任务缓存、拒绝策略)。
2. 核心结构
- 工作线程集合:长期存活,循环从队列中获取任务执行。
- 阻塞队列:缓存等待执行的任务(如生产者 - 消费者模型中的队列)。
- 拒绝策略:队列满且线程数达到最大时,处理新任务的策略(如抛异常、丢弃、阻塞)。
3. 简单实现(理解原理)
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();
}
public synchronized boolean isStopped() {
return isStopped;
}
}
4. JUC 中的线程池(重点)
使用 java.util.concurrent.ExecutorService,推荐手动创建 ThreadPoolExecutor(避免 Executors 工具类的 OOM 风险),核心参数:核心线程数、最大线程数、空闲线程存活时间、任务队列、拒绝策略。
十五、CAS 与原子类(无锁编程核心)
1. CAS 核心概念
CAS(Compare and Swap,比较并替换):一种无锁原子操作,底层由 CPU 指令(如 cmpxchg)保证原子性,核心逻辑:
- 传入 3 个参数:内存地址 V、期望值 A、新值 B。
- 比较 V 的值与 A:若相等,将 V 的值替换为 B;若不相等,不做操作。
- 返回操作结果(是否替换成功)。
2. 核心优点
无锁、无线程上下文切换开销、无死锁风险,性能优于 synchronized(高并发、低冲突场景)。
3. 核心问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|
| ABA 问题 | V 的值从 A→B→A,CAS 认为未修改,导致错误替换 | 用 AtomicStampedReference(加版本号) |
| 自旋消耗 CPU | 高并发下,CAS 多次失败,循环重试消耗 CPU | 限制自旋次数,或搭配锁使用 |
| 只能保证单个变量原子性 | CAS 仅能操作单个变量,无法保证多个变量的复合操作原子性 | 用 AtomicReference 包装多个变量,或使用锁 |
4. JUC 原子类(常用)
- 基本类型:AtomicInteger、AtomicLong、AtomicBoolean(如计数器、状态标记)。
- 引用类型:AtomicReference、AtomicStampedReference(解决 ABA 问题)。
- 数组类型:AtomicIntegerArray、AtomicLongArray(原子操作数组元素)。
十六、同步器核心思想(查漏补缺)
锁、信号量、阻塞队列等同步器,底层设计逻辑一致,均包含 4 个核心部分:
- 状态:控制线程访问权限(如 Lock 的 isLocked、Semaphore 的 permits)。
- 访问条件:基于状态判断线程是否可访问(如 while 循环检查,防止假唤醒)。
- 状态变化:线程获取/释放资源时,修改同步器状态(如 Lock 的 isLocked=true/false)。
- 通知策略:状态变化后,通知等待线程(notify()/notifyAll(),如释放锁后唤醒等待线程)。
同步器的两种核心方法
- Test-and-Set(测试 - 设置):原子操作,先检查访问条件,满足则修改状态(如 CAS、lock())。
- Set(设置):仅修改状态,不检查条件(如 unlock(),持有锁的线程可直接释放)。
十七、阻塞算法与非阻塞算法(补全笔记)
1. 核心区别
| 类型 | 核心逻辑 | 优点 | 缺点 | 示例 |
|---|
| 阻塞算法 | 获取不到资源时,线程挂起,直到资源可用 | 实现简单,无 CPU 自旋消耗 | 线程切换开销大,可能死锁 | synchronized、Lock |
| 非阻塞算法 | 获取不到资源时,不挂起,直接返回或重试 | 无线程切换开销,无死锁 | 实现复杂,高冲突下自旋消耗 CPU | CAS、原子类 |
2. 乐观锁(非阻塞算法核心)
核心思想:乐观假设'无并发冲突',线程先拷贝共享资源、修改,再通过 CAS 将修改写回主内存,冲突则重试。
- 适用场景:低并发、低冲突(如缓存更新),避免锁的开销。
- 注意:高冲突场景下,重试频繁,性能低于阻塞算法。
十八、高频面试重点(浓缩必记)
- 线程启动必须用 start(),调用 run() 仅为普通方法调用,不启动新线程。
- 线程安全三要素:原子性(CAS、锁)、可见性(synchronized、volatile)、有序性(synchronized、volatile)。
- synchronized 与 Lock 的区别:Lock 可中断、可超时、可公平/非公平,synchronized 自动释放锁、可重入、简单易用。
- 死锁产生的 4 个条件及避免方法(固定加锁顺序最常用)。
- CAS 的原理、优点、ABA 问题及解决方案。
- 线程池的核心价值、结构,手动创建 ThreadPoolExecutor 的原因(避免 Executors 的 OOM)。
- 读写锁的核心原则(读 - 读共存、读 - 写/写 - 写互斥),适用场景(读多写少)。
- wait() 与 sleep() 的区别:wait() 释放锁、需在同步块中,sleep() 不释放锁、可在任意位置。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online