☰
JUC:Java 并发编程学习笔记
2026/10/20 4:37:17 网站建设 项目流程

并发压测工具jcstress

概念进程(Process)线程(Thread)
本质程序的一次执行实例(Execution Instance)进程内的一个执行流(Execution Flow)
是什么操作系统分配资源的基本单位操作系统进行调度的基本单位

Java 底层会调用 pthread_create 来创建线程,所以本质上 java 程序创建的线程,就是和操作系统线程是一样的,是 1 对 1 的线程模型。

并发和并行

创建新线程:

Context Switch

常用方法

Sleep 让线程从running 到timed wating (阻塞)状态,释放 CPU,但不会释放持有的锁 醒来过不一定立即执行

yield方法 hint调度器,当前线程愿意放弃其当前的 CPU 时间片。状态从RUNNING(运行中) ->RUNNABLE(就绪) 。调度器完全可以忽略这个提示,让该线程立即再次运行

设置线程优先级 cpu忙的时候优先级高的时间片分的多 闲的时候优先级没什么用 最终 都是取决于os的任务调度器

join/join(long) 等待线程运行结束 可用于实现同步等待

中断线程

  1. 通过标志位判断需要正确使用volatile关键字
  2. 对目标线程调用interrupt()方法 如果该线程正在执行低级别的可中断方法(如Thread.sleep()、Thread.join()或Object.wait()),则会解除阻塞并抛出InterruptedException异常 →抛异常 + 清标志

非阻塞状态(运行、锁竞争) →不抛异常 + 保留打断标志,相当于“未被打断,依旧正常执行”

设计模式——两阶段终止模式

方案一:利用interrupt方法

  1. 第一阶段:通知线程准备停止(设置停止标志或发送中断信号)。
  2. 第二阶段:线程感知到终止请求后,完成剩余任务并释放资源,最终安全退出。 方案二:volatile变量替代打断标记
Park方法

但打断标记true的时候park失效

守护线程

线程状态

状态转换

BLOCKED和WAITING两个状态最大的区别有两个:

BLOCKED和WAITING都是用放弃 CPU的方式来换取不消耗资源,但代价是高昂的切换成本。

共享模式之管程(monitor)

临界区(对共享资源有读有写)的竞态条件

互斥 是解决“竞态条件”的手段(上下文切换是trigger),它通过保证“原子性”来确保并发安全 同步 是一个比“互斥”更宽泛的概念。它的目的就是协调多个线程的执行顺序或时机

线程安全

成员变量和静态变量 如果被共享了且有写操作 非线程安全 局部变量 是线程安全的 ,但如果是引用的对象需要判断是否发生引用逃逸

局部变量的引用本身是线程私有的,但被引用对象是否线程安全,要看对象是否逃逸出方法作用域

设计线程安全类时,禁止子类通过继承修改关键同步逻辑,通过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计数 当作“通行证”

  1. 每个线程都有且仅有一个关联的“通行证”。
  2. 这个通行证的“数量”非 0 即 1
  3. unpark(Thread t)的作用是给目标线程t“发放”一个通行证
  4. 调用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

同步模式——顺序控制

使用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 来保证其方法的原子性

  1. 基本类型AtomicInteger AtomicLong AtomicBoolean

  2. 引用类型AtomicReference

    在多线程环境下,修改一个对象的引用(例如User user = new User(...))本身是原子的,但是“检查后执行”(Check-Then-Act)的操作不是原子的。例如:if (user == expected) user = newOne;这段逻辑如果不加锁,在并发下是不安全的。 而AtomicReference利用 CPU 提供的 CAS指令,保证原子性,而无需使用重量级的synchronized锁。

AtomicStampedReference(带版本号/时间戳的原子引用)解决了ABA问题

  1. 数组类型使用原子的方式更新数组里的某个元素

  2. 对象的属性修改类型原子类/字段更新器它只是一个工具/视窗。它本身不存储数据FieldUpdater在底层利用了Unsafe.compareAndSwapObject指令。CAS 操作要想安全,前提是多线程必须能看到变量的“最新值”(即内存可见性)。 由于FieldUpdater无法改变字段原本的定义,它只能强制要求你在定义字段时就必须加上volatile

原子累加器

LongAdderLongAdder的思路是:既然一个变量(base)大家抢着更新太慢,那我就把这个变量拆成很多份(cell[]),让大家分头去更新,最后再加起来

缓存行伪共享 为防止缓存行失效导致效率降低:

CAS可能的问题

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。

循环时间长开销大 CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销

Unsafe类

共享模式之不可变

不可变类设计

天生线程安全 可以作为 HashMap 的 Key 原子性保障

享元模式

最小化内存使用

体现例子:包装类、String 串池、连接池

final 原理

final通过禁止构造函数内的指令重排序(写屏障),保证了对象在多线程环境下的初始化安全性

线程池

线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗。

线程池

处理流程

线程池状态

用一个AtomicInteger ctl 描述 前三bits是状态 后29bits是线程数量

构造方法

7个参数:

四种预置的拒绝策略:

提交任务

方法所属接口参数 / 返回值阻塞性异常处理机制 (核心)适用场景
executeExecutorRunnable

🔁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

异步模式之工作线程

池化死锁:

根本原因:当提交到线程池的任务(父任务)依赖于同一个线程池中的子任务的执行结果时,就可能发生死锁。 解决方案: 隔离原则,准备多个线程池

有线程池参数设置的经验吗?

核心线程数(corePoolSize)设置的经验:

fork/join 池


原文地址:JUC

本文持续更新,最新内容及相关资料请访问原文。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询