文章众多,先放上参考:
https://bbs.csdn.net/topics/392513107(volatile相关讨论,概念基本解析)
https://blog.csdn.net/javazejian/article/details/72772461(全面理解Java内存模型(JMM)及volatile关键字,全面的解析)
三篇同作者:

简介:
简要概括:volatile(翻译:易变的)volatile是解决多线程同步问题的一个轻量级关键字。可以保证变量的操作一定是操作内存中的地址的数据,也保证操作不会被cpu和编译器重排序,保证按照代码中的顺序执行,但当变量进行的是一整套操作时,不保证那套操作的完整性,也就是不能保证原子性,除非操作本身满足原子性条件。【注意:volatile是解决多线程的问题,当遇到多线程的情况下才需要考虑,如果本身就是线程安全的则无需用】

【注意:
volatile不能修饰局部变量:因为局部变量是每个线程独有的,不存在线程安全等问题。
volatile不能和final一起用:这个问题我思考过(此猜想待验证):volatile是被设计用来修饰被不同线程访问和修改的变量(也就是主要用于变量),而final修饰基本数据类型时,值不能变,此时是变为常量了,用volatile没有意义。当final修饰引用类型时,此时对象的引用不能变,但是对象的内容可以变(String和包装类例外),这个时候可能需要用到volatile,但是不一定非得修饰同一个对象,因为如果对象是自己创建的,那对象的属性用的肯定还是java提供的数据类型,如果含有基本数据类型,那就在类属性那里加volatile而不是给对象加volatile,这个时候就实现了给对象加final,给对象的属性加volatile,是可以的。如果对象的不是自己写的,或者自己写的对象里面含有引用类型,那么其实java本身的类对象很多内容是可以不能改变的,除了集合类、Stringbuilder等等,但是这些东西java也提供了线程安全的实现方式,用线程安全的就完全不需要volatile了。综上,final和volatile一起用没什么意义。
volatile可以和static一起用。

1.内存可见性(可见性简介);java中,volatile修饰的变量被某个线程改变后,需要将新值强制写回主存(JMM主存就认为是内存,当然物理上对应关系很复杂)强制对缓存的修改立即写入内存,而不是写在告诉缓存中,而读的时候则规定强制从主存刷新,任何时刻,不同的线程总是能够看到该变量的最新值。
2.禁止指令重排序(有序性简介):。确保指令重排序时,不会将屏障前的操作排到屏障后,也不会将后边的操作排到屏障前。【内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。它在执行到内存屏障这句指令时,在它前面的操作已经全部完成;】也就是说volatile可以防止重排序(通俗点就是执行此变量的相关操作前的代码一定会执行完,保证在总体中的顺序不会被调整)(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序做一些调整,导致执行的顺序并不是从上往下的。从而出现了一些意想不到的效果)。
【有序性和原子性的应用差别:有序性一般会把volatile修饰的变量作为条件,通过判断这个条件来执行其他语句,有序性关系到其他的语句,而原子性关心的是volatile修饰的变量和自己本身有关的操作,和其他语句没关系】

volatile它具有以下特性:

  1. volatile能够保证可见性
  2. volatile能够保证有序性
  3. volatile不能保证原子性


关于有序性,原子性,一致性其实是一直概念,主要是在并发编程中,处理java内存模型简称JMM(Java Memory Model )数据的概念,事务中也有这些概念,并不是volatile独有的,只是volatile的就是用来操作数据与内存的关系的,和这些概念密切相关,所以结合在一起讲解。



有序性

什么是有序性:程序按照写代码的先后顺序执行,就是有序的。
CPU的指令重排序问题:
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种
编译器优化的重排
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
指令并行的重排
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序
内存系统的重排
由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
其中编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题


处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。那它如何保证即使重排最终结果也能正确呢?
答案就是指令数据间依赖关系不会被重排所影响。例:假设a=4;a=a+3;b=a,三个语句由于存在对前者值的依赖,所以不会被重排。
重排问题在单线程中不会出现任何问题(因为依赖关系不会被重排),但是多线程时就容易因为先后顺序的不一致而导致,一般情况是把该变量作为条件,通过判断此条件来执行其他语句,此时就会出问题了;
例子(截图,具体的看参考文档):

注:2,3步没有依赖关系,执行第3步的时候singleton就不为null,但是却没有初始化。



原子性

原子性就是一个操作是不可再分割的,就像原子一样,可以理解为操作只有一步,一步已经是最小步骤了,自然就不能再分了。
典型的比如转账,其实分为两步,甲方少100,乙方多100,但这两步通常会加事务,强制变一步,这就是事务的原子性。其实所有的原子性/原子操作都是这个意思。

volatile不能保证原子性

例子:比如对一个数进行自加操作一万次:i=i+1,这是三步,一步是取值一步是加一,一步是赋值,然而当多线程时可能出现赋值操作还没完成赋值的时候就被其他线程读取了,从而导致赋值的时候结果一样,这样就导致自加一万次的结果小于一万,数值不准确。
如果操作本身符合原子操作,比如i=3,只是一个赋值操作(修改变量时不依赖变量的当前值),本身是原子操作,那就具有原子性。

如何保证原子性

那么对于i++这种非原子操作,我们如何让它变成原子操作呢?

1.可以通过synchronized关键字,因为i++是三步操作,多线程导致A在执行这三步操作期间被B干扰了,最终导致问题。我们对i++加上synchronized关键字,保证A在执行这三步操作时,不会被其他线程干扰,这样肯定就不会有问题了。
synchronized(this){
  i++;
}
2.可以使用java.util.concurrent.atomic包下面的封装的原子类来完成自增自减的操作,比如AtomicInteger。
这些原子类是通过CAS(Compare And Swap)来实现原子操作的,CAS是CPU指令集的操作,是一个原子操作,速度极快。CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”。其实现思路其实就是乐观锁,以上面的分析为例,B从主存读取到i=10,B认为主存中的值得是10我的操作才生效,B在加一操作后准备将11赋值给i,去主存对比,发现主存的值变成了11,和B的预期值不一样,说明这个值肯定被别的线程改了,B放弃本次操作,更新预期值为11,进行下一次重试。下一次B加一后再去比对,发现预期值11和主存值11相等,才会真的将12赋值给i。
由于CAS用CPU指令来实现无锁自增,所以,AtomicLong.incrementAndGet的自增比用synchronized的锁效率倍增。
有关CAS更详细的资料可以参考这篇文章 非阻塞同步算法与CAS(Compare and Swap)无锁算法https://www.cnblogs.com/Mainz/p/3546347.html

AtomicInteger:java并发原子包解析:https://www.cnblogs.com/scuwangjun/p/9098057.html;原理简介:通过结合volatile和Unsafe类(在sun.misc包下)来解决,Unsafe类用到了CAS思想。

可见性

可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。但在多线程环境中可就不一定了,前面我们分析过,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存***享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题

在了解可见性时我们还要介绍一下java内存区域、JMM以及硬件内存架构:

JAVA内存区域:



JMM:

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

注意:

需要注意的是,JMM与Java内存区域的划分(JVM)是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。
换句话说:线程在修改主存变量的时候会先拷贝一份到自己的线程栈中去处理,每个线程都有自己的空间,相互是独立的,在变量修改完成到写入主存中,这个过程对其它线程是不可见的。(这里不明白的话可以看一下JVM内存结构及线程间的通信)

硬件内存架构:

(简化图)
正如上图所示,经过简化CPU与内存操作的简易图,实际上没有这么简单,这里为了理解方便,我们省去了南北桥并将三级缓存统一为CPU缓存(高速缓冲存储器Cache)(有些CPU只有二级缓存,有些CPU有三级缓存)。就目前计算机而言,一般拥有多个CPU并且每个CPU可能存在多个核心,多核是指在一枚处理器(CPU)中集成两个或多个完整的计算引擎(内核),这样就可以支持多任务并行执行,从多线程的调度来说,每个线程都会映射到各个CPU核心中并行运行。在CPU内部有一组CPU寄存器,寄存器是cpu直接访问和处理的数据,是一个临时放数据的空间。一般CPU都会从内存取数据到寄存器,然后进行处理,但由于内存的处理速度远远低于CPU,导致CPU在处理指令时往往花费很多时间在等待内存做准备工作,于是在寄存器和主内存间添加了CPU缓存,CPU缓存比较小,但访问速度比主内存快得多,如果CPU总是操作主内存中的同一址地的数据,很容易影响CPU执行速度,此时CPU缓存就可以把从内存提取的数据暂时保存起来,如果寄存器要取内存中同一位置的数据,直接从缓存中提取,无需直接从主内存取。需要注意的是,寄存器并不每次数据都可以从缓存中取得数据,万一不是同一个内存地址中的数据,那寄存器还必须直接绕过缓存从内存中取数据。所以并不每次都得到缓存中取数据,这种现象有个专业的名称叫做缓存的命中率,从缓存中取就命中,不从缓存中取从内存中取,就没命中,可见缓存命中率的高低也会影响CPU执行性能,这就是CPU、缓存以及主内存间的简要交互过程,总而言之当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存(当然如果CPU缓存中存在需要的数据就会直接从缓存获取),进而在读取CPU缓存到寄存器,当CPU需要写数据到主存时,同样会先刷新寄存器中的数据到CPU缓存,然后再把数据刷新到主内存中。

Java内存模型与硬件内存架构的关系

Java内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存(线程私有数据区域)和主内存(堆内存)之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,并不实际存在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。(注意对于Java内存区域划分也是同样的道理)


volatile可见性例子:

volatile如何保证可见性?是先刷新缓存在同步到主存还是直接同步到主存呢,那读取的时候又再从主存读取吗??【面试题】

volatile规则:修改了volatile变量,必须立即同步到主存,同时使其他线程工作内存中的值变为无效使用volatile变量前,必须先从主存刷新,以此来保证可见性
同样以上面的代码为例:子线程将flag修改为true,同步到主存,同时使主线程的工作内存中的flag失效,主线程下次使用flag时if (thread.isFlag()),发现工作内存中的flag已经失效,而且由于volatile的影响,在使用flag前本来也会强制从主存刷新,将会得到最新的值true

其他办法保证可见性:

通过synchronized和Lock加锁