黑白客 发表于 2022-11-4 18:10

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
,修改完毕 我们再判断这个数字是否改变`

xiadongming 发表于 2022-11-5 17:05

页: [1]
查看完整版本: volatile保证线程可见性和禁止指令重排序