前置复习
[[../../IT/Java SE/4.面对对象编程/3.应用程序开发/多线程|../4.面对对象编程/3.应用程序开发/多线程]]
[[../../IT/Java SE/4.面对对象编程/3.应用程序开发/多线程#实现 Callable 接口|Java SE/3.应用程序开发/多线程 > 实现 Callable 接口]]
[[../../IT/Java SE/4.面对对象编程/3.应用程序开发/多线程#Thread 常用方法|THREAD常用方法]]
本质都是 Thread
类调用 start()
,start()
方法来启动新线程
- 继承
Thread
类,重写run
方法 - 传给
Thread
类runnable
实现类,重写run
方法 - 创建
Callable(实现call方法)-->传给FutureTask-->传给Thread
/也可直接call.call()
:直接在当前线程中执行Callable
任务(同步执行),这只是普通的方法调用,在当前线程中同步执行,不是多线程(解决传统Runnable
接口无法返回值和抛出受检异常的问题。) Executors
创建线程池
CompletableFuture
并不直接创建线程,它是对线程池(通常是 ForkJoinPool.commonPool()
)的封装, 利用了 多线程(或者线程池)来实现异步编程。
当使用 CompletableFuture
的异步方法(例如 supplyAsync
, runAsync
)时,它会将任务提交给线程池执行
[[../../IT/Java SE/4.面对对象编程/3.应用程序开发/多线程#实现 Callable 接口|Java SE/3.应用程序开发/多线程 > 实现 Callable 接口]]
并发(concurrent)
同一
实体的多个事件,一个
处理器上同时处理多个任务
并行(parallel)
: 不同
实体的多个事件,多个处理器处理多个任务
并行后并发
管程
: Monitor
监视器,也就是平时说的 锁
JVM
靠 管程
来实现方法级的同步和方法内部一段指令序列的同步
用户线程
: 系统工作线程
守护线程
: 为其他线程服务,如 GC
isDaemon
判断线程是用户false
还是守护 true
CompletableFuture
CompletableFuture
并不直接创建线程,它是对线程池(通常是 ForkJoinPool.commonPool()
)的封装, 利用了 多线程(或者线程池)来实现异步编程。
CompletableFuture
是 Future
的增强版,减少阻塞和轮询,可以传入回调对象,当异步任务完成或者发生异常的时候,自动调用回调对象的回调方法
需求 : 多线程、能返回,能异步任务,普通线程无法满足
Future
接口
FutureTask
是 Future
接口的一个具体实现类,都代表一个异步计算的结果
futureTask
对于结果的获取相关问题
get
存在阻塞问题
isDone()
方法容易消耗 CPU 资源
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(() -> { Thread.sleep(2000); // 模拟一个耗时操作 return 42; })
上面的轮询方式并不优雅,还应该满足以下需求
- 完成能够主动告诉
- 能够将不同的异步结果组合
- 能够选择出执行最快的
CompletionStage
接口
CompletionFuture
是 CompletionStage
接口和 Future
接口的一个具体实现类,提供了更多的功能和操作方法来处理
异步计算任务。
- 异步任务
结束/出错
时,会自动回调某个对象的方法; - 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
runAsync
supplyAsync
不推荐使用 CompatableFuture
的默认空参创建,因为这样创建的 CompletableFuture
实例并没有指定具体的异步任务,可能会导致在后续使用时出现逻辑错误或异常。正确的做法是在创建 CompletableFuture
实例时传入具体的异步任务,以确保程序的正确性和稳定性
如果使用 runAsync
,再 get
则还是之前的阻塞式
runAsync
线程池
类似于 守护线程
会在 Main
线程结束后立刻结束,因此要确保 main
线程最后结束
title:CompletableFuture
`whenComplete`
```java
public static void main(String[] args) throws ExecutionException, InterruptedException
{
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try
{
CompletableFuture.supplyAsync(
() -> {
System.out.println(Thread.currentThread().getName() + "----come in");
int result = ThreadLocalRandom.current().nextInt(10);
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("-----1秒钟后出结果:" + result);
if(result > 2)
{
int i=10/0;
}
return result;
},threadPool)
//v是上面的返回值
.whenComplete((v,e) -> {
if (e == null) {
System.out.println("-----计算完成,更新系统UpdateValue:"+v);
}
}).exceptionally(e -> {
e.printStackTrace();
System.out.println("异常情况:"+e.getCause()+"\t"+e.getMessage());
return null;
});
System.out.println(Thread.currentThread().getName()+"线程先去忙其它任务");
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
//主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:暂停3秒钟线程
//try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
}
电商网站比价开说
[[../../IT/Java SE/新特性/JDK8-17新特性#2 函数式接口|../新特性/JDK8-17新特性 > 2 函数式接口]]
title:转换采用多线程案例,模拟查询
将`list`中存储的`List<NetMall>`取出转化成`List<CompletableFuture<String>>`再转换成`List<String>`
用到stream中的中间方法map
[[Java SE/新特性/Stream流#中间方法]]

将NetMall对象转换为一个CompletableFuture对象,并返回一个代表异步计算结果的CompletableFuture对象
不使用lambda的写法

```java
public static List<String> getPriceByCompletableFuture(List<NetMall> list,String productName)
{
return list.stream().map(netMall -> CompletableFuture.supplyAsync(() -> String.format(productName + " in %s price is %.2f",netMall.getNetMallName(),netMall.calcPrice(productName)))).collect(Collectors.toList())//终结方法,将第一次计算的结果收集到一个列表中
.stream()//再次调用stream()方法将列表转换为一个新的流
.map(s -> s.join())//接着,使用map()方法对流中的每个元素 s 进行转换操作
.collect(Collectors.toList());//将所有的对象收集到一个列表中
}
CompatableFuture
常用方法
runAsync
supplyAsync
get()
get()
title: 1 获得结果和触发计算

title:2对计算结果进行处理
`thenApply、handle` 两者作用相同`handle`可以处理异常



title:3对计算结果进行消费
`thenRun` 不需要前一步的结果,会使用之前的线程池
`thenRunAsync` 异步,会使用`ForkJoinPool`,不使用自定义线程池
`thenAccept`需要前一步结果,无返回值,它返回的是一个新的 `CompletableFuture<Void>`
`thenApply`需要前一步结果,有返回值
`thenRunAsync` 会使用ForkJoinPool,不使用自定义线程池

`join()` 方法会阻塞当前线程,等待异步任务执行完成。这样可以确保在需要获取异步任务结果的地方,能够获取到最终的结果

- thenRunAsync
```java
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {}).join());//null
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenAccept(r -> System.out.println(r)).join());//resultA和null,这个为thenAccept的返回结果
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenAccept(r -> System.out.println(r)));
//resultA和java.util.concurrent.CompletableFuture@76fb509a[Completed normally] 这个为thenAccept的返回结果 ,此时由于没有运行完没有结果
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenApply(r -> r + "resultB").join());//resultAresultB
title:4对计算速度进行选用
`applyToEither`
`A.applyToEither(B, c -> {
return c + " is winer";
});`
```java
CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
System.out.println("A come in");
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
return "playA";
});
CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
System.out.println("B come in");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
return "playB";
});
CompletableFuture<String> result = playA.applyToEither(playB, f -> {
return f + " is winer";
});
System.out.println(Thread.currentThread().getName()+"\t"+"-----: "+result.join());
title:5 对计算结果进行合并
`x.thenCombine(y,(x,y)->{})`
两个CompletionStage任务完成后,把两个completableFuture的结果一起交给`thenCombine`来处理
CompletableFuture<Integer> result = completableFuture1.thenCombine(completableFuture2, (x, y) -> {
System.out.println("-----开始两个结果合并");
return x + y;
});


锁
synchronized
方法 (锁this
)
一个对象里面如果有多个synchronized
方法, 只要一个线程去调用其中的一个synchronized
方法了,该对象其它的线程的synchronized
方法都只能等待
也就是同一对象,只能有唯一的一个线程去访问这些synchronized
方法
如果两个对象,则不是同一把锁,则各自调用各自的 synchronized
方法,不会有影响
static synchronized
方法 (锁类模板文件
)
会加类锁,所有实例都会被锁定
对于普通同步方法,锁的是当前例对象,通常指 this
, 具体的一部部手机, 所有的普通同步方法用的都是同一把锁——>实例对象本身
对于静态同步方法,锁的是当前类的Class
对象,如Phone.class
唯一的一个模板
synchronized
title:`synchronized` 同步代码块
字节码 `monitorenter/monitorexit` 来控制锁的进出
一个 `enter`, 两个`exit`
如果有异常,为一个 `enter`, 一个 `exit`

title:`synchronized` 普通同步方法

title: `synchronized` 静态同步方法

title:为什么每个对象都可以成为一个锁
线程执行要求先持有管程

每个对象都天生带一个对象监视器,每个被锁住的对象都和Monitor关联起来
其中`_owner`会指向持有锁的线程,谁持有,谁记录,谁释放,谁取消


ReentrantLock
公平和非公平锁 (默认)
公平: 多个线程按照申请锁的顺序来获取锁
非公平锁: 不按照申请锁顺序,可能后申请的线程比先申请的线程优先获取锁,在高并发的环境下,有可能造成优先级反转或锁饥饿状态
T
title:为什么 `ReentrantLock` 默认非公平锁
1. 线程切换开销
2. 非公平锁利用率高

title:公平非公平的选择
非公平: 吞吐量,节省时间
公平: 雨露均沾
可重入锁 (递归锁)
[[../Redis/Redis高级篇#可重用锁|JAVA微服务生态/Redis7/基础/Redis高级篇 > 可重用锁]]
死锁案例以及如何排除死锁
互斥条件/占用且等待/不可抢夺/循环等待
两个线程互相想要对方的锁
title:死锁排查
`jsp -l`
`jstack 进程编号`

或者使用图形化工具:如jconsole
小节
LockSupport
与线程中断
线程中断机制
interrupt/interrupted/isinterrupted
是协商机制,不是强制性,线程自行决定是否中断
interrupt
会设置中断状态为 True
, 并不能中断线程,只是充当一个 标志位
,需要自己手动代码配合,
如果线程本身处于 sleep,wait,join
状态中,别的线程调用 interrupt
设置为 true
, 会抛出异常 interruptedException
,异常会把 interrupt
状态清除,变为 false
, 因此需要在异常处理的地方再次调用来确保线程终止
interrupted
: 返回当前
线程的中断状态,然后再把当前的状态设置为false
也就是假如中断状态为 True
,调用 interrupted
返回 当前
线程的中断状态true
, 然后设置为false
异常interruptedException
发生后会把 interrupt
设置为 false
,会导致其他线程设置的 true 失效
title:在没有`interrupt`时,使用中断标识中断
在需要中断的线程不断监听中断状态
- `volatile`
`volatile`,当`volatile`为ture时中断线程,然后在另外一个线程更改状态

- `AtomicBoolean`

- `interrupt`
就是把对应线程的interrupt设置为True,自己搭配while进行break,可以达到上面代码类似的效果
底层都是通过 isInterrupted
实现
title:底层

LockSupport
LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语
线程等待唤醒机制
- 三种让线程等待和唤醒的方式
wait/notify
必须在同步代码块或同步方法中成对出现
title:`wait/notify`
先`wait`后`notify`
必须在同步代码块或同步方法中成对出现

[[../../IT/Java SE/4.面对对象编程/3.应用程序开发/多线程#new Condition|Java SE/3.应用程序开发/多线程 > new Condition]]
在这个示例中,awaitCondition
方法会使当前线程等待,直到 signalCondition
方法发出信号。
title:`Lock condition | await/signal`
先获取锁`Lock.lock()`
`condition.await();`
`condition.signal();`

Object和Condition
的限制是
- 必须先获得锁或
lock
- 必须先等待后再唤醒
LockSupport
解决了上面的限制
无需锁块
支持先唤醒后等待 (凭证设计)
先执行 unpark
会设置一个许可证,即使 park
在 unpark
后执行,已经有了许可证,直接进行放行,会消耗一个许可证
unpark
会增加一个许可证,不可以累计,最多只有一个,调用多次 unpark
也是只有一个许可证
title: LockSupport 中park unpark




Java 内存模型 JMM
JVM
屏蔽掉各种硬件操作系统内存访问差异, JVM
本身是抽象的概念,描述一组规范,规范定义了程序中 (尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的 原子性、可见性和有序性展开的
。
JVM 三大特性 (可见,原子,有序)
title:可见性
可见性:一旦改变所有线程可见
`java`中的普通共享变量没有保证可见性,也就是说主`内存的共享变量`没有和`共享变量`副本进行同步,此时就需要修改的数据同步到主内存过,且其他线程从主内存更新
主线程做出的修改也是修改变量副本,没有直接刷新到主内存,也需要后续刷新



- 原子性: 一个操作不可以中断,多线程下不能被其他线程干扰
- 有序性
代码从上到下,有序执行
title:有序性
数据相互没有依赖,执行效果一致可以重排


语句四不可能语句重拍到第一个
多线程发生原子之 happens-before
title:案例

解决,有两种方式



volatile
与 Java
内存模型
写 volatile
时, JMM 将该线程对应本地内存的共享变量值立刻刷新到主内存
读 volatile
时, JMM 将 该线程对应本地内存的共享变量值
设置为无效,直接从主内存读取共享变量
也就是 volatile
读写都是直接和主内存交互
内存屏障
volatile
底层实现通过内存屏障
内存屏障是一种屏障指令,它使得 CPU 或编译器对屏障指令的前后所发出的内存操作,执行一个排序的约束
,也叫内存栅栏或栅栏指令
内存屏障保证了 volatile
的可见性,只保证 volatile
变量
title:四大内存屏障插入策略分类

`LoadLoad xxyy`,第一个`xx`执行过,才能执行第二个`yy`
第一个操作和第二个操作代表代码中的先后顺序



读之后的操作不会重排到读之前
写之前的操作不会重排到写之后
`volatile写`:在每个`volatile写`操作前/后插入一个`StoreStotre/Storeload`屏障
`volatile读`在每个`volatile读`操作后插入一个`LoadLoad,LoadStore`屏障


title:案例
```java
volatile int v = 0;
int a = 0;
void example() {
v = a; // volatile 写
a = 1; // 普通写
}
在这个例子中,有两个操作:
1. `v = a;` 是一个 `volatile` 写操作。
2. `a = 1;` 是一个普通写操作。
根据表格中的规则:
1. `volatile` 写操作之后的普通写操作是可以重排的。
2. 但是 `volatile` 写操作之后的 `volatile` 写操作是不可以重排的。
所以在你的例子中,`v = a;` 和 `a = 1;` 之间是可以重排的。也就是说,可能会发生以下重排:
a = 1;
v = a;
这种情况下,`volatile` 关键字的可见性保证是针对 `volatile` 变量本身的,即对 `v` 的写操作对其他线程是可见的,但它不能保证普通变量 `a` 的写操作的可见性。也就是说,`volatile` 仅能确保对 `v` 的写操作是可见的,但是不能确保 `v = a` 和 `a = 1` 之间的顺序。
如果你想确保顺序性和可见性,可以使用更严格的同步机制,例如 `synchronized` 关键字或显式的锁(如 `ReentrantLock`),这将确保所有线程对这些变量的访问是有序和可见的。
volatile
特性
JVM
自身自能保证单个命令的原子性, 多个命令只能靠锁来保证
volatile
不能保证原子性,只能保证可见性
保证多个命令的原子性需要依靠 锁
,锁机制提供了可见性和原子性可以代替 volatile
volotile
不适合参与到依赖当前值的运算
可见性
title:volatile保证可见性

不加`volatile`程序不会停止,主线程做出的修改也是修改变量副本,没有刷新到主内存,也有可能刷到了主内存,但另一个线程使用的还是副本,没有从主内存获取最新值
title: `volatile`变量的读写过程



没有原子性
title:不保证原子性操作
只能保证可见性,从主内存直接读存数据,而多线程的操作同样可能出错
原子性需要`lock`
如i++


正确结果10000,测试8177
根据上面的变量读写过程



指令禁重排
volatile
禁止 写之前
的操作重排到 volatile
后 (这意味着在 volatile
写操作之前的所有写操作都已经 完成
并对其他线程可见),但写之后的操作可能会被重排,此时如果有影响则需要上锁
也就是说 volatile
读
操作 后
的操作不会重排到读前,写
操作 前
的操作一定已经完成 ,而 锁
可以直接避免所有指令重排
volatile
读 后
加 LoadStore
和 LoadLoad
volatile
写 前
加 StoreStore
, 后加 Storeload
读之后的操作不会重排到读之前
写之前的操作不会重排到写之后
title:`volatile`禁重排







写之前的操作重排到 volatile
后 (这意味着在 volatile
写操作之前的所有写操作都已经完成
并对其他线程可见),但写之后的操作可能会被重排,此时如果有影响则需要上锁
在 Java
内存模型中,普通的写操作(如 y = 1
)是可以被重排序到 volatile
写操作之前的。这确实可能影响 volatile
变量的预期行为,导致在其他线程中读取到的值不一致。
title:案例
`int x = 0;`
`volatile int v = 0;`
`int y = 0;`
// Thread A
`x = 1; // 普通写操作`
`v = y; // volatile 写操作`
`y = 1; // 普通写操作`
### 线程A中的操作顺序
在Thread A中,我们有三步操作:
1. `x = 1;` (普通写操作)
2. `v = y;` (`volatile`写操作)
3. `y = 1;` (普通写操作)
### 重排序规则
根据Java内存模型,`volatile`写操作会在其前后插入内存屏障:
1. **写屏障(Store Barrier)**:在写入`volatile`变量之前,确保所有之前的普通写操作都已经完成并对其他线程可见。
2. **读屏障(Load Barrier)**:在读取`volatile`变量之后,确保所有后续的普通读操作读取到最新的值。
### 针对你的问题:普通写操作是否会被重排到`volatile`写操作之前
在Thread A中:
- `x = 1` 是普通写操作。
- `v = y` 是`volatile`写操作。
- `y = 1` 是普通写操作。
根据内存模型的规则:
- `volatile`写操作之前的所有普通写操作不能被重排到`volatile`写操作之后。
- 但是,`volatile`写操作之后的普通写操作是可以被重排到`volatile`写操作之前的。
### 具体示例
考虑以下代码:
x = 1; // 普通写操作
v = y; // volatile 写操作
y = 1; // 普通写操作
在这种情况下,可能会发生以下重排序:
- `x = 1` 和 `v = y` 的顺序是固定的,`x = 1` 必须在 `v = y` 之前完成。
- 但是 `y = 1` 可能会被重排序到 `v = y` 之前。
因此,可能的执行顺序包括:
1. `x = 1`
2. `y = 1`
3. `v = y`
### 对线程B的影响
对于Thread B:
int r1 = v; // volatile 读操作
int r2 = x; // 普通读操作
int r3 = y; // 普通读操作
如果r1 = y
,那么根据内存模型的保证,r2
的值必定是1
(因为x = 1
在v = y
之前完成)。但是,r3
的值不确定,因为y = 1
可能在v = y
之前,也可能在v = y
之后。
使用方式
title:当作状态标识

读 : volitile 保证可见性
写 : 锁保证原子性
title:开销较低的读,写锁策略
有很多读,降低开销,不用加锁,只需要保证可见性即可

多线程使用 volatile
禁用重排序
避免问题
对象创建分为三步,多线程可能发出重排导致出错,因此可以使用 volatile
禁用 重排序
避免问题
title: DCL双端锁
双检

第二次加锁因为:可能别的线程在开始检查时没有创建完成,而检查锁时正好创建完成释放了锁
由于`SafeDoubleCheckSingleton`没有加volatile
`singleton = new SafeDoubleCheckSingleton();`
这行代码实际上分为三个步骤:
1. 为 `SafeDoubleCheckSingleton` 分配内存空间。
2. 调用 `SafeDoubleCheckSingleton` 的构造函数来初始化对象。
3. 将 `singleton` 引用指向分配的内存地址。
由于重排序,可能进行到第二步,对象还没有创建完毕,另一个线程进行判断以为对象已经创建完成,导致出错,因此需要`volatile`保证不会发生重排

总结
可见性
原子性
title:禁重排



title:为什么写了 `volatile` 后系统底层就加入了内存屏障

CAS
比较并交换
CAS的全称为Compare-And-Swap
,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。采用乐观
锁重试机制
AtomicInteger
类主要利用CAS (compare and swap) + volatile
和 native
方法来保证原子操作,从而避免 synchronized
的高开销,执行效率大为提升。
没有CAS
时 : 多线程需要加 锁
来保证原子性
有 CAS
时 : 多线程使用原子类
可以保证原子性
但是存在ABA
问题
title:CAS是什么


如果主内存是5,多线程共享变量副本是5,更新6后准备写入主内存时,会检查共享变量副本是否依旧和主内存相等,相等则认为没有别的线程修改,否则将不更新,重新读取主内存的值
- 硬件级别保证

它通过硬件级别的指令(如cmpxchg指令)来确保比较-更新操作的原子性。这样可以避免传统锁机制带来的线程阻塞和上下文切换开销,从而提高系统的效率和响应速度。

源码分析
CAS
是一条 CPU
的原子指令,CPU 并发原句,不会造成所谓的数据不一致问题
Unsafe
是 CAS 的核心类,可以直接调用操作系统底层
title:源码分析






自旋
尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,
当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
title: java
```java
package com.atguigu.Interview.study.thread;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* @auther zzyy
* @create 2018-12-28 17:57
* 题目:实现一个自旋锁
* 自旋锁好处:循环比较获取没有类似wait的阻塞。
*
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
* 当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。
*/
public class SpinLockDemo
{
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock()
{
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t come in");
while(!atomicReference.compareAndSet(null,thread))
{
}
}
public void myUnLock()
{
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"\t myUnLock over");
}
public static void main(String[] args)
{
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.myLock();
try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); }
spinLockDemo.myUnLock();
},"A").start();
//暂停一会儿线程,保证A线程先于B线程启动并完成
try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
spinLockDemo.myLock();
spinLockDemo.myUnLock();
},"B").start();
}
}
CAS 问题
- 循环开销可能大
title: `ABA` 问题
线程 A 改内存中的数据时,线程 B 过来使用了数据,B改变数据后,又将数据恢复了原样,对于线程 A 检查后以为没有变过而修改成功,出现了过程不安全问题

因此引用了版本号机制, `AtomicStampedReference`


原子操作类之 18
罗汉增强
当线程数量增多时,使用原子操作类的优势会更加明显
基本类型原子类
AtomicInteger/AtomicBoolean/AtomicLong
class MyNumber
{
@Getter
private AtomicInteger atomicInteger = new AtomicInteger();//默认为0
public void addPlusPlus()
{
atomicInteger.incrementAndGet();
}
}
[[../../IT/Java SE/4.面对对象编程/3.应用程序开发/多线程#CountDownLatch|Java SE/3.应用程序开发/多线程 > CountDownLatch]]
数组类型原子类
AtomiclntegerArray
AtomicLongArray
AtomicReferenceArray
引用类型原子类
AtomicReference
AtomicStampedReference
版本号
AtomicMarkableReference
title:原子引用
`new AtomicReference<>();`

将之前设置的z3转换成li4
title:`AtomicMarkableReference`
true/false的更改


对象的属性修改原子类
可以以 线程安全的方式
操作非线程安全对象
内的某些字段
字段级别原子更新
AtomicIntegerFieldUpdater
原子更新对象中 int
类型字段的值
AtomicLongFieldUpdater
原子更新对象中 Long
类型字段的值
AtomicReferenceFieldUpdater
原子更新引用类型
字段的值
都必须使用 public volatile
关键字修饰
因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()
创建一个更新器,并且需要设置想要更新的类和属性。
不能避免 ABA
,不加synchronized
,保证高性能原子性
AtomicIntegerFieldUpdater
构造器是无保护,使用 newUpdater
来创建使用
title: AtomicIntegerFieldUpdaterDemo
```java
class BankAccount//资源类
{
String bankName = "CCB";
//更新的对象属性必须使用 public volatile 修饰符。
public volatile int money = 0;//钱数
public void add()
{
money++;
}
//因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须
// 使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
AtomicIntegerFieldUpdater<BankAccount> fieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");
//不加synchronized,保证高性能原子性,局部微创小手术
public void transMoney(BankAccount bankAccount)
{
fieldUpdater.getAndIncrement(bankAccount);
}
}
/**
* @auther zzyy
* 以一种线程安全的方式操作非线程安全对象的某些字段。
*
* 需求:
* 10个线程,
* 每个线程转账1000,
* 不使用synchronized,尝试使用AtomicIntegerFieldUpdater来实现。
*/
public class AtomicIntegerFieldUpdaterDemo
{
public static void main(String[] args) throws InterruptedException
{
BankAccount bankAccount = new BankAccount();
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 1; i <=10; i++) {
new Thread(() -> {
try {
for (int j = 1; j <=1000; j++) {
//bankAccount.add();
bankAccount.transMoney(bankAccount);
}
} finally {
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t"+"result: "+bankAccount.money);
}
}
AtomicReferenceFieldUpdater.newUpdater(当前类,当前参数类,参数名)
对类中字段更新
referenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE)
title:AtomicReferenceFieldUpdater
```java
class MyVar //资源类
{
public volatile Boolean isInit = Boolean.FALSE;
AtomicReferenceFieldUpdater<MyVar,Boolean> referenceFieldUpdater =
AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");
public void init(MyVar myVar)
{
if (referenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE))
{
System.out.println(Thread.currentThread().getName()+"\t"+"----- start init,need 2 seconds");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+"----- over init");
}else{
System.out.println(Thread.currentThread().getName()+"\t"+"----- 已经有线程在进行初始化工作。。。。。");
}
}
}
/**
* @auther zzyy
* 需求:
* 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,
* 要求只能被初始化一次,只有一个线程操作成功
*/
public class AtomicReferenceFieldUpdaterDemo
{
public static void main(String[] args)
{
MyVar myVar = new MyVar();
for (int i = 1; i <=5; i++) {
new Thread(() -> {
myVar.init(myVar);
},String.valueOf(i)).start();
}
}
}
原子操作增强类原理深度解析
title:为什么出现又增强了什么



DoubleAccumulator
创建一个初始总和 0的新加法器,之后和每次传入的新数做运输,取决于重写 applyAsLong
的逻辑
DoubleAdder
LongAccumulator
LongAdder
title:DoubleAdder,DoubleAccumulator和性能对比

对比测试,性能相差10倍

```java
import javax.lang.model.element.VariableElement;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
class ClickNumber //资源类
//四种计算方式,测试性能
{
int number = 0;
public synchronized void clickBySynchronized()
{
number++;
}
AtomicLong atomicLong = new AtomicLong(0);
public void clickByAtomicLong()
{
atomicLong.getAndIncrement();
}
LongAdder longAdder = new LongAdder();
public void clickByLongAdder()
{
longAdder.increment();
}
LongAccumulator longAccumulator = new LongAccumulator((x,y) -> x + y,0);
public void clickByLongAccumulator()
{
longAccumulator.accumulate(1);
}
}
/**
* @auther zzyy
* 需求: 50个线程,每个线程100W次,总点赞数出来
*/
public class AccumulatorCompareDemo
{
public static final int _1W = 10000;
public static final int threadNumber = 50;
public static void main(String[] args) throws InterruptedException
{
ClickNumber clickNumber = new ClickNumber();
long startTime;
long endTime;
CountDownLatch countDownLatch1 = new CountDownLatch(threadNumber);
CountDownLatch countDownLatch2 = new CountDownLatch(threadNumber);
CountDownLatch countDownLatch3 = new CountDownLatch(threadNumber);
CountDownLatch countDownLatch4 = new CountDownLatch(threadNumber);
startTime = System.currentTimeMillis();
for (int i = 1; i <=threadNumber; i++) {
new Thread(() -> {
try {
for (int j = 1; j <=100 * _1W; j++) {
clickNumber.clickBySynchronized();
}
} finally {
countDownLatch1.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch1.await();
endTime = System.currentTimeMillis();
System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickBySynchronized: "+clickNumber.number);
startTime = System.currentTimeMillis();
for (int i = 1; i <=threadNumber; i++) {
new Thread(() -> {
try {
for (int j = 1; j <=100 * _1W; j++) {
clickNumber.clickByAtomicLong();
}
} finally {
countDownLatch2.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch2.await();
endTime = System.currentTimeMillis();
System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByAtomicLong: "+clickNumber.atomicLong.get());
startTime = System.currentTimeMillis();
for (int i = 1; i <=threadNumber; i++) {
new Thread(() -> {
try {
for (int j = 1; j <=100 * _1W; j++) {
clickNumber.clickByLongAdder();
}
} finally {
countDownLatch3.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch3.await();
endTime = System.currentTimeMillis();
System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByLongAdder: "+clickNumber.longAdder.sum());
startTime = System.currentTimeMillis();
for (int i = 1; i <=threadNumber; i++) {
new Thread(() -> {
try {
for (int j = 1; j <=100 * _1W; j++) {
clickNumber.clickByLongAccumulator();
}
} finally {
countDownLatch4.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch4.await();
endTime = System.currentTimeMillis();
System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByLongAccumulator: "+clickNumber.longAccumulator.get());
}
}
底层源码
CAS 只允许一个线程成功修改共享变量的值,其余线程会失败并重试, 当并发量高时会出现非常多的重试, 而 LongAdder
内部维护了一个基于 CAS 的 cell 数组。每个 cell 独立维护一个计数值。会对每个 cell
都可以有一个线程进入然后进行更新, 多个线程可以操作不同的 cell
, 最后只需要将每个 cell 累加, 得到结果
每个线程 ID 进行 hash, hash 映射到某个数组 cell 的下标,然后再对该下标的值进行自增操作
title:LongAdder 为何快
`LongAdder`是`Striped64`的子类
`Striped64`源码

`CAS`只允许一个线程成功修改共享变量的值,其余线程会失败并重试,当并发量高时会出现非常多的重试,而`LongAdder`内部维护了一个基于`CAS`的`cell`数组。每个`cell`独立维护一个计数值。会对每个`cell`都可以有一个线程进入然后进行更新,多个线程可以操作不同的`cell`,最后只需要将每个`cell`累加,得到结果

`LongAdder`= base变量+Cell[]数组





https://www.bilibili.com/video/BV1ar4y1x727/?p=90&vd_source=17535a06cd4587318e9b67ac64afeab7
92-99 没看
title:`LongAdder`源码

(as = cells) != null : 检查 `cells` 数组是否已初始化,初始化为`ture`
未初始化则进行,`!casBase(b = base, b + x)`,尝试使用CAS操作直接更新基础计数器 `base`。

一个base,然后新建两个cell
锁-CAS
-Volatile
总结
JMM
: 可见性,原子性,有序 (指令重拍)
可见性 | 原子性 | 有序性 | |
---|---|---|---|
Volatile | √ | × | 部分 |
CAS | × | √ | × |
Volatile+CAS | √ | √ | 部分 |
锁 | √ | √ | √ |
Volatile
Volatile
提供了可见性, 部分
有序性,不保证原子性
读之后
的操作不会重排到读之前
写之前
的操作不会重排到写之后
读 : volitile
保证可见性
写 : 锁保证原子性
CAS
Compare-And-Swap
各种原子类,采用乐观锁机制
通过硬件级别的指令(如 cmpxchg
指令)来确保比较-更新操作的原子性
通常配合配合 volatile
实现 可见性
,和 部分有序性
没有 CAS 时 : 多线程需要加 锁
来保证原子性
有 CAS 时 : 多线程使用 原子类
可以保证原子性
- 锁
偏锁-轻量-重量
锁可以直接保证 可见,原子,有序
,无需volatile
通常情况下,如果并发读写操作较多且竞争不激烈,可以考虑使用CAS+volatile;如果需要更强的线程安全性或者竞争比较激烈,可以选择直接使用锁。
ThreadLocal
:提供了一种线程 局部变量
,每个线程都有一份,再也无需使用 Voliatile/CAS/锁
ThreadLocal
ThreadLocal
提供线程局部变量
。这些变量与正常的变量不同,因为每一个线程在访问 ThreadLocal
实例的时候(通过其 get 或 set 方法)都有自己的、独立初始化的变量副本。ThreadLocal
实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户 ID 或事务 ID
)与线程关联起来。
实现每一个线程都有自己专属的本地变量副本 (自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份)
提供了一种线程 局部变量
,每个线程都有一份,再也无需使用 Voliatile/CAS/锁
使用方式
- 初始化方式
initialValue/withInitial
两种方式一致
withInitial
是新的方式,比前者简洁
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
不建议直接初始化 默认为 null
可能报空指针异常
建议使用 withInitial
初始化
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
只需要初始化一次,之后每个线程使用都是有 一份
自己的初始值,之后可以操作这个数据,但每个线程只能有一份,每个线程自己用完了需要使用 remove()
进行回收
使用完后需要回收自定义的 ThreadLocal
变量,通常使用try-catch-finally
并不在于 ThreadLocal
实现了 ThreadLocal
, 而在于 ThreadLocalMap
,每个线程操作 ThreadLocal
中的 set
方法,都会和自己当前的线程进行绑定 , 所以 ThreadLocal
只需要初始化一次,只分配一块空间就够了,所以经常使用 static
修饰
title: java
案例测试每个线程记录自己的数据
```java
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
class House //资源类
{
int saleCount = 0;
public synchronized void saleHouse()
{
++saleCount;
}
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
public void saleVolumeByThreadLocal()
{
saleVolume.set(1+saleVolume.get());
}
}
/**
* @auther zzyy
* @create 2021-12-31 15:46
* 需求1: 5个销售卖房子,集团高层只关心销售总量的准确统计数。
* 需求2: 5个销售卖完随机数房子,各自独立销售额度,自己业绩按提成走,分灶吃饭,各个销售自己动手,丰衣足食
*/
public class ThreadLocalDemo
{
public static void main(String[] args) throws InterruptedException
{
House house = new House();
for (int i = 1; i <=5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5)+1;
try {
for (int j = 1; j <=size; j++) {
house.saleHouse();
house.saleVolumeByThreadLocal();
}
System.out.println(Thread.currentThread().getName()+"\t"+"号销售卖出:"+house.saleVolume.get());
} finally {
house.saleVolume.remove();
}
},String.valueOf(i)).start();
};
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t"+"共计卖出多少套: "+house.saleCount);
}
}
源码分析
Thread
--> ThreadLocal
-->ThreadLocalMap
-->当前线程和当前变量
Thread
包含 ThreadLocal
ThreadLocal
包含静态 ThreadLocalMap
类
ThreadLocalMap
包含 储存当前线程和当前变量
类似 : 人: 卡片 : 卡片信息的关系
内存泄露问题
ThreadLocalMap
是经过两层包装的 ThreadLocal
对象
(1)第一层包装是使用 WeakReference<ThreadLocal<?>>
将ThreadLocal
对象变成一个弱引用的对象(蓝框)
(2)第二层包装是定义了一个专门的类 Entry
来扩展WeakReference<ThreadLocal<?>>
[[上内存与垃圾回收#再谈引用|JVM/上内存与垃圾回收 > 再谈引用]]
title:引用复习
`软引用` 通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收

`弱引用`: 发现就回收

`虚引用`: 顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
线程运行完后 Threadlocal
应该被回收,而 ThreadLocalMap
中储存 ThreadLocal
的值,也就是引用了 ThreadLocal
, 如果是强引用,就会导致 Threadlocal
没有被回收,造成内存泄露
Entry
弱引用 ThreadLocal
Entry extends WeakReference<ThreadLocal<?>>
因此需要设计成弱引用,但此时仍不完善
设计成弱引用后, ThreadLocal
被回收后 Entry
的 key
就会指向 null
, 而value
的值是强引用Object value;
,且因为设置为 null
后该键对值无法访问,造成了内存泄露, 因此弱引用不能 100%
保证内存不泄露 , 所以需要手动清理 remove
, set/get
也会检查所有键为 null
的 Entry
对象
当然在线程结束的时候 Thread
停止会被回收,但是线程池会复用线程,线程是不会停止的,此时泄露就需要注意
A 类弱引用类 B : A extends WeakReference<B>
强引用链 Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
title:为什么源码用弱引用

`Entry` 扩展了 `WeakReference<ThreadLocal<?>>`,这意味着它持有一个 `ThreadLocal<?>` 对象的弱引用


使用弱引用回收后 `Entry` 的 `key` 就会指向 `null`
由于在线程池场景下,线程池经常被复用
Object value;
此时仅仅使用弱引用,就会存在很多key为ull的entry,但value一直存在也会再引发新的内存泄露,因此需要remove手动清理
值是强引用的主要原因是为了确保线程局部变量的值在键存在的情况下可以被正常访问。如果值也是弱引用,那么在垃圾回收时,即使键还存在,值也可能被回收,这样会导致意外的行为。




set、get方法会去检查所有键为null的Entry对象




阿里巴巴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.11.2</version>
</dependency>
阿里巴巴开源的 TransmittableThreadLocal
(TTL) 是一个增强版的 ThreadLocal
,它可以在创建新线程时传递父线程的上下文
。
使得父线程设置的值可以在子线程中传递,解决了线程池环境下 ThreadLocal 的数据传递问题。
总结
ThreadLocal
出现的意义:
多线程 : 让每个线程都有自己独有的空间,减少锁的使用
单线程: 如储存当前会话/事务信息,在当前线程中传递信息
[[../../IT/实战项目/苍穹外卖#4 3 ThreadLocal|单线程使用案例]]
如果设置为static
则是每个线程访问都是同一个ThreadLocal
实例
一般也只new
一个ThreadLocal
实例,多线程使用依旧是每个线程自己独有的变量
单线程下使用场景 : 当一个线程运行过程中,开始时使用的数据之后又需要使用,就可以用ThreadLocal
来进行储存,以便该线程之后的使用
ThreadLocal
和 static
变量在某种程度上可以看作是相反的形式:
总结:
-
ThreadLocal
变量是线程私有的,每个线程都有自己的副本。 -
static
变量是全局共享的,所有线程共享同一个实例。 -
remove
当ThreadLocal
不再使用的时候, 而ThreadLocalMap
中依旧存有ThreadLocal
导致无法回收所以设计为弱引用ThreadLocal
, 让ThreadLocal
可以正常被回收了, 但ThreadlocalMap
中键对值却无法访问了, 导致 value 无法被回收, 所以需要手动remove
,set/get
也会检查所有键为null
的Entry
对象
java
中多个对象之间通常设计强弱引用关系
Java 内存布局和对象头
[[上内存与垃圾回收#对象实例化,内存布局与访问定位|JVM/上内存与垃圾回收 > 对象实例化,内存布局与访问定位]]
- 对象头 (16 字节)
- 对象标记
Mark word
(8 字节) - 类型指针 (8 字节)
Mark word
8 个字节, 64 位的分布
类型指针
就是指向方法区类模板文件的指针, 为了确定是哪个类的实例
内存对齐
- 对象标记
- 实例数据: 类的属性信息,父类的属性信息,如果是数组的实例部分包括数组的长度,按照 4 字节对齐
- 对齐填充: 虚拟机要求对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这部分内存按 8 字节补充对齐。
Mark word
(8 字节) 为 64bit
64bit
各种组合就可以看出来有无加锁,加的是什么锁
最后两位就代表了不同的锁状态
4bit
可以表示 0到15
之间的数值分代年龄用 4bit
表示,所以不能支持改为 16
title: java



类型指针被压缩了,从8个压缩到了4个
`-XX:+UseCompressedClassPointers`
```java
<!--
官网:http://openjdk.java.net/projects/code-tools/jol/
定位:分析对象在JVM的大小和分布
-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
ackage com.atguigu.juc.senior.inner.object;
import org.openjdk.jol.vm.VM;
/**
* @auther zzyy
* @create 2020-06-13 11:24
*/
public class MyObject
{
public static void main(String[] args){
//VM的细节详细情况
System.out.println(VM.current().details());
//所有的对象分配的字节都是8的整数倍。
System.out.println(VM.current().objectAlignment());
}
}
Sychronized
与锁升级
性能变化
java
线程是映射到操作系统原生线程之上的,需要在用户态和内核态之间切换,消耗大量系统资源
title:内核态与用户态
用户态和内核态是描述操作系统中不同权限等级的术语。
用户态运行在较低权限级别,主要用于普通应用程序,限制对硬件和系统资源的直接访问。
内核态运行在最高权限级别,允许对所有硬件和系统资源进行直接访问和控制,主要用于操作系统内核和关键系统服务。


早期版本中,synchronized属于重量级锁
,效率低下,因为监视器锁(monitor
)是依赖于底层的操作系统的 Mutex Lock
来实现的
Java 6
之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁
title:简介



`Mutex Lock`

title:概览图


锁种类和升级步骤
由对象头中 Mark Word
根据锁标志位的不同而被复用及锁升级策略
无锁
title:无锁
无锁:初始状态,一个对象被实例化后,如果没有被任何线程竞争锁那么就是无锁状态

`0 4 ` 的 `01` 代表无锁
对象虽然生成了,但是没有调用 `hashcode` 是不会记录 `hash` 编码的
从右下角往上看
`unused`

https://www.bilibili.com/video/BV1ar4y1x727?p=124&vd_source=17535a06cd4587318e9b67ac64afeab7
最后八位


由对象头中

偏锁
默认打开,但是有延迟4s
,延迟时可能跳过偏锁加上轻量锁
实际运行经常出现一个线程很多次抢到一个锁,那么在未来的一段时间内,很可能会被同一个线程再次锁定。因此,为了减少获取锁的开销,JVM 会在对象头中存储锁信息,用于记录当前持有锁的线程 ID,这样在未来获取锁时,可以快速判断是否是同一个线程获取锁, 该线程在后续访问时便会自动获得锁
JVM
使用 CAS
操作把线程指针 ID
记录到 Mark Word
当中,并修改标偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁(通过 CAS
修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。
该线程无需再进 Monitor
去竞争对象,其他线程依旧需要。
线程的调度是由操作系统控制的,无法精确控制线程的执行顺序,因此可能会出现某个线程多次抢到锁的情况。
title:简介






title:偏锁测试
默认打开,但是有延迟4s,延迟时可能跳过偏锁加上轻量锁
`java -XX:+PrintFlagsInitial | grep BiasedLock*`


或等待5s,偏锁开启的时候

轻量锁
java15
以后逐步废弃偏向锁,开销和成本大
本质就是 自旋锁CAS
先自旋再阻塞, 有线程来参与锁的竞争,但是获取锁的 冲突时间极短
轻量级锁是为了在线程近乎交替
执行同步块时提高性能
一旦有别的线程竞争,发现 Mark Word 中储存当前线程 ID 不是自己的,就会采用 CAS 尝试获取锁,成功获取则将 Mark Word 中 ID 替换为自己的,失败则自旋等待, 偏向锁升级为轻量锁
当获取偏锁失败,会升级为轻量锁, JVM
会为每个线程在 当前线程的栈帧中创建用于存储锁记录的空间
(在 JVM
栈中),官方成为 Displaced Mark Word
。若一个线程获得锁时发现是轻量级锁,会把锁的 MarkWord
复制到自己的 Displaced Mark Word
. 里面。然后线程尝试用 CAS 将锁的 MarkWord
替换为 指向锁记录的指针
。如果成功,当前线程获得锁,如果失败,表示 Mark Word
已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
保证线程间公平竞争锁
title:偏向锁的撤销
1. 该线程执行完
2. 另一个线程进行抢夺
当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁
竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁。





重量锁
自旋次数 : 自适应自旋是一种优化技术,它根据当前线程在上一次自旋尝试获取锁时的表现来动态调整
下一次自旋的次数。然后会升级为重量锁
大部分情况下会使用轻量级锁而不是重量级锁
总结
一句话: 先自旋,不行重量锁
-
偏向锁
偏向锁
:Mark Word
中储存当前线程ID
(延迟4s
)这样在未来获取锁时,可以快速判断是否是同一个线程获取锁, 该线程在后续访问时便会自动获得锁
偏向锁获取后并不会主动释放,如果获取偏锁失败就会升级为轻量锁
为了解决只有在一个线程执行同步时提高性能。现代多核处理器经常竞争,所以偏向锁被废除 -
偏向锁
到轻量锁
一旦有别的线程竞争,发现Mark Word
中储存当前线程 ID 不是自己的,就会采用 CAS 尝试获取锁,成功则还是偏锁,失败则会等待到全局安全点,持有偏向锁的线程会撤销原来的偏向锁, 升级为轻量锁,把锁的MarkWord
复制到自己的Displaced Mark Word
. 里面。然后线程尝试用CAS
将锁的MarkWord
替换为指向锁记录的指针
。
偏向锁
和轻量锁
都不涉及内核到用户态的切换 -
轻量锁
轻量锁
自旋锁CAS
, 有线程来参与锁的竞争,但是获取锁的冲突时间极短
, 轻量级锁是为了在线程近乎交替
执行同步块时提高性能 -
到重量锁
在多线程竞争条件下,可能出现偏向锁跳过轻量锁直接升级为重量锁
或轻量锁直接升级为重量锁
- 多个线程频繁争抢同一个锁:当多个线程频繁尝试获取同一个锁时,偏向锁会被撤销,并直接升级为重量级锁。
- 线程数超过阈值:JVM 有一个阈值,当尝试获取锁的线程数超过这个阈值时,偏向锁会直接升级为重量级锁。
JVM
会根据线程竞争的激烈程度和系统的负载情况动态调整这个阈值,超过阈值就会升级
title:锁升级后哈希码去哪了?
偏锁不能和哈希码共存
如果在使用锁前计算哈希码会直接升级为轻量锁
如果在使用锁时计算哈希码会直接升级为重量锁
轻量锁可以共存

当对象计算过哈希码后,就不能进入锁,否则就会造成编码错乱
`o.hashCode()`




title:各种锁的优缺点

锁消除: JIT 会无视没有共用扩散到其他线程的锁
锁粗化: 一定范围内都是一个锁,申请一把锁即可,编译器会进行优化
title:JIT对锁的优化
锁消除


锁粗化


AbstractQueuedSynchronizer
之 AQS
抽象的队列同步器,有很多没有抢到锁,此时需要一个队列来对这些还需要抢锁的线程进行管理
是用来构建锁或者其它同步器组件的重量级基础框架及整个 JUC
体系的基石,
通过内置的 FFO
队列来完成资源获取线程的排队工作,并通过一个int
类变量表示持有锁的状态
AbstractQueuedSynchronizer
简称为 AQS
内部结构
阻塞入队
ReentrantLock
ReentrantLock
就是操作 sync
,对外保留 FairSync/NofairSync
lock-->acquire-->tryAcquire-->addWaiter-->acquireQueued
假设 A 需要时间特别长,在这种情况下进行讲解
入队
lock
lock
非公平上来直接CAS进行抢占,抢不到进入acquire
公平进入acquire
acquire
acquire
没有实际方法,参数为int类型,代表锁的可重用性
调用tryAcquire
acquire为父类的AbstractQueuedSynchronizer
的,两个类都一样,会调用子类的重写过的tryacquire
tryAcquire
tryAcquire(int)
进行抢锁
公平锁多了个hasQueuedPredecessors()
,其他都一样
getstatue
是1,说明有别的线程占用
非公平锁中
else if (current == getExclusiveOwnerThread())
保证锁的可重用性
两种锁唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors()
addWaiter
- 说明上面的抢锁失败,进入等待
addWaiter(Node.EXCLUSIVE)
创建Node并
加入等待队列
mode
有两种:
SHARED
共享等待锁
EXCLUSIVE
独占等待锁
tail
初始为null
然后开始直接进入enq
,只会运行一次,进行初始化
enq
方法只有第一次添加 node
才会调用, 第二次则不会调用
t
为上一个尾部
node.prev=t
当前node的前一个节点指向上一个尾部
compareAndSetTail(pred, node)
更新尾部,设置成当前node
t.next
将上一个节点指向当前节点
上半部分代码的作用是初始化创建一个空的节点,并将头节点和尾节点都指向这个新创建的节点。
然后t不再为null,,而是第一个虚拟节点,不进入第一个if,进入else
这串代码将第一个入队的节点设置为尾节点
队列中是如何唤醒, 排队, 稳定在队列中的
acquireQueued
acquireQueued(node, arg)
在等待队列中阻塞当前线程,直到获取同步状态成功。
head 最开始为初始的默认空 node, 之后都是当前正在使用锁的节点
前一个为 head
, 说明排到了自己然后继续 tryAcquire
,假设现在其他线程还在处理, 则不进入
failed
变量的存在是为了确保在任何异常或中断情况下,资源能够被正确释放和清理。
然后进入 shouldParkAfterFailedAcquire
对状态更改
上一个节点的初始 waitStatus
默认 0
,然后进入 compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
Node. SIGNAL
为 -1
,改为了 -1
,返回 flase
就跳出了,进入下一个循环,再次进入 shouldParkAfterFailedAcquire
对状态更改,第二次进入就是 -1
返回了 true
,然后 LockSupport.park(this);
,this 为当前 node
,进行了阻塞,会等待通行证,这就是 AQS
底层如何做到 node 停在这的
然后 C 节点进入,同样 waitStatus
, 第一次 0
返回 false
然后改为 -1
返回ture
,同样设置了 park(this)
也就是后面的节点都会把前面的 waitStatus
改为 -1
, 然后阻塞自己
只有虚拟节点的后一个 node
/或当前使用锁的节点的下一个节点
且抢占 tryacquire
成功才会进入蓝色的
队列中再往后的节点都会进入红色的
此时 B 就卡在这转圈
出队
unlock-->sync.release(1)-->tryRelease(arg)-->unparkSuccessor
sync.release(1)
tryRelease(arg)
Thread.currentThread() != getExclusiveOwnerThread()
确保当前调用解锁方法的线程是持有锁的线程。
如果当前线程不是持有锁的线程,则抛出 IllegalMonitorStateException
异常。
防止其他线程在没有持有锁的情况下尝试释放锁,保证锁的正确性和一致性。
state
是 1, 减去 1 为 0, 然后就释放该线程
然后 tryRelease(arg)
运行完毕, 返回 ture
h != null
头节点肯定不为null
h.waitStatus != 0
头节点的 waitStatus
,只要其他线程来抢了,肯定也不等于 0
unparkSuccessor
然后调用 unparkSuccessor(h)
传入head
重新把 虚拟节点
waitStatus
中之前的 -1
设置为 0
此时 s
为虚拟节点的 下一个节点
第一个 if
,S==null
为 false
不进入
第二个 进入
if (s != null)
LockSupport.unpark(s.thread);
释放了通行证
释放通行证后, parkAndCheckInterrupt
,返回的还是 flase
,但在 for (;;)
中,此时 tryAcquire
假设成功, 可以顺利进入蓝色的
(非公平锁并不是在队列中先进先出,可能直接被别的抢到)
title:`setHead`
将当前最新的 `node` 设置为 `head`
`node.thread = null;`
`node.prev = null;`

当 node 成功获取到锁后,thread 变量就不再需要了。此时,node 已经成为队列的头节点,线程信息不再需要保留在节点中。通过将 thread 字段设置为 null,可以帮助垃圾回收器回收不再需要的对象,从而避免内存泄漏


获取后就不需要 thread 了

p.next
设置为 null
,让上一个节点指向的下一个设置为 null
,此时可以被垃圾回收
取消流程
这通常用于跳过已取消的节点,确保当前节点的前驱节点是一个有效的、未取消的节点。
把node的prev的prev赋给现在的node.prev,直到找到前节点不取消的哪个
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
总结
Node
每个 node
节点包含上一个/下一个信息, 队列包含 head/tail
信息, 这样就构成了一个队列
title:简介


StampedLock
[[../../IT/Java SE/4.面对对象编程/3.应用程序开发/多线程#ReentrantLock|Java SE/3.应用程序开发/多线程 > ReentrantLock]]
ReentrantLock-->ReentranReadWriteLock-->StampedLock
ReentranReadWriteLock
当读很多写很少,可能出现写锁
饥饿,抢不到锁的情况
引出了 邮戳锁StampedLock
StampedLock
采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。
邮戳锁StampedLock
就是读的过程中允许写锁介入,避免了写锁饥饿
static StampedLock stampedLock = new StampedLock();
long stamp = stampedLock.tryOptimisticRead();
stampedLock.validate(stamp)
检查是否变动过,false
代表有修改
stampedLock.readLock()
和读写锁一样
stampedLock.writeLock()
和读写锁一样
title: note

- 缺点
StampedLock
不支持重入,没有 Re 开头
StampedLock
的悲观读锁和写锁都不支持条件变量 (Condition
), 这个也需要注意。
使用 StampedLock
一定不要调用中断操作,即不要调用 interrupt()
方法
思维
- 多线程时: 任意两行代码执行之间都可能存在别的线程执行了其他代码
- 它是什么,能干什么,怎么会出现的,没有它之前是怎么做的, 解决了哪些痛点,方便了哪些地方