精华内容
下载资源
问答
  • volatile关键字的作用

    万次阅读 多人点赞 2019-06-09 23:46:30
    volatile关键字的作用

    1 保证内存可见性

    说到内存可见性就必须要提到Java的内存模型,如下图所示:

    如上图所示,所有线程的共享变量都存储在主内存中,每一个线程都有一个独有的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据。当修改完毕后,再把修改后的结果放回到主内存中。每个线程都只操作自己工作内存中的变量,无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

    上述的Java内存模型在单线程的环境下不会出现问题,但在多线程的环境下可能会出现脏数据,例如:如果有AB两个线程同时拿到变量i,进行递增操作。A线程将变量i放到自己的工作内存中,然后做+1操作,然而此时,线程A还没有将修改后的值刷回到主内存中,而此时线程B也从主内存中拿到修改前的变量i,也进行了一遍+1的操作。最后A和B线程将各自的结果分别刷回到主内存中,看到的结果就是变量i只进行了一遍+1的操作,而实际上A和B进行了两次累加的操作,于是就出现了错误。究其原因,是因为线程B读取到了变量i的脏数据的缘故。

    此时如果对变量i加上volatile关键字修饰的话,它可以保证当A线程对变量i值做了变动之后,会立即刷回到主内存中,而其它线程读取到该变量的值也作废,强迫重新从主内存中读取该变量的值,这样在任何时刻,AB线程总是会看到变量i的同一个值。

    1.1 MESI缓存一致性协议

    volatile可见性是通过汇编加上Lock前缀指令,触发底层的MESI缓存一致性协议来实现的。当然这个协议有很多种,不过最常用的就是MESI。MESI表示四种状态,如下所示:

    状态 描述
    M 修改(Modified) 此时缓存行中的数据与主内存中的数据不一致,数据只存在于本工作内存中。其他线程从主内存中读取共享变量值的操作会被延迟执行,直到该缓存行将数据写回到主内存后
    E 独享(Exclusive) 此时缓存行中的数据与主内存中的数据一致,数据只存在于本工作内存中。此时会监听其他线程读主内存中共享变量的操作,如果发生,该缓存行需要变成共享状态
    S 共享(Shared) 此时缓存行中的数据与主内存中的数据一致,数据存在于很多工作内存中。此时会监听其他线程使该缓存行无效的请求,如果发生,该缓存行需要变成无效状态
    I 无效(Invalid) 此时该缓存行无效

    假如说当前有一个cpu去主内存拿到一个变量x的值初始为1,放到自己的工作内存中。此时它的状态就是独享状态E,然后此时另外一个cpu也拿到了这个x的值,放到自己的工作内存中。此时之前那个cpu会不断地监听内存总线,发现这个x有多个cpu在获取,那么这个时候这两个cpu所获得的x的值的状态就都是共享状态S。然后第一个cpu将自己工作内存中x的值带入到自己的ALU计算单元去进行计算,返回来x的值变为2,接着会告诉给内存总线,将此时自己的x的状态置为修改状态M。而另一个cpu此时也会去不断的监听内存总线,发现这个x已经有别的cpu将其置为了修改状态,所以自己内部的x的状态会被置为无效状态I,等待第一个cpu将修改后的值刷回到主内存后,重新去获取新的值。这个谁先改变x的值可能是同一时刻进行修改的,此时cpu就会通过底层硬件在同一个指令周期内进行裁决,裁决是谁进行修改的,就置为修改状态,而另一个就置为无效状态,被丢弃或者是被覆盖(有争论)。

    当然,MESI也会有失效的时候,缓存的最小单元是缓存行,如果当前的共享数据的长度超过一个缓存行的长度的时候,就会使MESI协议失败,此时的话就会触发总线加锁的机制,第一个线程cpu拿到这个x的时候,其他的线程都不允许去获取这个x的值。


    2 禁止指令重排序

    指令的执行顺序并不一定会像我们编写的顺序那样执行,为了保证执行上的效率,JVM(包括CPU)可能会对指令进行重排序。比方说下面的代码:

    int i = 1;
    int j = 2;

    上述的两条赋值语句在同一个线程之中,根据程序上的次序,“int i = 1;”的操作要先行发生于“int j = 2;”,但是“int j = 2;”的代码完全可能会被处理器先执行。JVM会保证在单线程的情况下,重排序后的执行结果会和重排序之前的结果一致。但是在多线程的场景下就不一定了。最典型的例子就是双重检查加锁版的单例实现,代码如下所示:

    public class Singleton {
    
        private volatile static Singleton instance;
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    由上可以看到,instance变量被volatile关键字所修饰,但是如果去掉该关键字,就不能保证该代码执行的正确性。这是因为“instance = new Singleton();”这行代码并不是原子操作,其在JVM中被分为如下三个阶段执行:

    1. 为instance分配内存
    2. 初始化instance
    3. 将instance变量指向分配的内存空间

    由于JVM可能存在重排序,上述的二三步骤没有依赖的关系,可能会出现先执行第三步,后执行第二步的情况。也就是说可能会出现instance变量还没初始化完成,其他线程就已经判断了该变量值不为null,结果返回了一个没有初始化完成的半成品的情况。而加上volatile关键字修饰后,可以保证instance变量的操作不会被JVM所重排序,每个线程都是按照上述一二三的步骤顺序的执行,这样就不会出现问题。

    2.1 内存屏障

    volatile有序性是通过内存屏障实现的。JVM和CPU都会对指令做重排优化,所以在指令间插入一个屏障点,就告诉JVM和CPU,不能进行重排优化。具体的会分为读读、读写、写读、写写屏障这四种,同时它也会有一些插入屏障点的策略,下面是JMM基于保守策略的内存屏障点插入策略:

    屏障点 描述
    每个volatile写的前面插入一个store-store屏障 禁止上面的普通写和下面的volatile写重排序
    每个volatile写的后面插入一个store-load屏障 禁止上面的volatile写与下面的volatile读/写重排序
    每个volatile读的后面插入一个load-load屏障 禁止下面的普通读和上面的volatile读重排序
    每个volatile读的后面插入一个load-store屏障 禁止下面的普通写和上面的volatile读重排序

    上面的插入策略非常保守,但是它可以保证在任意处理器平台上的正确性。在实际执行时,编译器可以省略没必要的屏障点,同时在某些处理器上会做进一步的优化。


    3 不保证原子性

    需要重点说明的一点是,尽管volatile关键字可以保证内存可见性和有序性,但不能保证原子性。也就是说,对volatile修饰的变量进行的操作,不保证多线程安全。请看以下的例子:

    public class Test {
    
        private static CountDownLatch countDownLatch = new CountDownLatch(1000);
        private volatile static int   num            = 0;
    
        public static void main(String[] args) {
            ExecutorService executor = Executors.newCachedThreadPool();
            for (int i = 0; i < 1000; i++) {
                executor.execute(() -> {
                    try {
                        num++;
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        countDownLatch.countDown();
                    }
                });
            }
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executor.shutdown();
            System.out.println(num);
        }
    }

    静态变量num被volatile所修饰,并且同时开启1000个线程对其进行累加的操作,按道理来说,其结果应该为1000,但实际的情况是,每次运行结果可能都是一个小于1000的数字(也有结果为1000的时候,但出现几率很小),并且不固定。那么这是为什么呢?原因是因为“num++;”这行代码并不是原子操作,尽管它被volatile所修饰了也依然如此。++操作的执行过程如下面所示:

    1. 首先获取变量i的值
    2. 将该变量的值+1
    3. 将该变量的值写回到对应的主内存中

    虽然每次获取num值的时候,也就是执行上述第一步的时候,都拿到的是主内存的最新变量值,但是在进行第二步num+1的时候,可能其他线程在此期间已经对num做了修改,这时候就会触发MESI协议的失效动作,将该线程内部的值作废。那么该次+1的动作就会失效了,也就是少加了一次1。比如说:线程A在执行第一步的时候读取到此时num的值为3,然后在执行第二步之前,其他多个线程已经对该值进行了修改,使得num值变为了4。而线程A此时的num值就会失效,重新从主内存中读取最新值。也就是两个线程做了两次+1的动作,但实际的结果最后只加了一次1。所以这也就是最后的执行结果为什么大概率会是一个小于1000的值的原因。

    所以如果要解决上面代码的多线程安全问题,可以采取加锁synchronized的方式,也可以使用JUC包下的原子类AtomicInteger,以下的代码演示了使用AtomicInteger来包装num变量的方式:

    public class Test {
    
        private static CountDownLatch countDownLatch = new CountDownLatch(1000);
        private static AtomicInteger  num            = new AtomicInteger();
    
        public static void main(String[] args) {
            ExecutorService executor = Executors.newCachedThreadPool();
            for (int i = 0; i < 1000; i++) {
                executor.execute(() -> {
                    try {
                        num.getAndIncrement();
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        countDownLatch.countDown();
                    }
                });
            }
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executor.shutdown();
            System.out.println(num);
        }
    }

    多次运行上面的代码,结果都为1000。

    展开全文
  • Java volatile关键字的作用volatile关键字的作用使变量在多个线程之间可见。package com.wkcto.volatilekw;/*** volatile的作用可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取* Author: 老崔*/public...

    Java volatile关键字的作用

    volatile关键字的作用使变量在多个线程之间可见。

    package com.wkcto.volatilekw;

    /**

    * volatile的作用可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取

    * Author: 老崔

    */

    public class Test02 {

    public static void main(String[] args) {

    //创建PrintString对象

    PrintString printString = new PrintString();

    //开启子线程,让子线程执行printString对象的printStringMethod()方法

    new Thread(new Runnable() {

    @Override

    public void run() {

    printString.printStringMethod();

    }

    }).start();

    //main线程睡眠1000毫秒

    try {

    Thread.sleep(1000);

    } catch (InterruptedException e) {

    e.printStackTrace();

    }

    System.out.println("在main线程中修改打印标志");

    printString.setContinuePrint(false);

    //程序运行,查看在main线程中修改了打印标志之后 ,子线程打印是否可以结束打印

    //程序运行后, 可能会出现死循环情况

    //分析原因: main线程修改了printString对象的打印标志后, 子线程读不到

    //解决办法: 使用volatile关键字修饰printString对象的打印标志.

    // volatile的作用可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取

    }

    //定义类打印字符串

    static class PrintString{

    private volatile boolean continuePrint = true;

    public PrintString setContinuePrint(boolean continuePrint) {

    this.continuePrint = continuePrint;

    return this;

    }

    public void printStringMethod(){

    System.out.println(Thread.currentThread().getName() + "开始....");

    while ( continuePrint ){

    }

    System.out.println(Thread.currentThread().getName() + "结束++++++++++++++");

    }

    }

    }

    volatile与synchronized比较

    ● volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好; volatile只能修饰变量,而synchronized可以修饰方法,代码块. 随着JDK新版本的发布,synchronized的执行效率也有较大的提升,在开发中使用sychronized的比率还是很大的。

    ● 多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞。

    ● volatile能保证数据的可见性,但是不能保证原子性; 而synchronized可以保证原子性,也可以保证可见性。

    ● 关键字volatile解决的是变量在多个线程之间的可见性; synchronized关键字解决多个线程之间访问公共资源的同步性。

    展开全文
  • volatile关键字的作用:让线程每次都从主内存中读/写数据,修改完成后,立即写回使用场景:一个线程写,多个线程读volatile是非线程安全的,并不是用来实现同步的,作用是保证变量的可见性:线程每次读取都是最新的值...

    volatile关键字的作用:让线程每次都从主内存中读/写数据,修改完成后,立即写回

    使用场景:一个线程写,多个线程读

    volatile是非线程安全的,并不是用来实现同步的,作用是保证变量的可见性:线程每次读取都是最新的值(因为每次都是从主内存读取)

    volatile不能保证原子性:多线程操作volatile修饰的变量,不能保证原子性

    2471d844a0acb1682c57e48d7f779bfd.png

    未使用volatile关键字之前:

    public class VolatileTest {    private static int num=10;    public static void main(String[] args){        Runnable reader=()->{            while(num==10){                //nothing to do            }            //这两行代码不会被执行            String name=Thread.currentThread().getName();            System.out.println("线程"+name+"读取到的是"+num);        };        for(int i=1;i<=3;i++){            new Thread(reader).start();        }        try{            System.out.println("主线程休眠3秒...");            Thread.sleep(3000);            System.out.println("主线程休眠结束");        }catch (InterruptedException e){            e.printStackTrace();        }        new Thread(()->{            num=15;            System.out.println("写线程完成写入...");        }).start();    }}

    虽然num被修改成了15,但是因为在修改成15之前,创建的3个子线程已经读取了num=10,已经存储在了线程本地中,而线程并不会每次都去重新读取num的值,所以读取的一直是存储在线程本地的值10。要解决这个问题可以使用volatile关键字,让线程每次都去重新读取num的值保证读取到的都是最新值

    public class VolatileTest {    //加上volatile关键字    private volatile static int num=10;    public static void main(String[] args){        Runnable reader=()->{            while(num==10){                //nothing to do            }            String name=Thread.currentThread().getName();            System.out.println("线程"+name+"读取到的是"+num);        };        for(int i=1;i<=3;i++){            new Thread(reader).start();        }        try{            System.out.println("主线程休眠3秒...");            Thread.sleep(3000);            System.out.println("主线程休眠结束");        }catch (InterruptedException e){            e.printStackTrace();        }        new Thread(()->{            num=15;            System.out.println("写线程完成写入...");        }).start();    }}
    展开全文
  • 1.volatile关键字是防止在共享空间发生读取错误。只保证其可见性,不保证原子性;使用volatile指每次从内存中读取数据,而不是从编译器优化后缓存中读取数据,简单来讲就是防止编译器优化。2.在单任务环境中,...

    1.volatile关键字是防止在共享的空间发生读取的错误。只保证其可见性,不保证原子性;使用volatile指每次从内存中读取数据,而不是从编译器优化后的缓存中读取数据,简单来讲就是防止编译器优化。

    2.在单任务环境中,如果在两次读取变量之间不改变变量的值,编译器就会发生优化,会将RAM中的值赋值到寄存器中;由于访问寄存器的效率要高于RAM,所以在需要读取变量时,直接寄存器中获取变量的值,而不是从RAM中。

    3.在多任务环境中,虽然在两次读取变量之间不改变变量的值,在一些情况下变量的值还是会发生改变,比如在发生中断程序或者有其他的线程。这时候如果编译器优化,依旧从寄存器中获取变量的值,修改的值就得不到及时的响应(在RAM还未将新的值赋值给寄存器,就已经获取到寄存器的值)。

    4.要想防止编译器优化,就需要在声明变量时加volatile关键字,加关键字后,就在RAM中读取变量的值,而不是直接在寄存器中取值。

    66c0020eab5b93274755f692ec344ea6.png
    展开全文
  • 1. volatile关键字的作用:保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。如以下代码片段,isShutDown被置为true后,doWork方法仍有执行...
  • 序言volatile关键字的特性及作用想要理解volatile关键字的作用,需要先对jvm中的内存模型有所了解。Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存(比如CPU的寄存器)...
  • 本文主要介绍Java语言中的volatile关键字,内容涵盖volatile保证内存可见性、禁止指令重排等。废话不多说,直接进去主题1 保证内存可见性1.1 基本概念可见性是指线程之间可见性,一个线程修改状态对另一个线程...
  • 本文将会介绍volatile关键字的作用以及其实现原理。volatile作用volatile在并发编程中扮演着重要的角色,volatile是轻量级的synchronized,volatile关键字有两个作用:1)保证共享变量的可见性可见性的意思是当一个...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,885
精华内容 1,154
关键字:

volatile关键字的作用