并发编程核心知识点

63 阅读18分钟

线程基础

  1. Thread 创建与启动
  2. RunnableCallable 的区别
  3. join(),sleep(),yield() 方法的作用

线程状态和生命周期

  1. 线程的6种状态:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED

同步机制

  1. synchronized 关键字(对象锁 vs 类锁)
  2. volalite 关键字的作用和实现原理
  3. ReentrantLockCondition

线程池

  1. ExecutorServiceThreadPoolExecutor
  2. 四种常见的线程池类型(FixedThreadPoolCachedThreadPoolSingleThreadPoolScheduledThreadPool

并发工具类

  1. CountDownLatch
  2. CyclicBarrier
  3. Semaphore
  4. Exchanger

原子类和CAS

  1. AtomicIntegerAtomicReferenceAtomictampedReference 避免 ABA 问题
  2. CAS 原理及 ABA 问题解决方案

并发集合类

  1. ConcurrentHashMap
  2. CopyOnWriterArrayList
  3. ConcurrentLinkedQueue

线程安全与设计模式

  1. 不可变对象(Immuable
  2. ThreadLocal (避免线程间共享数据)
  3. 单例模式的线程安全实现

常见面试题汇总

线程相关

  1. 如何创建线程?哪种方式更好?
  • 继承 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 只接受 RunnableCallable
  • 资源共享更方便:多个线程可以共享同一个 Runnable 实例
  1. start()run() 的区别?
特性start()run()
作用启动一个新线程执行任务普通方法调用,不会开启新线程
线程行为真正实现多线程在当前线程中同步执行
是否并发执行
能否重复调用一个线程只能调用一次 start()可以多次调用
底层机制JVM 创建线程并调用 run()直接调用方法,无并发支持
  1. 如何优雅的停止一个线程?

在 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() 协作退出;确保资源释放、避免死循环

线程同步

  1. synchronized 底层实现原理?

synchronizedjava 中用来控制多线程访问共享资源的关键字。其底层实现依赖于 JVM 的 “监视器锁(monitor)”机制。

  • synchronized 修饰代码块和方法时,JVM 会通过 monitorentermonitorexit 指令控制锁的获取和释放
  • 每个对象都有一个与之关联的监视器(monitor),当线程执行到 synchronized 代码块时,会尝试获取该对象的 monitor 锁:
    • 如果 monitor 计数器为0,表示锁未被占用,当前线程获得锁并将计数器加1;
    • 如果 monitor 计数器不为0,且是当时线程持有锁,则计数器加1(可重入);如果 monitor 被其他线程持有,则当前线程进入阻塞状态,等待锁释放。
  • 当线程执行完 synchronized 代码块或发生异常时,JVM 会自动执行 monitorexit 指令,释放锁并将 monitor 计数器减1

此外,JVM 在底层可能对 synchronized 进行优化,如偏向锁、轻量级锁、重量级锁等阶段的转换,以提升性能。

  1. volatile 能保证原子性吗?为什么?

volatile 不能保证原子性,它只能保证变量的可见性和禁止指令重排序。

  • 可见性:在多线程环境中,每个线程都有自己的工作内存(working memory),变量的值可能只存在线程的本地缓存中,不会立即刷新到主内存。使用 volatile 修饰的变量,在每次读取时都会从主内存中重新加载,写入时也会立即刷新到主内存,从而保证其他线程能及时看到最新的值。
  • 禁止指令重排序:编译器和 CPU 为了提高性能,可能会对指令进行重排序。volatile 通过插入内存屏障(Memory Barrier)来阻止这种重排序行为,来确保写 volatile 变量之前的代码不会被重排序到写操作之后;读 volatile 变量之后的代码不会被重排序到读操作之前。

但它没有锁机制,也不能保证复合操作的原子性。

  1. ReentrantLock 相比 synchronized 有哪些优势?

ReentrantLockjava.util.concurrent.locks 包中提供的一个可重入的互斥锁,相比 synchronized 关键字,它提供了更强大、更灵活的锁机制。其主要优势:

  • 尝试获取锁(tryLock):ReentrantLock 支持非阻塞方式获取锁,synchronized 获取不到锁会一直阻塞。
  • 超时获取锁:可设置等待锁的超时时间,避免死锁
  • 可中断获取锁:支持在等待锁的过程中响应中断
  • 支持公平锁:默认是非公平锁
  • 锁的精确控制:明确的控制加锁和解锁的时机,而不是依赖代码块结构

线程池

  1. 线程池的核心参数有哪些?各有什么作用?
参数名作用说明
corePoolSize核心线程数,即使空闲也不会超时回收(除非设置了 allowCoreThreadTimeOut
maximumPoolSize最大线程数。线程池中允许最大的线程数量
keepAliveTime非核心线程的空闲超时时间,超过这个时间未执行的任务则被回收
unitkeepAliveTime 的时间单位(如秒,毫秒等)
workQueue任务队列。用于存放等待执行的任务,常用 LinkedBlockingQueueSynchronousQueue
threadFactory线程工厂。用于创建新线程,可自定义线程命名,优先级等
handler拒绝策略。当任务无法提交时(如队列满且线程数达到最大),按此策略处理
  1. 如何自定义拒绝策略?

要自定义线程池拒绝策略,需要实现 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 包装)

并发工具类

  1. CountDownLatchCyclicBarrir 的区别?
行为和使用场景CountDownLatchCyclicBarrir
功能用途一个线程(或多个)等待其他线程完成操作后才继续执行,计数只能减到0不可重置多个线程 相互等待彼此到达某个屏障点后再一起继续执行,可重复使用(支持重置)
计数是否可重用不可重用(计数一旦减到 0 就不能再用了)可重用(所有线程释放后可再次使用)
线程角色主从模式:一个主线程等待多个工作线程完成任务对等模式:所有线程互相等待,共同到达屏障点
异常处理无特殊处理,线程中断不影响计数任一线程中断或超时会触发 BrokenBarrierException,并破坏屏障状态
  1. Semaphore 的用途是什么?
  • 控制资源访问:控制同时访问的线程数量(如数据库连接池、线程池、限流等)
  • 实现互斥锁:设置许可数为 1 的 Semaphore 可作为互斥锁(Mutex)使用,确保同一时刻只有一个线程执行临界区代码
  • 任务调度协作:控制多个线程之间的协作行为,比如某些线程必须等待其他线程释放资源后才能继续执行

Semaphore 的主要用途是控制并发线程的数量,适用于资源池管理、限流、互斥访问等需要限制并发度的场景。

并发集合

  1. ConcurrentHashMap 是如何实现线程安全的?

ConcurrentHashMapJava 中线程安全的哈希表实现,适用于高并发场景。它在不同版本(JDK 1.7JDK 1.8)中实现机制有所不同,但核心思想都是减少锁粒度、提高并发性能。

JDK 1.7 实现原理

核心结构:

  • 使用 Segment 分段锁 + HashEntry
  • 将整个 Map 分成多个 Segment(默认 16 段)
  • 每个 Segment 相当于一个独立的 HashMap,内部使用链表存储键值对

线程安全机制:

  • 写操作:只锁定当前 Segment,不影响其他 Segment 的读写
  • 读操作:不加锁,通过 volatile 保证可见性
  • 优点:并发度高,默认支持 16 个线程同时写入

JDK 1.8 实现原理

核心结构:

  • 使用 Node 数组 + 链表/红黑树
  • 与普通 HashMap 类似,但在并发控制上做了增强

线程安全机制:

  • 写操作(put):使用 CAS + synchronized
    • CAS 更新头节点
    • 如果冲突严重,则用 synchronized 锁定链表或红黑树头节点
  • 读操作(get):不加锁,通过 volatile 保证读取到最新值
  • 扩容(resize):多线程协作扩容,采用 迁移机制(transfer),避免一次性迁移所有数据
  • 优点:减少了锁的粒度,提高了并发性能;支持更高的并发访问量
  1. CopyOnWriteArrayList 适用于什么场景?

核心特点:写时复制(Copy-On-Write):每次修改操作(addsetremove 等)都会创建一个新的数组副本,原数组用于读取。

适用场景:

场景描述
读多写少适合高并发读操作、写操作较少的情况,如缓存、事件监听器列表、配置管理等
弱一致性要求读操作可以容忍一定程度的“旧数据”,不要求实时一致性
避免写锁竞争写操作不频繁,但需要保证线程安全,且不想引入复杂的同步机制

CopyOnWriteArrayList 适用于读多写少、允许弱一致性的并发场景,它通过牺牲写操作性能来换取高效的并发读取能力。

原子类和CAS

  1. CAS 的三大问题是什么?如何解决?
问题类型描述解决方法
ABA 问题值从 A → B → ACAS 无法感知变化使用 AtomicStampedReferenceAtomicMarkableReference
自选开销大CAS 失败后不断重试导致 CPU 占用高控制重试次数,或切换为锁机制
只能操作单变量无法保证多个变量的原子操作封装成对象、使用锁

CAS 虽高效,但存在 ABA、自旋开销和单变量限制等问题。合理结合版本号控制、锁机制和对象封装,可以有效规避这些风险。

  1. AtomicInteger 是如何实现原子性的?

AtomicIntegerjava.util.concurrent.atomic 包下的一个原子类,用于实现对 int 类型变量的线程安全操作。它通过 CASCompare and Swap)算法 结合 volatile 变量 实现了无需锁的原子性操作

核心实现机制

  • 底层依赖 CAS
    • AtomicInteger 使用了 Unsafe 类 提供的 CAS 操作来保证原子性。
    • CAS 是一种乐观锁机制,包含三个操作数:内存地址值(V);预期原值(A);要更新的新值(B

只有当内存中的值等于预期原值 A 时,才将值更新为 B,否则不更新并重试

  • volatile 修饰变量
    • AtomicInteger 内部使用一个 volatile int value 来保存数值,确保多线程之间的可见性

AtomicInteger 通过 volatile 变量 + CAS 操作 实现了线程安全的原子操作,适用于读写频繁但冲突较少的场景,是 Java 并发编程中高效、常用的原子类之一

线程安全设计

  1. 什么是线程封闭?

线程封闭(Thread Confinement) 是一种并发编程中的设计思想,指的是 将对象或变量的访问限制在单个线程内部,从而避免多线程并发访问带来的线程安全问题。

实现方式说明应用场景
局部变量(Local Variables方法内的变量只属于当前线程栈,天然线程封闭方法内部临时变量、无状态操作
ThreadLocal每个线程拥有独立的变量副本,通过 get() / set() 访问用户会话信息(如登录用户)、数据库连接、事务上下文
私有对象 + 不发布引用对象不被其他线程访问,仅当前线程使用工具类实例、临时对象等

优点:

  • 无需同步机制:因为没有共享,所以不会出现并发冲突;
  • 性能高:避免了锁、CAS 等开销;
  • 简化代码逻辑:更容易理解和维护。

线程封闭是一种“不共享、不并发”的并发安全策略,通过限制变量只能被一个线程访问,从根本上避免线程安全问题。

  1. 什么是不可变对象?为什么是天生安全?

不可变对象是指:一旦创建,其状态(属性值)就不能被修改的对象。换句话说,一个类的实例在构造完成后,其所有属性值都不能被更改。

因为它们具备以下特性,使得多线程环境下无需同步机制也能保证安全:

  • 状态不可变:对象一旦创建,内部状态就不能改变,不存在并发修改的问题。
  • 天然共享安全:多个线程可以同时读取同一个不可变对象,不会有数据竞争风险。
  • 不需要加锁:因为没有写操作,所以不会出现死锁、锁竞争等并发问题。
  • 可自由缓存和共享:可以放心地作为缓存、Mapkey、Set 的元素等使用。

不可变对象是指创建后状态不能被修改的对象,因其状态不变性,在多线程环境下天然避免了并发冲突,因此被称为“天生线程安全”。

JMM与可见性

  1. java 内存模型(JMM)的基本概念

Java 内存模型(Java Memory Model,简称 JMM)是 Java 虚拟机规范中定义的一组规则,用于控制多线程环境下变量的可见性、有序性和原子性。它的目标是屏蔽不同硬件平台和操作系统的内存访问差异,确保 Java 程序在各种平台下都能表现出一致的并发行为。

JMM 的核心特性

特性含义
原子性(Atomicity一个操作要么全部执行成功,要么全部失败,不会被其他线程中断。例如 longdouble 以外的基本类型读写是原子的。
可见性(Visibility当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。通过 volatilesynchronizedfinal 等机制实现。
有序性(Ordering程序执行的顺序与代码顺序一致。JMM 通过禁止指令重排序来保证有序性,如使用 volatilesynchronized

主内存与工作内存

类型描述
主内存(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 后面的操作
锁定规则对同一个锁,先 unlocklock,后面的线程可见前一个线程的修改
volatile 变量规则volatile 变量 happen-before 后续对该变量的读
传递性规则A happen-before BB 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 原则等机制,屏蔽底层差异,保障并发程序的正确性。

  1. volatile 是如何保证可见性和有序性?

volatileJava 中用于保证多线程环境下变量可见性和禁止指令重排序的关键字。它通过在底层插入 内存屏障(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 变量的修改,对其他线程立即可见;
  • ✅ 有序性:禁止指令重排序,保证程序执行顺序与代码顺序一致。

但它不保证原子性,适用于状态标志、控制标志等简单场景。复杂并发操作仍需配合锁或原子类使用。

推荐记忆技巧

画图辅助理解:

  1. 线程状态图

微信图片_20250516171659_8.png

  1. CAS 流程图

微信图片_20250516154423_6.png

  1. 线程池结构图

微信图片_20250516171152_7.png

动手实践:

每学一个知识点,都尝试写一个 Demo 验证。

对比记忆法:

  • 比较 synchronized vs ReentrantLock
  • 比较 CountDownLatch vs CyclicBarrier

口诀记忆法:

  • “线程五态:新、就绪、运行、阻塞、死亡”
  • CAS 三问题:循环时间长、只能保证一个变量原子性、ABA问题”