Java多线程核心面试知识点
一、线程协作:join()方法的等待通知机制
1.1 核心原理
join()方法的本质是让当前线程等待目标线程执行完毕后再继续执行 ,其底层基于Java的等待/通知机制实现。当调用thread.join()时,当前线程会进入等待状态,直到目标线程终止(isAlive()返回false),JVM会自动唤醒等待在该线程对象上的所有线程。
1.2 多米诺骨牌式线程执行示例
你的代码完美展示了"链式等待"的效果:每个线程等待前一个线程终止后才执行,最终形成从主线程开始,依次0→1→2→...→9的执行顺序。
java
import java.util.concurrent.TimeUnit;
public class JoinDemo {
public static void main(String[] args) throws Exception {
Thread previous = Thread.currentThread(); // 初始前驱是主线程
for (int i = 0; i < 10; i++) {
// 每个线程持有前一个线程的引用
Thread thread = new Thread(new DominoTask(previous), String.valueOf(i));
thread.start();
previous = thread; // 更新前驱为当前线程
}
TimeUnit.SECONDS.sleep(5); // 主线程休眠5秒
System.out.println(Thread.currentThread().getName() + " terminate.");
}
static class DominoTask implements Runnable {
private final Thread predecessor;
public DominoTask(Thread predecessor) {
this.predecessor = predecessor;
}
@Override
public void run() {
try {
// 等待前驱线程执行完毕
predecessor.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
}
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
1.3 执行结果与分析
main terminate.
0 terminate.
1 terminate.
2 terminate.
...
9 terminate.
- 主线程休眠5秒后先打印终止信息
- 线程0等待主线程终止后执行
- 线程1等待线程0终止后执行,以此类推
1.4 面试要点
join(long millis):带超时时间的等待,避免无限阻塞- 中断处理:
join()会抛出InterruptedException,捕获后建议恢复线程中断状态 - 底层实现:本质是调用了线程对象的
wait()方法,线程终止时JVM会调用notifyAll()唤醒等待线程
二、ThreadLocal:线程私有变量的实现原理
2.1 核心概念
ThreadLocal提供了线程级别的变量隔离,每个线程都有自己独立的变量副本,互不干扰。它解决了多线程环境下变量共享的线程安全问题,适用于每个线程需要自己独立实例的场景(如数据库连接、用户会话信息)。
2.2 底层实现原理(面试重点)
ThreadLocal的核心是Thread类中的threadLocals变量,它是一个ThreadLocal.ThreadLocalMap类型的哈希表:
- 存储结构 :每个
Thread对象维护一个ThreadLocalMap,键是ThreadLocal实例本身,值是线程私有变量 - 弱引用设计 :
ThreadLocalMap中的Entry继承自WeakReference<ThreadLocal<?>>,即键是弱引用,值是强引用 - get/set流程 :
set(value):获取当前线程的ThreadLocalMap,以当前ThreadLocal为键存储值get():获取当前线程的ThreadLocalMap,以当前ThreadLocal为键查找值remove():从ThreadLocalMap中删除当前ThreadLocal对应的键值对
2.3 内存泄漏问题与解决方案
问题:如果ThreadLocal没有被外部强引用,垃圾回收时会回收键(弱引用),但值仍然被Entry强引用,导致值无法回收,造成内存泄漏。
解决方案:
- 每次使用完ThreadLocal后,显式调用
remove()方法删除键值对 - 尽量将ThreadLocal声明为
private static final,延长其生命周期,减少频繁创建销毁
2.4 代码示例
java
public class ThreadLocalDemo {
// 声明为private static final是最佳实践
private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
int value = threadLocal.get();
value += 1;
threadLocal.set(value);
System.out.println(Thread.currentThread().getName() + ": " + value);
} finally {
threadLocal.remove(); // 必须显式移除,防止内存泄漏
}
}, "Thread-" + i).start();
}
}
}
2.5 纠正你的笔记错误
ThreadLocal和Session的实现原理不一样:
- ThreadLocal:基于线程隔离,每个线程有自己的变量副本,生命周期与线程一致
- Session:基于会话隔离,每个用户会话有自己的信息,通常通过Cookie传递Session ID,服务器端维护一个全局的Session Map,生命周期与会话一致
- 两者只是思想相似("每个X有自己的Y"),但底层实现和应用场景完全不同
三、线程池:Java并发编程的核心
3.1 为什么使用线程池
- 降低资源消耗:避免频繁创建和销毁线程的开销
- 提高响应速度:任务到达时无需等待线程创建即可执行
- 提高线程的可管理性:统一分配、调优和监控线程,避免无限制创建线程导致系统崩溃
3.2 线程池核心参数(ThreadPoolExecutor)
java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(常驻线程)
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程创建工厂
RejectedExecutionHandler handler // 拒绝策略
)
3.3 线程池工作流程
- 任务提交时,若核心线程数未满,创建核心线程执行任务
- 若核心线程数已满,将任务加入任务队列
- 若任务队列已满,创建非核心线程执行任务
- 若总线程数达到maximumPoolSize,执行拒绝策略
3.4 常见线程池类型
| 线程池类型 | 核心特点 | 适用场景 |
|---|---|---|
newFixedThreadPool(n) |
固定线程数,无界队列 | 执行长期任务,控制并发数 |
newCachedThreadPool() |
无界线程池,自动回收空闲线程 | 执行大量短期异步任务 |
newSingleThreadExecutor() |
单线程,保证任务顺序执行 | 需要串行执行任务的场景 |
newScheduledThreadPool(n) |
支持定时和周期性任务执行 | 定时任务调度 |
3.5 代码示例:自定义线程池
java
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 自定义线程池(推荐方式,避免使用Executors创建)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10), // 有界队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() // 默认拒绝策略:抛出异常
);
try {
// 提交10个任务
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("任务" + taskId + "由线程" + Thread.currentThread().getName() + "执行");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
} finally {
executor.shutdown(); // 关闭线程池
}
}
}
3.6 面试要点
- 不推荐使用
Executors创建线程池,因为它们可能导致OOM(如newCachedThreadPool无界线程数,newFixedThreadPool无界队列) - 拒绝策略:AbortPolicy(抛异常)、CallerRunsPolicy(调用者线程执行)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃最旧任务)
- 线程池状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED
四、锁的重入:可重入锁的原理与应用
4.1 什么是锁的重入
可重入锁 :当一个线程已经持有某个锁时,它可以再次获取该锁而不会被阻塞。Java中的synchronized关键字和ReentrantLock都是可重入锁。
4.2 为什么需要可重入锁
避免死锁。例如,一个同步方法调用另一个同步方法,如果锁不可重入,线程在调用第二个方法时会等待自己释放锁,导致死锁。
4.3 代码示例
java
public class ReentrantLockDemo {
// synchronized可重入示例
public synchronized void methodA() {
System.out.println("执行methodA");
methodB(); // 同一个线程再次获取锁
}
public synchronized void methodB() {
System.out.println("执行methodB");
}
// ReentrantLock可重入示例
private static final java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock();
public void methodC() {
lock.lock();
try {
System.out.println("执行methodC,持有锁次数:" + lock.getHoldCount());
methodD();
} finally {
lock.unlock();
System.out.println("释放methodC的锁,持有锁次数:" + lock.getHoldCount());
}
}
public void methodD() {
lock.lock();
try {
System.out.println("执行methodD,持有锁次数:" + lock.getHoldCount());
} finally {
lock.unlock();
System.out.println("释放methodD的锁,持有锁次数:" + lock.getHoldCount());
}
}
public static void main(String[] args) {
ReentrantLockDemo demo = new ReentrantLockDemo();
demo.methodA();
System.out.println("---");
demo.methodC();
}
}
4.4 执行结果
执行methodA
执行methodB
---
执行methodC,持有锁次数:1
执行methodD,持有锁次数:2
释放methodD的锁,持有锁次数:1
释放methodC的锁,持有锁次数:0
4.5 面试要点
- 可重入锁通过计数器实现:每次获取锁计数器+1,每次释放锁计数器-1,计数器为0时锁真正释放
ReentrantLock支持公平锁和非公平锁(默认非公平),而synchronized只能是非公平锁- 公平锁:按照线程等待顺序获取锁;非公平锁:线程直接尝试获取锁,失败才进入等待队列
五、Condition:精确唤醒线程的利器
5.1 核心概念
Condition接口提供了类似Object.wait()/notify()的等待/通知机制,但它可以与Lock配合使用,实现多个等待队列,从而能够精确唤醒指定的线程。
5.2 与Object.wait/notify的对比
| 特性 | Object.wait/notify | Condition |
|---|---|---|
| 关联对象 | 任意Java对象 | 与Lock绑定 |
| 等待队列 | 1个 | 多个 |
| 唤醒方式 | 随机唤醒一个或全部 | 精确唤醒指定队列的线程 |
| 中断支持 | 支持 | 支持,还支持不中断等待 |
| 超时支持 | 支持 | 支持更多超时方式 |
5.3 代码示例:生产者消费者模型
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
private static final Lock lock = new ReentrantLock();
// 创建两个等待队列:生产者队列和消费者队列
private static final Condition notFull = lock.newCondition();
private static final Condition notEmpty = lock.newCondition();
private static final int CAPACITY = 5;
private static int count = 0;
public static void main(String[] args) {
// 启动3个生产者和3个消费者
for (int i = 0; i < 3; i++) {
new Thread(new Producer(), "Producer-" + i).start();
new Thread(new Consumer(), "Consumer-" + i).start();
}
}
static class Producer implements Runnable {
@Override
public void run() {
while (true) {
lock.lock();
try {
// 队列满了,生产者等待
while (count == CAPACITY) {
notFull.await();
}
count++;
System.out.println(Thread.currentThread().getName() + "生产,当前数量:" + count);
// 唤醒一个消费者
notEmpty.signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} finally {
lock.unlock();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
static class Consumer implements Runnable {
@Override
public void run() {
while (true) {
lock.lock();
try {
// 队列空了,消费者等待
while (count == 0) {
notEmpty.await();
}
count--;
System.out.println(Thread.currentThread().getName() + "消费,当前数量:" + count);
// 唤醒一个生产者
notFull.signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} finally {
lock.unlock();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}
5.4 面试要点
await():当前线程进入等待状态,释放锁signal():唤醒一个等待在该Condition上的线程signalAll():唤醒所有等待在该Condition上的线程- 必须在
lock.lock()和lock.unlock()之间调用Condition的方法
六、CountDownLatch:一次性同步工具
6.1 核心概念
CountDownLatch是一个一次性使用的同步工具,它允许一个或多个线程等待其他线程完成一组操作。它通过一个计数器实现,初始值为需要等待的线程数,每个线程完成操作后计数器减1,当计数器变为0时,等待的线程被唤醒。
6.2 常用方法
CountDownLatch(int count):构造方法,指定初始计数值countDown():计数器减1,当计数器变为0时唤醒所有等待线程await():当前线程等待,直到计数器变为0await(long timeout, TimeUnit unit):带超时时间的等待
6.3 代码示例:等待多个线程完成任务
java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int threadCount = 5;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int taskId = i;
new Thread(() -> {
try {
System.out.println("任务" + taskId + "开始执行");
TimeUnit.SECONDS.sleep(1 + taskId); // 模拟不同执行时间
System.out.println("任务" + taskId + "执行完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 计数器减1
}
}).start();
}
System.out.println("主线程等待所有任务完成...");
latch.await(); // 等待计数器变为0
System.out.println("所有任务执行完成,主线程继续执行");
}
}
6.4 典型应用场景
- 并行计算:将大任务拆分为多个小任务,等待所有小任务完成后汇总结果
- 服务启动:等待多个依赖服务启动完成后再提供服务
- 测试:等待多个线程同时执行某个操作,测试并发性能
6.5 面试要点
CountDownLatch是一次性的,计数器变为0后无法重置- 与
CyclicBarrier的区别:CountDownLatch:一个线程等待多个线程完成,一次性使用CyclicBarrier:多个线程互相等待,可循环使用
总结
以上是Java多线程面试中最核心的8个知识点,涵盖了线程协作、线程私有变量、线程池、锁机制和并发工具等方面。在面试中,不仅要记住这些概念,更要理解其底层原理和使用场景,并能结合代码示例进行说明。