计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程当中会涉及到数据的读取和写入。因为程序运行过程当中的临时数据是存放在主内存(物理内存)当中的,这时就存在一个问题,因为CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,所以若是任什么时候候对数据的操做都要经过和内存的交互来进行,会大大下降指令执行的速度。为了处理这个问题,在CPU里面就有了高速缓存的概念。当程序在运行过程当中,会将运算须要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就能够直接从它的高速缓存读取数据和向其中写入数据,当运算结束以后,再将高速缓存中的数据刷新到主存当中。html
图一(注:L一、L二、L3表示CPU核心中的高速缓存)编程
Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每一个线程都有本身独立的工做内存,线程只能访问本身的工做内存,不能够访问其它线程的工做内存。工做内存中保存了主内存共享变量的副本,线程要操做这些共享变量,只能经过操做工做内存中的副原本实现,操做完毕以后再同步回到主内存当中。线程工做内存一个抽象的描述,工做内存中主要包括两个部分,一个是属于该线程私有的栈,二是对主存部分变量拷贝的寄存器(包括程序计数器PC和CPU工做的高速缓存区)。缓存
图二安全
有了以上概念,咱们进一步谈谈多线程状况下产生线程不安全的缘由。为了提升计算机的处理速度,CPU不会直接和内存进行通讯,而是将内存中的数据拷贝到高速缓存中进行操做,当多个线程的共享数据被拷贝到高速缓存后,各个线程对应的那块高速缓存彼此不可见,而各高速缓存中的数据被CPU修改后不知道什么时候会被写入主内存中,这就极有可能致使别的线程读不到最新数据,从而形成数据不一样步的线程安全问题。多线程
Java代码编译后会变成字节码在JVM中运行,而字节码最终须要转换成汇编语言在CPU上执行,所以Java的并发机制必然依赖于JVM的实现和CPU对指令的执行状况。并发
volatile的做用主要有两点,一是保证变量的可见性,另一个是保证代码执行的有序性。优化
如有这样一行代码:private static volatile LazySingleton instance = new LazySingleton();那么其转换成汇编指令的时候大概是这样:0x0000000002931351:lock add dword ptr [rsp],0h ;*putstatic instance; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)。加了volatile关键字的变量进行写操做时会多出含有有lock前缀汇编指令,而lock前缀会引起下面的事情:一是当前缓存行中的数据会当即写入到内存中去,二是这一写入内存的操做会致使其它高速缓存中缓存了该数据内存地址的数据无效,而且会利用缓存一致性协议来保证其余处理器中的缓存数据的一致性,从而保证变量的可见性。线程
实际上,当咱们把代码写好以后,虚拟机不必定会按照咱们写的代码的顺序来执行。例如对于这两句代码:int a = 1;int b = 2;你会发现不管是先执行a = 1仍是执行b = 2,都不会对a,b最终的值形成影响。因此虚拟机在编译的时候,是有可能把他们进行重排序的。那么为何要进行重排序呢?你想啊,假如执行 int a = 1这句代码须要100ms的时间,但执行int b = 2这句代码须要1ms的时间,而且先执行哪句代码并不会对a,b最终的值形成影响。那固然是先执行int b = 2这句代码了。因此,虚拟机在进行代码编译优化的时候,对于那些改变顺序以后不会对最终变量的值形成影响的代码,是有可能将他们进行重排序的。那么重排序以后真的不会对代码形成影响吗?实际上,对于有些代码进行重排序以后,虽然对变量的值没有形成影响,但有可能会出现线程安全问题的。具体请看下面的代码htm
public class NoVisibility{blog
private static boolean ready;
private static int number;
private static class Reader extends Thread{
public void run(){
while(!ready){
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args){
new Reader().start();
number = 42;
ready = true;
}
}
这段代码最终打印的必定是42吗?若是没有重排序的话,打印的确实会是42,但若是number = 42和ready = true被进行了重排序,颠倒了顺序,那么就有可能打印出0了,而不是42。(由于number的初始值会是0).所以,重排序是有可能致使线程安全问题的。若是一个变量被声明volatile的话,那么这个变量不会被进行重排序,也就是说,虚拟机会保证这个变量以前的代码必定会比它先执行,而以后的代码必定会比它慢执行。例如把上面中的number声明为volatile,那么number = 42必定会比ready = true先执行。不过这里须要注意的是,虚拟机只是保证这个变量以前的代码必定比它先执行,但并无保证这个变量以前的代码不能够重排序。以后的也同样。
全部内存的传输都发生在一条共享的总线上,而全部的处理器都能看到这条总线。线程中的处理器会一直在总线上嗅探其内部缓存中的内存地址在其余处理器的操做状况(可是注意,当处理器读取内存中的值后进行写操做前这段时间即使内存中的值改变了,其高速缓存中的值仍不会失效,能够认为这期间线程中的处理器没有在总线上嗅探其内部缓存中的内存地址在其余处理器的操做状况,这为volatile不能保证线程的安全埋下了伏笔),一旦嗅探到某到处理器打算修改其内存地址中的值,而该内存地址恰好也在本身的内部缓存中,那么处理器就会强制让本身对该缓存地址的无效。因此当该处理器要访问该数据的时候,因为发现本身缓存的数据无效了,就会去主存中访问。
内存屏障(lock前缀指令)会把这个屏障前写入的数据刷新到内存,这样任何试图读取该数据的线程将获得最新值。但当处理器读取内存中的值后进行写操做前这段时间即使内存中的值改变了,其高速缓存中的值仍不会失效,这为volatile不能保证线程安全埋下了伏笔。这样若是有一个变量i = 0用volatile修饰,两个线程对其进行i++操做,若是线程1从内存中读取i=0进了缓存,而后把数据读入寄存器,以后时间片用完了,而后线程2也从内存中读取i进缓存,由于线程1已进行读操做还未执行写操做,而后线程2执行完毕,内存中i=1,而后线程1又开始执行,而后将数据写回缓存再写回内存,结果仍是1。