今天周末,闲来无事,干吗呢?固然看书啊,总结啊!读完书光回想是没用的,必须有个本身的第一遍理解,第二遍理解.....,就好比简简单单的JMM说来轻松,网上博客虽多,图文代码加以解释的甚少,并无给读者一种层次感。因此我想写这么一篇博客,算是总结本身的第一遍理解,同时尽本身最大的可能让你们理解的时候有一种层次感。数组
整篇博客都是参考《深刻理解Java虚拟机》加上本身读了两遍以后的理解完成的,创做不易,望转载告之,谢谢!安全
先在记录此篇博客以前,给一个大概的目录结构方便读者留意:多线程
一、Java内存模型介绍并发
二、Volatile关键字规则ide
三、double与long的非原子性高并发
第一次见到Java的内存模型,正如《深刻理解JVM》中那样提到Cache与主存的关系,我也第一时间想起来了这个,因而便画了以下的存储系统的层次结构图性能
Cache与主内存之间为了能保持一致性,会不断跟Cache进行交互,也就是地址映像,主要有直接映像、全相联映像、组相联映像,emmmm...打住,这不是正题,只是顺便给本身个机会看《操做系统》就当作复习下,好了接下来是正题,先画出JMM图以下:优化
从图中能够看出要学好Java线程(高并发)是必需要知道JMM的,同时工做内存就比如Cache,与主内存之间进行交互,须要注意的的是这里的工做内存与主内存并非咱们所知道的内存这个概念,也不仅是简单的Java Heap与Java Stack那样简单的概念,为了进一步知道工做内存与主内存是什么,接下来先了解它们,此时你能够先不用看图,了解后再看更佳。spa
(1)工做内存:每条线程都有本身的内存,这就是工做内存,在工做内存中主要是保存使用到的变量在主内存的拷贝(即存放主内存中工做内存用到的变量拷贝);操作系统
(2)主内存:VM内存的一部分,是新增变量的地方以及每一个线程中全部变量来源之处,是能够被共享的数据元素。
(3)内存模型中的变量:是指实例字段与静态字段构成数组对象的元素,即能被共享的数据元素,而不是被线程私有的局部变量与方法参数等。全部的变量都会存储在主内存(VM内存的一部分)中;
(4)每条线程对变量的操做都必须在工做内存中进行,而不能直接操做主内存;
(5)每条线程之间的工做内存是不能被共享的,不能相互访问各自的变量,线程之间的变量“交流”只能经过主内存来实现;
(6)若是非要将JMM中的主内存与工做内存跟Java Heap、Java Stack、Method Area作比较(实则二者不是一个概念),那么能够认为工做内存就是Java Stack(很好理解,这是由于Java Stack是线程私有的,线程之间不能共享),主内存就是Java Heap中实例数据(很好理解,Java Heap中对象的实例数据是能够共享的)
这是理解多线程最重要的部分,多线程必然会涉及到内存之间的交互,Java的多线程之间的交互实则就是工做内存与主内存之间的交互,那么它们之间确定要有相互交互的规定(即协议),主要分为八种:
(1) Lock:做用于主内存的变量,将该变量标识为某一条线程独占的资源,其余线程不能占用。
(2) Unlock:与Lock相反,做用于主内存变量,释放被Lock的变量。
(3)Read:做用于主内存的变量,将该变量从主内从中取出,传到线程的工做内存中。以后Load加载到工做内存的变量副本中。
(4) Load:将Read取到的变量放入工做内存的变量副本中。
(5) Use:将工做内存中变量传递给执行引擎,遵从VM指令的安排(被使用到时会有相关指令)
(6)Assign:接受执行引擎返回Use以后执行的结果值,将该值赋值给工做内存中对应的变量。
(7)Store:将功能内存中的值传递到主内存中,以后Write放入主内存的变量中。
(8) Write:将Store的值放入主内存的变量中。
好了,忽然一会儿要记住八种操做,头也会并且可能还记不住,那么结合图总结下吧:
(1) 要把一个变量从主内存copy到工做内存,只须要Read->Load顺序便可。
(其中Variable Duplicate变量拷贝是属于工做内存Working Memory的,这里主要是为了能更好的展现,因此分离了,但愿不要误解!)
(2)若是把工做内存的变量同步回主内存,只须要Store->Write顺序便可。
(其中Variable是属于Main Memory的,这里主要是为了能更好的展现,因此分离了,但愿不要误解!)
(3) 若是VM用到这个变量(即有关的操做指令),则执行Use->Assign便可。
这里就不画图了,简单来讲就是咱们在程序中用到变量,对变量初始化、更新等,也就是只要在VM中有相关操做该变量的指令,就会从工做内存中被Use,以后Assign赋值会写回公共内存,如i++,先拿到i,以后i+1,最后赋值i=i。
注意:这些操做之间并不要求必定要连续,只要保证先后顺序便可,好比Read A, Read B, Load A, Load B便可,而不须要Read A, Load A, Read B, Load B
微观上讲咱们须要实现线程的一致性这个目标,而宏观上就是如何确保在高并发下是安全的,其实主要是经过八种操做之间的规定,才能保证多线程下一致性:
(1) 不容许read和load,store和write中单一操做出现;
(2)不容许最近的赋值动做即assgin被丢弃(工做内存中变量改变了必须同步回主内存中);
(3) 不容许线程中变量没有改变(即没有assign操做),就把该变量数据同步回主内存(不接受毫无理由的同步回主内存);
(4) 一个变量的产生只能在主内存中,不容许工做内存使用一个未被初始化(即未被assgin赋值或load加载)的变量,即一个新的变量产生必须在主内存中,再read->load到工做内存变量副本中,以后进行中Use->Assign赋值,最后才有可能stroe->write同步回主存,换句话说就是对一个变量进行use/store以前必须先进行assign/load操做。
(5)Lock与Unlock操做是成对出现的,一个变量只能被一个lock操做,一个线程能够屡次lock操做。
(6) 一个线程的lock与unlock操做要对应,不容许线程A的unlock线程B的变量。同理,若是没有lock,那么不容许unlock。
(7) 一个变量执行lock操做,会将工做内存中对应的变量清空,在执行引擎获取这个变量以前,必须load/assgin初始化这个变量,这是由于执行引擎要获取的变量必须是最新的值,在lock-unlock过程当中该变量可能发生改变,因此必须从新初始化保证得到最新的值。
实际上,咱们在程序中操做的变量是工做内存的变量副本,那么每次变量被改变(Use->Assign)后,都会同步回(Store->Write)主内存中,保持了变量的一致性,可是这只是单线程的状况下,那么在多线程状况下呢?好比线程A已经改变了变量的值,还没来的及同步回主内存,线程B就已经从主内存中将旧的变量值Read->Load到工做内存。这就形成了被线程A修改后的变量值对线程B不可见的问题,致使变量不一致。最轻量的能解决此问题就是利用好Volatile关键字,那么Volatile是如何实现的呢?
简单来讲被Volatile关键字的变量一旦被改变后就会当即同步回内存中,保证其余线程能得到最新的当前变量值,而广泛变量不会当即同步回内存(事实上何时同步回内存是不肯定的),因此致使不可见性。
(1)保证此变量对全部线程的可见性:
① 线程的可见性并非误认为“Volatile对全部线程的当即可见,也就是对某个变量写操做立马能反映到全部线程中,所以在高并发的状况下是安全的”,“Volatile在高并发下是安全的”这个最后的结论是不成立的。
② Java中相关的操做并非原子操做,好比i++,实际上是分为两步(可使用Javap反编译查看指令代码)的:先i+1,以后i=i+1。因此Volatile在高并发状况下并非安全的。
1 /** 2 * 演示使用Volatile在高并发下状态的不安全性: 3 * @author Jian 4 * 5 */ 6 public class VolatileDemo { 7 private static final int THREAD_NUM = 10;//线程数目 8 private static final long AWAIT_TIME = 5*1000;//等待时间 9 private volatile static int counter = 0; 10 11 public static void increase() { counter++; } 12 13 public static void main(String[] args) throws InterruptedException { 14 ExecutorService exe = Executors.newFixedThreadPool(THREAD_NUM); 15 for (int i = 0; i < THREAD_NUM; i++) { 16 exe.execute(new Runnable() { 17 @Override 18 public void run() { 19 for (int j = 0; j < 1000; j++) { 20 increase(); 21 } 22 } 23 }); 24 } 25 //检测ExecutorService线程池任务结束而且是否关闭:通常结合shutdown与awaitTermination共同使用 26 //shutdown中止接收新的任务而且等待已经提交的任务 27 exe.shutdown(); 28 //awaitTermination等待超时设置,监控ExecutorService是否关闭 29 while (!exe.awaitTermination(AWAIT_TIME, TimeUnit.SECONDS)) { 30 System.out.println("线程池没有关闭"); 31 } 32 System.out.println(counter); 33 } 34 }
按道理说最后变量i的结果应该是10*1000=10000,可是运行后你会发现输出结果都是小于10000且各不相同的值,形成这样的结果实则不是Volatile的锅,而是Java的非原子性,只是但愿咱们在关注并使用Volatile关键字的时候须要知道在高并发下不必定是安全的。
(2)使用Volatile能够禁止指令重排序优化:
也就是通常普通变量(未被Volatile修饰)只能保证最后的变量结果是对的,可是不会保证变量涉及到的程序代码中顺序与底层执行指令顺序是一致。须要注意的是重排序是一种编译过程当中的一种优化手段。
下列只能用伪代码的形式举例,由于指令重排序涉及到反编译指令码等(我并不了解,实际上一点也不)
1 public class VolatileDemo2 { 2 //是否已经完成初始化标志 3 private /*volatile*/ static boolean initialized = false; 4 private static int taskA = 0; 5 public static void main(String[] args) throws InterruptedException { 6 ExecutorService exe = Executors.newFixedThreadPool(2); 7 //线程A 8 exe.execute(new Runnable() { 9 @Override 10 public void run() { 11 //A线程的任务是加1,完成初始化 12 taskA++; 13 //initialized初始化完成,赋值为true,必须是先执行+1操做,才能赋值true 14 //可是因为重排序这里可能先于taskA++执行,致使读取到的结果可能为0。 15 initialized = true; 16 } 17 }); 18 exe.execute(new Runnable() { 19 @Override 20 public void run() { 21 //线程B的任务是等待线程A初始化完成后,再读取taskA的值 22 while(!initialized) { 23 try { 24 System.out.println("线程A还未初始化"); 25 Thread.sleep(1000); 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 } 30 System.out.println(taskA); 31 } 32 }); 33 exe.shutdown(); 34 while (!exe.awaitTermination(5*1000, TimeUnit.SECONDS)) { 35 System.out.println("线程池没有关闭"); 36 } 37 } 38 }
须要主要的就是下面的代码,虽然线程A中是保证了有序执行,再标志初始化完成,可是在指令中多是先赋initialized为true,而后线程B这时候“抢先一步”先读initialized,那么变量taskA的值就可能为0(实际业务中可能会是致命错误!)
taskA++; initialized = true;
若是不使用volatile关键字,那么只有当明确赋值了initialized的方法被调用,接下来的任务才能不会出错(只要结果是true就行,不用管指令顺序):
boolean volatile initialized; public void setInitialized(){ initialized = true; } public otherWorks(){ //初始化完成方法被明确调用,强制initialized结果为true,不用管指令顺序 setInitialized(); while(!initialized){ //other thread's tasks } }
(3)Volatile与Synchronized性能对比:通常状况下Volatile的同步机制要优于Synchronized(可是VM对Synchronized作了不少优化,因此其实也是说不许的),可是Volatile好就好在读取变量跟普通变量的读取几乎没啥差异,可是写操做会慢一点(这是由于会在代码中加入内存屏障,保证指令不会乱序)
(1)double与long的非原子性:在JMM中规定long与double这样的64位而且没有被volatile修饰数据能够划分为两部分32位来进行操做,即VM容许对64位的数据类型的load、store、read、write不保证其原子性。由于非原子性的存在,按理论上来讲某个线程在极小的几率下可能会存在读到“半个变量”的状况。
(2)虽然因为long与double非原子性存在,可是VM对其的操做是具备原子性的,即对操做原子性,对数据非原子性。因此long与double不须要被要求加volatile关键字。