Java 并发编程面试核心知识点精要

🔥 一、notify () 和 notifyAll () 的区别(重点)

✅ 核心区别

特性 notify() notifyAll()
唤醒数量 随机唤醒 一个 等待线程 唤醒 所有 等待线程
唤醒方式 JVM 随机选择(不可控) 所有在等待队列中的线程都被唤醒
资源竞争 被唤醒线程进入锁池争锁 所有被唤醒线程进入锁池共同争锁
使用安全性 较低,可能 “线程饥饿” 或漏唤醒 更安全,推荐复杂场景
性能开销 小(仅唤醒一个) 相对较大(多线程争锁)

🧠 关键概念解析

1. 等待池(Wait Set / 等待队列)

当线程调用对象的 wait() 方法时,会发生以下操作:

  • 释放该对象的 内置锁(monitor 锁)
  • 进入该对象的 等待池
  • 等待池中的线程 不再参与锁的竞争
  • 只能通过 notify() 或 notifyAll() 被唤醒
  • 唤醒后不会立即执行,需转移到 锁池 重新争锁
2. 锁池(Entry Set / 阻塞队列)
  • 所有试图获取同步锁但失败的线程会进入锁池
  • 只有获得对象锁的线程才能执行 synchronized 代码块
  • 每个对象的锁同一时间只能被一个线程持有
  • 被 notify() 唤醒的线程必须重新竞争锁才能继续执行

💡 使用建议(面试加分项)

场景 推荐使用
所有等待线程等待同一条件,且仅需一个线程处理(如单消费者场景) notify()
多个线程等待不同条件,或无法确定哪个线程满足当前条件 notifyAll () ✅(推荐)
对性能要求极高 根据场景谨慎选择

🧪 经典示例:生产者 - 消费者模型

public class BoundedBuffer {
    private final Object lock = new Object();
    private final List<Object> items = new ArrayList<>();
    private static final int MAX_SIZE = 10;
    
    // 生产者
    public void produce(Object item) throws InterruptedException {
        synchronized (lock) {
            while (items.size() == MAX_SIZE) {
                lock.wait(); // 缓冲区满,进入等待池
            }
            items.add(item);
            lock.notifyAll(); // 唤醒所有消费者(可能存在多个)
        }
    }
    
    // 消费者
    public Object consume() throws InterruptedException {
        synchronized (lock) {
            while (items.isEmpty()) {
                lock.wait(); // 缓冲区空,进入等待池
            }
            Object item = items.remove(0);
            lock.notifyAll(); // 唤醒所有生产者
            return item;
        }
    }
}

关键说明:若用 notify(),可能唤醒另一个生产者线程(而非消费者),导致消费者持续等待,引发 “信号丢失”。

📝 面试回答模板

notify() 会随机唤醒一个等待线程,具体唤醒哪个由 JVM 决定,可能因 “唤醒错误线程” 导致 “线程饥饿” 或信号丢失;notifyAll() 会唤醒所有等待线程,这些线程进入锁池重新争锁,虽性能开销略大,但能避免上述问题,在多条件等待场景中更安全可靠,是更推荐的选择。

🔥 二、如何选择锁的范围(重点)

✅ 核心原则

只对真正需要同步的代码加锁,避免不必要的同步操作 —— 锁的本质是串行化访问共享资源,过度同步会降低并发性能。

🔍 为什么要控制锁的范围?

  • 锁范围越大 → 线程持有锁时间越长 → 其他线程等待越久 → 并发度下降
  • 锁范围越小 → 持有锁时间越短 → 更多线程快速获取锁 → 吞吐量提升

🧱 如何合理选择锁的范围?

✅ 正确做法:细粒度加锁
public class Counter {
    private int count = 0;

    // ❌ 错误示范:锁范围过大(包含非同步操作)
    public void badMethod() throws InterruptedException {
        synchronized (this) {
            System.out.println("Doing some unrelated work..."); // 无共享状态操作
            Thread.sleep(100); // 耗时操作
            count++; // 仅这一句需要同步
        }
    }

    // ✅ 正确示范:缩小锁范围
    public void goodMethod() throws InterruptedException {
        // 非临界区代码(不加锁)
        System.out.println("Doing some unrelated work...");
        Thread.sleep(100);
        // 仅对共享变量操作加锁
        synchronized (this) {
            count++;
        }
    }
}

💡 小技巧:将耗时操作移出同步块

  • 禁止将 I/O 操作(文件读写、网络请求)、日志打印、Thread.sleep () 等耗时操作放入 synchronized 块内
  • 否则会导致其他线程长时间阻塞,严重降低并发性能

🛠️ 进阶策略:使用局部同步对象

避免直接锁定 this(整个对象),改用专门的锁对象,进一步控制锁粒度:

public class BankAccount {
    // 专用锁对象(private final 保证不可变,避免被外部修改)
    private final Object lock = new Object();
    private double balance;

    public void transfer(double amount) {
        synchronized (lock) {
            this.balance += amount;
        }
    }
}

优势

  1. 防止外部代码恶意锁定 this 对象
  2. 更灵活地控制锁的作用域(如多锁分离场景)

🔄 根据场景选择合适的锁机制

场景 推荐方式
简单原子操作(如 i++) 使用 AtomicInteger 等 CAS 类(无锁化)
读多写少 使用 ReentrantReadWriteLock 或 StampedLock
高并发争用严重 使用 LongAdder、ConcurrentHashMap 等分段锁结构
必须使用 synchronized 缩小同步块,避免嵌套锁

⚖️ 经典权衡案例:缓存加载

❌ 错误示例(耗时操作在同步块内)
public String getData(String key) {
    synchronized (cache) {
        String value = cache.get(key);
        if (value == null) {
            value = loadFromDB(); // 耗时数据库操作(不应在同步块内)
            cache.put(key, value);
        }
        return value;
    }
}
✅ 改进方案:双重检查 + 局部加锁
private final Map<String, String> cache = new ConcurrentHashMap<>();
private final Object lock = new Object();

public String getData(String key) {
    // 第一次检查(无锁,快速返回已存在数据)
    String value = cache.get(key);
    if (value == null) {
        // 局部加锁(仅缓存未命中时同步)
        synchronized (lock) {
            // 第二次检查(防止多线程重复加载)
            value = cache.get(key);
            if (value == null) {
                value = loadFromDB(); // 耗时操作移出锁外(仅同步时执行一次)
                cache.put(key, value);
            }
        }
    }
    return value;
}

📝 面试回答模板

选择锁范围的核心原则是 “最小粒度同步”:只对操作共享资源的临界区代码加锁,将非同步操作(如日志、I/O)和耗时操作(如数据库查询)移出同步块,减少线程持有锁的时间;同时可通过 “专用锁对象” 替代 this 锁定,避免外部干扰;复杂场景下,还可结合无锁类(如 Atomic 系列)或并发容器(如 ConcurrentHashMap)进一步优化,平衡线程安全与并发性能。

🔥 三、如何停止一个线程(重点)

🚫 不推荐:Thread.stop ()

  • Thread.stop() 已被 标记为废弃(deprecated)
  • 风险:立即终止线程并释放所有锁,可能导致:
    1. 数据不一致(如对象状态未完成更新)
    2. 锁未正确释放,引发死锁或资源泄漏

✅ 正确方式一:使用中断机制(interrupt)—— 推荐标准做法

Java 提供 “中断协议”,通过 “中断标志位” 实现线程优雅退出:

public class InterruptExample implements Runnable {
    @Override
    public void run() {
        // 定期检查中断状态
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 阻塞方法(如 sleep/wait/join)会响应中断
                Thread.sleep(1000);
                System.out.println("执行任务中...");
            } catch (InterruptedException e) {
                // 关键:捕获异常后,中断状态会被清除,需重新设置
                Thread.currentThread().interrupt();
                System.out.println("接收到中断信号,准备退出");
                break; // 退出循环,执行清理
            }
        }
        // 资源清理(如关闭连接、释放文件)
        cleanup();
    }

    private void cleanup() {
        System.out.println("执行资源清理");
    }

    // 外部触发中断
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new InterruptExample());
        thread.start();
        Thread.sleep(3000); // 运行3秒后触发中断
        thread.interrupt();
    }
}
📌 关键点
  1. interrupt() 不直接停止线程,仅设置 “中断标志位”
  2. 阻塞方法(sleep/wait/join)会触发 InterruptedException,并 自动清除中断状态
  3. 捕获异常后必须 重新调用 interrupt (),确保上层代码感知中断请求

✅ 正确方式二:使用 volatile 标志位控制(配合中断更佳)

适用于自定义循环任务场景,通过 volatile 保证变量可见性:

public class VolatileStopExample implements Runnable {
    // volatile 保证多线程间可见性
    private volatile boolean running = true;

    @Override
    public void run() {
        // 以标志位控制循环
        while (running) {
            System.out.println("执行任务中...");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        cleanup(); // 资源清理
    }

    // 外部调用此方法停止线程
    public void shutdown() {
        running = false;
    }

    private void cleanup() {
        System.out.println("执行资源清理");
    }
}
📌 为什么用 volatile?
  • 防止线程因 “CPU 缓存” 导致无法及时感知 running 的变化
  • 保证多线程间 running 变量的可见性(写操作立即刷新到主内存,读操作从主内存读取)

✅ 正确方式三:线程池管理 —— 大量线程场景的标准做法

若使用线程池(如 ExecutorService),需通过线程池 API 关闭,而非直接操作线程:

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ThreadPoolStopExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 提交任务
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> System.out.println("执行线程池任务"));
        }

        // 方式1:平滑关闭(不接受新任务,等待已提交任务完成)
        executor.shutdown();

        // 方式2:等待超时(5秒内未完成则强制处理)
        if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
            // 方式3:立即中断所有正在执行的任务
            List<Runnable> droppedTasks = executor.shutdownNow();
            System.out.println("未执行的任务数:" + droppedTasks.size());
        }
    }
}
📌 最佳实践
  1. 先调用 shutdown() 让任务自然结束
  2. 用 awaitTermination(timeout) 等待完成
  3. 超时未结束时,再调用 shutdownNow() 强制中断

🔔 注意事项(面试加分项)

注意点 说明
处理 InterruptedException 不可忽略该异常,需恢复中断状态或向上抛出,遵循中断语义
资源清理工作 线程退出前必须关闭文件、网络连接、数据库连接等资源
避免暴力终止 即使程序退出,也应让线程自行清理,防止数据损坏

📝 面试回答模板

停止线程的核心是 “优雅退出”,不推荐使用废弃的 stop() 方法;推荐两种标准方式:一是使用 中断机制,通过 interrupt() 发送中断信号,线程内部通过 isInterrupted() 检查状态,捕获 InterruptedException 后需重新设置中断状态;二是使用 volatile 标志位,通过可见性变量控制循环退出;若使用线程池,需调用 shutdown() + awaitTermination() 平滑关闭,超时后再用 shutdownNow() 强制中断,确保资源不泄漏。

🔥 四、synchronized 和 Lock 的区别(重点)

✅ 核心区别对比

对比维度 synchronized Lock(如 ReentrantLock)
实现层次 JVM 关键字(底层由 JVM 实现) JDK 接口(java.util.concurrent.locks)
锁类型 隐式锁(自动获取 / 释放) 显式锁(需手动调用 lock ()/unlock ())
异常处理 异常时自动释放锁,安全性高 异常不自动释放,需在 finally 中手动释放
中断响应 不支持(线程阻塞后无法中断) 支持(lockInterruptibly () 可中断等待)
尝试获取锁 无法尝试,只能阻塞等待 支持 tryLock (),可设置超时时间
读写分离 不支持 支持 ReadWriteLock,提升读并发
条件变量 依赖 wait ()/notify ()/notifyAll ()(仅 1 个等待队列) 依赖 Condition(支持多个等待队列)
公平性 仅非公平锁(默认,不可更改) 可选择公平 / 非公平锁(构造参数控制)
性能表现 低并发时性能好,高并发时低于 Lock 高并发场景性能更优,适合复杂控制

✅ 核心特性详解

1. 自动 vs 手动释放锁
// synchronized:自动释放(异常也会释放)
public synchronized void syncMethod() {
    throw new RuntimeException("异常测试"); // 锁会自动释放
}

// Lock:必须手动释放(依赖 try-finally)
public void lockMethod() {
    Lock lock = new ReentrantLock();
    lock.lock(); // 手动加锁
    try {
        // 业务逻辑
    } finally {
        lock.unlock(); // 必须在 finally 中释放,防止死锁
    }
}
2. 中断支持
public void interruptibleLock() throws InterruptedException {
    Lock lock = new ReentrantLock();
    // 支持中断的加锁方式:线程被中断时抛出 InterruptedException
    lock.lockInterruptibly();
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
}
  • synchronized:线程阻塞后无法响应中断,只能一直等待
  • Lock:通过 lockInterruptibly() 支持中断,适合 “需要取消等待” 的场景(如超时控制)
3. 尝试获取锁(tryLock)
public void tryLockExample() throws InterruptedException {
    Lock lock = new ReentrantLock();
    // 尝试获取锁,3秒超时后返回 false
    if (lock.tryLock(3, TimeUnit.SECONDS)) {
        try {
            System.out.println("获取锁成功");
        } finally {
            lock.unlock();
        }
    } else {
        System.out.println("获取锁失败,执行降级逻辑");
    }
}
  • 避免 “无限等待”,可在获取失败时执行降级策略(如返回默认值、记录日志)
4. 读写锁支持(ReadWriteLock)
public class ReadWriteLockExample {
    // 读写锁:读锁共享,写锁独占
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock(); // 读锁
    private final Lock writeLock = rwLock.writeLock(); // 写锁
    private int data;

    // 读操作(共享,多线程可同时执行)
    public int readData() {
        readLock.lock();
        try {
            return data;
        } finally {
            readLock.unlock();
        }
    }

    // 写操作(独占,仅一个线程可执行)
    public void writeData(int newData) {
        writeLock.lock();
        try {
            data = newData;
        } finally {
            writeLock.unlock();
        }
    }
}
  • synchronized 无读写分离,所有访问互斥;ReadWriteLock 在读多写少场景下可大幅提升并发性能
5. Condition 实现精准唤醒
public class ConditionExample {
    private final Lock lock = new ReentrantLock();
    // 多个条件队列(如“非满”和“非空”)
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final Object[] buffer = new Object[10];
    private int count;

    // 生产者:等待“非满”
    public void produce(Object item) throws InterruptedException {
        lock.lock();
        try {
            while (count == buffer.length) {
                notFull.await(); // 进入“非满”等待队列
            }
            buffer[count++] = item;
            notEmpty.signal(); // 唤醒“非空”等待队列的消费者
        } finally {
            lock.unlock();
        }
    }

    // 消费者:等待“非空”
    public Object consume() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await(); // 进入“非空”等待队列
            }
            Object item = buffer[--count];
            notFull.signal(); // 唤醒“非满”等待队列的生产者
        } finally {
            lock.unlock();
        }
    }
}
  • synchronized 仅一个等待队列,notify() 随机唤醒;Condition 支持多等待队列,实现 “精准唤醒”

✅ 使用建议(如何选择?)

场景 推荐使用
简单同步需求(方法 / 代码块级别) ✅ synchronized(简洁、安全,无需手动管理锁)
高并发、复杂控制(超时、中断、读写分离) ✅ Lock(功能更强,性能更优)
需要尝试获取锁或避免死锁 ✅ Lock.tryLock()
读多写少的共享资源(如缓存) ✅ ReentrantReadWriteLock
对性能要求极高且能保证锁安全释放 ✅ Lock

📝 面试回答模板

synchronized 是 JVM 层面的隐式锁,自动获取和释放,异常时安全释放锁,但不支持中断、超时和读写分离;Lock 是 JDK 层面的显式锁,需手动调用 lock()/unlock()(依赖 finally),支持三大核心优势:一是 中断等待(lockInterruptibly ()),二是 尝试获取锁(tryLock () 避免无限等待),三是 读写分离(ReadWriteLock 提升读并发),还可通过 Condition 实现多等待队列的精准唤醒;简单场景用 synchronized 更简洁,复杂高并发场景用 Lock 更灵活高效。

🔥 五、死锁与活锁的区别(重点)

✅ 核心区别

特征 死锁(Deadlock) 活锁(Livelock)
线程状态 被阻塞(Blocked),不再执行 运行状态(Runnable),持续尝试
资源占用 持有部分资源,等待其他资源不释放 主动释放资源并重试,但无进展
系统表现 相关线程完全停滞 CPU 占用高,但无实际任务完成
是否在运行 ❌ 实际已停止 ✅ 仍在活跃运行
检测难度 相对容易(工具可检测循环等待) 较难发现(看似正常运行)
典型场景 多线程互相持有对方所需的锁 多线程为避免冲突持续让步

🔍 一、死锁(Deadlock)—— “互相等待,谁也不放手”

📌 定义

多个线程因竞争资源而相互等待,且每个线程都持有对方所需的资源,导致所有线程无法继续执行,陷入永久阻塞。

💡 四大必要条件(必须同时满足)
  1. 互斥条件:资源一次只能被一个线程使用(如锁的独占性)
  2. 占有并等待:线程已持有部分资源,仍在等待其他资源
  3. 不可抢占:已分配的资源不能被强制剥夺(如锁不能被其他线程强制释放)
  4. 循环等待:存在线程环形链,每个线程等待下一个线程持有的资源
🧪 经典示例:哲学家进餐问题
public class DeadlockExample {
    // 两个叉子(资源)
    private static final Object fork1 = new Object();
    private static final Object fork2 = new Object();

    public static void main(String[] args) {
        // 线程A:先拿叉子1,再拿叉子2
        new Thread(() -> {
            synchronized (fork1) {
                System.out.println("线程A持有叉子1,等待叉子2");
                try {
                    Thread.sleep(100); // 模拟思考时间,让线程B有机会持有叉子2
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (fork2) {
                    System.out.println("线程A持有叉子2,开始吃饭");
                }
            }
        }).start();

        // 线程B:先拿叉子2,再拿叉子1
        new Thread(() -> {
            synchronized (fork2) {
                System.out.println("线程B持有叉子2,等待叉子1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (fork1) {
                    System.out.println("线程B持有叉子1,开始吃饭");
                }
            }
        }).start();
    }
}

结果:线程 A 持有 fork1 等待 fork2,线程 B 持有 fork2 等待 fork1,形成循环等待,陷入死锁。

🔁 二、活锁(Livelock)—— “积极退让,反而谁都做不了事”

📌 定义

线程未被阻塞,但因 “过度协作” 或 “避免冲突” 而不断重复相同动作(如主动释放资源、重试),导致任务始终无法完成,陷入 “忙碌但无效” 的循环。

🧪 经典生活比喻

两个人在狭窄走廊相遇,都礼貌地向同一侧避让 → 撞在一起;再换另一侧 → 又撞在一起,无限循环,即 “活锁”。

🧪 编程示例:银行转账中的活锁
public class LivelockExample {
    static class Account {
        private int balance;

        public Account(int balance) {
            this.balance = balance;
        }

        // 取款(成功返回true,失败返回false)
        public boolean withdraw(int amount) {
            if (amount <= balance) {
                balance -= amount;
                return true;
            }
            return false;
        }

        // 存款
        public void deposit(int amount) {
            balance += amount;
        }

        public int getBalance() {
            return balance;
        }
    }

    // 转账逻辑:余额不足时主动让出CPU,重试
    public static void transfer(Account from, Account to, int amount) {
        while (true) {
            if (from.withdraw(amount)) {
                to.deposit(amount);
                System.out.println("转账成功,from余额:" + from.getBalance() + ",to余额:" + to.getBalance());
                break;
            } else {
                System.out.println("余额不足,重试...");
                Thread.yield(); // 主动让出CPU,避免占用资源
            }
        }
    }

    public static void main(String[] args) {
        Account a = new Account(100);
        Account b = new Account(100);

        // 线程1:a向b转账150(余额不足,重试)
        new Thread(() -> transfer(a, b, 150)).start();
        // 线程2:b向a转账150(余额不足,重试)
        new Thread(() -> transfer(b, a, 150)).start();
    }
}

结果:两个线程均因余额不足持续重试并让出 CPU,陷入 “重试→失败→让出→再重试” 的循环,无法完成转账。

📝 面试回答模板

死锁和活锁的核心区别是 “线程状态” 和 “资源行为”:死锁的线程处于阻塞状态,持有部分资源并等待其他线程的资源,不释放自身资源,导致完全停滞;活锁的线程处于运行状态,主动释放资源并重试,但因 “同步重试” 或 “过度退让” 导致任务无进展,CPU 忙碌但无效。死锁需满足四大条件(互斥、占有等待、不可抢占、循环等待),活锁多因 “协作逻辑缺陷” 导致,检测难度更高。

🔥 六、如何避免活锁(重点)

✅ 四大常用避免活锁的策略

策略 说明 示例场景
1. 引入随机性 ✅ 在重试时加入随机延迟,打破多个线程 “同步重试” 的节奏 分布式系统消息重发、网络请求重试
2. 指数退避(Exponential Backoff)✅ 每次失败后等待时间成倍增长,减少竞争频率 API 调用限流、数据库连接重连
3. 协调机制 / 优先级控制 明确角色分工或设定优先级,避免 “互相谦让” 分布式锁选举、主从切换
4. 限制重试次数 ✅ 设置最大重试次数,防止无限循环 所有需要重试的操作(如转账、请求)

💡 经典代码示例:改进的重试机制(指数退避 + 随机化)

import java.util.Random;

public class AvoidLivelockUtil {
    private static final int MAX_RETRIES = 5; // 最大重试次数
    private static final long BASE_DELAY = 100; // 基础延迟(毫秒)
    private static final Random random = new Random();

    // 带指数退避和随机化的重试逻辑
    public boolean sendDataWithBackoff() {
        int attempts = 0;
        while (attempts < MAX_RETRIES) {
            if (sendData()) { // 尝试发送数据
                System.out.println("第 " + (attempts + 1) + " 次尝试成功");
                return true;
            }
            // 1. 指数退避:等待时间 = 2^attempts * 基础延迟
            long delay = (long) Math.pow(2, attempts) * BASE_DELAY;
            // 2. 加入随机扰动(±100ms),打破同步重试
            delay += random.nextInt(200) - 100; // 随机范围:[-100, 100]
            // 确保延迟不小于0
            delay = Math.max(delay, 0);

            try {
                Thread.sleep(delay);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false; // 中断则放弃重试
            }
            attempts++;
        }
        System.err.println("重试超过最大次数(" + MAX_RETRIES + "次),发送失败");
        return false;
    }

    // 模拟网络发送(30%成功率)
    private boolean sendData() {
        return random.nextDouble() > 0.7;
    }
}
📌 优点
  1. 指数退避Math.pow(2, attempts) 让等待时间随重试次数倍增,减少频繁冲突
  2. 随机扰动random.nextInt(200) - 100 避免多个线程 “同步重试”
  3. 重试上限MAX_RETRIES 防止无限循环,确保最终退出

🔍 为什么这些方法能避免活锁?

方法 破坏的活锁条件
随机延迟 打破线程间的 “同步节奏”,避免同时重试
指数退避 减少重试频率,降低资源竞争强度
限制重试次数 避免无限循环,保证最终能退出
优先级机制 明确 “谁先执行”,避免 “互相礼让”

📝 面试回答模板

避免活锁的核心是 “打破无效循环”,常用四种策略:一是引入随机性,在重试间隔中加入随机时间,防止多线程同步重试;二是指数退避,每次失败后等待时间成倍增长(如 100ms→200ms→400ms),降低冲突频率;三是限制重试次数,设置最大重试上限,避免无限循环;四是建立协调机制,通过优先级或角色分配明确执行顺序,避免 “互相礼让”。实际场景中,常结合 “指数退避 + 随机化”(如分布式请求重试),既减少竞争又打破同步,效果最佳。

🔥 七、死锁的解决方案有哪些(重点)

🛠️ 一、尽量减少锁的使用(根本性预防)

核心思路

从源头减少锁的依赖,优先使用 “无锁结构” 或 “不可变对象” 替代同步操作。

适用场景与方案
场景 推荐方案
简单原子操作(如 i++、计数) 使用 java.util.concurrent.atomic 包(AtomicInteger、AtomicLong)
高并发读写场景 使用并发容器(ConcurrentHashMap、CopyOnWriteArrayList)
无状态服务 设计不可变对象(如 String、Integer),避免共享状态
📌 示例:用 AtomicInteger 替代 synchronized
// ❌ 需加锁的计数
public class SynchronizedCounter {
    private int count = 0;
    public synchronized void increment() { count++; }
    public synchronized int getCount() { return count; }
}

// ✅ 无锁计数(AtomicInteger 基于 CAS 实现)
public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() { count.incrementAndGet(); }
    public int getCount() { return count.get(); }
}

🔁 二、统一锁的获取顺序(破坏循环等待条件)

✅ 核心思想

让所有线程以相同的顺序申请多把锁,打破 “循环等待” 这一死锁必要条件(最实用的方案)。

🧪 示例:转账问题中的死锁改进
❌ 可能发生死锁的代码
// 线程1:先锁 fromA,再锁 toB;线程2:先锁 fromB,再锁 toA → 循环等待
public void transfer(Account from, Account to, int amount) {
    synchronized (from) {
        synchronized (to) {
            from.balance -= amount;
            to.balance += amount;
        }
    }
}
✅ 改进:按账户 ID 排序加锁
public class SafeTransfer {
    static class Account {
        private final int id; // 账户唯一ID
        private int balance;

        public Account(int id, int balance) {
            this.id = id;
            this.balance = balance;
        }

        public int getId() { return id; }
        public int getBalance() { return balance; }
        public void setBalance(int balance) { this.balance = balance; }
    }

    // 按账户ID升序获取锁,避免循环等待
    public void safeTransfer(Account from, Account to, int amount) {
        // 1. 确定锁的顺序(ID小的先锁)
        Account firstLock = from.getId() < to.getId() ? from : to;
        Account secondLock = from.getId() > to.getId() ? from : to;

        // 2. 按顺序加锁
        synchronized (firstLock) {
            synchronized (secondLock) {
                if (from.getBalance() >= amount) {
                    from.setBalance(from.getBalance() - amount);
                    to.setBalance(to.getBalance() + amount);
                    System.out.println("转账成功");
                } else {
                    System.out.println("余额不足");
                }
            }
        }
    }
}

关键:所有线程均按 “ID 升序” 加锁,不会出现 “线程 1 锁 A 等 B,线程 2 锁 B 等 A” 的循环。

⏳ 三、使用带超时的锁(tryLock (timeout),避免无限等待)

核心思路

通过 ReentrantLock.tryLock(timeout) 设置锁的获取超时时间,超时后主动放弃,打破 “占有并等待” 条件。

🧪 示例:带超时的转账
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TimeoutLockTransfer {
    static class Account {
        private final Lock lock = new ReentrantLock();
        private int balance;

        public Account(int balance) {
            this.balance = balance;
        }

        public Lock getLock() { return lock; }
        public int getBalance() { return balance; }
        public void setBalance(int balance) { this.balance = balance; }
    }

    // 带超时的转账(超时时间3秒)
    public boolean transferWithTimeout(Account from, Account to, int amount) throws InterruptedException {
        // 1. 尝试获取“from”账户的锁(3秒超时)
        if (from.getLock().tryLock(3, TimeUnit.SECONDS)) {
            try {
                // 2. 尝试获取“to”账户的锁(3秒超时)
                if (to.getLock().tryLock(3, TimeUnit.SECONDS)) {
                    try {
                        if (from.getBalance() >= amount) {
                            from.setBalance(from.getBalance() - amount);
                            to.setBalance(to.getBalance() + amount);
                            System.out.println("转账成功");
                            return true;
                        } else {
                            System.out.println("余额不足");
                            return false;
                        }
                    } finally {
                        to.getLock().unlock(); // 释放to的锁
                    }
                } else {
                    System.out.println("获取to账户锁超时");
                    return false;
                }
            } finally {
                from.getLock().unlock(); // 释放from的锁
            }
        } else {
            System.out.println("获取from账户锁超时");
            return false;
        }
    }
}
✅ 优势
  1. 超时后主动退出,避免 “无限等待”
  2. 适用于响应时间敏感的系统(如金融交易、RPC 调用)
⚠️ 注意
  • 需合理设置超时时间(过短易误判,过长仍可能阻塞)
  • 超时后需有补偿机制(如记录日志、通知管理员、重试)

🔍 四、死锁检测 + 恢复机制

1. 工具检测(运维层面)
  • jstack 命令:通过 jstack <进程ID> 查看线程堆栈,JVM 会自动提示 “Found one Java-level deadlock”
  • 可视化工具:JConsole、VisualVM、Arthas 等,可实时监控线程状态,定位死锁线程
2. 程序级检测(高级用法)

通过 ThreadMXBean 实现死锁检测,发现死锁后主动中断线程:

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;

public class DeadlockDetector {
    public static void main(String[] args) {
        // 启动死锁检测线程(每5秒检测一次)
        new Thread(() -> {
            ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
            while (true) {
                // 检测死锁线程ID
                long[] deadlockedThreadIds = threadMXBean.findDeadlockedThreads();
                if (deadlockedThreadIds != null && deadlockedThreadIds.length > 0) {
                    System.err.println("检测到死锁,线程ID:" + Arrays.toString(deadlockedThreadIds));
                    // 恢复策略:中断其中一个线程(如第一个)
                    Thread deadThread = findThreadById(deadlockedThreadIds[0]);
                    if (deadThread != null) {
                        deadThread.interrupt();
                        System.err.println("已中断线程:" + deadThread.getId());
                    }
                }
                try {
                    Thread.sleep(5000); // 每5秒检测一次
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }).start();
    }

    // 根据线程ID查找线程(需遍历线程组)
    private static Thread findThreadById(long threadId) {
        ThreadGroup group = Thread.currentThread().getThreadGroup();
        while (group != null) {
            Thread[] threads = new Thread[group.activeCount()];
            int count = group.enumerate(threads);
            for (int i = 0; i < count; i++) {
                if (threads[i].getId() == threadId) {
                    return threads[i];
                }
            }
            group = group.getParent();
        }
        return null;
    }
}
📌 应用场景
  • 数据库系统:检测死锁后,选择一个 “代价最小” 的事务回滚(如执行时间最短的)
  • 分布式系统:通过 “心跳检测” 或 “协调器” 发现死锁,触发服务熔断或重启

🧩 五、破坏死锁四大必要条件(理论基础)

死锁条件 破坏方式
互斥条件 很难破坏(锁的本质是互斥,适合资源不可共享场景)
占有并等待 一次性申请所有所需资源(如银行家算法),避免 “持有部分资源等待”
不可抢占 允许资源被抢占(如通过 tryLock(timeout) 超时释放,或线程中断释放锁)
循环等待 统一锁的获取顺序(最实用,如按 ID 排序)

📝 面试回答模板

死锁的解决方案围绕 “破坏死锁四大必要条件” 展开,核心有五种方案:一是减少锁的使用,优先用 Atomic 类、并发容器等无锁结构,从源头降低风险;二是统一锁顺序,让所有线程按相同顺序(如 ID 升序)获取多把锁,打破循环等待;三是使用带超时的锁,通过 tryLock(timeout) 让线程超时后主动放弃,避免无限等待;四是死锁检测与恢复,用 jstack 等工具排查,或程序中通过 ThreadMXBean 检测,发现后中断线程;五是破坏占有并等待,一次性申请所有资源(如银行家算法)。实际开发中,“统一锁顺序” 和 “减少锁使用” 最常用,简单且有效。

💡 附:Java 并发编程面试高频问题速查表

问题类型 高频问题 核心要点
基础概念 线程与进程的区别 线程是进程的子集,共享进程资源;进程间内存隔离,切换开销大
什么是线程安全 多线程环境下,代码执行结果与单线程一致,无数据竞争或不一致
volatile 关键字的作用 保证可见性、禁止指令重排序;不保证原子性,不能替代锁
锁机制 synchronized 和 Lock 的区别 见第四部分(隐式 vs 显式、中断支持、读写分离等)
什么是可重入锁 同一线程可多次获取同一把锁(如 synchronized、ReentrantLock),避免自死锁
公平锁与非公平锁 公平锁按申请顺序获取,性能低;非公平锁允许插队,性能高(默认)
线程通信 wait ()、notify ()、notifyAll () 的区别 见第一部分(唤醒数量、安全性、使用场景)
sleep () 和 wait () 的区别 sleep 不释放锁,是 Thread 方法;wait 释放锁,是 Object 方法,需在同步块中调用
并发工具 ConcurrentHashMap 实现原理 JDK8 前:分段锁(Segment);JDK8 后:CAS + synchronized(Node 锁),粒度更细
CountDownLatch 和 CyclicBarrier 的区别 前者计数递减(不可复用),后者计数递增(可复用);前者等待线程不阻塞计数线程,后者所有线程阻塞
问题排查 如何排查死锁 jstack 命令、JConsole/VisualVM 工具、ThreadMXBean 程序检测
如何避免死锁 统一锁顺序、减少锁范围、超时锁、无锁结构

📌 总结:Java 并发编程核心思维

  1. 先思考,再加锁:不盲目使用同步,先判断是否真的需要共享状态,优先用无锁方案。
  2. 锁粒度越小越好:仅锁定临界区代码,将耗时操作(I/O、日志)移出同步块。
  3. 避免嵌套锁:若需多把锁,确保所有线程按相同顺序获取,防止循环等待。
  4. 善用并发工具:优先使用 JUC 包(Atomic、Concurrent*、Lock),而非原始 synchronized。
  5. 理解内存模型:掌握 happens-before 原则,解决可见性、有序性、原子性问题。
  6. 设计优于补救:系统设计阶段考虑并发场景,而非事后修复死锁、活锁等问题。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐