Java并发编程
- 一、 基础概念
- 1. 进程与线程的区别是什么?
- 2. 创建线程的几种方式?
- 3. 线程的生命周期(状态)有哪些?
- 4. 什么是守护线程(Daemon Thread)?
- 5. 线程优先级(Priority)的作用及问题?
- 二、线程安全与锁机制
- 1. 什么是线程安全?如何实现线程安全?
- 2. synchronized 关键字的底层实现原理(对象头、Monitor 机制)?
- 3. volatile 关键字的作用和实现原理(内存屏障、可见性、禁止指令重排)?
- 4. 什么是 CAS(Compare-And-Swap)?其优缺点?
- 5. synchronized 和 ReentrantLock 的区别?
- 6. 可重入锁(ReentrantLock)的实现原理?
- 7. 公平锁与非公平锁的区别?
- 8. 什么是死锁?如何避免或检测死锁?
- 9. 偏向锁、轻量级锁、重量级锁的升级过程?
- 10. 锁消除(Lock Elimination)和锁粗化(Lock Coarsening)的原理?
- 三、线程协作与通信
- 1. `wait()`、`notify()`、`notifyAll()` 的使用场景和注意事项?
- 2. `sleep()` 和 `wait()` 的区别?
- 3. 如何实现线程间通信(如生产者-消费者模型)?
- 4. `Condition` 接口的作用及与 `wait/notify` 的区别?
- 四、并发工具类(JUC)
- 1. `CountDownLatch`、`CyclicBarrier`、`Semaphore` 的使用场景和区别
- **1.1 `CountDownLatch`**
- **1.2 `CyclicBarrier`**
- **1.3 `Semaphore`**
- 2. `Exchanger` 的作用
- 3. `Phaser` 的使用场景
- 4. `Future` 和 `CompletableFuture` 的区别
- 5. `StampedLock` 的优化点及使用场景
- **优化点**
- **使用场景**
- 五、 线程池相关问题解析
- 1. 线程池的核心参数及作用
- 2. 线程池的工作流程(任务提交后的处理逻辑)
- 3. 常见的线程池类型及其问题
- 4. 线程池的拒绝策略
- 5. 如何合理配置线程池参数
- 6. 线程池中线程复用(Thread Reuse)的原理
- 示例代码
- 六、 高级并发特性解析
- 1. Java 内存模型(JMM)的核心概念
- 2. 原子性、可见性、有序性及其保证方法
- 3. 指令重排序及其避免方法
- 4. ThreadLocal 的原理、使用场景及内存泄漏问题
- 5. Fork/Join 框架与工作窃取机制
- 示例代码
- 七、 并发容器解析
- 1. ConcurrentHashMap 的实现原理(JDK7 vs JDK8)
- 2. CopyOnWriteArrayList 的适用场景及优缺点
- 3. BlockingQueue 的实现类及使用场景
- 4. ConcurrentLinkedQueue 的无锁实现原理
- 八、 并发设计模式与实战
- 1. 生产者-消费者模式的实现方式
- 2. 如何实现线程安全的单例模式
- 3. 如何排查和解决死锁问题
- 排查方法
- 解决措施
- 4. 如何避免竞态条件(Race Condition)
- 加锁同步
- 原子操作
- 不可变对象设计
- 线程安全的集合
- 5. 如何设计高并发场景下的计数器
- 原子变量
- LongAdder/LongAccumulator
- 分段计数器
- 九、底层原理与扩展
- 1. Java 线程与操作系统线程的关系
- 2. 什么是协程(Coroutine)?Java 中的虚拟线程(Loom 项目)
- 3. 如何通过 jstack 分析线程状态
- 4. 什么是伪共享(False Sharing)?如何避免?
- 十、其他扩展问题
- 1. 如何实现异步编程(如 CompletableFuture、Reactive Streams)?
- CompletableFuture
- Reactive Streams
- 2. 分布式锁与单机锁的区别
- **单机锁**
- **分布式锁**
- 3. 无锁编程(Lock-Free)的实现思路
- **CAS(Compare-And-Swap)**
- **乐观锁**
- **无锁数据结构**
- 4. 高并发场景下常见的性能优化手段
- **1. 减少锁竞争**
- **2. 线程池优化**
- **3. 缓存机制**
- **4. 异步 & 批量处理**
- **5. 数据结构优化**
一、 基础概念
1. 进程与线程的区别是什么?
进程与线程的主要区别如下:
- 定义
- 进程(Process)是操作系统分配资源的基本单位。
- 线程(Thread)是 CPU 调度的基本单位。
- 资源分配
- 进程拥有独立的内存空间和系统资源。
- 线程共享进程的资源,如内存和文件句柄。
- 通信方式
- 进程间通信(IPC)方式复杂,如管道、共享内存、消息队列等。
- 线程间通信更简单,可通过共享变量直接通信。
- 开销
- 进程创建和切换开销较大。
- 线程切换成本较小,效率更高。
- 崩溃影响
- 进程崩溃不会影响其他进程。
- 线程崩溃可能影响整个进程。
2. 创建线程的几种方式?
在 Java 中,创建线程主要有以下几种方式:
-
继承
Thread
类class MyThread extends Thread {public void run() {System.out.println("线程执行");} }public class Main {public static void main(String[] args) {MyThread thread = new MyThread();thread.start();} }
-
实现
Runnable
接口class MyRunnable implements Runnable {public void run() {System.out.println("线程执行");} }public class Main {public static void main(String[] args) {Thread thread = new Thread(new MyRunnable());thread.start();} }
-
使用
Callable
和FutureTask
(可以返回值和抛出异常)import java.util.concurrent.*;class MyCallable implements Callable<String> {public String call() throws Exception {return "线程执行完成";} }public class Main {public static void main(String[] args) throws Exception {FutureTask<String> task = new FutureTask<>(new MyCallable());Thread thread = new Thread(task);thread.start();System.out.println(task.get());} }
-
使用线程池
ExecutorService
import java.util.concurrent.*;public class Main {public static void main(String[] args) {ExecutorService executor = Executors.newFixedThreadPool(2);executor.execute(() -> System.out.println("线程执行"));executor.shutdown();} }
3. 线程的生命周期(状态)有哪些?
线程的生命周期可分为以下几种状态:
- NEW(新建状态):线程对象被创建,但未调用
start()
。 - RUNNABLE(就绪/运行状态):调用
start()
方法后,等待 CPU 调度。 - BLOCKED(阻塞状态):线程试图获取锁但被阻塞。
- WAITING(无限等待状态):线程调用
wait()
或join()
,需显式唤醒。 - TIMED_WAITING(计时等待状态):线程调用
sleep(time)
、wait(time)
等方法。 - TERMINATED(终止状态):线程执行完成或被异常终止。
4. 什么是守护线程(Daemon Thread)?
守护线程是后台运行的线程,主要用于执行后台任务,如垃圾回收。
- 特点
- 守护线程在所有非守护线程结束后,自动终止。
setDaemon(true)
方法可以设置线程为守护线程。
示例:
public class DaemonThreadExample {public static void main(String[] args) {Thread daemonThread = new Thread(() -> {while (true) {System.out.println("守护线程运行中...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});daemonThread.setDaemon(true);daemonThread.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("主线程结束");}
}
5. 线程优先级(Priority)的作用及问题?
- Java 线程优先级范围:
1
(最低)~10
(最高)。 - 线程默认优先级是
5
。 - 使用
setPriority(int newPriority)
设置优先级。
示例:
Thread thread = new Thread(() -> System.out.println("线程运行"));
thread.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级
thread.start();
问题:
- 不一定生效:线程调度由操作系统决定,Java 只是建议。
- 可能导致线程饥饿:高优先级线程可能长时间占用 CPU,低优先级线程无法执行。
- 平台相关:不同操作系统对线程优先级的实现可能不同。
二、线程安全与锁机制
1. 什么是线程安全?如何实现线程安全?
线程安全指的是多个线程同时访问共享数据时,不会导致数据的不一致性或错误。
实现线程安全的方法:
- synchronized 关键字:对方法或代码块加锁,确保同一时间只有一个线程访问。
- volatile 关键字:保证变量的可见性,防止指令重排序。
- ReentrantLock:可重入锁,提供更灵活的锁控制。
- CAS(Compare-And-Swap):无锁并发编程,保证变量的原子性更新。
- 线程安全的集合类:如
ConcurrentHashMap
、CopyOnWriteArrayList
。 - ThreadLocal:为每个线程提供独立变量,防止共享数据冲突。
- 原子操作类:如
AtomicInteger
、AtomicReference
。
2. synchronized 关键字的底层实现原理(对象头、Monitor 机制)?
synchronized
通过对象头(Mark Word)中的锁标志位和 Monitor 机制 实现。
- 对象头(Mark Word):存储锁信息,如偏向锁、轻量级锁、重量级锁。
- Monitor 机制:由操作系统的 互斥量(Mutex) 实现,线程竞争锁时可能进入阻塞状态。
- 锁升级过程:
- 偏向锁(Biased Locking)
- 轻量级锁(Lightweight Locking)
- 重量级锁(Heavyweight Locking)
示例:
synchronized (this) {System.out.println("同步代码块");
}
3. volatile 关键字的作用和实现原理(内存屏障、可见性、禁止指令重排)?
volatile
关键字作用:
- 保证可见性:修改后的值会立即刷新到主内存。
- 禁止指令重排序:防止 CPU 优化导致的顺序问题。
- 不保证原子性:多个线程修改
volatile
变量仍可能导致竞态条件。
底层实现:
- 通过 内存屏障(Memory Barrier) 确保可见性。
- 使用 MESI 缓存一致性协议 保证 CPU 缓存一致性。
示例:
private volatile boolean flag = true;
4. 什么是 CAS(Compare-And-Swap)?其优缺点?
CAS(比较并交换) 是一种无锁并发机制,核心思想是:
- 读取变量的当前值
V
。 - 如果
V
等于期望值E
,则更新为新值N
。 - 若
V
发生变化,则重新尝试。
优点:
- 无需加锁,性能高。
缺点:
- ABA 问题:可使用
AtomicStampedReference
解决。 - 自旋消耗 CPU:长时间自旋可能降低性能。
示例:
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.compareAndSet(0, 1);
5. synchronized 和 ReentrantLock 的区别?
对比项 | synchronized | ReentrantLock |
---|---|---|
锁的类型 | 内置锁 | 显式锁 |
可重入性 | 支持 | 支持 |
公平锁 | 不支持 | 支持 |
中断响应 | 不支持 | 支持 |
条件变量 | 不支持 | 支持 Condition |
6. 可重入锁(ReentrantLock)的实现原理?
可重入锁 允许同一线程多次获取锁。
ReentrantLock
通过 AQS(AbstractQueuedSynchronizer) 维护一个 state 变量,记录获取次数。- 线程释放锁时,
state
递减,直到state == 0
时真正释放锁。
示例:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {System.out.println("执行任务");
} finally {lock.unlock();
}
7. 公平锁与非公平锁的区别?
- 公平锁:线程按顺序获取锁,避免线程饥饿。
- 非公平锁:线程可以插队获取锁,提高性能。
示例:
ReentrantLock lock = new ReentrantLock(true); // 公平锁
8. 什么是死锁?如何避免或检测死锁?
死锁 是指多个线程相互等待对方释放资源,导致程序无法继续。
避免方法:
- 避免嵌套锁
- 资源分配有序
- 超时机制
- 死锁检测工具(jstack、jconsole)
9. 偏向锁、轻量级锁、重量级锁的升级过程?
锁的升级流程:
- 无锁状态
- 偏向锁:只有一个线程访问。
- 轻量级锁:多个线程竞争但无阻塞。
- 重量级锁:多个线程竞争,阻塞等待。
10. 锁消除(Lock Elimination)和锁粗化(Lock Coarsening)的原理?
- 锁消除:JIT 编译时,发现局部变量不逃逸,移除锁。
- 锁粗化:多个连续加锁的操作合并,减少锁的频繁释放与获取。
示例:
public void test() {StringBuilder sb = new StringBuilder(); // JIT 可能优化掉锁sb.append("Hello");sb.append("World");
}
三、线程协作与通信
1. wait()
、notify()
、notifyAll()
的使用场景和注意事项?
-
使用场景:
wait()
、notify()
和notifyAll()
主要用于线程间的同步,适用于生产者-消费者模型或多个线程共享资源的情况。wait()
: 让当前线程进入等待状态,释放锁,让其他线程可以获取锁并执行。notify()
: 唤醒一个在wait()
状态的线程,但不会立即释放锁,需等待当前线程执行完毕。notifyAll()
: 唤醒所有等待的线程,但只有一个线程能获取锁执行,其他线程仍需等待。
-
注意事项:
wait()
、notify()
、notifyAll()
必须在同步代码块或同步方法中使用,否则会抛IllegalMonitorStateException
。- 调用
wait()
方法后,线程会释放锁,进入等待队列,直到被notify()
或notifyAll()
唤醒。 notify()
只是通知一个等待线程,并不会立即释放锁,必须等待持有锁的线程执行完毕后才能释放。
2. sleep()
和 wait()
的区别?
对比项 | sleep() | wait() |
---|---|---|
作用 | 让当前线程休眠一段时间 | 让当前线程等待,直到被 notify() 唤醒 |
释放锁 | 不释放锁 | 释放锁 |
使用位置 | 可以在任何地方使用 | 必须在同步代码块或同步方法中使用 |
恢复方式 | 时间到后自动恢复运行 | 需要 notify() 或 notifyAll() 唤醒 |
抛出异常 | InterruptedException | InterruptedException |
3. 如何实现线程间通信(如生产者-消费者模型)?
示例:使用 wait()
和 notify()
实现生产者-消费者模式
class SharedResource {private int data;private boolean available = false;public synchronized void produce(int value) {while (available) {try {wait(); // 生产者等待} catch (InterruptedException e) {e.printStackTrace();}}data = value;available = true;System.out.println("生产者生产: " + value);notify(); // 唤醒消费者}public synchronized int consume() {while (!available) {try {wait(); // 消费者等待} catch (InterruptedException e) {e.printStackTrace();}}available = false;System.out.println("消费者消费: " + data);notify(); // 唤醒生产者return data;}
}
4. Condition
接口的作用及与 wait/notify
的区别?
-
作用:
Condition
提供了比wait()
和notify()
更强大的线程间通信机制。- 允许多个等待队列,可更精细地控制线程的唤醒。
-
区别:
| 对比项 |wait()/notify()
|Condition
|
|------------|-----------------|------------|
| 依赖的锁 | 必须配合synchronized
关键字使用 | 需要Lock
对象 |
| 等待方法 |wait()
|await()
|
| 通知方法 |notify()
/notifyAll()
|signal()
/signalAll()
|
| 多个等待队列 | 只有一个等待队列,notify()
可能会误唤醒非目标线程 | 可以创建多个Condition
,更精准唤醒特定线程 | -
示例:使用
Condition
实现生产者-消费者模型
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class SharedResourceWithCondition {private int data;private boolean available = false;private final Lock lock = new ReentrantLock();private final Condition condition = lock.newCondition();public void produce(int value) {lock.lock();try {while (available) {condition.await(); // 生产者等待}data = value;available = true;System.out.println("生产者生产: " + value);condition.signal(); // 唤醒消费者} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public int consume() {lock.lock();try {while (!available) {condition.await(); // 消费者等待}available = false;System.out.println("消费者消费: " + data);condition.signal(); // 唤醒生产者return data;} catch (InterruptedException e) {e.printStackTrace();return -1;} finally {lock.unlock();}}
}
四、并发工具类(JUC)
1. CountDownLatch
、CyclicBarrier
、Semaphore
的使用场景和区别
1.1 CountDownLatch
- 作用:用于 等待多个线程执行完毕后再继续,计数器只能减不能加。
- 使用场景:
- 任务分解:主线程等待多个子线程完成后再继续。
- 并发测试:多个线程同时执行某个任务,主线程等待所有线程完成。
- 系统启动依赖:等待多个资源加载完毕后再启动服务。
import java.util.concurrent.CountDownLatch;public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(3);for (int i = 0; i < 3; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 完成任务");latch.countDown();}).start();}latch.await(); // 等待所有子线程执行完毕System.out.println("所有任务完成,继续执行主线程");}
}
1.2 CyclicBarrier
- 作用:用于 让一组线程等待彼此到达某个同步点,并在到达后执行某个 回调任务。
- 与
CountDownLatch
的区别:CountDownLatch
只能使用一次,CyclicBarrier
可重复使用。CyclicBarrier
允许在所有线程到达屏障时执行额外任务。
- 使用场景:
- 多人游戏:等待所有玩家加载完毕后开始游戏。
- 并行计算:多线程分块计算,待所有线程计算完成后合并结果。
import java.util.concurrent.CyclicBarrier;public class CyclicBarrierExample {public static void main(String[] args) {CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("所有线程到达屏障点"));for (int i = 0; i < 3; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 到达屏障");try {barrier.await();} catch (Exception e) {e.printStackTrace();}}).start();}}
}
1.3 Semaphore
- 作用:限流,用于控制并发线程数,类似停车场的车位管理。
- 使用场景:
- 限制数据库连接数。
- 控制接口并发访问,防止系统崩溃。
- 线程池限流,限制任务提交速率。
import java.util.concurrent.Semaphore;public class SemaphoreExample {public static void main(String[] args) {Semaphore semaphore = new Semaphore(2);for (int i = 0; i < 5; i++) {new Thread(() -> {try {semaphore.acquire();System.out.println(Thread.currentThread().getName() + " 获取资源");Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + " 释放资源");semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}}).start();}}
}
2. Exchanger
的作用
-
作用:两个线程间的数据交换工具,用于在两个线程之间安全地交换对象。
-
特点:
- 线程 A 调用
exchange()
方法后会阻塞,直到线程 B 也调用exchange()
交换数据。 - 适用于 生产者-消费者模型,当生产者产生数据,消费者等待数据时,
Exchanger
可用作数据缓冲区。
- 线程 A 调用
-
使用场景:
- 数据处理:一个线程生产数据,另一个线程处理数据。
- 大数据计算:并行计算任务的阶段性交换。
3. Phaser
的使用场景
-
作用:可变并发任务同步工具,类似
CyclicBarrier
,但支持 动态注册/注销线程。 -
区别:
CyclicBarrier
需要指定固定数量的线程,Phaser
支持动态增减线程。Phaser
适用于多阶段任务,而CyclicBarrier
适用于单次屏障。
-
使用场景:
- 分阶段任务:如多轮竞赛,每轮线程数不同。
- 递归任务:动态管理并发线程的执行。
4. Future
和 CompletableFuture
的区别
对比项 | Future | CompletableFuture |
---|---|---|
异步能力 | 只能获取异步任务结果,不支持回调 | 支持回调和流式操作,能更好地管理异步流程 |
阻塞获取结果 | 需要 get() 方法,可能会阻塞线程 | 支持 thenApply() 等非阻塞操作 |
支持组合任务 | 不支持多个 Future 组合 | thenCombine() 支持多个任务组合 |
异常处理 | 只能手动 try-catch 处理 | exceptionally() 处理异常更方便 |
取消任务 | cancel() 方法 | cancel() 也可用 |
CompletableFuture
适用于更复杂的异步任务链,能提高代码可读性和效率。
5. StampedLock
的优化点及使用场景
优化点
-
支持三种模式:
- 写锁 (
writeLock()
):独占锁,类似ReentrantReadWriteLock
的写锁。 - 读锁 (
readLock()
):共享锁,多个线程可同时读。 - 乐观读 (
tryOptimisticRead()
):不阻塞写入,提高性能,适用于读多写少场景。
- 写锁 (
-
性能优势:
- 避免读锁竞争:乐观读锁不会阻塞写操作,适用于高读低写的情况。
- 降低锁的升级开销:先尝试乐观读,再回退为悲观锁,减少锁冲突。
使用场景
- 缓存系统:大多数操作是读取,写入较少的情况,避免
ReentrantReadWriteLock
造成的读锁开销。 - 多线程计数器:多个线程同时读计数值,少数线程写入。
- 金融交易系统:读取账户余额时使用乐观锁,只有在余额变更时才获取写锁。
import java.util.concurrent.locks.StampedLock;public class StampedLockExample {private int count = 0;private final StampedLock lock = new StampedLock();public int optimisticRead() {long stamp = lock.tryOptimisticRead();int current = count;if (!lock.validate(stamp)) {stamp = lock.readLock();try {current = count;} finally {lock.unlockRead(stamp);}}return current;}public void write(int value) {long stamp = lock.writeLock();try {count = value;} finally {lock.unlockWrite(stamp);}}
}
五、 线程池相关问题解析
1. 线程池的核心参数及作用
-
核心线程数 (corePoolSize)
指定了线程池中始终保持运行的最小线程数。当有新任务到达时,线程池会优先创建新线程直到达到核心线程数,之后任务才会进入队列等待执行。 -
最大线程数 (maximumPoolSize)
表示线程池允许创建的最大线程数。当队列已满且核心线程都在忙碌时,线程池会创建新线程直至达到最大线程数。 -
任务队列 (workQueue)
用于存放等待执行的任务。常用队列类型有有界队列(如ArrayBlockingQueue
)和无界队列(如LinkedBlockingQueue
)。队列的选择会影响任务调度的策略和系统资源使用。 -
线程空闲时间 (keepAliveTime)
当线程数超过核心线程数时,多余线程在空闲状态下等待新任务的最长时间,超时后这些线程会被终止,以节省资源。 -
线程工厂 (threadFactory)
用于创建新线程,可以通过自定义线程工厂设置线程名称、优先级、守护状态等,从而便于线程管理和调试。 -
拒绝策略 (rejectedExecutionHandler)
当线程池饱和(即线程数已达最大值且任务队列也满)时,对新提交任务的处理策略,如抛异常、调用者运行、丢弃任务或丢弃队列中最老任务等。
2. 线程池的工作流程(任务提交后的处理逻辑)
-
任务提交
当调用execute()
或submit()
方法提交任务时,线程池首先判断当前线程数是否小于核心线程数。 -
创建核心线程
如果当前线程数小于核心线程数,则立即创建新线程执行任务,而不是将任务放入队列。 -
任务入队
当核心线程数已满,新的任务将被放入任务队列中等待执行。 -
创建非核心线程
如果任务队列已满,并且当前线程数小于最大线程数,则线程池会创建新线程来处理额外的任务。 -
拒绝策略触发
当任务队列已满且线程数已经达到最大线程数时,线程池根据配置的拒绝策略处理新提交的任务。 -
线程复用与销毁
完成任务后,线程不会立即销毁,而是进入等待状态以便复用。当线程超过核心线程数且长时间空闲,则会被回收。
3. 常见的线程池类型及其问题
-
FixedThreadPool
- 特点:固定线程数,不会动态增减。
- 问题:若任务执行缓慢或任务积压,可能导致队列中任务长时间等待,无法利用资源动态扩展。
-
CachedThreadPool
- 特点:根据任务需要动态创建线程,线程空闲时会被回收,适用于任务执行时间较短的场景。
- 问题:在任务激增时可能会创建大量线程,导致系统资源耗尽,甚至引发性能问题。
-
SingleThreadExecutor
- 特点:单线程顺序执行所有任务,适用于需要保证任务顺序的场景。
- 问题:单线程可能成为性能瓶颈,且任务过多时容易造成任务堆积。
4. 线程池的拒绝策略
-
AbortPolicy (默认策略)
当任务无法执行时,直接抛出RejectedExecutionException
异常,中断任务提交流程。 -
CallerRunsPolicy
由任务提交者所在的线程执行该任务,从而降低新任务的提交速率,缓解线程池压力。 -
DiscardPolicy
直接丢弃新提交的任务,不会抛出异常,但可能导致部分任务未被执行。 -
DiscardOldestPolicy
丢弃任务队列中等待最久的任务,然后尝试提交当前任务,这样可以为新的任务腾出空间。
5. 如何合理配置线程池参数
-
根据任务性质配置线程数
- CPU密集型任务:通常将线程数设置为 CPU 核心数或略微超出。
- IO密集型任务:线程数可以设置为 CPU 核心数的多倍,因为等待时间较长。
-
任务队列的选择
根据任务特性选择合适的队列类型和容量。无界队列虽能避免拒绝任务,但可能导致任务堆积;有界队列则能限制任务数量,但在高负载时容易触发拒绝策略。 -
合理设置
keepAliveTime
防止非核心线程在任务较少时占用系统资源,确保线程在空闲超时后被回收。 -
拒绝策略的选取
根据系统对任务丢失或延迟的容忍度选择合适的拒绝策略,保障系统稳定性。 -
动态调整策略
根据实际运行情况,监控任务执行情况和系统资源使用情况,动态调整线程池参数以达到最佳性能。
6. 线程池中线程复用(Thread Reuse)的原理
-
预先创建与等待
线程池在初始化或任务提交时创建一定数量的核心线程,这些线程在完成任务后不会被销毁,而是进入等待状态,以便立即处理后续任务。 -
任务队列
线程通过不断从任务队列中取任务来执行,实现线程的重复利用,避免频繁创建和销毁线程带来的开销。 -
KeepAlive 机制
对于非核心线程,在任务执行完毕后,如果在指定的空闲时间内没有新任务到达,则会被回收。这样既能保证高负载时的扩展能力,也能在低负载时节省资源。
示例代码
下面是一个简单的 Java 示例,展示如何使用线程池提交任务。注意,Java 代码已做适当转义:
// 示例:使用线程池提交任务的简单代码
import java.util.concurrent.*;public class ThreadPoolExample {public static void main(String[] args) {// 创建一个固定大小的线程池,核心线程数和最大线程数都为4ExecutorService executor = Executors.newFixedThreadPool(4);for (int i = 0; i < 10; i++) {executor.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务 - " + Thread.currentThread().getName());}});}// 关闭线程池,等待所有任务执行完毕后退出executor.shutdown();}
}
六、 高级并发特性解析
1. Java 内存模型(JMM)的核心概念
-
主内存与工作内存
每个线程都有自己的工作内存,用于保存共享变量的副本,而共享变量本身存储在主内存中。线程对变量的所有操作都必须先在工作内存中进行,再同步到主内存,从而保证了数据的一致性和线程间的可见性。 -
happens-before 原则
规定了操作之间的先后关系,确保一个操作的结果在另一个操作执行前是可见的。典型规则包括:- 程序顺序规则:单线程中代码按照顺序执行。
- 锁规则:解锁操作 happens-before 后续对同一锁的加锁操作。
- volatile 规则:volatile 写操作 happens-before 随后的 volatile 读操作。
- 线程启动规则:线程启动前的操作 happens-before 线程内的所有操作。
2. 原子性、可见性、有序性及其保证方法
-
原子性
一个操作是不可分割的,要么全部执行,要么全部不执行。
保证方法:- 使用
synchronized
关键字或显示锁(如ReentrantLock
)来保证复合操作的原子性。 - 利用 Java 的原子类(如
AtomicInteger
)实现无锁的原子操作。
- 使用
-
可见性
确保当一个线程修改了共享变量,其他线程能够及时看到这个修改。
保证方法:- 使用
volatile
关键字,确保变量的修改立即刷新到主内存。 - 使用锁(
synchronized
或Lock
)来保证操作的可见性。
- 使用
-
有序性
保证程序中指令按照代码顺序执行(在单线程环境下成立)。
保证方法:- 使用
synchronized
或volatile
提供的内存屏障,防止指令重排序。 - JMM 中的 happens-before 规则确保了操作的有序性。
- 使用
3. 指令重排序及其避免方法
-
指令重排序原理
为了优化性能,编译器和处理器可能会重新排序指令。虽然在单线程环境中不会改变程序的正确性,但在多线程环境中可能会导致数据不一致的问题。 -
避免方法:
- 内存屏障
利用内存屏障指令保证某些操作在执行顺序上的严格顺序,从而防止重排序问题。 - volatile 关键字
使用 volatile 修饰的变量能确保写操作不会被重排序到读操作之后,并保证了可见性。
- 内存屏障
4. ThreadLocal 的原理、使用场景及内存泄漏问题
-
原理
每个线程内部都有一个 ThreadLocalMap,其中存储了 ThreadLocal 对象和对应的线程局部变量。这样,每个线程都可以独立访问和修改自己的变量副本,而不会影响其他线程。 -
使用场景
- 需要线程隔离的变量,如用户会话、数据库连接等。
- 避免在多线程环境中共享数据而导致的同步问题。
-
内存泄漏问题
当线程使用完 ThreadLocal 后,如果没有调用remove()
方法清理 ThreadLocalMap,可能会导致内存泄漏,特别是在使用线程池等长生命周期线程时。
解决方法:- 在任务执行结束后主动调用
ThreadLocal.remove()
清理数据。 - 注意设计长生命周期线程中 ThreadLocal 的使用,避免不必要的数据驻留。
- 在任务执行结束后主动调用
5. Fork/Join 框架与工作窃取机制
-
Fork/Join 框架
是 Java 7 引入的一个并行计算框架,适用于将大任务拆分成多个小任务(Fork),并行执行后将结果合并(Join)。核心组件包括ForkJoinPool
、ForkJoinTask
以及其子类RecursiveTask
(有返回值)和RecursiveAction
(无返回值)。 -
工作窃取机制 (Work-Stealing)
每个工作线程维护一个双端队列,当线程完成了自己队列中的任务时,会尝试从其他线程队列的尾部“窃取”任务以保持高效的资源利用和负载均衡。
示例代码
以下是一个使用 Fork/Join 框架计算数组求和的示例代码,注意 Java 代码中的特殊字符均已转义:
// 示例:使用 Fork/Join 框架计算数组求和
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;public class ForkJoinSum extends RecursiveTask<Long> {private static final int THRESHOLD = 1000;private long[] arr;private int start;private int end;public ForkJoinSum(long[] arr, int start, int end) {this.arr = arr;this.start = start;this.end = end;}@Overrideprotected Long compute() {if (end - start <= THRESHOLD) {long sum = 0;for (int i = start; i < end; i++) {sum += arr[i];}return sum;} else {int mid = (start + end) / 2;ForkJoinSum leftTask = new ForkJoinSum(arr, start, mid);ForkJoinSum rightTask = new ForkJoinSum(arr, mid, end);leftTask.fork(); // 异步执行左边任务long rightResult = rightTask.compute(); // 同步执行右边任务long leftResult = leftTask.join(); // 等待左边任务完成return leftResult + rightResult;}}public static void main(String[] args) {long[] array = new long[10000];// 初始化数组for (int i = 0; i < array.length; i++) {array[i] = i;}ForkJoinPool pool = new ForkJoinPool();ForkJoinSum task = new ForkJoinSum(array, 0, array.length);long result = pool.invoke(task);System.out.println("Sum: " + result);}
}
该示例展示了如何将一个大任务(数组求和)拆分成多个小任务,通过 Fork/Join 框架的工作窃取机制并行处理任务,并最终合并结果。
七、 并发容器解析
1. ConcurrentHashMap 的实现原理(JDK7 vs JDK8)
-
JDK7 实现原理
- 分段锁(Segment)机制:将整个 Map 划分为多个 Segment,每个 Segment 内部维护一部分哈希桶。
- 锁定粒度较小:操作某个键值对时,仅锁定所在的 Segment,读操作大多无锁,从而提升并发度。
- 局限性:Segment 数量在创建时固定,可能存在热点 Segment 导致争用。
-
JDK8 实现原理
- 取消分段锁:直接使用一个 Node 数组作为底层结构,通过 CAS 操作实现无锁更新。
- 细粒度锁:在发生冲突时,仅对链表或红黑树的某个桶加锁,而非整个 Segment。
- 链表转红黑树:当单个桶中链表长度超过阈值时,转换为红黑树以降低查找时间。
- 优势:读操作完全无锁,写操作在冲突较少时可通过 CAS 完成,提高了整体性能和扩展性。
2. CopyOnWriteArrayList 的适用场景及优缺点
-
适用场景
- 读操作远远多于写操作的场景,如缓存、事件监听器列表等。
- 需要保证在迭代时数据不被修改,避免
ConcurrentModificationException
的情况。
-
优点
- 无锁读操作:迭代时不需要加锁,性能高且线程安全。
- 简单安全:每次写操作都会复制整个底层数组,保证了数据的一致性。
-
缺点
- 写操作开销大:每次更新都会复制整个数组,对于写密集型应用效率低下。
- 内存占用增加:复制操作会导致额外的内存消耗,适用于数据量较小且变更较少的场景。
3. BlockingQueue 的实现类及使用场景
-
ArrayBlockingQueue
- 实现原理:基于数组实现的有界阻塞队列,内部通过
ReentrantLock
来保证线程安全。 - 使用场景:适用于固定容量的缓冲区,如生产者消费者模式中限制任务数量,防止过载。
- 实现原理:基于数组实现的有界阻塞队列,内部通过
-
LinkedBlockingQueue
- 实现原理:基于链表实现,可设置有界或无界队列,采用分离的
putLock
和takeLock
提高并发性能。 - 使用场景:适用于需要较大容量缓冲,或不希望受到固定数组大小限制的场景。
- 实现原理:基于链表实现,可设置有界或无界队列,采用分离的
-
其他实现类
- PriorityBlockingQueue:基于优先级的无界阻塞队列,适用于需要按照优先级处理任务的场景。
- DelayQueue:基于延时策略的队列,用于任务调度。
- SynchronousQueue:每个插入操作必须等待对应的移除操作,适合于任务直接交给消费者处理的场景。
4. ConcurrentLinkedQueue 的无锁实现原理
- 基于链表的无锁队列:采用链表结构作为底层数据结构。
- CAS 操作:利用 CAS(Compare-And-Swap)原子操作来保证在高并发环境下的线程安全,无需显式锁。
- 头尾指针维护:通过原子变量维护头结点和尾结点,入队和出队操作通过 CAS 更新指针,保证数据一致性。
- 优点:高并发情况下性能优越,避免了锁竞争;
- 缺点:在极端情况下可能存在短暂的不一致状态,但总体上能够保证最终一致性。
八、 并发设计模式与实战
1. 生产者-消费者模式的实现方式
- 阻塞队列实现
利用阻塞队列(如ArrayBlockingQueue
、LinkedBlockingQueue
)实现任务缓冲区,生产者调用put()
将任务放入队列,消费者调用take()
从队列中获取任务。 - 等待/通知机制
在共享缓冲区上使用wait()
与notify()/notifyAll()
实现线程间通信,但需要小心处理虚假唤醒和同步问题。 - 信号量机制
通过Semaphore
控制缓冲区中可用资源数量,协调生产者和消费者的访问。
2. 如何实现线程安全的单例模式
- 双重检查锁定(DCL)
在实例为空时进入同步代码块,再次检查后创建实例。需要将实例声明为volatile
,防止指令重排序导致问题。 - 静态内部类
利用 JVM 类加载机制,在第一次使用时创建单例,既保证线程安全,又实现延迟加载。 - 枚举实现
使用枚举类型,JVM 会保证枚举实例的唯一性和线程安全。
示例:使用双重检查锁定实现线程安全的单例模式
(注意:Java代码中的特殊字符已转义)
public class Singleton {private static volatile Singleton instance;private Singleton() {// 防止反射调用if (instance != null) {throw new IllegalStateException("Already initialized");}}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}
3. 如何排查和解决死锁问题
排查方法
- 使用线程转储(Thread Dump)工具分析线程的锁持有情况;
- 利用 JDK 内置的监控工具(如 VisualVM、JConsole、ThreadMXBean)检测死锁;
- 检查代码中锁的获取顺序是否一致,是否存在资源循环等待。
解决措施
- 统一锁顺序:确保所有线程按照相同顺序获取多个锁;
- 使用超时锁:采用
tryLock()
设置超时,避免长时间等待; - 细化锁粒度:减少单个锁保护的资源范围,或采用无锁算法与乐观锁。
4. 如何避免竞态条件(Race Condition)
加锁同步
- 使用
synchronized
或ReentrantLock
等互斥机制,确保同一时间只有一个线程访问共享数据。
原子操作
- 利用原子类(如
AtomicInteger
、AtomicLong
)保证对单个变量的原子更新。
不可变对象设计
- 使用不可变对象,避免在并发场景下对对象状态的修改。
线程安全的集合
- 采用并发容器(如
ConcurrentHashMap
、CopyOnWriteArrayList
)管理共享数据,避免并发修改问题。
5. 如何设计高并发场景下的计数器
原子变量
- 使用
AtomicInteger
或AtomicLong
提供无锁计数操作,适用于简单的计数场景。
LongAdder/LongAccumulator
- 在高并发场景下,使用
LongAdder
(JDK8及以上)分散热点,多个计数单元并行累加,最后合并结果。
分段计数器
- 将计数器分为多个独立部分,每个线程或线程组维护各自的局部计数,定期合并成全局计数,降低锁竞争。
九、底层原理与扩展
1. Java 线程与操作系统线程的关系
- 映射关系:Java 线程是由 JVM 映射到底层操作系统线程,通常采用 1:1 模型,即每个 Java 线程对应一个操作系统线程。
- 操作系统调度:操作系统负责线程的调度、上下文切换等底层管理,JVM 则在此基础上提供高级的线程管理功能,如线程池和并发工具。
- 资源管理:底层线程由操作系统管理资源,Java 线程的创建、销毁和调度都依赖于操作系统的支持。
2. 什么是协程(Coroutine)?Java 中的虚拟线程(Loom 项目)
-
协程(Coroutine)
- 协程是一种轻量级的线程实现,可以在单个线程中实现多个任务之间的切换。
- 它通过保存和恢复执行上下文来实现并发执行,切换开销远低于操作系统线程的上下文切换。
- 协程通常由用户级库调度,不依赖于操作系统的线程管理。
-
Java 中的虚拟线程(Loom 项目)
- Project Loom 是 Java 的一个实验性项目,旨在引入虚拟线程这一概念,使得创建成千上万的线程成为可能。
- 虚拟线程基于协程思想,能够在用户空间内高效调度,极大降低线程切换和资源消耗。
- 该项目有望简化并发编程模型,使得编写高并发应用程序更加直观和高效。
3. 如何通过 jstack 分析线程状态
- 生成线程堆栈信息:使用命令
jstack <pid>
(其中<pid>
是 Java 进程的标识)获取当前线程的状态及堆栈信息。 - 分析线程状态:
- 查看各线程的状态,如
RUNNABLE
、WAITING
、BLOCKED
等。 - 检查线程等待锁和持有锁的信息,确定是否存在死锁或锁竞争问题。
- 根据堆栈信息定位长时间阻塞或运行缓慢的线程,帮助分析性能瓶颈或故障原因。
- 查看各线程的状态,如
4. 什么是伪共享(False Sharing)?如何避免?
-
伪共享定义:
- 当多个线程在不同的变量上操作时,如果这些变量恰好位于同一个 CPU 缓存行中,会引起频繁的缓存行失效和重载,进而影响性能。
-
避免方法:
- 内存填充(Padding):通过在变量之间添加无用的数据字段(例如额外的 long 型字段)来确保它们不在同一缓存行上。
- 使用 @Contended 注解:在 Java 8 及以上版本中,可以使用
@Contended
注解标记容易发生伪共享的变量(需在 JVM 启动参数中启用-XX:-RestrictContended
)。 - 合理的数据结构设计:在设计并发数据结构时,考虑将易冲突的变量分散到不同的缓存行中,减少互相影响。
十、其他扩展问题
1. 如何实现异步编程(如 CompletableFuture、Reactive Streams)?
CompletableFuture
CompletableFuture
是 Java 8 引入的异步编程工具,它支持链式操作,可以避免传统回调地狱(Callback Hell)。- 主要方法包括
supplyAsync()
、thenApply()
、thenAccept()
、thenCompose()
、handle()
等。
import java.util.concurrent.CompletableFuture;public class AsyncExample {public static void main(String[] args) {CompletableFuture.supplyAsync(() -> {return "Hello, World!";}).thenApply(result -> {return result + " - Processed";}).thenAccept(System.out::println);}
}
Reactive Streams
Reactive Streams
是一种基于发布-订阅模式的异步处理标准,支持 非阻塞 与 背压(Back Pressure) 机制。- 常见实现有 Project Reactor(Spring WebFlux)和 RxJava。
- 核心组件:
- Publisher(发布者):生产数据流。
- Subscriber(订阅者):消费数据流。
- Subscription(订阅):连接
Publisher
和Subscriber
,管理数据传输速率。 - Processor(处理器):既是
Publisher
也是Subscriber
,可对流数据进行处理。
2. 分布式锁与单机锁的区别
单机锁
- 仅用于单个 JVM 内部,保证线程间同步。
- 实现方式:
synchronized
关键字ReentrantLock
可重入锁
- 优点:实现简单、开销低。
- 缺点:无法跨进程或跨服务器同步。
分布式锁
- 用于多个进程或服务器之间的同步控制。
- 常见实现:
- 基于 Redis:如
SET NX EX
方式。 - 基于 ZooKeeper:利用
EPHEMERAL
(临时节点)实现锁。 - 基于数据库:使用数据库表中的唯一字段实现锁(效率较低)。
- 基于 Redis:如
- 优点:支持跨服务同步,防止分布式环境下的数据竞争。
- 缺点:实现复杂,需要考虑网络延迟、锁超时、锁丢失等问题。
import redis.clients.jedis.Jedis;public class RedisLock {private Jedis jedis;private String lockKey = "distributed_lock";public RedisLock(Jedis jedis) {this.jedis = jedis;}public boolean lock() {return "OK".equals(jedis.set(lockKey, "locked", "NX", "EX", 10));}public void unlock() {jedis.del(lockKey);}
}
3. 无锁编程(Lock-Free)的实现思路
CAS(Compare-And-Swap)
- CAS 操作:比较并交换,保证多个线程同时更新时的原子性。
- Java 中的无锁实现:
AtomicInteger
AtomicLong
AtomicReference
- 缺点:
- 可能出现 ABA 问题(即值改变了但看起来未变)。
- 高并发下可能导致 自旋过多,消耗 CPU。
import java.util.concurrent.atomic.AtomicInteger;public class CASExample {private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) {count.incrementAndGet(); // 使用 CAS 增加计数System.out.println("Counter: " + count.get());}
}
乐观锁
- 原理:不主动加锁,而是在更新前检查数据版本,若检测到冲突则重试。
- 实现方式:
- CAS(Compare-And-Swap) 操作
- 版本号机制(如数据库的
version
字段)
- 适用于:高并发、冲突较少的场景。
无锁数据结构
- 设计时避免使用锁,如 ConcurrentLinkedQueue 采用 CAS + 链表的方式更新数据。
- 示例:
ConcurrentLinkedQueue
LongAdder
(分段计数,减少锁竞争)CopyOnWriteArrayList
(写时复制)
4. 高并发场景下常见的性能优化手段
1. 减少锁竞争
- 使用 细粒度锁 或 分段锁 降低锁竞争。
- 无锁数据结构 代替同步容器(如
ConcurrentHashMap
)。 - 读写锁 代替互斥锁(
ReentrantReadWriteLock
)。
2. 线程池优化
- 合理配置线程池:
- CPU 密集型任务:线程数 = CPU 核心数 + 1
- IO 密集型任务:线程数 = CPU 核心数 * 2
- 避免频繁创建/销毁线程,实现线程复用。
3. 缓存机制
- 本地缓存(如
Caffeine
、Guava Cache
)。 - 分布式缓存(如
Redis
、Memcached
)。 - 读写分离、数据预热,减少数据库压力。
4. 异步 & 批量处理
- 采用消息队列(MQ),如
Kafka
、RabbitMQ
进行异步处理。 - 批量操作(如数据库批量插入
batch insert
)。
5. 数据结构优化
- 选择合适的数据结构,避免不必要的锁竞争:
ConcurrentHashMap
代替Hashtable
CopyOnWriteArrayList
代替ArrayList
(适用于读多写少的场景)
- 避免伪共享(False Sharing),通过 缓存行填充 提高 CPU 缓存命中率。