volatile保证线程可见性和禁止指令重排序
volatile保证线程可见性和禁止指令重排序保证线程可见性
禁止指令重排序
单例模式
代码演示
双重锁
锁细化 锁粗化
volatile的使用
锁定的对象改变
CAS
atomic
ABA
# 保证线程可见性
> 使用了volatile之后,可以保证可见性,就是一个线程修改了,另一个线程能够立刻看到
> 可见性不是锁,并不能保证数据的安全,可能再同一个时间点,两个线程都看到了2,然后都去加1,本来是变成4.但是都是在2的基础上加的,所以变成了3
> 原理
>
`禁止线程私有区域 :在jvm中,堆是线程共有区域,多个线程操作堆中的同一个数据时,比如要在1上进行加一,这个时候会把堆中的数据复制到线程的一块私有内存中,在私有内存中进行加1,然后将结果在复制到堆中`
`加上volatile关键字之后,就禁止了线程的私有内存空间,操作时直接在堆上操作,因此其它线程可以立即看到`
> 类似于我们cpu之间的一致性,目前的多个cpu运行时,也需要保证缓存的一直性,用到了MESI 缓存一致性协议
# 禁止指令重排序
> 为什么会重排? 最早的cpu在运行指令时,是串行执行的,后来为了提高效率,cpu将多个指令并行运行,经过验证运行速度提高很大
> 我们平时的一行代码,到cpu那里可能会拆分为多个指令,
> 如: new Object();分为
>- 指定内存 (加载,验证和准备)
>- 赋初始值 (解析)
>- 将内存赋值给对象 (初始化 )
> 正常操作时,没有问题,但是上面的指令,执行顺序可能是 132
> 那么就会出现异常:
> 当执行完 3 之后,我们的对象已经不是null了。如果我们的代码里有判断null的逻辑,就会认为这个对象已经初始化完成,然后直接使用,但是这个时候可能我们这个对象里的值还没有初始化,就会导致错误
> i++ 也是不安全的 会拆分为多个指令
> 使用完volatile之后,会可以防止当前命令的所有指令重排
实现方式cpu有关
cpu会将指令并行执行,通过在所有指令的前后添加一下内容
loadfence 原语指令
storefence 原语指令
# 单例模式
## 代码演示
```java
package 线程同步volatile;
/**
* @program: solution
* @description: 单例模式
* @author: Wang Hai Xin
* @create: 2022-11-03 18:34
**/
public class T {
public static volatile T t;
public T init(){
/*这里写init的其它逻辑*/
/*双重锁*/
if (t == null) {
synchronized (T.class){
if (t == null) {
t =new T();
}
}
}
return t;
}
}
```
## 双重锁
if (t == null) {
synchronized (T.class){
if (t == null) {
t =new T();
}
}
}
> 第一个判断是为了防止更多的线程被锁,
> 在synchronized中,加if判断,是为了多个被锁的线程,只有第一个会去创建,剩下的直接跳过
## 锁细化 锁粗化
> 这里可扩展一下的知识,锁细化和锁粗化,我们本来可以直接在方法上添加synchronized,但是在init方法中就锁住了很多其它的逻辑,我们通过在里面只锁定了创建对象的一行代码,就是典型的锁细化。
>聊完锁细化,就可以聊聊锁粗化了,锁粗化和锁细化恰恰相反,加入我们需要加锁的代码很紧密,如果分别加锁,lock 和unlock也是很影响效率的,这个时候就可以用一个锁或者几个锁,把代码全部锁住,减少lock和unlock所需要的时间
## volatile的使用
> 聊的有点远了,再撤回来,我们在public static volatile T t;
> 上加了 volatile关键字,为什么加呢?
> 看了上面的 禁止指令重排序你应该就理解了
> 我们在执行顺序132,导致if中判断时不为null。
# 锁定的对象改变
属性改变不影响锁
锁定的对象变成另一个对象就会影响锁,(锁信息写在对象头里)
锁只能保证在同一个classloade中生效,在多个classloade不能生效
内存可以自定义 classloade
# CAS
Compare And Set
比较并且设定!
> CAS被称为无锁优化
## atomic
> java中util包中有一个 atomic类,里面都是线程安全的
有 AtomicInteger
Atomxxx 类本身方法都是原子性的,但不能保证多个方法连续调用的原子性。
AtomicInteger代码演示如下
```java
package 线程同步Atomic;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @program: solution
* @description: integer的原子操作类,保证线程安全
* @author: Wang Hai Xin
* @create: 2022-11-04 11:28
**/
public class To1AtomicInteger {
AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) {
To1AtomicInteger t = new To1AtomicInteger();
ArrayList<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threads.add( new Thread(t::m,"thread"+i));
}
threads.forEach((o)-> o.start());
threads.forEach((o)->
{
try {
/*join 让主线程等待子线程(一个线程内创建了另一个线程,创建的为主线程,被创建的为子线程)运行完毕,主线程再运行*/
o.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
);
System.out.println(t.atomicInteger);
}
private void m() {
for (int i = 0; i < 10; i++) {
atomicInteger.incrementAndGet();//自增1
}
}
}
```
cas(object要改的值,期望改成值,要该成的值 )
`当修改完成之后,判断要改的值有没有变,就是判断一下在修改的这段时间,有没有其它线程对数据进行写操作`
`cas在写操作少的时候,是可以提高效率的,但是如果线程过多,就会导致效率低下,因为当一个线程修改完之后,发现被其它线程修改了,就会重新读取再次修改,当竞争激烈时,会不停的重复这个动作`
# ABA
> 上面的cas操作,可能存在一个问题,假如线程1再修改A时,线程2 修改了线程A的某个属性,线程1回来时,发现还是A,以为没有被别的线程进行写操作,然后写入了,这个时候就会产生错误。
>
`主要发生在引用类型上`
> 解决办法
`加版本号,通过版本号控制,比如没进来一个我们都加1
,修改完毕 我们再判断这个数字是否改变`
页:
[1]