并发压测工具jcstress
| 概念 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 本质 | 程序的一次执行实例(Execution Instance) | 进程内的一个执行流(Execution Flow) |
| 是什么 | 操作系统分配资源的基本单位 | 操作系统进行调度的基本单位 |
Java 底层会调用 pthread_create 来创建线程,所以本质上 java 程序创建的线程,就是和操作系统线程是一样的,是 1 对 1 的线程模型。
并发和并行
创建新线程:
Context Switch
- 主动让出 CPU,比如调用了
sleep(),wait()等。 - 时间片用完
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
- 被终止或结束运行
常用方法
Sleep 让线程从running 到timed wating (阻塞)状态,释放 CPU,但不会释放持有的锁 醒来过不一定立即执行
yield方法 hint调度器,当前线程愿意放弃其当前的 CPU 时间片。状态从RUNNING(运行中) ->RUNNABLE(就绪) 。调度器完全可以忽略这个提示,让该线程立即再次运行
设置线程优先级 cpu忙的时候优先级高的时间片分的多 闲的时候优先级没什么用 最终 都是取决于os的任务调度器
join/join(long) 等待线程运行结束 可用于实现同步等待
中断线程
- 通过标志位判断需要正确使用
volatile关键字 - 对目标线程调用
interrupt()方法 如果该线程正在执行低级别的可中断方法(如Thread.sleep()、Thread.join()或Object.wait()),则会解除阻塞并抛出InterruptedException异常 →抛异常 + 清标志
非阻塞状态(运行、锁竞争) →不抛异常 + 保留打断标志,相当于“未被打断,依旧正常执行”
设计模式——两阶段终止模式
方案一:利用interrupt方法
- 第一阶段:通知线程准备停止(设置停止标志或发送中断信号)。
- 第二阶段:线程感知到终止请求后,完成剩余任务并释放资源,最终安全退出。 方案二:volatile变量替代打断标记
Park方法
但打断标记true的时候park失效
守护线程
线程状态
状态转换
BLOCKED和WAITING两个状态最大的区别有两个:
- BLOCKED是锁竞争失败后的被动结果,WAITING是主动触发的状态
- BLCKED的唤醒时jvm自动触发的,而WAITING状态是必须要通过特定的方法来主动唤醒
BLOCKED和WAITING都是用放弃 CPU的方式来换取不消耗资源,但代价是高昂的切换成本。
共享模式之管程(monitor)
临界区(对共享资源有读有写)的竞态条件
互斥 是解决“竞态条件”的手段(上下文切换是trigger),它通过保证“原子性”来确保并发安全 同步 是一个比“互斥”更宽泛的概念。它的目的就是协调多个线程的执行顺序或时机
线程安全
成员变量和静态变量 如果被共享了且有写操作 非线程安全 局部变量 是线程安全的 ,但如果是引用的对象需要判断是否发生引用逃逸
局部变量的引用本身是线程私有的,但被引用对象是否线程安全,要看对象是否逃逸出方法作用域
- 成员变量(实例字段)存放在堆(Heap)中;
- 静态变量(类字段)存放在方法区 / 元空间(MetaSpace),逻辑上也是共享的;
- 局部变量存在每个线程自己的栈帧(Stack Frame)中
设计线程安全类时,禁止子类通过继承修改关键同步逻辑,通过final和private
String类就是final的
常见线程安全类
组合调用不一定安全
Integer 和String 都是不可变类 就是read only 因此线程安全,即不改变只新建
monitor lock
(操作系统提供的) 把锁对象的markword置为指向monitor的指针 把owner设置为该线程
对象头
4字节markword 4字节kcalss word 数组对象还多一个4字节的数组长度
normal状态下 mark word的内容 是hashcode+GCage+biaslock+two bits的标识
synchronized优化
wait & notify
wait/notify等方法依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法
Owner线程发现条件不足,调用wait方法,进入waitset变成waiting 状态
BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
唤醒 BLOCKED线程 会在Owner线程释放锁时被jvm全部唤醒 WAITING线程在Owner线程调用notify才被唤醒 被唤醒后竞争是非公平的(可能导致虚假唤醒问题)
和sleep区别:
设计模式-同步模式:保护性暂停
用在一个线程等待另一个线程的结果 使用notifyAll(),并配合while循环来检查等待条件,是 Java 中实现线程间等待/通知的标准范式
join的原理就是应用了保护性暂停 只不过等待的是另一个线程的结束
解耦
异步模式
park & unpark
解决了suspend/resume的死锁问题
原理:通过_counter计数 当作“通行证”
- 每个线程都有且仅有一个关联的“通行证”。
- 这个通行证的“数量”非 0 即 1
unpark(Thread t)的作用是给目标线程t“发放”一个通行证- 调用
LockSupport.park()的作用是检查当前线程是否持有通行证,没有会进入WAITING状态
Condition(await/signal) 和synchronized(wait/notify) 都是“监视器”模型,而park/unpark是“许可证”模型。park/unpark的设计专门解决了wait/notify的一个核心痛点。
痛点:wait/notify必须先wait,后notify。如果一个线程notify发生在了wait之前,这个通知信号就会丢失,导致wait的线程永久等待。
多锁
细粒度的锁 增强程序并发性
死锁:死锁是指两个或多个线程(或进程)在执行过程中,因互相持有对方所需要的资源而导致的一种永久的阻塞状态。在这种状态下,所有线程都在等待一个永远不会被释放的资源,导致程序无法继续执行。
哲学家就餐问题 tryLock
定位死锁:jconsel 或者jsp定位id 再jstack分析
活锁问题:双方同时改变对方的结束条件 解决方案:引入随机性,如随机睡眠时间
饥饿现象 可以使用公平锁
ReentrantLock
可重入 可打断 锁超时 公平锁 条件变量(多个waitlist)
一定要在finally块里unlock
可打断: 必须调用.lockInterruptibly(); “可中断”特性只在阻塞状态的阶段响应中断
支持超时:
ReentrantLock提供了tryLock(timeout)的方法,可以指定等待获取锁的最长等待时间,如果超过了等待时间,就会获取锁失败,不会一直等待。条件变量 借助于
newCondition()方法和await()/signal()——实现等待/通知机制,注意Condition必须在锁内使用
同步模式——顺序控制
使用reentrantLock的await&signal机制 实现交替输出的时候 如果没有像wait/notify 一样使用状态变量 就会有死锁风险(信号丢失)
可见性
犹豫/balking模式
用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回 经典应用:线程安全的单例
线程安全单例实现:
共享模式之无锁
CAS(Compare-And-Swap)
基于乐观锁的思想 ,CAS配合volatile(因为你比较肯定比最新的值嘛)可以实现无锁并发无阻塞并发(上下文切换相对少),线程数最好不超过cpu核心数
怎么实现的? Java 中 CAS 是 C++ 内联汇编的形式实现的,通过 JNI调用。因此,CAS 的具体实现与操作系统以及 CPU 密切相关
原子类
原子类的组合操作不是线程安全的 解法就是标准的 CAS 自旋循环while(true)
原子操作类
Atomic类依赖于 CAS乐观锁+volatile 来保证其方法的原子性
基本类型AtomicInteger AtomicLong AtomicBoolean
引用类型AtomicReference
在多线程环境下,修改一个对象的引用(例如
User user = new User(...))本身是原子的,但是“检查后执行”(Check-Then-Act)的操作不是原子的。例如:if (user == expected) user = newOne;这段逻辑如果不加锁,在并发下是不安全的。 而AtomicReference利用 CPU 提供的 CAS指令,保证原子性,而无需使用重量级的synchronized锁。
AtomicStampedReference(带版本号/时间戳的原子引用)解决了ABA问题
数组类型使用原子的方式更新数组里的某个元素
对象的属性修改类型原子类/字段更新器它只是一个工具/视窗。它本身不存储数据
FieldUpdater在底层利用了Unsafe.compareAndSwapObject指令。CAS 操作要想安全,前提是多线程必须能看到变量的“最新值”(即内存可见性)。 由于FieldUpdater无法改变字段原本的定义,它只能强制要求你在定义字段时就必须加上volatile
原子累加器
LongAdderLongAdder的思路是:既然一个变量(base)大家抢着更新太慢,那我就把这个变量拆成很多份(cell[]),让大家分头去更新,最后再加起来
缓存行伪共享 为防止缓存行失效导致效率降低:
CAS可能的问题
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。
循环时间长开销大 CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销
Unsafe类
共享模式之不可变
不可变类设计
天生线程安全 可以作为 HashMap 的 Key 原子性保障
- 类声明为
final:防止被继承(子类可能会重写方法来破坏不可变性)。 - 所有成员变量为
private final:private隐藏内部实现。final保证引用一旦赋值不可变更,且保证了多线程下的初始化安全性(JMM 保证)。
- 不提供 Setter 方法:堵死修改的入口。
- 构造函数初始化所有变量:在创建时一次性赋值。
- 关键点:保护性拷贝
- 如果成员变量是引用类型(如
Date,List,Person),且该对象本身是可变的,你必须在构造函数和 Getter 方法中进行深拷贝。
- 如果成员变量是引用类型(如
享元模式
最小化内存使用
体现例子:包装类、String 串池、连接池
final 原理
final通过禁止构造函数内的指令重排序(写屏障),保证了对象在多线程环境下的初始化安全性
线程池
线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗。
线程池
处理流程
线程池状态
用一个AtomicInteger ctl 描述 前三bits是状态 后29bits是线程数量
构造方法
7个参数:
- corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。
- maximumPoolSize:限制了线程池能创建的最大线程总数(包括核心线程和非核心线程),当
corePoolSize已满 并且 尝试将新任务加入阻塞队列失败(即队列已满)并且 当前线程数 <maximumPoolSize,就会创建新线程执行此任务,但是当corePoolSize满 并且 队列满 并且 线程数已达maximumPoolSize并且 又有新任务提交时,就会触发拒绝策略。 - keepAliveTime:当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
- unit:就是keepAliveTime时间的单位。
- workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
- threadFactory:线程工厂。可以用来给线程取名字等等
- handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略
四种预置的拒绝策略:
- CallerRunsPolicy,使用线程池的调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的任务队列已有空缺。
- AbortPolicy,直接抛出一个任务被线程池拒绝的异常。
- DiscardPolicy,不做任何处理,静默拒绝提交的任务。
- DiscardOldestPolicy,抛弃最老的任务,然后执行该任务。
- 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略
提交任务
| 方法 | 所属接口 | 参数 / 返回值 | 阻塞性 | 异常处理机制 (核心) | 适用场景 |
|---|---|---|---|---|---|
execute | Executor | Runnable |
🔁void|否|直接抛出
❌ 堆栈可见
⚠️ Worker可能重建 |发后即忘
(日志、通知) |
|submit|Ex...Service|Run/Callable
🔁Future|否|内部捕获
如果是runnable就需要用trycatch块
future 需调用get()才会抛出 |异步求值
(需要结果/后续处理) |
|invokeAll|Ex...Service|Coll<Call>
🔁List<Future>|是
(等全部) |封装于 Future
⏳ 超时未完会被自动取消|并发聚合 (全量)
(比价、多源汇总) |
|invokeAny|Ex...Service|Coll<Call>
🔁T(结果) |是
(等最快) |丢弃异常
🗑️ 除非全失败才抛出 |竞争获取 (赛马)
(多备份查询,取最快) |
Shutdown
- shutdown使用了以后会置状态为SHUTDOWN,正在执行的任务会继续执行下去,没有被执行的则中断。此时,则不能再往线程池中添加任何任务,否则将会抛出 RejectedExecutionException 异常
- 而 shutdownNow 为STOP,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。 它试图终止线程的方法是通过调用 Thread.interrupt() 方法来实现的,但是这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。
异步模式之工作线程
池化死锁:
根本原因:当提交到线程池的任务(父任务)依赖于同一个线程池中的子任务的执行结果时,就可能发生死锁。 解决方案: 隔离原则,准备多个线程池
有线程池参数设置的经验吗?
核心线程数(corePoolSize)设置的经验:
- CPU密集型:corePoolSize = CPU核数 + 1(避免过多线程竞争CPU)
- IO密集型:corePoolSize = CPU核数 x 2(或更高,具体看IO等待时间)
fork/join 池
原文地址:JUC
本文持续更新,最新内容及相关资料请访问原文。