线程基础
线程状态
<b>NEW</b>:线程对象已创建,未调用 start ()<br><b>RUNNABLE</b>(就绪 + 运行):调用 start 后进入就绪队列,获取 CPU 时间片即为运行<br><b>BLOCKED</b>:争抢监视器锁被挂起<br><b>WAITING</b>:无时限等待其他线程唤醒 / 中断<br><b>TIMED_WAITING</b>(限时等待):设置超时时间,到期自动解除等待<br><b>TERMINATED</b>(终止):线程任务执行完毕<br>
join
等待调用join方法的线程结束之后,程序再继续执行,如thead2调用了thread1.join(),需要thread1线程执行完后thread2才能执行。
中断机制
interrupt () 只打中断标识,不强行结束线程;isInterrupted () 查看标识,静态 interrupted () 查看并清除标识。<br>线程 sleep、wait、join 阻塞时被中断,抛出异常且自动清空中断标记。
Callable CompletableFuture
Callable & Runnable
Runnable:无返回值,run () 不能抛出受检异常;<br>Callable<V>:call () 有泛型返回值,可抛出 Exception,适合需要获取异步结果场景。
FutureTask
<font color="#e74f4c">同时实现 Runnable、Future</font>,是二者桥梁;<b>用法</b>:包装 Callable,丢入 Thread / 线程池执行,通过自身 get () 拿返回值 , 业务场景:多接口并行查询
<b>Future的局限性</b><br>- 获取结果方式阻塞:仅 get () 获取结果,只能等待,无其他办法<br>- 不支持多任务链式调用:任务完成后无法自动<font color="#e74f4c">执行后续操作</font><br>- 不能组合多个任务:无法实现多任务全部结束后执行动作<br>- 无内置异常处理相关方法
CompletableFuture
CompletableFuture是Future接口的扩展和增强。CompletableFuture 实现了非阻塞回调、多任务编排、异常统一处理,异步编程更灵活。
ThreadLocal
特性
特性:<br>1.线程安全: 在多线程并发的场景下保证线程安全<br>2.传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量<br>3.线程隔离: 每个线程的变量都是独立的,不会互相影响
构成
1.每个Thread线程内部都有一个Map (ThreadLocalMap)<br>2.Map里面存储ThreadLocal对象(key)和线程的变量副本(value)<br>3.Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。<br>4.对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰
内存泄漏
底层结构
每个Thread持有ThreadLocalMap,Map 中 Entry:key 是 ThreadLocal 弱引用,value 是数据强引用。
泄漏点
1.引用不对称:ThreadLocal 外部引用置空后或者gc,key (弱引用)会被 GC 置为 null,但 value 被 Entry 强引用无法释放。<br>2.线程长期存活:线程池复用线程,线程不会销毁(threadLocalMap是线程的属性),value 一直被线程持有堆积。<br>3.未手动 remove:自带的过期 Entry 清理只会在 get/set 时被动局部清理,不可靠。<br>引用链(一句话)
解决方案
使用代码包裹在try-finally,finally 中执行remove()。
Hash冲突解决方式
1.源头减少冲突:魔数生成均匀 hash 值 2 线性探测开放寻址法处理 (槽位被其他数据占用(冲突),向后找空位写入)<br>
synchronized
重量级锁原理
synchronized 依托 monitor、CAS、互斥锁实现,依靠 EntryList、cxq 存放抢锁失败线程,waitSet 存放 wait 阻塞线程;解锁 /notify 唤醒队列线程重新竞争锁。因线程阻塞唤醒需要用户态、内核态切换,系统开销大,属于重量级锁。
重量级锁的优化策略
锁粗化、锁消除、轻量级锁、偏向锁、自适应自旋
锁升级流程
- 最开始是偏向锁:大多时候就一个线程反复拿锁,直接记线程 id,不用频繁 CAS,开销最低。来了第二个线程抢锁,触发撤销偏向,升级成轻量级锁。<br>- 轻量级锁:线程在自己栈内存放锁记录,用 CAS 抢锁;抢不到的线程原地自旋循环重试,全程用户态运行,不用切换系统内核。自旋一直拿不到锁,就膨胀成重量级锁。<br>- 重量级锁:依赖操作系统内核互斥锁,抢不到资源的线程直接被系统挂起阻塞,进入同步队列(EntryList/cxq;线程切换内核态开销很大,性能最差。<br>- 关键点:锁只能单向升级,偏向→轻量→重量,不会降级。
AQS之独占锁ReentrantLock
什么是AQS
AQS 封装了线程排队、阻塞唤醒、独占 / 共享获取的通用底层能力,JDK 各类同步组件通过内部类继承 AQS,依托 state 状态值实现自身同步逻辑。<br><br>JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的<br>- 一般是通过一个内部类Sync继承 AQS<br>- 将同步器所有调用都映射到Sync对应的方法<br>
AQS 核心结构
AQS 是 JUC 同步组件的底层通用框架,四大核心组成:<br>state 同步状态:独占模式标记锁持有状态,数值代表锁重入次数<br>双向同步队列:获取锁失败的线程入队阻塞排队<br>Condition 单向等待队列:线程 await 阻塞,signal 唤醒后移入同步队列抢锁<br>LockSupport:依靠 park、unpark 完成线程阻塞与唤醒
ReentrantLock 加锁逻辑(非公平锁)
1.线程执行 lock (),直接 CAS 尝试将 state 改为 1,抢占成功直接持有锁;<br>2.CAS 失败,判断当前线程是否是锁持有者,是则 state 自增,完成锁重入;<br>3.既抢锁失败、也非重入,线程封装为 Node 加入同步队列,被 park 阻塞;<br>4.旧线程释放锁唤醒队列节点时,新到来的线程依旧可以 CAS 插队抢锁,并发吞吐量更高。
ReentrantLock 加锁逻辑(公平锁)
1<b>.lock () 不会直接 CAS 抢锁,先检查同步队列是否存在排队线程</b>;<br>2.队列无等待线程才执行 CAS 上锁,有排队线程则直接排入队列尾部阻塞;<br>3.锁释放时只会按队列顺序唤醒队首后继节点,严格遵循先来先得,无插队,性能开销更大。
释放锁 unlock () 流程
1.调用unlock(),底层执行 AQS 的release()方法,对 state 执行减 1 操作;<br>2.递减后 state > 0:说明是多层重入锁,仅扣除重入计数,锁没有真正释放,流程结束;<br>3.递减后 state == 0:彻底释放锁,清空 AQS 记录的当前持有线程;<br>4.校验同步队列 head 节点状态正常,使用 unpark 唤醒 head 的下一个有效等待线程;<br>5.被唤醒的线程退出 park 阻塞,再次通过 CAS 尝试获取锁。
并发同步工具类
Semaphore
主要用于在一个时刻允许多个线程对共享资源进行并行操作的场景。
CountDownLatch
<font color="#e74f4c">用来让主线程等待全部子任务执行完毕再运行</font>。构造方法指定计数器初始值,子任务执行完毕调用countDown()让计数器自减;<b>主线程执行await()阻塞等待</b>,计数器归零后主线程被唤醒继续执行。
CyclicBarrier
- CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的 CyclicBarrier内部的计数器count是可被重置的,进而使得CyclicBarrier也可被重复使用
Exchanger
exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。
CAS
CAS定义
CAS:CPU 硬件级原子指令,传入原值 V、预期值 E、新值 N<br>逻辑:V==E 则更新为 N,修改失败则重试;比较赋值一体原子执行,硬件保证安全,无锁实现并发。
缺陷
- 自旋 CAS 长时间不成功,则会给 CPU 带来非常大的开销<br>- 只能保证一个共享变量原子操作<br>- ABA 问题
LongAdder原理
AtomicLong:依靠单个 value 变量 CAS 更新,多线程争抢同一变量,CAS 冲突频繁<br>LongAdder:拆分热点至 Cell 数组,线程各自 CAS 操作对应数组槽位,降低竞争冲突;取值时累加所有槽位数值得到总和
线程池
好处
<b>节约资源</b>:复用线程,规避频繁创建销毁的开销<br><b>响应更快</b>:任务到来直接复用空闲线程执行<br><b>统一管控</b>:限制线程数量,集中调度、监控,提升系统稳定<br><b>能力扩展</b>:支持定时、延时等进阶任务执行
核心参数
corePoolSize
核心线程不会预创建,提交任务时才按需初始化创建。<br>
maximumPoolSize
核心线程、队列全部占满后,线程总数未达最大值时,新建非核心线程处理任务主题
keepAliveTime
非核心线程空闲超时自动回收;核心数等于最大线程数时,该超时配置失效。
workQueue
任务队列分有界、无界、同步队列;线程数超过核心数后,新任务入队存储
threadFactory
默认采用系统线程工厂,也可实现接口自定义线程工厂
handler
线程池无法继续接收任务时(workQueue已满和maximumPoolSize已满)的拒绝策略
关闭线程池
- shutdownNow():立即关闭线程池,正在执行中的任务和队列中的任务都会被中断
shutdown():关闭线程池,正在执行中的任务和队列中的任务都能执行完成,后续进来的新任务会被执行拒绝策略。
- isTerminated():当正在执行的任务和队列中的任务全部都执行完时返回true。
线程池原理
当前线程数 < 核心线程数:新建核心线程执行任务<br>核心线程占满:任务存入阻塞队列<br>队列已满 && 总线程 < 最大线程数:创建非核心线程执行任务<br>队列已满 && 总线程达到最大值:执行拒绝策略<br>闲置非核心线程超时销毁;核心线程默认常驻
<br>注意:提交一个Runnable时,不管当前线程池中的线程是否空闲,只要数量小于核心线程数就会创建新线程。<br>注意:ThreadPoolExecutor相当于是非公平的,比如队列满了之后提交的Runnable可能会比正在排队的Runnable先执行
为什么一定得是阻塞队列
如果队列中没有任务了,线程为了不自然消亡,就会阻塞在获取队列任务时,等着队列中有任务过来就会拿到任务从而去执行任务。
线程发生异常,会被移出线程池
当执行任务时出现异常时,最终会执行processWorkerExit(),执行完这个方法后,当前线程也就自然消亡了,但是!processWorkerExit()方法中会额外再新增一个线程,这样就能维持住固定的核心线程数。
addWorke()
addWorker 负责创建工作线程,core 参数区分核心 / 非核心线程:<br>核心线程:校验线程数<核心阈值才创建,超限则任务入队<br>非核心线程:校验线程数<最大阈值才创建,超限执行拒绝策略<br>创建前校验线程池状态:仅 RUNNING 正常新建;SHUTDOWN 状态下队列有残留任务时,仍可新建线程处理遗留任务<br>线程区别仅为回收规则:非核心线程空闲超时销毁;核心线程默认常驻,开启超时参数后才会被回收