《JMM内存模型以及volatile详解+synchronized介绍》
一、JMM内存模型

JMM内存模型是一个抽象出来的模式、为了更好的方便程序员去理解,首先我们看到了core1和core2代表了cpu的核数,同时也表示了当前有两个线程,两个线程每次从主内存中读取或者修改共享的变量时,都必须先把数据读取到自己的独有的工作内存中,然后开始对变量进行操作;同时cpu提供了8大原子操作,今天咱们只说6种,lock和ulock咱们放在线程池在说, 啥也不说了,上图 :

八大原子操作(其中六种):
read(读取): 从主内存中读取数据;
load(加载): 从read读取数据加载到线程的工作内存中;
use(使用): cpu把工作内存中的数据拿到寄存器中进行使用运算等等;
assign(赋值): 操作完成后赋值到工作内存中的变量;
store(存储):作用于工作内存中变量存储到主内存中变量,以便write写入;
write(写入): 这个就简单了,就是变量写入到主内存中;
JMM内存模型通过八大原子操作给我们抽象出来一种规范,让我们清楚的明白了cpu和内存是如何交互数据,那么我们知道多线程必须遵循三个性质:可见性、原子性、有序性,JMM内存模型是否遵循了呢?我们通过代码分析:我们创建了变量isShow默认值false, save和load是两个成员方法,save方法修改isShow变量值为true,load方法执行死循环,死循环的条件是(!isShow ), 然后分别启动了两个线程A和B ,A调用了save方法修改isShow=true, B线程调用了load方法开始死循环;我们看结果:A线程修改了变量,但是B线程并没有读到,一直在死循环中,说明线程之间不可见,违反了可见性;


1、通过上面的代码引出了java中一个关键字volatile,添加在共享变量上,B线程立马跳出死循环,说明B线程读到了A线程的修改的变量值,符合了可见性(volatile第一个特性);


2、看下面代码,通过循环开启了两个线程,每个线程循环5000次,调用VolatileVisibility.increase(),具有volatile自身静态成员变量i++,正常来说结果应该是10000,但是结果确实<= 10000; 说明volatile 不保证原子性;


3、在看下面的代码,你会发现,平常写的代码其实在jvm里会给优化,又可能会指令重排序,比方在一个类中 (1. a 依赖x , 2.中间逻辑代码。。。。。,3. b也依赖了x, 这时候1执行完了,有可能就直接去执行3了,然后在执行2 ,因为当你执行完后,cpu缓存中已经存在了x,jvm不如把b依赖x代码先执行,这样就避免销毁x,创建x频繁重复动作,但是你加上volatile就防止指令重排序了,我们拿最典型的例子,那就是懒汉式单例来举例;(volatile 具有有序性)

讲到这里,我们发现volatile 有可见性和有序性,但是没有原子性(要实现原子性就只能上锁);
我们先不讲如何实现原子性,我们先来说一下为什么volatile具有可见性,因为cpu 总线锁和MESI 总线缓存一致,最早的时候在并发量还不是很大的时候用的总线锁,就是当两个线程同时修改共享变量的时候,就直接加上一个总线锁,一直等到抢到锁的线程执行完修改然后写入主内存中的时候,才会释放锁,下个线程才能操作,后来为了提高性能,出现了MESI总线缓存一致性,我们锁的是线程的缓存行(缓存行是线程独有的),当我们加上volatile的时候,在字节码中就会出来acc_volatile的标识,当总线识别这个信号后,就会用总线嗅探机制,让所有的线程都感应到;
我们举例: 假如T1 ,T2 两个线程同时都去读取主内存的共享变量了x = 0; 分别通过read 和load两步让共享变量读到自己的工作内存中,这时候T1和T2的本地工作内存中都有x=0的副本;

我们在看下面这个图, 为什么volatile不能保证原子性,首先要清楚线程来改变x,需要分三步:读取、修改以及写回,这三步在多线程并发下,T1和T2有可能同时读取了x=0, 比如T1读取后,T1 执行x++ ,变量 x=1,这时候volatile通过MESI缓存一致性使T1会通知T2,这时候T2工作内存的x=0会失效,但是因为是并发编程,我们要有并发思想,这时候T2有可能也已经在寄存器中对x执行了++操作 ,这时候T2的x也=1; 意思就是x其实一共操作了两步,应该x=2,但是T1写回主内存 x=1, T2紧随其后把自己x=1也写回了主内存中覆盖了T1,所以执行了两步x的值还是1(不保证原子性);

最后 有序性这块,因为是字节码的指令重排序,所以我们就硬性记忆吧,只要知道在jvm底层字节码具有重排序,我们使用volatile就可以禁止指令重排序;
synchronized
大家应该都不陌生,初学java时,老师肯定都会说,遇到多线程并发就加锁,那我们在这里就介绍一下这个java的关键字;
首先为什么要说它,是因为我们说了volatile解决了可见性和有序性,但是没有解决原子性,并且只能修饰变量,而马上介绍的synchronized就很好的解决了原子性并且还具有volatile的可见性和有序性的特性;
一、synchronized是java的一个关键字,互斥锁;
二、synchronized的使用, 方法上(锁是谁?谁new了就是谁,锁的对象的实例), 静态方法上(锁的就是当前类), 代码块(可以自己创建Object对象作为锁(最优))
1、这个是synchronized 用在普通方法上,锁的是实例对象,两个方法save 和save1 ,启动了两个线程,thread死循环执行save, thread1 循环10次执行save1,但是我们看的结果是只有save执行了,这就是用锁的技巧,并不是看到代码加synchronized就可以了,因为是加在普通方法上,所以锁就是对象的实例,然后这两个线程用的同一个对象实例,所以thread1永远都不会执行的;(解决办法,避免死循环或者用一个对象实例)


2. 在静态方法上添加一样,这个锁的范围更大,直接把整个类全锁了,而且这个类是唯一的,只要这个类被锁着,就谁也别用;

3.最优的方式就是使用代码块,这样的方式,我们只锁住有并发的代码就可以了,不用把方法里的所有代码都锁住,同时我们自定义个对象作为锁,这样灵活多变,避免锁住整个类等等;

三、介绍完怎么使用,然后我们在来说一下,就是当用synchronized关键字后,在字节码角度会在此方法上添加一个acc_synchronized,当jvm遇到此信号就会开启Monitor监视锁;我们知道锁是加在对象头的mark word上的,所以synchronized锁的是对象,当对象开启锁啊, 字节码层面就会有monitorenter /.........代码逻辑......../monitorexit ,在代码块和方法的前后显示,表明此代码已加锁;
执行monitorenter指令的时候,当前线程尝试获取Monitro的所有权(加锁):
如果对象上Monitor的进入数为0,当前线程就可以获取Monitor的所有权(加锁成功),Monitor进入数就会+1;
如果当前线程已经获取了Monitor的所有权(重入锁),在Monitor的进入数上+1;
如果其他线程占用了Monitor,那么当前线程就进入阻塞状态,直到Monitor进入数为0 ,当前线程有可以去争取此对象锁的使用;
执行monitorexit指令的时候,Monitor对象进入数减1,如果减1后进入数是0,那么当前线程就释放了锁,其他阻塞或者自旋的线程就可以抢占Monitro的所有权;
四 、synchronized的优化, 首先synchronized一开始诞生出来的时候,是一个重量级锁,性能是很差的,一旦上锁,只有释放了此锁其他线程才能执行,如果代码执行时间很长,那么就是毁灭的性打击;

本人工作3年中级菜鸟程序员, 最近想回顾一下知识,做了一些简单总结同时也为了自己今后复习方便,如果有逻辑错误,大家体谅,同时也希望大牛们能给出正确答案让我改正,谢谢!