☰
【Java面试题】Java并发
2026/10/5 2:06:45 网站建设 项目流程

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. 创建线程有哪些方式?

常见方式:

方式一:继承 Thread

classMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println("执行任务");}}

方式二:实现 Runnable

Runnabletask=()->{System.out.println("执行任务");};newThread(task).start();

方式三:实现 Callable

Callable<Integer>task=()->100;FutureTask<Integer>futureTask=newFutureTask<>(task);newThread(futureTask).start();

方式四:使用线程池

实际开发中更推荐线程池:

ExecutorServiceexecutor=Executors.newFixedThreadPool(10);executor.submit(()->{System.out.println("执行任务");});

二、线程的生命周期

Java 中线程主要有以下状态:

NEW │ │ start() ↓ RUNNABLE │ ├── BLOCKED ├── WAITING └── TIMED_WAITING │ ↓ TERMINATED

4. NEW

线程对象创建完成,但是还没有调用start()。

Threadthread=newThread();

5. RUNNABLE

调用start()后进入RUNNABLE状态。

需要注意:

Java 中的 RUNNABLE 包含操作系统层面的“就绪”和“运行”两种状态。


6. BLOCKED

线程等待获取synchronized锁时,如果锁已经被其他线程占用,就会进入 BLOCKED。


7. WAITING

线程主动等待其他线程通知。

例如:

Objectlock=newObject();synchronized(lock){lock.wait();}

8. TIMED_WAITING

带时间限制的等待。

例如:

Thread.sleep(1000);

或者:

lock.wait(1000);

三、并发编程存在什么问题?

并发编程最核心的三个问题:

可见性、原子性、有序性


9. 什么是可见性?

一个线程修改共享变量之后,其他线程能够立即看到修改结果。

例如:

booleanflag=false;ThreadA:flag=true;ThreadB:while(!flag){}

如果没有正确的内存同步机制,线程 B 可能一直读取到旧值。


10. 什么是原子性?

一个操作要么全部执行,要么全部不执行,中间不能被其他线程打断。

例如:

count++;

看起来是一条语句,实际上可以拆成:

读取 count ↓ count + 1 ↓ 写回 count

如果两个线程同时执行,就可能发生数据覆盖。


11. 什么是有序性?

程序执行过程中,编译器、JIT 和 CPU 为了优化性能,可能对指令进行重排序。

在单线程环境下,重排序通常不会影响最终结果。

但是在多线程环境下,如果没有正确的同步机制,就可能产生问题。


四、synchronized

12. synchronized 是什么?

synchronized是 Java 提供的内置锁机制,可以保证多个线程对共享资源的互斥访问。

同时可以保证:

  • 原子性
  • 可见性
  • 有序性

例如:

publicsynchronizedvoidadd(){count++;}

13. synchronized 锁的是什么?

synchronized本质上是对对象监视器 Monitor进行加锁。

例如:

synchronized(obj){// 临界区}

锁住的是obj对象。


14. synchronized 修饰实例方法锁的是谁?

publicsynchronizedvoidmethod(){}

等价于:

publicvoidmethod(){synchronized(this){}}

所以锁的是:

当前对象this


15. synchronized 修饰静态方法锁的是谁?

publicstaticsynchronizedvoidmethod(){}

锁的是:

当前类的 Class 对象。

例如:

synchronized(MyClass.class){}

16. synchronized 的锁升级过程

早期 JVM 中经常介绍:

无锁 ↓ 偏向锁 ↓ 轻量级锁 ↓ 重量级锁

不过需要注意:

JDK 15 开始偏向锁默认被禁用,JDK 18 移除了偏向锁相关实现。因此面试时可以了解历史上的锁升级机制,但不要把“偏向锁一定存在”当成当前 JVM 的绝对事实。

传统锁升级主要是为了:

在不同竞争程度下,用不同成本的锁实现提高性能。


五、volatile

17. volatile 是什么?

volatile是 Java 提供的轻量级同步机制,可以保证:

可见性 + 禁止特定类型的指令重排序

例如:

privatevolatilebooleanrunning=true;

一个线程修改:

running=false;

其他线程能够及时看到最新值。


18. volatile 能保证原子性吗?

不能。

例如:

volatileintcount=0;count++;

依然不是原子操作。

因为:

读取 count ↓ count + 1 ↓ 写回 count

多个线程依然可能同时读取到相同的值。


19. synchronized 和 volatile 有什么区别?

对比synchronizedvolatile
原子性✅❌
可见性✅✅
有序性✅部分保证
阻塞可能阻塞不会因为读写本身产生锁阻塞
使用场景临界区、复合操作状态标志、简单共享变量

一句话:

volatile适合解决共享变量的可见性问题,synchronized更适合解决需要互斥执行的临界区问题。


六、Java 内存模型 JMM

20. 什么是 JMM?

JMM,即 Java Memory Model,Java 内存模型。

它不是简单指 JVM 中的某一块物理内存,而是一套规范,用于解决:

多线程环境下共享变量的可见性、原子性和有序性问题。

线程拥有自己的工作内存:

主内存 / | \ / | \ 线程 A 线程 B 线程 C 工作内存 工作内存 工作内存

线程对共享变量的操作,需要通过主内存进行同步。


七、happens-before

21. 什么是 happens-before?

happens-before 是 JMM 中用于描述操作之间可见性关系的规则。

如果:

A happens-before B

那么意味着:

A 操作的结果对 B 操作可见。

常见规则:

① 程序次序规则

同一个线程中:

inta=10;intb=a;

前面的操作 happens-before 后面的操作。

② 锁规则

解锁操作 happens-before 后续对同一个锁的加锁操作。

③ volatile 规则

对 volatile 变量的写操作 happens-before 后续对该变量的读操作。

④ 线程启动规则

Thread.start()happens-before 新线程中的操作。

⑤ 线程终止规则

线程中的所有操作 happens-before 其他线程检测到该线程已经终止。

例如:

thread.join();

八、CAS

22. 什么是 CAS?

CAS:

Compare And Swap,比较并交换。

它是一种乐观锁思想。

CAS 包含三个值:

内存地址 旧值 新值

执行逻辑:

当前值 == 预期旧值? │ ┌────┴────┐ │ │ 是 否 │ │ 修改新值 修改失败

23. CAS 有什么优点?

CAS 不需要像传统互斥锁一样让线程阻塞等待。

因此在竞争不激烈的情况下性能较好。

Java 中很多原子类底层都会使用 CAS。

例如:

AtomicIntegercount=newAtomicInteger();count.incrementAndGet();

24. CAS 有什么问题?

主要有两个:

① ABA 问题

假设:

线程 A:读取 A 线程 B:A → B 线程 B:B → A 线程 A:发现还是 A,于是 CAS 成功

线程 A 无法感知中间发生过变化。

解决方式:

使用带版本号的 CAS。

例如:

AtomicStampedReference

② 自旋开销

CAS 失败后可能不断重试:

CAS失败 ↓ 重新读取 ↓ CAS ↓ 失败 ↓ 继续重试

如果竞争非常激烈,会消耗 CPU。


九、Atomic 原子类

25. AtomicInteger 是什么?

AtomicInteger是 Java 并发包提供的原子操作类。

例如:

AtomicIntegercount=newAtomicInteger();count.incrementAndGet();

相比:

count++;

可以保证自增操作的原子性。


26. AtomicInteger 为什么线程安全?

底层主要依赖:

CAS + volatile

例如:

读取当前值 ↓ 计算新值 ↓ CAS 更新 ↓ 失败则重试

十、AQS

27. 什么是 AQS?

AQS:

AbstractQueuedSynchronizer,抽象队列同步器。

它是 Java 并发包中非常核心的基础组件。

很多并发工具都是基于 AQS 实现的,例如:

  • ReentrantLock
  • ReentrantReadWriteLock
  • Semaphore
  • CountDownLatch

28. AQS 的核心思想是什么?

AQS 核心可以理解成:

一个 state + 一个等待队列

例如:

AQS │ ┌─────┴─────┐ │ │ state CLH队列 │ ┌──────┼──────┐ ↓ ↓ ↓ 线程A 线程B 线程C

state用来表示同步状态。

当线程获取资源失败后,就进入等待队列。


29. AQS 为什么需要队列?

如果线程获取锁失败,不能让所有线程一直 CAS 自旋,否则会浪费大量 CPU。

所以 AQS 会:

获取资源失败 ↓ 进入等待队列 ↓ 阻塞线程 ↓ 资源释放 ↓ 唤醒后继节点

这样可以降低 CPU 消耗。


十一、ReentrantLock

30. ReentrantLock 是什么?

ReentrantLock是 Java 提供的可重入锁。

例如:

ReentrantLocklock=newReentrantLock();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

锁才真正释放。


十三、Condition

32. Condition 是什么?

Condition可以理解为:

ReentrantLock 版本的 wait/notify。

例如:

Conditioncondition=lock.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

直接抛出异常。

RejectedExecutionException

CallerRunsPolicy

由提交任务的线程自己执行任务。

DiscardPolicy

直接丢弃任务。

DiscardOldestPolicy

丢弃队列中最旧的任务,然后重新提交。


十九、ThreadLocal

34. 什么是 ThreadLocal?

ThreadLocal可以为每个线程提供独立的变量副本。

例如:

ThreadLocal<String>threadLocal=newThreadLocal<>();threadLocal.set("hello");Stringvalue=threadLocal.get();

不同线程获取到的是不同的数据。


35. ThreadLocal 的底层原理

可以简单理解为:

Thread │ └── ThreadLocalMap │ ├── ThreadLocal → value ├── ThreadLocal → value └── ThreadLocal → value

注意:

ThreadLocalMap 是 Thread 对象内部的一部分,每个线程都有自己的 ThreadLocalMap。


二十、ThreadLocal 为什么会内存泄漏?

这是非常高频的问题。

ThreadLocalMap 中:

Entry ├── Key:ThreadLocal 的弱引用 └── Value:强引用

如果 ThreadLocal 对象没有其他强引用:

ThreadLocal ↓ 被 GC 回收 ↓ Entry 的 Key = null

但是:

Thread ↓ ThreadLocalMap ↓ Entry ↓ Value

Value 仍然可能存在强引用。

如果线程长期存活,例如线程池中的工作线程,就可能导致 Value 长时间无法回收。

因此使用 ThreadLocal 后应该及时:

try{threadLocal.set(value);}finally{threadLocal.remove();}

二十一、并发容器

Java 提供了一系列线程安全的并发容器。

常见的有:

ConcurrentHashMap CopyOnWriteArrayList ConcurrentLinkedQueue BlockingQueue

二十二、ConcurrentHashMap

36. 为什么需要 ConcurrentHashMap?

普通 HashMap 在多线程环境下可能出现:

  • 数据覆盖
  • 数据不一致
  • 并发修改问题

因此需要线程安全的 Map。


37. ConcurrentHashMap 和 Hashtable 有什么区别?

Hashtable很多方法直接使用 synchronized。

锁粒度比较大。

而ConcurrentHashMap采用更加细粒度的并发控制方式,因此并发性能更好。


38. JDK 1.7 的 ConcurrentHashMap

JDK 1.7 使用:

Segment + HashEntry

整体结构:

ConcurrentHashMap │ ┌──────┼──────┐ Segment Segment Segment │ HashEntry...

不同 Segment 可以同时执行操作。


39. JDK 1.8 的 ConcurrentHashMap

JDK 1.8 去掉了 Segment,整体结构更接近:

Node[] │ ├── Node ├── Node └── TreeBin

主要通过:

CAS + synchronized

实现并发控制。

面试可以简单总结:

JDK 1.7 的 ConcurrentHashMap 采用 Segment 分段锁;JDK 1.8 去掉 Segment,采用 CAS + synchronized 对桶进行更细粒度的控制,并在链表过长时转成红黑树。


二十三、CopyOnWriteArrayList

40. 什么是 CopyOnWriteArrayList?

CopyOnWriteArrayList 是一种适合:

读多写少

场景的线程安全 List。

写操作时会复制一份数组:

原数组 [ A B C ] ↓ 写操作 新数组 [ A B C D ]

然后再替换原来的数组。


41. CopyOnWriteArrayList 有什么缺点?

① 写操作成本高

每次写都需要复制数组。

② 占用额外内存

修改期间会存在新旧两份数组。

③ 不适合大量写操作

所以它比较适合:

读多写少的场景。


二十四、BlockingQueue

42. 什么是 BlockingQueue?

阻塞队列。

当队列满时:

生产者阻塞。

当队列为空时:

消费者阻塞。

典型应用:

生产者-消费者模型。

例如线程池中的任务队列,本质上也是一种生产者消费者思想。


二十五、CountDownLatch

43. CountDownLatch 是什么?

CountDownLatch 可以让一个或多个线程等待其他线程完成任务。

例如:

CountDownLatchlatch=newCountDownLatch(3);

三个线程分别执行:

latch.countDown();

主线程:

latch.await();

直到计数器:

3 → 2 → 1 → 0

主线程才继续执行。


二十六、Semaphore

44. Semaphore 是什么?

Semaphore 用于控制同时访问某个资源的线程数量。

例如限制最多 3 个线程访问数据库:

Semaphoresemaphore=newSemaphore(3);

线程获取许可:

semaphore.acquire();

使用完成:

semaphore.release();

可以把它理解成:

一个“许可证池”。


二十七、死锁

45. 什么是死锁?

两个或多个线程互相等待对方持有的资源,导致所有线程都无法继续执行。

例如:

线程 A: 持有锁1 等待锁2 线程 B: 持有锁2 等待锁1

形成:

A → 等待 B ↑ ↓ └───────┘

二十八、死锁产生的四个必要条件

经典死锁条件:

① 互斥

资源一次只能被一个线程使用。

② 持有并等待

线程已经持有资源,同时继续等待其他资源。

③ 不可剥夺

线程已经获得的资源不能被强制抢走。

④ 循环等待

多个线程形成循环等待关系。

只要破坏其中一个条件,就可以避免死锁。


二十九、如何排查死锁?

可以使用:

jstack

例如:

jstack<pid>

JVM 会输出线程之间的锁竞争情况。

另外也可以使用:

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 为什么不能保证原子性
  • JMM
  • happens-before
  • CAS
  • ABA 问题
  • AQS
  • ReentrantLock
  • 线程池核心参数
  • 线程池执行流程
  • 线程池拒绝策略
  • ThreadLocal 原理
  • ThreadLocal 内存泄漏
  • ConcurrentHashMap 1.7 和 1.8 的区别
  • 死锁

第二梯队:需要理解

  • sleep 和 wait 区别
  • notify 和 notifyAll
  • CountDownLatch
  • Semaphore
  • CopyOnWriteArrayList
  • BlockingQueue
  • Atomic 原子类

第三梯队:了解即可

  • CyclicBarrier
  • Phaser
  • Exchanger
  • ForkJoinPool
  • CompletableFuture

三十四、最后总结

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

把这一条主线真正理解之后,再去看源码,会比单纯背八股轻松很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询