JUC
JUC
线程有几种状态
Java中线程的状态分为6种:
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
运行(RUNNABLE):Java线程中将就绪(READY)和运行中(RUNNING)两种状态笼统的称为“运行”。
就绪(READY):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中并分配cpu使用权 。
运行中(RUNNING):就绪(READY)的线程获得了cpu 时间片,开始执行程序代码。
阻塞(BLOCKED):表示线程阻塞于锁(关于锁,在后面章节会介绍)。
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
终止(TERMINATED):表示该线程已经执行完毕。
什么是守护线程?
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。用户线程一般用于执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务,守护线程最典型的应用就是GC(垃圾回收器)。
这两种线程其实是没有什么区别的,唯一的区别就是Java虚拟机在所有<用户线程>都结束后就会退出,而不会等<守护线程>执行完。
线程数设定成多少更合适?
- 如果是CPU密集型应用,则线程池大小设置为N+1
- 如果是IO密集型应用,则线程池大小设置为2N+1
ThreadLocal的实现原理
- ThreadLocal 原理:
- 每个线程都拥有自己的 ThreadLocal 实例,该实例内部维护了一个 ThreadLocalMap 对象。
- ThreadLocalMap 是一个散列表(哈希表),用于存储线程局部变量的值,其中的每个元素是一个键值对,键为 ThreadLocal 实例,值为对应线程的局部变量。
- 当通过 ThreadLocal 获取或设置值时,首先会根据当前线程获取对应的 ThreadLocalMap 对象,然后使用 ThreadLocal 实例作为键来查找对应的值。
- 每个线程独立维护自己的数据,不同线程之间的数据互不干扰,从而实现了数据在线程之间的隔离。
- ThreadLocal 实现方式:
- ThreadLocal 使用了弱引用(WeakReference)来防止内存泄漏。ThreadLocal 实例本身是一个强引用,而与每个线程关联的局部变量则是弱引用。当线程被回收时,对应的局部变量也会被自动回收。
- 当调用 ThreadLocal 的 set() 方法时,实际上是将传入的值与当前线程关联起来,并存储到当前线程的 ThreadLocalMap 中。
- 当调用 ThreadLocal 的 get() 方法时,实际上是从当前线程的 ThreadLocalMap 中根据 ThreadLocal 实例查找对应的值并返回。如果没有找到,则返回 null 或指定的默认值。
- 在多线程环境下,由于每个线程都有自己独立的 ThreadLocalMap,因此每个线程可以独立地读取和修改自己的局部变量,而不会影响其他线程的数据。
什么是Java内存模型(JMM)?
JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

特点:
所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
什么是Monitor
sychronized 中的重量级锁就是使用monitor机制实现的。
monitor是一个同步工具,或者说是一种同步机制
monitor就是监视器,监视啥呢,就是监视同步代码块的运行情况,比如说能不能运行这一段代码块。
更进一步简单理解,monitor就是锁。
- 特点:
1、互斥,一个monitor在同一时刻只能被一个线程持有。
2、提供singnal机制,允许持有锁的这个线程暂时放弃锁,然后等待某个条件成立之后,比如典型的wait notify,wait就是放弃锁,notify就是等待某个条件成立之后唤醒之后继续持有锁。
- 实现:
monitor依赖底层操作系统的Mutex lock实现的,它是要由操作系统去实现线程之间的切换的,需要从用户态到操作系统内核态的转化,所以说成本相对比较高,因此monitor是一种典型的重量级锁的实现方式。
java对monitor的支持:java会为每个Object对象分配一个monitor,也就是说,所有的java对象都是天生的monitor,因此monitor在java里也被称之为内置锁,或者叫monitor锁
monitor在jvm中的基本实现:
数据方面:monitor是线程私有的数据结构,在运行期间,每个线程都有一个可用的monitor列表,同时还有一个全局的可用列表。当然在运行期间这个monitor自然是要表现为一个java对象,这个就是ObjectMonitor对象实现的,当然这个底层的实现是c++的,咱只需要了解几个核心属性:
1、_owner(持有锁的线程) 2、_waitset(存放所有处于wait状态的线程,集合) 3、EntryList(存放所有等待获取锁的这些线程,也就是处于block状态的线程队列) 4、recursions(记录的是锁的重入次数) 5、_count(记录这个线程获取锁的次数)
总之,每一个锁的对象,它就一定会有一个对应的这么一个monitor,每一个被锁住的对象都会和一个monitor关联起来,在对象头里有一个markword,会存放指向monitor的指针。


Syncronized锁升级的过程讲一下
具体的锁升级的过程是:无锁->偏向锁->轻量级锁->重量级锁。
- 无锁:这是没有开启偏向锁的时候的状态,在JDK1.6之后偏向锁的默认开启的,但是有一个偏向延迟,需要在JVM启动之后的多少秒之后才能开启,这个可以通过JVM参数进行设置,同时是否开启偏向锁也可以通过JVM参数设置。
- 偏向锁:这个是在偏向锁开启之后的锁的状态,如果还没有一个线程拿到这个锁的话,这个状态叫做匿名偏向,当一个线程拿到偏向锁的时候,下次想要竞争锁只需要拿线程ID跟MarkWord当中存储的线程ID进行比较,如果线程ID相同则直接获取锁(相当于锁偏向于这个线程),不需要进行CAS操作和将线程挂起的操作。
- 轻量级锁:在这个状态下线程主要是通过CAS操作实现的。将对象的MarkWord存储到线程的虚拟机栈上,然后通过CAS将对象的MarkWord的内容设置为指向Displaced Mark Word的指针,如果设置成功则获取锁。在线程出临界区的时候,也需要使用CAS,如果使用CAS替换成功则同步成功,如果失败表示有其他线程在获取锁,那么就需要在释放锁之后将被挂起的线程唤醒。
- 重量级锁:当有两个以上的线程获取锁的时候轻量级锁就会升级为重量级锁,因为CAS如果没有成功的话始终都在自旋,进行while循环操作,这是非常消耗CPU的,但是在升级为重量级锁之后,线程会被操作系统调度然后挂起,这可以节约CPU资源。
了解完 4 种锁状态之后,我们就可以整体的来看一下锁升级的过程了。 线程A进入 synchronized 开始抢锁,JVM 会判断当前是否是偏向锁的状态,如果是就会根据 Mark Word 中存储的线程 ID 来判断,当前线程A是否就是持有偏向锁的线程。如果是,则忽略 check,线程A直接执行临界区内的代码。
但如果 Mark Word 里的线程不是线程 A,就会通过自旋尝试获取锁,如果获取到了,就将 Mark Word 中的线程 ID 改为自己的;如果竞争失败,就会立马撤销偏向锁,膨胀为轻量级锁。
后续的竞争线程都会通过自旋来尝试获取锁,如果自旋成功那么锁的状态仍然是轻量级锁。然而如果竞争失败,锁会膨胀为重量级锁,后续等待的竞争的线程都会被阻塞。
JVM对Synchornized的优化?
synchronized 核心优化方案主要包含以下 4 个:
- 锁膨胀:synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级。JDK 1.6 之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁时都会从用户态转换成内核态,而转换的效率是比较低的。但有了锁膨胀机制之后,synchronized 的状态就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核态的转换了,这样就大幅的提升了 synchronized 的性能。
- 锁消除:指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。
- 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
- 自适应自旋锁:指通过自身循环,尝试获取锁的一种方式,优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。
什么是AQS
AQS(AbstractQueuedSynchronizer)是 Java 并发编程中的核心框架,全称为抽象队列同步器。它是 java.util.concurrent.locks
包中的基础组件,用于构建各种锁和同步工具(如 ReentrantLock
、Semaphore
、CountDownLatch
等)。AQS 的设计目标是简化并发工具的实现,提供统一的线程排队、资源管理和阻塞/唤醒机制。
AQS 的核心原理
状态管理(state)
- AQS 使用一个
volatile int state
变量表示共享资源的状态。 state
的含义由子类定义,例如:- ReentrantLock:
state=0
表示锁未被占用,state>0
表示锁被重入的次数。 - Semaphore:
state
表示剩余的许可数量。 - CountDownLatch:
state
表示倒计时的剩余计数。
- ReentrantLock:
- AQS 使用一个
线程等待队列(CLH 队列)
- AQS 内部维护一个双向链表队列(FIFO),用于管理等待资源的线程。
- 每个节点(
Node
)封装一个线程及其状态(如是否被取消、是否为共享模式等)。 - 当线程无法获取资源时,会被封装成
Node
加入队列尾部;当资源释放时,会唤醒队列中的线程。
同步模式
- 独占模式(Exclusive):同一时刻只有一个线程能获取资源(如
ReentrantLock
)。 - 共享模式(Shared):允许多个线程同时获取资源(如
Semaphore
、CountDownLatch
)。
- 独占模式(Exclusive):同一时刻只有一个线程能获取资源(如
模板方法模式
- AQS 将通用逻辑(如线程排队、阻塞唤醒)封装,子类只需实现特定方法(如
tryAcquire
、tryRelease
)。 - 子类需要实现的方法:
tryAcquire(int arg)
:尝试以独占模式获取资源。tryRelease(int arg)
:尝试以独占模式释放资源。tryAcquireShared(int arg)
:尝试以共享模式获取资源。tryReleaseShared(int arg)
:尝试以共享模式释放资源。isHeldExclusively()
:判断当前线程是否持有独占资源。
- AQS 将通用逻辑(如线程排队、阻塞唤醒)封装,子类只需实现特定方法(如
AQS 的核心功能
资源状态管理
- 通过
state
变量管理资源的占用状态,确保线程安全(使用CAS
原子操作更新state
)。
- 通过
线程阻塞与唤醒
- 当线程无法获取资源时,会被阻塞并加入等待队列。
- 当资源释放时,会唤醒等待队列中的线程,使其重新尝试获取资源。
公平性与非公平性
- 公平模式:线程按等待顺序获取资源(如
ReentrantLock.FairSync
)。 - 非公平模式:允许插队(如
ReentrantLock.NonfairSync
)。
- 公平模式:线程按等待顺序获取资源(如
条件等待(Condition)
- AQS 支持
Condition
对象,用于实现线程的等待/通知机制(类似Object.wait()
/notify()
)。
- AQS 支持
AQS 的典型应用场景
锁的实现
- ReentrantLock:基于 AQS 实现可重入锁,支持公平锁和非公平锁。
- ReentrantReadWriteLock:读写锁,读锁共享,写锁独占。
信号量(Semaphore)
- 控制同时访问资源的线程数量,
state
表示可用许可数。
- 控制同时访问资源的线程数量,
倒计时门栓(CountDownLatch)
state
表示倒计时次数,线程等待直到state
减至 0。
屏障(CyclicBarrier)
- 多线程等待彼此到达某个点后再继续执行。
FutureTask
- 管理异步任务的状态,等待任务完成。
AQS 的优势
简化并发工具开发
- 将线程排队、阻塞唤醒等复杂逻辑封装,开发者只需关注资源管理逻辑。
高性能
- 使用 CAS 和 volatile 确保线程安全,减少上下文切换开销。
灵活性
- 支持独占模式和共享模式,适用于多种并发场景。
什么是CAS
CAS(Compare-And-Swap) 是一种用于实现多线程环境下原子操作的机制,属于乐观锁(Optimistic Locking)的实现方式。它通过硬件指令直接操作内存,确保在并发场景下的数据一致性。
核心原理
CAS 操作包含三个参数:
- 内存地址 V:需要更新的变量的内存地址。
- 旧值 A:预期的原始值。
- 新值 B:要更新的新值。
操作流程:
- 将内存地址 V 的当前值与旧值 A 进行比较。
- 如果相等,说明未被其他线程修改,将内存地址 V 的值更新为新值 B。
- 如果不相等,说明已被其他线程修改,操作失败,返回当前值。
原子性:CAS 是一条 CPU 原子指令(如 x86 架构的 lock cmpxchg
),由硬件保证操作不可分割。
CountDownLatch、CyclicBarrier、Semaphore区别?
CountDownLatch、CyclicBarrier、Semaphore都是Java并发库中的同步辅助类,它们都可以用来协调多个线程之间的执行。
CountDownLatch是一个计数器,它允许一个或多个线程等待其他线程完成操作。它通常用来实现一个线程等待其他多个线程完成操作之后再继续执行的操作。
CyclicBarrier是一个同步屏障,它允许多个线程相互等待,直到到达某个公共屏障点,才能继续执行。它通常用来实现多个线程在同一个屏障处等待,然后再一起继续执行的操作。
Semaphore是一个计数信号量,它允许多个线程同时访问共享资源,并通过计数器来控制访问数量。它通常用来实现一个线程需要等待获取一个许可证才能访问共享资源,或者需要释放一个许可证才能完成操作的操作。
CountDownLatch适用于一个线程等待多个线程完成操作的情况
CyclicBarrier适用于多个线程在同一个屏障处等待
Semaphore适用于一个线程需要等待获取许可证才能访问共享
有三个线程T1,T2,T3如何保证顺序执行?
想要让三个线程依次执行,并且严格按照T1,T2,T3的顺序的话,主要就是想办法让三个线程之间可以通信、或者可以排队。
想让多个线程之间可以通信,可以通过join方法实现,还可以通过CountDownLatch、CyclicBarrier和Semaphore来实现通信。
想要让线程之间排队的话,可以通过线程池或者CompletableFuture的方式来实现。
三个线程分别顺序打印0-100
这个问题主要考察多线程的线程安全和通信机制,常见的处理方式有notify/synchorized和condition/ reentrantlock。但是往往有同学只注意线程安全,而忽略了通信机制,常见的错误写法如下:
public class Test {
private static int i = 1;
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new Print(i)).start();
}
}
private static class Print implements Runnable {
private final int index;
public Print(int index) {
this.index = index;
}
@Override
public void run() {
while(true) {
synchronized (Print.class) {
if (i >= 101) {
return;
}
System.out.println("Thread-" + index + " " + i++);
}
}
}
}
}
这样写固然能通过锁来保证循环打印了1-100,但是却不能保证线程是按照顺序打印的,这个时候就需要用到线程的通信机制。
Synchronized
我们可以结合Sync和Object#notifyAll来完成,如下所示
public class SortTest {
private static final Object LOCK = new Object();
private static volatile int count = 0;
private static final int MAX = 100;
public static void main(String[] args) {
Thread thread = new Thread(new Seq(0));
Thread thread1 = new Thread(new Seq(1));
Thread thread2 = new Thread(new Seq(2));
thread.start();
thread1.start();
thread2.start();
}
static class Seq implements Runnable {
private final int index;
public Seq(int index) {
this.index = index;
}
@Override
public void run() {
while (count < MAX) {
synchronized (LOCK) {
try {
while (count % 3 != index) {
LOCK.wait();
}
if(count <=MAX){
System.out.println("Thread-" + index + ": " + count);
}
count++;
LOCK.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
ReentrantLock
我们可以使用ReentrantLock和Condition尝试解决这种问题。大概的解决思路就是先通过lock对资源加锁,然后通过condition指定的唤醒下一个线程。相信大家都已经发现,这种方式比Synchronized的优点就是sync只能唤醒一个线程或者全部唤醒来让大家竞争,但是通过condition我们可以唤醒指定线程,避免资源浪费
public class Test {
private static final int WORKER_COUNT = 3;
private static int countIndex = 0;
private static final ReentrantLock LOCK = new ReentrantLock();
public static void main(String[] args){
final List<Condition> conditions = new ArrayList<>();
for(int i=0; i< WORKER_COUNT; i++){
// 为每一个线程分配一个condition
Condition condition = LOCK.newCondition();
conditions.add(condition);
Worker worker = new Worker(i, conditions);
worker.start();
}
}
static class Worker extends Thread{
int index;
List<Condition> conditions;
public Worker(int index, List<Condition> conditions){
super("Thread-"+index);
this.index = index;
this.conditions = conditions;
}
private void signalNext(){
int nextIndex = (index + 1) % conditions.size();
conditions.get(nextIndex).signal();
}
@Override
public void run(){
while(true) {
//锁住 保证操作间同时只有一个线程
LOCK.lock();
try {
// 如果当前线程不满足打印条件,则等待
if (countIndex % 3 != index) {
conditions.get(index).await();
}
if (countIndex > 100) {
// 唤醒下一个线程,保证程序正常退出
signalNext();
// 退出循环 线程运行结束
return;
}
System.out.println((this.getName() + " " + countIndex));
// 计数器+1
countIndex ++;
// 通知下一个干活
signalNext();
}catch (Exception e){
e.printStackTrace();
}finally {
LOCK.unlock();
}
}
}
}
}
注意,对于Worker里面的逻辑,不能为了图省事,用下面的写法:
public void run(){
while(true) {
//锁住 保证操作间同时只有一个线程
LOCK.lock();
try {
if (countIndex > 100) {
// 唤醒下一个线程,保证程序正常退出
signalNext();
// 退出循环 线程运行结束
return;
}
System.out.println((this.getName() + " " + countIndex));
// 计数器+1
countIndex ++;
// 通知下一个干活
signalNext();
conditions.get(index).await();
}catch (Exception e){
e.printStackTrace();
}finally {
LOCK.unlock();
}
}
}
这种写法是错误的,原因是如果刚开始不让所有线程都等待,就有可能会导致线程竞争,举个例子:
step1:thread0执行逻辑
step2:thread2被锁阻塞
step3:thread0执行完成,唤醒thread1
step4:thread2和thread1竞争锁,thread2竞争成功,就会导致thread0执行完成后,直接由thread2执行
Thread#yield
除了线程之间的通信之外,我们还可以使用一种取巧的方式,就是通过指定线程打印某些值,如Thread0打印0,3,9等值。
核心思想是通过yield自旋的方式,如果当前的值不需要被当前线程打印,那么就让出该线程。如下所示:
private static volatile int count = 0;
private static final int MAX = 100;
static class OtherWorker implements Runnable {
private final int index;
public OtherWorker(int index) {
this.index = index;
}
@Override
public void run() {
while (count < MAX) {
while (count % 3 != index) {
Thread.yield();
}
if (count > MAX) {
return;
}
System.out.println("Thread-" + index + " " + count);
count++;
}
}
}
这种方式需要线程不停的竞争和自旋,性能显然比不过前两种方法。同时,因为yield并不能保证立刻让出CPU,所以这种方法是有风险的
线程池的拒绝策略有哪些?
在Java中,线程池的拒绝策略决定了在任务队列已满的情况下,如何处理新提交的任务。当线程池达到最大容量,并且任务队列也已满时,拒绝策略就会起作用。Java提供了四种内置的拒绝策略,它们分别是:
- AbortPolicy - 这是默认的拒绝策略,当线程池无法接受新任务时,会抛出RejectedExecutionException异常。这意味着新任务会被立即拒绝,不会加入到任务队列中,也不会执行。通常情况下都是使用这种拒绝策略。
- DiscardPolicy - 这个策略在任务队列已满时,会丢弃新的任务而且不会抛出异常。新任务提交后会被默默地丢弃,不会有任何提示或执行。这个策略一般用于日志记录、统计等不是非常关键的任务。
- DiscardOldestPolicy - 这个策略也会丢弃任务,但它会先尝试将任务队列中最早的任务删除,然后再尝试提交新任务。如果任务队列已满,且线程池中的线程都在工作,可能会导致一些任务被丢弃。这个策略对于一些实时性要求较高的场景比较合适。
- CallerRunsPolicy - 这个策略将任务回退给调用线程,而不会抛出异常。调用线程会尝试执行任务。这个策略可以降低任务提交速度,适用于任务提交者能够承受任务执行的压力,但希望有一种缓冲机制的情况。