线程入门(五)---关键字volatile

volatile的作用

1、保证线程可见性
-MESI
-缓存一致性协议
2、禁止指令重排序
-DCL单例
-Double Check Lock
-Mgr06.java

一、线程可见性

线程本身并不直接与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作。这也是导致线程间数据不可见的本质原因。

对volatile变量的写操作与普通变量的主要区别有两点:

1、修改volatile变量时会强制将修改后的值刷新的主内存中。

2、修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。


我们用代码写个例子来看看volatile的作用:

在这里插入图片描述

上面的代码按照逻辑上应该是没有问题的,先是启动线程t1,输出“m start… ”,然后main函数中线程sleep1秒后把t.running 设置为false,此时应当输出"m end!"。我们看一下输出结果:
在这里插入图片描述
程序一直在运行,一直都没有结束,也没有输出"m end!”。
我们加上volatile关键字试试:


import java.util.concurrent.TimeUnit;

public class VolatileDemo1 {
        volatile boolean running=true;
        void m(){
            System.out.println("m start...");
            while(running) {

            }
            System.out.println("m end! ");
        }

    public static void main(String[] args) {
        VolatileDemo1 t=new VolatileDemo1();

        new Thread(t::m ,"t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t.running=false;
    }
}

输出结果:
在这里插入图片描述

停了一秒后便输出"m end!",看来volatiel还是有作用的~~~~~~~

我们来分析一下为什么???

在这里插入图片描述

每一个线程都有一份自己的本地内存,所有线程共用一份主内存。如果一个线程对主内存中的数据进行了修改,而此时另外一个线程不知道是否已经发生了修改,就说此时是不可见的。

在上面的没有用volatie的代码中,mian线程的running改了,主内存的数据也改了,可是t1线程的runnig不知道还没有知道发生了改变,所以t1的running还没有改变,running一直是true,所以程序一直不结束。

volatile关键字的作用很简单,就是一个线程在对主内存的某一份数据进行更改时,改完之后会立刻刷新到主内存。并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。这样一来就保证了可见性。

所以后面使用了volatile,程序就可以正常输出"m end!"并结束。

二、禁止指令重排序

我们用单例模式的双重校验来看一下Volatile的禁止指令重排序的作用。
在这里插入图片描述

在这里插入图片描述

这样的代码一般运行是不会出错的,可是在高并发的时候(例如京东抢拍)有可能会出错。

双重校验的写法:第一次判断是否为null是为了拒绝掉当对象不为空的时候剩余的线程。里面加锁是为了当对象为null的时候,此时同时进来两个线程(A和B两个线程),我们要保证只有一个线程才可以初始化对象,所以在这里面加上了锁,这样A拿到了锁进去初始化对象,然后进行返回,B再进去此时发现不为null,那么就不执行初始化的过程。这样就能保证上面的单例模式的正常运行,同时为系统也是节约了许多开销(避免每个线程进来加锁–懒汉式写法等。。)

我们来分析一段代码:
INSTANCE = new VolatileDemo2();

在这里插入图片描述
我们首先要理解对象实例化的步骤:

1、分配内存空间。
2、初始化对象。
3、将内存空间的地址赋值给对应的引用。

以上是一般正常的步骤,如果在高并发的时候,有可能2和3 的顺序会颠倒,导致还没有初始化,就把默认值直接赋值了。如果发生这种情况,那么此时拿到的对象也只是一个引用,对于后面的业务操作可能存在错误的发生。

为了防止这种情况发生,我们要加上volatile关键字:

public class VolatileDemo2 {
        private static volatile VolatileDemo2 INSTANCE;
        private VolatileDemo2(){}

        public  static VolatileDemo2 getInstance(){
            if(INSTANCE == null){
                synchronized (VolatileDemo2.class){
                    if(INSTANCE == null){
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        INSTANCE = new VolatileDemo2();
                    }
                }
            }
            return INSTANCE;
        }

    public static void main(String[] args){
            for(int i=0;i<100;i++){
                new Thread(()->{
                    System.out.println(VolatileDemo2.getInstance().hashCode());
                }).start();
            }
    }
}

输出结果也是一样的,只是保证在高并发时也不会出错。
在这里插入图片描述

再举个栗子

public class VolatileTest {

  public volatile static boolean shareFlag = false;

  public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
      System.out.print("开始执行线程1 =>");
      while (!shareFlag){  //shareFlag = false则一直死循环
        //System.out.println("shareFlag=" + shareFlag);
      }
      System.out.print("线程1执行完成 =>");
    }).start();

    Thread.sleep(2000);

    new Thread(() -> {
      System.out.print("开始执行线程2 =>");
      shareFlag = true;
      System.out.print("线程2执行完成 =>");
    }).start();
  }

}

输出:开始执行线程1 =>开始执行线程2 =>线程2执行完成 =>线程1执行完成

在这里插入图片描述

简单的说就是

  • 当线程2修改shareFlag的时候(参考Modify),告知bus总线我修改了共享变量shareFlag,
  • 线程1对Bus总线进行监听,当它获知共享变量shareFlag发生了修改就会将自己工作内存中的shareFlag副本删除使其失效。
  • 当线程1再次需要使用到shareFlag的时候,发现工作内存中没有shareFlag变量副本,就会重新从主内存中加载(read&load)

参考文章:JAVA中volatile介绍
你应该要理解的java并发关键字volatile
Java并发-volatile与JMM多线程内存模型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值