JUC个人理解
进程、线程和协程
进程是操作系统进行资源分配的一个基本单位,像启动一个springboot项目,操作系统会为它创建一个进程,这个进程会单独占用一部分系统资源。而在进程中可以创建多个线程,也就是刚刚的springboot项目,假设我们cpu是8核的,一个cpu核心可以执行一个线程。那我就可以创建八个线程,每个线程会通过指令去通过CPU执行指令。协程是jdk19后面才有的,也叫虚拟线程。它更加的轻量化,它是用户态的一个资源分配的基本单位,一个线程内部可以有多个协程,在JVM中进行创建和销毁。还有个共享变量的问题,进程之间不共享全局变量,线程之间能共享进程的堆和方法区资源,每个线程有自己的程序计数器、虚拟机栈和本地方法栈,协程能共享线程和进程的共享变量。

进程是操作系统进行资源分配的基本单位。比如我们启动一个 Java 程序(双击 jar 包或用
java命令运行),操作系统会为它创建一个进程,这个进程会独占一部分系统资源 —— 比如独立的内存空间(堆、方法区等)、文件描述符、CPU 时间片配额等。可以理解为 “一个正在运行的程序实例”。线程是进程内的执行单元,也是操作系统调度的基本单位。一个进程至少有一个线程(主线程),也可以创建多个线程,这些线程共享该进程的所有资源(比如内存、文件句柄),但每个线程有自己独立的执行路径(比如虚拟机栈、程序计数器)。可以理解为 “进程内的一个任务执行者”。
协程是用户态的轻量级 “执行单元”,由程序(而非操作系统)自身管理调度。一个线程内部可以有多个协程,java中是由JVM进行创建和销毁的。
再从资源占用和开销来看,差异很明显:
进程的资源是 “独立且厚重的”:每个进程有自己独立的内存空间,互不干扰,所以创建、销毁进程时,操作系统需要分配 / 回收大量资源(比如内存、IO 句柄),开销很大,启动速度慢。
线程的资源是 “共享且轻量的”:线程共享所属进程的内存和资源,自己只需要少量私有资源(比如栈空间、程序计数器),所以创建、销毁线程的开销小得多,启动速度快(通常比进程快 10-100 倍)。
并发、并行和串行
并发就是在操作系统中,安装了多个程序,并发就是同一个时间段在宏观上多个程序同时运行。但是单核CPU,一个时刻只能执行一个程序,所以在微观上这些程序是切换交替运行的,那么并行就是比如4个程序运行在一个4核心的cpu操作系统中,他们是可以同时执行。串行就是当一个程序执行完了,再去执行下一个程序。
单核CPU如何处理多线程
通过时间片,之前说过一个核心只能处理一个线程。有时候我们其实观察到,比如安装虚拟机的时候只分配了一个核心,但是我们可能会部署mysql、redis、jar包,这明显不是单线程。在宏观上我们认为他们是同时执行的。但是其实在微观上并不是,依然是切换执行。它使用了时间片轮转算法,比如每一个时间片是1ms,实际是由操作系统内核设定,第一个时间片用来执行mysql程序了,1ms到了,切换到redis,依然给他1ms,1ms又到了,切换到jar。当然实际上并不是按照顺序来决定切换哪个线程的(调度算法)。这就涉及到几个问题,首先是频繁切换线程,肯定会消耗CPU资源。第二在每个线程切换的时候会涉及到线程上下文切换,CPU寄存器和程序计数器的内容会先保存,再切换,再恢复,极其消耗资源。所以单核CPU处理多线程瓶颈很大。
创建线程的方式
创建线程的方式是有三种,第一种继承Thread类,并重写run方法。但是继承只能继承一个类,所以一个类又要继承其他类又要创建线程,就要通过接口的形式,第二种实现runnable接口,重写run方法。但是这种方法也有个弊端,run方法是void,没法获取返回值。第三种是实现callable接口,这个接口可以抛异常和返回值,我们要先重写它的call方法。并将实例对象传到futureTask,因为futureTask实现了runnable接口,再将futureTask传入Thread构造器创建线程。可以通过futureTask获取创建的线程中call方法的返回值。
start和run区别
start在被线程执行的时候会调用本地方法,这里是由C++或者C写的,它会帮我们调度操作系统创建新的线程,操作系统会给新线程分配资源(程序计数器,虚拟机栈),把该线程状态改成就绪,等待cpu分配时间片,再通过JVM回调到Thread中的run方法,再执行run方法中的逻辑。所以我们线程也可以直接调用run方法。但是还是有区别的,首先是start方法是启动线程,而run方法只是执行线程中重写run方法的逻辑,它会被当前所在线程调用。第二start方法是异步的,当程序执行start方法,不会等待执行完毕,程序计数器会直接执行下一行字节码,而run方法是同步的需要等待线程执行完毕。start方法会创建一个新的线程,run方法只是执行了逻辑并不会创建新线程。
sleep
sleep会让对应的线程暂停指定时间(期间不参与 CPU 调度,主动释放资源),这个特性虽然不常用,但在 Web 服务器中常配合while(true)死循环,目的是让主线程保持存活(防止 JVM 退出),同时不占用 CPU 资源,让 CPU 可以专注调度 Tomcat 的工作线程处理请求。在 Spring Boot 内嵌 Tomcat 的源码中,我观察到启动后主线程会进入一个每 10 秒sleep一次的死循环,这正是为了实现上述效果 —— 主线程 “休息” 时,CPU 会优先处理请求相关的线程。
sleep和wait的区别
首先两个所属的类不同,sleep是属于thread类的方法,wait是object类的方法,每个类都有wait方法。然后是参数也不一样,sleep必须填写睡眠时间参数,wait的话可以填写时间参数也可以不填写,不填写要等待notify唤醒。所以在线程进入的状态也不同,两者有参数的都会进入TIMED_WAITING,wait如果无参数会进入WAITING状态。wait要依赖于Synchronized同步代码块,同时wait方法会释放锁资源,把资源加入等待队列。而sleep因为不依赖Synchronized同步代码块所以不释放锁资源。又因为wait独特的释放锁机制,所以可以通过wait和队列做一个小的生产者和消费者模型。MQ和线程池其实都有使用到该模型。
tomcat服务器启动
这里细说一下。Spring内嵌tomcat服务器怎么通过sleep做到服务一直挂起的。当通过SpringApplication.run启动主线程的时候,去启动tomcat的线程服务,tomcat线程服务内部通过while(true)加sleep,一直挂起,假设分配十秒的睡眠时间,这样主线程会保持启动。同时每十秒springboot会判断内嵌tomcat服务器的启动状态,比如遇到了服务超时,启动失败,都可以抛出异常让主线程停止,同时tomcat服务器停止,抛出停止原因。Spring2.x同样也可以通过守护线程+await方法,做到保持主线程不退出,内嵌的tomcat服务器持续运行。Spring创建一个守护线程,守护线程调用tomcat服务器线程的await方法。当主线程停止,守护线程也会停止,tomcat线程也就会停止等待进入终止了。
join
这里有两种用法,第一种假设创建了一个线程t1。我在主线程先启动t1线程,t1.start——t1.join.后面才是主线程的代码。我们知道如果没有join,正常会由线程调度器给t1和主线程分配时间片,决定执行谁一会再执行另一个线程一会。但是在主线程加了t1.join,主线程就会等待t1执行完毕,再执行主线程的代码。
第二join方法中可以添加一个long参数,当添加了long参数,依旧拿刚刚的情况举例子,表示主线程愿意等待t1的时间,我个人这么理解的,假如设置的1s。如果t1执行的时间一共会是5s,那么一秒过后,主线程就不等了,线程执行顺序就有线程调度器来分配时间片决定了。如果t1执行的时间小于1s,那么t1执行完以后主线程执行。就和不添加long参数一样了。
线程的几种状态
我看过Thread源码中的一个state的枚举类,里面是有六种,我按照线程的执行的经过顺序说一下。首先是NEW状态,这个是线程被创建了但是没有执行start方法的时候,此时是NEW状态,当线程执行start方法会进入RUNNABLE状态,也就是运行时。如果线程1和线程2竞争锁资源了,没拿到锁资源的线程会进入阻塞BLOCKED状态。如果线程在执行run方法的时候,其中有join、wait(obejct)等方法,该线程是WAITING等待状态。如果线程在执行run方法的时候,其中有sleep、wait或者join都加了时间参数,join的话线程执行时间要大于它设置的等待参数。这个时候就会进入TIME_WAITING。线程如果执行完毕就进入TERMINATED终止状态
线程中断
之前是使用stop方法,中断线程,会导致的资源未释放、数据不一致。核心是通过设置线程的中断状态(一个 boolean 标志)来实现:调用线程的 interrupt () 方法会设置这个标志;线程可通过 isInterrupted () 检查标志,或通过静态方法 Thread.interrupted () 检查并清除标志。如果线程在阻塞状态停止会抛出异常,清除中断标志。线程运行的时候每次都会检查中断标志。
线程的痛点
- 线程的频繁创建和销毁

- 线程不断的创建出现的两个问题
内存溢出(OOM):每个线程占用独立的栈内存(即使是轻量级线程,也需占用一定内存),当线程数量达到数万甚至数十万时,栈内存总和会超出 JVM 堆外内存限制,直接导致进程崩溃。
CPU上下文频繁切换: 我们知道一个核心在微观上只能执行一个线程,在宏观上之所以看起来单核也能执行多个线程,只是分配了时间片进行线程的切换,如果线程数量远远大于CPU的核心数,那CPU会频繁在不同线程的上下文(程序计数器、寄存器、栈指针)之间切换 —— 每次切换需保存当前线程状态、加载目标线程状态。这个过程会消耗大量的CPU时间,导致切换线程的时间都大于任务的实际执行时间了,降低了吞吐量。 - 手动创建的线程是分散的,无法集中管理,要单独配置线程的优先级、命名规则、异常处理啊或者 若需停止所有线程(如应用停机),只能手动遍历所有线程调用
interrupt(),但无法保证线程能安全释放资源(如正在操作数据库的线程被强制中断,可能导致事务回滚失败),容易引发数据一致性问题。
线程池解决线程的痛点
- 解决线程频繁的创建和销毁: 创建线程池的时候指定核心线程数和最大线程数,将线程统一放入池子中管理,线程池提交任务,分配线程去处理任务,任务处理完,线程不会立刻结束回收,而是通过自旋的方式,它会从阻塞队列调用take或者poll方法,等待从队列中获取新的任务执行。当线程池执行关闭的时候,分两种情况,shutdown和shutdownNew,最后线程 统一销毁。见shutdown和shutdownNow
将线程放入线程池统一管理,通过线程工厂线程工厂其实内部就是一个接口并且只有一个方法,就是创建线程,我们一般使用默认的defaultFactoryThread就好了,当然可以自定义线程工厂全局设置异常处理器,提供线程池优雅关闭,自动统一销毁线程。
线程工厂
这个参数其实是线程工厂的实现类,接口里面就一个newThread方法,如果要自定义我们可以创建一个实现类重写newThread方法,java内部有很多封装好的线程工厂,它的作用主要是解决了线程的一些统一管理,它可以将避免线程命名混乱,优先级无序,守护线程属性不一致。同样可以自定义一些线程的异常统一处理,并且实现了线程创建和业务代码的解耦。
线程池的执行流程
正常创建线程会频繁的创建和销毁,只要任务执行完毕就会销毁,极其浪费CPU资源和性能。这个时候就可以使用线程池实现线程复用,将线程统一放入一个池子中。假设现在我给线程池设置了最大线程数10,核心线程数6个。现在有任务源源不断的来了,线程池会分配核心线程去执行任务,当这六个核心线程都在执行任务,还有新的任务来了,怎么办,线程池会创建一个任务队列,它是由阻塞队列实现,将新来的任务放入任务队列中。任务队列有两种,一种有队列,一种无界队列。先拿有界队列来说,当核心线程执行完手里的任务,会去任务队列中执行新的任务。这是理想情况,假如任务来的很快,来的速度比处理速度更快,或者有几个任务执行时间很长,导致任务队列满了,这个时候所有的核心线程也在执行任务,又来了个任务3,任务3发现队列满了,之前我们不是设置过核心线程数和最大线程数么,又发现核心线程数没到最大线程数,这个时候线程池会创建非核心线程来执行这个新来的任务3。如果又有任务来,核心线程还在执行,队列还是满的,线程池就会创建新的非核心线程来执行新任务。直到任务N来了,发现核心线程数+非核心线程数等于最大线程数了,并且队列还是满的,就会执行线程池设置好的拒绝策略,可能会抛出异常终止任务,可能会由提交任务的线程处理,可能会直接丢弃,当然你也可以自定义拒绝策略,比如把任务放数据库或者通知MQ,后续重试。那核心线程和非核心线程都能处理任务,为什么还分开?在默认情况下,非核心线程如果空闲了,我们在创建线程池的时候会设置参数,空闲时间,当非核心线程超过设置的空闲时间
keepAliveTime,就会自动回收。我们也可以给核心线程设置空闲时间,这样核心线程就像非核心线程一样超过空闲时间了就回收。当然有新任务来了线程池会自动创建核心线程的。
线程池核心参数七个
核心线程数
int corePoolSize:线程池创建时候,会创建的核心线程数量,即使空闲也不会被回收,但是可以设置threadPool.setAllowCoreThreadTimeOut(true)这样当核心线程超过空闲时间KeepAliveTime也会被回收。- 最大线程数
int maximumPoolSize:线程池允许创建的最大线程数量,也就是核心线程+非核心线程。 - 空闲时间
long KeepAliveTime:当线程池默认允许非核心线程或者设置核心线程允许空闲回收的空闲时间,当线程空闲时间超过设置的空闲时间,则线程回收。 - 时间单位
TimeUnit unit:空闲时间的时间单位 - 任务队列
kingQueue<Runnable> workQueue: 用于缓存等待执行的任务的阻塞队列,当核心线程全忙时,新任务会先进入队列等待。缓冲任务峰值,实现 “削峰填谷”,避免任务直接被拒绝。 - 线程工厂
ThreadFactory threadFactory:用于创建线程的工厂,可自定义线程的名称、优先级、是否为守护线程等。标准化线程配置,方便问题排查(如通过线程名定位日志)。 - 拒绝策略
RejectedExecutionHandler handler:当 任务队列满 + 线程数达maximumPoolSize时,对新提交任务的处理策略。线程池如何知道一个线程的任务执行完成
当线程池内的工作线程执行任务的时候,工作线程会去同步的执行任务的run方法,当任务的run方法为执行完毕以后,线程池就认为该工作线程已经执行完任务了,线程就会回到先吃吃的等待队列,等新任务来,如果在线程池外部要感知任务是否执行完毕,可以使用future对象,通过他的get方法,当使用线程池的submit方法的时候,会返回一个future对象,通过get方法获取返回值,get方法会阻塞线程,当线程执行完任务以后才会有返回值,来判断任务是否执行完毕,当然也可以通过一个计数器,记录任务的数量,执行完毕-1.来判断任务是否执行完毕。任务队列
ArrayBlockingQueue(有界队列):需指定容量(如new ArrayBlockingQueue<>(100)),队列满时会触发非核心线程创建,适合资源受限场景。LinkedBlockingQueue(无界队列):默认容量为Integer.MAX_VALUE(约 20 亿),任务可无限入队,可能导致 OOM(内存溢出),此时maximumPoolSize失效。SynchronousQueue(同步队列):不存储任务,每个任务必须立即被线程处理(若没有空闲线程,则直接创建非核心线程,直到达到maximumPoolSize),适合任务处理快、提交频繁的场景。PriorityBlockingQueue(优先级队列):按任务优先级排序,适合需要优先处理重要任务的场景。拒绝策略
JDK 内置 4 种拒绝策略:- AbortPolicy:直接抛出异常中断任务提交流程
- CallerRunsPolicy:由提交任务的线程自己执行任务(在哪个线程提交的哪个线程执行)
- DiscardPolicy:直接丢弃新提交的任务,不抛异常也不执行
- DiscardOldestPolicy:丢弃任务队列头部(最先进队列)的任务,然后将心任务加入队列
- 自定义:实现RejectedExecutionHandler接口,重写rejectedExecution方法,可以将任务放数据库/消息队列,后续重试。具体:可以重写rejectedExecution,把runnable任务,设置为可重试任务,线程池拒绝任务时发送到 MQ,并做一个发送失败的兜底保存到数据库或者本地文件,当线程池关闭的时候,停止接受新任务,把任务队列任务执行完毕,再处理被拒绝的任务,执行重试逻辑,发起重试的通知或者数据库查讯,确保之前被拒绝的任务全部处理完毕。
shutdown和shutdownNow
线程池中shutdown和shutdownNow的区别在于,前者会等待所有任务包括排队的任务执行完毕才关闭,而后者只等待正在执行的线程完成便关闭。源码中每次执行任务的时候,使用if判断了线程池的状态,shutdown把线程池状态改成了shutdown,而shutdownNow是改成了stop,如果判断线程池状态是shutdown,会再判断任务队列是否还有任务。如果没任务了就关闭线程池。当执行任务的线程,这个的时候线程池关闭了,那它执行下一个任务的时候,就会进入if判断,从而决定是否关闭线程,比如当前线程池内有5个线程在执行任务,还有五个任务在等待,此时如果执行了shutdown,线程池会等排队的五个线程的任务也执行完毕再关闭。如果是shutdownNow则会等正在执行任务的五个线程执行完毕,就关闭,排队的五个线程任务就不被执行了。源码如下1
2
3
4if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}execute和submit
execute方法用于执行无返回值任务,不返回结果且异常需在子线程处理;submit方法支持Runnable和Callable,返回Future对象,可通过get获取结果或异常,异常在调用get时抛出,便于主线程统一处理,更适用于需要返回值或异常管理的场景。原子性
四种保证原子性的性能对比 2亿数值递减
synchronized 耗时: 3855 毫秒
ReentrantLock 耗时: 1169 毫秒
AtomicInteger 耗时: 2268 毫秒
LongAdder 耗时: 88 毫秒可见性
比如主线程有个变量a=true,线程1(子线程)使用了变量a进行循环,启动线程1以后,在主线程修改a=false,子线程并看不见,继续循环。
原因:子线程首次从主内存读取a=true后,会将其缓存到自己的工作内存中。主线程修改a=false后,仅更新主内存,但子线程未重新从主内存加载,因此始终使用缓存的true,导致循环无法终止。
synchronized:在子线程循环中随便锁一个对象,当锁释放的时候,会去主内存中找新数据,这样就把a改成false了。”解锁时将工作内存数据刷新到主内存,加锁时从主内存加载最新数据“。println: 循环中加println也可以是因为println底层加了synchronized锁。
volatile:
有序性
双重检查锁Double-checked locking(有序性经典例子)
懒惰实例化,目的:即使在多线程的环境下也只实例化一次,两次判断INSTANCE成员变量。当类没被实例化的时候,线程A和线程B都进入了第一次if判断,synchronized加锁了,只有一个线程能拿到,实例化了对象。其他等待锁的线程,拿到锁执行的时候,if判断发现已经实例化了,跳出返回。此时被实例化以后,其他线程再来实例化的时候都发现被实例化了,在拿锁之前就返回了。提高了效率和安全性。
如果没有volatile,在对象创建的过程中,正常是先初始化,再分配内存地址,但是这一块并没有有序性,是会被JIT重排序的,大量的并发过来创建对象,假如线程1重排序了,先分配了内存地址,还没初始化。线程2发现INSTANCE不为null了,直接返回了INSTANCE对象,这个INSTANCE对象只是分配了内存地址,并没有属性赋值。又因为是单例模式,所以后面线程再初始化该类,都是半初始化的实例对象。
1 | public final class Singleton { |
CAS
好处:比起synchronized锁,能防止线程阻塞,涉及到线程的上下文切换,因为线程阻塞休眠,会保留阻塞前的状态,等待唤醒,再加载阻塞前的状态。提升效率!
synchronized锁升级
1.6之前。Java 1.6 是 synchronized 锁机制的一个重要分水岭 —— 在这之前,synchronized 性能较差(被戏称为 “重量级锁”)
1.6之后
synchronized 的锁有 4 种状态,按升级顺序是:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
锁升级的条件:
一、从 “无锁” 到 “偏向锁”
- 触发条件:当第一个线程第一次进入 synchronized 代码块时。
- 过程:
JVM 会通过 CAS 操作,在对象头的 “Mark Word” 里记录当前线程的 ID,把锁标记为 “偏向模式”。
这样后续该线程再进入同步块时,不需要任何加锁 / 解锁操作(连 CAS 都不用),直接就能执行 —— 相当于 “这把锁偏心这个线程”。二、从 “偏向锁” 到 “轻量级锁”
- 触发条件:有第二个线程尝试竞争这把锁时(即出现了锁竞争)。
- 细节:
当第二个线程过来时,发现锁是偏向锁且偏向的不是自己,会先检查持有偏向锁的线程是否还在执行(是否存活): - 触发条件:多个线程竞争激烈,轻量级锁的 “自旋” 解决不了问题时。
- 细节:
轻量级锁依赖 “自旋”(线程循环等待,不阻塞),但自旋是有代价的(消耗 CPU): - monitor 是 synchronized 的底层锁机制,每个对象都有一个,负责管理锁的持有和等待。
- 当锁升级到重量级锁时,monitor 会依赖操作系统的内核态操作(互斥量)来阻塞 / 唤醒线程,这是高竞争场景下的 “保底方案”。
- 锁升级的本质就是:能在用户态解决(偏向锁、轻量级锁)就尽量不进内核态,实在不行才用内核态的 monitor 机制。
ReentrantLock
阻塞线程拿锁需要唤醒,是java里面的一个可重入锁,他的使用方法很简单,创建锁,lock加锁,unlock解锁。他的特点是可重入性,比如一个线程拿两次同一把锁,会记录重入次数两次,也需要释放两次。他比Synchronized锁灵活的地方,他可以使用trylock加时间,尝试获取锁,如果时间内没获取到返回false拿锁失败。并且可以中断等待锁的线程,也可以同时实现非公平锁和公平锁。
Synchronized和Lock区别
首先两者的目的都是解决线程安全的一个问题。我从三个方面来讲他们具体的区别,首先是在实现方式,Synchronized是jvm底层的监视器锁实现的,所以在锁升级成重量级锁的时候会从用户态切换到内核态,同时会自动释放锁。而Lock的是juc工具包的一个方法,在代码层面的,需要我们手动的去lock上锁,unlock释放锁。第二个层面在核心特性,从灵活性来说,lock支持非阻塞的获取锁,使用trylock,当获取不到锁的时候可以做降级处理。同时trylock还支持等待获取锁的时间。synchronized如果进程失败会直接阻塞无法中止推出。lock可以通过中断函数主动中断,避免线程出现异常导致无限期的等待。lock可以通过构造函数添加true,实现公平锁,而synchronized只支持非公平锁,这就导致无法保证线程的执行顺序。lock还支持判断是否获取到了锁,而synchronized不行。在适用场景下,如果业务比较简单,只需要使用锁保证原子性推荐使用synchronized锁,而如果代码量比较大,需要自己控制加锁释放锁和获取不到锁出现异常锁的处理方式等等,推荐使用lock锁。
公平锁和非公平锁的实现原理
都是通过AQS中双向链表实现的,公平锁会判断等待队列中有没有前驱节点,没有的话才能修改状态,而非公平锁直接尝试抢锁不按照顺序。
- 公平锁:它会检查同步队列中是否有比当前线程更早请求锁的线程(即队列中是否有前驱节点)。如果有,当前线程不能抢锁,必须排到队尾。
- 只有当队列中没有更早的线程,且 CAS 成功修改 state(从 0 变为 1),才能获取到锁。
- 非公平锁在锁空闲时(state=0),会直接尝试 CAS 抢锁,不管队列中是否有等待的线程。这就可能出现 “后请求的线程先拿到锁”(插队)的情况。
AQS
是 Java 并发包(java.util.concurrent)的 “核心骨架”,像 ReentrantLock、CountDownLatch、Semaphore 这些并发工具,底层都是基于 AQS 实现的。简单说,AQS 就是一个 “模板类”,帮你搞定了并发场景中最复杂的 “线程排队” 和 “锁竞争” 逻辑,你只需要根据自己的需求实现少量代码就行。
- 当多个线程抢一个资源(比如锁)时,AQS 会把没抢到的线程排成一个队列(等待队列),让它们按顺序等待,避免混乱。
- 当资源被释放时,AQS 会按规则唤醒队列中的线程,让它们再次尝试获取资源。
AQS 内部有两个关键部分
状态变量(state)
一个用volatile修饰的 int 变量,用来表示 “共享资源的状态”: - 比如在 ReentrantLock 中,
state=0表示锁没被占用,state>0表示被占用(数值等于重入次数)。 - 在 Semaphore 中,
state表示 “可用的许可数量”。 - 线程通过
CAS操作修改这个状态(比如抢锁时把 state 从 0 改成 1),保证线程安全。
等待队列(同步队列)
一个双向链表,用来存放 “没抢到资源而等待的线程”: - 每个等待的线程会被包装成一个
Node节点,包含线程本身、等待状态(比如是否被中断)等信息。 - 队列遵循 “FIFO”(先进先出)原则,确保线程按顺序等待(公平锁场景)。
ThreadLocal
threadlocal是单线程的副本变量,它是属于当前线程的,线程之间互相不影响,保证了线程变量的安全。它的结构是在thread里面有一个threadlocalmap是kv的集合,当我给threadlocal set的时候会将当前线程threadlocal对象当key 值为value。get也是一样通过当前threadlocal在threadlocalmap中获取value。存在一个内存泄露问题 threadlocal和thread引用在栈中,对象在堆中,产生关键,thread里面还有一个threadlocalmap,他也是在堆中,并且和thread关联,同时它的key是弱引用,并且和threadlocal对象关联,这样在垃圾回收的时候,就回收只有弱引用关联的threadlocal,又因为value是强引用,thread存在value就存在。不销毁就一直存在,所以map会有一个key为null,value存在的情况,在线程池的情况下,线程又不会销毁,而是一直复用,那就会导致一直添加空key又有value导致内存溢出。所以我们需要在每次回收的时候手动remove将强引用的value删除。





