JUC八股文
【要图】

线程
并发编程存在的三大问题?
- 原子性
同一时刻只能有一个线程对数据进行操作。在java中使用了atomic包和synchronized关键字来确保原子性
- 可见性
一个线程对主内存进行修改,其他线程可以看到。在java中使用synchronized和volatile两个关键字实现
- 有序性
一个线程观察其他线程的执行顺序,一般无序。在java中用happens-before原则来确保有序性
happens-before:如果 A happens-before B,那么 A 的结果(内存写入)对 B 可见,并且 A 的执行顺序在 B 之前(逻辑上有序)。
线程创建的方式有哪些?
继承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接口
1 | public class MyCallable implements Callable<Integer> { |
优点:适合多个相同线程处理同一份资源;缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法
怎么启动线程?
通过Thread类的start()
1 | // 创建两个线程,使用start()开启线程 |
怎么停止自己的线程运行?
- 异常法停止:线程调用interrupt()方法后,在线程的run方法中判断当前对象的interrupted()状态,如果是中断状态则抛出异常,达到中断线程的效果
- 在沉睡中停止:先将线程sleep,再调用interrupt标记中断状态,interrupt会将阻塞状态的线程中断。抛出中断异常,达到停止线程的效果
1 | // 在沉睡中停止 |
- stop() 暴力停止:线程调用stop()方法会被暴力停止,方法已弃用,强制让线程停止可能会使一些请理性工作得不到完成
- 使用return停止线程:调用interrupt标记为中断状态后,在run方法中判断当前线程状态,如果为中断状态则return,达到停止线程的效果
1 | // 使用return停止线程 |
调用interrupt是如何让线程抛出异常的?
interrupt是什么?每个线程都有一个中断状态(boolean值),默认是false;调用thread.interrupt()会把这个状态设为true,表示希望你停下来;线程本身需要配合检查这个状态来决定是否停止
interrupt() 不是杀死线程,而是发出一个“中断请求”。
Interrupt() 适用的两种情况:1. 线程在阻塞(sleep/join/wait),会抛出InterruptedException并结束阻塞,2. 线程在运行,Interrupt() 只会设置一个标记,需要线程自己检查并退出
怎么停止其他线程运行?
- 通过共享标志位主动终止
1 | public class SafeStopWithFlag implements Runnable { |
- 通过线程中断机制
通过 Thread.interrupt 触发线程中断状态,结合中断检测逻辑实现安全停止
1 | public class InterruptExample implements Runnable { |
- 通过 Future 取消任务
核心思想:用线程池执行任务,获取Future对象来控制任务,调 future.cancel(true) —> 调用任务线程的 interrupt(),依赖中断机制让任务结束
1 | // 一、创建线程池 |

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类的静态方法,可以在任何地方直接通过Thread.sleep()调用,无需依赖实例对象;wait是Object类的实例方法,必须通过实例对象来调用
锁释放的情况:在使用Thread.sleep()期间,其他线程无法获得该线程持有的锁;调用Object.wait()时,线程会释放持有的锁,进入等待状态,直到其他线程调用相同对象的notify() 或 notifyAll() 唤醒它
使用条件:sleep可以在任意位置调用,无需事先获取锁;wait必须在同步块或同步方法内调用(该线程必须持有该对象的锁),否则会抛出 IllegalMonitorStateException 异常
唤醒机制: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唤醒
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()方法类似于wait()方法,signal()方法类似于nofity()方法,signalAll()方法类似于nofityAll()方法;
一个Lock可以对应多个Condition
1 | public class MultiConditionExample { |
和wait、nofity的区别?
wait/nofity 必须用 synchronized 搭配
await/signal 必须用 Lock 搭配,而且一个锁可以有多个 Condition (更灵活,可实现多条件唤醒)
- ReentrantLock
是Lock接口的一个实现类
Reentrant 表示可重入:同一个线程可以多次获取一把锁而不会死锁(通过维护一个持有计数)
主要特点(对比synchronized)

构造方式
1 | // 默认非公平锁(性能好,但可能“插队”) |
示例
1 | ReentrantLock lock = new ReentrantLock(); |
守护线程了解吗?
了解,守护线程可以理解为后台服务线程,比如垃圾回收线程
它的特点就是:当所有用户线程(非守护线程)都结束时,即使守护线程的代码还没执行完,它也会自动结束。通过 setDaemon(true) 来设置
Java的并发工具有哪些?
- CountDownLatch(倒计数器)
调用 countDown() 方法会使计数器减一,当计数器的值减为0时,等待的线程会被唤醒
CountDownLatch是什么?
它是java并发包中的一个同步工具类,用于让一个或多个线程等待其他线程操作完后再执行;核心通过一个计数器(Counter)实现线程间的协调
核心原理
- 初始化计数器:创建CountDownLatch时指定一个初始计数值(如7)
- 等待线程阻塞:调用 await() 的线程会被阻塞,直到计数器变为0
- 任务完成通知:其他线程完成任务后调用 countDown(),使计数器减1
- 唤醒等待线程:当计数器减到0时,所有等待的线程会被唤醒
- CyclicBarrier(同步屏障)
让一组线程相互等待,直到全部达到屏障点;比如new CyclicBarrier(5),表示需要5个线程都调用await()方法等待。当第5个线程到达时,屏障打开,所有被阻塞的线程同时被唤醒继续执行。
- Semaphore(信号量)
用于设置同时访问某个共享资源的线程数
- 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之类的,根本不用每次访问都同步
- CAS成本下降:现在硬件好了,JVM优化好了,CAS成本下降很多
- 线程池模型不友好:同一把锁容易被不同线程先后用到,那偏向的就经常要被撤销,撤销就很贵
无锁:标志位 01
无锁没有对资源进行锁定,所有线程都能访问并修改同一个资源,但是同一时间只有一个线程可以修改成功;CAS的原理就是无锁的实现
偏向锁:标志位 01
概括:适用于只有一个线程访问同步块。 Mark Word里记录线程ID,下次同一个线程来,无需加锁。一段同步代码块一直被一个线程所访问,那么该线程就会自动获取锁,降低获取锁的代价
目的:在只有一个线程执行同步代码块时能提高性能,比如早期的HashTable、Vector;在无多线程竞争下尽量减少不必要的轻量级锁
实现:当一个线程访问同步代码块并获取锁时,会在Mark Word中存储偏向锁的线程ID,在线程进行和退出同步块时不通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着当前线程的偏向锁
偏向撤销:偏向锁在被其他线程竞争时,会先进行偏向撤销,把对象头从偏向模式恢复为普通可锁状态,再升级为轻量级锁/重量级锁
轻量级锁:标志位 00
概括:当有少量竞争时,通过CAS自旋来尝试获取锁,避免直接阻塞线程
当前的并发不是很严重
当指向锁时偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁;其他线程会通过自旋尝试获取锁,不会阻塞,从而提高性能
实现:假设T1线程拿到锁,T2,T3线程没有拿到,此时T2,T3线程不再进行阻塞,而是进行一个自旋操作尝试获取锁
升级为轻量级锁的过程:轻量级锁通过在线程栈中创建 Lock Record,把对象头obj的 Mark Word 复制到栈上,然后用 CAS 把对象头替换为指向 Lock Record 的指针并标记为轻量级锁。
无竞争时加解锁只是在对象头和栈上来回 CAS,遇到竞争就先自旋,严重时再膨胀成重量级锁。
重量级锁:标志位 10
概括:竞争激烈时,轻量级锁自旋超过一定次数,就会升级为重量级锁。未获取到锁的线程会直接进入阻塞队列,等待操作系统调度,开销最大
当前的并发严重
图示:
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值的修改
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 根本不依赖“期间有没有变过”,只依赖“当前时刻的值是否满足条件”
ThreadLocal
什么是ThreadLocal?
ThreadLocal就是线程局部变量
ThreadLocal的作用
实现线程变量的存取,在线程生命周期内任何位置都可以直接获取到这个变量,不同线程拿到的是各自线程维护的那份值,彼此隔离。
【例子】:当一个用户的请求发送到服务器上的时候,创建处理这个用户的线程,将用户的信息提前加载到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:拒绝策略
线程池工作队列满了有哪些拒绝策略?
- CallerRunsPolicy:使用线程池的调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的任务队列已有空缺。
- AbortPolicy:直接抛出一个任务被线程池拒绝的异常
- DiscardPolicy:不做任何处理,静默拒绝提交的任务
- DiscardOldestPolicy:抛弃最老的任务,然后执行该任务
- 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略
线程池提交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);








