线程基础
Thread创建与启动Runnable与Callable的区别join(),sleep(),yield()方法的作用
线程状态和生命周期
- 线程的6种状态:
NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
同步机制
synchronized关键字(对象锁 vs 类锁)volalite关键字的作用和实现原理ReentrantLock和Condition
线程池
ExecutorService和ThreadPoolExecutor- 四种常见的线程池类型(
FixedThreadPool、CachedThreadPool、SingleThreadPool、ScheduledThreadPool)
并发工具类
CountDownLatchCyclicBarrierSemaphoreExchanger
原子类和CAS
AtomicInteger、AtomicReference、AtomictampedReference避免ABA问题CAS原理及ABA问题解决方案
并发集合类
ConcurrentHashMapCopyOnWriterArrayListConcurrentLinkedQueue
线程安全与设计模式
- 不可变对象(
Immuable) ThreadLocal(避免线程间共享数据)- 单例模式的线程安全实现
常见面试题汇总
线程相关
- 如何创建线程?哪种方式更好?
- 继承
Thread类
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
}
// 使用
MyThread t = new MyThread();
t.start();
- 实现
Runnbale接口
class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable is running");
}
}
// 使用
Thread t = new Thread(new MyRunnable());
t.start();
使用第二种方法 Runnable 接口实现更好
- 避免单继承限制:
Java不支持多继承,继承Thread后无法再继承其他类 - 更适合线程池:
ExecutorService只接受Runnable或Callable - 资源共享更方便:多个线程可以共享同一个
Runnable实例
start()和run()的区别?
| 特性 | start() | run() |
|---|---|---|
| 作用 | 启动一个新线程执行任务 | 普通方法调用,不会开启新线程 |
| 线程行为 | 真正实现多线程 | 在当前线程中同步执行 |
| 是否并发执行 | 是 | 否 |
| 能否重复调用 | 一个线程只能调用一次 start() | 可以多次调用 |
| 底层机制 | JVM 创建线程并调用 run() | 直接调用方法,无并发支持 |
- 如何优雅的停止一个线程?
在 Java 中,没有一种“强制停止”线程的方式是线程安全的,因此推荐使用 协作式方式 来优雅地停止线程。
- 推荐做法:使用标志位控制线程退出
public class MyTask implements Runnable {
private volatile boolean running = true;
@Override
public void run() {
while (running) {
// 执行任务
}
}
public void shutdown() {
running = false;
}
}
// 使用
MyTask task = new MyTask();
Thread thread = new Thread(task);
thread.start();
// 停止线程
task.shutdown();
- 其他方式(适用于特定场景):
| 方式 | 是否推荐 | 说明 |
|---|---|---|
interrupt() | ✅ 推荐 | 中断线程,配合 isInterrupted() 检查中断状态 |
isInterrupted() | ✅ 推荐 | 在循环中检查是否被中断 |
Thread.interrupted() | ⚠️ 谨慎使用 | 静态方法,会清除中断状态 |
stop() | ❌ 不推荐 | 已废弃,可能导致资源未释放、数据不一致等问题 |
总结:不要使用
stop();优先使用volatile标志位或interrupt()协作退出;确保资源释放、避免死循环
线程同步
synchronized底层实现原理?
synchronized 是 java 中用来控制多线程访问共享资源的关键字。其底层实现依赖于 JVM 的 “监视器锁(monitor)”机制。
synchronized修饰代码块和方法时,JVM会通过monitorenter和monitorexit指令控制锁的获取和释放- 每个对象都有一个与之关联的监视器(
monitor),当线程执行到synchronized代码块时,会尝试获取该对象的monitor锁:- 如果
monitor计数器为0,表示锁未被占用,当前线程获得锁并将计数器加1; - 如果
monitor计数器不为0,且是当时线程持有锁,则计数器加1(可重入);如果monitor被其他线程持有,则当前线程进入阻塞状态,等待锁释放。
- 如果
- 当线程执行完
synchronized代码块或发生异常时,JVM会自动执行monitorexit指令,释放锁并将monitor计数器减1
此外,JVM 在底层可能对 synchronized 进行优化,如偏向锁、轻量级锁、重量级锁等阶段的转换,以提升性能。
volatile能保证原子性吗?为什么?
volatile 不能保证原子性,它只能保证变量的可见性和禁止指令重排序。
- 可见性:在多线程环境中,每个线程都有自己的工作内存(
working memory),变量的值可能只存在线程的本地缓存中,不会立即刷新到主内存。使用volatile修饰的变量,在每次读取时都会从主内存中重新加载,写入时也会立即刷新到主内存,从而保证其他线程能及时看到最新的值。 - 禁止指令重排序:编译器和
CPU为了提高性能,可能会对指令进行重排序。volatile通过插入内存屏障(Memory Barrier)来阻止这种重排序行为,来确保写volatile变量之前的代码不会被重排序到写操作之后;读volatile变量之后的代码不会被重排序到读操作之前。
但它没有锁机制,也不能保证复合操作的原子性。
ReentrantLock相比synchronized有哪些优势?
ReentrantLock 是 java.util.concurrent.locks 包中提供的一个可重入的互斥锁,相比 synchronized 关键字,它提供了更强大、更灵活的锁机制。其主要优势:
- 尝试获取锁(
tryLock):ReentrantLock支持非阻塞方式获取锁,synchronized获取不到锁会一直阻塞。 - 超时获取锁:可设置等待锁的超时时间,避免死锁
- 可中断获取锁:支持在等待锁的过程中响应中断
- 支持公平锁:默认是非公平锁
- 锁的精确控制:明确的控制加锁和解锁的时机,而不是依赖代码块结构
线程池
- 线程池的核心参数有哪些?各有什么作用?
| 参数名 | 作用说明 |
|---|---|
corePoolSize | 核心线程数,即使空闲也不会超时回收(除非设置了 allowCoreThreadTimeOut) |
maximumPoolSize | 最大线程数。线程池中允许最大的线程数量 |
keepAliveTime | 非核心线程的空闲超时时间,超过这个时间未执行的任务则被回收 |
unit | keepAliveTime 的时间单位(如秒,毫秒等) |
workQueue | 任务队列。用于存放等待执行的任务,常用 LinkedBlockingQueue 或 SynchronousQueue |
threadFactory | 线程工厂。用于创建新线程,可自定义线程命名,优先级等 |
handler | 拒绝策略。当任务无法提交时(如队列满且线程数达到最大),按此策略处理 |
- 如何自定义拒绝策略?
要自定义线程池拒绝策略,需要实现 RejectedExecutionHandler 接口,并重写其 rejectedExecution 方法。这样可以在任务被拒绝时,执行自定义的逻辑。比如日志记录、保存任务到数据库、或者返回提示信息等
public class MyRejectedHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义拒绝逻辑,例如:
System.out.println("任务被拒绝:" + r.toString());
// 可以记录日志、发送告警、持久化任务等
}
}
3. 线程池中 submit() 和 execute() 的区别?
execute()是基础的任务提交方式,没有返回值。适合不需要返回结果,不关心任务执行状态的场景。如果任务执行过程中抛出异常,该异常会直接从线程的run()方法中抛出,可能导致线程终止。submit()是增强版,支持返回值和异常捕获,适用于需要获取任务执行结果,或需要处理异常的场景。异常会被封装在Future对象中,只有在调用Future.get()时才会抛出异常(通过ExecutionException包装)
并发工具类
CountDownLatch和CyclicBarrir的区别?
| 行为和使用场景 | CountDownLatch | CyclicBarrir |
|---|---|---|
| 功能用途 | 一个线程(或多个)等待其他线程完成操作后才继续执行,计数只能减到0不可重置 | 多个线程 相互等待彼此到达某个屏障点后再一起继续执行,可重复使用(支持重置) |
| 计数是否可重用 | 不可重用(计数一旦减到 0 就不能再用了) | 可重用(所有线程释放后可再次使用) |
| 线程角色 | 主从模式:一个主线程等待多个工作线程完成任务 | 对等模式:所有线程互相等待,共同到达屏障点 |
| 异常处理 | 无特殊处理,线程中断不影响计数 | 任一线程中断或超时会触发 BrokenBarrierException,并破坏屏障状态 |
Semaphore的用途是什么?
- 控制资源访问:控制同时访问的线程数量(如数据库连接池、线程池、限流等)
- 实现互斥锁:设置许可数为 1 的
Semaphore可作为互斥锁(Mutex)使用,确保同一时刻只有一个线程执行临界区代码 - 任务调度协作:控制多个线程之间的协作行为,比如某些线程必须等待其他线程释放资源后才能继续执行
Semaphore 的主要用途是控制并发线程的数量,适用于资源池管理、限流、互斥访问等需要限制并发度的场景。
并发集合
ConcurrentHashMap是如何实现线程安全的?
ConcurrentHashMap 是 Java 中线程安全的哈希表实现,适用于高并发场景。它在不同版本(JDK 1.7 和 JDK 1.8)中实现机制有所不同,但核心思想都是减少锁粒度、提高并发性能。
JDK 1.7 实现原理
核心结构:
- 使用 Segment 分段锁 + HashEntry
- 将整个 Map 分成多个 Segment(默认 16 段)
- 每个 Segment 相当于一个独立的 HashMap,内部使用链表存储键值对
线程安全机制:
- 写操作:只锁定当前 Segment,不影响其他 Segment 的读写
- 读操作:不加锁,通过 volatile 保证可见性
- 优点:并发度高,默认支持 16 个线程同时写入
JDK 1.8 实现原理
核心结构:
- 使用
Node数组 + 链表/红黑树 - 与普通
HashMap类似,但在并发控制上做了增强
线程安全机制:
- 写操作(
put):使用CAS+synchronizedCAS更新头节点- 如果冲突严重,则用
synchronized锁定链表或红黑树头节点
- 读操作(
get):不加锁,通过volatile保证读取到最新值 - 扩容(
resize):多线程协作扩容,采用 迁移机制(transfer),避免一次性迁移所有数据 - 优点:减少了锁的粒度,提高了并发性能;支持更高的并发访问量
CopyOnWriteArrayList适用于什么场景?
核心特点:写时复制(Copy-On-Write):每次修改操作(add、set、remove 等)都会创建一个新的数组副本,原数组用于读取。
适用场景:
| 场景 | 描述 |
|---|---|
| 读多写少 | 适合高并发读操作、写操作较少的情况,如缓存、事件监听器列表、配置管理等 |
| 弱一致性要求 | 读操作可以容忍一定程度的“旧数据”,不要求实时一致性 |
| 避免写锁竞争 | 写操作不频繁,但需要保证线程安全,且不想引入复杂的同步机制 |
CopyOnWriteArrayList 适用于读多写少、允许弱一致性的并发场景,它通过牺牲写操作性能来换取高效的并发读取能力。
原子类和CAS
CAS的三大问题是什么?如何解决?
| 问题类型 | 描述 | 解决方法 |
|---|---|---|
ABA 问题 | 值从 A → B → A,CAS 无法感知变化 | 使用 AtomicStampedReference 或 AtomicMarkableReference |
| 自选开销大 | CAS 失败后不断重试导致 CPU 占用高 | 控制重试次数,或切换为锁机制 |
| 只能操作单变量 | 无法保证多个变量的原子操作 | 封装成对象、使用锁 |
CAS 虽高效,但存在 ABA、自旋开销和单变量限制等问题。合理结合版本号控制、锁机制和对象封装,可以有效规避这些风险。
AtomicInteger是如何实现原子性的?
AtomicInteger 是 java.util.concurrent.atomic 包下的一个原子类,用于实现对 int 类型变量的线程安全操作。它通过 CAS(Compare and Swap)算法 结合 volatile 变量 实现了无需锁的原子性操作
核心实现机制
- 底层依赖 CAS
AtomicInteger使用了Unsafe类 提供的CAS操作来保证原子性。CAS是一种乐观锁机制,包含三个操作数:内存地址值(V);预期原值(A);要更新的新值(B)
只有当内存中的值等于预期原值 A 时,才将值更新为 B,否则不更新并重试
volatile修饰变量AtomicInteger内部使用一个volatile int value来保存数值,确保多线程之间的可见性
AtomicInteger 通过 volatile 变量 + CAS 操作 实现了线程安全的原子操作,适用于读写频繁但冲突较少的场景,是 Java 并发编程中高效、常用的原子类之一
线程安全设计
- 什么是线程封闭?
线程封闭(Thread Confinement) 是一种并发编程中的设计思想,指的是 将对象或变量的访问限制在单个线程内部,从而避免多线程并发访问带来的线程安全问题。
| 实现方式 | 说明 | 应用场景 |
|---|---|---|
局部变量(Local Variables) | 方法内的变量只属于当前线程栈,天然线程封闭 | 方法内部临时变量、无状态操作 |
ThreadLocal | 每个线程拥有独立的变量副本,通过 get() / set() 访问 | 用户会话信息(如登录用户)、数据库连接、事务上下文 |
| 私有对象 + 不发布引用 | 对象不被其他线程访问,仅当前线程使用 | 工具类实例、临时对象等 |
优点:
- 无需同步机制:因为没有共享,所以不会出现并发冲突;
- 性能高:避免了锁、
CAS等开销; - 简化代码逻辑:更容易理解和维护。
线程封闭是一种“不共享、不并发”的并发安全策略,通过限制变量只能被一个线程访问,从根本上避免线程安全问题。
- 什么是不可变对象?为什么是天生安全?
不可变对象是指:一旦创建,其状态(属性值)就不能被修改的对象。换句话说,一个类的实例在构造完成后,其所有属性值都不能被更改。
因为它们具备以下特性,使得多线程环境下无需同步机制也能保证安全:
- 状态不可变:对象一旦创建,内部状态就不能改变,不存在并发修改的问题。
- 天然共享安全:多个线程可以同时读取同一个不可变对象,不会有数据竞争风险。
- 不需要加锁:因为没有写操作,所以不会出现死锁、锁竞争等并发问题。
- 可自由缓存和共享:可以放心地作为缓存、
Map的key、Set的元素等使用。
不可变对象是指创建后状态不能被修改的对象,因其状态不变性,在多线程环境下天然避免了并发冲突,因此被称为“天生线程安全”。
JMM与可见性
java内存模型(JMM)的基本概念
Java 内存模型(Java Memory Model,简称 JMM)是 Java 虚拟机规范中定义的一组规则,用于控制多线程环境下变量的可见性、有序性和原子性。它的目标是屏蔽不同硬件平台和操作系统的内存访问差异,确保 Java 程序在各种平台下都能表现出一致的并发行为。
JMM 的核心特性
| 特性 | 含义 |
|---|---|
原子性(Atomicity) | 一个操作要么全部执行成功,要么全部失败,不会被其他线程中断。例如 long 和 double 以外的基本类型读写是原子的。 |
可见性(Visibility) | 当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。通过 volatile、synchronized、final 等机制实现。 |
有序性(Ordering) | 程序执行的顺序与代码顺序一致。JMM 通过禁止指令重排序来保证有序性,如使用 volatile 或 synchronized。 |
主内存与工作内存
| 类型 | 描述 |
|---|---|
主内存(Main Memory) | 所有线程共享的内存区域,存放所有变量(不包括局部变量和方法参数) |
工作内存(Working Memory) | 每个线程私有的内存区域,保存该线程使用的变量副本,线程对变量的所有操作都在工作内存中进行,并与主内存同步 |
内存交互操作
JMM 定义了线程与主内存之间交互的八种操作,用于描述变量如何从主内存加载到工作内存,以及如何写回主内存:
read(读取):从主内存读取变量load(载入):将读取的变量放入工作内存use(使用):将工作内存中的变量传递给执行引擎assign(赋值):将执行引擎的结果赋值给工作内存中的变量store(存储):将工作内存中的变量传送到主内存write(写入):将存储的变量写入主内存lock(锁定):作用于主内存的变量unlock(解锁):释放主内存上的锁
这些操作必须满足 JMM 规定的规则,以确保并发一致性。
happens-before 原则
为了简化并发编程,JMM 提供了一组 happens-before 规则,只要两个操作之间存在 happens-before 关系,那么前者的结果对后者是可见的。
常见的 happens-before 规则:
| 规则 | 说明 |
|---|---|
| 程序顺序规则 | 同一个线程中,前面的操作 happen-before 后面的操作 |
| 锁定规则 | 对同一个锁,先 unlock 再 lock,后面的线程可见前一个线程的修改 |
volatile 变量规则 | 写 volatile 变量 happen-before 后续对该变量的读 |
| 传递性规则 | A happen-before B,B happen-before C,则 A happen-before C |
| 线程启动规则 | Thread.start() 的调用 happen-before 线程内的任何操作 |
| 线程终止规则 | 线程内的所有操作 happen-before 其他线程检测到该线程结束 |
| 中断规则 | 线程 A 调用 threadB.interrupt(),happen-before 线程 B 收到中断异常 |
| 对象终结规则 | 构造函数执行完成 happen-before finalize() 方法执行 |
Java 内存模型(JMM)是一套规范,定义了多线程程序中变量的可见性、有序性和原子性,通过主内存与工作内存的抽象、happens-before 原则等机制,屏蔽底层差异,保障并发程序的正确性。
volatile是如何保证可见性和有序性?
volatile 是 Java 中用于保证多线程环境下变量可见性和禁止指令重排序的关键字。它通过在底层插入 内存屏障(Memory Barrier) 来实现这两个特性
如何保证可见性?
🔍 原理说明:
每个线程都有自己的工作内存(Working Memory),普通变量的读写只发生在工作内存中,不保证对其他线程立即可见。
volatile 变量的读写会直接操作主内存(Main Memory),确保所有线程看到的是同一个最新值。
🧠 具体机制:
写操作(Write):
当一个线程修改 volatile 变量时,JVM 会立即将该变量的值刷新到主内存。
插入一个 写屏障(Store Barrier),确保写操作不会被重排序到屏障之后。
读操作(Read):
当一个线程读取 volatile 变量时,JVM 会从主内存中重新加载该变量的值,而不是使用本地缓存。
插入一个 读屏障(Load Barrier),确保读操作不会被重排序到屏障之前。
如何保证有序性?
🔍 原理说明:
Java 编译器和 CPU 为了优化性能,可能会对指令进行 重排序(Reordering)。虽然重排序不会影响单线程执行结果,但在多线程环境下可能导致不可预期的行为。
volatile 通过插入 内存屏障 来禁止特定类型的指令重排序。
🧠 内存屏障类型及作用:
| 屏障类型 | 作用 |
|---|---|
LoadLoad Barriers | 确保前面的读操作在后面的读操作之前完成 |
StoreStore Barriers | 确保前面的写操作在后面的写操作之前完成 |
LoadStore Barriers | 确保前面的读操作在后面的写操作之前完成 |
StoreLoad Barriers | 确保前面的写操作在后面的读操作之前完成 |
volatile 读写操作前后都会插入相应的内存屏障,从而防止编译器和 CPU 的重排序优化。
volatile 通过 强制读写主内存 + 插入内存屏障,实现了两个关键并发特性:
- ✅ 可见性:一个线程对 volatile 变量的修改,对其他线程立即可见;
- ✅ 有序性:禁止指令重排序,保证程序执行顺序与代码顺序一致。
但它不保证原子性,适用于状态标志、控制标志等简单场景。复杂并发操作仍需配合锁或原子类使用。
推荐记忆技巧
画图辅助理解:
- 线程状态图
CAS流程图
- 线程池结构图
动手实践:
每学一个知识点,都尝试写一个 Demo 验证。
对比记忆法:
- 比较
synchronizedvsReentrantLock - 比较
CountDownLatchvsCyclicBarrier
口诀记忆法:
- “线程五态:新、就绪、运行、阻塞、死亡”
- “
CAS三问题:循环时间长、只能保证一个变量原子性、ABA问题”