JUC八股文
【要图】

线程
并发编程存在的三大问题?
- 原子性
同一时刻只能有一个线程对数据进行操作。在java中使用了atomic包和synchronized关键字来确保原子性
- 可见性
一个线程对主内存进行修改,其他线程可以看到。在java中使用synchronized和volatile两个关键字实现
- 有序性
一个线程观察其他线程的执行顺序,一般无序。在java中用happens-before原则来确保有序性
happens-before:如果 A happens-before B,那么 A 的结果(内存写入)对 B 可见,并且 A 的执行顺序在 B 之前(逻辑上有序)。
!!!只要不同时满足以上三个条件,就会参数并发问题
线程和进程的区别?
进程是操作系统分配资源的最小单位,线程是 CPU 调度的最小单位。
一个进程内部可以包含多个线程,这些线程共享进程的内存资源。
在 Java 中,比如一个 Spring Boot 微服务通常运行在一个 JVM 进程中,而这个进程内部会通过多个线程(如请求线程、业务线程)来并发处理任务。
线程创建的方式有哪些?
继承Thread类
继承Thread类,重写run()方法;
创建该类实例后,调用start()方法启动线程

优点:编写简单,若需要访问当前线程,无需使用Thread.currentThread()方法,使用this关键字即可获取当前线程;缺点:不能继承其他父类
实现Runnable接口
实现Runnable接口需要重写run()方法,将Runnable对象作为参数传递给Thread类,再调用start()方法启动线程

优点:线程只实现了Runnable接口,还可以继承其他类,适合多个相同线程处理同一份资源;缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法
实现Callable接口与FutureTask
Callable接口类似于Runnable,但是它的call()方法有返回值,并且可以抛出异常
要执行Callable任务,需要包装进一个FutureTask
- Thread类构造器只接受Runnable参数
- 而FutureTask实现了Runnable接口,使用FutureTask包装Callable接口,传入Thread类
1 | public class MyCallable implements Callable<Integer> { |
优点:适合多个相同线程处理同一份资源;可以有返回值并且能抛出异常。
缺点:编程稍微复杂,如果需要访问当前线程,必须使Thread.currentThread()方法
为什么一般推荐使用 Runnable / Callable,而不是直接继承 Thread?
一般推荐使用 Runnable 或 Callable,而不是直接继承 Thread。
第一,Java 是单继承,继承 Thread 会限制类的扩展性,而接口更灵活;
第二,Runnable / Callable 实现的是“任务”,而 Thread 负责“执行”,实现了任务和线程的解耦;
第三,Callable 可以有返回值并能感知异常,更适合复杂并发场景;
第四,线程池只接受 Runnable / Callable,更符合实际开发。
怎么启动线程?
通过Thread类的start()
1 | // 创建两个线程,使用start()开启线程 |
怎么停止自己的线程运行?
- 异常法停止:线程调用interrupt()方法后,在线程的run方法中判断当前对象的interrupted()状态,如果是中断状态则抛出异常,达到中断线程的效果
调用interrupt是如何让线程抛出异常的?
interrupt是什么?每个线程都有一个中断状态(boolean值),默认是false;调用thread.interrupt()会把这个状态设为true,表示希望你停下来;线程本身需要配合检查这个状态来决定是否停止
interrupt() 不是杀死线程,而是发出一个“中断请求”。
Interrupt() 适用的两种情况:1. 线程在阻塞(sleep/join/wait),会抛出InterruptedException并结束阻塞,2. 线程在运行,Interrupt() 只会设置一个标记
Thread.currentThread().isInterrupted(),需要线程自己检查并退出
- 在沉睡中停止:先将线程sleep,再调用interrupt标记中断状态,interrupt会将阻塞状态的线程中断。抛出中断异常,达到停止线程的效果
1 | // 在沉睡中停止 |
- stop() 暴力停止:线程调用stop()方法会被暴力停止,方法已弃用,强制让线程停止可能会使一些请理性工作得不到完成
- 使用return停止线程:调用interrupt标记为中断状态后,在run方法中判断当前线程状态,如果为中断状态则return,达到停止线程的效果
1 | // 使用return停止线程 |
怎么停止其他线程运行?
- 通过共享标志位主动终止
1 | public class SafeStopWithFlag implements Runnable { |
- 通过线程中断机制
通过 Thread.interrupt 触发线程中断状态,结合中断检测逻辑实现安全停止
1 | public class InterruptExample implements Runnable { |
- 通过 Future 取消任务
核心思想:用线程池执行任务,获取Future对象来控制任务,调 future.cancel(true) —> 调用任务线程的 interrupt(),依赖中断机制让任务结束
1 | // 一、创建线程池 |
为什么要重新设置中断标志
当线程在 sleep/wait/join 时被 interrupt:
JVM 会做两件事:
- 清除中断标志(设回 false)
- 抛出 InterruptedException
如果JVM做完这两件事的时候,执行到了catch(InterruptedException e) {},此时中断标志会被设置为false,while(!isInterrupted()) 就会继续跑,不能停止下来,这就是为什么要在catch中补充一句Thread.currentThread().interrupt();让中断标志回到true,外层while才能识别到,跳出循环

juc包下常用的类
线程池相关
ThreadPoolExecutor:最核心的线程池,用于创建和管理线程池。可以灵活配置线程池的参数(核心线程数、最大线程数、任务队列)
Executors:线程池工厂类,提供了一系列静态方法来创建不同的线程池。newFixedThreadPool(创建固定线程数的线程池)、newCachedThreadPool(创建可缓存线程池)、newSingleThreadExecutor(创建单线程线程池)
并发集合类
ConcurrentHashMap:线程安全的哈希映射表,用于在多线程环境下高效存储和访问键值对
CopyOnWriteArrayList:线程安全的列表,在操作列表时,会创建一个新的底层数组,将修改操作应用到新数组上,读操作仍可以在旧数据上读,实现了读写分离
同步工具类
CountDownLatch:让一个线程等待其他线程执行完毕后再执行;用于主线程等待多个子任务完成后再汇总结果
1 | ExecutorService pool = Executors.newFixedThreadPool(3); |
CyclicBarrier:让一组线程相互等待,直到全部达到屏障点;用于多线程分阶段计算。
屏障点,一组线程必须全部到达的一个同步位置,比如await()方法就是到达屏障点的动作
Semaphore:控制同时访问某个资源的线程数量,用于限制资源访问数量(数据库连接、线程池)
原子类
AtomicInteger:原子整数类,提供了对整数类型的原子操作,如自增、自删、比较并交换
AtomicReference:原子引用类,用于对对象引用进行的原子操作
如何保证多线程安全?
可以通过synchronized关键字、volatile关键字、Lock接口和ReentrantLock类、原子类、线程局部变量(ThreadLocal类可以为每个线程提供自己的变量)、并发集合、JUC工具类(Semaphore、CyclicBarrier)
如何保证数据一致性的方案?
- 事务管理
使用数据库事务来确保一组数据操作要么全部成功,要么全部失败回滚。通过ACID(原子性、一致性、隔离性、持久性)来确保
- 锁机制
使用锁来实现对共享资源的互斥访问
- 版本控制
通过乐观锁,再更新数据时,记录数据的版本,避免对同一数据进行修改
乐观锁、悲观锁
乐观锁:假设冲突很少发生,不加锁直接去更新,失败了再处理
核心思想:在数据库中,加一个版本号字段;每次更新时,先检查版本号是否一致,一致才更新,并将版本号 + 1;如果版本号不一致,说明在你更新前,别人已经修改了这条数据,更新会失败
悲观锁:假设冲突会发生,更新前先锁住记录,别人不能修改
核心思想:假设并发冲突一定会发生,所以在操作数据前锁住数据;其他事务在此期间不能修改,直到锁释放;通过数据库锁机制,来保证同一时刻只能有一个事务能操作这条数据
Java的线程状态有哪些?

通过Thread中的getState()方法可以获取当前线程的状态
- new:线程正在创建,还未调用start方法
- runnable:就绪状态(调用start,等待调度)+ 正在运行
- blocked:等待锁时,陷入阻塞状态
- waiting:无限期的等待
- timed_waiting:有时限的等待
- terminated:线程完成执行,终止状态
sleep和wait的区别是什么?
所属分类不同:sleep() 是 Thread 类的静态方法,wait() 是 Object 类的方法;
锁释放的情况:sleep() 不会释放锁,但会让线程进入 TIMED_WAITING 状态,时间到自动唤醒;
wait() 会释放锁,线程进入 WAITING 或 TIMED_WAITING 状态,需要通过 notify 或 notifyAll 唤醒;
使用条件:sleep() 可以在任何地方使用,而 wait() 必须在 synchronized 同步代码块中使用;
唤醒机制:sleep休眠时间结束后,线程自动恢复到就绪状态;wait需要其他线程调用相同对象的nofity() 或 notifyAll()方法才能被唤醒
设计用途:sleep() 暂停线程执行,不涉及锁协作;wait()进程之间的协作
blocked和waiting区别是什么?
触发机制:blocked是锁竞争失败后被动触发的状态;waiting是人为主动触发的状态
唤醒方式:blocked唤醒时自动触发;waiting是必须通过notify,nofityAll主动唤醒
notify和notifyAll的区别?
notify:唤醒一个线程,其余线程依然处于wait的等待唤醒状态
notifyAll() :唤醒等待集中的所有线程,它们在当前持锁线程释放锁后进入锁竞争;竞争顺序未指定,且任意时刻只有一个线程能获得该监视器锁
sleep会释放cpu吗?
会的,调用Thread.sleep()时,线程会释放cpu,但不会释放持有的锁;当线程调用sleep后,会主动让出CPU时间片,进入timed_waiting状态,此时操作系统将CPU分配给其他就绪的线程
wait状态下的进程如何恢复到running状态?
由等待(wait)状态到运行(running)状态的核心机制是通过外部事件触发或资源可用性变化,比如等待线程被其他线程通过nofity和nofityAll唤醒
yield() 是干嘛的?实际用得多吗?
yield() 会让当前线程主动让出 CPU 执行权,但不会阻塞线程,线程仍然处于 RUNNABLE 状态;
是否真的让出 CPU 由调度器决定,因此实际开发中很少使用。
notify选择哪个线程?
notify在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm
1 | static final Object lock = new Object(); |
不同的线程之间如何通信?
- Volatile关键字
1 | private static volatile boolean flag = false; |
volatile关键字确保了一个变量在多个线程之间的可见性和禁止指令重排序,一个线程修改了一个变量,另一个线程能立即看见
生产者线程睡眠0.2s后,将flag设置为true;消费者线程在flag为false时,一直等待,直到flag变为true才继续执行
- notify()方法
Object类中的wait()、nofity()和notifyAll()方法都可以用于线程间的协作。
- wait()方法:使当前线程进入等待状态
- nofity()方法:唤醒此对象监视器上等待的单个随机线程
- nofityAll方法:唤醒此对象监视器上等待的所有线程
1 | public class WaitNotifyExample { |
lock 是一个用于同步的对象,生产者和消费者线程都需要获取该对象的锁才能执行相关操作,wait、nofity、notifyAll
- Lock、Condition接口
Lock和Condition接口提供了比synchronized更灵活的线程通讯方式;
Condition接口的await()方法类似于Lock的wait()方法,signal()方法类似于nofity()方法,signalAll()方法类似于nofityAll()方法;
一个Lock可以对应多个Condition
1 | public class MultiConditionExample { |
和wait、nofity的区别?
wait/nofity 必须用 synchronized 、Lock 搭配
await/signal 必须用 Lock 搭配,而且一个锁可以有多个 Condition (更灵活,可实现多条件唤醒)
- ReentrantLock
是Lock接口的一个实现类
Reentrant 表示可重入:同一个线程可以多次获取一把锁而不会死锁(通过维护一个持有计数)
主要特点(对比synchronized)

构造方式
1 | // 默认非公平锁(性能好,但可能“插队”) |
示例
1 | ReentrantLock lock = new ReentrantLock(); |
守护线程了解吗?
了解,守护线程可以理解为后台服务线程,比如垃圾回收线程
它的特点就是:当所有用户线程(非守护线程)都结束时,即使守护线程的代码还没执行完,它也会自动结束。通过 setDaemon(true) 来设置
Java的同步并发工具有哪些?
- CountDownLatch(倒计数器)
调用 countDown() 方法会使计数器减一,当计数器的值减为0时,等待的线程会被唤醒
CountDownLatch是什么?
CountDownLatch 用于一个或多个线程等待一组线程执行完成,计数器减到 0 后等待线程继续执行;它是基于计数器实现的,计数器归零无法重置,不能复用。
核心原理
- 初始化计数器:创建
CountDownLatch时指定一个初始计数值(如3) - 等待线程阻塞:线程A调用
await()的线程会被阻塞,直到计数器变为0 - 任务完成通知:其他线程(B、C、D)完成任务后调用
countDown(),使计数器减1 - 唤醒等待线程:当计数器减到0时,线程A会被唤醒
- CyclicBarrier(同步屏障)
让一组线程相互等待,所有线程都到达同一个屏障点之后,才会继续向下执行;比如new CyclicBarrier(5),表示需要5个线程都调用await()方法等待。当第5个线程到达时,屏障打开,所有被阻塞的线程同时被唤醒继续执行。之后会重置状态,所以可以复用。
- Semaphore(信号量)
用于设置同时访问某个共享资源的线程数,可以看作带数量的锁
Semaphore ≠ 锁
- 锁:同一时间只能 1 个
- Semaphore:同一时间可以 N 个
- Future和Callable
配合线程池,将Callable创建的线程交给线程池,由Future创建的对象接收结果
- ConcurrentHashMap
线程安全的哈希表,允许多个线程同时进行读写操作
CountDownLatch和CyclicBarrier有什么区别?
核心思想:CountDownLatch是一个线程等多个线程;CyclicBarrier是多个线程互相等
重置性:CountDownLatch的计数器只能用一次;CyclicBarrier的计数器可以重置,循环使用
动作:CountDownLatch的减计数操作countDown()可以在多个线程中分散调用;CyclicBarrier的等待操作await()必须在每个需要同步的线程中自己调用
锁
Java中有哪些常用的锁
- 内置锁(synchronized)
- ReentrantLock
- 读写锁(ReadWriteLock)
- 乐观锁、悲观锁:synchronized和ReentrantLock默认都是悲观锁
- 自旋锁(CAS):自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃CPU并阻塞
非公平锁吞吐量为什么比公平锁大?
- 公平锁执行流程:获取锁时,先将线程自己添加到等待队列队尾,当某线程用完锁之后,会唤醒等待队列队首的线程尝试去获取锁。整个过程中,线程会从 运行状态 ——> 休眠状态,再从 休眠状态 ——> 运行状态。每次休眠和恢复都要从用户态切换到内核态,切换状态比较慢
- 非公平锁的执行流程:当线程获取锁时,先通过CAS尝试获取锁,不用唤醒其他线程,成功直接拥有锁,失败进入等待队列中阻塞,等待下次尝试获取锁
什么情况下会产生死锁、如何解决?

如何解决?
使用资源有序分配法
给所有资源类型规定一个全局总顺序(全序),例如:A > B > C > …
线程1 先获取A资源,再获取B资源
线程2同样,先获取A资源,再获取B资源
线程n先获取A资源,再获取B资源
就是说线程1 ~ n总以相同的全局总顺序获取资源
什么是“可重入锁”?为什么一定要可重入?
可重入锁是指同一个线程在已经持有锁的情况下,可以再次获取同一把锁而不会发生死锁;
这样可以支持方法的嵌套调用,避免线程自己阻塞自己。
Synchronized和Reentrantlock
Synchronized
工作原理
- synchronized是java提供的原子性内置锁,这种使用者看不到的锁也被称为监视器锁
- 使用synchronized后,会在编译之后,同步的代码块前后加上monitorenter和monitorexit字节码指令
- 执行monitorenter指令时会尝试获取锁对象,如果代码块没有被锁定或已经获得了锁,锁计数器 + 1,此时其他竞争锁的线程则会进入等待队列中
- 执行monitorexit指令时会将计数器 - 1,当计数器为0时,锁释放,处于等待队列中的线程再继续竞争锁
- synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后,才能竞争锁
应用场景
- 简单同步需求
- 代码块同步
- 内置锁的使用
除了用synchronized,还有什么方法可以实现线程同步?
- 使用volatile关键字
- 使用ReentrantLock类
- 使用Atomic类
synchronized锁静态方法和普通方法区别?

- 普通方法
1 | public synchronized void normalMethod() { ... } |
锁的是 this 对象本身。
也就是说:
- 同一个对象 A:
- 线程1 调用
A.normalMethod() - 线程2 调用
A.otherSyncMethod() - 两个方法不能并发,因为锁都是
A这一个对象。
- 线程1 调用
- 不同对象 A、B:
- 线程1 调用
A.normalMethod() - 线程2 调用
B.normalMethod() - 可以并发执行,因为锁对象是
A和B,两把完全不同的锁。
- 线程1 调用
- 静态方法
1 | public static synchronized void staticMethod() { ... } |
static synchronized锁的是类,也就是说,同一个类里的所有 static synchronized 方法,以及任何 synchronized(SomeClass.class) 的代码块,同一时刻只能被一个线程执行。
JVM对Synchronized的优化
- 锁膨胀:Synchronized从无锁升级到偏向锁、再到轻量级锁,最后到重量级锁的过程,叫做锁膨胀或者锁升级;大部分场景不需要用户态到内核态(内存中阻塞)的转换了
- 锁消除:JVM虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而提高程序性能的目的
- 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁
- 自适应自旋锁:通过自身循环尝试获取锁,避免了从用户态转化为内核态
Synchronized锁升级过程
前置信息
- Lock Record 是存在线程栈里的锁记录结构,用来保存加锁前对象头的 Mark Word
- Java对象头
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息
Klass Point:虚拟机通过这个指针来确定这个对象是哪个类的实例
- Monitor:同步工具、同步机制,每一个Java对象就有一把看不见的锁,称为Monitor锁
在JDK15之前过程是:无锁 —> 偏向锁 —> 轻量级锁 —> 重量级锁
JDK15:偏向锁默认禁止
JDK18:偏向锁废除——为什么不使用了?
- 早年代码同步过度:早年的集合类如HashTable、Vector,内部存在大量的synchronized,如果单线程使用它不加偏向锁,每次都要验证;而现在单线程的HashMap、ArrayList,多线程ConcurrentHashMap、CopyOnWriteArrayList之类的,根本不用每次访问都同步
- CAS成本下降:现在硬件好了,JVM优化好了,CAS成本下降很多
- 线程池模型不友好:同一把锁容易被不同线程先后用到,那偏向的就经常要被撤销,撤销就很贵
无锁:标志位 01
无锁没有对资源进行锁定,所有线程都能访问并修改同一个资源,但是同一时间只有一个线程可以修改成功;
偏向锁:标志位 01
概括:首次线程访问时,通过CAS将线程ID写入obj的MarkWord,下次同一个线程来,无需加锁。一段同步代码块一直被一个线程所访问,那么该线程就会自动获取锁,降低获取锁的代价
目的:在只有一个线程执行同步代码块时能提高性能,比如早期的HashTable、Vector;在无多线程竞争下尽量减少不必要的轻量级锁
偏向撤销:偏向锁在被其他线程竞争时,会先进行偏向撤销,把对象头从偏向模式恢复为普通可锁状态,再升级为轻量级锁/重量级锁
轻量级锁:标志位 00
概括:当多个线程交替执行(非竞争),JVM在栈帧创建Lock Record,通过CAS将Mark Word指向该记录
当前的并发不是很严重
实现:假设T1线程拿到锁,T2,T3线程没有拿到,此时T2,T3线程不再进行阻塞,而是进行一个自旋操作尝试获取锁
升级为轻量级锁的过程:轻量级锁通过在线程栈中创建 Lock Record,把对象头obj的 Mark Word 复制到栈上,然后用 CAS 把对象头替换为指向 Lock Record 的指针并标记为轻量级锁。
无竞争时加解锁只是在对象头和栈上来回 CAS,遇到竞争就先自旋,严重时再膨胀成重量级锁。
重量级锁:标志位 10
概括:当自旋失败(竞争激烈),Mark Word指向操作系统维护的Monitor对象,线程进入阻塞队列
当前的并发严重
图示:
Thread-2访问obj对象
![]()
Thread-2释放锁,由EntryList(Thread-1、Thread-2)中的Thread-1抢到锁
![]()
说明:EntryList 抢锁的队列,WaitSet 等条件;Owner 正在执行的线程,MarkWord 指针告诉你锁已经膨胀
- 当线程Thread-2访问obj对象:此时它会尝试将obj对象和操作系统提供的monitor对象相关联,靠一个指针地址关联,通过这个指针可以找到monitor对象;此时没有其他线程,Thread-2就绑定在Monitor的Owner上
- Thread-1来了,访问obj对象:首先它会根据obj对象中的MarkWord指向的Monitor对象,看里面有没有Owner,如果有,就将Thread-1存储在EntryList中,将线程置为Block阻塞状态
- Thread-3来了,访问obj对象:此时同步代码块没有执行完毕,将Thread-3同理存储在EntryList中
- 当Thread-2线程结束后,会变遍历EntryList中的线程进行抢锁,抢到锁的线程绑定再Owner上(假设Thread-1抢到了锁)
Reentrantlock
工作原理
- 底层依赖于AbstractQueuedSynchronizer(AQS)这个抽象类,在它的基础上通过内部类Sync实现具体锁操作
- 可中断性:线程在等待锁的过程中,可以被其他线程中断而提前结束等待
- 设置超时时间:等待一段时间后如果没获得锁,则放弃锁的获取
- 公平锁和非公平锁:默认是非公平锁,在创建的时加 true 就可以创建公平锁
公平锁和非公平锁的Lock()方法唯一区别就在于是否判断 hasQueuedPredecessors() 这个方法
hasQueuedPredecessors():表示在等待队列中是否已经有线程在排队了
公平锁会判断hasQueuedPredecessors()
👉 判断“当前线程前面,还有没有已经排队等锁的线程”
- 有 → 我得老实排队
- 没有 → 我可以直接和队头第一个线程抢锁
👉 这是公平锁“公平”的核心判断条件
【注意】tryLock():tryLock可以插队,当线程执行tryLock() 方法时,一旦有线程释放了锁,那么正在tryLock的线程就能获取锁,即是公平锁模式
1
2
3 public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
-
多个条件变量:支持多个条件变量,每一个变量可以与ReentrantLock关联——一把 ReentrantLock 可以生成很多个 Condition
-
可重入性
同一个线程可以多次获取同一把锁,不产生死锁。通过holdCount实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void methodA() {
lock.lock(); // 第一次获取锁
try {
System.out.println(Thread.currentThread().getName() + " 进入 methodA");
methodB(); // 调用另一个上锁方法
} finally {
lock.unlock(); // 对应释放一次
}
}
public void methodB() {
lock.lock(); // 第二次获取同一把锁
try {
System.out.println(Thread.currentThread().getName() + " 进入 methodB");
} finally {
lock.unlock(); // 对应释放一次
}
}
public static void main(String[] args) {
ReentrantLockDemo demo = new ReentrantLockDemo();
demo.methodA();
}
}
- 一个线程第一次获取锁时,计数器 + 1
- 如果同一个线程再次获取锁,计数器 + 1
- 线程释放锁时,计数器 - 1
- 当计数器为0,锁才完全释放
应用场景
- 高级锁功能需求
- 性能优化
Synchronized和Reentrantlock的区别

CAS、AQS
CAS
CAS是一个原子指令,本身不是锁,包含三个操作数:需要读写的内存值V,需要比较的值(预期值)A,新值B
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。
Java中的CAS是( 由 AtomicInteger 类中 compareAndSet 方法 )调用 unsafe对象方法
compareAndSet(int expect, int update)——如果类内部的value值 = except,就将value值更新为update
1 | // compareAndSet源码 |
unsafe.compareAndSwapInt()中的字段
- Object obj:表示要更新的对象
- long offset:对象内字段的偏移量,表示AtomicInteger对象中的值内存位置
- int expected:字段预期值
- int x:要设置的新值
应用场景:抢票
1 | public class CASTicketingSystem { |
通过while自旋:一旦CAS失败,就重新读取最新的值,再计算新的结果,直到成功为止
compareAndSet逻辑
- CAS 会检查当前余票是不是刚才读到的 currentAvailable。
- 如果没被别人改过,就更新成功。
- 如果这期间有人也在抢票,把余票改掉了,那 CAS 就失败,返回 false。
该应用相比较于传统方法,用synchronized将“读余票 —> 减扣 —> 返回结果”整个过程抱起来,线程是安全,但是效率低;而用CAS保证了在并发环境下修改共享变量的正确性
CAS有什么缺点?
- ABA问题
线程一:先读到内存值为A,准备改成C;
线程二:将它修改“A —> B —> A”修改一圈回到了A;
线程一:再做CAS以为数据没动过,CAS成功了;
问题是:期间的状态线程一是不知道的
解决方法:将“值“变为“版本号 + 值”,只要发现版本号不一致就说明其他线程修改了的;变化过程就从“A-B-A”变成了“1A-2B-3A”
-
循环时间长开销大
-
只能保证一个共享变量的原子操作
CAS对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的
比如:同一个对象的不同字段:比如 order.status 和 order.version。
order对象中有几十种字段,但我就想让status、version完成一次CAS
- 原始值:
status = '1', version = 1 - 期望更新为:
status = '2', version = 2
我想要的效果是:
对其他线程来说,要么看到
('1', 1),
要么看到('2', 2),
不要看到中间奇怪的组合:('1', 2)或('2', 1)。
另一个线程 B 在两步之间读:
- 可能看到:
status = "2", version = 1 - 或:
status = "1", version = 2
这就不是原子操作了
就不满足原子性的操作,要解决很简单,就是将这两个字段单独抽出来做成一个变量OrderState,做一次CAS
1 | class OrderState { |
对于order中几十种字段,如果都需要这样,代码非常复杂
AQS
全称为AbstractQueuedSynchronizer,是Java中的一个抽象类;AQS是一个用于构建锁、同步器、协作工具类的工具类
核心
- 如果共享资源空闲:就将当前请求资源的线程设为有效的工作线程,将共享资源设置为被占用状态
- 如果共享资源被占用:需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要是用CLH队列的变体实现的,将暂时获取不到的锁的线程加入队列中
CLH队列
CLH队列是单向链表
但是AQS中是CLH变体的虚拟双向链表,AQS通过将每条请求共享资源的线程封装成一个节点来实现锁的分配
AQS使用一个Volatile的int类型的成员变量State来表示同步状态,通过内置的FIFO队列来完成资源的获取的排队工作,通过CAS完成对State值的修改
同步器 state 含义 ReentrantLock 可重入次数 Semaphore 剩余许可数 CountDownLatch 计数值 ReentrantReadWriteLock 高位读锁计数,低位写锁计数 当线程获取锁失败的时候,AQS会
- 创建一个 Node 节点(包含线程引用、等待状态)
- CAS 使用尾插法把这个节点放到队列尾部(tail)
- 设置它的前驱节点为旧的 tail
- 返回这个前驱节点,用于后续判断是否应该阻塞
为什么 AQS 只让 head 的后继去抢锁?
1 | head (线程 = null,虚拟头结点) |
AQS 只让 head 的后继线程去抢锁,是为了:
是为了保证 FIFO 公平性,同时避免大量线程同时 CAS 竞争造成性能浪费
AQS主要完成的任务
- 同步状态(比如计数器)的原子性管理
- 线程的阻塞和解除阻塞
- 队列的管理
AQS最核心的三大部分
- 状态:state
它会根据不同的实现类而改变
- Semapore:剩余许可证的数量
- CountDownLatch:当前剩余的计数
- Reentrantlock:锁的占用状态,包括可重入计数,当state的值为0的时候,表示该Lock不被任何线程占用
- FIFO队列(双向链表)
AQS会维护一个等待的队列,把线程都放在这个队列中,这个队列是双向链表的形式
这个队列用来存放等待的线程,当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁
- 实现的获取/释放等重要方法(重写)
由协作类自己去实现,每个实现类都要重写tryAcquire和tryRelease等方法
如何用AQS实现一个可重入的公平锁
- 继承AbstractQueuedSynchronizer
创建一个内部类继承于它,重写tryAcquire(获取锁)、tryRelease(释放锁)、isHeldExclusively(锁是否被当前线程持有)
- 实现可重入逻辑
在tryAcquire方法中,检查当前线程是否持有锁
- 持有锁:通过state增加锁的持有次数
- 没有持有锁:尝试用CAS操作来获取锁
- 实现公平性
在tryAcquire方法中,按照队列顺序来获取锁,先检查等待队列中是否有线程在等待,如果有,当前线程必须进入队列等待,而不是直接竞争锁
- 创建锁的外部类
创建一个外部类,内部持有AbstractQueuedSynchronizer的子类对象,并提供lock、unlock方法,这些方法将调用AbstractQueuedSynchronizer子类中的方法
1 | import java.util.concurrent.locks.AbstractQueuedSynchronizer; |
CAS和AQS有什么关系?
CAS为AQS提供原子操作的支持:AQS内部使用CAS操作来更新state变量,实现线程安全的状态修改
为什么AQS中CAS不需要加版本号避免ABA问题?
AQS 的 CAS 根本不依赖“期间有没有变过”,只依赖“当前时刻的值是否满足条件”
AQS 获取锁的完整源码流程(从 lock 到 park)
lock() → tryAcquire → 失败入队 → 自旋判断 → park 阻塞 → 被唤醒 → 再次 tryAcquire
尝试 → 排队 → 自旋 → 阻塞 → 唤醒 → 再试
ThreadLocal
什么是ThreadLocal?
ThreadLocal 用来为“每个线程”提供一份独立的变量副本,线程之间互不影响。
ThreadLocal的作用
ThreadLocal 的核心思想是线程隔离,为每个线程提供独立的变量副本,数据实际存储在线程内部的 ThreadLocalMap 中,线程之间互不干扰。
【例子】:当一个用户的请求发送到服务器上的时候,创建处理这个用户的线程,将用户的信息提前加载到ThreadLocal中,这样就可以避免多次重复的查询数据库
1 | import java.util.*; |
包含关系

- 在Thread类中定义了ThreadLocalMap
- 在ThreadLocalMap中有内部成员 entry[]
- entry里面存的值是键值对
- key: ThreadLocal
- value: value

为什么key(ThreadLocal)是弱引用?
避免内存泄漏,若使用强引用,当栈帧结束后,由于entry对象有引用,因此不会被垃圾回收,造成内存泄漏;若使用了弱引用,下一次GC就会被回收
为什么value是强引用?
当使用ThreadLocal对象中的set、get、delete方法会检查是否存在key,如果不存在就将value删除;会自动检测值;如果使用了弱引用,一次GC就会将值给清零,数据不一致
ThreadLocal 怎样实现线程隔离?/ 父子线程怎么共享数据给子线程?
每个线程都会有自己的 ThreadLocalMap 变量副本,存储于线程私有的虚拟机栈中,而不是堆中,不会被其他线程访问到,因此实现了线程隔离。
ThreadLocal 有什么问题?
ThreadLocal 的键是弱引用,键被回收后,值仍由线程持有,因此在长寿命线程(尤其线程池、SpringBoot)且没有后续清理操作时,value 可能长期无法释放。为避免泄漏,应在使用完马上 remove()
ThreadLocalMap怎么解决Hash冲突?
通过开放寻址法
ThreadLocalMap扩容机制?
它的扩容阈值是数组长度的2/3。当数组中的Entry数量超过这个阈值时,就会先尝试探测式清理(expungeStaleEntries)掉那些Key为null的陈旧Entry。如果清理后数量还超过阈值的 3/4,也就是数组长度的1/2,才会进行真正的扩容(resize),创建一个2倍大的新数组,然后重新哈希(rehash)所有有效的Entry。
volatile关键字
volatile关键字有什么作用?
- 保证对这个变量的写操作会立即刷新到主存中,对这个变量的读操作会直接从主存中读取,保证了多线程下对变量访问的可见性
- 禁止指令重排序优化
volatile关键字在java中对内存屏障来禁止特点的指令重排序

指令重排序是什么?
执行程序时,为了提高性能,处理器和编译器会对指令进行重排序,需要满足
- 在单线程环境下不能改变程序的运行结果
- 存在数据依赖关系的不允许重排序
【例子】
![]()
C依赖于A、B;
A、B相互没关系;于是A、B可以互换位置,但是C不行,必须在A、B下面
volatile可以保证线程安全吗?
volatile关键字可以保证可见性,但不能保证原子性,因此不能完全保证线程安全。
线程池
介绍一下线程池工作原理

wc = workerCount(当前工作线程数)
1 | submit 任务 |
线程池的参数有哪些?

- corePoolSize:线程池核心线程数量,默认情况下,线程池中线程的数量如果 ≤ corePoolSize,那么即使这些线程处于空闲状态,也不会被销毁
- maximumPoolSize
限制了线程池能创建的最大线程总数
当corePoolSize已满 并且 尝试将新任务加入阻塞队列失败(队列已满),此时线程数 < maximumPoolSize,就会创建新线程执行此任务
当corePoolSize满,队列满,线程数已达maximumPoolSize,此时有新任务提交,就会出发拒绝策略
- keepAliveTime:当线程池中线程数量 > corePoolSize,并且某个线程空闲时间超过keepAliveTime,这个线程就会被销毁
- unit:keepAliveTime的时间单位
- workQueue:工作队列,当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行
- threadFactory:线程工厂,给线程取名字等等
- handler:拒绝策略
线程池工作队列满了有哪些拒绝策略?
- AbortPolicy:直接抛出一个任务被线程池拒绝的异常(默认)
- CallerRunsPolicy:不抛异常,也不丢任务,而是让提交任务的那个线程(调用
execute()的线程)自己去跑这个任务。 - DiscardPolicy:直接把新来的任务丢掉
- DiscardOldestPolicy:抛弃工作队列中最老的任务(也就是队头的任务,FIFO队列嘛),然后将该任务存放在工作队列中
- 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略
RejectedExecutionHandler
线程池提交execute和submit有什么区别?
返回值:execute()没有返回值;submit()会返回一个Future对象,可以通过它获取任务执行结果或取消任务
异常处理:execute()提交的任务如果抛出异常,会直接抛出,可能会终止线程;submit()提交的任务,异常会被封装在Future.get()里,需要调用该方法才能捕获到
参数:execute()只能提交Runnable任务;submit()可以提交Runnable和Callable任务
线程池参数设置的经验?
核心线程数(corePoolSize)
- CPU密集型:corePoolSize = CPU核数 + 1(避免过多线程竞争CPU)
- IO密集型:corePoolSize = CPU核数 x 2(或更高,具体看IO等待时间)
- 电商场景:特点瞬时高并发,任务处理时间短,线程池配置可设置如下

- 后台数据处理服务:稳定流量,任务处理时间长,允许一定延迟,线程池配置可设置如下

核心线程数可不可以设置为0?
可以,当核心线程数为0时候,这表示线程池不会常驻任何线程,当有任务提交的时候,会创建非核心线程来执行任务
线程池种类有哪些?
- ScheduledThreadPool:可以设置定期的执行任务,支持定时或周期性执行任务
- FixedThreadPool:核心线程数和最大线程数一样,可以把它看作是固定线程数的线程池
- CachedThreadPool:可缓存线程池,特点在于线程数几乎可以无限增加的,当线程闲置时还可以对线程进行回收,也就是说线程池的线程数量不是固定不变的
- SingleThreadExecutor:使用唯一的线程去执行任务,原理和FixedThreadPool是一样的,由于只有一个线程,非常适合任务按照顺序提交来执行
线程池一般怎么用?
虽然Java中Executors类定义了一些快捷工具方法,能快速创建线程池。
但是《阿里巴巴Java开发手册》中提到,禁止使用这些方法,应该手动new ThreadPoolExecutor来创建线程
要根据实际的业务来评估核心参数(可以举**线程池参数设置的经验?**例子)
线程池中shutdown (),shutdownNow()这两个关闭方法有什么作用?
- shutdown()
使用了会让状态为SHUTDOWN,温和关闭,正在执行的任务会继续执行下去,没有被执行的则会中断,此时不能往线程池中添加任何任务,否则会抛出异常
- shutdownNow()
使用了会让状态为STOP,暴力关闭,通过调用Thread.interrupt()方法尝试停止所有正在执行的线程,不再处理还在线程池队列中等待的任务,也会返回那些未执行的任务
提交给线程池中的任务可以被撤回吗?
可以,当向线程池提交任务时,会得到一个Futrue对象,这个对象提供了几种方法来管理任务的执行,包括取消任务
取消任务主要的方法是 Future 接口中的 cancel(bool mayInterruptIfRunning) 方法
- true:如果任务开始执行,中断任务
- false:如果任务开始执行,不被中断
线程池中线程是如何保活和定时回收的?
在getTask() 方法中
核心线程常驻、非核心该走就走

allowCoreThreadTimeOut:
要不要对“核心线程”也启用 keepAliveTime 的闲置超时和回收机制。默认是 false,也就是核心线程闲着也不走。
通过allowCoreThreadTimeOut设置timed的取值,根据timed的值调用相应的函数

- false(默认):取任务时用 workQueue.take() 取不到就无限等待,不存在闲置超时和回收
- true:取任务时用 workQueue.poll(),超时拿不到任务可能被干掉
总结:
想要开启闲置超时和回收机制
通过
ThreadPoolExecutor.allowCoreThreadTimeOut(true);并且设置一个生存期
ThreadPoolExecutor.setKeepAliveTime(60, TimeUnit.SECONDS);












