多线程并发篇面试题
目录
- 多线程基础
- 线程安全
- 锁机制
- 并发工具类
- 线程池
- JUC包
- 内存模型
- 性能优化
多线程基础
1. 什么是线程?线程和进程的区别?
答案:
线程:CPU调度的基本单位,是进程内的执行单元。
进程:操作系统资源分配的基本单位。
主要区别:
-
进程:
- 独立的内存空间
- 进程间通信复杂(需要IPC机制)
- 创建和销毁开销大
-
线程:
- 共享进程内存空间
- 线程间通信简单(共享内存)
- 创建和销毁开销小
示例:
Thread thread = new Thread(() -> System.out.println("Hello Thread"));
thread.start();
2. 创建线程的方式有哪些?
答案:
- 继承Thread类
class MyThread extends Thread {@Overridepublic void run() {System.out.println("Thread running");}
}
- 实现Runnable接口
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("Runnable running");}
}
- 实现Callable接口
class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {return "Callable result";}
}
- 使用线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> System.out.println("Pool thread"));
3. 线程的生命周期?
答案:
线程状态:
- NEW(新建)- 线程创建但未启动
- RUNNABLE(可运行)- 线程正在JVM中执行
- BLOCKED(阻塞)- 线程被阻塞等待监视器锁
- WAITING(等待)- 线程无限期等待另一个线程执行特定操作
- TIMED_WAITING(超时等待)- 线程等待另一个线程执行操作,但有时限
- TERMINATED(终止)- 线程执行完毕
状态转换:
NEW → RUNNABLE ⇄ BLOCKED/WAITING/TIMED_WAITING → TERMINATED
4. 线程的优先级?
答案:
优先级范围:1-10,默认优先级为5。
设置优先级:
thread.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级(10)
thread.setPriority(Thread.MIN_PRIORITY); // 设置最低优先级(1)
thread.setPriority(Thread.NORM_PRIORITY); // 设置默认优先级(5)
注意事项:
- 优先级只是建议,不保证执行顺序
- 最终执行顺序依赖操作系统调度
- 不要过度依赖线程优先级控制程序逻辑
线程安全
5. 什么是线程安全?
答案:
线程安全 指多线程环境下,程序能够正确处理共享数据,不会出现数据不一致或竞态条件。
线程安全级别:
- 不可变(Immutable)- String、Integer等不可变对象
- 绝对线程安全(Absolutely Thread-Safe)- Vector、Hashtable等
- 相对线程安全(Relatively Thread-Safe)- 大部分JDK类,如ArrayList、HashMap
- 线程兼容(Thread-Compatible)- 需要外部同步
- 线程对立(Thread-Hostile)- 无论是否同步都无法在多线程环境使用
6. 什么是竞态条件?
答案: 竞态条件 指多个线程访问共享资源时,执行结果依赖于线程执行的时序。
示例:
public class Counter {private int count = 0;public void increment() {count++; // 非原子操作,存在竞态条件}
}
解决方案: 使用synchronized、volatile、原子类等同步机制。
7. 什么是死锁?如何避免死锁?
答案:
死锁 指两个或多个线程互相等待对方释放资源,导致程序无法继续执行。
死锁的四个必要条件:
- 互斥条件:资源不能被多个线程同时使用
- 请求和保持条件:线程持有资源的同时请求其他资源
- 不剥夺条件:资源不能被强制释放
- 循环等待条件:线程形成循环等待链
避免死锁的方法:
- 避免嵌套锁:尽量不要在持有一个锁的情况下获取另一个锁
- 按顺序获取锁:所有线程按照相同的顺序获取锁
- 使用超时锁:使用
tryLock(timeout)
避免无限等待 - 死锁检测和恢复:定期检测死锁并采取恢复措施
示例:
// 按固定顺序获取锁,避免死锁
synchronized(lock1) {synchronized(lock2) {// 临界区代码}
}
锁机制
8. synchronized关键字的作用?
答案:
synchronized 是Java内置的同步机制,用于实现线程安全,确保同一时刻只有一个线程执行同步代码。
三种使用方式:
- 同步方法
public synchronized void method() {// 同步方法
}
- 同步代码块
synchronized(object) {// 同步代码块
}
- 静态同步方法
public static synchronized void method() {// 静态同步方法
}
锁对象说明:
- 实例方法:锁对象是
this
(当前实例对象) - 静态方法:锁对象是
Class
对象(类对象) - 同步代码块:锁对象是括号中指定的对象
9. synchronized的底层原理?
答案:
底层实现:基于JVM的monitor(监视器)机制,通过monitorenter
和monitorexit
指令实现。
锁升级过程:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
各级锁的特点:
- 偏向锁:只有一个线程访问时,记录线程ID,避免CAS操作,性能最好
- 轻量级锁:多线程竞争不激烈时,使用CAS操作,避免操作系统互斥量
- 重量级锁:多线程竞争激烈时,使用操作系统互斥量(Mutex),会导致线程阻塞
10. volatile关键字的作用?
答案:
volatile 保证变量的可见性和有序性,但不保证原子性。
三大特性:
- 可见性:一个线程修改volatile变量,其他线程立即可见
- 有序性:禁止指令重排序(通过内存屏障实现)
- 不保证原子性:复合操作(如i++)仍需要同步
使用场景:
- 状态标志位
- 双重检查锁定(DCL单例模式)
- 独立观察变量
示例:
private volatile boolean flag = false;// 线程1
public void setFlag() {flag = true; // 写操作对其他线程立即可见
}// 线程2
public void checkFlag() {if (flag) { // 能立即看到线程1的修改// 执行逻辑}
}
11. ReentrantLock和synchronized的区别?
答案:
ReentrantLock 是JUC包提供的可重入锁,相比synchronized提供了更多高级功能。
详细区别对比:
特性 | synchronized | ReentrantLock |
---|---|---|
锁获取 | 自动获取和释放 | 手动获取和释放 |
中断响应 | 不支持 | 支持 |
超时获取 | 不支持 | 支持 |
公平锁 | 非公平 | 可选择公平或非公平 |
条件变量 | 单一条件 | 多个条件 |
性能 | JVM优化,性能好 | 需要手动优化 |
示例:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {// 临界区
} finally {lock.unlock();
}
12. 什么是读写锁?
答案:
读写锁(ReentrantReadWriteLock)允许多个线程同时读取,但写操作是独占的。
核心特性:
- 读锁(共享锁):多个线程可以同时持有读锁
- 写锁(排他锁):只有一个线程可以持有写锁,且写锁排斥所有读锁
使用示例:
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();// 读操作
rwLock.readLock().lock();
try {// 读取数据
} finally {rwLock.readLock().unlock();
}// 写操作
rwLock.writeLock().lock();
try {// 修改数据
} finally {rwLock.writeLock().unlock();
}
使用场景:读多写少的场景,如缓存、配置管理等。
优势:提高并发性能,多个读操作可以并发执行,不会相互阻塞。
并发工具类
13. CountDownLatch详解(源码+应用场景)
答案:
CountDownLatch 是一个同步辅助类,允许一个或多个线程等待其他线程完成操作。它基于AQS(AbstractQueuedSynchronizer)的共享锁模式实现。
核心特性:
- 一次性使用:计数到达零后不能重置
- 共享锁模式:多个线程可以同时等待
- 倒计时功能:从初始计数开始递减到0
核心方法:
await()
:等待计数器到达0countDown()
:计数器减1getCount()
:获取当前计数
基础示例:
// 创建CountDownLatch,初始计数为3
CountDownLatch latch = new CountDownLatch(3);// 工作线程
for (int i = 0; i < 3; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + " 正在执行任务");Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + " 任务完成");} finally {latch.countDown(); // 计数器减1}}).start();
}// 主线程等待
System.out.println("主线程等待所有任务完成...");
latch.await(); // 阻塞,直到计数器为0
System.out.println("所有任务完成,主线程继续执行");
典型应用场景:
场景1:系统初始化等待
public class SystemInitializer {private final CountDownLatch initLatch = new CountDownLatch(3);public void initialize() throws InterruptedException {// 初始化数据库new Thread(() -> {try {System.out.println("初始化数据库...");Thread.sleep(2000);System.out.println("数据库初始化完成");} finally {initLatch.countDown();}}).start();// 初始化缓存new Thread(() -> {try {System.out.println("初始化缓存...");Thread.sleep(1500);System.out.println("缓存初始化完成");} finally {initLatch.countDown();}}).start();// 初始化配置new Thread(() -> {try {System.out.println("加载配置...");Thread.sleep(1000);System.out.println("配置加载完成");} finally {initLatch.countDown();}}).start();// 等待所有初始化完成initLatch.await();System.out.println("系统初始化完成,可以对外提供服务");}
}
场景2:并发测试(模拟高并发)
public class ConcurrentTest {public void testConcurrent() throws InterruptedException {int threadCount = 100;CountDownLatch startLatch = new CountDownLatch(1); // 开始信号CountDownLatch endLatch = new CountDownLatch(threadCount); // 完成信号// 创建100个线程for (int i = 0; i < threadCount; i++) {new Thread(() -> {try {startLatch.await(); // 等待开始信号// 执行并发测试performTest();} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {endLatch.countDown();}}).start();}// 所有线程就位后,同时启动System.out.println("所有线程准备就绪,开始并发测试");startLatch.countDown(); // 释放开始信号// 等待所有线程完成endLatch.await();System.out.println("并发测试完成");}private void performTest() {// 测试逻辑}
}
源码实现原理(简化版):
public class CountDownLatch {// 内部同步器,基于AQSprivate static final class Sync extends AbstractQueuedSynchronizer {Sync(int count) {setState(count); // 设置初始计数}// 尝试获取共享锁:计数为0返回1,否则返回-1protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;}// 尝试释放共享锁:递减计数,为0时返回trueprotected boolean tryReleaseShared(int releases) {for (;;) {int c = getState();if (c == 0) return false; // 已经为0,不能继续减int nextc = c - 1;if (compareAndSetState(c, nextc)) // CAS更新计数return nextc == 0; // 返回是否到达0}}}private final Sync sync;public CountDownLatch(int count) {this.sync = new Sync(count);}public void await() throws InterruptedException {sync.acquireSharedInterruptibly(1); // 获取共享锁}public void countDown() {sync.releaseShared(1); // 释放共享锁,计数减1}
}
vs wait/notify 的优势:
- 代码简洁:无需手动管理同步代码块和条件判断
- 线程安全:内置线程安全保证,无需额外同步
- 无虚假唤醒:不会出现wait/notify的虚假唤醒问题
- 性能更好:基于AQS的无锁算法,性能优于synchronized
- 易于维护:代码清晰,不易出错
14. CyclicBarrier的作用?
答案:
CyclicBarrier(循环屏障)是一个同步辅助类,允许多个线程相互等待,直到所有线程都到达某个公共屏障点。
核心特性:
- 可重复使用:所有线程到达屏障后,屏障会重置,可以重复使用
- 相互等待:所有线程必须到达屏障点才能继续执行
- 回调函数:可以设置所有线程到达后执行的回调动作
与CountDownLatch区别:
特性 | CountDownLatch | CyclicBarrier |
---|---|---|
使用次数 | 一次性 | 可重复使用 |
计数方向 | 递减到0 | 递增到指定值 |
等待模式 | 一个或多个线程等待 | 多个线程相互等待 |
重置能力 | 不支持 | 支持reset() |
示例:
// 创建CyclicBarrier,3个线程到达后执行回调
CyclicBarrier barrier = new CyclicBarrier(3, () -> {System.out.println("所有线程到达屏障,执行汇总任务");
});// 工作线程
for (int i = 0; i < 3; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + " 开始执行");Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + " 到达屏障");barrier.await(); // 等待其他线程到达System.out.println(Thread.currentThread().getName() + " 继续执行");} catch (Exception e) {e.printStackTrace();}}).start();
}
15. Semaphore的作用?
答案:
Semaphore(信号量)是一个计数信号量,用于控制同时访问特定资源的线程数量。
核心特性:
- 许可控制:通过许可证(permits)控制并发访问数量
- 公平性:支持公平和非公平模式
- 可中断:支持中断和超时获取
使用场景:
- 限制并发访问数量(如数据库连接池)
- 实现资源池(如线程池、对象池)
- 控制流量(如限流器)
示例:
// 创建Semaphore,允许3个线程同时访问
Semaphore semaphore = new Semaphore(3);// 访问资源
for (int i = 0; i < 10; i++) {new Thread(() -> {try {semaphore.acquire(); // 获取许可System.out.println(Thread.currentThread().getName() + " 获取许可,开始访问资源");Thread.sleep(2000); // 模拟资源访问System.out.println(Thread.currentThread().getName() + " 释放许可");} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {semaphore.release(); // 释放许可}}).start();
}
16. Exchanger的作用?
答案:
Exchanger 是一个同步点,用于两个线程之间交换数据。
核心特性:
- 双向交换:两个线程互相交换数据
- 同步点:两个线程必须同时到达才能交换
- 类型安全:泛型支持,保证类型安全
使用场景:
- 两个线程需要交换数据
- 生产者消费者模式
- 数据校验(两个线程处理相同数据,交换结果进行校验)
示例:
Exchanger<String> exchanger = new Exchanger<>();// 线程1
new Thread(() -> {try {String data = "来自线程1的数据";System.out.println("线程1准备交换数据:" + data);String received = exchanger.exchange(data); // 交换数据,阻塞等待线程2System.out.println("线程1收到数据:" + received);} catch (InterruptedException e) {Thread.currentThread().interrupt();}
}).start();// 线程2
new Thread(() -> {try {String data = "来自线程2的数据";System.out.println("线程2准备交换数据:" + data);String received = exchanger.exchange(data); // 交换数据,阻塞等待线程1System.out.println("线程2收到数据:" + received);} catch (InterruptedException e) {Thread.currentThread().interrupt();}
}).start();
线程池
17. 什么是线程池?为什么使用线程池?
答案:
线程池 是预先创建一定数量的线程,用于执行任务的管理机制。
四大优势:
-
降低资源消耗
- 避免频繁创建和销毁线程
- 减少内存开销和GC压力
-
提高响应速度
- 任务到达时无需等待线程创建
- 线程已经就绪,可以立即执行
-
提高线程可管理性
- 统一管理线程数量、状态
- 避免线程数量失控
-
提供更多功能
- 支持定时执行、定期执行
- 支持任务队列、拒绝策略等
18. 线程池的核心参数?
答案:
ThreadPoolExecutor七大核心参数:
-
corePoolSize(核心线程数)
- 线程池中始终保持活跃的线程数量
- 即使线程空闲也不会被销毁
-
maximumPoolSize(最大线程数)
- 线程池中允许的最大线程数量
- 当队列满时,会创建新线程直到达到最大值
-
keepAliveTime(线程空闲时间)
- 非核心线程的空闲存活时间
- 超过此时间的空闲线程会被销毁
-
unit(时间单位)
- keepAliveTime的时间单位
- TimeUnit枚举值(SECONDS、MINUTES等)
-
workQueue(工作队列)
- 存储待执行任务的阻塞队列
- 常用:LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue
-
threadFactory(线程工厂)
- 用于创建新线程
- 可以自定义线程名称、优先级等
-
handler(拒绝策略)
- 当队列满且线程数达到最大值时的处理策略
- 四种默认策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy
示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, // 核心线程数10, // 最大线程数60L, // 空闲时间TimeUnit.SECONDS, // 时间单位new LinkedBlockingQueue<>(100), // 工作队列new ThreadFactory() {private final AtomicInteger threadNumber = new AtomicInteger(1);@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r, "MyPool-thread-" + threadNumber.getAndIncrement());t.setDaemon(false);t.setPriority(Thread.NORM_PRIORITY);return t;}}, new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
19. 线程池的拒绝策略?
答案:
当线程池无法接受新任务时(队列满且线程数达到最大值),会触发拒绝策略。
四种默认拒绝策略:
-
AbortPolicy(默认策略)
- 直接抛出
RejectedExecutionException
异常 - 让调用者感知到任务被拒绝
- 直接抛出
-
CallerRunsPolicy(调用者运行策略)
- 由调用线程(提交任务的线程)直接执行任务
- 降低任务提交速度,起到负反馈作用
-
DiscardPolicy(丢弃策略)
- 静默丢弃任务,不抛出异常
- 可能导致任务丢失,需谨慎使用
-
DiscardOldestPolicy(丢弃最老任务策略)
- 丢弃队列中最老的任务
- 然后重新提交当前任务
自定义拒绝策略:
new RejectedExecutionHandler() {@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {// 自定义处理逻辑// 例如:记录日志、存入数据库、放入MQ等System.err.println("任务被拒绝: " + r.toString());// 可以选择性地处理任务}
}
20. 常见的线程池类型?
答案:
Executors提供的四种线程池:
-
newFixedThreadPool(固定大小线程池)
- 核心线程数 = 最大线程数
- 使用无界队列LinkedBlockingQueue
- 适合:负载较重的服务器
-
newCachedThreadPool(缓存线程池)
- 核心线程数 = 0,最大线程数 = Integer.MAX_VALUE
- 使用SynchronousQueue(不存储任务)
- 适合:执行大量短期异步任务
-
newSingleThreadExecutor(单线程池)
- 只有一个线程的线程池
- 保证任务按提交顺序串行执行
- 适合:需要顺序执行的场景
-
newScheduledThreadPool(定时任务线程池)
- 支持定时及周期性任务执行
- 适合:定时任务场景
⚠️ 重要提示:
生产环境不推荐使用Executors创建线程池,原因:
newFixedThreadPool
和newSingleThreadExecutor
使用无界队列,可能导致OOMnewCachedThreadPool
最大线程数为Integer.MAX_VALUE,可能创建大量线程导致OOM
**推荐做法:**使用ThreadPoolExecutor自定义参数,明确指定队列大小和最大线程数。
JUC包
21. 什么是CAS?CAS的ABA问题?
答案:
CAS(Compare And Swap)是一种无锁算法,通过比较和交换实现原子操作。
CAS三要素:
- 内存位置(V):要更新的变量
- 预期原值(A):期望的当前值
- 新值(B):要设置的新值
CAS操作流程:
// 伪代码
boolean compareAndSwap(V, A, B) {if (V == A) { // 如果当前值等于期望值V = B; // 则更新为新值return true;}return false; // 否则更新失败
}// 实际使用
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.compareAndSet(0, 1); // 期望值为0,更新为1
ABA问题:
- 问题描述:变量从A变为B,再变回A,CAS认为没有变化,但实际已经被修改过
- 影响场景:链表、栈等数据结构操作
解决方案:
- 使用版本号:AtomicStampedReference(带版本戳的原子引用)
AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(0, 0);
int stamp = atomicRef.getStamp();
atomicRef.compareAndSet(0, 1, stamp, stamp + 1); // 同时比较值和版本号
- 使用时间戳:AtomicMarkableReference(带标记的原子引用)
22. 原子类的实现原理?
答案: 原子类 基于CAS操作实现,如AtomicInteger、AtomicLong等。
实现原理: 使用Unsafe类的CAS操作,底层调用CPU的CAS指令。
示例:
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet(); // 原子递增
atomicInt.compareAndSet(0, 1); // CAS操作
23. ConcurrentHashMap的实现原理?
答案: ConcurrentHashMap 是线程安全的HashMap实现。
JDK 1.7实现: 使用分段锁(Segment),每个Segment包含一个HashEntry数组。
JDK 1.8实现: 使用CAS + synchronized,数组 + 链表 + 红黑树结构。
优势: 高并发性能,读操作无锁,写操作使用CAS和synchronized。
24. BlockingQueue的实现原理?
答案: BlockingQueue 是支持阻塞操作的队列接口。
常见实现: 1. ArrayBlockingQueue (有界数组队列) 2. LinkedBlockingQueue (有界链表队列) 3. PriorityBlockingQueue (优先级队列) 4. DelayQueue (延迟队列) 5. SynchronousQueue (同步队列)
阻塞操作:
put() // 阻塞插入
take() // 阻塞获取
offer() // 非阻塞插入
poll() // 非阻塞获取
内存模型
25. 什么是JMM?
答案:
JMM(Java Memory Model,Java内存模型)定义了Java程序中各种变量的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
内存区域:
- 主内存(Main Memory):所有线程共享,存储所有变量
- 工作内存(Working Memory):每个线程私有,存储该线程使用的变量副本
八种内存交互操作:
- lock(锁定):作用于主内存的变量
- unlock(解锁):作用于主内存的变量
- read(读取):从主内存读取变量到工作内存
- load(载入):将read的变量值放入工作内存的变量副本
- use(使用):从工作内存读取变量值
- assign(赋值):将新值赋给工作内存的变量
- store(存储):将工作内存的变量值传送到主内存
- write(写入):将store的变量值写入主内存
26. 什么是happens-before?
答案:
happens-before 是JMM定义的内存可见性规则,用于确定操作之间的内存可见性。
核心含义:
如果操作A happens-before 操作B,那么A的执行结果对B可见,且A的执行顺序排在B之前。
八大happens-before规则:
-
程序顺序规则(Program Order Rule)
- 同一个线程内,前面的操作 happens-before 后面的操作
-
监视器锁规则(Monitor Lock Rule)
- unlock操作 happens-before 后续对同一个锁的lock操作
-
volatile变量规则(Volatile Variable Rule)
- volatile写操作 happens-before 后续对该变量的读操作
-
线程启动规则(Thread Start Rule)
- Thread.start() happens-before 该线程的每一个动作
-
线程终止规则(Thread Termination Rule)
- 线程的所有操作 happens-before 其他线程检测到该线程终止
-
线程中断规则(Thread Interruption Rule)
- 线程interrupt()调用 happens-before 被中断线程检测到中断事件
-
对象终结规则(Finalizer Rule)
- 对象的构造函数执行结束 happens-before finalize()方法
-
传递性(Transitivity)
- 如果A happens-before B,B happens-before C,则A happens-before C
27. 什么是内存屏障?
答案:
内存屏障(Memory Barrier)是CPU指令,用于防止指令重排序和保证内存可见性。
四种类型:
-
LoadLoad屏障(读-读屏障)
- 确保Load1的数据装载先于Load2及后续装载指令
- 格式:
Load1; LoadLoad; Load2
-
StoreStore屏障(写-写屏障)
- 确保Store1的数据刷新到主内存先于Store2及后续存储指令
- 格式:
Store1; StoreStore; Store2
-
LoadStore屏障(读-写屏障)
- 确保Load1的数据装载先于Store2及后续存储指令
- 格式:
Load1; LoadStore; Store2
-
StoreLoad屏障(写-读屏障)
- 确保Store1的数据刷新到主内存先于Load2及后续装载指令
- 格式:
Store1; StoreLoad; Load2
- 开销最大的屏障
作用:
- 防止指令重排序
- 保证内存可见性
- 确保并发程序的正确性
性能优化
28. 如何优化多线程性能?
答案:
六大优化策略:
-
减少锁的粒度
- 使用细粒度锁替代粗粒度锁
- 例如:ConcurrentHashMap的分段锁
-
减少锁的持有时间
- 尽快释放锁,缩小同步代码块范围
- 将耗时操作移到锁外执行
-
使用无锁数据结构
- 使用CAS操作替代锁
- 使用原子类(AtomicInteger等)
- 使用并发容器(ConcurrentHashMap等)
-
合理使用线程池
- 避免频繁创建和销毁线程
- 根据任务特性选择合适的线程池配置
-
使用读写锁
- 读多写少场景使用ReentrantReadWriteLock
- 允许多个读操作并发执行
-
避免锁竞争
- 减少同步代码块
- 使用ThreadLocal避免共享
- 使用不可变对象
29. 什么是伪共享?如何避免?
答案:
伪共享(False Sharing)指多个线程访问同一缓存行(Cache Line)的不同变量,导致缓存行在CPU核心之间频繁同步,严重影响性能。
产生原因:
- CPU缓存行通常是64字节
- 多个变量可能位于同一缓存行
- 一个变量被修改,整个缓存行失效,影响其他变量
避免方法:
- 使用@Contended注解(JDK 8+)
public class PaddedData {@Contended // 填充缓存行,避免伪共享public volatile long value1;@Contendedpublic volatile long value2;
}
- 手动填充字节
public class PaddedData {public volatile long value;private long p1, p2, p3, p4, p5, p6, p7; // 填充56字节
}
-
重新排列字段
- 将可能被不同线程访问的字段分开
- 避免它们位于同一缓存行
-
使用局部变量
- 减少对共享变量的访问
- 使用局部变量累加,最后再写回共享变量
30. 如何调试多线程问题?
答案:
五种调试方法:
-
使用日志
- 记录线程执行状态、时间戳
- 使用线程名称标识不同线程
- 记录关键操作和状态变化
-
使用调试器
- 设置断点,观察变量状态
- 查看线程堆栈信息
- 单步执行,观察执行流程
-
使用线程转储(Thread Dump)
- 使用
jstack
命令获取线程堆栈 - 分析死锁、线程状态等问题
- 命令:
jstack <pid> > thread_dump.txt
- 使用
-
使用性能分析工具
- JProfiler:专业的性能分析工具
- VisualVM:JDK自带的可视化工具
- JConsole:监控线程、内存、CPU等
-
使用并发测试工具
- JCStress:并发压力测试工具
- ThreadSanitizer:线程安全检测工具
- FindBugs/SpotBugs:静态代码分析
常见问题及排查:
- 死锁:使用jstack查看线程堆栈
- 活锁:观察线程状态变化
- 饥饿:检查线程优先级和调度
- 竞态条件:使用断点调试,观察共享变量
- 内存泄漏:使用内存分析工具(MAT、JProfiler)
31. 多线程环境下如何避免内存泄露?
答案: 内存泄露 指程序在运行过程中,不再使用的对象无法被垃圾回收器回收,导致内存占用持续增长。
多线程环境下的内存泄露原因:
- 线程未正确关闭
// 错误示例:线程未正确关闭
Thread thread = new Thread(() -> {while (true) {// 长时间运行的任务}
});
thread.start();
// 忘记调用 thread.interrupt() 或设置停止标志
- 线程池未正确关闭
// 错误示例:线程池未关闭
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {// 任务执行
});
// 忘记调用 executor.shutdown()
- 监听器未移除
// 错误示例:监听器未移除
public class EventManager {private List<EventListener> listeners = new ArrayList<>();public void addListener(EventListener listener) {listeners.add(listener);}// 缺少移除监听器的方法// public void removeListener(EventListener listener) {// listeners.remove(listener);// }
}
- 静态集合持有对象引用
// 错误示例:静态集合持有对象引用
public class CacheManager {private static Map<String, Object> cache = new HashMap<>();public void put(String key, Object value) {cache.put(key, value); // 对象永远不会被回收}// 缺少清理方法
}
避免内存泄露的方法:
- 正确管理线程生命周期
// 正确示例:使用标志位控制线程停止
public class WorkerThread extends Thread {private volatile boolean running = true;@Overridepublic void run() {while (running) {// 执行任务}}public void stopWorker() {running = false;}
}// 使用
WorkerThread worker = new WorkerThread();
worker.start();
// 需要停止时
worker.stopWorker();
- 正确关闭线程池
// 正确示例:正确关闭线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
try {executor.submit(() -> {// 任务执行});
} finally {executor.shutdown(); // 关闭线程池try {if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {executor.shutdownNow(); // 强制关闭}} catch (InterruptedException e) {executor.shutdownNow();}
}
- 使用弱引用
// 正确示例:使用弱引用避免内存泄露
public class WeakReferenceCache {private Map<String, WeakReference<Object>> cache = new HashMap<>();public void put(String key, Object value) {cache.put(key, new WeakReference<>(value));}public Object get(String key) {WeakReference<Object> ref = cache.get(key);return ref != null ? ref.get() : null;}
}
- 及时清理资源
// 正确示例:及时清理资源
public class ResourceManager {private List<Resource> resources = new ArrayList<>();public void addResource(Resource resource) {resources.add(resource);}public void removeResource(Resource resource) {resources.remove(resource);resource.close(); // 及时关闭资源}public void cleanup() {for (Resource resource : resources) {resource.close();}resources.clear();}
}
- 使用ThreadLocal的正确方式
// 正确示例:正确使用ThreadLocal
public class ThreadLocalManager {private static final ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}};public static String formatDate(Date date) {return dateFormat.get().format(date);}// 在适当的时候清理ThreadLocalpublic static void cleanup() {dateFormat.remove();}
}
- 避免循环引用
// 正确示例:避免循环引用
public class Node {private String data;private Node next;public void setNext(Node next) {this.next = next;}// 提供清理方法public void clear() {this.next = null;this.data = null;}
}
内存泄露检测工具:
- JProfiler - 专业的内存分析工具
- VisualVM - JDK自带的性能分析工具
- MAT (Memory Analyzer Tool) - Eclipse内存分析工具
- JConsole - JDK自带的监控工具
最佳实践:
- 定期检查 - 使用工具定期检查内存使用情况
- 代码审查 - 在代码审查中重点关注资源管理
- 单元测试 - 编写测试验证资源正确释放
- 监控告警 - 在生产环境设置内存使用告警
总结
多线程和并发编程是Java开发中的重要技能,掌握线程安全、锁机制、并发工具类、线程池等核心概念对于编写高性能、高并发的应用程序至关重要。
重点掌握: 线程安全机制、锁的使用和选择、并发工具类的应用、线程池的配置和优化、JMM内存模型、性能优化技巧
通过深入理解这些概念和原理,能够在面试中展现出扎实的多线程编程功底。