介绍一下AQS
AQS(AbstractQueuedSynchronizer)是 Java 并发包的核心同步框架,通过一个volatile int state
状态变量和 CLH 双向队列实现多线程同步。它采用模板方法模式,将锁的获取(如tryAcquire
)和释放(如tryRelease
)等核心逻辑交由子类实现,自身负责线程的排队、阻塞及唤醒。获取锁时,线程通过 CAS 操作修改state
,成功则获得锁(可重入锁会递增state
),失败则封装为 Node 节点进入队列阻塞;释放锁时递减state
,并唤醒队列后继节点。AQS 支持独占(如 ReentrantLock)和共享(如 Semaphore)两种模式,通过LockSupport
实现高效线程调度,是 Java 并发工具的基础。
AQS工作原理
1. 获取锁流程
① 线程调用tryAcquire()
尝试获取锁(由子类实现)
- 独占锁(一次只能被一个线程持有):
例如ReentrantLock.lock()
,其底层调用Sync.tryAcquire()
,通过 CAS 设置state=1
实现锁的独占获取。 - 共享锁(允许多线程同时获取):
例如Semaphore.acquire()
和CountDownLatch.await()
,其底层调用Sync.tryAcquireShared()
,通过state
表示剩余资源量(如许可数、计数),允许多线程并发获取。
② 若state
为 0,CAS 将其设为 1,获取成功
③ 若state
非 0 且当前线程已持有锁(可重入),state
递增
④ 获取失败则创建Node
节点,通过 CAS 加入队列尾部
⑤ 节点进入自旋状态,检查前驱节点是否为头节点
⑥ 若是头节点且再次尝试获取锁成功,则移除当前节点并返回
- ⑤⑥点解析:在 AQS 的 CLH 队列中,当线程获取锁失败被封装为节点入队后,会进入自旋状态反复检查前驱节点是否为头节点。这是因为头节点代表当前持有锁的线程(或刚释放锁的线程),只有头节点的后继节点有资格竞争锁。若检查发现前驱确为头节点,则说明轮到自己尝试获取锁,于是再次调用
tryAcquire()
尝试 CAS 竞争锁;若成功,则将当前节点设为新头节点(断开与前驱的连接),原头节点因不再被引用而被 GC 回收,当前线程退出等待状态继续执行。这一机制既保证了公平性(线程按入队顺序获取锁),又通过自旋减少了线程频繁阻塞 / 唤醒的开销,尤其适合锁持有时间短的场景。
2.释放锁流程
① 线程调用tryRelease()
释放锁(子类实现)
② state
递减,若减为 0 表示完全释放
③ 唤醒队列头节点的后继节点(通过LockSupport.unpark()
)
④ 被唤醒的线程重新尝试获取锁
CAS 和 AQS 有什么关系?
CAS(比较并交换)是 CPU 支持的底层原子操作原语,通过硬件指令实现单个变量的非阻塞同步(如AtomicInteger
的自增),依赖自旋重试且可能存在 ABA 问题;AQS(抽象队列同步器)是 Java 并发包中的中层框架,基于 CAS+volatile 维护state
状态和 CLH 等待队列,通过阻塞线程实现复杂同步逻辑(如ReentrantLock
的可重入锁、Semaphore
的许可控制)。两者联系在于 AQS 的状态更新和队列操作依赖 CAS 实现原子性,而 AQS 则是 CAS 在高层同步场景中的封装应用,CAS 是底层技术基础,AQS 是上层功能框架。
Threadlocal作用,原理,具体里面存的key value是啥,会有什么问题,如何解决?
作用:
线程隔离:ThreadLocal为每个线程提供了独立的变量副本,这意味着线程之间不会相互影响,可以安全地在多线程环境中使用这些变量而不必担心数据竞争或同步问题。
降低耦合度:在同一个线程内的多个函数或组件之间,使用ThreadLocal可以减少参数的传递,降低代码之间的耦合度,使代码更加清晰和模块化。
性能优势:由于ThreadLocal避免了线程间的同步开销,所以在大量线程并发执行时,相比传统的锁机制,它可以提供更好的性能。
底层原理:
ThreadLocal 的核心原理是通过线程的私有存储实现数据隔离:每个 Thread 实例内部维护一个 ThreadLocalMap,该 Map 的键为 ThreadLocal 实例(弱引用),值为用户存储的对象;当调用 ThreadLocal 的 set/get 方法时,实际操作的是当前线程的 ThreadLocalMap 中的对应 Entry;这种设计使得不同线程对同一 ThreadLocal 实例的操作互不干扰,实现线程封闭;
存在的问题:
ThreadLocal 的内存泄漏核心源于引用关系与线程生命周期的不匹配。当 ThreadLocal 实例通过弱引用作为 ThreadLocalMap 的 Key 时,若外部强引用消失,Key 会被 GC 回收,但 Value 仍被 Entry 的强引用持有,且线程若长期存活(如线程池场景),Value 无法被释放,导致内存泄漏。即便外部使用弱引用持有 ThreadLocal 实例,Value 的强引用链仍依赖于线程存活,无法避免泄漏。
具体分析
当外部代码将 ThreadLocal 实例置为 null(如tl = null
)时,由于 Entry 的 Key 是弱引用:
- Key 会被 GC 自动回收(弱引用在下次 GC 时被清理)
- Entry 的 Key 变为 null,但 Value 仍被强引用持有
此时 Entry 变成 “无效 Entry”(Key=null 但 Value 存在),需通过remove()
清理 Value。
若不调用remove()
,即使 Key 被回收,Value 也会一直存在于 Map 中,直到:
- 线程终止(ThreadLocalMap 随线程销毁)
- 下次操作 Map(如
get/set
)触发expungeStaleEntry()
方法清理无效 Entry
但在线程池场景中,线程不会终止,因此必须主动remove()
。
如何解决:
使用完ThreadLocal变量后调用remove()方法释放资源。
悲观锁和乐观锁的区别
悲观锁
悲观锁基于 “最坏情况” 假设,认为数据在并发访问时极易被修改,因此在操作数据前会先获取锁,确保同一时间只有一个线程能操作数据,其他线程需等待锁释放。
乐观锁
乐观锁则秉持 “乐观” 态度,假设数据在并发访问时冲突概率低,不主动加锁,而是通过版本号、时间戳或 CAS(比较并交换)机制在更新时检查数据是否被修改
实现乐观锁,有哪些方法?
-
CAS(Compare and Swap)操作:CAS是乐观锁的基础。Java提供了java.util.concurrent…atomic包,包含各种原子变量类(如Atomiclnteger、AtomicLong),这些类使用CAS操作实现了线程安全的原子操作,可以用来实现乐观锁。
-
版本号控制:增加一个版本号字段记录数据更新时候的版本,每次更新时递增版本号。在更新数据时,同时比较版本号,若当前版本号和更新前获取的版本号一致,则更新成功,否则失败。
-
**时间戳:**使用时间戳记录数据的更新时间,在更新数据时,在比较时间戳。如果当前时间戳大于数据的时间戳,则说明数据已经被其他线程更新,更新失败。
CAS有什么缺点?
-
ABA问题:
ABA 问题是指在并发编程中,当线程 1 读取某变量的初始值为 A(预期值),准备进行更新操作时,线程 2 先将该变量修改为 B,再改回 A,此时线程 1 通过 CAS(比较并交换)机制检查时,发现当前值与预期值 A 一致,误认为变量未被修改而执行更新操作,但实际上变量在中间过程已被修改过,这种 “结果一致但过程变更” 的情况会导致业务逻辑错误或数据不一致,本质是 CAS 仅验证最终值而忽略中间操作带来的语义偏差。
-
只能保证单个变量的原子性:CAS 仅能保证单个变量的原子操作,无法像
synchronized
或ReentrantLock
那样支持多个变量的原子性操作。使用AtomicReference
包装对象,将多个变量封装为一个对象,通过 CAS 操作对象引用来保证整体原子性 -
长时间自旋,CPU开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
CAS的ABA问题怎么解决?
CAS 的 ABA 问题可通过为变量添加版本号、时间戳等变更标记来解决,如 Java 中AtomicStampedReference
通过stamp
记录版本,确保 CAS 操作时不仅验证当前值,还检查版本是否与预期一致,避免因变量中间被修改后又恢复原值导致的误更新。
voliatle关键字有什么作用?
-
保证变量的可见性:
当一个变量被声明为volatile时,它会保证对这个变量的写操作会立即刷新到主存中,而对这个变量的读操作会直接从主存中读取,从而确保了多线程环境下对该变量访问的可见性。这意味着一个线程修改了volatile变量的值,其他线程能够立刻看到这个修改,不会受到各自线程工作内存的影响。
-
禁止指令重排序:
JVM 为优化性能可能对指令重新排序(如先执行写操作后执行前置逻辑),但
volatile
可限制特定场景的重排序。四种场景
volatile和sychronized比较?
Synchronized是一种排他性的同步机制,保证了多个线程访问共享资源时的互斥性,即同一时刻只允许一个线程访问共享资源。通过对代码块或方法添加Synchronized关键字来实现同步。
Volatile是一种轻量级的同步机制,用来保证变量的可见性和禁止指令重排序。当一个变量被声明为Volatile时,线程在读取该变量时会直接从内存中读取,而不会使用缓存,同时对该变量的写操作会立即刷回主内存,而不是缓存在本地内存中
volatile可以保证线程安全吗?
volatile关键字可以保证可见性,但不能保证原子性,因此不能完全保证线程安全。volatile关键字用于修饰变量,当一个线程修改了volatile修饰的变量的值,其他线程能够立即看到最新的值,从而避免了线程之间的数据不一致。 但是,volatile并不能解决多线程并发下的复合操作问题,比如i++这种操作不是原子操作,如果多个线程同时对i进行自增操作,volatile不能保证线程安全。对于复合操作,需要使用synchronized关键字或者Lock来保证原子性和线程安全
非公平锁吞吐量为什么比公平锁大?
非公平锁吞吐量比公平锁大,是因为其在锁释放时允许新到达线程直接尝试获取锁(可能 “插队”),减少了队列维护、线程唤醒等开销,避免了公平锁中线程必须严格排队导致的上下文切换损耗(如 CPU 缓存失效),从而在高并发场景下更高效利用 CPU 资源,提升系统整体吞吐量,但可能引发线程 “饥饿” 问题,而公平锁则通过严格遵循 “先到先得” 原则保证了获取锁的公平性
什么情况会产生死锁问题?如何解决?
**互斥条件:**互斥条件是指多个线程不能同时使用同一个资源。
**持有并等待条件:**持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
**不可剥夺条件:**不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
**环路等待条件:**环路等待条件指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。
解决方法?
破坏上述条件的其中一个。常见的有资源有序分配法。
资源有序分配法是通过给资源设定递增序号(如R1<R2),强制线程按序号从小到大申请资源的死锁预防策略,确保依赖关系单向化,避免循环等待。
例子:线程A和线程B需同时获取资源X(序号1)和资源Y(序号2):
-
线程A:先申请X,成功后再申请Y;
-
线程B:同样先申请X,若X被占用则等待,获取X后再申请Y;
两线程按统一顺序申请资源,不会出现“线程A持有Y等X,线程B持有X等Y”的死锁环。若线程B试图先申请Y,系统会因序号逆序拒绝,强制其按规则申请。
线程池工作原理
线程池分为核心线程池,线程池的最大容量,还有等待任务的队列,提交一个任务,如果核心线程没有满,就创建一个线程,如果满了,就是会加入等待队列,如果等待队列满了,就会增加线程,如果达到最大线程数量,如果都达到最大线程数量,就会按照一些丢弃的策略进行处理。
线程池的核心参数
1. corePoolSize(核心线程数)
- 作用:线程池保持活跃的最小线程数量。即使线程处于空闲状态,也不会被销毁(除非设置了
allowCoreThreadTimeOut
),当有任务时优先使用核心线程处理。当核心线程数已满,新的任务则会加入任务队列等候。
2. maximumPoolSize(最大线程数)
- 作用:线程池允许创建的最大线程数量。当任务队列已满且线程数未达到此值时,会创建新非核心线程执行任务。
3. keepAliveTime(线程存活时间)
- 作用:当线程数超过
corePoolSize
时,多余的空闲线程在被销毁前等待新任务的最长时间。
4. TimeUnit(时间单位)
5. workQueue(任务队列)
- 作用:存储待执行任务的阻塞队列。常用实现包括:
LinkedBlockingQueue
:无界队列(可能导致 OOM)。ArrayBlockingQueue
:有界队列,需指定容量。SynchronousQueue
:不存储任务,每个插入操作必须等待另一个线程的移除操作。PriorityBlockingQueue
:优先级队列,按任务优先级排序。
6. threadFactory(线程工厂)
- 作用:创建线程的工厂类,可自定义线程的名称、优先级、守护状态等。
7. RejectedExecutionHandler(拒绝策略)
- 作用:当线程池和任务队列都满时,处理新提交任务的策略。JDK 提供 4 种内置策略:
AbortPolicy
(默认):抛出RejectedExecutionException
异常。(直接拒绝并道歉)CallerRunsPolicy
:由提交任务的线程(调用者)直接执行该任务。(顾客自己动手)DiscardPolicy
:直接丢弃新任务,不做任何处理。(直接拒绝也不道歉)DiscardOldestPolicy
:丢弃队列中最老的任务,然后尝试提交新任务。(丢弃最先等待的顾客,做最后来的顾客)
线程池核心参数调优策略
1. CPU 密集型任务
- 场景:大量计算(如科学计算、机器学习训练)。
- 参数建议:
corePoolSize = CPU核心数 + 1
(避免上下文切换,充分利用 CPU)maximumPoolSize = corePoolSize
(CPU 密集型任务创建过多线程会导致频繁切换,效率反而降低)workQueue = SynchronousQueue
(不排队,直接移交任务,减少线程等待时间)
2. IO 密集型任务
- 场景:网络请求、文件读写、数据库操作等。
- 参数建议:
corePoolSize = 2 * CPU核心数
(CPU 在等待 IO 时可处理其他任务)maximumPoolSize = 更大值
(视系统资源而定,避免 IO 阻塞导致任务堆积)workQueue = LinkedBlockingQueue(较大容量)
(IO 操作耗时较长,用队列缓冲任务)
线程池种类
1. FixedThreadPool(固定大小线程池)
- 核心特性:线程数量固定(核心线程数 = 最大线程数),任务队列使用无界队列(
LinkedBlockingQueue
)。
2. CachedThreadPool(缓存线程池)
- 核心特性:核心线程数为 0,最大线程数为
Integer.MAX_VALUE
,使用SynchronousQueue
直接移交任务,线程闲置 60 秒后自动销毁。
3. SingleThreadExecutor(单线程线程池)
- 核心特性:核心线程数和最大线程数均为 1,使用无界队列(
LinkedBlockingQueue
),确保所有任务按顺序执行。
4. ScheduledThreadPool(定时任务线程池)
- 核心特性:核心线程数固定,最大线程数为
Integer.MAX_VALUE
,使用DelayedWorkQueue
实现定时 / 周期任务。
线程池 shutdown () 和 shutdownNow () 方法的作用与区别
1. shutdown () 方法:平滑关闭线程池
- 核心作用:
- 禁止线程池接收新任务,但会等待已提交的任务(包括正在执行和队列中等待的任务)执行完毕。
- 线程池状态变为
SHUTDOWN
,此时不会创建新线程,但已存在的线程会继续处理剩余任务
2. shutdownNow () 方法:立即中断线程池
- 核心作用:
- 立即尝试停止所有正在执行的任务,同时拒绝接收新任务。
- 尝试中断所有工作线程(通过
Thread.interrupt()
),并返回队列中未开始执行的任务列表。 - 线程池状态变为
STOP
,不会等待任务完成。
线程池中的任务可以被撤回吗?
在 Java 线程池中,提交的任务通常无法直接撤回,但可通过以下间接方式实现类似效果:
- 若任务通过submit()提交并返回Future对象,可调用Future.cancel(boolean mayInterruptIfRunning)尝试取消任务:
- 若任务未开始执行,取消成功且不再执行;
- 若任务正在执行,
mayInterruptIfRunning
为true
时会中断线程(需任务响应中断),为false
则允许任务执行完毕; - 已完成的任务无法取消,
cancel()
返回false
。
- 对于未通过
Future
管理的任务(如execute()
提交的Runnable
),只能通过关闭线程池(shutdownNow()
)中断所有执行中任务,但会影响其他任务且可能导致资源未正常释放。
总结:线程池不提供通用撤回接口,需借助Future
的cancel()
方法结合任务对中断的响应机制实现有限取消,或通过关闭线程池强制中断,但均存在局限性。