Java 并发面试题集
总题数: 62道 | 重点领域: 线程、锁、并发容器、JMM | 难度分布: 中级到高级
本文档整理了 Java 并发的完整62道面试题目,涵盖线程基础、锁机制、并发容器、内存模型等各个方面。
面试题目列表
1. 什么是 Java 中的线程同步?
答案:
线程同步是指协调多个线程对共享资源的访问,确保在同一时刻只有一个线程能够访问特定的资源,从而避免数据不一致和竞态条件。
主要同步机制:
- synchronized 关键字:提供互斥锁机制
- Lock 接口:提供更灵活的锁操作(ReentrantLock、ReadWriteLock)
- volatile 关键字:保证变量的可见性
- wait/notify 机制:线程间的协作通信
- 并发工具类:Semaphore、CountDownLatch、CyclicBarrier 等
1// synchronized 同步示例2public class Counter {3 private int count = 0;4 5 public synchronized void increment() {6 count++;7 }8 9 public synchronized int getCount() {10 return count;11 }12}2. Java 中的线程安全是什么意思?
答案:
线程安全是指当多个线程同时访问某个类、对象或方法时,这个类、对象或方法始终能表现出正确的行为,不会出现数据不一致或不可预期的结果。
线程安全的实现方式:
- 互斥同步(悲观锁):synchronized、ReentrantLock
- 非阻塞同步(乐观锁):CAS 操作、原子类
- 无同步方案:
- 栈封闭(局部变量)
- ThreadLocal
- 不可变对象(final、String)
线程安全级别:
- 不可变:String、Integer 等
- 绝对线程安全:所有操作都是线程安全的
- 相对线程安全:单次操作是线程安全的,如 Vector、HashTable
- 线程兼容:需要外部同步,如 ArrayList、HashMap
- 线程对立:无法在多线程环境使用
1// 线程安全的单例模式2public class Singleton {3 private static volatile Singleton instance;4 5 private Singleton() {}6 7 public static Singleton getInstance() {8 if (instance == null) {9 synchronized (Singleton.class) {10 if (instance == null) {11 instance = new Singleton();12 }13 }14 }15 return instance;16 }17}3. 什么是协程?Java 支持协程吗?
答案:
协程(Coroutine) 是一种轻量级的用户态线程,由程序自己控制调度,不需要操作系统内核参与。协程的切换开销远小于线程切换。
协程的特点:
- 轻量级:占用内存小,创建销毁快
- 用户态调度:不需要内核态切换
- 协作式调度:由程序控制何时挂起和恢复
- 高并发:可以创建大量协程
Java 对协程的支持:
传统 Java(Java 8-18)不直接支持协程,但可以通过以下方式实现类似功能:
- Quasar 库:提供 Fiber(纤程)实现
- Kotlin 协程:在 JVM 上运行的协程
- Project Loom:Java 19+ 引入的虚拟线程(Virtual Threads)
1// Java 19+ 虚拟线程示例2try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {3 IntStream.range(0, 10_000).forEach(i -> {4 executor.submit(() -> {5 Thread.sleep(Duration.ofSeconds(1));6 return i;7 });8 });9}4. 线程的生命周期在 Java 中是如何定义的?
答案:
Java 线程有 6 种状态,定义在 Thread.State 枚举中:
1. NEW(新建)
- 线程被创建但还未调用 start() 方法
2. RUNNABLE(可运行)
- 调用 start() 后,线程可能正在运行或等待 CPU 时间片
- 包含操作系统的 Ready 和 Running 状态
3. BLOCKED(阻塞)
- 线程等待获取监视器锁(synchronized)
4. WAITING(等待)
- 无限期等待另一个线程执行特定操作
- 触发条件:Object.wait()、Thread.join()、LockSupport.park()
5. TIMED_WAITING(计时等待)
- 等待指定时间后自动返回
- 触发条件:Thread.sleep()、Object.wait(timeout)、Thread.join(timeout)
6. TERMINATED(终止)
- 线程执行完成或异常退出
1// 状态转换示例2public class ThreadStateDemo {3 public static void main(String[] args) throws InterruptedException {4 Thread thread = new Thread(() -> {5 try {6 Thread.sleep(1000); // TIMED_WAITING7 synchronized (ThreadStateDemo.class) {8 ThreadStateDemo.class.wait(); // WAITING9 }10 } catch (InterruptedException e) {11 e.printStackTrace();12 }13 });14 15 System.out.println("NEW: " + thread.getState()); // NEW16 thread.start();17 Thread.sleep(100);18 System.out.println("RUNNABLE: " + thread.getState()); // RUNNABLE19 Thread.sleep(1000);20 System.out.println("TIMED_WAITING: " + thread.getState()); // TIMED_WAITING21 }22}5. Java 中线程之间如何进行通信?
答案:
Java 提供了多种线程间通信机制:
1. wait/notify/notifyAll 机制
1public class WaitNotifyDemo {2 private static final Object lock = new Object();3 4 public void producer() throws InterruptedException {5 synchronized (lock) {6 System.out.println("生产数据");7 lock.notify(); // 唤醒等待线程8 }9 }10 11 public void consumer() throws InterruptedException {12 synchronized (lock) {13 lock.wait(); // 等待通知14 System.out.println("消费数据");15 }16 }17}2. volatile 共享变量
1private volatile boolean flag = false;23// 线程14flag = true;56// 线程27while (!flag) {8 // 等待9}3. CountDownLatch
1CountDownLatch latch = new CountDownLatch(3);2// 线程完成后调用3latch.countDown();4// 主线程等待5latch.await();4. CyclicBarrier
1CyclicBarrier barrier = new CyclicBarrier(3, () -> {2 System.out.println("所有线程到达屏障");3});4barrier.await();5. Semaphore 信号量
1Semaphore semaphore = new Semaphore(3);2semaphore.acquire(); // 获取许可3semaphore.release(); // 释放许可6. BlockingQueue 阻塞队列
1BlockingQueue<String> queue = new LinkedBlockingQueue<>();2queue.put("data"); // 生产者3String data = queue.take(); // 消费者7. Condition 条件变量
1Lock lock = new ReentrantLock();2Condition condition = lock.newCondition();3condition.await(); // 等待4condition.signal(); // 唤醒6. Java 中如何创建多线程?
答案:
Java 创建线程有 4 种主要方式:
1. 继承 Thread 类
1public class MyThread extends Thread {2 @Override3 public void run() {4 System.out.println("Thread running");5 }6}78// 使用9MyThread thread = new MyThread();10thread.start();2. 实现 Runnable 接口
1public class MyRunnable implements Runnable {2 @Override3 public void run() {4 System.out.println("Runnable running");5 }6}78// 使用9Thread thread = new Thread(new MyRunnable());10thread.start();1112// Lambda 方式13new Thread(() -> System.out.println("Lambda")).start();3. 实现 Callable 接口(有返回值)
1public class MyCallable implements Callable<String> {2 @Override3 public String call() throws Exception {4 return "Callable result";5 }6}78// 使用9FutureTask<String> futureTask = new FutureTask<>(new MyCallable());10new Thread(futureTask).start();11String result = futureTask.get(); // 获取返回值4. 使用线程池
1ExecutorService executor = Executors.newFixedThreadPool(5);23// 提交 Runnable4executor.submit(() -> System.out.println("Task"));56// 提交 Callable7Future<String> future = executor.submit(() -> "Result");8String result = future.get();910executor.shutdown();推荐方式:
- 实现 Runnable/Callable 接口(避免单继承限制)
- 使用线程池(更好的资源管理和性能)
7. 你了解 Java 线程池的原理吗?
答案:
线程池核心参数(ThreadPoolExecutor):
1public ThreadPoolExecutor(2 int corePoolSize, // 核心线程数3 int maximumPoolSize, // 最大线程数4 long keepAliveTime, // 空闲线程存活时间5 TimeUnit unit, // 时间单位6 BlockingQueue<Runnable> workQueue, // 任务队列7 ThreadFactory threadFactory, // 线程工厂8 RejectedExecutionHandler handler // 拒绝策略9)执行流程:
- 提交任务时,如果线程数 < corePoolSize,创建新线程执行
- 如果线程数 >= corePoolSize,任务放入队列
- 如果队列满了且线程数 < maximumPoolSize,创建新线程
- 如果线程数 >= maximumPoolSize 且队列满了,执行拒绝策略
1提交任务2 ↓3线程数 < corePoolSize? → 是 → 创建核心线程执行4 ↓ 否5队列未满? → 是 → 加入队列等待6 ↓ 否7线程数 < maximumPoolSize? → 是 → 创建非核心线程执行8 ↓ 否9执行拒绝策略示例:
1ThreadPoolExecutor executor = new ThreadPoolExecutor(2 5, // 核心线程数3 10, // 最大线程数4 60L, // 空闲线程存活时间5 TimeUnit.SECONDS,6 new LinkedBlockingQueue<>(100), // 队列容量7 Executors.defaultThreadFactory(),8 new ThreadPoolExecutor.AbortPolicy() // 拒绝策略9);线程池状态:
- RUNNING:接受新任务并处理队列任务
- SHUTDOWN:不接受新任务,但处理队列任务
- STOP:不接受新任务,不处理队列任务,中断正在执行的任务
- TIDYING:所有任务已终止
- TERMINATED:terminated() 方法执行完成
8. 如何合理地设置 Java 线程池的线程数?
答案:
线程数设置需要根据任务类型和系统资源来决定:
1. CPU 密集型任务
- 特点:大量计算,很少 I/O 操作
- 推荐线程数:CPU 核心数 + 1
- 原因:避免过多线程切换开销
1int cpuCount = Runtime.getRuntime().availableProcessors();2int threadCount = cpuCount + 1;2. I/O 密集型任务
- 特点:大量网络、磁盘 I/O 操作
- 推荐线程数:CPU 核心数 × 2 或 CPU 核心数 / (1 - 阻塞系数)
- 阻塞系数:0.8-0.9 之间
1int cpuCount = Runtime.getRuntime().availableProcessors();2// 方式13int threadCount = cpuCount * 2;45// 方式2(更精确)6double blockingCoefficient = 0.9; // I/O 时间占比7int threadCount = (int) (cpuCount / (1 - blockingCoefficient));3. 混合型任务
- 可以拆分为 CPU 密集和 I/O 密集,分别使用不同线程池
4. 实际考虑因素:
- 系统内存大小(每个线程占用内存)
- 任务执行时间
- 任务优先级
- 系统负载情况
动态调整示例:
1ThreadPoolExecutor executor = new ThreadPoolExecutor(2 10, 50, 60L, TimeUnit.SECONDS,3 new LinkedBlockingQueue<>(1000)4);56// 运行时动态调整7executor.setCorePoolSize(20);8executor.setMaximumPoolSize(100);最佳实践:
- 通过压测确定最优值
- 监控线程池运行状态(活跃线程数、队列大小)
- 根据实际情况动态调整
9. Java 线程池有哪些拒绝策略?
答案:
Java 提供了 4 种内置拒绝策略(RejectedExecutionHandler):
1. AbortPolicy(默认)
- 直接抛出 RejectedExecutionException 异常
- 适用于关键任务,不允许丢失
1new ThreadPoolExecutor.AbortPolicy()2// 抛出异常,调用者需要处理2. CallerRunsPolicy
- 由调用线程(提交任务的线程)执行该任务
- 适用于不允许任务丢失,且可以降低提交速度的场景
1new ThreadPoolExecutor.CallerRunsPolicy()2// 主线程执行任务,降低提交速度3. DiscardPolicy
- 静默丢弃任务,不抛出异常
- 适用于允许任务丢失的场景
1new ThreadPoolExecutor.DiscardPolicy()2// 直接丢弃,不做任何处理4. DiscardOldestPolicy
- 丢弃队列中最老的任务,然后重新提交当前任务
- 适用于任务有时效性的场景
1new ThreadPoolExecutor.DiscardOldestPolicy()2// 丢弃队列头部任务,重试当前任务自定义拒绝策略:
1public class CustomRejectedHandler implements RejectedExecutionHandler {2 @Override3 public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {4 // 记录日志5 log.warn("Task rejected: {}", r.toString());6 7 // 保存到数据库或消息队列8 saveToDatabase(r);9 10 // 或者使用备用线程池11 backupExecutor.execute(r);12 }13}选择建议:
- 关键业务:AbortPolicy + 异常处理
- 可降级业务:CallerRunsPolicy
- 允许丢失:DiscardPolicy
- 时效性任务:DiscardOldestPolicy
10. Java 并发库中提供了哪些线程池实现?它们有什么区别?
答案:
Executors 工厂类提供了几种常用线程池:
1. FixedThreadPool(固定大小线程池)
1ExecutorService executor = Executors.newFixedThreadPool(5);2// 等价于3new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,4 new LinkedBlockingQueue<Runnable>());- 核心线程数 = 最大线程数
- 使用无界队列 LinkedBlockingQueue
- 适用于负载较重的服务器,限制线程数
2. CachedThreadPool(缓存线程池)
1ExecutorService executor = Executors.newCachedThreadPool();2// 等价于3new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,4 new SynchronousQueue<Runnable>());- 核心线程数为 0,最大线程数无限
- 使用 SynchronousQueue(不存储元素)
- 线程空闲 60 秒后回收
- 适用于执行大量短期异步任务
3. SingleThreadExecutor(单线程线程池)
1ExecutorService executor = Executors.newSingleThreadExecutor();2// 等价于3new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,4 new LinkedBlockingQueue<Runnable>());- 只有一个线程
- 保证任务按顺序执行
- 适用于需要顺序执行的场景
4. ScheduledThreadPool(定时任务线程池)
1ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);- 支持定时和周期性任务执行
- 使用 DelayedWorkQueue
1// 延迟执行2executor.schedule(task, 5, TimeUnit.SECONDS);34// 固定频率执行5executor.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);67// 固定延迟执行8executor.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);5. WorkStealingPool(工作窃取线程池,Java 8+)
1ExecutorService executor = Executors.newWorkStealingPool();- 基于 ForkJoinPool 实现
- 每个线程有自己的任务队列
- 空闲线程可以"窃取"其他线程的任务
- 适用于任务执行时间不均匀的场景
对比总结:
| 类型 | 核心线程数 | 最大线程数 | 队列 | 适用场景 |
|---|---|---|---|---|
| FixedThreadPool | n | n | 无界队列 | 负载较重的服务器 |
| CachedThreadPool | 0 | 无限 | SynchronousQueue | 大量短期任务 |
| SingleThreadExecutor | 1 | 1 | 无界队列 | 顺序执行任务 |
| ScheduledThreadPool | n | Integer.MAX_VALUE | DelayedWorkQueue | 定时任务 |
| WorkStealingPool | CPU核心数 | - | 多队列 | 任务时间不均 |
注意: 阿里巴巴开发手册不推荐使用 Executors 创建线程池,建议手动创建 ThreadPoolExecutor,明确参数,避免资源耗尽风险。
11. Java 线程池核心线程数在运行过程中能修改吗?如何修改?
答案:
可以修改。ThreadPoolExecutor 提供了动态调整线程池参数的方法:
修改核心线程数:
1ThreadPoolExecutor executor = new ThreadPoolExecutor(2 5, 10, 60L, TimeUnit.SECONDS,3 new LinkedBlockingQueue<>(100)4);56// 动态修改核心线程数7executor.setCorePoolSize(10);89// 获取当前核心线程数10int coreSize = executor.getCorePoolSize();修改最大线程数:
1executor.setMaximumPoolSize(20);修改空闲线程存活时间:
1executor.setKeepAliveTime(120L, TimeUnit.SECONDS);注意事项:
- 增加核心线程数:会立即创建新线程(如果有任务在等待)
- 减少核心线程数:多余的核心线程会在空闲后被回收
- 最大线程数必须 >= 核心线程数:否则抛出 IllegalArgumentException
动态调整场景示例:
1public class DynamicThreadPool {2 private ThreadPoolExecutor executor;3 4 public void adjustByLoad() {5 int queueSize = executor.getQueue().size();6 int activeCount = executor.getActiveCount();7 8 // 根据队列大小和活跃线程数动态调整9 if (queueSize > 100 && activeCount < executor.getMaximumPoolSize()) {10 executor.setCorePoolSize(executor.getCorePoolSize() + 5);11 } else if (queueSize < 10 && executor.getCorePoolSize() > 5) {12 executor.setCorePoolSize(executor.getCorePoolSize() - 5);13 }14 }15}监控线程池状态:
1// 当前线程池大小2int poolSize = executor.getPoolSize();34// 活跃线程数5int activeCount = executor.getActiveCount();67// 队列中任务数8int queueSize = executor.getQueue().size();910// 已完成任务数11long completedTaskCount = executor.getCompletedTaskCount();1213// 总任务数14long taskCount = executor.getTaskCount();12. 线程池中 shutdown 与 shutdownNow 的区别是什么?
答案:
shutdown() 方法:
- 温和关闭,不接受新任务
- 继续执行队列中的任务
- 等待所有任务执行完成
- 不会中断正在执行的任务
1executor.shutdown();23// 等待所有任务完成4try {5 if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {6 executor.shutdownNow(); // 超时后强制关闭7 }8} catch (InterruptedException e) {9 executor.shutdownNow();10}shutdownNow() 方法:
- 立即关闭,不接受新任务
- 尝试停止所有正在执行的任务(发送中断信号)
- 不再执行队列中等待的任务
- 返回等待执行的任务列表
1List<Runnable> notExecutedTasks = executor.shutdownNow();2System.out.println("未执行的任务数: " + notExecutedTasks.size());对比表:
| 特性 | shutdown() | shutdownNow() |
|---|---|---|
| 接受新任务 | 否 | 否 |
| 执行队列任务 | 是 | 否 |
| 中断运行任务 | 否 | 是(发送中断) |
| 返回值 | void | List<Runnable> |
| 关闭速度 | 慢(等待完成) | 快(立即停止) |
完整关闭示例:
1public void shutdownGracefully(ExecutorService executor) {2 executor.shutdown(); // 禁止新任务提交3 4 try {5 // 等待 60 秒让任务完成6 if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {7 // 超时后强制关闭8 List<Runnable> droppedTasks = executor.shutdownNow();9 System.out.println("强制关闭,丢弃任务: " + droppedTasks.size());10 11 // 再等待 60 秒12 if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {13 System.err.println("线程池未能正常关闭");14 }15 }16 } catch (InterruptedException e) {17 // 当前线程被中断,强制关闭线程池18 executor.shutdownNow();19 Thread.currentThread().interrupt();20 }21}注意事项:
- shutdownNow() 不保证能停止正在执行的任务(任务需要响应中断)
- 任务应该正确处理 InterruptedException
- 使用 awaitTermination() 等待线程池真正终止
13. 线程池内部任务出异常后,如何知道是哪个线程出了异常?
答案:
线程池任务异常处理有多种方式:
1. 使用 Future.get() 捕获异常
1ExecutorService executor = Executors.newFixedThreadPool(5);23Future<?> future = executor.submit(() -> {4 throw new RuntimeException("任务异常");5});67try {8 future.get(); // 会抛出 ExecutionException9} catch (ExecutionException e) {10 Throwable cause = e.getCause();11 System.out.println("任务异常: " + cause.getMessage());12}2. 在任务内部捕获异常
1executor.submit(() -> {2 try {3 // 任务逻辑4 int result = 1 / 0;5 } catch (Exception e) {6 System.out.println("线程: " + Thread.currentThread().getName() 7 + " 异常: " + e.getMessage());8 // 记录日志或上报9 }10});3. 自定义 ThreadFactory 设置 UncaughtExceptionHandler
1ThreadFactory factory = new ThreadFactory() {2 private AtomicInteger threadNumber = new AtomicInteger(1);3 4 @Override5 public Thread newThread(Runnable r) {6 Thread thread = new Thread(r);7 thread.setName("MyPool-" + threadNumber.getAndIncrement());8 9 // 设置未捕获异常处理器10 thread.setUncaughtExceptionHandler((t, e) -> {11 System.out.println("线程 " + t.getName() + " 异常: " + e.getMessage());12 // 记录日志、发送告警13 });14 15 return thread;16 }17};1819ThreadPoolExecutor executor = new ThreadPoolExecutor(20 5, 10, 60L, TimeUnit.SECONDS,21 new LinkedBlockingQueue<>(),22 factory23);4. 重写 ThreadPoolExecutor.afterExecute() 方法
1public class CustomThreadPoolExecutor extends ThreadPoolExecutor {2 3 public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize,4 long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {5 super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);6 }7 8 @Override9 protected void afterExecute(Runnable r, Throwable t) {10 super.afterExecute(r, t);11 12 if (t == null && r instanceof Future<?>) {13 try {14 Future<?> future = (Future<?>) r;15 if (future.isDone()) {16 future.get();17 }18 } catch (ExecutionException e) {19 t = e.getCause();20 } catch (InterruptedException e) {21 Thread.currentThread().interrupt();22 }23 }24 25 if (t != null) {26 System.out.println("任务执行异常: " + t.getMessage());27 // 记录日志、发送告警28 }29 }30}5. 使用 CompletableFuture 处理异常
1CompletableFuture.supplyAsync(() -> {2 // 任务逻辑3 return 1 / 0;4}, executor).exceptionally(ex -> {5 System.out.println("异常处理: " + ex.getMessage());6 return null;7});最佳实践:
- submit() 提交的任务使用 Future.get() 获取异常
- execute() 提交的任务使用 UncaughtExceptionHandler
- 重要任务在内部 try-catch 处理异常
- 记录详细的异常信息(线程名、时间、堆栈)
14. Java 中的 DelayQueue 和 ScheduledThreadPool 有什么区别?
答案:
DelayQueue:
- 是一个阻塞队列,存储 Delayed 元素
- 只有到期的元素才能被取出
- 需要手动从队列中取元素并处理
- 更灵活,可以自定义处理逻辑
1public class DelayedTask implements Delayed {2 private String name;3 private long delayTime; // 延迟时间(毫秒)4 private long expire; // 到期时间5 6 public DelayedTask(String name, long delayTime) {7 this.name = name;8 this.delayTime = delayTime;9 this.expire = System.currentTimeMillis() + delayTime;10 }11 12 @Override13 public long getDelay(TimeUnit unit) {14 return unit.convert(expire - System.currentTimeMillis(), 15 TimeUnit.MILLISECONDS);16 }17 18 @Override19 public int compareTo(Delayed o) {20 return Long.compare(this.expire, ((DelayedTask) o).expire);21 }22}2324// 使用25DelayQueue<DelayedTask> queue = new DelayQueue<>();26queue.put(new DelayedTask("task1", 5000)); // 5秒后到期2728// 手动取出并处理29DelayedTask task = queue.take(); // 阻塞直到有元素到期30System.out.println("执行任务: " + task.getName());ScheduledThreadPool:
- 是一个线程池,专门用于执行定时任务
- 自动调度和执行任务
- 支持固定延迟和固定频率执行
- 内部使用 DelayedWorkQueue
1ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);23// 延迟执行(一次性)4scheduler.schedule(() -> {5 System.out.println("5秒后执行");6}, 5, TimeUnit.SECONDS);78// 固定频率执行(周期性)9scheduler.scheduleAtFixedRate(() -> {10 System.out.println("每秒执行一次");11}, 0, 1, TimeUnit.SECONDS);1213// 固定延迟执行(周期性)14scheduler.scheduleWithFixedDelay(() -> {15 System.out.println("上次执行完成后延迟1秒再执行");16}, 0, 1, TimeUnit.SECONDS);主要区别:
| 特性 | DelayQueue | ScheduledThreadPool |
|---|---|---|
| 类型 | 阻塞队列 | 线程池 |
| 自动执行 | 否,需手动取出 | 是,自动调度执行 |
| 周期任务 | 不支持 | 支持 |
| 线程管理 | 需自己管理 | 自动管理 |
| 使用复杂度 | 较高 | 较低 |
| 灵活性 | 高 | 中 |
| 适用场景 | 自定义调度逻辑 | 标准定时任务 |
scheduleAtFixedRate vs scheduleWithFixedDelay:
1// scheduleAtFixedRate: 固定频率2// 如果任务执行时间 < 间隔时间,按固定频率执行3// 如果任务执行时间 > 间隔时间,任务执行完立即开始下一次4scheduler.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);5// 0s -> 1s -> 2s -> 3s (理想情况)67// scheduleWithFixedDelay: 固定延迟8// 上次任务执行完成后,延迟指定时间再执行9scheduler.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);10// 0s -> 任务完成(0.5s) -> 1.5s -> 任务完成(2s) -> 3s选择建议:
- 简单定时任务:使用 ScheduledThreadPool
- 复杂调度逻辑:使用 DelayQueue
- 需要周期执行:使用 ScheduledThreadPool
- 需要动态调整延迟:使用 DelayQueue
15. 什么是 Java 的 Timer?
答案:
Timer 是 Java 早期提供的定时任务工具类,使用单线程执行所有定时任务。
1Timer timer = new Timer();23// 延迟执行4timer.schedule(new TimerTask() {5 @Override6 public void run() {7 System.out.println("5秒后执行");8 }9}, 5000);1011// 周期执行12timer.scheduleAtFixedRate(new TimerTask() {13 @Override14 public void run() {15 System.out.println("每秒执行");16 }17}, 0, 1000);Timer 的缺点:
- 单线程执行:所有任务串行执行,一个任务延迟会影响其他任务
- 异常处理差:任务抛出异常会导致整个 Timer 终止
- 不支持绝对时间:基于相对时间,系统时间改变会影响调度
- 功能有限:不支持线程池、返回值等
1// 问题示例:一个任务异常导致 Timer 终止2Timer timer = new Timer();3timer.schedule(new TimerTask() {4 @Override5 public void run() {6 throw new RuntimeException("异常");7 }8}, 1000);910timer.schedule(new TimerTask() {11 @Override12 public void run() {13 System.out.println("这个任务不会执行");14 }15}, 2000);替代方案:
- 使用 ScheduledExecutorService(推荐)
- 使用 Quartz 等专业调度框架
- 使用 Spring 的 @Scheduled 注解
16. 你了解时间轮(Time Wheel)吗?有哪些应用场景?
答案:
时间轮(Time Wheel) 是一种高效的定时器算法,用于管理大量定时任务。
原理:
- 类似时钟,将时间分成多个槽位(slot)
- 每个槽位存储该时间点要执行的任务
- 指针按固定频率移动,执行当前槽位的任务
- 支持多层时间轮(秒、分、时)
1时间轮示意图:2 03 7 146 25 5 36 478每个槽位存储任务链表9指针每秒移动一格简单实现:
1public class SimpleTimeWheel {2 private List<Set<Task>> wheel;3 private int tickDuration; // 每格时间(毫秒)4 private int wheelSize; // 轮子大小5 private int currentTick; // 当前指针位置6 7 public SimpleTimeWheel(int tickDuration, int wheelSize) {8 this.tickDuration = tickDuration;9 this.wheelSize = wheelSize;10 this.wheel = new ArrayList<>(wheelSize);11 for (int i = 0; i < wheelSize; i++) {12 wheel.add(new HashSet<>());13 }14 }15 16 public void addTask(Task task, long delayMs) {17 int ticks = (int) (delayMs / tickDuration);18 int index = (currentTick + ticks) % wheelSize;19 wheel.get(index).add(task);20 }21 22 public void tick() {23 Set<Task> tasks = wheel.get(currentTick);24 tasks.forEach(Task::run);25 tasks.clear();26 currentTick = (currentTick + 1) % wheelSize;27 }28}优点:
- 添加/删除任务时间复杂度 O(1)
- 适合大量定时任务
- 内存占用可控
缺点:
- 精度受限于 tick 间隔
- 不适合长时间延迟(需要多层时间轮)
应用场景:
- Netty 的 HashedWheelTimer
1HashedWheelTimer timer = new HashedWheelTimer(2 100, TimeUnit.MILLISECONDS, // tick 间隔3 512 // 轮子大小4);56timer.newTimeout(timeout -> {7 System.out.println("任务执行");8}, 5, TimeUnit.SECONDS);- Kafka 的延迟操作
- Dubbo 的超时检测
- 连接超时管理
- 会话过期检测
与其他定时器对比:
| 方案 | 添加任务 | 删除任务 | 适用场景 |
|---|---|---|---|
| Timer | O(log n) | O(n) | 少量任务 |
| ScheduledThreadPool | O(log n) | O(log n) | 中等任务 |
| 时间轮 | O(1) | O(1) | 大量任务 |
17. 你使用过哪些 Java 并发工具类?
答案:
Java 并发包(java.util.concurrent)提供了丰富的并发工具类:
1. 同步工具类
CountDownLatch(倒计数门栓)
1CountDownLatch latch = new CountDownLatch(3);23// 工作线程4new Thread(() -> {5 System.out.println("任务1完成");6 latch.countDown();7}).start();89// 主线程等待10latch.await(); // 等待计数归零11System.out.println("所有任务完成");CyclicBarrier(循环栅栏)
1CyclicBarrier barrier = new CyclicBarrier(3, () -> {2 System.out.println("所有线程到达屏障");3});45// 每个线程6barrier.await(); // 等待其他线程Semaphore(信号量)
1Semaphore semaphore = new Semaphore(3); // 3个许可23semaphore.acquire(); // 获取许可4try {5 // 执行任务6} finally {7 semaphore.release(); // 释放许可8}Exchanger(交换器)
1Exchanger<String> exchanger = new Exchanger<>();23// 线程14String data1 = exchanger.exchange("数据1");56// 线程27String data2 = exchanger.exchange("数据2");2. 并发容器
1// 线程安全的 Map2ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();34// 线程安全的 List(读多写少)5CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();67// 线程安全的 Set8CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();910// 阻塞队列11BlockingQueue<String> queue = new LinkedBlockingQueue<>();3. 原子类
1// 原子整数2AtomicInteger count = new AtomicInteger(0);3count.incrementAndGet();45// 原子引用6AtomicReference<User> userRef = new AtomicReference<>();7userRef.compareAndSet(oldUser, newUser);89// 原子数组10AtomicIntegerArray array = new AtomicIntegerArray(10);1112// 原子字段更新器13AtomicIntegerFieldUpdater<User> updater = 14 AtomicIntegerFieldUpdater.newUpdater(User.class, "age");4. 锁
1// 可重入锁2ReentrantLock lock = new ReentrantLock();34// 读写锁5ReadWriteLock rwLock = new ReentrantReadWriteLock();67// 邮戳锁(Java 8+)8StampedLock stampedLock = new StampedLock();5. 异步编程
1// CompletableFuture2CompletableFuture<String> future = CompletableFuture3 .supplyAsync(() -> "Hello")4 .thenApply(s -> s + " World");56// ForkJoinPool7ForkJoinPool pool = new ForkJoinPool();6. 线程池
1// 标准线程池2ExecutorService executor = Executors.newFixedThreadPool(5);34// 定时线程池5ScheduledExecutorService scheduler = 6 Executors.newScheduledThreadPool(5);常用场景总结:
- 等待多个任务完成:CountDownLatch
- 多线程协同工作:CyclicBarrier
- 限流控制:Semaphore
- 线程安全集合:ConcurrentHashMap、CopyOnWriteArrayList
- 原子操作:AtomicInteger、AtomicReference
- 异步编程:CompletableFuture
18. 什么是 Java 的 Semaphore?
答案:
Semaphore(信号量)是一个计数信号量,用于控制同时访问特定资源的线程数量。
核心方法:
acquire():获取许可,如果没有可用许可则阻塞release():释放许可tryAcquire():尝试获取许可,立即返回availablePermits():返回可用许可数
基本使用:
1// 创建3个许可的信号量2Semaphore semaphore = new Semaphore(3);34public void accessResource() {5 try {6 semaphore.acquire(); // 获取许可7 System.out.println(Thread.currentThread().getName() + " 访问资源");8 Thread.sleep(2000);9 } catch (InterruptedException e) {10 e.printStackTrace();11 } finally {12 semaphore.release(); // 释放许可13 }14}应用场景:
1. 限流控制
1public class RateLimiter {2 private Semaphore semaphore;3 4 public RateLimiter(int maxConcurrent) {5 this.semaphore = new Semaphore(maxConcurrent);6 }7 8 public void execute(Runnable task) {9 try {10 semaphore.acquire();11 task.run();12 } catch (InterruptedException e) {13 Thread.currentThread().interrupt();14 } finally {15 semaphore.release();16 }17 }18}2. 数据库连接池
1public class ConnectionPool {2 private Semaphore semaphore;3 private List<Connection> connections;4 5 public ConnectionPool(int size) {6 this.semaphore = new Semaphore(size);7 this.connections = new ArrayList<>(size);8 // 初始化连接9 }10 11 public Connection getConnection() throws InterruptedException {12 semaphore.acquire();13 return connections.remove(0);14 }15 16 public void releaseConnection(Connection conn) {17 connections.add(conn);18 semaphore.release();19 }20}公平性:
1// 非公平模式(默认):性能更好2Semaphore semaphore = new Semaphore(3);34// 公平模式:按照请求顺序获取许可5Semaphore fairSemaphore = new Semaphore(3, true);19. 什么是 Java 的 CyclicBarrier?
答案:
CyclicBarrier(循环栅栏)是一个同步辅助类,让一组线程相互等待,直到所有线程都到达某个公共屏障点。
特点:
- 可循环使用(Cyclic)
- 所有线程到达屏障点后,可以执行一个可选的屏障动作
- 适用于多线程协同工作的场景
基本使用:
1CyclicBarrier barrier = new CyclicBarrier(3, () -> {2 System.out.println("所有线程都到达屏障,执行汇总操作");3});45// 每个线程6public void doWork() {7 try {8 System.out.println(Thread.currentThread().getName() + " 开始工作");9 Thread.sleep(1000);10 System.out.println(Thread.currentThread().getName() + " 完成工作,等待其他线程");11 12 barrier.await(); // 等待其他线程13 14 System.out.println(Thread.currentThread().getName() + " 继续执行");15 } catch (InterruptedException | BrokenBarrierException e) {16 e.printStackTrace();17 }18}应用场景:
1. 多线程计算后汇总
1public class ParallelCalculation {2 private CyclicBarrier barrier;3 private int[] results;4 5 public ParallelCalculation(int threadCount) {6 this.results = new int[threadCount];7 this.barrier = new CyclicBarrier(threadCount, () -> {8 // 所有线程计算完成,汇总结果9 int sum = Arrays.stream(results).sum();10 System.out.println("总和: " + sum);11 });12 }13 14 public void calculate(int index) {15 try {16 // 执行计算17 results[index] = index * 10;18 19 // 等待其他线程20 barrier.await();21 } catch (Exception e) {22 e.printStackTrace();23 }24 }25}2. 多阶段任务
1CyclicBarrier barrier = new CyclicBarrier(3);23for (int phase = 0; phase < 3; phase++) {4 final int currentPhase = phase;5 for (int i = 0; i < 3; i++) {6 new Thread(() -> {7 System.out.println("阶段" + currentPhase + " 执行");8 try {9 barrier.await(); // 等待本阶段所有线程完成10 } catch (Exception e) {11 e.printStackTrace();12 }13 }).start();14 }15}重要方法:
1// 等待其他线程2barrier.await();34// 带超时的等待5barrier.await(10, TimeUnit.SECONDS);67// 重置屏障8barrier.reset();910// 获取等待线程数11int waiting = barrier.getNumberWaiting();1213// 检查屏障是否被破坏14boolean broken = barrier.isBroken();20. 什么是 Java 的 CountDownLatch?
答案:
CountDownLatch(倒计数门栓)是一个同步辅助类,允许一个或多个线程等待其他线程完成操作。
特点:
- 基于计数器,初始化时设置计数值
- 调用
countDown()减少计数 - 调用
await()阻塞直到计数归零 - 一次性使用,不能重置
基本使用:
1CountDownLatch latch = new CountDownLatch(3);23// 工作线程4for (int i = 0; i < 3; i++) {5 new Thread(() -> {6 System.out.println(Thread.currentThread().getName() + " 执行任务");7 try {8 Thread.sleep(1000);9 } catch (InterruptedException e) {10 e.printStackTrace();11 }12 latch.countDown(); // 计数减113 }).start();14}1516// 主线程等待17latch.await(); // 阻塞直到计数归零18System.out.println("所有任务完成");应用场景:
1. 等待多个线程完成初始化
1public class ApplicationStartup {2 private CountDownLatch latch;3 4 public void start() throws InterruptedException {5 latch = new CountDownLatch(3);6 7 // 初始化数据库8 new Thread(() -> {9 System.out.println("初始化数据库");10 latch.countDown();11 }).start();12 13 // 初始化缓存14 new Thread(() -> {15 System.out.println("初始化缓存");16 latch.countDown();17 }).start();18 19 // 初始化配置20 new Thread(() -> {21 System.out.println("初始化配置");22 latch.countDown();23 }).start();24 25 // 等待所有初始化完成26 latch.await();27 System.out.println("应用启动完成");28 }29}2. 并行任务执行
1public class ParallelTask {2 public void executeParallel(List<Runnable> tasks) throws InterruptedException {3 CountDownLatch latch = new CountDownLatch(tasks.size());4 5 for (Runnable task : tasks) {6 new Thread(() -> {7 try {8 task.run();9 } finally {10 latch.countDown();11 }12 }).start();13 }14 15 latch.await(); // 等待所有任务完成16 }17}3. 模拟并发测试
1public class ConcurrencyTest {2 public void testConcurrency(int threadCount) throws InterruptedException {3 CountDownLatch startLatch = new CountDownLatch(1);4 CountDownLatch endLatch = new CountDownLatch(threadCount);5 6 for (int i = 0; i < threadCount; i++) {7 new Thread(() -> {8 try {9 startLatch.await(); // 等待开始信号10 // 执行测试11 System.out.println("执行测试");12 } catch (InterruptedException e) {13 e.printStackTrace();14 } finally {15 endLatch.countDown();16 }17 }).start();18 }19 20 startLatch.countDown(); // 发出开始信号,所有线程同时开始21 endLatch.await(); // 等待所有线程完成22 System.out.println("测试完成");23 }24}CountDownLatch vs CyclicBarrier:
| 特性 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 可重用性 | 不可重用 | 可重用 |
| 等待方式 | 一个或多个线程等待 | 所有线程相互等待 |
| 计数方式 | 递减到0 | 递增到N |
| 屏障动作 | 无 | 有 |
| 使用场景 | 主线程等待子线程 | 多线程协同 |
21. 什么是 Java 的 StampedLock?
22. 什么是 Java 的 CompletableFuture?
23. 什么是 Java 的 ForkJoinPool?
24. 如何在 Java 中控制多个线程的执行顺序?
25. 你使用过 Java 中的哪些阻塞队列?
26. 你使用过 Java 中的哪些原子类?
答案:
Java 提供了 java.util.concurrent.atomic 包,包含多种原子类,保证操作的原子性。
1. 基本类型原子类
1// AtomicInteger2AtomicInteger count = new AtomicInteger(0);3count.incrementAndGet(); // i++4count.getAndIncrement(); // ++i5count.addAndGet(5); // i += 56count.compareAndSet(0, 1); // CAS操作78// AtomicLong9AtomicLong longValue = new AtomicLong(0L);1011// AtomicBoolean12AtomicBoolean flag = new AtomicBoolean(false);13flag.compareAndSet(false, true);2. 数组类型原子类
1// AtomicIntegerArray2AtomicIntegerArray array = new AtomicIntegerArray(10);3array.incrementAndGet(0); // 数组索引0的元素+14array.compareAndSet(0, 1, 2); // CAS操作56// AtomicLongArray7AtomicLongArray longArray = new AtomicLongArray(10);89// AtomicReferenceArray10AtomicReferenceArray<String> refArray = new AtomicReferenceArray<>(10);3. 引用类型原子类
1// AtomicReference2AtomicReference<User> userRef = new AtomicReference<>();3User oldUser = new User("张三");4User newUser = new User("李四");5userRef.compareAndSet(oldUser, newUser);67// AtomicStampedReference(解决ABA问题)8AtomicStampedReference<String> stampedRef = 9 new AtomicStampedReference<>("初始值", 0);10int stamp = stampedRef.getStamp();11stampedRef.compareAndSet("初始值", "新值", stamp, stamp + 1);1213// AtomicMarkableReference(标记是否被修改过)14AtomicMarkableReference<String> markableRef = 15 new AtomicMarkableReference<>("初始值", false);16markableRef.compareAndSet("初始值", "新值", false, true);4. 字段更新器
1class User {2 volatile int age;3 volatile String name;4}56// AtomicIntegerFieldUpdater7AtomicIntegerFieldUpdater<User> ageUpdater = 8 AtomicIntegerFieldUpdater.newUpdater(User.class, "age");9User user = new User();10ageUpdater.incrementAndGet(user);1112// AtomicReferenceFieldUpdater13AtomicReferenceFieldUpdater<User, String> nameUpdater = 14 AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");15nameUpdater.compareAndSet(user, "旧名字", "新名字");应用场景示例:
1// 1. 计数器2public class Counter {3 private AtomicInteger count = new AtomicInteger(0);4 5 public void increment() {6 count.incrementAndGet();7 }8 9 public int getCount() {10 return count.get();11 }12}1314// 2. 单例模式15public class Singleton {16 private static final AtomicReference<Singleton> INSTANCE = 17 new AtomicReference<>();18 19 public static Singleton getInstance() {20 while (true) {21 Singleton current = INSTANCE.get();22 if (current != null) {23 return current;24 }25 current = new Singleton();26 if (INSTANCE.compareAndSet(null, current)) {27 return current;28 }29 }30 }31}27. 你使用过 Java 的累加器吗?
答案:
Java 8 引入了累加器(Accumulator),在高并发场景下性能优于原子类。
1. LongAdder(长整型累加器)
1LongAdder adder = new LongAdder();23// 多线程累加4for (int i = 0; i < 10; i++) {5 new Thread(() -> {6 for (int j = 0; j < 1000; j++) {7 adder.increment(); // 加18 adder.add(5); // 加59 }10 }).start();11}1213// 获取结果14long sum = adder.sum();2. LongAccumulator(长整型累积器)
1// 自定义累积函数(求最大值)2LongAccumulator accumulator = new LongAccumulator(Long::max, Long.MIN_VALUE);34accumulator.accumulate(100);5accumulator.accumulate(200);6accumulator.accumulate(50);78long max = accumulator.get(); // 2003. DoubleAdder 和 DoubleAccumulator
1DoubleAdder doubleAdder = new DoubleAdder();2doubleAdder.add(1.5);3double sum = doubleAdder.sum();45DoubleAccumulator doubleAccumulator = 6 new DoubleAccumulator(Double::sum, 0.0);性能对比:
1// AtomicLong vs LongAdder 性能测试2public class PerformanceTest {3 public static void testAtomicLong() {4 AtomicLong atomicLong = new AtomicLong(0);5 long start = System.currentTimeMillis();6 7 // 10个线程,每个累加100万次8 for (int i = 0; i < 10; i++) {9 new Thread(() -> {10 for (int j = 0; j < 1000000; j++) {11 atomicLong.incrementAndGet();12 }13 }).start();14 }15 16 // AtomicLong: 约2000ms17 }18 19 public static void testLongAdder() {20 LongAdder longAdder = new LongAdder();21 long start = System.currentTimeMillis();22 23 for (int i = 0; i < 10; i++) {24 new Thread(() -> {25 for (int j = 0; j < 1000000; j++) {26 longAdder.increment();27 }28 }).start();29 }30 31 // LongAdder: 约500ms(快4倍)32 }33}原理:
- AtomicLong:所有线程竞争同一个变量,CAS 冲突多
- LongAdder:每个线程维护自己的计数器(Cell),最后汇总,减少竞争
内部结构:
1LongAdder2├── base (基础值)3└── cells[] (Cell数组)4 ├── Cell[0] (线程1的计数)5 ├── Cell[1] (线程2的计数)6 └── Cell[2] (线程3的计数)78sum() = base + Cell[0] + Cell[1] + Cell[2] + ...选择建议:
- 低并发场景:AtomicLong(更简单)
- 高并发累加:LongAdder(性能更好)
- 需要 CAS 操作:AtomicLong
- 只需要累加:LongAdder
28. 什么是 Java 的 CAS(Compare-And-Swap)操作?
答案:
CAS(Compare-And-Swap)是一种无锁的原子操作,用于实现并发控制。
原理: CAS 包含三个操作数:
- V:内存位置(变量)
- E:预期值(Expected)
- N:新值(New)
只有当 V 的值等于 E 时,才将 V 的值设置为 N,否则不做任何操作。整个过程是原子的。
伪代码:
1boolean compareAndSwap(Variable V, int E, int N) {2 if (V == E) {3 V = N;4 return true;5 }6 return false;7}Java 中的 CAS:
1AtomicInteger count = new AtomicInteger(0);23// CAS 操作4boolean success = count.compareAndSet(0, 1);5// 如果当前值是0,则设置为1,返回true6// 如果当前值不是0,不做任何操作,返回false78// 自旋 CAS9public void increment() {10 int oldValue;11 int newValue;12 do {13 oldValue = count.get();14 newValue = oldValue + 1;15 } while (!count.compareAndSet(oldValue, newValue));16}底层实现:
1// Unsafe 类提供的 CAS 方法2public final native boolean compareAndSwapInt(3 Object o, // 对象4 long offset, // 字段偏移量5 int expected, // 预期值6 int x // 新值7);89// CPU 指令(x86)10lock cmpxchg [内存地址], 新值CAS 的优点:
- 无锁,避免线程阻塞和上下文切换
- 性能高于 synchronized
- 适合低并发场景
CAS 的缺点:
1. ABA 问题
1// 线程1:读取值A2int value = atomicInt.get(); // A34// 线程2:A -> B -> A5atomicInt.compareAndSet(A, B);6atomicInt.compareAndSet(B, A);78// 线程1:CAS成功,但值已经被修改过9atomicInt.compareAndSet(A, C); // 成功,但不知道中间变化1011// 解决方案:使用版本号12AtomicStampedReference<Integer> stampedRef = 13 new AtomicStampedReference<>(A, 0);14int stamp = stampedRef.getStamp();15stampedRef.compareAndSet(A, C, stamp, stamp + 1);2. 循环时间长开销大
1// 高并发下,CAS 失败次数多,CPU 开销大2public void increment() {3 int oldValue;4 int newValue;5 do {6 oldValue = count.get();7 newValue = oldValue + 1;8 // 高并发下可能循环很多次9 } while (!count.compareAndSet(oldValue, newValue));10}1112// 解决方案:使用 LongAdder 或加锁3. 只能保证一个共享变量的原子操作
1// 不能同时 CAS 多个变量2// 解决方案:3// 1. 使用 AtomicReference 包装多个变量4class Pair {5 int a;6 int b;7}8AtomicReference<Pair> pairRef = new AtomicReference<>(new Pair());910// 2. 使用锁11synchronized (lock) {12 a++;13 b++;14}应用场景:
- 原子类(AtomicInteger、AtomicReference)
- 并发容器(ConcurrentHashMap)
- AQS(AbstractQueuedSynchronizer)
- 乐观锁实现
29. 说说 AQS 吧?
答案:
AQS(AbstractQueuedSynchronizer)是 Java 并发包的核心基础框架,用于构建锁和同步器。
核心思想:
- 使用一个 int 类型的 state 变量表示同步状态
- 使用 FIFO 队列管理等待线程
- 提供独占模式和共享模式
核心组件:
1. 同步状态(state)
1private volatile int state;23// 获取状态4protected final int getState()56// 设置状态7protected final void setState(int newState)89// CAS 设置状态10protected final boolean compareAndSetState(int expect, int update)2. 等待队列(CLH 队列)
1head -> Node1 -> Node2 -> Node3 -> tail2 (等待) (等待) (等待)每个 Node 包含:
- thread:等待的线程
- waitStatus:等待状态
- prev/next:前驱/后继节点
工作流程:
独占模式(如 ReentrantLock):
1// 1. 尝试获取锁2public final void acquire(int arg) {3 if (!tryAcquire(arg) && // 尝试获取(子类实现)4 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { // 加入队列等待5 selfInterrupt();6 }7}89// 2. 释放锁10public final boolean release(int arg) {11 if (tryRelease(arg)) { // 尝试释放(子类实现)12 Node h = head;13 if (h != null && h.waitStatus != 0)14 unparkSuccessor(h); // 唤醒后继节点15 return true;16 }17 return false;18}共享模式(如 Semaphore、CountDownLatch):
1// 获取共享锁2public final void acquireShared(int arg) {3 if (tryAcquireShared(arg) < 0) // 尝试获取4 doAcquireShared(arg); // 加入队列等待5}67// 释放共享锁8public final boolean releaseShared(int arg) {9 if (tryReleaseShared(arg)) { // 尝试释放10 doReleaseShared(); // 唤醒后继节点11 return true;12 }13 return false;14}自定义同步器示例:
1// 自定义互斥锁2public class Mutex implements Lock {3 private static class Sync extends AbstractQueuedSynchronizer {4 // 尝试获取锁5 @Override6 protected boolean tryAcquire(int arg) {7 return compareAndSetState(0, 1);8 }9 10 // 尝试释放锁11 @Override12 protected boolean tryRelease(int arg) {13 setState(0);14 return true;15 }16 17 // 是否被独占18 @Override19 protected boolean isHeldExclusively() {20 return getState() == 1;21 }22 23 Condition newCondition() {24 return new ConditionObject();25 }26 }27 28 private final Sync sync = new Sync();29 30 @Override31 public void lock() {32 sync.acquire(1);33 }34 35 @Override36 public void unlock() {37 sync.release(1);38 }39 40 // 其他方法实现...41}基于 AQS 的同步器:
- ReentrantLock:独占锁
- ReentrantReadWriteLock:读写锁
- Semaphore:信号量
- CountDownLatch:倒计数门栓
- CyclicBarrier:循环栅栏(基于 ReentrantLock)
AQS 的优势:
- 提供统一的框架,简化同步器实现
- 高效的等待队列管理
- 支持可中断、超时、公平/非公平
- 提供 Condition 支持
30. Java 中 ReentrantLock 的实现原理是什么?
答案:
ReentrantLock 是基于 AQS 实现的可重入独占锁。
核心特性:
- 可重入:同一线程可以多次获取锁
- 公平/非公平:支持两种模式
- 可中断:支持中断等待
- 可超时:支持超时获取锁
- Condition 支持:支持多个条件变量
内部结构:
1public class ReentrantLock implements Lock {2 private final Sync sync;3 4 // 抽象同步器5 abstract static class Sync extends AbstractQueuedSynchronizer {6 // state 表示重入次数7 }8 9 // 非公平同步器10 static final class NonfairSync extends Sync {11 final void lock() {12 // 直接尝试 CAS 获取锁13 if (compareAndSetState(0, 1))14 setExclusiveOwnerThread(Thread.currentThread());15 else16 acquire(1);17 }18 }19 20 // 公平同步器21 static final class FairSync extends Sync {22 final void lock() {23 // 必须排队24 acquire(1);25 }26 27 protected final boolean tryAcquire(int acquires) {28 // 检查队列中是否有等待线程29 if (hasQueuedPredecessors())30 return false;31 // ...32 }33 }34}加锁流程:
非公平锁:
1public void lock() {2 // 1. 直接尝试 CAS 获取锁3 if (compareAndSetState(0, 1)) {4 setExclusiveOwnerThread(Thread.currentThread());5 return;6 }7 8 // 2. CAS 失败,调用 AQS 的 acquire9 acquire(1);10 // 2.1 tryAcquire:再次尝试获取11 // 2.2 失败则加入等待队列12 // 2.3 park 阻塞线程13}公平锁:
1public void lock() {2 acquire(1);3 // 1. tryAcquire:检查队列是否有等待线程4 // 2. 有等待线程则直接失败5 // 3. 没有等待线程则尝试 CAS 获取6 // 4. 失败则加入队列等待7}重入实现:
1protected final boolean tryAcquire(int acquires) {2 final Thread current = Thread.currentThread();3 int c = getState();4 5 if (c == 0) {6 // 锁空闲,尝试获取7 if (compareAndSetState(0, acquires)) {8 setExclusiveOwnerThread(current);9 return true;10 }11 } else if (current == getExclusiveOwnerThread()) {12 // 当前线程已持有锁,重入13 int nextc = c + acquires;14 if (nextc < 0) // 溢出检查15 throw new Error("Maximum lock count exceeded");16 setState(nextc); // 增加重入次数17 return true;18 }19 return false;20}解锁流程:
1public void unlock() {2 release(1);3}45protected final boolean tryRelease(int releases) {6 int c = getState() - releases;7 if (Thread.currentThread() != getExclusiveOwnerThread())8 throw new IllegalMonitorStateException();9 10 boolean free = false;11 if (c == 0) {12 // 重入次数归零,完全释放锁13 free = true;14 setExclusiveOwnerThread(null);15 }16 setState(c); // 减少重入次数17 return free;18}使用示例:
1ReentrantLock lock = new ReentrantLock();23// 基本使用4lock.lock();5try {6 // 临界区代码7} finally {8 lock.unlock();9}1011// 可中断12try {13 lock.lockInterruptibly();14 // 临界区代码15} catch (InterruptedException e) {16 // 处理中断17} finally {18 lock.unlock();19}2021// 超时获取22if (lock.tryLock(1, TimeUnit.SECONDS)) {23 try {24 // 临界区代码25 } finally {26 lock.unlock();27 }28} else {29 // 获取锁失败30}3132// Condition 使用33Condition condition = lock.newCondition();34lock.lock();35try {36 condition.await(); // 等待37 condition.signal(); // 唤醒38} finally {39 lock.unlock();40}公平锁 vs 非公平锁:
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 按排队顺序 | 可插队 |
| 性能 | 较低 | 较高 |
| 吞吐量 | 较低 | 较高 |
| 线程切换 | 更多 | 更少 |
| 适用场景 | 需要公平性 | 追求性能 |
默认使用非公平锁:
1// 非公平锁(默认)2ReentrantLock lock = new ReentrantLock();34// 公平锁5ReentrantLock fairLock = new ReentrantLock(true);31. Java 的 synchronized 是怎么实现的?
答案:
synchronized 是 Java 提供的内置锁机制,基于对象监视器(Monitor)实现。
实现层次:
1. 字节码层面
1public void syncMethod() {2 synchronized (this) {3 // 临界区代码4 }5}67// 字节码8monitorenter // 进入监视器(获取锁)9// 临界区代码10monitorexit // 退出监视器(释放锁)11monitorexit // 异常情况的释放2. 对象头(Mark Word)
Java 对象在内存中的布局:
1|--------------------------------------------------------------|2| Object Header (对象头) |3|--------------------------------------------------------------|4| Mark Word (标记字段,8字节) |5| Class Pointer (类型指针,4/8字节) |6|--------------------------------------------------------------|7| Instance Data (实例数据) |8|--------------------------------------------------------------|9| Padding (对齐填充) |10|--------------------------------------------------------------|Mark Word 存储锁信息:
1|----------|---------------------------|2| 锁状态 | Mark Word 内容 |3|----------|---------------------------|4| 无锁 | hashCode | age | 0 | 01 |5| 偏向锁 | threadID | epoch | 1 | 01 |6| 轻量级锁 | 栈中锁记录指针 | 00 |7| 重量级锁 | Monitor 指针 | 10 |8| GC标记 | 空 | 11 |9|----------|---------------------------|3. 锁升级过程
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
偏向锁(Biased Locking):
1// 第一次获取锁2// 1. 检查 Mark Word 是否为可偏向状态3// 2. CAS 将线程 ID 写入 Mark Word4// 3. 成功则获取偏向锁56// 再次获取锁(同一线程)7// 1. 检查 Mark Word 中的线程 ID8// 2. 如果是当前线程,直接执行(无需 CAS)910// 其他线程竞争11// 1. 撤销偏向锁12// 2. 升级为轻量级锁轻量级锁(Lightweight Locking):
1// 获取锁2// 1. 在栈帧中创建锁记录(Lock Record)3// 2. 复制 Mark Word 到锁记录4// 3. CAS 将对象头的 Mark Word 替换为指向锁记录的指针5// 4. 成功则获取轻量级锁6// 5. 失败则自旋重试7// 6. 自旋一定次数后升级为重量级锁89// 释放锁10// 1. CAS 将锁记录中的 Mark Word 替换回对象头11// 2. 成功则释放锁12// 3. 失败则升级为重量级锁重量级锁(Heavyweight Locking):
1// 获取锁2// 1. 对象头指向 Monitor 对象3// 2. 线程进入 Monitor 的 EntryList4// 3. 竞争 Monitor 的 owner5// 4. 获取失败则阻塞(park)67// Monitor 结构8ObjectMonitor {9 _owner; // 持有锁的线程10 _EntryList; // 等待获取锁的线程队列11 _WaitSet; // 调用 wait() 的线程队列12 _count; // 重入次数13}完整流程图:
1对象创建2 ↓3无锁状态(001)4 ↓ 线程1访问5偏向锁(101)- 线程ID = 线程16 ↓ 线程2竞争7轻量级锁(00)- 自旋获取8 ↓ 自旋失败/竞争激烈9重量级锁(10)- 阻塞等待代码示例:
1public class SynchronizedDemo {2 private Object lock = new Object();3 4 // 同步代码块5 public void method1() {6 synchronized (lock) {7 // 临界区8 }9 }10 11 // 同步实例方法(锁是 this)12 public synchronized void method2() {13 // 临界区14 }15 16 // 同步静态方法(锁是 Class 对象)17 public static synchronized void method3() {18 // 临界区19 }20}优化技术:
- 锁消除:JIT 编译器检测到不可能存在竞争,消除锁
- 锁粗化:多个连续的加锁解锁操作合并为一次
- 自适应自旋:根据历史自旋成功率动态调整自旋次数
32. Synchronized 修饰静态方法和修饰普通方法有什么区别?
答案:
主要区别在于锁的对象不同:
1. 修饰普通方法(实例方法)
1public class MyClass {2 // 锁对象是 this(当前实例)3 public synchronized void instanceMethod() {4 // 临界区5 }6 7 // 等价于8 public void instanceMethod2() {9 synchronized (this) {10 // 临界区11 }12 }13}特点:
- 锁对象是当前实例(this)
- 不同实例之间不互斥
- 同一实例的多个同步方法互斥
示例:
1MyClass obj1 = new MyClass();2MyClass obj2 = new MyClass();34// 线程1和线程2不互斥(不同实例)5new Thread(() -> obj1.instanceMethod()).start();6new Thread(() -> obj2.instanceMethod()).start();78// 线程3和线程4互斥(同一实例)9new Thread(() -> obj1.instanceMethod()).start();10new Thread(() -> obj1.instanceMethod()).start();2. 修饰静态方法(类方法)
1public class MyClass {2 // 锁对象是 MyClass.class(类对象)3 public static synchronized void staticMethod() {4 // 临界区5 }6 7 // 等价于8 public static void staticMethod2() {9 synchronized (MyClass.class) {10 // 临界区11 }12 }13}特点:
- 锁对象是 Class 对象(MyClass.class)
- 所有实例共享同一个锁
- 类的所有静态同步方法互斥
示例:
1MyClass obj1 = new MyClass();2MyClass obj2 = new MyClass();34// 线程1和线程2互斥(共享类锁)5new Thread(() -> MyClass.staticMethod()).start();6new Thread(() -> MyClass.staticMethod()).start();78// 线程3和线程4也互斥(共享类锁)9new Thread(() -> obj1.staticMethod()).start();10new Thread(() -> obj2.staticMethod()).start();3. 混合使用
1public class MyClass {2 // 实例锁3 public synchronized void instanceMethod() {4 System.out.println("实例方法");5 }6 7 // 类锁8 public static synchronized void staticMethod() {9 System.out.println("静态方法");10 }11}1213// 不互斥(锁对象不同)14new Thread(() -> new MyClass().instanceMethod()).start();15new Thread(() -> MyClass.staticMethod()).start();对比总结:
| 特性 | 修饰实例方法 | 修饰静态方法 |
|---|---|---|
| 锁对象 | this(实例对象) | Class 对象 |
| 作用范围 | 当前实例 | 所有实例 |
| 互斥范围 | 同一实例的同步方法 | 所有静态同步方法 |
| 使用场景 | 实例变量保护 | 静态变量保护 |
最佳实践:
1public class Counter {2 private int instanceCount = 0;3 private static int staticCount = 0;4 5 // 保护实例变量6 public synchronized void incrementInstance() {7 instanceCount++;8 }9 10 // 保护静态变量11 public static synchronized void incrementStatic() {12 staticCount++;13 }14 15 // 或者使用显式锁对象16 private final Object instanceLock = new Object();17 private static final Object staticLock = new Object();18 19 public void increment() {20 synchronized (instanceLock) {21 instanceCount++;22 }23 }24 25 public static void incrementStatic2() {26 synchronized (staticLock) {27 staticCount++;28 }29 }30}33. Java 中的 synchronized 轻量级锁是否会进行自旋?
答案:
是的,轻量级锁会进行自旋,但有条件限制。
自旋的原因:
- 避免线程阻塞和唤醒的开销
- 适用于锁持有时间短的场景
- 线程在用户态自旋,不进入内核态
自旋过程:
1// 轻量级锁获取流程2while (true) {3 // 1. 尝试 CAS 获取锁4 if (CAS成功) {5 获取锁成功;6 break;7 }8 9 // 2. CAS 失败,自旋重试10 if (自旋次数 < 最大自旋次数) {11 自旋次数++;12 continue; // 继续自旋13 } else {14 // 3. 自旋失败,升级为重量级锁15 升级为重量级锁();16 阻塞等待();17 break;18 }19}自旋策略:
1. 固定次数自旋(JDK 1.6 之前)
1// 默认自旋10次2-XX:PreBlockSpin=102. 自适应自旋(JDK 1.6+)
1// 根据历史自旋成功率动态调整2if (上次自旋成功) {3 允许更多次自旋;4} else {5 减少自旋次数;6}78// 如果某个锁自旋很少成功,可能直接省略自旋自旋的条件:
-
CPU 核心数 > 1
- 单核 CPU 自旋无意义(持有锁的线程无法释放)
-
锁持有时间短
- 自旋时间 < 线程阻塞/唤醒时间
-
竞争不激烈
- 竞争激烈时直接升级为重量级锁
自旋优化示例:
1public class SpinLockDemo {2 private AtomicReference<Thread> owner = new AtomicReference<>();3 4 public void lock() {5 Thread current = Thread.currentThread();6 7 // 自旋获取锁8 while (!owner.compareAndSet(null, current)) {9 // 自旋等待10 // CPU 空转,不释放 CPU11 }12 }13 14 public void unlock() {15 Thread current = Thread.currentThread();16 owner.compareAndSet(current, null);17 }18}自旋的优缺点:
优点:
- 避免线程阻塞和上下文切换
- 适合锁持有时间短的场景
- 提高并发性能
缺点:
- 占用 CPU 资源
- 锁持有时间长时浪费 CPU
- 可能导致 CPU 使用率高
JVM 参数控制:
1# 开启/关闭自旋锁优化(默认开启)2-XX:+UseSpinning3-XX:-UseSpinning45# 设置自旋次数(JDK 1.6 之前)6-XX:PreBlockSpin=1078# 开启自适应自旋(JDK 1.6+ 默认开启)9-XX:+UseAdaptiveSpinning锁升级与自旋的关系:
1偏向锁2 ↓ 有竞争3轻量级锁(自旋获取)4 ↓ 自旋失败5重量级锁(阻塞等待)实际应用建议:
- 锁持有时间短(几十个指令):自旋有效
- 锁持有时间长:自旋浪费 CPU,应直接阻塞
- 高并发场景:考虑使用 ReentrantLock 或无锁方案
34. Synchronized 能不能禁止指令重排序?
35. 当 Java 的 synchronized 升级到重量级锁后,所有线程都释放锁了,此时它还是重量级锁吗?
36. 什么是 Java 中的锁自适应自旋?
37. Synchronized 和 ReentrantLock 有什么区别?
38. Volatile 与 Synchronized 的区别是什么?
39. 如何优化 Java 中的锁的使用?
40. 你了解 Java 中的读写锁吗?
41. 什么是 Java 内存模型(JMM)?
42. 什么是 Java 中的原子性、可见性和有序性?
43. 什么是 Java 的 happens-before 规则?
44. 什么是 Java 中的指令重排?
45. Java 中的 final 关键字是否能保证变量的可见性?
46. 为什么在 Java 中需要使用 ThreadLocal?
47. Java 中的 ThreadLocal 是如何实现线程资源隔离的?
48. 为什么 Java 中的 ThreadLocal 对 key 的引用为弱引用?
49. Java 中使用 ThreadLocal 的最佳实践是什么?
50. Java 中的 InheritableThreadLocal 是什么?
51. ThreadLocal 的缺点?
52. 为什么 Netty 不使用 ThreadLocal 而是自定义了一个 FastThreadLocal ?
53. 什么是 Java 的 TransmittableThreadLocal?
54. Java 中 Thread.sleep 和 Thread.yield 的区别?
55. Java 中 Thread.sleep(0) 的作用是什么?
56. Java 中什么情况会导致死锁?如何避免?
57. Java 中 volatile 关键字的作用是什么?
58. 什么是 Java 中的 ABA 问题?
59. 在 Java 中主线程如何知晓创建的子线程是否执行成功?
60. 创建线程池有哪些方式?
61. 线程安全的集合有哪些?
62. Java 创建线程有哪些方式?
学习指南
核心要点:
- Java 线程生命周期和状态转换
- 锁机制和并发控制原理
- 并发容器和原子操作
- 内存模型和可见性保证
学习路径建议:
- 掌握 Java 线程基础和生命周期
- 深入理解锁机制和 AQS 原理
- 熟悉并发容器和原子类
- 掌握 JMM 和并发编程最佳实践
21. 什么是 Java 的 StampedLock?
答案:
StampedLock 是 Java 8 引入的一种锁,提供了三种模式的读写控制,性能优于 ReadWriteLock。
三种锁模式:
- 写锁(Writing):独占锁
- 悲观读锁(Reading):共享锁,不允许写
- 乐观读(Optimistic Reading):不加锁,通过版本号验证
基本使用:
1public class Point {2 private double x, y;3 private final StampedLock lock = new StampedLock();4 5 // 写操作6 public void move(double deltaX, double deltaY) {7 long stamp = lock.writeLock(); // 获取写锁8 try {9 x += deltaX;10 y += deltaY;11 } finally {12 lock.unlockWrite(stamp); // 释放写锁13 }14 }15 16 // 乐观读17 public double distanceFromOrigin() {18 long stamp = lock.tryOptimisticRead(); // 乐观读19 double currentX = x;20 double currentY = y;21 22 if (!lock.validate(stamp)) { // 验证是否有写操作23 stamp = lock.readLock(); // 升级为悲观读锁24 try {25 currentX = x;26 currentY = y;27 } finally {28 lock.unlockRead(stamp);29 }30 }31 32 return Math.sqrt(currentX * currentX + currentY * currentY);33 }34 35 // 悲观读36 public double getX() {37 long stamp = lock.readLock();38 try {39 return x;40 } finally {41 lock.unlockRead(stamp);42 }43 }44}锁升级:
1// 读锁升级为写锁2long stamp = lock.readLock();3try {4 // 读取数据5 long writeStamp = lock.tryConvertToWriteLock(stamp);6 if (writeStamp != 0L) {7 stamp = writeStamp;8 // 执行写操作9 } else {10 lock.unlockRead(stamp);11 stamp = lock.writeLock();12 // 执行写操作13 }14} finally {15 lock.unlock(stamp);16}特点:
- 乐观读不阻塞写操作,性能更好
- 不支持重入
- 不支持 Condition
- 适合读多写少的场景
与 ReadWriteLock 对比:
| 特性 | StampedLock | ReadWriteLock |
|---|---|---|
| 乐观读 | 支持 | 不支持 |
| 性能 | 更高 | 较低 |
| 重入 | 不支持 | 支持 |
| Condition | 不支持 | 支持 |
22. 什么是 Java 的 CompletableFuture?
答案:
CompletableFuture 是 Java 8 引入的异步编程工具,实现了 Future 和 CompletionStage 接口,支持链式调用和组合操作。
创建 CompletableFuture:
1// 1. 无返回值异步任务2CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {3 System.out.println("异步任务");4});56// 2. 有返回值异步任务7CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {8 return "结果";9});1011// 3. 手动完成12CompletableFuture<String> future3 = new CompletableFuture<>();13future3.complete("手动设置结果");链式调用:
1CompletableFuture.supplyAsync(() -> {2 return "Hello";3})4.thenApply(s -> s + " World") // 转换结果5.thenApply(String::toUpperCase) // 继续转换6.thenAccept(System.out::println) // 消费结果7.thenRun(() -> System.out.println("完成")); // 执行动作组合操作:
1// 1. 两个任务都完成后执行2CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");3CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");45CompletableFuture<String> combined = future1.thenCombine(future2, (s1, s2) -> {6 return s1 + " " + s2;7});89// 2. 任意一个完成后执行10CompletableFuture<String> fastest = future1.applyToEither(future2, s -> s);1112// 3. 所有任务完成13CompletableFuture<Void> allOf = CompletableFuture.allOf(future1, future2);1415// 4. 任意任务完成16CompletableFuture<Object> anyOf = CompletableFuture.anyOf(future1, future2);异常处理:
1CompletableFuture.supplyAsync(() -> {2 if (true) throw new RuntimeException("错误");3 return "结果";4})5.exceptionally(ex -> {6 System.out.println("异常处理: " + ex.getMessage());7 return "默认值";8})9.handle((result, ex) -> {10 if (ex != null) {11 return "异常: " + ex.getMessage();12 }13 return result;14})15.whenComplete((result, ex) -> {16 if (ex != null) {17 System.out.println("失败: " + ex.getMessage());18 } else {19 System.out.println("成功: " + result);20 }21});实际应用场景:
1// 并行调用多个服务2public CompletableFuture<OrderInfo> getOrderInfo(String orderId) {3 CompletableFuture<Order> orderFuture = 4 CompletableFuture.supplyAsync(() -> orderService.getOrder(orderId));5 6 CompletableFuture<User> userFuture = 7 CompletableFuture.supplyAsync(() -> userService.getUser(orderId));8 9 CompletableFuture<Product> productFuture = 10 CompletableFuture.supplyAsync(() -> productService.getProduct(orderId));11 12 return orderFuture.thenCombine(userFuture, (order, user) -> {13 return new OrderInfo(order, user);14 }).thenCombine(productFuture, (orderInfo, product) -> {15 orderInfo.setProduct(product);16 return orderInfo;17 });18}常用方法总结:
thenApply:转换结果thenAccept:消费结果thenRun:执行动作thenCompose:扁平化嵌套 FuturethenCombine:组合两个 Futureexceptionally:异常处理handle:结果和异常统一处理whenComplete:完成时回调
23. 什么是 Java 的 ForkJoinPool?
答案:
ForkJoinPool 是 Java 7 引入的线程池,专门用于执行可以递归分解的任务,采用"分而治之"的策略和工作窃取算法。
核心概念:
- Fork(分解):将大任务分解为小任务
- Join(合并):合并小任务的结果
- Work Stealing(工作窃取):空闲线程从其他线程的队列中窃取任务
使用方式:
1. RecursiveTask(有返回值)
1public class SumTask extends RecursiveTask<Long> {2 private static final int THRESHOLD = 10000;3 private long[] array;4 private int start;5 private int end;6 7 public SumTask(long[] array, int start, int end) {8 this.array = array;9 this.start = start;10 this.end = end;11 }12 13 @Override14 protected Long compute() {15 if (end - start <= THRESHOLD) {16 // 任务足够小,直接计算17 long sum = 0;18 for (int i = start; i < end; i++) {19 sum += array[i];20 }21 return sum;22 } else {23 // 任务太大,分解为两个子任务24 int middle = (start + end) / 2;25 SumTask leftTask = new SumTask(array, start, middle);26 SumTask rightTask = new SumTask(array, middle, end);27 28 // Fork:异步执行子任务29 leftTask.fork();30 rightTask.fork();31 32 // Join:等待子任务完成并合并结果33 long leftResult = leftTask.join();34 long rightResult = rightTask.join();35 36 return leftResult + rightResult;37 }38 }39}4041// 使用42ForkJoinPool pool = new ForkJoinPool();43long[] array = new long[100000];44SumTask task = new SumTask(array, 0, array.length);45long result = pool.invoke(task);2. RecursiveAction(无返回值)
1public class PrintTask extends RecursiveAction {2 private static final int THRESHOLD = 50;3 private int start;4 private int end;5 6 public PrintTask(int start, int end) {7 this.start = start;8 this.end = end;9 }10 11 @Override12 protected void compute() {13 if (end - start <= THRESHOLD) {14 for (int i = start; i < end; i++) {15 System.out.println(i);16 }17 } else {18 int middle = (start + end) / 2;19 PrintTask leftTask = new PrintTask(start, middle);20 PrintTask rightTask = new PrintTask(middle, end);21 22 invokeAll(leftTask, rightTask); // 执行所有子任务23 }24 }25}工作窃取算法:
1线程1队列: [Task1, Task2, Task3] ← 从尾部取任务2线程2队列: [Task4, Task5] ← 从尾部取任务3线程3队列: [] ← 空闲,从其他队列头部窃取任务45线程3窃取 → 从线程1队列头部取 Task1特点:
- 适合计算密集型任务
- 自动负载均衡(工作窃取)
- 减少线程竞争(每个线程有自己的队列)
- 默认线程数等于 CPU 核心数
应用场景:
- 大数组排序、求和
- 并行流(Stream.parallel())
- 递归算法并行化
- 图像处理、数据分析
与普通线程池对比:
| 特性 | ForkJoinPool | ThreadPoolExecutor |
|---|---|---|
| 任务类型 | 可分解的递归任务 | 独立任务 |
| 队列 | 双端队列(每线程) | 单一队列 |
| 负载均衡 | 工作窃取 | 无 |
| 适用场景 | 计算密集型 | 通用 |
24. 如何在 Java 中控制多个线程的执行顺序?
答案:
Java 提供了多种方式控制线程执行顺序:
1. 使用 join() 方法
1Thread t1 = new Thread(() -> System.out.println("线程1"));2Thread t2 = new Thread(() -> System.out.println("线程2"));3Thread t3 = new Thread(() -> System.out.println("线程3"));45t1.start();6t1.join(); // 等待 t1 完成78t2.start();9t2.join(); // 等待 t2 完成1011t3.start();12t3.join(); // 等待 t3 完成2. 使用 CountDownLatch
1CountDownLatch latch1 = new CountDownLatch(1);2CountDownLatch latch2 = new CountDownLatch(1);34new Thread(() -> {5 System.out.println("线程1");6 latch1.countDown();7}).start();89new Thread(() -> {10 try {11 latch1.await(); // 等待线程1完成12 System.out.println("线程2");13 latch2.countDown();14 } catch (InterruptedException e) {15 e.printStackTrace();16 }17}).start();1819new Thread(() -> {20 try {21 latch2.await(); // 等待线程2完成22 System.out.println("线程3");23 } catch (InterruptedException e) {24 e.printStackTrace();25 }26}).start();3. 使用 CyclicBarrier
1CyclicBarrier barrier = new CyclicBarrier(3);23// 所有线程同时开始4for (int i = 0; i < 3; i++) {5 final int id = i;6 new Thread(() -> {7 try {8 System.out.println("线程" + id + " 准备");9 barrier.await(); // 等待其他线程10 System.out.println("线程" + id + " 开始执行");11 } catch (Exception e) {12 e.printStackTrace();13 }14 }).start();15}4. 使用 Semaphore
1Semaphore semaphore1 = new Semaphore(1);2Semaphore semaphore2 = new Semaphore(0);3Semaphore semaphore3 = new Semaphore(0);45new Thread(() -> {6 try {7 semaphore1.acquire();8 System.out.println("线程1");9 semaphore2.release();10 } catch (InterruptedException e) {11 e.printStackTrace();12 }13}).start();1415new Thread(() -> {16 try {17 semaphore2.acquire();18 System.out.println("线程2");19 semaphore3.release();20 } catch (InterruptedException e) {21 e.printStackTrace();22 }23}).start();2425new Thread(() -> {26 try {27 semaphore3.acquire();28 System.out.println("线程3");29 } catch (InterruptedException e) {30 e.printStackTrace();31 }32}).start();5. 使用 wait/notify
1public class SequentialExecution {2 private int flag = 1;3 private final Object lock = new Object();4 5 public void thread1() {6 synchronized (lock) {7 while (flag != 1) {8 try {9 lock.wait();10 } catch (InterruptedException e) {11 e.printStackTrace();12 }13 }14 System.out.println("线程1");15 flag = 2;16 lock.notifyAll();17 }18 }19 20 public void thread2() {21 synchronized (lock) {22 while (flag != 2) {23 try {24 lock.wait();25 } catch (InterruptedException e) {26 e.printStackTrace();27 }28 }29 System.out.println("线程2");30 flag = 3;31 lock.notifyAll();32 }33 }34}6. 使用 CompletableFuture
1CompletableFuture.runAsync(() -> {2 System.out.println("线程1");3})4.thenRun(() -> {5 System.out.println("线程2");6})7.thenRun(() -> {8 System.out.println("线程3");9});7. 使用单线程线程池
1ExecutorService executor = Executors.newSingleThreadExecutor();23executor.submit(() -> System.out.println("任务1"));4executor.submit(() -> System.out.println("任务2"));5executor.submit(() -> System.out.println("任务3"));67executor.shutdown();选择建议:
- 简单顺序执行:join() 或单线程线程池
- 复杂依赖关系:CountDownLatch 或 CompletableFuture
- 循环执行:CyclicBarrier
- 精确控制:Semaphore 或 wait/notify
25. 你使用过 Java 中的哪些阻塞队列?
答案:
Java 并发包提供了多种阻塞队列(BlockingQueue),用于生产者-消费者模式。
1. ArrayBlockingQueue(有界数组队列)
1// 创建容量为10的队列2BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);34// 添加元素5queue.put("element"); // 队列满时阻塞6queue.offer("element", 1, TimeUnit.SECONDS); // 超时返回false78// 获取元素9String element = queue.take(); // 队列空时阻塞10String element2 = queue.poll(1, TimeUnit.SECONDS); // 超时返回null特点:
- 基于数组实现
- 有界队列,必须指定容量
- FIFO 顺序
- 支持公平/非公平锁
2. LinkedBlockingQueue(有界链表队列)
1// 无界队列(最大容量 Integer.MAX_VALUE)2BlockingQueue<String> queue1 = new LinkedBlockingQueue<>();34// 有界队列5BlockingQueue<String> queue2 = new LinkedBlockingQueue<>(100);特点:
- 基于链表实现
- 可选有界/无界
- FIFO 顺序
- 吞吐量通常高于 ArrayBlockingQueue
3. PriorityBlockingQueue(优先级队列)
1BlockingQueue<Task> queue = new PriorityBlockingQueue<>();23class Task implements Comparable<Task> {4 private int priority;5 6 @Override7 public int compareTo(Task other) {8 return Integer.compare(this.priority, other.priority);9 }10}特点:
- 基于堆实现
- 无界队列
- 按优先级排序
- 不保证同优先级的顺序
4. DelayQueue(延迟队列)
1BlockingQueue<DelayedTask> queue = new DelayQueue<>();23class DelayedTask implements Delayed {4 private long delayTime;5 private long expire;6 7 @Override8 public long getDelay(TimeUnit unit) {9 return unit.convert(expire - System.currentTimeMillis(), 10 TimeUnit.MILLISECONDS);11 }12 13 @Override14 public int compareTo(Delayed o) {15 return Long.compare(this.expire, ((DelayedTask) o).expire);16 }17}特点:
- 元素必须实现 Delayed 接口
- 只有到期的元素才能被取出
- 适用于定时任务、缓存过期
5. SynchronousQueue(同步队列)
1BlockingQueue<String> queue = new SynchronousQueue<>();23// 生产者4new Thread(() -> {5 try {6 queue.put("data"); // 阻塞直到有消费者取走7 } catch (InterruptedException e) {8 e.printStackTrace();9 }10}).start();1112// 消费者13new Thread(() -> {14 try {15 String data = queue.take(); // 阻塞直到有生产者放入16 } catch (InterruptedException e) {17 e.printStackTrace();18 }19}).start();特点:
- 不存储元素
- 每个 put 必须等待一个 take
- 适用于传递性场景
- CachedThreadPool 使用此队列
6. LinkedTransferQueue(传输队列)
1TransferQueue<String> queue = new LinkedTransferQueue<>();23// 生产者4queue.transfer("data"); // 阻塞直到消费者接收56// 消费者7String data = queue.take();特点:
- 无界队列
- 支持 transfer 方法(直接传递)
- 性能优于 LinkedBlockingQueue
7. LinkedBlockingDeque(双端队列)
1BlockingDeque<String> deque = new LinkedBlockingDeque<>();23// 可以从两端操作4deque.putFirst("first");5deque.putLast("last");6String first = deque.takeFirst();7String last = deque.takeLast();特点:
- 双端队列
- 可以从头尾两端操作
- 适用于工作窃取模式
对比总结:
| 队列类型 | 有界性 | 数据结构 | 特点 |
|---|---|---|---|
| ArrayBlockingQueue | 有界 | 数组 | 固定容量 |
| LinkedBlockingQueue | 可选 | 链表 | 高吞吐量 |
| PriorityBlockingQueue | 无界 | 堆 | 优先级排序 |
| DelayQueue | 无界 | 堆 | 延迟获取 |
| SynchronousQueue | 0 | 无 | 直接传递 |
| LinkedTransferQueue | 无界 | 链表 | 支持传输 |
| LinkedBlockingDeque | 可选 | 链表 | 双端操作 |
使用场景:
- 固定容量限流:ArrayBlockingQueue
- 生产者-消费者:LinkedBlockingQueue
- 任务优先级:PriorityBlockingQueue
- 定时任务:DelayQueue
- 线程池任务传递:SynchronousQueue
26. 你使用过 Java 中的哪些原子类?
答案:
Java 提供了 java.util.concurrent.atomic 包,包含多种原子类,保证操作的原子性。
1. 基本类型原子类
1// AtomicInteger2AtomicInteger count = new AtomicInteger(0);3count.incrementAndGet(); // i++4count.getAndIncrement(); // ++i5count.addAndGet(5); // i += 56count.compareAndSet(0, 1); // CAS操作78// AtomicLong9AtomicLong longValue = new AtomicLong(0L);1011// AtomicBoolean12AtomicBoolean flag = new AtomicBoolean(false);13flag.compareAndSet(false, true);2. 数组类型原子类
1// AtomicIntegerArray2AtomicIntegerArray array = new AtomicIntegerArray(10);3array.incrementAndGet(0); // 数组索引0的元素+14array.compareAndSet(0, 1, 2); // CAS操作56// AtomicLongArray7AtomicLongArray longArray = new AtomicLongArray(10);89// AtomicReferenceArray10AtomicReferenceArray<String> refArray = new AtomicReferenceArray<>(10);3. 引用类型原子类
1// AtomicReference2AtomicReference<User> userRef = new AtomicReference<>();3User oldUser = new User("张三");4User newUser = new User("李四");5userRef.compareAndSet(oldUser, newUser);67// AtomicStampedReference(解决ABA问题)8AtomicStampedReference<String> stampedRef = 9 new AtomicStampedReference<>("初始值", 0);10int stamp = stampedRef.getStamp();11stampedRef.compareAndSet("初始值", "新值", stamp, stamp + 1);1213// AtomicMarkableReference(标记是否被修改过)14AtomicMarkableReference<String> markableRef = 15 new AtomicMarkableReference<>("初始值", false);16markableRef.compareAndSet("初始值", "新值", false, true);4. 字段更新器
1class User {2 volatile int age;3 volatile String name;4}56// AtomicIntegerFieldUpdater7AtomicIntegerFieldUpdater<User> ageUpdater = 8 AtomicIntegerFieldUpdater.newUpdater(User.class, "age");9User user = new User();10ageUpdater.incrementAndGet(user);1112// AtomicReferenceFieldUpdater13AtomicReferenceFieldUpdater<User, String> nameUpdater = 14 AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");15nameUpdater.compareAndSet(user, "旧名字", "新名字");应用场景示例:
1// 1. 计数器2public class Counter {3 private AtomicInteger count = new AtomicInteger(0);4 5 public void increment() {6 count.incrementAndGet();7 }8 9 public int getCount() {10 return count.get();11 }12}1314// 2. 单例模式15public class Singleton {16 private static final AtomicReference<Singleton> INSTANCE = 17 new AtomicReference<>();18 19 public static Singleton getInstance() {20 while (true) {21 Singleton current = INSTANCE.get();22 if (current != null) {23 return current;24 }25 current = new Singleton();26 if (INSTANCE.compareAndSet(null, current)) {27 return current;28 }29 }30 }31}27. 你使用过 Java 的累加器吗?
答案:
Java 8 引入了累加器(Accumulator),在高并发场景下性能优于原子类。
1. LongAdder(长整型累加器)
1LongAdder adder = new LongAdder();23// 多线程累加4for (int i = 0; i < 10; i++) {5 new Thread(() -> {6 for (int j = 0; j < 1000; j++) {7 adder.increment(); // 加18 adder.add(5); // 加59 }10 }).start();11}1213// 获取结果14long sum = adder.sum();2. LongAccumulator(长整型累积器)
1// 自定义累积函数(求最大值)2LongAccumulator accumulator = new LongAccumulator(Long::max, Long.MIN_VALUE);34accumulator.accumulate(100);5accumulator.accumulate(200);6accumulator.accumulate(50);78long max = accumulator.get(); // 2003. DoubleAdder 和 DoubleAccumulator
1DoubleAdder doubleAdder = new DoubleAdder();2doubleAdder.add(1.5);3double sum = doubleAdder.sum();45DoubleAccumulator doubleAccumulator = 6 new DoubleAccumulator(Double::sum, 0.0);性能对比:
1// AtomicLong vs LongAdder 性能测试2public class PerformanceTest {3 public static void testAtomicLong() {4 AtomicLong atomicLong = new AtomicLong(0);5 // 10个线程,每个累加100万次6 // AtomicLong: 约2000ms7 }8 9 public static void testLongAdder() {10 LongAdder longAdder = new LongAdder();11 // 10个线程,每个累加100万次12 // LongAdder: 约500ms(快4倍)13 }14}原理:
- AtomicLong:所有线程竞争同一个变量,CAS 冲突多
- LongAdder:每个线程维护自己的计数器(Cell),最后汇总,减少竞争
内部结构:
1LongAdder2├── base (基础值)3└── cells[] (Cell数组)4 ├── Cell[0] (线程1的计数)5 ├── Cell[1] (线程2的计数)6 └── Cell[2] (线程3的计数)78sum() = base + Cell[0] + Cell[1] + Cell[2] + ...选择建议:
- 低并发场景:AtomicLong(更简单)
- 高并发累加:LongAdder(性能更好)
- 需要 CAS 操作:AtomicLong
- 只需要累加:LongAdder
28. 什么是 Java 的 CAS(Compare-And-Swap)操作?
答案:
CAS(Compare-And-Swap)是一种无锁的原子操作,用于实现并发控制。
原理: CAS 包含三个操作数:
- V:内存位置(变量)
- E:预期值(Expected)
- N:新值(New)
只有当 V 的值等于 E 时,才将 V 的值设置为 N,否则不做任何操作。整个过程是原子的。
伪代码:
1boolean compareAndSwap(Variable V, int E, int N) {2 if (V == E) {3 V = N;4 return true;5 }6 return false;7}Java 中的 CAS:
1AtomicInteger count = new AtomicInteger(0);23// CAS 操作4boolean success = count.compareAndSet(0, 1);5// 如果当前值是0,则设置为1,返回true6// 如果当前值不是0,不做任何操作,返回false78// 自旋 CAS9public void increment() {10 int oldValue;11 int newValue;12 do {13 oldValue = count.get();14 newValue = oldValue + 1;15 } while (!count.compareAndSet(oldValue, newValue));16}底层实现:
1// Unsafe 类提供的 CAS 方法2public final native boolean compareAndSwapInt(3 Object o, // 对象4 long offset, // 字段偏移量5 int expected, // 预期值6 int x // 新值7);89// CPU 指令(x86)10lock cmpxchg [内存地址], 新值CAS 的优点:
- 无锁,避免线程阻塞和上下文切换
- 性能高于 synchronized
- 适合低并发场景
CAS 的缺点:
1. ABA 问题
1// 线程1:读取值A2int value = atomicInt.get(); // A34// 线程2:A -> B -> A5atomicInt.compareAndSet(A, B);6atomicInt.compareAndSet(B, A);78// 线程1:CAS成功,但值已经被修改过9atomicInt.compareAndSet(A, C); // 成功,但不知道中间变化1011// 解决方案:使用版本号12AtomicStampedReference<Integer> stampedRef = 13 new AtomicStampedReference<>(A, 0);14int stamp = stampedRef.getStamp();15stampedRef.compareAndSet(A, C, stamp, stamp + 1);2. 循环时间长开销大
1// 高并发下,CAS 失败次数多,CPU 开销大2public void increment() {3 int oldValue;4 int newValue;5 do {6 oldValue = count.get();7 newValue = oldValue + 1;8 // 高并发下可能循环很多次9 } while (!count.compareAndSet(oldValue, newValue));10}1112// 解决方案:使用 LongAdder 或加锁3. 只能保证一个共享变量的原子操作
1// 不能同时 CAS 多个变量2// 解决方案:3// 1. 使用 AtomicReference 包装多个变量4class Pair {5 int a;6 int b;7}8AtomicReference<Pair> pairRef = new AtomicReference<>(new Pair());910// 2. 使用锁11synchronized (lock) {12 a++;13 b++;14}应用场景:
- 原子类(AtomicInteger、AtomicReference)
- 并发容器(ConcurrentHashMap)
- AQS(AbstractQueuedSynchronizer)
- 乐观锁实现
29. 说说 AQS 吧?
答案:
AQS(AbstractQueuedSynchronizer)是 Java 并发包的核心基础框架,用于构建锁和同步器。
核心思想:
- 使用一个 int 类型的 state 变量表示同步状态
- 使用 FIFO 队列管理等待线程
- 提供独占模式和共享模式
核心组件:
1. 同步状态(state)
1private volatile int state;23// 获取状态4protected final int getState()56// 设置状态7protected final void setState(int newState)89// CAS 设置状态10protected final boolean compareAndSetState(int expect, int update)2. 等待队列(CLH 队列)
1head -> Node1 -> Node2 -> Node3 -> tail2 (等待) (等待) (等待)每个 Node 包含:
- thread:等待的线程
- waitStatus:等待状态
- prev/next:前驱/后继节点
工作流程:
独占模式(如 ReentrantLock):
1// 1. 尝试获取锁2public final void acquire(int arg) {3 if (!tryAcquire(arg) && // 尝试获取(子类实现)4 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { // 加入队列等待5 selfInterrupt();6 }7}89// 2. 释放锁10public final boolean release(int arg) {11 if (tryRelease(arg)) { // 尝试释放(子类实现)12 Node h = head;13 if (h != null && h.waitStatus != 0)14 unparkSuccessor(h); // 唤醒后继节点15 return true;16 }17 return false;18}共享模式(如 Semaphore、CountDownLatch):
1// 获取共享锁2public final void acquireShared(int arg) {3 if (tryAcquireShared(arg) < 0) // 尝试获取4 doAcquireShared(arg); // 加入队列等待5}67// 释放共享锁8public final boolean releaseShared(int arg) {9 if (tryReleaseShared(arg)) { // 尝试释放10 doReleaseShared(); // 唤醒后继节点11 return true;12 }13 return false;14}自定义同步器示例:
1// 自定义互斥锁2public class Mutex implements Lock {3 private static class Sync extends AbstractQueuedSynchronizer {4 // 尝试获取锁5 @Override6 protected boolean tryAcquire(int arg) {7 return compareAndSetState(0, 1);8 }9 10 // 尝试释放锁11 @Override12 protected boolean tryRelease(int arg) {13 setState(0);14 return true;15 }16 17 // 是否被独占18 @Override19 protected boolean isHeldExclusively() {20 return getState() == 1;21 }22 23 Condition newCondition() {24 return new ConditionObject();25 }26 }27 28 private final Sync sync = new Sync();29 30 @Override31 public void lock() {32 sync.acquire(1);33 }34 35 @Override36 public void unlock() {37 sync.release(1);38 }39 40 // 其他方法实现...41}基于 AQS 的同步器:
- ReentrantLock:独占锁
- ReentrantReadWriteLock:读写锁
- Semaphore:信号量
- CountDownLatch:倒计数门栓
- CyclicBarrier:循环栅栏(基于 ReentrantLock)
AQS 的优势:
- 提供统一的框架,简化同步器实现
- 高效的等待队列管理
- 支持可中断、超时、公平/非公平
- 提供 Condition 支持
30. Java 中 ReentrantLock 的实现原理是什么?
答案:
ReentrantLock 是基于 AQS 实现的可重入独占锁。
核心特性:
- 可重入:同一线程可以多次获取锁
- 公平/非公平:支持两种模式
- 可中断:支持中断等待
- 可超时:支持超时获取锁
- Condition 支持:支持多个条件变量
重入实现:
1protected final boolean tryAcquire(int acquires) {2 final Thread current = Thread.currentThread();3 int c = getState();4 5 if (c == 0) {6 // 锁空闲,尝试获取7 if (compareAndSetState(0, acquires)) {8 setExclusiveOwnerThread(current);9 return true;10 }11 } else if (current == getExclusiveOwnerThread()) {12 // 当前线程已持有锁,重入13 int nextc = c + acquires;14 if (nextc < 0) // 溢出检查15 throw new Error("Maximum lock count exceeded");16 setState(nextc); // 增加重入次数17 return true;18 }19 return false;20}解锁流程:
1protected final boolean tryRelease(int releases) {2 int c = getState() - releases;3 if (Thread.currentThread() != getExclusiveOwnerThread())4 throw new IllegalMonitorStateException();5 6 boolean free = false;7 if (c == 0) {8 // 重入次数归零,完全释放锁9 free = true;10 setExclusiveOwnerThread(null);11 }12 setState(c); // 减少重入次数13 return free;14}使用示例:
1ReentrantLock lock = new ReentrantLock();23// 基本使用4lock.lock();5try {6 // 临界区代码7} finally {8 lock.unlock();9}1011// 可中断12try {13 lock.lockInterruptibly();14 // 临界区代码15} catch (InterruptedException e) {16 // 处理中断17} finally {18 lock.unlock();19}2021// 超时获取22if (lock.tryLock(1, TimeUnit.SECONDS)) {23 try {24 // 临界区代码25 } finally {26 lock.unlock();27 }28} else {29 // 获取锁失败30}公平锁 vs 非公平锁:
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 按排队顺序 | 可插队 |
| 性能 | 较低 | 较高 |
| 吞吐量 | 较低 | 较高 |
| 线程切换 | 更多 | 更少 |
| 适用场景 | 需要公平性 | 追求性能 |
31. Java 的 synchronized 是怎么实现的?
答案:
synchronized 是 Java 提供的内置锁机制,基于对象监视器(Monitor)实现。
实现层次:
1. 字节码层面
1public void syncMethod() {2 synchronized (this) {3 // 临界区代码4 }5}67// 字节码8monitorenter // 进入监视器(获取锁)9// 临界区代码10monitorexit // 退出监视器(释放锁)11monitorexit // 异常情况的释放2. 对象头(Mark Word)
Java 对象在内存中的布局:
1|--------------------------------------------------------------|2| Object Header (对象头) |3|--------------------------------------------------------------|4| Mark Word (标记字段,8字节) |5| Class Pointer (类型指针,4/8字节) |6|--------------------------------------------------------------|7| Instance Data (实例数据) |8|--------------------------------------------------------------|9| Padding (对齐填充) |10|--------------------------------------------------------------|Mark Word 存储锁信息:
1|----------|---------------------------|2| 锁状态 | Mark Word 内容 |3|----------|---------------------------|4| 无锁 | hashCode | age | 0 | 01 |5| 偏向锁 | threadID | epoch | 1 | 01 |6| 轻量级锁 | 栈中锁记录指针 | 00 |7| 重量级锁 | Monitor 指针 | 10 |8| GC标记 | 空 | 11 |9|----------|---------------------------|3. 锁升级过程
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
偏向锁(Biased Locking):
- 第一次获取锁时,CAS 将线程 ID 写入 Mark Word
- 再次获取锁(同一线程),直接执行(无需 CAS)
- 其他线程竞争时,撤销偏向锁,升级为轻量级锁
轻量级锁(Lightweight Locking):
- 在栈帧中创建锁记录(Lock Record)
- CAS 将对象头的 Mark Word 替换为指向锁记录的指针
- 成功则获取轻量级锁
- 失败则自旋重试
- 自旋一定次数后升级为重量级锁
重量级锁(Heavyweight Locking):
- 对象头指向 Monitor 对象
- 线程进入 Monitor 的 EntryList
- 竞争 Monitor 的 owner
- 获取失败则阻塞(park)
Monitor 结构:
1ObjectMonitor {2 _owner; // 持有锁的线程3 _EntryList; // 等待获取锁的线程队列4 _WaitSet; // 调用 wait() 的线程队列5 _count; // 重入次数6}完整流程图:
1对象创建2 ↓3无锁状态(001)4 ↓ 线程1访问5偏向锁(101)- 线程ID = 线程16 ↓ 线程2竞争7轻量级锁(00)- 自旋获取8 ↓ 自旋失败/竞争激烈9重量级锁(10)- 阻塞等待优化技术:
- 锁消除:JIT 编译器检测到不可能存在竞争,消除锁
- 锁粗化:多个连续的加锁解锁操作合并为一次
- 自适应自旋:根据历史自旋成功率动态调整自旋次数
32. Synchronized 修饰静态方法和修饰普通方法有什么区别?
答案:
主要区别在于锁的对象不同:
1. 修饰普通方法(实例方法)
1public class MyClass {2 // 锁对象是 this(当前实例)3 public synchronized void instanceMethod() {4 // 临界区5 }6 7 // 等价于8 public void instanceMethod2() {9 synchronized (this) {10 // 临界区11 }12 }13}特点:
- 锁对象是当前实例(this)
- 不同实例之间不互斥
- 同一实例的多个同步方法互斥
2. 修饰静态方法(类方法)
1public class MyClass {2 // 锁对象是 MyClass.class(类对象)3 public static synchronized void staticMethod() {4 // 临界区5 }6 7 // 等价于8 public static void staticMethod2() {9 synchronized (MyClass.class) {10 // 临界区11 }12 }13}特点:
- 锁对象是 Class 对象(MyClass.class)
- 所有实例共享同一个锁
- 类的所有静态同步方法互斥
对比总结:
| 特性 | 修饰实例方法 | 修饰静态方法 |
|---|---|---|
| 锁对象 | this(实例对象) | Class 对象 |
| 作用范围 | 当前实例 | 所有实例 |
| 互斥范围 | 同一实例的同步方法 | 所有静态同步方法 |
| 使用场景 | 实例变量保护 | 静态变量保护 |
33. Java 中的 synchronized 轻量级锁是否会进行自旋?
答案:
是的,轻量级锁会进行自旋,但有条件限制。
自旋的原因:
- 避免线程阻塞和唤醒的开销
- 适用于锁持有时间短的场景
- 线程在用户态自旋,不进入内核态
自旋过程:
1// 轻量级锁获取流程2while (true) {3 // 1. 尝试 CAS 获取锁4 if (CAS成功) {5 获取锁成功;6 break;7 }8 9 // 2. CAS 失败,自旋重试10 if (自旋次数 < 最大自旋次数) {11 自旋次数++;12 continue; // 继续自旋13 } else {14 // 3. 自旋失败,升级为重量级锁15 升级为重量级锁();16 阻塞等待();17 break;18 }19}自旋策略:
1. 固定次数自旋(JDK 1.6 之前)
1// 默认自旋10次2-XX:PreBlockSpin=102. 自适应自旋(JDK 1.6+)
- 根据历史自旋成功率动态调整
- 如果上次自旋成功,允许更多次自旋
- 如果某个锁自旋很少成功,可能直接省略自旋
自旋的条件:
- CPU 核心数 > 1(单核 CPU 自旋无意义)
- 锁持有时间短
- 竞争不激烈
JVM 参数控制:
1# 开启/关闭自旋锁优化(默认开启)2-XX:+UseSpinning3-XX:-UseSpinning45# 设置自旋次数(JDK 1.6 之前)6-XX:PreBlockSpin=1078# 开启自适应自旋(JDK 1.6+ 默认开启)9-XX:+UseAdaptiveSpinning34. Synchronized 能不能禁止指令重排序?
答案:
能。synchronized 可以禁止指令重排序,保证有序性。
原理:
synchronized 通过以下机制保证有序性:
-
happens-before 规则
- 对一个锁的解锁 happens-before 于后续对这个锁的加锁
- 保证解锁前的所有操作对加锁后的操作可见
-
内存屏障
- 进入 synchronized 块时插入 Load 屏障
- 退出 synchronized 块时插入 Store 屏障
- 防止临界区内的指令重排到临界区外
示例:
1public class SynchronizedOrdering {2 private int a = 0;3 private int b = 0;4 5 public synchronized void method1() {6 a = 1; // 操作17 b = 2; // 操作28 }9 10 public synchronized void method2() {11 int x = b; // 操作312 int y = a; // 操作413 }14}保证:
- 操作1和操作2不会重排到 synchronized 块外
- 操作3和操作4不会重排到 synchronized 块外
- 如果线程1先执行 method1,线程2后执行 method2,则线程2一定能看到 a=1, b=2
与 volatile 的区别:
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | 保证 | 不保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证 |
| 性能 | 较低 | 较高 |
| 适用场景 | 复合操作 | 单个变量 |
35. 当 Java 的 synchronized 升级到重量级锁后,所有线程都释放锁了,此时它还是重量级锁吗?
答案:
是的,锁升级是单向的,不会降级(在大多数 JVM 实现中)。
原因:
-
性能考虑
- 锁降级需要额外的检测和处理逻辑
- 降级过程本身有开销
- 如果频繁升级降级,反而降低性能
-
实现复杂度
- 需要判断何时降级
- 需要处理降级过程中的并发问题
- 增加 JVM 实现复杂度
-
实际场景
- 一旦升级为重量级锁,说明存在竞争
- 竞争可能再次发生
- 保持重量级锁状态更合理
锁升级路径(单向):
1无锁 → 偏向锁 → 轻量级锁 → 重量级锁2 ↓ ↓ ↓3 不可逆 不可逆 不可逆特殊情况:
某些 JVM 实现(如 JDK 15+ 的 ZGC)在 GC 时可能会重置锁状态,但这不是常规的锁降级机制。
JVM 参数:
1# 禁用偏向锁(直接从无锁到轻量级锁)2-XX:-UseBiasedLocking34# 偏向锁延迟启动时间(默认4秒)5-XX:BiasedLockingStartupDelay=0最佳实践:
- 避免不必要的锁竞争
- 减少锁持有时间
- 使用合适的并发工具类
- 考虑使用 ReentrantLock(可以更灵活地控制)
36. 什么是 Java 中的锁自适应自旋?
答案:
锁自适应自旋是 JDK 1.6 引入的优化技术,根据历史自旋成功率动态调整自旋次数。
传统自旋(固定次数):
1// JDK 1.6 之前,固定自旋10次2for (int i = 0; i < 10; i++) {3 if (tryAcquireLock()) {4 return; // 获取成功5 }6}7// 自旋失败,阻塞自适应自旋:
1// 根据历史记录动态调整2if (上次在这个锁上自旋成功) {3 允许更长时间的自旋;4} else if (上次自旋失败) {5 减少自旋时间或直接阻塞;6}78// 如果某个锁很少自旋成功,可能直接跳过自旋9if (这个锁历史上自旋成功率很低) {10 直接阻塞,不自旋;11}自适应策略:
-
基于锁的历史
- 记录每个锁的自旋成功率
- 成功率高的锁允许更多自旋
- 成功率低的锁减少或跳过自旋
-
基于线程的历史
- 如果线程最近自旋成功过,允许更多自旋
- 如果线程最近自旋总是失败,减少自旋
-
基于持有锁的线程状态
- 如果持有锁的线程正在运行,自旋等待
- 如果持有锁的线程已被挂起,直接阻塞
优点:
- 更智能的自旋策略
- 减少无效自旋,降低 CPU 浪费
- 提高锁的整体性能
JVM 参数:
1# 开启自适应自旋(JDK 1.6+ 默认开启)2-XX:+UseAdaptiveSpinning34# 关闭自适应自旋5-XX:-UseAdaptiveSpinning示例场景:
1// 场景1:锁持有时间短,竞争少2// 自适应自旋会增加自旋次数,提高性能34// 场景2:锁持有时间长,竞争激烈5// 自适应自旋会减少自旋次数,避免 CPU 浪费67// 场景3:某个锁从不自旋成功8// 自适应自旋会跳过自旋,直接阻塞37. Synchronized 和 ReentrantLock 有什么区别?
答案:
对比总结:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置(关键字) | JDK 实现(类) |
| 锁的获取/释放 | 自动 | 手动(需要 try-finally) |
| 可中断 | 不支持 | 支持(lockInterruptibly) |
| 超时获取 | 不支持 | 支持(tryLock(timeout)) |
| 公平锁 | 非公平 | 支持公平/非公平 |
| 条件变量 | 单个(wait/notify) | 多个(Condition) |
| 可重入 | 支持 | 支持 |
| 锁状态查询 | 不支持 | 支持(isLocked、getHoldCount) |
| 性能 | JDK 1.6+ 优化后相当 | 相当 |
1. 使用方式
1// synchronized:自动释放2public synchronized void method() {3 // 临界区4}56// ReentrantLock:手动释放7ReentrantLock lock = new ReentrantLock();8lock.lock();9try {10 // 临界区11} finally {12 lock.unlock(); // 必须在 finally 中释放13}2. 可中断
1// synchronized:不可中断2synchronized (lock) {3 // 无法响应中断4}56// ReentrantLock:可中断7try {8 lock.lockInterruptibly();9 // 可以响应中断10} catch (InterruptedException e) {11 // 处理中断12}3. 超时获取
1// synchronized:不支持超时23// ReentrantLock:支持超时4if (lock.tryLock(1, TimeUnit.SECONDS)) {5 try {6 // 获取锁成功7 } finally {8 lock.unlock();9 }10} else {11 // 获取锁超时12}4. 公平锁
1// synchronized:只支持非公平锁23// ReentrantLock:支持公平锁4ReentrantLock fairLock = new ReentrantLock(true);5. 条件变量
1// synchronized:单个条件变量2synchronized (lock) {3 lock.wait();4 lock.notify();5}67// ReentrantLock:多个条件变量8Condition condition1 = lock.newCondition();9Condition condition2 = lock.newCondition();10condition1.await();11condition1.signal();6. 锁状态查询
1// synchronized:不支持23// ReentrantLock:支持4boolean isLocked = lock.isLocked();5int holdCount = lock.getHoldCount();6boolean hasQueuedThreads = lock.hasQueuedThreads();选择建议:
- 简单场景:使用 synchronized(代码简洁)
- 需要高级功能:使用 ReentrantLock
- 可中断
- 超时获取
- 公平锁
- 多个条件变量
- 锁状态查询
38. Volatile 与 Synchronized 的区别是什么?
答案:
对比总结:
| 特性 | volatile | synchronized |
|---|---|---|
| 类型 | 关键字(变量修饰符) | 关键字(方法/代码块) |
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证(禁止重排) | 保证 |
| 阻塞 | 不阻塞 | 可能阻塞 |
| 性能 | 高 | 较低 |
| 适用场景 | 单个变量 | 复合操作 |
1. 原子性
1// volatile:不保证原子性2private volatile int count = 0;34public void increment() {5 count++; // 非原子操作,线程不安全6}78// synchronized:保证原子性9private int count = 0;1011public synchronized void increment() {12 count++; // 原子操作,线程安全13}2. 可见性
1// volatile:保证可见性2private volatile boolean flag = false;34// 线程15flag = true; // 立即对其他线程可见67// 线程28while (!flag) {9 // 能立即看到 flag 的变化10}1112// synchronized:也保证可见性13private boolean flag = false;1415synchronized (lock) {16 flag = true; // 解锁时刷新到主内存17}1819synchronized (lock) {20 if (flag) { // 加锁时从主内存读取21 // ...22 }23}3. 有序性
1// volatile:禁止指令重排2private volatile boolean initialized = false;3private int value;45// 线程16value = 10;7initialized = true; // 不会重排到 value = 10 之前89// 线程210if (initialized) {11 int x = value; // 一定能看到 value = 1012}1314// synchronized:也禁止指令重排15synchronized (lock) {16 value = 10;17 initialized = true;18}4. 使用场景
volatile 适用场景:
1// 1. 状态标志2private volatile boolean running = true;34public void stop() {5 running = false;6}78public void run() {9 while (running) {10 // 执行任务11 }12}1314// 2. 双重检查锁定(DCL)15private volatile static Singleton instance;1617public static Singleton getInstance() {18 if (instance == null) {19 synchronized (Singleton.class) {20 if (instance == null) {21 instance = new Singleton();22 }23 }24 }25 return instance;26}2728// 3. 独立观察29private volatile long lastUpdateTime;3031public void update() {32 // 更新数据33 lastUpdateTime = System.currentTimeMillis();34}synchronized 适用场景:
1// 1. 复合操作2private int count = 0;34public synchronized void increment() {5 count++; // 读-改-写6}78// 2. 多个变量的一致性9private int a = 0;10private int b = 0;1112public synchronized void update() {13 a++;14 b++; // 保证 a 和 b 同时更新15}1617// 3. 等待/通知机制18synchronized (lock) {19 while (!condition) {20 lock.wait();21 }22 // 执行操作23 lock.notify();24}性能对比:
- volatile:读写操作几乎无开销
- synchronized:有锁竞争时开销较大
选择建议:
- 单个变量的可见性:volatile
- 复合操作:synchronized 或 Lock
- 需要等待/通知:synchronized 或 Lock + Condition
39. 如何优化 Java 中的锁的使用?
答案:
1. 减少锁的持有时间
1// 不好的做法2public synchronized void method() {3 // 大量非临界区代码4 doSomething();5 // 临界区代码6 criticalSection();7 // 更多非临界区代码8 doMore();9}1011// 优化后12public void method() {13 doSomething();14 15 synchronized (lock) {16 // 只锁临界区17 criticalSection();18 }19 20 doMore();21}2. 减小锁的粒度
1// 不好的做法:锁整个对象2public class UserService {3 private synchronized void updateUser() { }4 private synchronized void updateOrder() { }5}67// 优化后:使用不同的锁8public class UserService {9 private final Object userLock = new Object();10 private final Object orderLock = new Object();11 12 public void updateUser() {13 synchronized (userLock) { }14 }15 16 public void updateOrder() {17 synchronized (orderLock) { }18 }19}3. 锁分段(Lock Striping)
1// ConcurrentHashMap 的实现思想2public class StripedMap {3 private static final int N_LOCKS = 16;4 private final Node[] buckets;5 private final Object[] locks;6 7 public StripedMap(int numBuckets) {8 buckets = new Node[numBuckets];9 locks = new Object[N_LOCKS];10 for (int i = 0; i < N_LOCKS; i++) {11 locks[i] = new Object();12 }13 }14 15 private final int hash(Object key) {16 return Math.abs(key.hashCode() % buckets.length);17 }18 19 public Object get(Object key) {20 int hash = hash(key);21 synchronized (locks[hash % N_LOCKS]) {22 // 访问 buckets[hash]23 }24 }25}4. 使用读写锁
1// 不好的做法:读写都用同一个锁2private final Object lock = new Object();34public Object read() {5 synchronized (lock) {6 return data;7 }8}910public void write(Object newData) {11 synchronized (lock) {12 data = newData;13 }14}1516// 优化后:使用读写锁17private final ReadWriteLock rwLock = new ReentrantReadWriteLock();1819public Object read() {20 rwLock.readLock().lock();21 try {22 return data;23 } finally {24 rwLock.readLock().unlock();25 }26}2728public void write(Object newData) {29 rwLock.writeLock().lock();30 try {31 data = newData;32 } finally {33 rwLock.writeLock().unlock();34 }35}5. 使用无锁数据结构
1// 不好的做法:使用锁2private int count = 0;34public synchronized void increment() {5 count++;6}78// 优化后:使用原子类9private AtomicInteger count = new AtomicInteger(0);1011public void increment() {12 count.incrementAndGet();13}6. 避免锁嵌套
1// 不好的做法:可能死锁2public synchronized void method1() {3 synchronized (lock2) {4 // ...5 }6}78public synchronized void method2() {9 synchronized (lock1) {10 // ...11 }12}1314// 优化后:统一加锁顺序或避免嵌套15public void method1() {16 synchronized (lock1) {17 synchronized (lock2) {18 // ...19 }20 }21}7. 使用 ConcurrentHashMap 替代 Hashtable
1// 不好的做法2Map<String, String> map = new Hashtable<>();34// 优化后5Map<String, String> map = new ConcurrentHashMap<>();8. 使用 ThreadLocal
1// 不好的做法:共享变量需要同步2private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-DD");34public synchronized String format(Date date) {5 return sdf.format(date);6}78// 优化后:每个线程独立9private ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(10 () -> new SimpleDateFormat("yyyy-MM-DD")11);1213public String format(Date date) {14 return sdf.get().format(date);15}9. 使用 StampedLock(读多写少)
1// 使用乐观读2private final StampedLock lock = new StampedLock();34public double read() {5 long stamp = lock.tryOptimisticRead();6 double value = this.value;7 8 if (!lock.validate(stamp)) {9 stamp = lock.readLock();10 try {11 value = this.value;12 } finally {13 lock.unlockRead(stamp);14 }15 }16 17 return value;18}10. 锁粗化(适当情况下)
1// 不好的做法:频繁加锁解锁2for (int i = 0; i < 1000; i++) {3 synchronized (lock) {4 // 操作5 }6}78// 优化后:锁粗化9synchronized (lock) {10 for (int i = 0; i < 1000; i++) {11 // 操作12 }13}40. 你了解 Java 中的读写锁吗?
答案:
读写锁(ReadWriteLock)允许多个读线程同时访问,但写线程访问时会阻塞所有读写线程。
核心思想:
- 读-读:不互斥
- 读-写:互斥
- 写-写:互斥
基本使用:
1public class Cache {2 private final Map<String, Object> cache = new HashMap<>();3 private final ReadWriteLock rwLock = new ReentrantReadWriteLock();4 private final Lock readLock = rwLock.readLock();5 private final Lock writeLock = rwLock.writeLock();6 7 // 读操作8 public Object get(String key) {9 readLock.lock();10 try {11 return cache.get(key);12 } finally {13 readLock.unlock();14 }15 }16 17 // 写操作18 public void put(String key, Object value) {19 writeLock.lock();20 try {21 cache.put(key, value);22 } finally {23 writeLock.unlock();24 }25 }26}特性:
1. 公平性
1// 非公平锁(默认)2ReadWriteLock rwLock = new ReentrantReadWriteLock();34// 公平锁5ReadWriteLock fairRwLock = new ReentrantReadWriteLock(true);2. 可重入
1// 同一线程可以多次获取读锁或写锁2readLock.lock();3readLock.lock(); // 可重入4try {5 // ...6} finally {7 readLock.unlock();8 readLock.unlock();9}3. 锁降级
1// 支持写锁降级为读锁2writeLock.lock();3try {4 // 写操作5 6 readLock.lock(); // 获取读锁7} finally {8 writeLock.unlock(); // 释放写锁,降级为读锁9}1011try {12 // 读操作13} finally {14 readLock.unlock();15}4. 不支持锁升级
1// 不支持读锁升级为写锁2readLock.lock();3try {4 writeLock.lock(); // 会死锁!5 // ...6} finally {7 readLock.unlock();8}应用场景:
1. 缓存系统
1public class DataCache {2 private Map<String, Data> cache = new HashMap<>();3 private ReadWriteLock rwLock = new ReentrantReadWriteLock();4 5 public Data getData(String key) {6 rwLock.readLock().lock();7 try {8 Data data = cache.get(key);9 if (data == null) {10 rwLock.readLock().unlock();11 rwLock.writeLock().lock();12 try {13 // 双重检查14 data = cache.get(key);15 if (data == null) {16 data = loadFromDatabase(key);17 cache.put(key, data);18 }19 rwLock.readLock().lock();20 } finally {21 rwLock.writeLock().unlock();22 }23 }24 return data;25 } finally {26 rwLock.readLock().unlock();27 }28 }29}2. 配置管理
1public class ConfigManager {2 private Properties config = new Properties();3 private ReadWriteLock rwLock = new ReentrantReadWriteLock();4 5 public String getProperty(String key) {6 rwLock.readLock().lock();7 try {8 return config.getProperty(key);9 } finally {10 rwLock.readLock().unlock();11 }12 }13 14 public void reload() {15 rwLock.writeLock().lock();16 try {17 config = loadConfig();18 } finally {19 rwLock.writeLock().unlock();20 }21 }22}性能对比:
| 场景 | synchronized | ReadWriteLock |
|---|---|---|
| 读多写少 | 性能较低 | 性能高 |
| 读少写多 | 性能相当 | 性能相当 |
| 只读 | 性能较低 | 性能高 |
最佳实践:
- 读多写少场景使用读写锁
- 读写比例相当时使用普通锁
- 考虑使用 StampedLock(性能更好)
41. 什么是 Java 内存模型(JMM)?
答案:
Java 内存模型(Java Memory Model,JMM)是一种规范,定义了 Java 程序中各个变量的访问规则,以及在 JVM 中将变量存储到内存和从内存中读取变量的底层细节。
核心概念:
1. 主内存与工作内存
1线程1 线程2 线程32工作内存 工作内存 工作内存3 ↓↑ ↓↑ ↓↑4 ←←←←←←←←←←←←←←←←←←←←←→→→→→→→→→→→→→→→→→→→→5 主内存6 (共享变量存储位置)- 主内存:所有线程共享,存储共享变量
- 工作内存:每个线程私有,存储主内存变量的副本
2. 内存交互操作
8 种原子操作:
- lock(锁定):作用于主内存,标识变量为线程独占
- unlock(解锁):作用于主内存,释放锁定的变量
- read(读取):作用于主内存,把变量值传输到工作内存
- load(载入):作用于工作内存,把 read 的值放入工作内存副本
- use(使用):作用于工作内存,把值传递给执行引擎
- assign(赋值):作用于工作内存,把执行引擎的值赋给工作内存变量
- store(存储):作用于工作内存,把值传送到主内存
- write(写入):作用于主内存,把 store 的值写入主内存变量
3. JMM 三大特性
原子性(Atomicity):
1// 原子操作2int a = 10; // 原子3a = b; // 非原子(读b + 写a)4a++; // 非原子(读a + 加1 + 写a)5a = a + 1; // 非原子67// 保证原子性8synchronized (lock) {9 a++;10}1112AtomicInteger count = new AtomicInteger(0);13count.incrementAndGet();可见性(Visibility):
1// 问题:线程2可能看不到线程1的修改2private boolean flag = false;34// 线程15flag = true;67// 线程28while (!flag) {9 // 可能一直循环10}1112// 解决方案1:volatile13private volatile boolean flag = false;1415// 解决方案2:synchronized16synchronized (lock) {17 flag = true;18}有序性(Ordering):
1// 可能发生指令重排2int a = 0;3boolean flag = false;45// 线程16a = 1; // 操作17flag = true; // 操作2(可能重排到操作1之前)89// 线程210if (flag) {11 int b = a; // 可能 b = 012}1314// 解决方案:volatile 禁止重排15private volatile boolean flag = false;4. happens-before 规则
如果操作 A happens-before 操作 B,则 A 的结果对 B 可见。
规则列表:
- 程序顺序规则:单线程内,前面的操作 happens-before 后面的操作
- 锁规则:unlock happens-before 后续对同一个锁的 lock
- volatile 规则:volatile 写 happens-before 后续对该变量的读
- 传递性:A happens-before B,B happens-before C,则 A happens-before C
- 线程启动规则:Thread.start() happens-before 线程内的所有操作
- 线程终止规则:线程内所有操作 happens-before Thread.join() 返回
- 中断规则:interrupt() happens-before 检测到中断事件
- 对象终结规则:构造函数结束 happens-before finalize() 方法
示例:
1// 程序顺序规则2int a = 1; // 操作13int b = 2; // 操作2 happens-before 操作145// 锁规则6synchronized (lock) {7 a = 1; // 操作18}9// unlock happens-before 下一个 lock1011synchronized (lock) {12 int b = a; // 能看到 a = 113}1415// volatile 规则16volatile boolean flag = false;17int a = 0;1819// 线程120a = 1;21flag = true; // volatile 写2223// 线程224if (flag) { // volatile 读25 int b = a; // 一定能看到 a = 126}5. 内存屏障
JMM 通过内存屏障(Memory Barrier)实现:
- LoadLoad 屏障:Load1; LoadLoad; Load2(确保 Load1 先于 Load2)
- StoreStore 屏障:Store1; StoreStore; Store2(确保 Store1 先于 Store2)
- LoadStore 屏障:Load1; LoadStore; Store2(确保 Load1 先于 Store2)
- StoreLoad 屏障:Store1; StoreLoad; Load2(确保 Store1 先于 Load2)
volatile 的内存屏障:
1// volatile 写2StoreStore 屏障3volatile 写操作4StoreLoad 屏障56// volatile 读7volatile 读操作8LoadLoad 屏障9LoadStore 屏障42. 什么是 Java 中的原子性、可见性和有序性?
答案:
这是并发编程的三大特性,JMM 的核心内容。
1. 原子性(Atomicity)
定义: 一个操作或多个操作要么全部执行成功,要么全部不执行,中间不会被打断。
原子操作:
1// 原子操作2int a = 10; // 赋值操作3int b = a; // 非原子(读a + 写b)4a++; // 非原子(读a + 加1 + 写a)5long c = 0L; // 非原子(64位,分两次写入)保证原子性的方式:
1// 1. synchronized2private int count = 0;34public synchronized void increment() {5 count++; // 保证原子性6}78// 2. Lock9private final Lock lock = new ReentrantLock();1011public void increment() {12 lock.lock();13 try {14 count++;15 } finally {16 lock.unlock();17 }18}1920// 3. 原子类21private AtomicInteger count = new AtomicInteger(0);2223public void increment() {24 count.incrementAndGet(); // 原子操作25}2. 可见性(Visibility)
定义: 当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
可见性问题:
1// 线程12private boolean flag = false;34public void writer() {5 flag = true; // 写入工作内存,可能不会立即刷新到主内存6}78// 线程29public void reader() {10 while (!flag) { // 从工作内存读取,可能读到旧值11 // 可能一直循环12 }13}保证可见性的方式:
1// 1. volatile2private volatile boolean flag = false;34// 2. synchronized5private boolean flag = false;67public synchronized void writer() {8 flag = true; // 解锁时刷新到主内存9}1011public synchronized void reader() {12 if (flag) { // 加锁时从主内存读取13 // ...14 }15}1617// 3. final18private final int value = 10; // final 变量保证可见性1920// 4. Lock21private final Lock lock = new ReentrantLock();2223public void writer() {24 lock.lock();25 try {26 flag = true;27 } finally {28 lock.unlock(); // 解锁时刷新29 }30}3. 有序性(Ordering)
定义: 程序执行的顺序按照代码的先后顺序执行。
指令重排问题:
1// 单例模式的双重检查锁定2public class Singleton {3 private static Singleton instance;4 5 public static Singleton getInstance() {6 if (instance == null) { // 第一次检查7 synchronized (Singleton.class) {8 if (instance == null) { // 第二次检查9 instance = new Singleton(); // 可能发生指令重排10 // 1. 分配内存11 // 2. 初始化对象12 // 3. 将 instance 指向内存13 // 可能重排为:1 -> 3 -> 214 }15 }16 }17 return instance;18 }19}2021// 问题:线程A执行到3,线程B看到 instance != null,但对象未初始化保证有序性的方式:
1// 1. volatile(禁止指令重排)2private static volatile Singleton instance;34// 2. synchronized(happens-before 规则)5synchronized (lock) {6 a = 1;7 b = 2; // 不会重排到 synchronized 块外8}910// 3. happens-before 规则11// 程序顺序规则、锁规则、volatile 规则等三者关系:
| 特性 | volatile | synchronized | Lock | Atomic |
|---|---|---|---|---|
| 原子性 | ✗ | ✓ | ✓ | ✓ |
| 可见性 | ✓ | ✓ | ✓ | ✓ |
| 有序性 | ✓ | ✓ | ✓ | ✓ |
实际应用:
1public class Counter {2 // 可见性:volatile3 private volatile boolean running = true;4 5 // 原子性:AtomicInteger6 private AtomicInteger count = new AtomicInteger(0);7 8 // 有序性 + 原子性:synchronized9 private int value = 0;10 11 public synchronized void updateValue() {12 value++; // 保证原子性和有序性13 }14 15 public void stop() {16 running = false; // 保证可见性17 }18 19 public void increment() {20 count.incrementAndGet(); // 保证原子性21 }22}43. 什么是 Java 的 happens-before 规则?
答案:
happens-before 是 JMM 中定义的两个操作之间的偏序关系,用于保证内存可见性。
定义: 如果操作 A happens-before 操作 B,那么 A 的执行结果对 B 可见,且 A 的执行顺序在 B 之前。
8 大 happens-before 规则:
1. 程序顺序规则(Program Order Rule)
1// 单线程内,前面的操作 happens-before 后面的操作2int a = 1; // 操作13int b = 2; // 操作2 happens-before 操作14int c = a + b; // 能看到 a=1, b=22. 锁规则(Monitor Lock Rule)
1// 对一个锁的解锁 happens-before 后续对这个锁的加锁2synchronized (lock) {3 a = 1; // 操作14} // unlock56// 另一个线程7synchronized (lock) { // lock8 int b = a; // 操作2,能看到 a=19}3. volatile 变量规则(Volatile Variable Rule)
1// volatile 写 happens-before 后续对该变量的读2volatile boolean flag = false;3int a = 0;45// 线程16a = 1; // 操作17flag = true; // volatile 写,操作289// 线程210if (flag) { // volatile 读,操作311 int b = a; // 操作4,能看到 a=112}13// 操作2 happens-before 操作314// 操作1 happens-before 操作2(程序顺序规则)15// 操作3 happens-before 操作4(程序顺序规则)16// 因此操作1 happens-before 操作44. 传递性规则(Transitivity)
1// A happens-before B,B happens-before C2// 则 A happens-before C3int a = 1; // 操作A4int b = a; // 操作B5int c = b; // 操作C6// A happens-before B happens-before C5. 线程启动规则(Thread Start Rule)
1// Thread.start() happens-before 线程内的所有操作2int a = 1; // 操作134Thread thread = new Thread(() -> {5 int b = a; // 操作2,能看到 a=16});78thread.start(); // happens-before 操作26. 线程终止规则(Thread Termination Rule)
1// 线程内所有操作 happens-before Thread.join() 返回2Thread thread = new Thread(() -> {3 a = 1; // 操作14});56thread.start();7thread.join(); // 等待线程结束89int b = a; // 操作2,能看到 a=110// 操作1 happens-before join() 返回7. 线程中断规则(Thread Interruption Rule)
1// interrupt() happens-before 检测到中断事件2Thread thread = new Thread(() -> {3 while (!Thread.interrupted()) { // 操作24 // 能检测到中断5 }6});78thread.start();9thread.interrupt(); // 操作1 happens-before 操作28. 对象终结规则(Finalizer Rule)
1// 对象的构造函数结束 happens-before finalize() 方法2public class MyObject {3 private int value;4 5 public MyObject() {6 value = 10; // 操作17 } // 构造函数结束8 9 @Override10 protected void finalize() {11 int a = value; // 操作2,能看到 value=1012 }13}实际应用示例:
1. 双重检查锁定(DCL)
1public class Singleton {2 private static volatile Singleton instance;3 4 public static Singleton getInstance() {5 if (instance == null) {6 synchronized (Singleton.class) {7 if (instance == null) {8 instance = new Singleton();9 // volatile 写 happens-before 后续读10 }11 }12 }13 return instance;14 }15}2. 生产者-消费者
1public class ProducerConsumer {2 private volatile boolean ready = false;3 private int data;4 5 // 生产者6 public void produce() {7 data = 42; // 操作18 ready = true; // volatile 写,操作29 }10 11 // 消费者12 public void consume() {13 if (ready) { // volatile 读,操作314 int value = data; // 操作4,能看到 data=4215 }16 }17}3. 线程安全的延迟初始化
1public class LazyInit {2 private volatile Helper helper;3 4 public Helper getHelper() {5 Helper h = helper;6 if (h == null) {7 synchronized (this) {8 h = helper;9 if (h == null) {10 h = new Helper();11 helper = h; // volatile 写12 }13 }14 }15 return h; // volatile 读,能看到完整初始化的对象16 }17}总结:
- happens-before 保证内存可见性
- 不需要显式同步也能保证可见性
- 是 JMM 的核心概念
- 理解 happens-before 有助于编写正确的并发代码
44. 什么是 Java 中的指令重排?
答案:
指令重排是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
三种重排:
1. 编译器优化重排
1// 原始代码2int a = 1;3int b = 2;4int c = a + b;56// 编译器可能重排为7int b = 2;8int a = 1;9int c = a + b;2. 指令级并行重排
1// CPU 可能并行执行不相关的指令2int a = 1; // 指令13int b = 2; // 指令2(可能与指令1并行)3. 内存系统重排
1// 写缓冲区可能导致写操作重排2a = 1; // 写入缓冲区3b = 2; // 可能先刷新到内存重排的原则:
as-if-serial 语义:
- 单线程内,重排后的执行结果与顺序执行的结果一致
- 不会对存在数据依赖的操作进行重排
1// 有数据依赖,不会重排2int a = 1;3int b = a + 1; // 依赖 a,不会重排到 a=1 之前45// 无数据依赖,可能重排6int a = 1;7int b = 2; // 可能重排重排导致的问题:
1. 双重检查锁定问题
1public class Singleton {2 private static Singleton instance;3 4 public static Singleton getInstance() {5 if (instance == null) {6 synchronized (Singleton.class) {7 if (instance == null) {8 instance = new Singleton();9 // 正常顺序:10 // 1. 分配内存11 // 2. 初始化对象12 // 3. instance 指向内存13 14 // 重排后可能:15 // 1. 分配内存16 // 3. instance 指向内存17 // 2. 初始化对象18 }19 }20 }21 return instance; // 可能返回未初始化的对象22 }23}2425// 解决方案:volatile26private static volatile Singleton instance;2. 可见性问题
1// 线程12int a = 0;3boolean flag = false;45public void writer() {6 a = 1; // 操作17 flag = true; // 操作2(可能重排到操作1之前)8}910// 线程211public void reader() {12 if (flag) { // 可能看到 flag=true13 int b = a; // 但 a 可能还是 014 }15}1617// 解决方案:volatile18private volatile boolean flag = false;3. 初始化安全问题
1public class UnsafeInit {2 private int value;3 private boolean initialized;4 5 public UnsafeInit() {6 value = 10; // 操作17 initialized = true; // 操作2(可能重排到操作1之前)8 }9 10 public int getValue() {11 if (initialized) { // 可能看到 initialized=true12 return value; // 但 value 可能还是 013 }14 return -1;15 }16}禁止重排的方式:
1. volatile
1// volatile 写之前的操作不会重排到 volatile 写之后2// volatile 读之后的操作不会重排到 volatile 读之前3private volatile boolean flag = false;45public void writer() {6 a = 1; // 不会重排到 flag=true 之后7 flag = true; // volatile 写8}910public void reader() {11 if (flag) { // volatile 读12 int b = a; // 不会重排到 flag 读之前13 }14}2. synchronized
1// synchronized 块内的操作不会重排到块外2synchronized (lock) {3 a = 1;4 b = 2; // 不会重排到 synchronized 块外5}3. final
1// final 字段的初始化不会重排到构造函数之外2public class FinalExample {3 private final int value;4 5 public FinalExample() {6 value = 10; // 不会重排到构造函数之外7 }8}4. happens-before 规则
1// 遵循 happens-before 规则的操作不会重排内存屏障:
JVM 通过插入内存屏障指令来禁止重排:
1// volatile 写2StoreStore 屏障 // 禁止前面的写与 volatile 写重排3volatile 写4StoreLoad 屏障 // 禁止 volatile 写与后面的读/写重排56// volatile 读7volatile 读8LoadLoad 屏障 // 禁止 volatile 读与后面的读重排9LoadStore 屏障 // 禁止 volatile 读与后面的写重排最佳实践:
- 使用 volatile 保证可见性和有序性
- 使用 synchronized 或 Lock 保证原子性和有序性
- 理解 happens-before 规则
- 避免依赖指令执行顺序
- 使用不可变对象
45. Java 中的 final 关键字是否能保证变量的可见性?
答案:
是的,final 关键字能保证变量的可见性,但有特定的条件和限制。
final 的可见性保证:
1. final 字段的初始化安全
1public class FinalExample {2 private final int value;3 private final String name;4 5 public FinalExample() {6 value = 10;7 name = "test";8 // 构造函数结束时,final 字段对其他线程可见9 }10}1112// 线程113FinalExample obj = new FinalExample();1415// 线程216int v = obj.value; // 一定能看到 value=1017String n = obj.name; // 一定能看到 name="test"2. final 的 happens-before 规则
1// 对象的构造函数中对 final 字段的写入2// happens-before3// 在构造函数外对该对象的 final 字段的读取45public class SafePublication {6 private final int value;7 8 public SafePublication() {9 value = 42; // final 写10 } // 构造函数结束11 12 public int getValue() {13 return value; // final 读,一定能看到 4214 }15}3. final 引用对象的可见性
1public class FinalReference {2 private final int[] array;3 4 public FinalReference() {5 array = new int[10];6 array[0] = 1; // 对 final 引用对象的修改也可见7 }8 9 public int getFirst() {10 return array[0]; // 一定能看到 array[0]=111 }12}final 的限制:
1. 不保证非 final 字段的可见性
1public class MixedFields {2 private final int finalValue;3 private int normalValue;4 5 public MixedFields() {6 finalValue = 10; // 保证可见7 normalValue = 20; // 不保证可见8 }9}1011// 其他线程可能看到 finalValue=10,但 normalValue=02. 不保证 final 字段修改后的可见性
1public class MutableFinal {2 private final List<String> list = new ArrayList<>();3 4 public void add(String item) {5 list.add(item); // 修改 final 引用的对象,不保证可见性6 }7}89// 解决方案:使用 volatile 或同步10private final List<String> list = new CopyOnWriteArrayList<>();3. 构造函数中的 this 逃逸
1// 错误示例:this 逃逸2public class ThisEscape {3 private final int value;4 5 public ThisEscape() {6 // 在构造函数中将 this 传递出去7 EventListener listener = new EventListener() {8 public void onEvent(Event e) {9 doSomething(e);10 }11 };12 13 source.registerListener(listener); // this 逃逸14 15 value = 42; // final 字段的可见性保证失效16 }17}1819// 正确做法:构造函数完成后再注册20public class SafeConstruction {21 private final int value;22 23 private SafeConstruction() {24 value = 42;25 }26 27 public static SafeConstruction newInstance() {28 SafeConstruction obj = new SafeConstruction();29 source.registerListener(obj); // 构造完成后注册30 return obj;31 }32}final 与 volatile 的对比:
| 特性 | final | volatile |
|---|---|---|
| 可见性 | 保证(初始化) | 保证(所有操作) |
| 有序性 | 保证(初始化) | 保证(所有操作) |
| 可修改 | 不可修改 | 可修改 |
| 性能 | 无开销 | 有轻微开销 |
| 适用场景 | 不可变字段 | 可变状态标志 |
实际应用:
1. 不可变对象
1public final class ImmutablePoint {2 private final int x;3 private final int y;4 5 public ImmutablePoint(int x, int y) {6 this.x = x;7 this.y = y;8 }9 10 public int getX() { return x; }11 public int getY() { return y; }12}13// 线程安全,无需同步2. 安全发布
1public class SafePublisher {2 private final Map<String, String> config;3 4 public SafePublisher() {5 Map<String, String> temp = new HashMap<>();6 temp.put("key1", "value1");7 temp.put("key2", "value2");8 config = Collections.unmodifiableMap(temp);9 // final 保证 config 对其他线程可见10 }11}3. 单例模式
1public class Singleton {2 private static final Singleton INSTANCE = new Singleton();3 4 private Singleton() {5 // 初始化6 }7 8 public static Singleton getInstance() {9 return INSTANCE; // final 保证可见性10 }11}最佳实践:
- 尽可能使用 final 修饰不可变字段
- 避免构造函数中的 this 逃逸
- final 引用的可变对象仍需同步
- 结合不可变模式使用
- 理解 final 的可见性保证范围
46. 为什么在 Java 中需要使用 ThreadLocal?
答案:
ThreadLocal 提供线程局部变量,每个线程都有自己独立的变量副本,互不干扰。
使用场景:
1. 线程安全的日期格式化
1// 问题:SimpleDateFormat 不是线程安全的2private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");34public String format(Date date) {5 return sdf.format(date); // 多线程下会出错6}78// 解决方案:ThreadLocal9private static final ThreadLocal<SimpleDateFormat> sdf = 10 ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));1112public String format(Date date) {13 return sdf.get().format(date); // 每个线程独立14}2. 数据库连接管理
1public class ConnectionManager {2 private static final ThreadLocal<Connection> connectionHolder = 3 new ThreadLocal<>();4 5 public static Connection getConnection() {6 Connection conn = connectionHolder.get();7 if (conn == null) {8 conn = DriverManager.getConnection(url, user, password);9 connectionHolder.set(conn);10 }11 return conn;12 }13 14 public static void closeConnection() {15 Connection conn = connectionHolder.get();16 if (conn != null) {17 conn.close();18 connectionHolder.remove(); // 清理19 }20 }21}3. 用户上下文传递
1public class UserContext {2 private static final ThreadLocal<User> currentUser = new ThreadLocal<>();3 4 public static void setUser(User user) {5 currentUser.set(user);6 }7 8 public static User getUser() {9 return currentUser.get();10 }11 12 public static void clear() {13 currentUser.remove();14 }15}1617// 在过滤器中设置18public class AuthFilter implements Filter {19 public void doFilter(ServletRequest request, ServletResponse response, 20 FilterChain chain) {21 User user = authenticate(request);22 UserContext.setUser(user);23 try {24 chain.doFilter(request, response);25 } finally {26 UserContext.clear(); // 清理27 }28 }29}4. 事务管理
1public class TransactionManager {2 private static final ThreadLocal<Transaction> transactionHolder = 3 new ThreadLocal<>();4 5 public static void beginTransaction() {6 Transaction tx = new Transaction();7 transactionHolder.set(tx);8 }9 10 public static Transaction getCurrentTransaction() {11 return transactionHolder.get();12 }13 14 public static void commit() {15 Transaction tx = transactionHolder.get();16 if (tx != null) {17 tx.commit();18 transactionHolder.remove();19 }20 }21}优点:
- 线程安全,无需同步
- 避免参数传递
- 性能好,无锁竞争
缺点:
- 内存泄漏风险(需要手动清理)
- 不适合线程池场景(线程复用)
- 增加内存开销
47. Java 中的 ThreadLocal 是如何实现线程资源隔离的?
答案:
ThreadLocal 通过在每个线程内部维护一个 Map 来实现资源隔离。
实现原理:
1. 数据结构
1// Thread 类中的字段2public class Thread {3 ThreadLocal.ThreadLocalMap threadLocals = null;4}56// ThreadLocalMap 是 ThreadLocal 的内部类7static class ThreadLocalMap {8 static class Entry extends WeakReference<ThreadLocal<?>> {9 Object value;10 11 Entry(ThreadLocal<?> k, Object v) {12 super(k);13 value = v;14 }15 }16 17 private Entry[] table; // 哈希表18}2. set 方法
1public void set(T value) {2 Thread t = Thread.currentThread(); // 获取当前线程3 ThreadLocalMap map = getMap(t); // 获取线程的 Map4 if (map != null)5 map.set(this, value); // 以 ThreadLocal 为 key6 else7 createMap(t, value); // 创建 Map8}910ThreadLocalMap getMap(Thread t) {11 return t.threadLocals;12}3. get 方法
1public T get() {2 Thread t = Thread.currentThread();3 ThreadLocalMap map = getMap(t);4 if (map != null) {5 ThreadLocalMap.Entry e = map.getEntry(this);6 if (e != null) {7 return (T)e.value;8 }9 }10 return setInitialValue(); // 初始化11}4. remove 方法
1public void remove() {2 ThreadLocalMap m = getMap(Thread.currentThread());3 if (m != null)4 m.remove(this);5}内存结构:
1Thread-12 └── threadLocals (ThreadLocalMap)3 ├── Entry[0]: ThreadLocal1 -> Value14 ├── Entry[1]: ThreadLocal2 -> Value25 └── Entry[2]: null67Thread-28 └── threadLocals (ThreadLocalMap)9 ├── Entry[0]: ThreadLocal1 -> Value310 └── Entry[1]: ThreadLocal2 -> Value4哈希冲突解决:
1// 使用线性探测法2private void set(ThreadLocal<?> key, Object value) {3 Entry[] tab = table;4 int len = tab.length;5 int i = key.threadLocalHashCode & (len-1); // 计算索引6 7 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {8 ThreadLocal<?> k = e.get();9 10 if (k == key) {11 e.value = value; // 更新12 return;13 }14 15 if (k == null) {16 replaceStaleEntry(key, value, i); // 替换过期 Entry17 return;18 }19 }20 21 tab[i] = new Entry(key, value); // 插入新 Entry22 int sz = ++size;23 if (!cleanSomeSlots(i, sz) && sz >= threshold)24 rehash(); // 扩容25}示例:
1public class ThreadLocalDemo {2 private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();3 4 public static void main(String[] args) {5 // 线程16 new Thread(() -> {7 threadLocal.set(100);8 System.out.println("Thread-1: " + threadLocal.get()); // 1009 }).start();10 11 // 线程212 new Thread(() -> {13 threadLocal.set(200);14 System.out.println("Thread-2: " + threadLocal.get()); // 20015 }).start();16 17 // 主线程18 threadLocal.set(300);19 System.out.println("Main: " + threadLocal.get()); // 30020 }21}48. 为什么 Java 中的 ThreadLocal 对 key 的引用为弱引用?
答案:
ThreadLocal 使用弱引用作为 key 是为了避免内存泄漏。
弱引用的定义:
1static class Entry extends WeakReference<ThreadLocal<?>> {2 Object value;3 4 Entry(ThreadLocal<?> k, Object v) {5 super(k); // ThreadLocal 作为弱引用6 value = v; // value 是强引用7 }8}引用关系:
1栈中的引用 → ThreadLocal 对象 ← 弱引用 ← Entry2 ↓3 Value(强引用)为什么使用弱引用:
1. 避免 ThreadLocal 对象无法回收
1// 如果使用强引用2public void method() {3 ThreadLocal<String> local = new ThreadLocal<>();4 local.set("value");5 // method 结束后,local 变量被回收6 // 但如果 Entry 持有 ThreadLocal 的强引用7 // ThreadLocal 对象无法被 GC 回收8}910// 使用弱引用11// method 结束后,local 变量被回收12// ThreadLocal 对象没有强引用,可以被 GC 回收13// Entry 的 key 变为 null2. 自动清理机制
1// ThreadLocalMap 在操作时会清理 key 为 null 的 Entry2private int expungeStaleEntry(int staleSlot) {3 Entry[] tab = table;4 int len = tab.length;5 6 // 清理当前位置7 tab[staleSlot].value = null;8 tab[staleSlot] = null;9 size--;10 11 // 清理后续位置的过期 Entry12 Entry e;13 int i;14 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; 15 i = nextIndex(i, len)) {16 ThreadLocal<?> k = e.get();17 if (k == null) {18 e.value = null;19 tab[i] = null;20 size--;21 } else {22 // rehash23 }24 }25 return i;26}内存泄漏问题:
问题场景:
1// ThreadLocal 被回收,key 变为 null2// 但 value 仍然被 Entry 强引用3// 如果线程长期存活(如线程池),value 无法回收45ThreadLocal<byte[]> local = new ThreadLocal<>();6local.set(new byte[1024 * 1024]); // 1MB7local = null; // ThreadLocal 被回收89// Entry: key=null, value=1MB 数据10// value 无法被回收,造成内存泄漏解决方案:
1// 1. 手动调用 remove()2ThreadLocal<String> local = new ThreadLocal<>();3try {4 local.set("value");5 // 使用 local6} finally {7 local.remove(); // 清理8}910// 2. 使用 try-with-resources(自定义)11public class AutoCleanThreadLocal<T> implements AutoCloseable {12 private ThreadLocal<T> threadLocal = new ThreadLocal<>();13 14 public void set(T value) {15 threadLocal.set(value);16 }17 18 public T get() {19 return threadLocal.get();20 }21 22 @Override23 public void close() {24 threadLocal.remove();25 }26}2728// 使用29try (AutoCleanThreadLocal<String> local = new AutoCleanThreadLocal<>()) {30 local.set("value");31 // 使用 local32} // 自动清理引用类型对比:
| 引用类型 | 回收时机 | 适用场景 |
|---|---|---|
| 强引用 | 永不回收 | 正常对象 |
| 软引用 | 内存不足时 | 缓存 |
| 弱引用 | GC 时 | ThreadLocal key |
| 虚引用 | GC 时 | 回收通知 |
49. Java 中使用 ThreadLocal 的最佳实践是什么?
答案:
1. 使用 static final 修饰
1// 推荐:static final2private static final ThreadLocal<SimpleDateFormat> sdf = 3 ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));45// 不推荐:实例变量6private ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<>();原因:
- ThreadLocal 通常作为全局变量使用
- 避免创建多个 ThreadLocal 实例
- 减少内存开销
2. 及时清理(remove)
1// 推荐:使用 try-finally2ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();34public void doWork() {5 try {6 Connection conn = getConnection();7 connectionHolder.set(conn);8 // 执行业务逻辑9 } finally {10 connectionHolder.remove(); // 清理11 }12}1314// 在过滤器中使用15public class RequestContextFilter implements Filter {16 private static final ThreadLocal<RequestContext> contextHolder = 17 new ThreadLocal<>();18 19 @Override20 public void doFilter(ServletRequest request, ServletResponse response,21 FilterChain chain) throws IOException, ServletException {22 try {23 contextHolder.set(new RequestContext(request));24 chain.doFilter(request, response);25 } finally {26 contextHolder.remove(); // 清理27 }28 }29}3. 使用 initialValue 或 withInitial
1// 方式1:重写 initialValue2private static final ThreadLocal<SimpleDateFormat> sdf = 3 new ThreadLocal<SimpleDateFormat>() {4 @Override5 protected SimpleDateFormat initialValue() {6 return new SimpleDateFormat("yyyy-MM-dd");7 }8 };910// 方式2:使用 withInitial(推荐)11private static final ThreadLocal<SimpleDateFormat> sdf = 12 ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));4. 避免在线程池中使用
1// 问题:线程池中线程复用,ThreadLocal 数据可能污染2ExecutorService executor = Executors.newFixedThreadPool(10);34executor.submit(() -> {5 threadLocal.set("value1");6 // 任务执行完,线程返回线程池7 // threadLocal 中的数据仍然存在8});910executor.submit(() -> {11 String value = threadLocal.get(); // 可能获取到上一个任务的数据12});1314// 解决方案:每次使用前清理15executor.submit(() -> {16 try {17 threadLocal.remove(); // 清理旧数据18 threadLocal.set("value");19 // 执行任务20 } finally {21 threadLocal.remove(); // 清理22 }23});5. 不要存储大对象
1// 不推荐:存储大对象2private static final ThreadLocal<byte[]> largeData = new ThreadLocal<>();3largeData.set(new byte[10 * 1024 * 1024]); // 10MB45// 推荐:存储引用或小对象6private static final ThreadLocal<String> userId = new ThreadLocal<>();7userId.set("user123");6. 文档化使用场景
1/**2 * 存储当前请求的用户信息3 * 注意:必须在请求结束时调用 clear() 清理4 */5private static final ThreadLocal<User> currentUser = new ThreadLocal<>();67public static void setUser(User user) {8 currentUser.set(user);9}1011public static User getUser() {12 return currentUser.get();13}1415/**16 * 清理当前线程的用户信息17 * 必须在请求结束时调用18 */19public static void clear() {20 currentUser.remove();21}7. 监控和检测
1public class ThreadLocalMonitor {2 private static final ThreadLocal<Long> startTime = new ThreadLocal<>();3 4 public static void start() {5 startTime.set(System.currentTimeMillis());6 }7 8 public static void end() {9 Long start = startTime.get();10 if (start != null) {11 long duration = System.currentTimeMillis() - start;12 System.out.println("Duration: " + duration + "ms");13 startTime.remove(); // 清理14 }15 }16}8. 使用包装类
1public class ThreadLocalContext<T> {2 private final ThreadLocal<T> threadLocal;3 4 public ThreadLocalContext(Supplier<T> supplier) {5 this.threadLocal = ThreadLocal.withInitial(supplier);6 }7 8 public T get() {9 return threadLocal.get();10 }11 12 public void set(T value) {13 threadLocal.set(value);14 }15 16 public void remove() {17 threadLocal.remove();18 }19 20 // 自动清理21 public void execute(Consumer<T> action) {22 try {23 action.accept(get());24 } finally {25 remove();26 }27 }28}50. Java 中的 InheritableThreadLocal 是什么?
答案:
InheritableThreadLocal 是 ThreadLocal 的子类,允许子线程继承父线程的 ThreadLocal 值。
基本使用:
1// ThreadLocal:子线程无法访问父线程的值2private static ThreadLocal<String> threadLocal = new ThreadLocal<>();34public static void main(String[] args) {5 threadLocal.set("父线程的值");6 7 new Thread(() -> {8 System.out.println(threadLocal.get()); // null9 }).start();10}1112// InheritableThreadLocal:子线程可以访问父线程的值13private static InheritableThreadLocal<String> inheritableThreadLocal = 14 new InheritableThreadLocal<>();1516public static void main(String[] args) {17 inheritableThreadLocal.set("父线程的值");18 19 new Thread(() -> {20 System.out.println(inheritableThreadLocal.get()); // 父线程的值21 }).start();22}实现原理:
1// Thread 类中的字段2public class Thread {3 ThreadLocal.ThreadLocalMap threadLocals = null;4 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 可继承的5}67// Thread 构造函数8private void init(ThreadGroup g, Runnable target, String name, 9 long stackSize, AccessControlContext acc, 10 boolean inheritThreadLocals) {11 Thread parent = currentThread();12 13 // 如果父线程有 inheritableThreadLocals,复制给子线程14 if (inheritThreadLocals && parent.inheritableThreadLocals != null)15 this.inheritableThreadLocals = 16 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);17}自定义继承逻辑:
1// 重写 childValue 方法2private static InheritableThreadLocal<List<String>> inheritableThreadLocal = 3 new InheritableThreadLocal<List<String>>() {4 @Override5 protected List<String> childValue(List<String> parentValue) {6 // 返回父线程值的副本,避免共享7 return new ArrayList<>(parentValue);8 }9 };1011public static void main(String[] args) {12 List<String> list = new ArrayList<>();13 list.add("item1");14 inheritableThreadLocal.set(list);15 16 new Thread(() -> {17 List<String> childList = inheritableThreadLocal.get();18 childList.add("item2"); // 不影响父线程的 list19 System.out.println(childList); // [item1, item2]20 }).start();21 22 Thread.sleep(100);23 System.out.println(list); // [item1]24}应用场景:
1. 链路追踪
1public class TraceContext {2 private static final InheritableThreadLocal<String> traceId = 3 new InheritableThreadLocal<>();4 5 public static void setTraceId(String id) {6 traceId.set(id);7 }8 9 public static String getTraceId() {10 return traceId.get();11 }12}1314// 主线程15TraceContext.setTraceId("trace-123");1617// 子线程自动继承18new Thread(() -> {19 System.out.println(TraceContext.getTraceId()); // trace-12320 // 执行业务逻辑21}).start();2. 用户上下文传递
1public class UserContext {2 private static final InheritableThreadLocal<User> currentUser = 3 new InheritableThreadLocal<>();4 5 public static void setUser(User user) {6 currentUser.set(user);7 }8 9 public static User getUser() {10 return currentUser.get();11 }12}1314// 主线程设置用户15UserContext.setUser(new User("张三"));1617// 异步任务自动继承18CompletableFuture.runAsync(() -> {19 User user = UserContext.getUser(); // 张三20 // 执行异步任务21});局限性:
1. 线程池问题
1// 问题:线程池中线程复用,继承的值不会更新2ExecutorService executor = Executors.newFixedThreadPool(10);34inheritableThreadLocal.set("value1");5executor.submit(() -> {6 System.out.println(inheritableThreadLocal.get()); // value17});89inheritableThreadLocal.set("value2");10executor.submit(() -> {11 // 如果复用了之前的线程,仍然是 value112 System.out.println(inheritableThreadLocal.get()); // value1(错误)13});1415// 解决方案:使用 TransmittableThreadLocal(阿里开源)2. 内存泄漏
1// 子线程继承父线程的值,增加内存占用2// 需要及时清理3try {4 inheritableThreadLocal.set(value);5 // 创建子线程6} finally {7 inheritableThreadLocal.remove();8}对比总结:
| 特性 | ThreadLocal | InheritableThreadLocal |
|---|---|---|
| 子线程继承 | 否 | 是 |
| 性能 | 较好 | 稍差(需要复制) |
| 内存占用 | 较小 | 较大 |
| 适用场景 | 线程隔离 | 父子线程传递 |
51. ThreadLocal 的缺点?
答案:
1. 内存泄漏风险
- Entry 的 key 是弱引用,但 value 是强引用
- ThreadLocal 被回收后,value 仍然存在
- 线程长期存活(如线程池)会导致内存泄漏
2. 线程池场景下的问题
- 线程复用导致数据污染
- 上一个任务的数据可能影响下一个任务
3. 增加内存开销
- 每个线程都有独立的副本
- 大对象会占用大量内存
4. 不适合父子线程传递
- 普通 ThreadLocal 无法传递给子线程
- InheritableThreadLocal 在线程池中失效
5. 调试困难
- 数据隐藏在线程内部
- 难以追踪数据流向
6. 容易被滥用
- 过度使用导致代码难以理解
- 隐式依赖增加耦合
52. 为什么 Netty 不使用 ThreadLocal 而是自定义了一个 FastThreadLocal?
答案:
Netty 的 FastThreadLocal 解决了 ThreadLocal 的性能问题。
ThreadLocal 的性能问题:
- 使用线性探测法解决哈希冲突,性能较差
- 需要清理过期 Entry,增加开销
- 哈希计算有开销
FastThreadLocal 的优化:
1. 使用数组代替哈希表
1// ThreadLocal:使用哈希表2ThreadLocalMap {3 Entry[] table; // 哈希表4 // 需要计算哈希值,处理冲突5}67// FastThreadLocal:使用数组8InternalThreadLocalMap {9 Object[] indexedVariables; // 直接数组访问10 // 每个 FastThreadLocal 有固定索引11}2. 直接索引访问
1// FastThreadLocal 有固定索引2public class FastThreadLocal<V> {3 private final int index; // 固定索引4 5 public final V get() {6 return get(InternalThreadLocalMap.get());7 }8 9 public final V get(InternalThreadLocalMap threadLocalMap) {10 Object v = threadLocalMap.indexedVariable(index); // 直接访问11 if (v != InternalThreadLocalMap.UNSET) {12 return (V) v;13 }14 return initialize(threadLocalMap);15 }16}3. 配合 FastThreadLocalThread 使用
1// Netty 的线程类2public class FastThreadLocalThread extends Thread {3 private InternalThreadLocalMap threadLocalMap;4 5 public final InternalThreadLocalMap threadLocalMap() {6 return threadLocalMap;7 }8 9 public final void setThreadLocalMap(InternalThreadLocalMap threadLocalMap) {10 this.threadLocalMap = threadLocalMap;11 }12}性能对比:
- ThreadLocal:O(n) 最坏情况(线性探测)
- FastThreadLocal:O(1) 直接访问
使用示例:
1// 定义 FastThreadLocal2private static final FastThreadLocal<String> context = 3 new FastThreadLocal<String>() {4 @Override5 protected String initialValue() {6 return "default";7 }8 };910// 使用11context.set("value");12String value = context.get();13context.remove();53. 什么是 Java 的 TransmittableThreadLocal?
答案:
TransmittableThreadLocal(TTL)是阿里开源的 ThreadLocal 增强库,解决线程池场景下的上下文传递问题。
问题场景:
1// InheritableThreadLocal 在线程池中失效2InheritableThreadLocal<String> context = new InheritableThreadLocal<>();34context.set("value1");5executor.submit(() -> {6 System.out.println(context.get()); // value17});89context.set("value2");10executor.submit(() -> {11 // 如果复用了之前的线程,仍然是 value112 System.out.println(context.get()); // value1(错误)13});TTL 解决方案:
1// 使用 TransmittableThreadLocal2TransmittableThreadLocal<String> context = 3 new TransmittableThreadLocal<>();45// 包装线程池6ExecutorService executor = TtlExecutors.getTtlExecutorService(7 Executors.newFixedThreadPool(10)8);910context.set("value1");11executor.submit(() -> {12 System.out.println(context.get()); // value113});1415context.set("value2");16executor.submit(() -> {17 System.out.println(context.get()); // value2(正确)18});实现原理:
- 在任务提交时捕获当前线程的 TTL 值
- 在任务执行前恢复 TTL 值
- 在任务执行后清理 TTL 值
应用场景:
- 分布式链路追踪
- 日志 MDC 传递
- 用户上下文传递
54. Java 中 Thread.sleep 和 Thread.yield 的区别?
答案:
| 特性 | Thread.sleep | Thread.yield |
|---|---|---|
| 作用 | 让线程休眠指定时间 | 让出 CPU 时间片 |
| 状态 | TIMED_WAITING | RUNNABLE |
| 锁 | 不释放锁 | 不释放锁 |
| 时间 | 指定休眠时间 | 立即返回 |
| 保证 | 至少休眠指定时间 | 不保证让出 |
| 异常 | InterruptedException | 无 |
Thread.sleep:
1public static void sleep(long millis) throws InterruptedException23// 使用4try {5 Thread.sleep(1000); // 休眠1秒6} catch (InterruptedException e) {7 // 处理中断8}Thread.yield:
1public static native void yield();23// 使用4Thread.yield(); // 建议让出 CPU使用场景:
- sleep:需要暂停执行一段时间
- yield:降低线程优先级,让其他线程执行
55. Java 中 Thread.sleep(0) 的作用是什么?
答案:
Thread.sleep(0) 会触发操作系统重新调度线程,但不会让线程进入休眠状态。
作用:
- 让出当前时间片
- 触发线程调度
- 给其他线程执行机会
与 yield 的区别:
- sleep(0):触发操作系统调度
- yield():仅建议 JVM 调度
使用场景:
1// 在长时间循环中给其他线程机会2while (running) {3 // 执行任务4 doWork();5 6 // 避免独占 CPU7 Thread.sleep(0);8}56. Java 中什么情况会导致死锁?如何避免?
答案:
死锁的四个必要条件:
- 互斥:资源不能被共享
- 持有并等待:持有资源的同时等待其他资源
- 不可剥夺:资源不能被强制释放
- 循环等待:存在资源的循环等待链
死锁示例:
1public class DeadlockDemo {2 private static Object lock1 = new Object();3 private static Object lock2 = new Object();4 5 public static void main(String[] args) {6 // 线程1:先锁 lock1,再锁 lock27 new Thread(() -> {8 synchronized (lock1) {9 System.out.println("Thread1 获取 lock1");10 try { Thread.sleep(100); } catch (InterruptedException e) {}11 synchronized (lock2) {12 System.out.println("Thread1 获取 lock2");13 }14 }15 }).start();16 17 // 线程2:先锁 lock2,再锁 lock118 new Thread(() -> {19 synchronized (lock2) {20 System.out.println("Thread2 获取 lock2");21 try { Thread.sleep(100); } catch (InterruptedException e) {}22 synchronized (lock1) {23 System.out.println("Thread2 获取 lock1");24 }25 }26 }).start();27 }28}避免死锁的方法:
1. 固定加锁顺序
1// 统一加锁顺序2public void transfer(Account from, Account to, int amount) {3 Account first = from.id < to.id ? from : to;4 Account second = from.id < to.id ? to : from;5 6 synchronized (first) {7 synchronized (second) {8 from.debit(amount);9 to.credit(amount);10 }11 }12}2. 使用超时
1Lock lock1 = new ReentrantLock();2Lock lock2 = new ReentrantLock();34public void method() {5 try {6 if (lock1.tryLock(1, TimeUnit.SECONDS)) {7 try {8 if (lock2.tryLock(1, TimeUnit.SECONDS)) {9 try {10 // 执行操作11 } finally {12 lock2.unlock();13 }14 }15 } finally {16 lock1.unlock();17 }18 }19 } catch (InterruptedException e) {20 // 处理中断21 }22}3. 死锁检测
1// 使用 JMX 检测死锁2ThreadMXBean tmx = ManagementFactory.getThreadMXBean();3long[] deadlockedThreads = tmx.findDeadlockedThreads();4if (deadlockedThreads != null) {5 ThreadInfo[] threadInfos = tmx.getThreadInfo(deadlockedThreads);6 // 处理死锁7}57. Java 中 volatile 关键字的作用是什么?
答案:
volatile 保证变量的可见性和有序性,但不保证原子性。
作用:
- 保证可见性:修改立即对其他线程可见
- 禁止指令重排:保证有序性
- 不保证原子性:复合操作不是原子的
使用场景:
1// 1. 状态标志2private volatile boolean running = true;34public void stop() {5 running = false;6}78// 2. 双重检查锁定9private volatile static Singleton instance;1011// 3. 独立观察12private volatile long lastUpdateTime;58. 什么是 Java 中的 ABA 问题?
答案:
ABA 问题是指一个值从 A 变成 B,再变回 A,CAS 操作无法检测到这个变化。
问题示例:
1AtomicInteger value = new AtomicInteger(100);23// 线程1:读取值 1004int oldValue = value.get(); // 10056// 线程2:100 -> 200 -> 1007value.compareAndSet(100, 200);8value.compareAndSet(200, 100);910// 线程1:CAS 成功,但值已经被修改过11value.compareAndSet(oldValue, 101); // 成功解决方案:
1// 使用 AtomicStampedReference(版本号)2AtomicStampedReference<Integer> ref = 3 new AtomicStampedReference<>(100, 0);45int stamp = ref.getStamp();6ref.compareAndSet(100, 101, stamp, stamp + 1);59. 在 Java 中主线程如何知晓创建的子线程是否执行成功?
答案:
1. 使用 join()
1Thread thread = new Thread(() -> {2 // 执行任务3});4thread.start();5thread.join(); // 等待线程结束2. 使用 Future
1ExecutorService executor = Executors.newSingleThreadExecutor();2Future<String> future = executor.submit(() -> {3 return "结果";4});56try {7 String result = future.get(); // 获取结果8 System.out.println("成功: " + result);9} catch (ExecutionException e) {10 System.out.println("失败: " + e.getCause());11}3. 使用 CompletableFuture
1CompletableFuture.supplyAsync(() -> {2 return "结果";3}).whenComplete((result, ex) -> {4 if (ex != null) {5 System.out.println("失败: " + ex);6 } else {7 System.out.println("成功: " + result);8 }9});4. 使用 CountDownLatch
1CountDownLatch latch = new CountDownLatch(1);2AtomicBoolean success = new AtomicBoolean(false);34new Thread(() -> {5 try {6 // 执行任务7 success.set(true);8 } finally {9 latch.countDown();10 }11}).start();1213latch.await();14System.out.println("成功: " + success.get());60. 创建线程池有哪些方式?
答案:
1. Executors 工厂方法
1ExecutorService executor1 = Executors.newFixedThreadPool(5);2ExecutorService executor2 = Executors.newCachedThreadPool();3ExecutorService executor3 = Executors.newSingleThreadExecutor();4ScheduledExecutorService executor4 = Executors.newScheduledThreadPool(5);2. ThreadPoolExecutor 构造函数(推荐)
1ThreadPoolExecutor executor = new ThreadPoolExecutor(2 5, // 核心线程数3 10, // 最大线程数4 60L, // 空闲线程存活时间5 TimeUnit.SECONDS,6 new LinkedBlockingQueue<>(100),7 Executors.defaultThreadFactory(),8 new ThreadPoolExecutor.AbortPolicy()9);3. Spring 配置
1@Configuration2public class ThreadPoolConfig {3 @Bean4 public Executor taskExecutor() {5 ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();6 executor.setCorePoolSize(5);7 executor.setMaxPoolSize(10);8 executor.setQueueCapacity(100);9 executor.setThreadNamePrefix("async-");10 executor.initialize();11 return executor;12 }13}61. 线程安全的集合有哪些?
答案:
1. 同步集合(synchronized)
1Vector<String> vector = new Vector<>();2Hashtable<String, String> hashtable = new Hashtable<>();3Stack<String> stack = new Stack<>();2. 并发集合(java.util.concurrent)
1// Map2ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();34// List5CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();67// Set8CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();9ConcurrentSkipListSet<String> sortedSet = new ConcurrentSkipListSet<>();1011// Queue12ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();13LinkedBlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();3. Collections 工具类
1List<String> syncList = Collections.synchronizedList(new ArrayList<>());2Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());3Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());62. Java 创建线程有哪些方式?
答案:
1. 继承 Thread 类
1class MyThread extends Thread {2 @Override3 public void run() {4 System.out.println("Thread running");5 }6}78new MyThread().start();2. 实现 Runnable 接口
1class MyRunnable implements Runnable {2 @Override3 public void run() {4 System.out.println("Runnable running");5 }6}78new Thread(new MyRunnable()).start();3. 实现 Callable 接口
1class MyCallable implements Callable<String> {2 @Override3 public String call() throws Exception {4 return "Callable result";5 }6}78FutureTask<String> task = new FutureTask<>(new MyCallable());9new Thread(task).start();10String result = task.get();4. 使用线程池
1ExecutorService executor = Executors.newFixedThreadPool(5);2executor.submit(() -> System.out.println("Task"));3executor.shutdown();5. 使用 CompletableFuture
1CompletableFuture.runAsync(() -> {2 System.out.println("Async task");3});6. 使用 ForkJoinPool
1ForkJoinPool pool = new ForkJoinPool();2pool.submit(() -> System.out.println("ForkJoin task"));7. 使用 Timer
1Timer timer = new Timer();2timer.schedule(new TimerTask() {3 @Override4 public void run() {5 System.out.println("Timer task");6 }7}, 1000);总结
本文档涵盖了 Java 并发编程的 62 个核心面试题,包括:
- 线程基础:线程创建、生命周期、通信机制
- 线程池:原理、参数配置、拒绝策略
- 锁机制:synchronized、ReentrantLock、读写锁、AQS
- 并发工具:CountDownLatch、CyclicBarrier、Semaphore
- 并发容器:ConcurrentHashMap、阻塞队列
- 原子类:AtomicInteger、LongAdder、CAS
- 内存模型:JMM、happens-before、指令重排
- ThreadLocal:原理、使用、最佳实践
掌握这些知识点,能够帮助你在面试中脱颖而出,在实际工作中编写高质量的并发代码。
评论区 / Comments