6、volatile与JMM

目录

1、被volatile修饰的变量有2大特点

2、volatile特性-可见性

volatile 变量的读写过程

3、volatile特性-没有原子性

3.1 volatile变量的复合操作不具有原子性,比如number++

3.2 volatile修饰的为什么会引起线程安全问题回答

3.3 结论

4、volatile特性-禁止指令重排

4.1 什么叫指令重排

4.2 内存屏障分类

4.3 读屏障

4.4 写屏障

5、如何正确使用volatile

5.1 单一赋值可以,例如状态标志,但是含有符合运算赋值的不可以(i++)

5.2 开销较低的读,写锁策略

5.3 DCL双锁案例

6、小总结

6.1 volatile特性有什么?

6.2 为什么我们java写了一个volatile关键字系统底层加入内存屏障?两者怎么做的关联?

6.3 内存屏障是什么?

6.4 内存屏障能干什么?

6.5 内存屏障四大指令?


1、被volatile修饰的变量有2大特点

Volatile修饰的变量在某个工作内存修改后立刻会刷新回主内存,并把其他工作内存的该变量副本设置为无效,其实就是缓存一致性协议(MESI)

2、volatile特性-可见性

保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见

public class VolatileSeeDemo{
    //static  boolean flag = true;       //不加volatile,没有可见性
    static volatile boolean flag = true;       //加了volatile,保证可见性

    public static void main(String[] args){
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t come in");
            //默认flag是true,如果未被修改就一直循环,下面那句话也打不出来
            while (flag){

            }
            System.out.println(Thread.currentThread().getName()+"\t flag被修改为false,退出.....");
        },"t1").start();

        //暂停几秒
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        flag = false;

        System.out.println("main线程修改完成");
    }
}
//没有volatile时
//t1   come in
//main线程修改完成
//--------程序一直在跑(在循环里)

  

//有volatile时
//t1   come in
//main线程修改完成
//t1   flag被修改为false,退出.....

 线程t1中为何看不到被主线程修改为flase的flag的值?

问题:

        1、主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到

        2、主线程将flag刷新到了主内存,倒是t1一直读取的是自己工作内存中的flag的值,没有去主内存中更新获取flag最新的值

volatile解决:

        1、线程中读取的时候,每次都会去主内存中读取共享变量最新的值,然后将其复制到工作内存中

        2、线程中修改了工作内存中变量的副本,修改之后会立即刷新主内存

volatile 变量的读写过程

 简单理解如下图:

read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存

load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载

use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作

assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作

store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存

write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量

由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:

lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。

unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用

3、volatile特性-没有原子性

什么叫原子性:

        原子性指的是一个操作是不可中断的,即使多线程环境下,一个操作一旦开始就不会被其他线程影响

3.1 volatile变量的复合操作不具有原子性,比如number++

public class VolatileNoAtomicDemo {
    public static void main(String[] args) {
        MyNumber myNumber = new MyNumber();

        for (int i = 1; i <=10; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myNumber.addPlusPlusVolatile();
                    myNumber.addPlusPlusSynchronized();
                }
            },String.valueOf(i)).start();
        }

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("volatile" + "\t" + myNumber.numberVolatile);
        System.out.println("synchronized" + "\t" + myNumber.numberSynchronized);
    }

}

// volatile	9837
// synchronized	10000

class MyNumber{
    int numberSynchronized = 0;
    volatile int numberVolatile = 0;

    public synchronized void addPlusPlusSynchronized(){
        numberSynchronized++;
    }
    public void addPlusPlusVolatile(){
        numberVolatile++;
    }
}

synchronized 分析 

synchronized加了之后保证了串行执行,每次只有一个线程进来

volatile 分析

        没有锁保证每次只有一个线程对主内存中的数据进行保证,大家一起读,一起做操作,一起提交,但是两次计算是基于同一个值(举例:两个线程都读到主内存中的5,都进行了加1操作,则回写到主内存中都是6,但是其实是进行了两次操作,应该主内存中出现的是7这个数才是对的)(后续会用循环+CAS算法弥补该缺陷)

3.2 volatile修饰的为什么会引起线程安全问题回答

        对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也只是数据加载时是最新的。如果第二个线程在第一个线程读取旧值写回新值期间读取i的阈值,也就造成了线程安全问题。

3.3 结论

        volatile不适合参与到依赖当前值的运算,如i = i + 1,i++之类。

        通常volatile用作保存某个状态的boolean值或int值(一旦布尔值被改变迅速被看到,就可以做其他操作)

《深入理解Java虚拟机》提到:

4、volatile特性-禁止指令重排

4.1 什么叫指令重排

        代码执行的顺序与我们编码写的顺序不一致,这个主要来源于编译器,指令级等优化重排序。

public class VolatileReOrderSample {

    private static int x = 0;
    private static int y = 0;
    private static int a = 0;
    private static int b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;

        for(;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();

            // 如果不重排 这里只有三种情况
            // 1、t1 先执行完毕 结果: x = 0,y = 1
            // 1、t2 先执行完毕 结果: x = 1,y = 0
            // 1、t1,t2 交替执行 结果: x = 1,y = 1
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                // 如果不指令重排,则永远不会出现该情况;如果出现该情况则说明了出现了指令重排
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}

         给变量加上volatile之后就不会出现以上代码情况!因为在volatile修饰的变量添加了内存屏障,防止了指令重排序

4.2 内存屏障分类

粗分两种:

        写屏障:在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中

        读屏障:在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据

细分四种:

//Unsafe.class


// 读屏障
public native void loadFence();
    
// 写屏障
public native void storeFence();
    
// 所有屏障
public native void fullFence();
//unsafe.cpp

UNSAFE_ENTRY(void, Unsafe_LoadFence(JNIEnv* env, jobject unsafe))//读屏障
  UnsafeWrapper("Unsafe_LoadFence");
  OrderAccess::acquire();
UNSAFE_END

UNSAFE_ENTRY(void, Unsafe_StoreFence(JNIEnv* env, jobject unsafe))//写屏障
  UnsafeWrapper("Unsafe_StoreFence");
  OrderAccess::release();
UNSAFE_END

UNSAFE_ENTRY(void, Unsafe_FullFence(JNIEnv* env, jobject unsafe)) // 全屏障
  UnsafeWrapper("Unsafe_FullFence");
  OrderAccess::fence();
UNSAFE_END
class OrderAccess : AllStatic {
 public:
  static void     loadload();//读读
  static void     storestore();//写写
  static void     loadstore();//读写
  static void     storeload();//写读

  static void     acquire();
  static void     release();
  static void     fence();
inline void OrderAccess::loadload()   { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore()  { acquire(); }
inline void OrderAccess::storeload()  { fence(); }

inline void OrderAccess::acquire() {
  volatile intptr_t local_dummy;
#ifdef AMD64
  __asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else__
  asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}

 1、当一个操作是volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被排到volatile读之前

2、当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后

3、当第一个操作为volatile写时,第二个操作为volatile读时,不能重排

4.3 读屏障

        在每个volatile读操作的后面插入一个LoadLoad屏障(禁止后面的普通读重排序到该volatile读前)

        在每个volatile读操作的后面插入一个LoadStore屏障(禁止后面的普通写操作重排序到volatile读前)

4.4 写屏障

        在每个volatile写操作的前面面插入一个StoreStore屏障(禁止上面的普通写和该volatile写重排序)

        在每个volatile写操作的后面插入一个StoreLoad屏障(禁止该volatile写与后面的volatilc读/写重排序)

5、如何正确使用volatile

5.1 单一赋值可以,例如状态标志,但是含有符合运算赋值的不可以(i++)

public class UseVolatileDemo{
    private volatile static boolean flag = true;

    public static void main(String[] args){
        new Thread(() -> {
            while(flag) {
                //do something......循环
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            flag = false;
        },"t2").start();
    }
}

5.2 开销较低的读,写锁策略

当读远高于写的时候

        最方便的方法就是在读写两个方法都是加上synchronized;但是读用volatile的性能比synchronized高很多

public class UseVolatileDemo{
    //
   // 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
   // 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
     
    public class Counter{
        private volatile int value;

        public int getValue(){
            return value;   //利用volatile保证读取操作的可见性
        }
        public synchronized int increment(){
            return value++; //利用synchronized保证复合操作的原子性
        }
    }
}

5.3 DCL双锁案例

正常的DCL双锁案例:

public class SafeDoubleCheckSingleton{
    private static SafeDoubleCheckSingleton singleton; //-----这里没加volatile
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                    singleton = new SafeDoubleCheckSingleton();
                    //实例化分为三步
                    //1.分配对象的内存空间
                    //2.初始化对象
                    //3.设置对象指向分配的内存地址
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

单线程情况下分析:

        单线程环境下(或者说正常情况下),在"问题代码处",会执行如下操作,保证能获取到已完成初始化的实例

//三步
memory = allocate(); //1.分配对象的内存空间
ctorInstance(memory); //2.初始化对象
instance = memory; //3.设置对象指向分配的内存地址

多线程情况下(由于指令重排序):

        隐患:多线程环境下,在"问题代码处",会执行如下操作,由于重排序导致2,3乱序,后果就是其他线程得到的是null而不是完成初始化的对象 。(没初始化完的就是null

正常情况下
//三步
memory = allocate(); //1.分配对象的内存空间
ctorInstance(memory); //2.初始化对象
instance = memory; //3.设置对象指向分配的内存地址
```*


非正常情况

```java
//三步
memory = allocate(); //1.分配对象的内存空间
instance = memory; //3.设置对象指向分配的内存地址---这里指令重排了,但是对象还没有初始化
ctorInstance(memory); //2.初始化对象


实例化singleton分多步执行(分配内存空间、初始化对象、将对象指向分配的内存空间),
某些编译器为了性能原因,会将第二步和第三步进行重排序(分配内存空间、将对象指向分配的内存空间、初始化对象)。
这样,某个线程可能会获得一个未完全初始化的实例。

解决:

public class SafeDoubleCheckSingleton{
    //通过volatile声明,实现线程安全的延迟初始化。
    private volatile static SafeDoubleCheckSingleton singleton;
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                    //原理:利用volatile,禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

6、小总结

6.1 volatile特性有什么?

可见性不保证原子性

有序性(禁止指令重排序)

 

6.2 为什么我们java写了一个volatile关键字系统底层加入内存屏障?两者怎么做的关联?

        从字节码层面 javap -c xx.class 可以看出,编译的变量上添加了一个ACC_VOLATILE标识。

6.3 内存屏障是什么?

        内存屏障是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。也叫内存栅栏或栅栏指令。

6.4 内存屏障能干什么?

1、阻止屏障两边的指令重排序

2、写数据时加入屏障,强制将线程工作内存的数据刷回物理内存中

3、读数据时加入屏障,线程私有工作内存的数据会失效(缓存一致性协议),重新到主物理内存中获取最新数据

6.5 内存屏障四大指令?

三句话总结:

1、volatile写之前的操作,都禁止重排到volatile之后

2、volatile读之后的操作,都禁止重排到volatile之前

3、volatile写之后volatile读,禁止重排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

郭吱吱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值