一。共享内存模型数组
共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另外一个线程可见。缓存
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:安全
线程之间的共享变量存储在主内存(main memory)中,每一个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。多线程
本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化。app
当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,因此就会发生线程安全问题。ide
二。Volatile性能
一旦某个线程修改了被volatile修饰的基本类型变量,它会保证修改的值会当即被更新到主存,当有其余线程须要读取时,能够当即获取修改以后的值。优化
在Java中为了加快程序的运行效率,对一些变量的操做一般是在该线程的寄存器或是CPU缓存上(即本地内存)进行的,以后才会同步到主存中,而加了volatile修饰符的变量则是直this
接读写主存。Volatile 保证了线程间共享变量的及时可见性,但不能保证原子性。spa
class ThreadVolatileDemo extends Thread { public boolean flag = true; @Override public void run() { System.out.println("开始执行子线程...."); while (flag) { } System.out.println("线程中止"); } public void setRuning(boolean flag) { this.flag = flag; } } public class ThreadVolatile { public static void main(String[] args) throws InterruptedException { ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo(); threadVolatileDemo.start(); Thread.sleep(3000); threadVolatileDemo.setRuning(false); System.out.println("flag 已经设置成false"); Thread.sleep(1000); System.out.println(threadVolatileDemo.flag); } }
能够看到结果为:flag已经设置为false了,但子线程却还在运行,问题就出在线程之间是不可见的,读取的是副本,没有及时读取到主内存结果。
解决办法使用Volatile关键字将解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存”中取值。
1。Volatile特性:
1> 保证基本类型变量的可见性,即一个线程修改了某个基本类型变量的值,这新值对其余线程来讲是当即可见的。
但对于引用类型如数组,实体bean,仅仅保证引用的可见性,但并不保证引用内容的可见性,须要基于CAS的原子结构才行(CAS的原子结构在锁的深刻会讲到)。
2> 禁止进行指令重排序。(重排序后面会讲到)
2。volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,可是写操做稍慢,由于它须要在本地代码中插入许多内存屏障指令来保证处理器不发生重排序优化。
3。Volatile与Synchronized区别:
1> volatile仅能使用在变量级别。synchronized则可使用在变量、方法、和类级别的。
2> volatile仅能实现变量的修改可见性,并不能保证原子性.。synchronized则能够保证变量的修改可见性和原子性。
3> volatile不会形成线程的阻塞。synchronized会形成线程的阻塞。
4> volatile标记的变量不会被编译器优化。synchronized标记的变量能够被编译器优化。
5> volatile修饰的变量在两个线程并行时,一个线程修改其值后会强制改变另一个线程的该变量值。
例如多线程状况下须要用到某变量共享数据,当进行只读操做时可使用volatile修饰变量。
当进行写操做的时候,因为volatile没法保证原子性因此建议使用synchronized来修饰变量。
三。重排序
在执行程序时,编译器和处理器会对指令进行重排序,重排序分为:
1> 编译器重排序:在不改变代码语义的状况下,优化性能而改变了代码执行顺序。
2> 指令并行的重排序:处理器采用并行技术使多条指令重叠执行,在不存在数据依赖的状况下,改变机器指令的执行顺序。
3> 内存系统的重排序:使用缓存和读写缓冲区时,加载和存储多是乱序执行。
好比编译器重排序的典型就是经过调整指令顺序,在不改变程序语义的前提下,尽量的减小寄存器的读取、存储次数,充分复用寄存器的存储值。
int a = 5;① int b = 10;② int c = a + 1;③ 假设用的同一个寄存器
这三条语句,若是按照顺序一致性,执行顺序为①②③寄存器要被读写三次;但为了下降重复读写的开销,编译器会交换第二和第三的位置,即执行顺序为①③②。
再好比对于不存在数据依赖的操做,前面new操做比较费时间,但也会先执行后面省时间的操做。
1。数据依赖性
若是两个操做访问同一个变量,且这两个操做中有一个为写操做,此时这两个操做之间就存在数据依赖性。数据依赖分下列三种类型:
名称 |
代码示例 |
说明 |
写后读 |
a = 1;b = a; |
写一个变量以后,再读这个位置。 |
写后写 |
a = 1;a = 2; |
写一个变量以后,再写这个变量。 |
读后写 |
a = b;b = 1; |
读一个变量以后,再写这个变量。 |
上面三种状况,只要重排序两个操做的执行顺序,程序的执行结果将会被改变。
编译器和处理器在重排序时,会遵照数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操做的执行顺序。
这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操做,不一样处理器之间和不一样线程之间的数据依赖性不被编译器和处理器考虑。
2。as-if-serial语义
无论怎么重排序(编译器和处理器为了提升并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵照as-if-serial语义。
为了遵照as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操做作重排序,由于这种重排序会改变执行结果。
可是,若是操做之间不存在数据依赖关系,这些操做可能被编译器和处理器重排序。好比
double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C |
上面三个操做的数据依赖关系以下图所示:
如上图所示,A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。
所以在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。
但A和B之间没有数据依赖关系,编译器和处理器能够重排序A和B之间的执行顺序。
3。程序次序规则(Program Order Rule)
根据happens- before的程序顺序规则,上面计算圆的面积的示例代码存在三个happens- before关系:
1> A happens- before B;
2> B happens- before C;
3> A happens- before C;
这里A happens- before B,但实际执行时B却能够排在A以前执行,在Java中,若是A happens- before B,JMM并不要求A必定要在B以前执行。
JMM仅仅要求前一个操做(执行的结果)对后一个操做可见,且前一个操做按顺序排在第二个操做以前。这里操做A的执行结果不须要对操做B可见;
并且重排序操做A和操做B后的执行结果,与操做A和操做B按happens- before顺序执行的结果一致。
在这种状况下,JMM会认为这种重排序并不非法(not illegal),JMM容许这种重排序。
在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽量的开发并行度。
编译器和处理器听从这一目标,从happens- before的定义咱们能够看出,JMM一样听从这一目标。
4。重排序对多线程的影响
重排序可能会改变多线程程序的执行结果。
public class SimpleHappenBefore { private static int a=0; private static boolean flag=false; public static void main(String[] args) throws InterruptedException { for(int i=0;i<1000;i++){ Thread t1 = new Thread(new Runnable() { public void run() { a=1; flag=true; } }); Thread t2 = new Thread(new Runnable() { public void run() { if(flag){ a=a*1; } if(a==0){ System.out.println("ha,a==0"); } } }); t1.start(); t2.start(); t1.join(); t2.join(); a=0; flag=false; } } }
若是按照有序的话,那么在ThreadB中若是if(flag)成功的话,则应该a=1,而a=a*1以后a仍然为1,下方的if(a==0)应该永远不会为真,永远不会打印。而由于重排序却打印了几十次