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 原语指令
单例模式
代码演示
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代码演示如下
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 ,修改完毕 我们再判断这个数字是否改变