【Java面试题】Java并发
Java 并发面试题汇总Java 并发是 Java 后端面试中的高频考点尤其是synchronized、volatile、CAS、AQS、ReentrantLock、线程池、ThreadLocal等。本文按照“线程基础 → Java 内存模型 → synchronized → volatile → CAS → AQS → Lock → 线程池 → ThreadLocal → 并发容器”的顺序进行整理方便面试前快速复习。一、线程基础1. 什么是进程什么是线程进程是操作系统进行资源分配的基本单位一个进程拥有独立的内存空间。线程是操作系统进行 CPU 调度的基本单位一个进程可以包含多个线程这些线程共享进程的内存资源。例如JVM 进程 │ ├── 主线程 ├── GC 线程 ├── JIT 编译线程 └── 其他业务线程面试回答进程是资源分配的基本单位线程是 CPU 调度的基本单位。同一个进程中的多个线程共享堆、方法区等资源但每个线程拥有自己的程序计数器、虚拟机栈和本地方法栈。2. 为什么要使用多线程主要有三个原因① 提高 CPU 利用率如果一个线程因为 IO 操作阻塞CPU 可以切换去执行其他线程。② 提高程序响应速度多个任务可以并发执行。③ 提高系统吞吐量例如 Web 服务中可以使用线程池同时处理多个用户请求。3. 创建线程有哪些方式常见方式方式一继承 ThreadclassMyThreadextendsThread{Overridepublicvoidrun(){System.out.println(执行任务);}}方式二实现 RunnableRunnabletask()-{System.out.println(执行任务);};newThread(task).start();方式三实现 CallableCallableIntegertask()-100;FutureTaskIntegerfutureTasknewFutureTask(task);newThread(futureTask).start();方式四使用线程池实际开发中更推荐线程池ExecutorServiceexecutorExecutors.newFixedThreadPool(10);executor.submit(()-{System.out.println(执行任务);});二、线程的生命周期Java 中线程主要有以下状态NEW │ │ start() ↓ RUNNABLE │ ├── BLOCKED ├── WAITING └── TIMED_WAITING │ ↓ TERMINATED4. NEW线程对象创建完成但是还没有调用start()。ThreadthreadnewThread();5. RUNNABLE调用start()后进入RUNNABLE状态。需要注意Java 中的 RUNNABLE 包含操作系统层面的“就绪”和“运行”两种状态。6. BLOCKED线程等待获取synchronized锁时如果锁已经被其他线程占用就会进入 BLOCKED。7. WAITING线程主动等待其他线程通知。例如ObjectlocknewObject();synchronized(lock){lock.wait();}8. TIMED_WAITING带时间限制的等待。例如Thread.sleep(1000);或者lock.wait(1000);三、并发编程存在什么问题并发编程最核心的三个问题可见性、原子性、有序性9. 什么是可见性一个线程修改共享变量之后其他线程能够立即看到修改结果。例如booleanflagfalse;ThreadA:flagtrue;ThreadB:while(!flag){}如果没有正确的内存同步机制线程 B 可能一直读取到旧值。10. 什么是原子性一个操作要么全部执行要么全部不执行中间不能被其他线程打断。例如count;看起来是一条语句实际上可以拆成读取 count ↓ count 1 ↓ 写回 count如果两个线程同时执行就可能发生数据覆盖。11. 什么是有序性程序执行过程中编译器、JIT 和 CPU 为了优化性能可能对指令进行重排序。在单线程环境下重排序通常不会影响最终结果。但是在多线程环境下如果没有正确的同步机制就可能产生问题。四、synchronized12. synchronized 是什么synchronized是 Java 提供的内置锁机制可以保证多个线程对共享资源的互斥访问。同时可以保证原子性可见性有序性例如publicsynchronizedvoidadd(){count;}13. synchronized 锁的是什么synchronized本质上是对对象监视器 Monitor进行加锁。例如synchronized(obj){// 临界区}锁住的是obj对象。14. synchronized 修饰实例方法锁的是谁publicsynchronizedvoidmethod(){}等价于publicvoidmethod(){synchronized(this){}}所以锁的是当前对象this15. synchronized 修饰静态方法锁的是谁publicstaticsynchronizedvoidmethod(){}锁的是当前类的 Class 对象。例如synchronized(MyClass.class){}16. synchronized 的锁升级过程早期 JVM 中经常介绍无锁 ↓ 偏向锁 ↓ 轻量级锁 ↓ 重量级锁不过需要注意JDK 15 开始偏向锁默认被禁用JDK 18 移除了偏向锁相关实现。因此面试时可以了解历史上的锁升级机制但不要把“偏向锁一定存在”当成当前 JVM 的绝对事实。传统锁升级主要是为了在不同竞争程度下用不同成本的锁实现提高性能。五、volatile17. volatile 是什么volatile是 Java 提供的轻量级同步机制可以保证可见性 禁止特定类型的指令重排序例如privatevolatilebooleanrunningtrue;一个线程修改runningfalse;其他线程能够及时看到最新值。18. volatile 能保证原子性吗不能。例如volatileintcount0;count;依然不是原子操作。因为读取 count ↓ count 1 ↓ 写回 count多个线程依然可能同时读取到相同的值。19. synchronized 和 volatile 有什么区别对比synchronizedvolatile原子性✅❌可见性✅✅有序性✅部分保证阻塞可能阻塞不会因为读写本身产生锁阻塞使用场景临界区、复合操作状态标志、简单共享变量一句话volatile适合解决共享变量的可见性问题synchronized更适合解决需要互斥执行的临界区问题。六、Java 内存模型 JMM20. 什么是 JMMJMM即 Java Memory ModelJava 内存模型。它不是简单指 JVM 中的某一块物理内存而是一套规范用于解决多线程环境下共享变量的可见性、原子性和有序性问题。线程拥有自己的工作内存主内存 / | \ / | \ 线程 A 线程 B 线程 C 工作内存 工作内存 工作内存线程对共享变量的操作需要通过主内存进行同步。七、happens-before21. 什么是 happens-beforehappens-before 是 JMM 中用于描述操作之间可见性关系的规则。如果A happens-before B那么意味着A 操作的结果对 B 操作可见。常见规则① 程序次序规则同一个线程中inta10;intba;前面的操作 happens-before 后面的操作。② 锁规则解锁操作 happens-before 后续对同一个锁的加锁操作。③ volatile 规则对 volatile 变量的写操作 happens-before 后续对该变量的读操作。④ 线程启动规则Thread.start()happens-before 新线程中的操作。⑤ 线程终止规则线程中的所有操作 happens-before 其他线程检测到该线程已经终止。例如thread.join();八、CAS22. 什么是 CASCASCompare And Swap比较并交换。它是一种乐观锁思想。CAS 包含三个值内存地址 旧值 新值执行逻辑当前值 预期旧值 │ ┌────┴────┐ │ │ 是 否 │ │ 修改新值 修改失败23. CAS 有什么优点CAS 不需要像传统互斥锁一样让线程阻塞等待。因此在竞争不激烈的情况下性能较好。Java 中很多原子类底层都会使用 CAS。例如AtomicIntegercountnewAtomicInteger();count.incrementAndGet();24. CAS 有什么问题主要有两个① ABA 问题假设线程 A读取 A 线程 BA → B 线程 BB → A 线程 A发现还是 A于是 CAS 成功线程 A 无法感知中间发生过变化。解决方式使用带版本号的 CAS。例如AtomicStampedReference② 自旋开销CAS 失败后可能不断重试CAS失败 ↓ 重新读取 ↓ CAS ↓ 失败 ↓ 继续重试如果竞争非常激烈会消耗 CPU。九、Atomic 原子类25. AtomicInteger 是什么AtomicInteger是 Java 并发包提供的原子操作类。例如AtomicIntegercountnewAtomicInteger();count.incrementAndGet();相比count;可以保证自增操作的原子性。26. AtomicInteger 为什么线程安全底层主要依赖CAS volatile例如读取当前值 ↓ 计算新值 ↓ CAS 更新 ↓ 失败则重试十、AQS27. 什么是 AQSAQSAbstractQueuedSynchronizer抽象队列同步器。它是 Java 并发包中非常核心的基础组件。很多并发工具都是基于 AQS 实现的例如ReentrantLockReentrantReadWriteLockSemaphoreCountDownLatch28. AQS 的核心思想是什么AQS 核心可以理解成一个 state 一个等待队列例如AQS │ ┌─────┴─────┐ │ │ state CLH队列 │ ┌──────┼──────┐ ↓ ↓ ↓ 线程A 线程B 线程Cstate用来表示同步状态。当线程获取资源失败后就进入等待队列。29. AQS 为什么需要队列如果线程获取锁失败不能让所有线程一直 CAS 自旋否则会浪费大量 CPU。所以 AQS 会获取资源失败 ↓ 进入等待队列 ↓ 阻塞线程 ↓ 资源释放 ↓ 唤醒后继节点这样可以降低 CPU 消耗。十一、ReentrantLock30. ReentrantLock 是什么ReentrantLock是 Java 提供的可重入锁。例如ReentrantLocklocknewReentrantLock();lock.lock();try{// 临界区}finally{lock.unlock();}31. ReentrantLock 和 synchronized 有什么区别对比synchronizedReentrantLock使用方式JVM 内置Java API可重入✅✅公平锁不支持显式设置支持尝试获取锁不方便tryLock()可中断支持一定形式lockInterruptibly()条件队列wait/notifyCondition自动释放✅❌需要手动 unlock面试时可以这样回答两者都可以实现互斥和可重入但 ReentrantLock 提供了更丰富的功能例如公平锁、尝试获取锁、可中断获取锁以及多个 Condition 条件队列。synchronized 使用起来更加简单正常情况下优先使用 synchronized 即可。十二、ReentrantLock 为什么可重入例如lock.lock();lock.lock();try{// ...}finally{lock.unlock();lock.unlock();}同一个线程可以多次获取同一把锁。内部会记录持有线程 当前线程 state 2每获取一次state每释放一次state--直到state 0锁才真正释放。十三、Condition32. Condition 是什么Condition可以理解为ReentrantLock 版本的 wait/notify。例如Conditionconditionlock.newCondition();lock.lock();try{condition.await();}finally{lock.unlock();}其他线程lock.lock();try{condition.signal();}finally{lock.unlock();}十四、线程池33. 为什么要使用线程池线程池主要解决三个问题① 降低线程创建和销毁的开销线程可以重复使用。② 控制线程数量避免创建大量线程导致 CPU 和内存资源耗尽。③ 统一管理线程可以统一进行任务提交线程管理任务排队拒绝策略十五、线程池核心参数JavaThreadPoolExecutor最重要的参数ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler);分别表示参数含义corePoolSize核心线程数maximumPoolSize最大线程数keepAliveTime非核心线程空闲存活时间workQueue任务等待队列threadFactory创建线程的工厂handler拒绝策略十六、线程池执行流程这是线程池面试中非常重要的一道题。假设核心线程数 2 最大线程数 4 队列容量 10提交任务后任务提交 ↓ 核心线程是否未满 ↓ 是 → 创建核心线程执行 ↓ 否 ↓ 任务加入阻塞队列 ↓ 队列满了吗 ↓ 否 → 等待 ↓ 是 ↓ 线程数 最大线程数 ↓ 是 → 创建非核心线程 ↓ 否 ↓ 执行拒绝策略面试时一定要能把这个流程讲出来。十七、线程池为什么不推荐 Executors例如Executors.newFixedThreadPool(10);看起来方便但是实际开发中通常不推荐直接使用。原因是FixedThreadPool底层使用无界队列LinkedBlockingQueue任务过多时可能导致大量任务堆积最终造成 OOM。CachedThreadPool线程数量理论上可以无限增长。大量任务提交时可能创建大量线程。所以实际开发中一般直接使用ThreadPoolExecutor明确指定核心线程数最大线程数队列容量拒绝策略十八、线程池拒绝策略线程池无法继续接收任务时会执行拒绝策略。JDK 提供四种常见策略AbortPolicy直接抛出异常。RejectedExecutionExceptionCallerRunsPolicy由提交任务的线程自己执行任务。DiscardPolicy直接丢弃任务。DiscardOldestPolicy丢弃队列中最旧的任务然后重新提交。十九、ThreadLocal34. 什么是 ThreadLocalThreadLocal可以为每个线程提供独立的变量副本。例如ThreadLocalStringthreadLocalnewThreadLocal();threadLocal.set(hello);StringvaluethreadLocal.get();不同线程获取到的是不同的数据。35. ThreadLocal 的底层原理可以简单理解为Thread │ └── ThreadLocalMap │ ├── ThreadLocal → value ├── ThreadLocal → value └── ThreadLocal → value注意ThreadLocalMap 是 Thread 对象内部的一部分每个线程都有自己的 ThreadLocalMap。二十、ThreadLocal 为什么会内存泄漏这是非常高频的问题。ThreadLocalMap 中Entry ├── KeyThreadLocal 的弱引用 └── Value强引用如果 ThreadLocal 对象没有其他强引用ThreadLocal ↓ 被 GC 回收 ↓ Entry 的 Key null但是Thread ↓ ThreadLocalMap ↓ Entry ↓ ValueValue 仍然可能存在强引用。如果线程长期存活例如线程池中的工作线程就可能导致 Value 长时间无法回收。因此使用 ThreadLocal 后应该及时try{threadLocal.set(value);}finally{threadLocal.remove();}二十一、并发容器Java 提供了一系列线程安全的并发容器。常见的有ConcurrentHashMap CopyOnWriteArrayList ConcurrentLinkedQueue BlockingQueue二十二、ConcurrentHashMap36. 为什么需要 ConcurrentHashMap普通 HashMap 在多线程环境下可能出现数据覆盖数据不一致并发修改问题因此需要线程安全的 Map。37. ConcurrentHashMap 和 Hashtable 有什么区别Hashtable很多方法直接使用 synchronized。锁粒度比较大。而ConcurrentHashMap采用更加细粒度的并发控制方式因此并发性能更好。38. JDK 1.7 的 ConcurrentHashMapJDK 1.7 使用Segment HashEntry整体结构ConcurrentHashMap │ ┌──────┼──────┐ Segment Segment Segment │ HashEntry...不同 Segment 可以同时执行操作。39. JDK 1.8 的 ConcurrentHashMapJDK 1.8 去掉了 Segment整体结构更接近Node[] │ ├── Node ├── Node └── TreeBin主要通过CAS synchronized实现并发控制。面试可以简单总结JDK 1.7 的 ConcurrentHashMap 采用 Segment 分段锁JDK 1.8 去掉 Segment采用 CAS synchronized 对桶进行更细粒度的控制并在链表过长时转成红黑树。二十三、CopyOnWriteArrayList40. 什么是 CopyOnWriteArrayListCopyOnWriteArrayList 是一种适合读多写少场景的线程安全 List。写操作时会复制一份数组原数组 [ A B C ] ↓ 写操作 新数组 [ A B C D ]然后再替换原来的数组。41. CopyOnWriteArrayList 有什么缺点① 写操作成本高每次写都需要复制数组。② 占用额外内存修改期间会存在新旧两份数组。③ 不适合大量写操作所以它比较适合读多写少的场景。二十四、BlockingQueue42. 什么是 BlockingQueue阻塞队列。当队列满时生产者阻塞。当队列为空时消费者阻塞。典型应用生产者-消费者模型。例如线程池中的任务队列本质上也是一种生产者消费者思想。二十五、CountDownLatch43. CountDownLatch 是什么CountDownLatch 可以让一个或多个线程等待其他线程完成任务。例如CountDownLatchlatchnewCountDownLatch(3);三个线程分别执行latch.countDown();主线程latch.await();直到计数器3 → 2 → 1 → 0主线程才继续执行。二十六、Semaphore44. Semaphore 是什么Semaphore 用于控制同时访问某个资源的线程数量。例如限制最多 3 个线程访问数据库SemaphoresemaphorenewSemaphore(3);线程获取许可semaphore.acquire();使用完成semaphore.release();可以把它理解成一个“许可证池”。二十七、死锁45. 什么是死锁两个或多个线程互相等待对方持有的资源导致所有线程都无法继续执行。例如线程 A 持有锁1 等待锁2 线程 B 持有锁2 等待锁1形成A → 等待 B ↑ ↓ └───────┘二十八、死锁产生的四个必要条件经典死锁条件① 互斥资源一次只能被一个线程使用。② 持有并等待线程已经持有资源同时继续等待其他资源。③ 不可剥夺线程已经获得的资源不能被强制抢走。④ 循环等待多个线程形成循环等待关系。只要破坏其中一个条件就可以避免死锁。二十九、如何排查死锁可以使用jstack例如jstackpidJVM 会输出线程之间的锁竞争情况。另外也可以使用jconsole或者VisualVM辅助分析。三十、sleep 和 wait 有什么区别这是面试中的经典题。对比sleepwait所属类ThreadObject是否释放锁❌✅是否需要 synchronized不需要需要唤醒方式时间到自动恢复notify/notifyAll 或超时使用场景线程休眠线程间通信例如synchronized(lock){lock.wait();}调用wait()后当前线程释放锁并进入等待状态。三十一、notify 和 notifyAll 有什么区别notify()随机唤醒一个等待线程。notifyAll()唤醒所有等待线程。实际开发中如果无法明确判断应该唤醒哪个线程通常更安全的是notifyAll();三十二、并发编程面试高频总结面试 Java 并发时可以重点掌握下面这张知识图Java 并发 │ ├── 线程基础 │ ├── 线程状态 │ ├── sleep / wait │ └── notify / notifyAll │ ├── JMM │ ├── 可见性 │ ├── 原子性 │ ├── 有序性 │ └── happens-before │ ├── synchronized │ ├── Monitor │ ├── 可重入 │ └── 锁机制 │ ├── volatile │ ├── 可见性 │ └── 禁止特定重排序 │ ├── CAS │ ├── Atomic │ ├── ABA │ └── 自旋 │ ├── AQS │ ├── state │ ├── 等待队列 │ └── ReentrantLock │ ├── 线程池 │ ├── 核心参数 │ ├── 执行流程 │ └── 拒绝策略 │ ├── ThreadLocal │ ├── ThreadLocalMap │ └── 内存泄漏 │ ├── 并发容器 │ ├── ConcurrentHashMap │ ├── CopyOnWriteArrayList │ └── BlockingQueue │ └── 并发工具类 ├── CountDownLatch ├── Semaphore └── CyclicBarrier三十三、面试前重点背诵清单如果时间比较紧不需要平均分配时间优先掌握下面这些。第一梯队必须掌握synchronized 原理synchronized 和 volatile 的区别volatile 为什么不能保证原子性JMMhappens-beforeCASABA 问题AQSReentrantLock线程池核心参数线程池执行流程线程池拒绝策略ThreadLocal 原理ThreadLocal 内存泄漏ConcurrentHashMap 1.7 和 1.8 的区别死锁第二梯队需要理解sleep 和 wait 区别notify 和 notifyAllCountDownLatchSemaphoreCopyOnWriteArrayListBlockingQueueAtomic 原子类第三梯队了解即可CyclicBarrierPhaserExchangerForkJoinPoolCompletableFuture三十四、最后总结Java 并发这部分看起来知识点很多但实际上可以围绕几个核心问题展开多个线程同时操作共享数据时如何保证数据正确于是就产生了可见性 ↓ volatile / synchronized / JMM 原子性 ↓ synchronized / Lock / CAS / Atomic 有序性 ↓ volatile / happens-before 线程协作 ↓ wait / notify / Condition 线程竞争 ↓ synchronized / AQS / ReentrantLock 任务管理 ↓ ThreadPoolExecutor 线程隔离 ↓ ThreadLocal 并发容器 ↓ ConcurrentHashMap / CopyOnWriteArrayList真正面试的时候不要只背定义。例如面试官问“为什么 volatile 不能保证 count 的线程安全”不要只回答“因为 volatile 不能保证原子性。”更好的回答方式是count实际上包含读取、修改、写回三个步骤即使 count 使用 volatile 保证了可见性多个线程仍然可能同时读取到相同的旧值然后分别进行加一并写回导致其中一次修改被覆盖。因此 volatile 只能保证单次读写的可见性不能保证这种复合操作的原子性。如果需要保证自增操作的线程安全可以使用 AtomicInteger 的 CAS或者使用 synchronized / Lock。这才是面试中真正有区分度的回答。Java 并发学习路线如果按照后端面试的顺序学习可以记成线程基础 ↓ JMM ↓ synchronized ↓ volatile ↓ CAS ↓ Atomic ↓ AQS ↓ ReentrantLock ↓ 线程池 ↓ ThreadLocal ↓ ConcurrentHashMap ↓ 其他并发工具类 ↓ 源码 实战其中最重要的是JMM → synchronized / volatile → CAS → AQS → 线程池 → ThreadLocal → ConcurrentHashMap把这一条主线真正理解之后再去看源码会比单纯背八股轻松很多。