Java并发编程面试题大全
一、并发编程基础
1.1 什么是并发编程?
答案:
并发编程是指在一个程序中同时执行多个独立的任务。它允许程序的不同部分看似同时运行,从而提高程序的执行效率和资源利用率。在Java中,并发编程主要通过线程来实现。
并发编程的主要特点包括:
- 多任务同时执行
- 资源共享和同步
- 线程间的通信和协调
1.2 串行与并行的区别是什么?
答案:
串行和并行是两种不同的任务执行方式:
- 串行(Sequential):
- 任务按顺序一个接一个地执行
- 前一个任务完成后才开始下一个任务
- 单线程环境下的执行方式
- 简单但效率较低
- 并行(Parallel):
- 多个任务同时执行
- 需要多核CPU或多处理器支持
- 真正的多任务同时执行
- 效率高但编程复杂度增加
关键区别在于:并行是真正的多任务同时执行(需要多核支持),而并发是通过时间片轮转实现的"看似同时"执行。
1.3 并发编程的目的是什么?
答案:
并发编程的主要目的包括:
- 提高性能:充分利用多核CPU资源,加快程序执行速度
- 提高响应性:避免长时间操作阻塞主线程,保持UI响应
- 简化建模:某些问题(如服务器处理多个客户端请求)用并发模型更自然
- 资源利用:在等待I/O操作时,CPU可以执行其他任务
- 公平性:多个用户或任务可以公平地共享资源
1.4 什么时候适合使用并发编程?
答案:
适合使用并发编程的场景包括:
- CPU密集型任务:需要大量计算且可分解为独立子任务
- I/O密集型任务:程序经常等待I/O操作(如网络、磁盘)
- 需要高响应性:如GUI程序需要保持界面响应
- 多用户/多客户端:如Web服务器处理并发请求
- 异步处理:后台执行不影响主流程的任务
不适合的场景:
- 任务间有强依赖关系
- 共享资源频繁访问且难以同步
- 任务非常简单且执行时间很短
二、线程基础
2.1 进程与线程的区别是什么?
答案:
进程和线程的主要区别:
比较点 | 进程 | 线程 |
---|---|---|
定义 | 操作系统资源分配的基本单位 | CPU调度的基本单位 |
内存 | 有独立的内存空间 | 共享进程的内存空间 |
开销 | 创建、切换开销大 | 创建、切换开销小 |
通信 | 进程间通信(IPC)复杂 | 线程间通信简单(共享内存) |
稳定性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
资源 | 系统分配资源给进程 | 线程共享进程资源 |
简单来说:进程是"程序的一次执行",线程是"进程中的一个执行路径"。
2.2 线程的状态及其相互转换
答案:
Java线程有以下6种状态:
- NEW(初始状态):线程被创建但尚未启动
- RUNNABLE(可运行状态):线程正在JVM中执行或等待操作系统资源
- BLOCKED(阻塞状态):线程等待获取监视器锁(进入synchronized块)
- WAITING(等待状态):无限期等待其他线程执行特定操作(无超时)
- TIMED_WAITING(超时等待):有限期等待(有超时参数)
- TERMINATED(终止状态):线程执行完毕
状态转换图:
NEW → start() → RUNNABLE
RUNNABLE → 获取锁 → BLOCKED
RUNNABLE → wait() → WAITING
RUNNABLE → sleep(ms) → TIMED_WAITING
WAITING/TIMED_WAITING → notify()/notifyAll() → RUNNABLE
RUNNABLE → run()结束 → TERMINATED
2.3 创建线程的两种方式
答案:
- 继承Thread类:
class MyThread extends Thread {
public void run() {
// 线程执行代码
}
}
// 使用
MyThread t = new MyThread();
t.start();
- 实现Runnable接口:
class MyRunnable implements Runnable {
public void run() {
// 线程执行代码
}
}
// 使用
Thread t = new Thread(new MyRunnable());
t.start();
推荐使用实现Runnable接口的方式,因为:
- Java不支持多重继承,继承Thread类后不能再继承其他类
- 实现Runnable接口更符合面向对象设计
- 线程池只能接受Runnable或Callable任务
2.4 线程挂起和恢复
答案:
- 挂起线程:
- 早期使用suspend()方法,但已废弃,因为它容易导致死锁
- 现代做法:使用wait()或条件变量让线程等待
- 恢复线程:
- 早期使用resume()方法,也已废弃
- 现代做法:使用notify()/notifyAll()或条件变量唤醒线程
注意:suspend()和resume()方法已被废弃,不应再使用,因为:
- suspend()挂起线程但不释放锁,容易导致死锁
- resume()如果意外先于suspend()调用,可能导致线程永远挂起
2.5 线程的中断操作
答案:
正确中断线程的三种方式:
- 使用interrupt()方法:
Thread t = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
// 工作代码
}
});
t.start();
// 中断线程
t.interrupt();
- 使用boolean标志位(volatile修饰):
class MyRunnable implements Runnable {
private volatile boolean stopped = false;
public void run() {
while(!stopped) {
// 工作代码
}
}
public void stop() {
stopped = true;
}
}
- 通过Future取消:
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
// 长时间任务
});
future.cancel(true); // 中断线程
注意:stop()方法已废弃,因为它会强制终止线程,可能导致资源未释放或数据不一致。
2.6 线程的优先级
答案:
- 优先级介绍:
- Java线程优先级范围:1(MIN_PRIORITY)到10(MAX_PRIORITY)
- 默认优先级:5(NORM_PRIORITY)
- 高优先级线程更可能被调度执行,但不保证
- 设置优先级:
Thread t = new Thread();
t.setPriority(Thread.MAX_PRIORITY); // 设置为最高优先级
注意:
- 优先级只是给调度器的建议,不保证执行顺序
- 不同操作系统对优先级的处理可能不同
- 过度依赖优先级可能导致线程饥饿问题
2.7 守护线程
答案:
- 线程分类:
- 用户线程:主线程和用户创建的线程默认都是用户线程
- 守护线程:为其他线程提供服务的后台线程
- 守护线程特点:
- JVM在所有用户线程结束后退出,不管守护线程是否还在运行
- 守护线程通常用于执行后台任务,如垃圾回收、心跳检测等
- 设置守护线程:
Thread daemonThread = new Thread();
daemonThread.setDaemon(true); // 必须在start()前调用
daemonThread.start();
注意:
- 守护线程中finally块不保证执行
- 守护线程创建的子线程也是守护线程
- 不要将I/O操作等关键任务放在守护线程中
三、线程安全性
3.1 什么是线程安全?
答案:
线程安全是指当多个线程访问某个类或对象时,这个类或对象始终能表现出正确的行为,无需额外的同步或协调。
线程安全的三个核心概念:
- 原子性:操作不可中断,要么全部执行,要么都不执行
- 可见性:一个线程对共享变量的修改能立即被其他线程看到
- 有序性:程序执行的顺序按照代码的先后顺序执行
实现线程安全的常见方法:
- 使用synchronized关键字
- 使用volatile关键字
- 使用原子类(AtomicInteger等)
- 使用锁(Lock接口实现类)
- 使用线程安全的容器
3.2 从字节码角度剖析线程不安全操作
答案:
以i++操作为例,它不是原子操作,从字节码看分为三步:
- getfield:获取当前值
- iconst_1:加载常量1
- iadd:执行加法
- putfield:写回新值
在多线程环境下,可能出现以下情况:
线程A:getfield (i=0)
线程B:getfield (i=0)
线程A:iadd (0+1=1)
线程B:iadd (0+1=1)
线程A:putfield (i=1)
线程B:putfield (i=1) // 结果应该是2,实际是1
这就是典型的竞态条件问题,需要通过同步机制解决。
3.3 原子性操作
答案:
-
什么是原子性:
原子性是指一个操作是不可中断的,要么全部执行成功,要么全部不执行,不会出现部分执行的情况。 -
如何实现原子性:
- 使用synchronized关键字
public synchronized void increment() { count++; }
- 使用原子类
private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); }
- 使用Lock
private Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } }
3.4 深入理解synchronized关键字
答案:
-
内置锁(监视器锁):
- 每个Java对象都有一个内置锁
- synchronized使用对象的内置锁实现同步
- 进入同步代码块前自动获取锁,退出时自动释放
-
互斥锁特性:
- 互斥性:同一时刻只有一个线程能持有锁
- 可见性:锁的释放会将工作内存中的修改刷新到主内存
- 可重入性:同一个线程可以多次获取同一个锁
-
使用方式:
- 同步实例方法:锁是当前实例对象
public synchronized void method() {}
- 同步静态方法:锁是当前类的Class对象
public static synchronized void method() {}
- 同步代码块:锁是指定对象
synchronized(obj) { // 同步代码 }
-
volatile关键字:
- 保证变量的可见性:写操作立即刷新到主内存,读操作直接从主内存读取
- 禁止指令重排序
- 不保证原子性(适合单写多读场景)
3.5 单例与线程安全
答案:
- 饿汉式(线程安全):
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
- 双重检查锁定(DCL):
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 静态内部类(推荐):
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
- 枚举单例(最佳实践):
public enum Singleton {
INSTANCE;
public void doSomething() {}
}
3.6 如何避免线程安全性问题
答案:
- 线程安全性问题成因:
- 多线程环境
- 共享资源
- 非原子性操作
- 可见性问题
- 指令重排序
- 避免方法:
- 不共享数据:
- 使用ThreadLocal
- 使用局部变量
- 不可变对象:
- 使用final修饰的不可变对象
- 同步机制:
- synchronized
- volatile
- 原子类
- Lock
- 线程安全容器:
- ConcurrentHashMap
- CopyOnWriteArrayList
- 避免逃逸:
- 不要将内部对象发布到外部
- 不共享数据:
四、锁
4.1 锁的分类
答案:
- 自旋锁:线程不放弃CPU时间片,循环尝试获取锁(减少上下文切换)
- 阻塞锁:获取不到锁时线程进入阻塞状态
- 重入锁(ReentrantLock):同一线程可多次获取同一把锁
- 读写锁(ReadWriteLock):读锁共享,写锁互斥
- 互斥锁:同一时刻只有一个线程能持有锁(synchronized)
- 悲观锁:假定并发冲突严重,先加锁再操作(synchronized, ReentrantLock)
- 乐观锁:假定冲突少,先操作再检查(CAS, 版本号机制)
- 公平锁:按申请锁的顺序获取锁(ReentrantLock(true))
- 非公平锁:不保证顺序,可能插队(synchronized, ReentrantLock(false))
- 偏向锁:无竞争时消除同步开销
- 独占锁:同一时刻只有一个线程能持有锁
- 共享锁:多个线程可以同时持有锁(如读锁)
- 轻量级锁(Lightweight Lock):共享资源发生竞争的时候,CAS 操作成功,当前线程就获得了锁,并且锁的状态被标记为轻量级锁。
- 重量级锁(Heavyweight Lock):当轻量级锁膨胀失败时,锁会升级为重量级锁。重量级锁会使其他线程阻塞,而不是进行自旋等待,防止CPU空转浪费资源
4.2 Lock接口
答案:
- 使用方式:
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须在finally中释放
}
- 与synchronized的区别:
- synchronized是关键字,Lock是接口
- synchronized自动释放锁,Lock必须手动释放
- Lock可以尝试获取锁(tryLock)、可中断(lockInterruptibly)、可定时
- Lock可以实现公平锁
- Lock提供Condition实现更灵活的线程通信
- synchronized更简单,Lock更灵活
- 实现自定义锁:
需要实现Lock接口,通常基于AQS(AbstractQueuedSynchronizer)实现。 - AQS浅析:
AQS是构建锁和同步器的框架,核心包括:- 一个volatile int state表示同步状态
- 一个FIFO线程等待队列
- 通过CAS操作修改state
- 提供acquire/release模板方法
- ReentrantLock公平锁实现:
公平锁在获取锁时检查等待队列是否有前驱节点:
final boolean fairSyncTryAcquire(int acquires) {
if (getState() == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 重入逻辑...
}
- 锁降级:
在持有写锁的情况下获取读锁,然后释放写锁的过程:
// 获取写锁
writeLock.lock();
try {
// 修改数据...
// 获取读锁(锁降级开始)
readLock.lock();
} finally {
writeLock.unlock(); // 释放写锁
}
// 此时只有读锁,完成降级
try {
// 读取数据...
} finally {
readLock.unlock();
}
- StampedLock:
- 三种模式:写锁、悲观读锁、乐观读
- 乐观读不阻塞写操作
- 适合读多写少场景
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead(); // 乐观读
if (!lock.validate(stamp)) { // 检查是否被修改
stamp = lock.readLock(); // 转为悲观读
try {
// 读取数据...
} finally {
lock.unlockRead(stamp);
}
}
五、线程间通信
5.1 wait、notify、notifyAll
答案:
- 基本用法:
- wait(): 释放锁并进入等待状态
- notify(): 随机唤醒一个等待线程
- notifyAll(): 唤醒所有等待线程
- 使用规范:
synchronized(lock) {
while(条件不满足) { // 必须用while而不是if
lock.wait();
}
// 执行操作
// 可选:lock.notifyAll();
}
- 注意事项:
- 必须在同步块中调用
- wait()会释放锁,notify()不会释放锁
- 被唤醒的线程需要重新获取锁才能继续执行
- 优先使用notifyAll()避免信号丢失
5.3 生产者消费者模式
答案:
经典实现:
class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private int capacity;
public Buffer(int capacity) {
this.capacity = capacity;
}
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == capacity) {
wait();
}
queue.add(item);
notifyAll();
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
int item = queue.remove();
notifyAll();
return item;
}
}
使用BlockingQueue简化:
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 生产者
queue.put(item);
// 消费者
int item = queue.take();
5.5 Thread.join()
答案:
- 作用:
- 让当前线程等待被调用join()的线程执行完毕
- 常用于等待多个线程完成后再继续执行
- 使用:
Thread t1 = new Thread(() -> {...});
t1.start();
t1.join(); // 主线程等待t1完成
- 原理:
底层调用wait()实现:
public final synchronized void join(long millis) {
while (isAlive()) {
wait(millis);
}
}
5.6 ThreadLocal
答案:
- 简介:
- 线程局部变量,每个线程有自己独立的副本
- 避免共享变量带来的线程安全问题
- 使用:
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "init");
threadLocal.set("value");
String value = threadLocal.get();
threadLocal.remove(); // 重要:防止内存泄漏
- 原理:
- 每个Thread对象内部有ThreadLocalMap
- ThreadLocal作为key,存储线程私有值
- key是弱引用,value是强引用
- 内存泄漏问题:
- 原因:ThreadLocalMap的Entry中key是弱引用,value是强引用
- 解决:使用完调用remove()清理
- InheritableThreadLocal:
- 子线程可以继承父线程的ThreadLocal值
- 适用于需要传递上下文给子线程的场景
5.7 Condition
答案:
- 作用:
- 类似于wait/notify,但更灵活
- 一个Lock可以创建多个Condition
- 使用:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待
lock.lock();
try {
while (条件不满足) {
condition.await();
}
// 处理
} finally {
lock.unlock();
}
// 唤醒
lock.lock();
try {
// 改变条件
condition.signalAll();
} finally {
lock.unlock();
}
- 优势:
- 可以精确唤醒特定条件的线程
- 支持中断等待
- 支持超时等待
六、原子类
6.1 什么是原子类?
答案:
原子类是基于CAS(Compare And Swap)实现的线程安全类,位于java.util.concurrent.atomic包中。它们提供原子操作来更新基本类型、数组、引用等,无需使用锁。
特点:
- 线程安全
- 高性能(无锁或低锁)
- 基于CAS实现
- 适用于计数器、累加器等场景
6.2-6.5 原子类分类
答案:
- 基本类型:
- AtomicInteger
- AtomicLong
- AtomicBoolean
- 数组类型:
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
- 引用类型:
- AtomicReference
- AtomicStampedReference(解决ABA问题)
- AtomicMarkableReference
- 字段更新器:
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
示例:
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子自增
counter.compareAndSet(expect, update); // CAS操作
七、并发容器
7.1-7.3 同步容器与并发容器
答案:
- 同步容器:
- Collections.synchronizedXXX()包装的容器
- 如:synchronizedList, synchronizedMap
- 通过synchronized实现线程安全
- 性能较差(全表锁)
- 并发容器:
- java.util.concurrent包中的容器
- 实现:
- ConcurrentHashMap:分段锁/CAS
- CopyOnWriteArrayList:写时复制
- ConcurrentLinkedQueue:CAS无锁队列
- BlockingQueue:阻塞队列(Array/LinkedBlockingQueue)
- 特点:
- 高并发性能
- 弱一致性迭代器
- 更细粒度的锁或无锁
八、并发工具类
8.1-8.4 并发工具类
答案:
-
CountDownLatch:
- 允许一个或多个线程等待其他线程完成
- 一次性使用
CountDownLatch latch = new CountDownLatch(3); // 工作线程 latch.countDown(); // 主线程 latch.await();
-
CyclicBarrier:
- 让一组线程互相等待到达屏障点
- 可重复使用
CyclicBarrier barrier = new CyclicBarrier(3, () -> { // 所有线程到达后执行 }); // 工作线程 barrier.await();
-
Semaphore:
- 控制同时访问资源的线程数
Semaphore semaphore = new Semaphore(5); semaphore.acquire(); // 访问资源 semaphore.release();
-
Exchanger:
- 两个线程交换数据的同步点
Exchanger<String> exchanger = new Exchanger<>(); // 线程A String dataB = exchanger.exchange(dataA); // 线程B String dataA = exchanger.exchange(dataB);
九、线程池
9.1-9.7 线程池
答案:
- 为什么使用线程池:
- 降低资源消耗(减少创建销毁开销)
- 提高响应速度(任务到达时已有线程)
- 提高线程可管理性(统一分配、监控)
- 创建线程池:
- 通过ThreadPoolExecutor构造函数
- 通过Executors工厂方法(不推荐生产环境使用)
- 核心参数:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:空闲线程存活时间
- workQueue:任务队列
- threadFactory:线程工厂
- handler:拒绝策略
- 执行流程:
- 提交任务
- 核心线程未满 → 创建新线程执行
- 核心线程已满 → 放入队列
- 队列已满 → 创建非核心线程执行
- 达到最大线程数 → 执行拒绝策略
- 拒绝策略:
- AbortPolicy:抛出RejectedExecutionException(默认)
- CallerRunsPolicy:由提交任务的线程执行
- DiscardPolicy:直接丢弃
- DiscardOldestPolicy:丢弃队列最前面的任务
- Executor框架:
- Executor:执行接口
- ExecutorService:扩展接口(生命周期管理)
- ScheduledExecutorService:定时任务
- Executors:工厂类
- 使用建议:
- 根据任务类型配置线程池(CPU密集型、IO密集型)
- 使用有界队列防止资源耗尽
- 自定义线程命名便于排查问题
- 监控线程池状态
十、JVM与并发
10.1-10.3 JVM内存模型与并发
答案:
- JVM内存模型(JMM):
- 主内存:存储共享变量
- 工作内存:每个线程私有的内存空间
- 规则:
- 线程对变量的所有操作都必须在工作内存中进行
- 不同线程不能直接访问对方工作内存中的变量
- 变量传递需要通过主内存完成
- happens-before原则:
定义操作间的偏序关系,保证前一个操作的结果对后一个操作可见:- 程序顺序规则
- 锁规则
- volatile规则
- 线程启动规则
- 线程终止规则
- 中断规则
- 终结器规则
- 传递性
- 指令重排序:
- 编译器/处理器为了优化性能可能改变指令顺序
- as-if-serial语义:单线程程序执行结果不变
- 多线程环境下可能导致问题
- 通过volatile、synchronized、final等防止重排序
十一、实战与总结
11.1-11.4 并发实战
答案:
并发编程实战要点:
- 数据同步接口:
- 设计幂等接口
- 使用乐观锁处理并发更新
- 考虑分布式锁(如Redis)解决跨JVM同步
- 中间表设计:
- 读写分离
- 适当冗余减少关联查询
- 考虑分库分表策略
- 生产者-消费者模式:
- 使用BlockingQueue简化实现
- 控制生产/消费速度匹配
- 考虑背压机制防止队列积压
12. 并发编程面试总结
答案:
高频面试题总结:
- 线程状态及转换
- synchronized原理及优化(偏向锁、轻量级锁、重量级锁)
- volatile原理(内存屏障、禁止重排序)
- CAS原理及ABA问题
- AQS原理及实现(ReentrantLock、CountDownLatch等)
- ConcurrentHashMap实现原理
- 线程池参数配置及工作流程
- ThreadLocal原理及内存泄漏问题
- 死锁条件及排查方法(jstack)
- happens-before原则
掌握这些核心知识点,结合实际项目经验,能够应对大多数Java并发编程相关的面试问题