Java volatile关键字介绍

Posted 发量尚可仍需努力

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java volatile关键字介绍相关的知识,希望对你有一定的参考价值。

Java volatile关键字介绍

Java中的`volatile`是一种关键字,用于保证多个线程之间对共享变量的可见性。当一个变量被声明为`volatile`时,对这个变量的读取和写入操作都会直接从主内存中读取和写入,而不是从线程本地缓存中读取和写入。

具体来说,volatile关键字能够保证以下两个特性:

  1. 可见性:当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程中的volatile变量缓存无效。
  2. 有序性:使用volatile关键字修饰共享变量通过禁止指令重排序来保证有序性。

volatile保证可见性的代码演示

public class VolatileExample 
    private volatile Integer num = 0;
    public void run()
        System.out.println("running...");
        for (int i = 0; i < 100; i++) 
            try 
                num++;
                Thread.sleep(100);
             catch (InterruptedException e) 
                throw new RuntimeException(e);
            
        
    
    public void stop()
        while (true) 
            if (num==100)
                System.out.println("num is 100, stop!");
                return;
            
        
    
    public static void main(String[] args) 
        /*
        * 在类中定义一个volatile修饰的变量初始为0
        * 线程1 执行run方法将num递加到100
        * 线程2 执行stop方法判断num是否等于100 等于100时退出
        * 如果线程2先执行 且可以查看到num==100,则表明volatile可以保证不同线程对同一个变量可见
        * */
        VolatileExample ve = new VolatileExample();
        new Thread(()->
            ve.stop();
        ).start();
        new Thread(()->
            ve.run();
        ).start();
    

volatile保证可见性和有序性的原理

Java中的volatile关键字主要是通过内存屏障(Memory Barrier)来实现可见性和有序性的。

内存屏障是一种硬件或软件机制,用于限制编译器和处理器对内存访问的优化,以确保对内存操作的顺序和可见性。Java中的内存屏障有以下三种:

  1. Load Barrier(读屏障):确保本条屏障之前的所有读操作都完成。
  2. Store Barrier(写屏障):确保本条屏障之前的所有写操作都完成。
  3. Full Barrier(完全屏障):同时包含读屏障和写屏障的效果,确保本条屏障之前的所有读/写操作都完成。

在使用volatile关键字修饰变量时,Java编译器会在生成字节码时插入内存屏障,以确保对volatile变量的读写操作都是有序的,并且保证了变量的可见性。

具体来说,当一个线程对volatile变量进行写操作时,Java虚拟机会在写操作的前后插入写屏障。这个写屏障会强制将写入操作刷新到主内存中,同时会使其他线程中的本地缓存失效。同样地,当一个线程对volatile变量进行读操作时,Java虚拟机会在读操作的前后插入读屏障。这个读屏障会强制从主内存中读取变量的值,而不是从线程本地缓存中读取。

博客:Volatile如何保证可见性和有序性?

java多线程03-----------------volatile内存语义

java多线程02-----------------volatile内存语义

  volatile关键字是java虚拟机提供的最轻量级额的同步机制。由于volatile关键字与java内存模型相关,因此,我们在介绍volatile关键字之前,对java内存模型进行更多的补充(之前的博文也曾介绍过)。

  1. java内存模型(JMM)

  JMM是一种规范,主要用于定义共享变量的访问规则,目的是解决多个线程本地内存与共享内存的数据不一致、编译器处理器的指令重排序造成的各种线程安全问题,以保障多线程编程的原子性、可见性和有序性。
  JMM规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程中的工作内存中存储了该线程用到的变量的主内存的拷贝,各线程对变量的所有操作都必须在工作内存中进行,
线程之间的变量值的传递都必须通过主内存来进行。

  JMM定义了8中操作实现主内存与工作内存的交互协议:
    1)lock:作用于主内存,它把一个变量标识为一条线程的独占状态。
    2)unlock:作用于主内存,它把一个处于锁定状态的变量的释放出来。
    3)read:作用于主内存,它把一个变量的值从主内存传输到线程的工作内存中。
    4)load:作用于工作内存,它把从主内存中read到的值放入工作内存的变量副本中。
    5)use:作用于工作内存,它把一个变量的值从主内存传递给执行引擎
    6)assign:作用与工作内存,它把一个从执行引擎接收到的值赋值给工作内存的变量。
    7)store:作用于工作内存,把工作内存中一个变量的值传送到主内存。
    8)write:作用于主内存,它把store操作从工作内存中得到的值放入主内存中的变量中。

  这8中操作以及对着8中操作的规则的限制就能确定哪些内存访问在并发条件下是线程安全的,这种方式比较繁琐,jdk1.5之后提出了提出了happens-before规则来判断线程是否安全。

  可以这么理解,happens-before规则是JMM的核心.Happens-before就是用来确定两个操作的执行顺序。这两个操作可在同一线程中,也可以在两个线程中。
happens-before规定:如果一个操作happens-before另个一操作,那么第一个操作的结果对第二个操作可见(但这并不意味着处理器必须按照happens-before顺序执行,只要不改变执行结果,可任意优化)。happens-before规则已在前边博文中介绍,这里不再重复(http://www.cnblogs.com/gdy1993/p/9117331.html)

  JMM内存规则仅仅是一种规则,规则的最终落实是通过java虚拟机、编译器以及处理器一同协作来落实的,而内存屏障是java虚拟机、编译器、处理器之间沟通的纽带。
而java原因封装了这些底层的具体实现与控制,提供了synchronized、lock和volatile等关键字的来保障多线程安全问题。

  2. volatile关键字

  (1)volatile对可见性的保证

  在介绍volatile关键字之前,先来看这样一段代码:

      //线程1
        boolean stop = false;
        while(!stop) {
            doSomething();
        }
        
        //线程2
        stop = true;

  有两个线程:线程1和线程2,线程1在stop==false时,不停的执行doSomething()方法;线程2在执行到一定情况时,将stop设置为true,将线程1中断,很多人采用这种方式中断线程,但这并不是安全的。因为stop作为一个普通变量,线程2对其的修改,并不能立刻被线程1所感知,即线程1对stop的修改仅仅在自己的工作内存中,还没来的急写入主内存,线程2工作内存中的stop并未修改,可能导致线程无法中断,虽然这种可能性很小,但一旦发生,后果严重。

  而使用volatile变量修饰就能避免这个问题,这也是volatile第一个重要含义:

    volatile修饰的变量,能够保证不同线程对这个变量操作的可见性,即一个线程修改了这个变量的值,这个新值对于其他线程是立即可见的。

    volatile的对可见性保证的原理:

  对于volatile修饰的变量,当某个线程对其进行修改时,会强制将该值刷新到主内存,这就使得其他线程对该变量在各自工作内存中的缓存无效,因而在其他线程对该变量进行操作时,必须从主内存中重新加载

   (2)volatile对原子性的保障?

  首先来看这样一段代码(深入理解java虚拟机):

public class VolatileTest {
    public static volatile int race = 0;
    
    public static void increase() {
        race++;
    }
    
    public static final int THREAD_COUNT = 20;
    
    public static void main(String[] args) {
        Thread[] threads = new Thread[THREAD_COUNT];
        for (Thread t : threads) {
            t = new Thread(new Runnable() {
                
                @Override
                public void run() {
                    for(int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            t.start();
        }
        
        while(Thread.activeCount() > 1) {
            Thread.yield();
        }
        
        System.out.println(race);//race < 200000
        
    }
}

   race是volatile修饰的共享变量,创建20个线程对这个共享变量进行自增操作,每个线程自增的次数为10000次,如果volatile能够保证原子性的话,最终race的结果肯定是200000。但结果不然,每次程序运行race\'的值总是小于200000,这也侧面证明了volatile并不能保证共享变量操作的原子性。原理如下:

  线程1读取了race的值,然后cp分配的时间片结束,线程2此时读取了共享变量的值,并对race进行自增操作,并将操作后的值刷新到主内存,此时线程1已经读取了race的值,因此保留的依然是原来的值,此时这个值已是旧值,对race进行自增操作后刷新到主内存,因此主内存中的值也是旧值。这也是volatile仅仅能保障读到的是相对新值的原因。

  (3)volatile对有序性的保障

  首先来看这样一段代码:

      //线程1
        boolean initialized = false;
        context = loadContext();
        initialized = true;
        
        
        //线程2
        while(!initialized) {
            sleep();
        }
        
        doSomething(context);

  线程2在initialized变量为true时,使用context变量完成一些操作;线程1负责加载context,并在加载完成后将initialized变量设为true。但是,由于initialized只是一个普通变量,普通变量仅仅能够保证在该方法的执行过程中,所有依赖赋值结果的地方都能获得正确的值,而不能保证变量的赋值顺序与程序代码的执行顺序一致。因此就可能出现这样一种情况,当线程1将initialized变量设为true时,context依然没有加载完成,但线程2由于读到initialized为true,就可能执行了doSomething()方法,可能会产生非常奇怪的效果。

  而volatile的第二个语义就是禁止重排序: 

    写volatile变量的操作与该操作之前的任何读写操作都不会被重排序;

    读volatile变量操作与该操作之后的任何读写操作都不会重排序。

  (4) volatile的底层实现原理

  java语言底层是通过内存屏障来实现volatile语义的。

  对于volatile变量的写操作:
  ①java虚拟机会在该操作之前插入一个释放屏障(loadstore+storestore),释放屏障禁止了volatile变量的写操作与该操作之前的任何读写操作的重排序。
  ②java虚拟机会在该操作之后插入一个存储屏障(storeload),存储屏障使得对volatile变量的写操作能够同步到主内存。
  对于volatile变量的读操作:
  ③java虚拟机会在该操作之前插入一个loadload,使得每次对volatile变量的读取都从主内存中重新加载(刷新处理器缓存)
  ④java虚拟机会在该操作之后插入一个获得屏障(loadstore+loadload),使得volatile后的任何读写操作与该操作进行重排序。
  ①③保障可见性,②④保障有序性。

  (5)volatile关键字与happens-before的关系

  Happens-before规则中的volatile规则为:对于一个volatile域的写happens-before后续每一个针对该变量的读操作。

 

  写线程执行write(),然后读线程执行read()方法,图中每个箭头都代表一个happens-before关系,黑色箭头是根据程序顺序规则,蓝色箭头根据volatile规则,红色箭头是根据传递性推出的,即操作2happens-before操作3,即对volatile共享变量的更新操作排在后续读取操作之前,对volatile变量的修改对后续volatile变量的读取可见。

   

 

以上是关于Java volatile关键字介绍的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程-volatile可见性的介绍

java里volatile关键字有啥特性?

多线程下的volatile关键字使用详解及Java先行发生原则

java内存模型及volatile关键字

java内存模型及volatile关键字

Java并发编程:volatile关键字解析