或者说,volatile解决什么问题?html
我本身的总结:volatile解决多线程下变量访问的内存可见性问题,用于线程间通讯。java
通讯怎能理解呢,线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A经过
主内存向线程B发送消息。c++
java语言标准规范对volatile的描述是这样的:程序员
The Java programming language allows threads to access shared variables (§17.1). As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.编程
A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable (§17.4).缓存
上面这段话摘自这个连接,有兴趣的能够本身点开看。多线程
https://docs.oracle.com/javas...架构
大概意思是,java语言容许多个线程访问共享变量。为了保证共享变量能准确一致的更新,线程要保证经过锁的机制单独得到这个变量。java提供了一种机制,容许你把变量定义成volatile,在某些状况下比直接用锁更加方便。并发
若是一个变量被定义成volatile,java内存模型确保全部线程看到的这个共享变量是一致的。oracle
这个一致怎么理解呢?继续往下看。
先来看一幅图,
这是一幅计算的内存架构图。
如今的CPU大部分都是多核的,在计算机内部,变量读写的流程是这样的:
这里的一个关键点是,何时刷新?答案是不知道。咱们不能假设CPU何时会刷新。这样就会带来一些问题,好比一个线程写完一个共享变量,尚未刷新到主内存。而后另外一个线程读这个变量仍是旧的值,在不少场景下,这个结果和程序员指望的并不一致。
幸运的是,咱们虽然不知道CPU何时刷新,可是咱们能够强制CPU执行刷新。
再来看一个图,这是JAVA的内存模型图。
本地内存是JVM里一个抽象的概念,它能够涵盖寄存器,缓存等。
咱们把这两幅图对应起来,能够这样解释。
在JAVA中,当一个线程写变量时,会先把这个变量从主内存拷贝一份线程的本地内存,而后在本地内存操做。操做完成以后,再刷新到主内存。只有刷新后,另外一个线程才能读取新的值。
来看个例子:
public class VolatileTest implements Runnable { private boolean running = true; @Override public void run() { if (running) { System.out.println("I am running"); } } public void stop() { running = false; } }
这段代码在多线程环境下执行的时候,假设A线程正在执行run
方法,B线程执行了stop
方法,咱们的程序无法保证A线程何时会立刻中止。由于这取决于CPU何时进行刷新,把最新变量的值同步到主内存。
解决方法是,把running
这个共享变量用volatile修饰便可,这样能够保证B线程的修改会马上刷新到主内存,对其它线程可见。
public class VolatileTest implements Runnable { private volatile boolean running = true;
再来看个稍微复杂一点的例子。
public class VolatileTest { public volatile int a = 0; volatile boolean flag = false; public void write() { a = 1; // 位置1 flag = true; //// 位置2 } public void read() { if (flag) { // 位置3 int i = a; // 位置4 } } }
Java规范对于volatile变量规则是:对一个volatile域的写,happens-before于任意后续对这个volatile域的
读。
假设线程A执行writer()方法以后,线程B执行reader()方法。根据volatile变量的happens-before规则,位置2必然先于位置3执行。同时咱们知道在同一个线程中,全部操做必须按照程序的顺序来执行,因此位置1确定早于位置2,位置3早于位置4。而后咱们能推出位置1早于位置4。
这样的顺序是符合咱们预期的。
这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之
前全部可见的共享变量,在B线程读同一个volatile变量后,将当即变得对B线程可见。
经过上面的例子,咱们能够总结下volatile的使用场景。
一般是,存在一个或者多个共享变量,会有线程对他们写操做,也会有其它线程对他们读操做。这样的变量都应该使用volatile修饰。
ConcurrentHashMap里用到了一些volatile的操做,好比:
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; Node(int hash, K key, V val, Node<K,V> next) { this.hash = hash; this.key = key; this.val = val; this.next = next; } ...
能够看到,用于存储值的value变量就是volatile类型,这样能够保证在多线程读取的时候,不会读到过时的值。之因此不会读到过时的值,是由于根据Java内存模型的happen before原则,对volatile字段的写入操做先于读操做,即便两个线程同时修改和获取volatile变量,get操做也能拿到最新的值,这是用volatile替换锁的经典应用场景。
不要过分使用volatile,没必要要的场景没有必要用volatile修饰变量,尽管这样作程序也不会出什么错。
根据前面的描述,volatile至关于给变量的操做加了“锁”,每次操做都有加锁和释放锁的动做,效率天然会受影响。
对volatile常常有一中误解就是,它能够保证原子操做。
经过上面的例子,咱们知道,volatile关键字能够保证内存可见性,指令执行的有序性。可是请必定记住,它无法保证原子性。举个例子你可能比较容易明白。
public class VolatileTest { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final VolatileTest test = new VolatileTest(); for(int i=0;i<10;i++){ new Thread(() -> { for(int j=0;j<1000;j++) test.increase(); }).start(); } while(Thread.activeCount()>2) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
执行这段代码,会发现结果每次通常都不一样,可是确定都小于10*1000。这就是volatile不保证原子性的最好证据。那么深层次的缘由是什么呢?
事实上,自增操做包括三个步骤:
既然分了三个步骤,就有可能出现下面这种状况:
假如某个时刻变量inc的值为10。
第一步,线程1对变量进行自增操做,线程1先读取了变量inc的原始值,而后线程1被阻塞了;
第二步, 而后线程2对变量进行自增操做,线程2也去读取变量inc的原始值,因为线程1只是对变量inc进行读取操做,而没有对变量进行修改操做,因此不会致使线程2会直接去主存读取inc的值,此时inc的值时10;
第三步, 线程2进行加1操做,并把11写入工做内存,最后写入主存。
第四步,线程1接着进行加1操做,因为已经读取了inc的值,此时在线程1的工做内存中inc的值仍然为10,因此线程1对inc进行加1操做后inc的值为11,而后将11写入工做内存,最后写入主存。
最后,两个线程分别进行了一次自增操做后,可是inc只增长了1。
有不少人会在第三步和第四步那里有疑问,线程2更新inc的值之后,不是会致使线程1工做内存中的值失效吗?
答案是不会,由于在一个操做中,值只会读取一次。这个是原子性和可见性区分的核心。
解决方案是使用increase方法使用synchronized
同步锁修饰。具体不展开了。
参考: