浅谈Java:Volatile浅析

Volatile浅析

volatile关键字介绍

volatile关键字只能修饰类变量和实例变量,对于方法参数,局部变量以及实例常量,类常量多不能进行修饰。不如下面代码中MAX变量就无法使用volatile进行修饰。

我们先来看一个简单的程序

public class VolatileFoo {
    final static int MAX = 5;
    static int value = 0;

    public static void main(String[] args){
        new Thread(() -> {
            int localValue = value;
            while(localValue < MAX) {
                if(value != localValue) {
                    System.out.println("The value is updated to" + value);
                    localValue = value;
                }
            }
        }).start();

        new Thread(() -> {
            int localValue = value;
            while(localValue < MAX) {
                System.out.println("The value well be changed to" + ++localValue);
                value = localValue;

                try{
                    // 短暂休眠,为使上一个线程能过输出变化内容
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException ignored) {
                }
            }
        }).start();
    }
}

我们可以先想以下运行结果。时Thread-1更新一次,Thread-0就能够及时输出呢?下面我们来看下运行结果

The value is updated to 1
The value well be changed to 2
The value well be changed to 3
The value well be changed to 4
The value well be changed to 5

我们可以看到Thread-0线程几乎没有感知到Thread-1对value造成的变化,从而陷入了死循环,为什么呢?我们对代码坐下小小的调整:

static volatile int value = 0;

在value变量增加volatile关键字进行修饰,再来看下运行结果,你会发现Thread-0可以感知到Thread-1对value做出的修改。

The value is updated to 1
The value well be changed to 2
The value is updated to 2
The value well be changed to 3
The value is updated to 3
The value well be changed to 4
The value is updated to 4
The value well be changed to 5
The value is updated to 5

为什么会这样呢?我们来看下面的内容。

CPU缓存一致性问题

在针对缓存一致性问题的协议中最为出名的时Intel的MESI协议,MESI协议保证了每一个缓存中使用的共享变量副本多是一致的,他的大概思想是,当CPU在操作Cache中的数据时,如果发现该变量是一个共享变量,也就是说在其他的CPU Cache中也存在一个副本,那么就进行如下操作:

​ 1、读取操作:不做任何处理,只是将Cache中的数据读取到寄存器中
​ 2、写入操作:发出信号通知其他CPU将该变量的Cache line设置为无效。其他CPU在对该变量进行操作的时候,将从主内存中再次获取。

Java的内存模型

Java内存模型决定了一个线程对共享变量的写入何时对其他线程可见,Java内存模型定义了线程和主存之间的抽象关系,具体如下:

  1. 共享变量存储于内存之中,每个线程都可以访问。
  2. 每个线程都是私有的工作内存或者成为本地内存。
  3. 工作内存只存储该线程对共享变量的副本
  4. 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存
  5. 工作内存和Java内存模型一样也是一个抽象的概念,他其实并不是真实存在的,它涵盖了缓存、寄存器、编译优化以及硬件等。

假设主存有个共享变量X为0,线程1和线程2分别拥有共享变量X的副本,线程1此时将工作内存的X修改为1,同时刷到了主存中,当线程2想要去使用工作内存X的副本时,就会发现该变量副本已经失效了,必须到主存中重新去获取新的X的副本,存储在自己的工作内存中。这一点和CPU和CPU Cache之间的关系非常相似。

并发的三大特性以及JVM如何保证三大特性

1、原子性
a、多个原子性的操作在一起就不再具备原子性操作了
b、简单的读取与赋值操作是原子性的,将一个变量赋值给另一个变量的操作不是原子性的
c、Java内存模型只保证了基本读取和赋值的原子性操作,其他的均不保证,如果想要是的某些代码具备原子性,需要使用关键字Synchronized或者JUC中的lock。
总结:volatile不具备原子性

2、有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,在单线程的情况下,重排序并不会引起什么问题,但是在多线程模式下,重排序会影响程序的正常运行。Java提供了三种保证有些性的方式,具体如下:
a、使用volatile可以保证有序性
b、使用synchronized关键字来保证有序性
c、使用显示锁Lock来保证有序性
后两者采用同步机制来保证有序性。volatile保证有序性是直接禁止JVM和处理器对volatile关键字修饰的指令进行重排序,但是对于volatile前后无依赖的指令则无影响。

3、可见性

​ 在多线程的环境下,如果某个线程读取共享变量,则首先获取主存中该变量,然后存入工作内存中。以后工作就只需操作工作内存中的变量副本即可。同样如果对改变了执行了修改操作,则先将新值写入工作内存中然后再刷入主存中。但是什么时候最新值会被刷入主存中是不确定的。这也就解释了我们刚开始提到了那个代码Thread-1修改线程后Thread-0无法获取到value的最新变化了。

Java提供了三种方式来保证可见性:
a、使用volatile关键字,当一个变量被volatile关键字修饰时,对于共享资源的读操作会直接在内存中进行。对于共享资源的写操作也是先修改工作内存,但是修改完后会立即刷新到主内存中。
b、通过synchronized关键字能够保证可见性
c、通过JUC提供的显示锁Lock也能够保证可见性。

volatile的原理和实现机制

可以在OpenJDK下的unsafe.cpp源码中可以发现,被volatile修饰的变量存在一个“lock;”的前缀,源码如下:
```C++
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

……

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
asm volatile (LOCK_IF_MP(%4) "cmpxchg1 %1,(%3)"
: "=a" (exchage_value)
: "r" (exchage_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchage_value;
}



"lock;"前缀相当于是一个内存屏障,该内存屏障会为指令的执行提供如下几个保障:
    1、确保指令重排是不会将其后面的代码重排到内存屏障之前和之后
    2、确保指令在执行到内存屏障修饰的指令是前面的代码全部执行完毕
    3、强制将线程工作内存中的修改的值刷新至主内存中
    4、如果是写操作,则会导致其他的线程工作内存中的缓存数据失效

## 补充:volatile 和 synchronized区别

1、使用的区别
    a、volatile关键字只能用于修饰实变量或类变量,不能用于修饰方法以及方法参数、局部变量和常量等。
    b、synchronized关键字不能对变量进行修饰,只能用于修饰方法和语句块
    c、volatile修饰的变量可以为null,synchronized同步语句块的monitor对象不能为空

2、对原子性的保证
    a、volatile不能保证原子性
    b、由于synchronized是一种排他的锁机制,因此被synchronized修饰的同步代码块是无法被中途打断的,一次可以保证代码的原子性。

3、对可见性的保证
    a、两者均可以保证共享资源在多线程间的可见性,但是实现机制完全不相同。
    b、synchronized借助与JVM指令monitor enter 和 monitor exit通过排他的方式使得同步代码串行化,在monitor exit是所有共享资源的会被刷新到主内存中去。
    c、volatile使用机器指令“lock;"的方式迫使其他线程工作内存中的数据失效。

4、对有序性的保证
    a、volatile关键字禁止JVM编译器以及处理器对代码进行重排序,所以他能够保证有序性。
    b、synchronized是通过排他禁止实现同步代码块前后代码的有序性,而在同步代码块里面的代码无法保证其有序性。

5、其他
    a、volatile不会进入阻塞状态
    b、synchronized会是线程进入阻塞状态。

猜你喜欢

转载自blog.51cto.com/15138078/2670272