从零开始了解多线程知识之开始篇目 -- jvm&volatile

本文章主要介绍到到了一些CPU缓存一致性协议的基础知识,由此引出的多线程知识,同时谈到了多线程中数据操做 原子性 可见性 有序性 的问题
从线程的基本概念到多线程下工做的数据安全问题,主要谈到了java知识中volatile关键字,使用实例的模式讲解了 volatile可见性,有序性,指令重排的问题
接下来你们一块儿来学习学习吧java

CPU多核缓存存储结构图算法

1.电脑存储结构概念编程

多CPU数组

一个现代计算机一般由两个或者多个CPU,若是要运行多个程序(进程)的话,假如只有 一个CPU的话,就意味着要常常进行进程上下文切换
     由于单CPU即使是多核的,也只是多个 处理器核心,其余设备都是共用的,因此多个进程就必然要常常进行进程上下文切换,这个代价是很高的。

CPU多核缓存

一个现代CPU除了处理器核心以外还包括寄存器、L1L2L3缓存这些存储设备、浮点运算 单元、整数运算单元等一些辅助运算设备以及内部总线等。
    一个多核的CPU也就是一个CPU上 有多个处理器核心,这样有什么好处呢?好比说如今咱们要在一台计算机上跑一个多线程的程序
    由于是一个进程里的线程,因此须要一些共享一些存储变量,若是这台计算机都是单核单线程CPU的话,就意味着这个程序的不一样线程须要常常在CPU之间的外部总线上通讯,
    同时还 要处理不一样CPU之间不一样缓存致使数据不一致的问题,因此在这种场景下多核单CPU的架构就 能发挥很大的优点,通讯都在内部总线,共用同一个缓存。

CPU寄存器安全

每一个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操做的速度远大于在主存上执行的速度。
    这是由于CPU访问寄存器的速度远大于主存。

CPU缓存多线程

即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器。
    因为CPU的速度远高于主内存,CPU直接从内存中存取数据要等待必定时间周期,所以出现了CPU缓存
    Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用, 减小CPU的等待时间,提升了系统的效率。 
    CPU缓存包括 一级Cache(L1 Cache) 二级Cache(L2 Cache) 三级Cache(L3 Cache)

内存架构

一个计算机还包含一个主存。
    全部的CPU均可以访问主存。主存一般比CPU中的缓存大得多。

CPU读取存储器数据过程并发

CPU要取寄存器XX的值,只须要一步:直接读取。 
    CPU要取L1 cache的某个值,须要1-3步(或者更多):把cache行锁住,把某个数据拿 来,解锁,若是没锁住就慢了。
    CPU要取L2 cache的某个值,先要到L1 cache里取,L1当中不存在,在L2里,L2开始加 锁,加锁之后,把L2里的数据复制到L1,再执行读L1的过程,上面的3步,再解锁。
    CPU取L3 cache的也是同样,只不过先由L3复制到L2,从L2复制到L1,从L1到CPU。 
    CPU取内存则复杂:通知内存控制器占用总线带宽,通知内存加锁,发起内存读请求, 
    等待回应,回应数据保存到L3(若是没有就到L2),再从L3/2到L1,再从L1到CPU,以后解除总线锁定。

多线程环境下存在的问题app

缓存一致性问题

在多处理器系统中,每一个处理器都有本身的高速缓存,而它们又共享同一主内存 (MainMemory)。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,可是 也引入了新的问题:缓存一致性(CacheCoherence)。
当多个处理器的运算任务都涉及同一 块主内存区域时,将可能致使各自的缓存数据不一致的状况,若是真的发生这种状况,
那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,须要各个处理器访问缓存时都 遵循一些协议,
在读写时要根据协议来进行操做,这类协议有MSI、 MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等

指令重排序问题

为了使得处理器内部的运算单元能尽可能被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,
处理器会在计算以后将乱序执行的结果重组,保证该 结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的前后顺序与输入代码中的 顺序一致。
所以,若是存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不 能靠代码的前后顺序来保证。
与处理器的乱序执行优化相似,Java虚拟机的即时编译器中也有 相似的指令重排序(Instruction Reorder)优化

2.什么是线程

现代操做系统在运行一个程序时,会为其建立一个进程。例如,启动一个Java程序,操做系统就会建立一个Java进程。
现代操做系统调度CPU的最小单元是线程,也叫轻量级进程,在一个进程里能够建立多个线程,
这些线程都拥有各自的计数器、堆栈和局部变量等属性,而且可以访问共享的内存变量。
处理器在这些线程上高速切换,让使用者感受到这些线程在同时执行。

线程的实现能够分为两类:

一、用户级线程(User-Level Thread)
二、内核线线程(Kernel-Level Thread)

在了解线程以前,须要知道系统存在两种空间:用户空间和内核空间
其中内核空间只能由内核代码进行方访问,用户代码没法直接访问若是用户代码要访问内核空间,
须要借助内核空间提供的访问接口
系统中用户程序运行在用户方式下,而系统调用运行在内核方式下。
在这两种方式下所用的堆栈不同:用户方式下用的是通常的堆栈,而内核方式下用的是固定大小的堆栈(通常为一个内存页的大小)

好比一个4G内存的空间,可能只有3GB能够用于用户应用程序。一个进程只能运行在用户方式(usermode)或内核方式(kernelmode)下,
每一个进程都有本身的3G用户空间,它们共享1GB的内核空间。当一个进程从用户空间进入内核空间时,它就再也不有本身的进程空间了。
这也就是为何咱们常常说线程上下文切换会涉及到用户态到内核态的切换缘由所在

用户线程:

指不须要内核支持而在用户程序中实现的线程,其不依赖于操做系统核心,应用进程利用线程库提供建立、同步、调度和管理线程的函数来控制用户线程。
另外,用户线程是由应用进程利用线程库建立和管理,不依赖于操做系统核心。不须要用户态/核心态切换,速度快。
操做系统内核不知道多线程的存在,所以一个线程阻塞将使得整个进程(包括它的全部线程)阻塞(可理解为串行化的)
因为这里的处理器时间片分配是以进程为基本单位,因此每一个线程执行的时间相对减小。

内核线程:

线程的全部管理操做都是由操做系统内核完成的。内核保存线程的状态和上下文信息,
当一个线程执行了引发阻塞的系统调用时,内核能够调度该进程的其余线程执行。
在多处理器系统上,内核能够分派属于同一进程的多个线程在多个处理器上运行,提升进程执行的并行度。
因为须要内核完成线程的建立、调度和管理,因此和用户级线程相比这些操做要慢得多,可是仍然比进程的建立和管理操做要快。
大多数市场上的操做系统,如Windows,Linux等都支持内核级线程。

如下是用户线程和内核线程的接口图,用户线程空间中,并无进程-线程对应关系表,但内核线程中有

Java线程与系统内核线程关系模型

Java线程
JVM中建立线程有2种方式

1. new java.lang.Thread().start()
   2. 使用JNI将一个native thread attach到JVM中
   针对 new java.lang.Thread().start()这种方式,只有调用start()方法的时候,才会真正的在

JVM中去建立线程,主要的生命周期步骤有:

1. 建立对应的JavaThread的instance
2. 建立对应的OSThread的instance
3. 建立实际的底层操做系统的native thread
4. 准备相应的JVM状态,好比ThreadLocal存储空间分配等
5. 底层的native thread开始运行,调用java.lang.Thread生成的Object的run()方法
6. 当java.lang.Thread生成的Object的run()方法执行完毕返回后,或者抛出异常终止后,终止native thread
7. 释放JVM相关的thread的资源,清除对应的JavaThread和OSThread

针对JNI将一个native thread attach到JVM中,主要的步骤有:

1. 经过JNI call AttachCurrentThread申请链接到执行的JVM实例
2. JVM建立相应的JavaThread和OSThread对象
3. 建立相应的java.lang.Thread的对象
4. 一旦java.lang.Thread的Object建立以后,JNI就能够调用Java代码了
5. 当经过JNI call DetachCurrentThread以后,JNI就从JVM实例中断开链接
6. JVM清除相应的JavaThread, OSThread, java.lang.Thread对象

3.为何用到并发?并发会产生什么问题

并发编程的本质其实就是利用多线程技术,在现代多核的CPU的背景下,催生了并发编程
的趋势,经过并发编程的形式能够将多核CPU的计算能力发挥到极致,性能获得提高。除此之
外,面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分。

即便是单核处理器也支持多线程执行代码,CPU经过给每一个线程分配CPU时间片来实现
这个机制。时间片是CPU分配给各个线程的时间,由于时间片很是短,因此CPU经过不停地切
换线程执行,让咱们感受多个线程是同时执行的,时间片通常是几十毫秒(ms)。
并发不等于并行:并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进
行”。实际上,若是系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,
只能经过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出如今拥有多个CPU的系统中。

并发的优势:

1. 充分利用多核CPU的计算能力;
2. 方便进行业务拆分,提高应用性能;

并发产生的问题:

高并发场景下,致使频繁的上下文切换
临界区线程安全问题,容易出现死锁的,产生死锁就会形成系统功能不可用

其它

CPU经过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。
可是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,能够再加载这个任务的状态。
因此任务从保存到再加载的过程就是一次上下文切换。

什么是JMM模型?

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

JMM不一样于JVM内存区域模型 (JVM是是实际存在的,JMM只是逻辑规则)

JMM与JVM内存区域的划分是不一样的概念层次,更恰当说JMM描述的是一组规则,经过
这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开。

JMM与Java内存区域惟一类似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,
从某个程度上讲应该包括了堆和方法区,而工做内存数据线程私有数据区域,
从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。

线程,工做内存,主内存工做交互图(基于JMM规范):

主内存

主要存储的是Java实例对象,全部线程建立的实例对象都存放在主内存中,无论该实例对
 象是成员变量仍是方法中的本地变量(也称局部变量),固然也包括了共享的类信息、常量、静
 态变量。因为是共享数据区域,多条线程对同一个变量进行访问可能会发生线程安全问题。

工做内存

主要存储当前方法的全部本地变量信息(工做内存中存储着主内存中的变量副本拷贝),每
个线程只能访问本身的工做内存,即线程中的本地变量对其它线程是不可见的,就算是两个线
程执行的是同一段代码,它们也会各自在本身的工做内存中建立属于当前线程的本地变量,当
然也包括了字节码行号指示器、相关Native方法的信息。注意因为工做内存是每一个线程的私有
数据,线程间没法相互访问工做内存,所以存储在工做内存的数据不存在线程安全问题。

根据JVM虚拟机规范主内存与工做内存的数据存储类型以及操做方式,对于一个实例对象中的成员方法而言,
若是方法中包含本地变量是基本数据类型(boolean,byte,short,char,int,long,float,double),
将直接存储在工做内存的帧栈结构中,但假若本地变量是引用类型,那么该变量的引用会存储在功能内存的帧栈中,
而对象实例将存储在主内存(共享数据区域,堆)中。但对于实例对象的成员变量,无论它是基本数据类型或者
包装类型(Integer、Double等)仍是引用类型,都会被存储到堆区。至于static变量以及类自己
相关信息将会存储在主内存中。须要注意的是,在主内存中的实例对象能够被多线程共享,倘
若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操做的数据拷贝一份到
本身的工做内存中,执行完成操做后才刷新到主内存

模型以下图所示

JMM存在的必要性

在明白了Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型的具
体关系后,接着来谈谈Java内存模型存在的必要性。因为JVM运行程序的实体是线程,而每一个
线程建立时JVM都会为其建立一个工做内存(有些地方称为栈空间),用于存储线程私有的数
据,线程与主内存中的变量操做必须经过工做内存间接完成,主要过程是将变量从主内存拷贝
的每一个线程各自的工做内存空间,而后对变量进行操做,操做完成后再将变量写回主内存,如
果存在两个线程同时对一个主内存中的实例对象的变量进行操做就有可能诱发线程安全问题。

假设主内存中存在一个共享变量x,如今有A和B两条线程分别对该变量x=1进行操做,
A/B线程各自的工做内存中存在共享变量副本x。假设如今A线程想要修改x的值为2,而B线程
却想要读取x的值,那么B线程读取到的值是A线程更新后的值2仍是更新前的值1呢?答案是,不肯定,
即B线程有可能读取到A线程更新前的值1,也有可能读取到A线程更新后的值2,
这是由于工做内存是每一个线程私有的数据区域,而线程A变量x时,首先是将变量从主内存拷贝到A
线程的工做内存中,而后对变量进行操做,操做完成后再将变量x写回主内,而对于B线程的也
是相似的,这样就有可能形成主内存与工做内存间数据存在一致性问题,假如A线程修改完后
正在将数据写回主内存,而B线程此时正在读取主内存,即将x=1拷贝到本身的工做内存中,
这样B线程读取到的值就是x=1,但若是A线程已将x=2写回主内存后,B线程才开始读取的
话,那么此时B线程读取到的就是x=2,但究竟是哪一种状况先发生呢?

如图

以上关于主内存与工做内存之间的具体交互协议,即一个变量如何从主内存拷贝到工做内
存、如何从工做内存同步到主内存之间的实现细节,Java内存模型定义了如下八种操做来完
成。

JMM-同步八种操做介绍

(1)lock(锁定):做用于主内存的变量,把一个变量标记为一条线程独占状态
(2)unlock(解锁):做用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定
(3)read(读取):做用于主内存的变量,把一个变量值从主内存传输到线程的工做内存中,以便随后的load动做使用
(4)load(载入):做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中
(5)use(使用):做用于工做内存的变量,把工做内存中的一个变量值传递给执行引擎
(6)assign(赋值):做用于工做内存的变量,它把一个从执行引擎接收到的值赋给工做内存的变量
(7)store(存储):做用于工做内存的变量,把工做内存中的一个变量的值传送到主内存中,以便随后的write的操做
(8)write(写入):做用于工做内存的变量,它把store操做从工做内存中的一个变量的值传送到主内存的变量中

若是要把一个变量从主内存中复制到工做内存中,就须要按顺序地执行read和load操做,
若是把变量从工做内存中同步到主内存中,就须要按顺序地执行store和write操做。但Java内
存模型只要求上述操做必须按顺序执行,而没有保证必须是连续执行。

同步规则分析

1)不容许一个线程无缘由地(没有发生过任何assign操做)把数据从工做内存同步回主内存中

2)一个新的变量只能在主内存中诞生,不容许在工做内存中直接使用一个未被初始化(load或者assign)的变量。
   即就是对一个变量实施use和store操做以前,必须先自行assign和load操做。

3)一个变量在同一时刻只容许一条线程对其进行lock操做,但lock操做能够被同一线程重复执行屡次,屡次执行lock后,
  只有执行相同次数的unlock操做,变量才会被解锁。lock和unlock必须成对出现。

4)若是对一个变量执行lock操做,将会清空工做内存中此变量的值,在执行引擎使用这个变
  量以前须要从新执行load或assign操做初始化变量的值。

5)若是一个变量事先没有被lock操做锁定,则不容许对它执行unlock操做;也不容许去unlock一个被其余线程锁定的变量。

6)对一个变量执行unlock操做以前,必须先把此变量同步到主内存中(执行store和write操做)

并发编程的可见性,原子性与有序性问题

原子性

原子性指的是一个操做是不可中断的,即便是在多线程环境下,一个操做一旦开始就不会
被其余线程影响。
在java中,对基本数据类型的变量的读取和赋值操做是原子性操做有点要注意的是,对于
32位系统的来讲,long类型数据和double类型数据(对于基本数据类型,
byte,short,int,float,boolean,char读写是原子操做),它们的读写并不是原子性的,也就是说如
果存在两条线程同时对long类型或者double类型的数据进行读写是存在相互干扰的,由于对
于32位虚拟机来讲,每次原子读写是32位的,而long和double则是64位的存储单元,这样会
致使一个线程在写时,操做完前32位的原子操做后,轮到B线程读取时,刚好只读取到了后32
位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它多是“半个变
量”的数值,即64位数据被两个线程分红了两次读取。但也没必要太担忧,由于读取到“半个变
量”的状况比较少见,至少在目前的商用的虚拟机中,几乎都把64位的数据的读写操做做为原
子操做来执行,所以对于这个问题没必要太在乎,知道这么回事便可。
X=10;  //原子性(简单的读取、将数字赋值给变量)
Y = x;  //变量之间的相互赋值,不是原子操做
X++;  //对变量进行计算操做
X = x+1;

可见性

理解了指令重排现象后,可见性容易了,可见性指的是当一个线程修改了某个共享变量的
值,其余线程是否可以立刻得知这个修改的值。对于串行程序来讲,可见性是不存在的,由于
咱们在任何一个操做中修改了某个变量的值,后续的操做中都能读取这个变量值,而且是修改
过的新值。
但在多线程环境中可就不必定了,前面咱们分析过,因为线程对共享变量的操做都是线程
拷贝到各自的工做内存进行操做后才写回到主内存中的,这就可能存在一个线程A修改了共享
变量x的值,还未写回主内存时,另一个线程B又对主内存中同一个共享变量x进行操做,但
此时A线程工做内存中共享变量x对线程B来讲并不可见,这种工做内存与主内存同步延迟现象
就形成了可见性问题,另外指令重排以及编译器优化也可能致使可见性问题,经过前面的分
析,咱们知道不管是编译器优化仍是处理器优化的重排现象,在多线程环境下,确实会致使程
序轮序执行的问题,从而也就致使可见性问题。

有序性

有序性是指对于单线程的执行代码,咱们老是认为代码的执行是按顺序依次执行的,这样
的理解并无毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现
象,由于程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未
必一致,要明白的是,在Java程序中,假若在本线程内,全部操做都视为有序行为,若是是多
线程环境下,一个线程中观察另一个线程,全部操做都是无序的,前半句指的是单线程内保
证串行语义执行的一致性,后半句则指指令重排现象和工做内存与主内存同步延迟现象。

JMM如何解决原子性&可见性&有序性问题?

原子性问题

除了JVM自身提供的对基本数据类型读写操做的原子性外,能够经过 synchronized和Lock实现原子性。
 由于synchronized和Lock可以保证任一时刻只有一个线程访问该代码块。

可见性问题

volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值当即被
其余的线程看到,即修改的值当即更新到主存中,当其余线程须要读取时,它会去内存中读取
新值。synchronized和Lock也能够保证可见性,由于它们能够保证任一时刻只有一个线程能
访问共享资源,并在其释放锁以前将修改的变量刷新到内存中

有序性问题

在Java里面,能够经过volatile关键字来保证必定的“有序性”(具体原理在下一节讲述
volatile关键字)。另外能够经过synchronized和Lock来保证有序性,很显然,synchronized
和Lock保证每一个时刻是有一个线程执行同步代码,至关因而让线程顺序执行同步代码,天然就
保证了有序性。

Java内存模型:

每一个线程都有本身的工做内存(相似于前面的高速缓存)。线程对变量的
全部操做都必须在工做内存中进行,而不能直接对主存进行操做。而且每一个线程不能访问其余
线程的工做内存。Java内存模型具有一些先天的“有序性”,即不须要经过任何手段就可以得
到保证的有序性,这个一般也称为happens-before 原则。若是两个操做的执行次序没法从
happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机能够随意地对它
们进行重排序。

指令重排序:

java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与
它顺序化状况的结果相等,那么指令的执行顺序能够与代码顺序不一致,此过程叫指令的重排
序。指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)
适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性
能

下图为从源码到最终执行的指令序列示意图

as-if-serial语义

as-if-serial语义的意思是:无论怎么重排序(编译器和处理器为了提升并行度),(单线
    程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵照as-if-serial语义。
    为了遵照as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操做作重排序,因
    为这种重排序会改变执行结果。可是,若是操做之间不存在数据依赖关系,这些操做就可能被
    编译器和处理器重排序。

happens-before 原则

只靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,
幸运的是,从JDK5开始,Java使用新的JSR-133内存模型,提供了happens-before原则来辅助保证程序执行的原子性、可见性以及有序性的问题,
它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容以下

1. 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

2. 锁规则解锁(unlock)操做必然发生在后续的同一个锁的加锁(lock)以前,也就是说,
    若是对于一个锁解锁后,再加锁,那么加锁的动做必须在解锁动做以后(同一个锁)。
    
3. volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单
    的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当
    该变量发生变化时,又会强迫将最新的值刷新到主内存,任什么时候刻,不一样的线程老是能
    够看到该变量的最新值。
    
4. 线程启动规则 线程的start()方法先于它的每个动做,即若是线程A在执行线程B的
    start方法以前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量
    的修改对线程B可见
    
5. 传递性 A先于B ,B先于C 那么A必然先于C

6. 线程终止规则 线程的全部操做先于线程的终结,Thread.join()方法的做用是等待当前
    执行的线程终止。假设在线程B终止以前,修改了共享变量,线程A从线程B的join方法
    成功返回后,线程B对共享变量的修改将对线程A可见。
    
7. 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中
    断事件的发生,能够经过Thread.interrupted()方法检测线程是否中断。
    
8. 对象终结规则 对象的构造函数执行,结束先于finalize()方法

volatile内存语义
volatile是Java虚拟机提供的轻量级的同步机制。
volatile保证可见性与有序性,可是不能保证原子性,要保证原子性须要借助synchronized、Lock锁机制,同理也能保证有序性与可见性。
由于synchronized和Lock可以保证任一时刻只有一个线程访问该代码块。

volatile关键字有以下两个做用

保证被volatile修饰的共享变量对全部线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值老是能够被其余线程当即得知。
禁止指令重排序优化。

volatile的可见性
关于volatile的可见性做用,咱们必须意识到被volatile修饰的变量对全部线程总数当即可见的,会showtime,底层被编译的时候会有lock信号
对volatile变量的全部写操做老是能马上反应到其余线程中
示例

/
     * 如下代码先执行线程A,一直执行i++,而后执行线程B,更改initFlag的值为true,想要退出循环
     * 可是若是变量不加volatile或者不加锁,因为线程A中的initFlag从第一次从主内存中load到线程A工做内存后
     * 一直使用的线程A的缓存数据,即使在线程B中更改了initFlag,可是并无showtime给线程A
     * 线程A使用的仍然是他缓存中的,并无去主内存中获取,因此当前代码要实现initFlag可见,
     * 能够加volatile关键字实现volatile写(更改后必定会写到主内存中而且会showtime),(保证可见性)
     * 或者加同步代码块synchronized
     *      加synchronized缘由:看代码第三版
     *
     */
    public class VolatileVisibilitySample {
        private boolean  initFlag = false;
        static Object object = new Object();
        public void refresh(){
            //普通写操做,(主要改为volatile写就能够)
            this.initFlag = true;
            String threadname = Thread.currentThread().getName();
            System.out.println("线程:"+threadname+":修改共享变量initFlag");
        }
        public void load(){
            String threadname = Thread.currentThread().getName();
            int i = 0;
    
    
            // 初版 initFlag没加volatile,后面的打印不会出现  (空跑会一直占用CPU使用权,优先级别很是高)
            //while (!initFlag){ }
    
            // 第二版,加一个变量 initFlag没加volatile,后面的打印不会出现,由于i和他不要紧
            //while (!initFlag){ i++;}
    
            // 第三版 加同步块 initFlag没加volatile,后面的打印会出现,
            // 存在同步块,这里可能引发阻塞,竞争可能致使上下文切换,线程的上下文切换会把线程的信息等数据回写到内存的 任务状态段 里面
            // 因此可能从新去主内存load数据,能知道initFlag已经改变,得以更新线程A的内存副本
            while (!initFlag){
                synchronized (object){
                    i++;
                }
            }
            System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i);
        }
        public static void main(String[] args){
            VolatileVisibilitySample sample = new VolatileVisibilitySample();
            Thread threadA = new Thread(()->{ sample.refresh(); },"threadA");
            Thread threadB = new Thread(()->{ sample.load(); },"threadB");
            threadB.start();
            try {
                 Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadA.start();
        }
    }

volatile没法保证原子性

public class VolatileVisibility {
        public  static  volatile int  i =0;
        public  static  void  increase(){i++;}
    }

    在并发场景下,i变量的任何改变都会立马反应到其余线程中,可是如此存在多条线程同时调用increase()方法的话,
    就会出现线程安全问题,毕竟i++;操做并不具有原子性,该操做是先读取值,而后写回一个新值,至关于原来的值加上1,
    分两步完成,若是第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一块儿看到同一
    个值,并执行相同值的加1操做,这也就形成了线程安全失败,所以对于increase方法必须使用synchronized修饰,以便保证线程安全,需
    要注意的是一旦使用synchronized修饰方法后,因为synchronized自己也具有与volatile相同的特性,便可见性,
    所以在这样种状况下就彻底能够省去volatile修饰变量

volatile禁止重排优化

volatile关键字另外一个做用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序
执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实
现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。
内存屏障,又称内存栅栏,是一个CPU指令,它的做用有两个,一是保证特定操做的执行顺序,
二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
因为编译器和处理器都能执行指令重排优化。若是在指令间插入一条Memory Barrier则会告诉编译器和CPU,
无论什么指令都不能和这条Memory Barrier指令重排序,也就是说经过插入内存屏
障禁止在内存屏障先后的指令执行重排序优化。Memory Barrier的另一个做用是强制刷出
各类CPU的缓存数据,所以任何CPU上的线程都能读取到这些数据的最新版本。
总之,volatile变量正是经过内存屏障实现其在内存中的语义,便可见性和禁止重排优化。

下面看一个很是典型的禁止重排优化的例子DCL,以下

    /
     * volatile保证指令重排(原理是插入了屏障)
     */
    public class Singleton {
    
        /
         * 查看汇编指令
         * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
         */
        private volatile static Singleton myinstance;
    
        public static Singleton getInstance() {
            if (myinstance == null) {
                synchronized (Singleton.class) {
                    if (myinstance == null) {
    
                        //多线程环境下可能会出现问题的地方
    
                        / 对象建立过程,本质能够分文三步
                         * 1. 申请地址 address=allocate
                         * 2.地址上实例化对象 new Singleton()
                         * 3.第三步 myinstance=address
                         *
                         * 要加volatile关键字,为了阻止指令重排,缘由:
                         *  其中这三步没法保证原子性,第二步和第三步可能存在指令重排
                         *  当很高的高并发请求下,若是不进行两层判断,
                         *  若是程序执行了第一步申请地址以后
                         *  若是第三步和第二步进行了指令重排,那么会致使myinstance=address
                         *  可是这时候address是空的,在使用的时候就会报错
                         *
                         *
                         *
                         */
                        myinstance = new Singleton();
                        //对象延迟初始化
                        //
                    }
                }
            }
            return myinstance;
        }
        public static void main(String[] args) {
            Singleton.getInstance();
        }
    
        /
         * 若是在多线程环境下就能够出现线程安全问题。缘由在于某一个线程执行到第一次检测,读
         * 取到的instance不为null时,instance的引用对象可能没有完成初始化。
         * 由于instance = new Singleton();能够分为如下3步完成(伪代码)
              memory = allocate();//1.分配对象内存空间
              instance(memory);//2.初始化对象
              instance = memory;//3.设置instance指向刚分配的内存地址,此时
              instance!=null
         * 因为步骤1和步骤2间可能会重排序,以下:
              memory=allocate();//1.分配对象内存空间
              instance=memory;//3.设置instance指向刚分配的内存地址,此时instance!
              =null,可是对象尚未初始化完成!
              instance(memory);//2.初始化对象
         * 因为步骤2和步骤3不存在数据依赖关系,并且不管重排前仍是重排后程序的执行结果在单
         * 线程中并无改变,所以这种重排优化是容许的。可是指令重排只会保证串行语义的执行的一
         * 致性(单线程),但并不会关心多线程间的语义一致性。因此当一条线程访问instance不为null
         * 时,因为instance实例未必已初始化完成,也就形成了线程安全问题。那么该如何解决呢,很
         * 简单,咱们使用volatile禁止instance变量被执行指令重排优化便可。
         * 
         //禁止指令重排优化
         private volatile static Singleton myinstance;
         */
    }

指令重排+读写屏障实例

/
 * 指令重排,不容许使用volatile的话,手动插入屏障理解
 *
 * 从代码上理解,正常的逻辑思惟状况下下,可能打印的结果只有三种
 *  1,1 (当线程1执行了a=1,同时线程2执行了b=1的时候)
 *  1,0(当线程1执行了a=1,线程2还没执行,y=b取了默认值)
 *  0,1(当线程2执行了b=1,线程1还没执行,x=a取了默认值)
 *
 *  指令重排的结果
 *  0,0(a=1和x=b进行了指令重排,b=1和y=a进行了指令重排,xy都取了ab默认值)
 *
 */
public class VolatileReOrderSample {
    private static int x = 0, y = 0;
    private static int a = 0, b =0;
    //private volatile static int a = 0, b =0;
    static Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        int i = 0;

        for (;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //因为线程one先启动,下面这句话让它等一等线程two. 读着可根据本身电脑的实际性能适当调整等待时间.
                    shortWait(10000);
                    a = 1; //是读仍是写?store,volatile写
                    //storeload ,读写屏障,不容许volatile写与第二部volatile读发生重排
                    //手动加内存屏障
                    //UnsafeInstance.reflectGetUnsafe().storeFence();

                    // 若是a,b使用volatile修饰,防止指令重排:这个操做 先读volatile,而后写普通变量b
                    x = b;
                    //分两步进行,第一步先volatile读,第二步再普通写
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    //UnsafeInstance.reflectGetUnsafe().storeFence();
                    y = a;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }

    }
    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

volatile内存语义的实现

前面提到太重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM
会分别限制这两种类型的重排序类型。
下面是JMM针对编译器制定的volatile重排序规则表

    是否能重排序                    第二个操做
      第一个操做       普通读/写      volatile读      volatile写
      普通读/写                                        NO
      volatile读       NO              NO             NO 
      volatile写                       NO             NO

举例来讲,第三行最后一个单元格的意思是:

在程序中,当第一个操做为普通变量的读或写时,若是第二个操做为volatile写,则编译器不能重排序这两个操做。
从上图能够看出:

当第二个操做是volatile写时,无论第一个操做是什么,都不能重排序。
这个规则确保volatile写以前的操做不会被编译器重排序到volatile写以后。

当第一个操做是volatile读时,无论第二个操做是什么,都不能重排序。
这个规则确保volatile读以后的操做不会被编译器重排序到volatile读以前。

当第一个操做是volatile写,第二个操做是volatile读时,不能重排序。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
对于编译器来讲,发现一个最优布置来最小化插入屏障的总数几乎不可能。
为此,JMM采起保守策略。下面是基于保守策略的JMM内存屏障插入策略。

∙在每一个volatile写操做的前面插入一个StoreStore屏障。
∙在每一个volatile写操做的后面插入一个StoreLoad屏障。
∙在每一个volatile读操做的后面插入一个LoadLoad屏障。
∙在每一个volatile读操做的后面插入一个LoadStore屏障。

上述内存屏障插入策略很是保守,但它能够保证在任意处理器平台,任意的程序中都能得
到正确的volatile内存语义。
下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图

上图中StoreStore屏障能够保证在volatile写以前,其前面的全部普通写操做已经对任意处理器可见了。
这是由于StoreStore屏障将保障上面全部的普通写在volatile写以前刷新到主内存。

这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的做用是避免volatile写与后面可能有的volatile读/写操做重排序。
由于编译器经常没法准确判断在一个volatile写的后面是否须要插入一个StoreLoad屏障(好比,一个volatile写以后方法当即return)。

为了保证能正确实现volatile的内存语义,JMM在采起了保守策略:

在每一个volatile写的后面,或者在每一个volatile 读的前面插入一个StoreLoad屏障。从整
体执行效率的角度考虑,JMM最终选择了在每一个 volatile写的后面插入一个StoreLoad屏障。

由于volatile写-读内存语义的常见使用模式是:

一个写线程写volatile变量,多个读线程读同一个volatile变量。
当读线程的数量大大超过写线程时,选择在volatile写以后插入StoreLoad屏障将带来可观的执行效率的提高。
从这里能够看到JMM 在实现上的一个特色:首先确保正确性,而后再去追求执行效率。

下图是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图

上图中LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
上述volatile写和volatile读的内存屏障插入策略很是保守。在实际执行时,只要不
改变 volatile写-读的内存语义,编译器能够根据具体状况省略没必要要的屏障。
下面经过具体的示例

public class VolatileBarrierExample {
    int a;
    volatile int m1 = 1;
    volatile int m2 = 2;

    void readAndWrite() {
        int i = m1;   // 第一个volatile读
        int j = m2;   // 第二个volatile读

        a = i + j;    // 普通写

        m1 = i + 1;   // 第一个volatile写
        m2 = j * 2;   // 第二个 volatile写
    }
}

针对readAndWrite()方法,编译器在生成字节码时能够作以下的优化

注意,最后的StoreLoad屏障不能省略。由于第二个volatile写以后,方法当即return。
此时编 译器可能没法准确判定后面是否会有volatile读或写,为了安全起见,编译器一般会在这里插 入一个StoreLoad屏障。
上面的优化针对任意处理器平台,因为不一样的处理器有不一样“松紧度”的处理器内 存模 型,
内存屏障的插入还能够根据具体的处理器内存模型继续优化。以X86处理器为
例,图3-21 中除最后的StoreLoad屏障外,其余的屏障都会被省略。
前面保守策略下的volatile读和写,在X86处理器平台能够优化成以下图所示。前文提到过,X86处理器仅会对写-读操做作重排序。
X86不会对读-读、读-写和写-写操做作重排序,所以在X86处理器中会省略掉这3种操做类型对应的内存屏障。
在X86中,JMM仅需 在volatile写后面插入一个StoreLoad屏障便可正确实现volatile写-读的内存
语义。这意味着在 X86处理器中,volatile写的开销比volatile读的开销会大不少(由于执行StoreLoad屏障开销会比较大)

过多使用cas(compareandswap)和volatile致使的bus总线风暴
volatile 基于底层缓存一致协议

cpu --> 工做内存 --> bus总线(缓存一致性协议) ---> 主内存
cpu1 --> 工做内存 --> bus总线(缓存一致性协议) ---> 主内存

若是使用volatile特别多或者热别多原子的cas,会致使工做内存见产生特别多无效工做内存变量,因为volatile在bus中无限showtime, 致使bus总线交互变得特别多,其余有意义的操做交互变得延迟 这时候和synchronized比较,还不如使用synchronized

相关文章
相关标签/搜索