init
This commit is contained in:
@@ -0,0 +1,647 @@
|
||||
---
|
||||
title: JUC笔记
|
||||
date: 2024-08-25
|
||||
tags: [JUC]
|
||||
---
|
||||
|
||||
|
||||
- JUC:java.util.concurrent 并发编程
|
||||
|
||||

|
||||
|
||||
## 常见线程安全类
|
||||
|
||||
- String
|
||||
- Integer
|
||||
- StringBuffer
|
||||
- Random
|
||||
- Vector
|
||||
- Hashtable
|
||||
- java.util.concurrent包下的类(juc)
|
||||
|
||||
这里说它们是线程安全的是指,**多个线程调用它们同一个实例**的某个方法时,是线程安全的。但是自己手动组合方法是线程不安全的
|
||||
|
||||
> [!important] String、Integer是不可变线程,因为值是不可修改
|
||||
|
||||
### 效率问题
|
||||
|
||||
1. 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用
|
||||
cpu ,不至于一个线程总占用 cpu,别的线程没法干活
|
||||
2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
|
||||
有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任
|
||||
务都能拆分(参考后文的【阿姆达尔定律】)
|
||||
也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
|
||||
3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一
|
||||
直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
|
||||
|
||||
> 注意
|
||||
> 需要在多核 cpu 才能提高效率,单核仍然时是轮流执行
|
||||
|
||||
### 原子性
|
||||
|
||||
- 原子操作可以保证原子性,加锁本质是保证了多线程修改操作为原子操作
|
||||
- 原子操作:一个操作或者多个操作组合在一起,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。特点:不可打断、一致性
|
||||
|
||||
- 两个线程对一个变量进行修改,产生线程问题就是因为修改不是原子操作
|
||||
|
||||
例如对于 i++、i-- 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
|
||||
|
||||
```Java
|
||||
//i++操作
|
||||
getstatic i // 获取静态变量i的值
|
||||
iconst_1 // 准备常量1
|
||||
iadd // 自增
|
||||
putstatic i // 将修改后的值存入静态变量i
|
||||
//i--操作
|
||||
getstatic i // 获取静态变量i的值
|
||||
iconst_1 // 准备常量1
|
||||
isub // 自减
|
||||
putstatic i // 将修改后的值存入静态变量i
|
||||
```
|
||||
|
||||
多线程,指令交错执行
|
||||
|
||||
```Java
|
||||
// 假设i的初始值为0
|
||||
getstatic i // 线程2-获取静态变量i的值 线程内i=0
|
||||
|
||||
getstatic i // 线程1-获取静态变量i的值 线程内i=0
|
||||
iconst_1 // 线程1-准备常量1
|
||||
iadd // 线程1-自增 线程内i=1
|
||||
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
|
||||
iconst_1 // 线程2-准备常量1
|
||||
isub // 线程2-自减 线程内i=-1
|
||||
|
||||
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
|
||||
```
|
||||
|
||||
|
||||
## synchronized原理
|
||||
|
||||
### Monitor 概念
|
||||
|
||||
- Java对象头:内存结构
|
||||
- 普通对象
|
||||
|
||||
```Java
|
||||
|--------------------------------------------------------------|
|
||||
| Object Header (64 bits) |
|
||||
|------------------------------------|-------------------------|
|
||||
| Mark Word (32 bits) | Klass Word (32 bits) |
|
||||
|------------------------------------|-------------------------|
|
||||
```
|
||||
|
||||
- 数组对象
|
||||
|
||||
```Java
|
||||
|---------------------------------------------------------------------------------|
|
||||
| Object Header (96 bits) |
|
||||
|--------------------------------|-----------------------|------------------------|
|
||||
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|
||||
|--------------------------------|-----------------------|------------------------|
|
||||
```
|
||||
|
||||
- 其中 Mark Word 结构为
|
||||
|
||||
```Java
|
||||
|-------------------------------------------------------|--------------------|
|
||||
| Mark Word (32 bits) | State |
|
||||
|-------------------------------------------------------|--------------------|
|
||||
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|
||||
|-------------------------------------------------------|--------------------|
|
||||
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|
||||
|-------------------------------------------------------|--------------------|
|
||||
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|
||||
|-------------------------------------------------------|--------------------|
|
||||
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|
||||
|-------------------------------------------------------|--------------------|
|
||||
| | 11 | Marked for GC |
|
||||
|-------------------------------------------------------|--------------------|
|
||||
```
|
||||
|
||||
```Java
|
||||
|--------------------------------------------------------------------|--------------------|
|
||||
| Mark Word (64 bits) | State |
|
||||
|--------------------------------------------------------------------|--------------------|
|
||||
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|
||||
|--------------------------------------------------------------------|--------------------|
|
||||
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|
||||
|--------------------------------------------------------------------|--------------------|
|
||||
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|
||||
|--------------------------------------------------------------------|--------------------|
|
||||
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|
||||
|--------------------------------------------------------------------|--------------------|
|
||||
| | 11 | Marked for GC |
|
||||
|--------------------------------------------------------------------|--------------------|
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
- Monitor锁,记录了持有对象,阻塞线程,等待线程,Monitor是==操作系统提供的==,由c++实现的,每个锁对象关联一个Monitor锁(**锁对象保存了Monitor的地址**)
|
||||
|
||||

|
||||
|
||||
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
|
||||
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
|
||||
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
|
||||
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
|
||||
- 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(==重量级==)之后,该**对象头**的Mark Word 中就被设置指向 Monitor 对象的指针
|
||||
|
||||

|
||||
|
||||
|
||||
**synchronized的3种状态**
|
||||
|
||||
- 线程与对象绑定,是在线程中使用对象锁,而不是调用对象
|
||||
|
||||
### 偏向锁
|
||||
|
||||
- 偏向锁从 Java 6 引入,在 Java 15 被废弃,Java 16-17 默认关闭,最终在 Java 18 中被完全移除
|
||||
- 17中,不使用锁就是默认状态,使用就是轻量锁
|
||||
- **默认是偏向锁**(对象头后3位:101),解决锁重入问题,即只有一个线程多次获取(已经获取)锁对象的情况
|
||||
- 锁升级
|
||||
- 当调用**hashcode**方法时,由于偏向锁代替了hash值的位置,对象头会恢复hash值正常,所以将锁升级为轻量级锁
|
||||
- 当多个线程使用锁时(以该对象为锁,错开使用,无竞争),锁期间为轻量锁
|
||||
- 调用**wait/notify**时,这是重量级锁的方法
|
||||
- 批量重偏向:当同一个类的**多个对象频繁地发生重偏向时**(重偏向阈值:20),JVM 将超出阈值的对象==偏向锁重置==,而不是升级为轻量锁
|
||||
|
||||
- 详解
|
||||
- 当锁对象与其他线程绑定,此时锁变为轻量级锁(00),解除时,变为初始状态(001),当多个相同类的对象修改绑定(集合中的元素),从第20个开始,对象进行重偏向(101),将与新的线程绑定
|
||||
- 101(偏向锁) ——> 001(无状态) 101(偏向锁) ——> 101(偏向锁)
|
||||
|
||||
> [!important] 总结:批量重偏向会以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。
|
||||
|
||||
- 批量撤销:当一个类的所有对象进行了40次重偏向时,那么这个类下的对象再偏向、创建都会赋默认状态(001)
|
||||
|
||||
### 轻量级锁
|
||||
|
||||
- 高版本jdk默认是轻量级锁(对象头后2位:00),当一个对象锁**没有竞争**,那么这个锁就是轻量级锁,锁被占用(发生竞争)升级为重量级锁
|
||||
|
||||
- 详情
|
||||
- 在即将开始执行同步代码块中的内容时,会首先检查对象的Mark Word,查看锁对象是否被其他线程占用,如果没有任何线程占用,那么会在当前线程中所处的栈帧中建立一个名为锁记录(Lock Record)的空间,用于复制并存储对象目前的Mark Word信息(官方称为Displaced Mark Word)。接着,虚拟机将使用**CAS操作**将对象的Mark Word更新为轻量级锁状态(数据结构变为指向Lock Record的指针,指向的是当前的栈帧)
|
||||
- 如果CAS操作失败了的话,那么说明可能这时有线程已经进入这个同步代码块了,这时虚拟机会再次检查对象的Mark Word,是否指向当前线程的栈帧,如果是,说明不是其他线程,而是当前线程已经有了这个对象的锁,直接放心大胆进同步代码块即可。如果不是,那确实是被其他线程占用了。
|
||||
- 这时,轻量级锁一开始的想法就是错的(这时有对象在竞争资源,已经赌输了),所以说只能将锁膨胀为重量级锁,按照重量级锁的操作执行(注意锁的膨胀是不可逆的)
|
||||
|
||||

|
||||
|
||||
### 重量级锁
|
||||
|
||||
- 当对象锁,发生竞争时,对象锁要关联一个Monitor对象,管理锁的状态
|
||||
|
||||
**自旋优化**
|
||||
|
||||
- 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。说白了就是,等一下
|
||||
|
||||
- 详情
|
||||
|
||||
| | | |
|
||||
|---|---|---|
|
||||
|线程 1 (core 1 上)|对象 Mark|线程 2 (core 2 上)|
|
||||
|-|10(重量锁)|-|
|
||||
|访问同步块,获取 monitor|10(重量锁)|-|
|
||||
|成功(加锁)|10(重量锁)|-|
|
||||
|执行同步块|10(重量锁)|-|
|
||||
|执行同步块|10(重量锁)|访问同步块,获取 monitor|
|
||||
|执行同步块|10(重量锁)|自旋重试|
|
||||
|执行完毕|10(重量锁)|自旋重试|
|
||||
|成功(解锁)|01(无锁)|自旋重试|
|
||||
|-|10(重量锁)|成功(加锁)|
|
||||
|-|10(重量锁)|执行同步块|
|
||||
|-|||
|
||||
|
||||
|
||||
## 保护性设计模式
|
||||
|
||||
**join原理**
|
||||
|
||||

|
||||
|
||||
- 当线程结束时,会调用notifyAll方法,释放资源和锁
|
||||
|
||||
### park&unpark
|
||||
|
||||
- 基本使用:它们是 `**LockSupport**` 类中的**静态方法**
|
||||
|
||||
```Java
|
||||
// 暂停当前线程
|
||||
LockSupport.park();
|
||||
// 恢复某个线程的运行
|
||||
LockSupport.unpark(暂停线程对象)
|
||||
```
|
||||
|
||||
- 正常:先 `park` 再 `unpark` ,但是,先`unpark`再`park`也可以,不过`park`将暂停不了
|
||||
- 特点:
|
||||
- 与 `Object` 的 `wait` & `notify` 相比`wait`,`notify` 和 `notifyAll` 必须配合 `Object Monitor` 一起使用,而 `park`,`unpark` 不必
|
||||
- `park` & `unpark` 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【**精确**】
|
||||
- `park` & `unpark` 可以先 `unpark`,而 `wait` & `notify` 不能先 `notify`
|
||||
|
||||
- **原理**
|
||||
|
||||
- 具体是靠c/c++实现,每个线程都有自己的一个Parker对象,由三部分组成`_counter`、`_cond`和`_mutex`
|
||||
- `_counter` :计数,1:表示线程继续,0:表示线程暂停
|
||||
- `_cond` : 条件变量,当线程暂停时存储在这里
|
||||
- `_mutex` : 互斥锁
|
||||
|
||||
- 小故事理解
|
||||
- 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。`_counter`就好比背包中的备用干粮(0 为耗尽,1 为充足)
|
||||
- 调用 `park`就是要看需不需要停下来歇息
|
||||
- 如果备用干粮耗尽,那么钻进帐篷歇息
|
||||
- 如果备用干粮充足,那么不需停留,继续前进
|
||||
- 调用`unpark`,就好比令干粮充足
|
||||
- 如果这时线程还在帐篷,就唤醒让他继续前进
|
||||
- 如果这时线程还在运行,那么下次他调用 `park`时,仅是消耗掉备用干粮,不需停留继续前进因为背包空间有限,多次调用 `unpark`仅会补充一份备用干粮
|
||||
|
||||

|
||||
|
||||
park方法
|
||||
|
||||
- 当线程调用`**park**`方法,检查`_counter`的值
|
||||
- 为0,则线程获取`_mutex`互斥锁,进入`_cond`条件变量,阻塞线程,设置`_counter=0`
|
||||
- 为1,则线程无需阻塞,继续运行,设置`_counter=0`
|
||||
|
||||

|
||||
|
||||
unpark方法
|
||||
|
||||
- 当线程调用`**unpark**` 方法,将`_counter=1` ,并尝试唤醒 `_cond`条件变量中的线程
|
||||
- `_cond` 中有线程,则恢复运行,设置 `_counter=0`
|
||||
- `_cond` 中无线程,`_counter` 值不变
|
||||
|
||||
- 总结:park方法,一定会将`_cond` 值设置为0,unpark方法,无阻塞线程时,才能将`_cond` 值设置为1
|
||||
|
||||
## 活跃性问题
|
||||
|
||||
### 死锁
|
||||
|
||||
- 一个线程需要同时获取多把锁,这时就容易发生死锁
|
||||
- 解决方式
|
||||
|
||||
- 按序加锁
|
||||
|
||||
- 超时释放锁:使用ReentrantLock
|
||||
|
||||
- 使用工具检测是否死锁
|
||||
|
||||
- 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
|
||||
|
||||
|
||||
|
||||
|
||||
### 活锁
|
||||
|
||||
- 活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
|
||||
|
||||
### 哲学家就餐问题
|
||||
|
||||
- 有五位哲学家,围坐在圆桌旁。他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。如果筷子被身边的人拿着,自己就得等待
|
||||
|
||||

|
||||
|
||||
|
||||
## ReentrantLock
|
||||
|
||||
- 可重入锁(ReentrantLock) 比synchronized更灵活,体现在可以不用包裹整个代码块中
|
||||
|
||||
```Java
|
||||
synchronized(){
|
||||
if(){
|
||||
//内容
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```Java
|
||||
lock.lock();
|
||||
if(){
|
||||
//内容
|
||||
lock.unlock();
|
||||
}
|
||||
```
|
||||
|
||||
- 可重入锁(嵌套自己🔒)
|
||||
- `lock.lock()`
|
||||
- **可打断**(阻塞状态)
|
||||
- 使用`lock.lockInterruptibly()` ,允许线程在等待获取锁的过程(阻塞状态)中被打断,普通锁无法打断,打断后报异常
|
||||
- 当线程获取`lockInterruptibly`锁后,则与`lock`方法效果一样,此时其他线程调用`thread.interrupt()`方法,无法中断线程
|
||||
- **锁超时**(谦让锁)
|
||||
- `lock.tryLock()` ,返回布尔值,表示是否获取到锁
|
||||
- `lock.tryLock(long timeout, TimeUnit unit)` ,返回布尔值,表示是否获取到锁,可打断
|
||||
- 公平锁
|
||||
- 默认是不公平锁,创建实例时,设置为公平锁(true)
|
||||
- 一般不设置,影响性能
|
||||
- **条件变量**(线程通信)
|
||||
- 一个ReentrantLock实例可以创建**多个条件变量**,条件变量相等于`WaitSet`区域,使线程等待更加细分,不用多个线程在一个区域
|
||||
- 使用:要先获得锁,在对应条件变量中等待
|
||||
|
||||
```Java
|
||||
//创建条件变量(休息室)
|
||||
Condition condition1 = lock.newCondition();
|
||||
Condition condition2 = lock.newCondition();
|
||||
Condition condition3 = lock.newCondition();
|
||||
//线程首先获取锁
|
||||
lock.lock();
|
||||
//进入等待状态,也可以设置时间
|
||||
condition1.await();
|
||||
//叫醒等待线程
|
||||
condition1.signal();
|
||||
condition1.signalAll();
|
||||
```
|
||||
|
||||
- wait/notify→await/signal
|
||||
|
||||
## 内存
|
||||
|
||||
### java内存模型
|
||||
|
||||
- 原理详解
|
||||
|
||||
因为 java是对硬件虚拟化,所有**对 ,以及编译器、CPU指令重排机制这些提示效率的方式进行了实现**,所以定义了一套规范,抽象了线程和主内存之间的关系,规定了从 Java 源代码到 CPU 可执行指令,的这个转化过程要遵守哪些和并发相关的原则和规范,主要目的是为了简化多线程编程,增强程序可移植性的
|
||||
|
||||
- JMM(java Memory Model),JMM 是一种规范,定义了 Java 程序中各个变量(包括实例字段、静态字段和数组元素)的访问方式。它通过抽象的主内存(堆)和工作内存(栈)模型,确保线程间的内存**可见性**和**有序性**
|
||||
- 了解java内存规范,有利于开发者编写**线程安全的程序**
|
||||
- JMM 体现在以下几个方面
|
||||
- 原子性 - 保证指令不会受到**线程上下文切**换的影响
|
||||
- 可见性 - 保证指令不会受 **cpu 缓存**的影响
|
||||
- 有序性 - 保证指令不会受 **cpu 指令并行优化**的影响
|
||||
|
||||
### 可见性
|
||||
|
||||
- 现象:当一个线程对公共变量进行修改,另一个线程中数据无法同步(线程不安全)
|
||||
|
||||
```Java
|
||||
static boolean run = true;
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
Thread t = new Thread(()->{
|
||||
while(run){
|
||||
// ....
|
||||
}
|
||||
});
|
||||
t.start();
|
||||
sleep(1);
|
||||
run = false; // 线程t不会如预想的停下来
|
||||
}
|
||||
```
|
||||
|
||||
- 循环中有 `System.out.println()`输出语句也可以退出线程,因为输出语句中用到了锁
|
||||
|
||||
- 产生原因:t线程频繁读取run的值,**JIT编译器将run的值写入t的工作内存中的缓冲区(**模仿CPU高速缓冲区),其他线程修改的是主内存的值,所以导致数据不一致
|
||||
|
||||

|
||||
|
||||
- 解决方式:**[[JUC]]、s**ynchronized
|
||||
- 使用场景:仅用在**一个写线程,多个读线程**的情况
|
||||
|
||||
> [!important] synchronized 语句块既可以**保证代码块的原子性**,也同时**保证代码块内变量的可见性**。但缺点是synchronized 是属于重量级操作,性能相对更低
|
||||
>
|
||||
> - 使用synchronized锁,JVM 会插入内存屏障指令。这些内存屏障会**刷新本地线程的缓存**,确保线程可以**看到共享变量的最新值**
|
||||
|
||||
### 有序性
|
||||
|
||||
- Java 源代码会经历 **编译器优化重排 —> 指令并行重排 —> 内存系统重排** 的过程,最终才变成操作系统可执行的指令序列。**指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致** ,所以在多线程下,指令重排序可能会导致一些问题。
|
||||
|
||||
- 现象:**双重检查实现对象单例**无法保证单例
|
||||
|
||||
```Java
|
||||
public class Singleton {
|
||||
private volatile static Singleton uniqueInstance;
|
||||
private Singleton() {}
|
||||
public static Singleton getUniqueInstance() {
|
||||
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
|
||||
if (uniqueInstance == null) {
|
||||
//类对象加锁
|
||||
synchronized (Singleton.class) {
|
||||
if (uniqueInstance == null) {
|
||||
uniqueInstance = new Singleton();
|
||||
}
|
||||
}
|
||||
}
|
||||
return uniqueInstance;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
无法保证单例的原因是这条语句`**uniqueInstance = new Singleton();**` ,也就是new对象的过程,
|
||||
|
||||
- new对象对应的指令:1、创建对象 2、调用构造方法(初始化)3、赋值给静态变量
|
||||
|
||||
```Java
|
||||
17: new #3 // class cn/itcast/n5/Singleton
|
||||
20: dup
|
||||
21: invokespecial #4 // Method "<init>":()V
|
||||
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
|
||||
|
||||
//堆内创建对象,返回地址存入操作数栈
|
||||
//复制一份地址,存入操作数栈
|
||||
//调用构造方法,地址出栈
|
||||
//将地址赋给静态变量,地址出栈
|
||||
```
|
||||
|
||||
- 由于指令重排,导致先3,后2,当其他线程进入方法时,`if (uniqueInstance == null)` 不满足,因为此时静态变量被赋值为未初始化对象,所以**其他线程可能会获得未初始化的对象**,使用该对象就可能触发空指针异常
|
||||
|
||||
- 所以,当在多线程环境下多数据的修改会出现难以预料的情况!
|
||||
- 解决方式:**[[JUC]]、s**ynchronized
|
||||
|
||||
### **volatile**
|
||||
|
||||
- 是一个**修饰符**,修饰成员变量(多线程环境),它可以避免线程访问时,访问工作内存(缓存)中的值,==**直接访问主内存中的值**==**,**保证了==**可见性**==,它还可以==**禁止指令重排**==,保证了==**有序性**==
|
||||
- volatile 的底层实现原理是**内存屏障**,Memory Barrier(Memory Fence)
|
||||
- 对 volatile 变量的(赋值)写指令后会加入写屏障
|
||||
- 写屏障(sfence)保证在==**写屏障之前的指令不进行重排**==**,并且**==**对共享变量的改动,都同步到主存中**==
|
||||
- 对 volatile 变量的(读取)读指令前会加入读屏障
|
||||
- 读屏障(lfence)保证在==**读屏障之后的指令不进行重排**==**,并且**==**对共享变量的读取,加载的主存中最新数据**==
|
||||
|
||||
> [!important] 重要的是要注意,synchronized 并不阻止块内部的指令重排,volatile 提供了更严格的重排序规则,
|
||||
>
|
||||
> **它不允许 volatile 变量的读写与其他内存操作重排**,synchronized 的重排序规则相对没那么严格
|
||||
|
||||
### Happens-before
|
||||
|
||||
- 是 Java 内存模型(JMM)中的一个核心**概念**,用于定义操作之间的内存可见性保证。它是一种偏序关系,用来**描述程序中不同操作的执行顺序和可见性**
|
||||
- 定义:如果一个操作 A happens-before 另一个操作 B,那么 ==**A 的结果对 B 是可见的**==,且 A 的执行顺序排在 B 之前
|
||||
- 主要作用:
|
||||
- 确保内存可见性:保证一个线程的写操作对其他线程是可见的。
|
||||
- 防止重排序:限制编译器和处理器对指令进行重排序的能力
|
||||
|
||||
- 常见情况
|
||||
|
||||
a. 程序顺序规则:
|
||||
|
||||
在同一个线程中,按照程序的顺序,前面的操作 happens-before 后面的操作。
|
||||
|
||||
b. 监视器锁规则:
|
||||
|
||||
一个锁的解锁 happens-before 于后续对这个锁的加锁。
|
||||
|
||||
c. volatile 变量规则:
|
||||
|
||||
对一个 volatile 变量的写操作 happens-before 于后续对这个变量的读操作。
|
||||
|
||||
d. 线程启动规则:
|
||||
|
||||
Thread 对象的 start() 方法 happens-before 于这个线程的每一个动作。
|
||||
|
||||
e. 线程终止规则:
|
||||
|
||||
线程中的所有操作都 happens-before 于其他线程检测到这个线程已经终止。
|
||||
|
||||
f. 中断规则:
|
||||
|
||||
一个线程调用另一个线程的 interrupt() 方法 happens-before 于被中断线程检测到中断事件的发生。
|
||||
|
||||
g. 终结器规则:
|
||||
|
||||
一个对象的初始化完成 happens-before 于它的 finalize() 方法的开始。
|
||||
|
||||
h. 传递性:
|
||||
|
||||
如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
|
||||
|
||||
|
||||
## 无锁
|
||||
|
||||
### CAS
|
||||
|
||||
- 使用`CAS`机制解决并发修改问题
|
||||
|
||||
```Java
|
||||
//修改共享数据
|
||||
AtomicInteger balance;//封装的共享数据
|
||||
while(true) {
|
||||
// 比如拿到了旧值 1000
|
||||
int prev = balance.get();
|
||||
// 在这个基础上 1000-10 = 990
|
||||
int next = prev - amount;
|
||||
/*
|
||||
compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值- 不一致了,next 作废,返回 false 表示失败
|
||||
比如,别的线程已经做了减法,当前值已经被减成了 990
|
||||
那么本线程的这次 990 就作废了,进入 while 下次循环重试- 一致,以 next 设置为新值,返回 true 表示成功
|
||||
*/
|
||||
if (balance.compareAndSet(prev, next)) break;
|
||||
}
|
||||
```
|
||||
|
||||
- Compare And Swap,它是一种用于实现多线程环境下的**无锁并发**编程技术,是基于 的思想,不怕共享变量被修改,保障每一次修改是成功的(原子修改),无锁并发、无阻塞并发,
|
||||
|
||||
- 底层 ,封装数据对应的变量,由volatitle修饰
|
||||
- CAS 的底层是`lock cmpxchg` 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性
|
||||
|
||||
- 效率问题:因为线程需要一直运行(只有就绪、运行状态),所以线程数小于cpu核心数时才能提升效率
|
||||
|
||||
- 使用
|
||||
|
||||
**原子整数**
|
||||
|
||||
- 对基本数据类型进行了封装,比如AtomicBoolean、AtomicInteger、AtomicLong使用类似,以AtomicInteger为例
|
||||
- 构造方法:
|
||||
- 常用方法:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|方法名|描述|
|
||||
||获取值|
|
||||
||修改数据,返回是否修改成功,可能修改失败|
|
||||
||返回自增后的值/返回值再自增|
|
||||
|||
|
||||
||返回增加data后的值/返回值再增加data|
|
||||
|||
|
||||
||修改数据|
|
||||
|
||||
|
||||
**原子引用**
|
||||
|
||||
- 对引用对象进行封装,如AtomicReference、AtomicMarkableReference、AtomicStampedReference,每次修改对象时,**更新的实际上是对象的引用地址**
|
||||
|
||||
上同
|
||||
|
||||
AtomicReference的·使用·
|
||||
|
||||
- ABA问题:线程无法感知数据被修改(指修改完,值不变),可以使用版本号记录
|
||||
|
||||
AtomicStampedReference基于**版本号**的方法,每次修改需要修改版本号,告知共享数据是否被修改
|
||||
|
||||
AtomicMarkableReference维护一个**布尔值**表示状态,只关心数据是否被修改而不看次数
|
||||
|
||||
|
||||
**原子数组**
|
||||
|
||||
- 因为原子引用每次修改的都是地址,而数组的修改是对内容的修改,所以推出原子数组AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
字段更新器
|
||||
|
||||
原子累加器
|
||||
|
||||
### 🚀 什么是 CAS?
|
||||
|
||||
**Compare And Swap** 是一种原子操作,用于在多线程环境中安全地更新变量,避免使用互斥锁(`synchronized` 或 `mutex`)造成的性能瓶颈。
|
||||
|
||||
它的基本思路就是:
|
||||
|
||||
> “我想把一个值改成新值,但前提是它现在还是旧值。”
|
||||
|
||||
---
|
||||
|
||||
### 🔧 CAS 的原理
|
||||
|
||||
假设你有一个变量 `V`,你想把它从旧值 `A` 改成新值 `B`,CAS 会做这样的操作:
|
||||
|
||||
```Plain
|
||||
if (V == A) {
|
||||
V = B;
|
||||
return true; // 修改成功
|
||||
} else {
|
||||
return false; // 修改失败,有别的线程动过它
|
||||
}
|
||||
```
|
||||
|
||||
这个操作是 **原子的**(不会被中断),通常由硬件指令(如 x86 的 `CMPXCHG`)直接支持。
|
||||
|
||||
---
|
||||
|
||||
### 📦 应用场景
|
||||
|
||||
- Java 中的 `AtomicInteger`, `AtomicReference` 等类底层就是用 CAS 实现的。
|
||||
- 实现无锁数据结构(如无锁队列、无锁栈)
|
||||
- 避免使用传统的锁,提高并发性能
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ CAS 的问题
|
||||
|
||||
1. **ABA 问题**:
|
||||
- 如果变量从 A 改成 B,然后又改回 A,CAS 认为它没变,其实发生了两次变化。
|
||||
- 解决方法:使用 **版本号** 或 **带时间戳的引用**,比如 Java 的 `AtomicStampedReference`。
|
||||
2. **自旋问题**:
|
||||
- 如果一直 CAS 失败(比如多个线程争抢同一个变量),会反复尝试,造成高 CPU 占用。
|
||||
3. **只能操作一个变量**:
|
||||
- 无法同时原子地更新多个变量,除非封装成一个整体结构。
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 悲观锁、乐观锁
|
||||
|
||||
### 悲观锁
|
||||
|
||||
- 悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。
|
||||
- 像 Java 中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。
|
||||
|
||||
### 乐观锁
|
||||
|
||||
- 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)
|
||||
- 在 Java 中`java.util.concurrent.atomic`包下面的原子变量类(比如`AtomicInteger`、`LongAdder`)就是使用了乐观锁的一种实现方式 **CAS** 实现的。
|
||||
Reference in New Issue
Block a user