从底层理解volatile关键字

  1. CPU内存模型

    根据摩尔定律,CPU的运行性能每18-24个月就可以增加一倍,而主内存的性能在很长一段时间并没有质的提升.导致主内存的运行速率跟不上CPU的需求,所以在CPU与主内存之间其实存在一个CPU多级缓存,当然这个CPU多级缓存是存在于CPU内部的.

在这里插入图片描述

打开电脑的任务管理器也可以看到CPU的多级缓存.当CPU第一次处理该数据时,会先将数据从主内存加载到CPU多级缓存中,然后后续的操作会直接操作CPU多级缓存中的数据.

在这里插入图片描述

  1. JAVA线程内存模型

    JAVA的线程内存模型时基于CPU缓存模型来建立的.

    JAVA内存模型中规定所有变量都存储在主内存中,主要对应java的堆内存.这里提到的变量实际上指的是共享变量,存在线程间竞争的变量,比如实例变量,静态变量,构成数组对象的元素等,而局部变量和方法参数是因为线程私有的,所以不存在线程间共享和竞争关系.

在这里插入图片描述

每个线程有着自己独有的工作内存,工作内存中保存了被该线程使用到的变量,这些变量来自主内存变量分副本拷贝.线程对变量的所有读写操作都必须在工作内存中进行,不能直接读写主内存中的变量.而不同的线程间的工作内存也是独立的,一个线程无法访问其它线程工作内存中的变量.

我们写个程序测试一下:

public class VolatileDemo1 {

    static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {

                System.out.println("Thread1 waiting flag********************");
                while (!flag) { //线程一:只要flag没有变为TRUE,则一直堵塞在这里

                }
                System.out.println("Thread1 success===================="); //Flag没有变为true,这句话不会打印
            }
        }).start();

        Thread.sleep(2000);//确保第一个线程先执行

        new Thread(new Runnable() {//线程二将flag变为true
            @Override
            public void run() {
                changeFlag();
            }
        }).start();

    }

    private static void changeFlag() {

        System.out.println("Thread2 start change Flag*************");
        flag = true;
        System.out.println("Thread2 end change Flag*************");
    }

}

输出结果如下:

Thread1 waiting flag********************
Thread2 start change Flag*************
Thread2 end change Flag*************

线程二将flag变为了true,但是线程一却检测不到,一直在堵塞

线程工作时,把需要的变量从主内存中拷贝到自己的工作内存,线程运行结束之后再将自己工作内存中的变量写回到主内存中,而多个线程对变量的交互只能通过主内存来实现.

  1. 添加volatile关键字

    将上述代码静态变量增加volatile修饰

    static volatile boolean flag = false;

    运行结果如下:

    Thread1 waiting flag********************
    Thread2 start change Flag*************
    Thread2 end change Flag*************
    Thread1 success=========================

  2. 八大原子操作
    • read(读取):从主内存读取数据
    • load(载入):将主内存读取到的数据写入到工作内存
    • use(使用):从工作内存读取数据来计算
    • assign(赋值):将计算好的值重新赋值到工作内存中
    • store(存储):将工作内存中的数据写入到主内存中
    • write(写入):将store过去的变量值赋值给主内存中的变量
    • lock(锁定):将主内存变量加锁,标识为线程独占状态
    • unlock(解锁):将主内存变量解锁,解锁后其它线程可以锁定该变量

    大家看到这几个方法基本上已经脑补出线程内存的操作过程了

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k89IgsGX-1597287361161)(D:\mingbyte\typora\image-20200812145655900.png)]

  3. volatile是如何实现共享变量线程可见性的呢?
    1. 总线加锁:*(性能很低)

      cpu从主内存读取数据到高速缓存,会再总线对这个数据加锁,这样其它线程没法取读或写这个数据,直到这个线程使用完数据释放锁之后其它线程才能读取该数据

    2. MESI缓存一致性协议

      M:modified修改
      E:exclusive独占
      S:shared共享
      I:invalid无效
      多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其它线程通过总线嗅探机制可以感知到数据的变化而将自己缓存里的数据失效

  4. volatile并不能保证并发线程的原子性

    并发编程的三大特性

    • 可见性

    • 原子性

    • 有序性

      Volatile保证可见性与有序性,但不是保证原子性,保证原子性需要借助synchronized这样的锁机制
      在这里插入图片描述

      下面我们写一段代码证明一下

      public class VolatileDemo2 {
      
          //使用volatile修饰,保证内存可见性
          public static volatile int num = 0;
      
          public static void increment() {
              num++;
          }
      
          public static void main(String[] args) throws InterruptedException {
      
              //10线程,每个线程对num增加1000次,理论上最后num应该是10000
              Thread[] threads = new Thread[10];
              for (int i = 0; i < threads.length; i++) {
      
                  threads[i] = new Thread(new Runnable() {
                      @Override
                      public void run() {
                          for (int i = 0; i < 1000; i++) {
                              increment();
                          }
                      }
                  });
                  threads[i].start();
              }
      
              //join方法,保证这是个线程跑完之后再输出num的值
              for (Thread thread : threads) {
                  thread.join();
              }
      
              System.out.println(num);
      
          }
      }
      

      输出结果:运气好的情况下可以得到10000,但大多数情况下是小于10000的

      原因就在于,当第一个线程修改了num的值由0变为1,还未store-write到主内存中去,此时线程二从主内存中读取数据依然是0,然后进行++操作变为1.然后线程1将数据写入到主内存中,一句总线嗅探机制会将线程二中的值判为失效,相当于少了一次循环,所以最后num值会小于10000

在这里插入图片描述

 解决办法时对increment()方法加锁
  public static synchronized void increment() {
      num++;
  }
  1. volatile保证并发编程的顺序性
    public class VolatileDemo3 {
        static int x = 0;
        static int y = 0;
    
        public static void main(String[] args) throws InterruptedException {
    
            Set<String> resultSet = new HashSet<>();
            Map<String, Integer> resultMap = new HashMap<>();
    
            for (int i = 0; i < 1000000; i++) {
                x = 0;
                y = 0;
                resultMap.clear();
                Thread t1 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        int a = y;
                        x = 1;
                        resultMap.put("a", a);
    
                    }
                });
    
    
                Thread t2 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        int b = x;
                        y = 1;
                        resultMap.put("b", b);
                    }
                });
                t1.start();
                t2.start();
                t1.join(); //保证两个线程执行完成之后再打印
                t2.join();
                resultSet.add("a=" + resultMap.get("a") + "," + "b=" + resultMap.get("b"));
    
                System.out.println(resultSet);
            }
    
        }
    }
    

    输出结果:

    [a=1,b=0, a=0,b=1, a=1,b=1,a=0,b=0]

    因为指令重排序的发生,导致四种情况都有可能发生

指令重排序表现为两个层面:
1.虚拟机层面:为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则将程序编写顺序打乱—即卸载后面的代码在时间顺序上可能会限制性,以尽可能充分利用CPU.
2.在硬件层面,由于CPU指令寄存器的操作速度远快于内存速度,所以CPU会将接收的指令按照其规则重排序
8. ###### happens-before

因为JVM会对代码进行编译优化,指令会出现重排序的情况,为了避免编译优化对并发编程安全性的影响,为了保证程序再多线程条件下运行结果能够与单一线程下一致,引入happengs-before规则.

happens-before规则的主要目的是用来确保并发情况下数据的正确性.

public class VolatileExample {
    int x = 0 ;
    volatile boolean v = false;
    public void writer(){
        x = 42;
        v = true;
    }

    public void reader(){
        if (v == true){
            System.out.println(x);// 这里x会是多少呢
        }
    }
}

**问题:**假设有两个线程A和B,A执行了writer方法,B执行reader方法,那么B线程中独到的变量x的值会是多少呢?

dk1.5之前,线程B读到的变量x的值可能是0,也可能是42,jdk1.5之后,变量x的值就是42了。原因是jdk1.5中,对volatile的语义进行了增强。来看一下happens-before规则在这段代码中的体现。

happens-before:

  • 程序的顺序性规则

    一个线程中,按照程序的顺序,前面的操作happens-before后续的任何操作

    顺序性是指:我们可以按照顺序推按程序的执行结果,但是编译器未必一定会按照这个顺序编译,但是编译器结果一定==顺序推演的结果

  • volatile规则

    对一个volatile变量的写操作,happens-before后续对这个变量的读操作

  • 传递性规则

    如果A happens-before B,B happens-before C,那么A happens-before C

  • 管程中的所规则

    对一个锁的解锁操作,happens-before后续对这个锁的加锁操作

  • 线程start()规则

    线程A启动线程B,线程B中可以看到主线程启动B之前的操作…也就是start()

    happens-before 线程B中的操作

  • 线程join()规则

    主线程A等在子线程B完成,当子线程B执行完毕后,主线程A可以看到线程B的所有操作,也就是说子线程B中的任意操作,happens-before join()返回

as-if-serial:

as-if-serial语义是:不管怎么重排序,单线程程序的执行结果不能被改变,编译器,runtime和处理器都必须遵守as-if-serial语义.所以编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值