一个设计网站多少钱,sku电商是什么意思,网站建设有哪些方法,网站维护运行建设报告一#xff0c;深入理解JMM内存模型
1#xff0c;什么是可见性
在谈jmm的内存模型之前#xff0c;先了解一下并发并发编程的三大特性#xff0c;分别是#xff1a;可见性#xff0c;原子性#xff0c;有序性。可见性指的就是当一个线程修改某个变量的值之后#xff0c…一深入理解JMM内存模型
1什么是可见性
在谈jmm的内存模型之前先了解一下并发并发编程的三大特性分别是可见性原子性有序性。可见性指的就是当一个线程修改某个变量的值之后其他的线程可以立马感知到。
接下来看一个例子看一个线程改变值之后另一个线程能否立马感知到这个值被改变了。
public class JmmTest {private boolean flag true;private int count 0;public void refresh() {flag false;System.out.println(Thread.currentThread().getName() 修改flag:flag);}public void load() {while (flag) {//TODO 业务逻辑count;}System.out.println(Thread.currentThread().getName() 跳出循环: count count);}public static void main(String[] args) throws InterruptedException {JmmTest test new JmmTest();// 线程threadA模拟数据加载场景Thread threadA new Thread(() - test.load(), threadA);threadA.start();// 让threadA执行一会儿Thread.sleep(1000);// 线程threadB通过flag控制threadA的执行时间Thread threadB new Thread(() - test.refresh(), threadB);threadB.start();}
}可以发现以上操作线程A先加载这个flag值由于是true因此一直处于while循环中空转但是线程B随后修改了这个值但是可以发现线程A是还在这个while循环中的并没有跳出循环其结果值如下
threadB修改flag:false也就是说在一个正常的多线程之间的通信是不能够直接的进行通信的因此这就需要了解JMM的底层原理了
2什么是JMM
Java Memory Model 就是JMM的全称意思是java内存模型。主要用于规范java虚拟机和计算机内存时如何协调工作的规定了当一个线程改变某个共享变量值后其他线程需要如何查看以及合适可以查看这个被改变的共享数据。
jmm的内存模型如下java采用的是共享变量的模型方式在创建一个共享变量之后这些共享变量时存储在主内存中的所有线程都能访问但是每个线程需要操作这个变量时需要先将这个值加载到每个线程的工作内存中即每个线程都有对应栈帧将这个值加入到局部变量表即可就成为了共享变量的一个副本随后线程A才能去修改这个值 而由于主内存中的变量都是共享变量因此为了解决并发问题在JMM内部又引入了八大原子操作
1lock作用于主内存的变量把一个变量标记为一条线程独占状态 2unlock把一个处于锁定状态的变量释放出来释放后的变量才可以被其他线程锁定 3read(读取)作用于主内存中需要先对变量进行副本的拷贝然后将变量值传输到工作内存中 4load(载入)在工作内存中需要对传输过来的副本变量进行一个获取并且存入到工作内存中 5use(使用) 需要将获取的变量传给执行引擎 6assign(赋值)执行引擎会将这个收到的变量赋值给工作内存的变量 7store(存储)修改这个传过来的副本之后会将修改的值存储并送到主内存中 8write(写入)会将这个存储的变量写回到主内存中即修改主内存的值
如当一个线程去修改主内存中的共享变量的方式如下比如说内存中的 x 5 进行 1 的操作如下图所示首先线程A会read读取主内存中的x 5的值随后将读取到的值load载入到线程A的本地内存中一般栈帧中存放变量的都是这个局部变量表随后会通过use的指令使用这个变量将这个值加入到cpu中结果cpu内部的运算之后此时 x 6会通过assign方式将这个结果值从cpu返回到本地内存中随后将这个值返回到主内存中并通过store的方式将这个值存储最后将被修改的变量写回到主内存中。 同时在使用这八种原子操作时需要满足以下的规则
如果要把一个变量从主内存中复制到工作内存就需要按顺寻地执行read和load操作 如果把变量从工作内存中同步回主内存中就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行而没有保证必须是连续执行。不允许read和load、store和write操作之一单独出现不允许一个线程丢弃它的最近assign的操作即变量在工作内存中改变了之后必须同步到主内存中。不允许一个线程无原因地没有发生过任何assign操作把数据从工作内存同步回主内存中。一个新的变量只能在主内存中诞生不允许在工作内存中直接使用一个未被初始化load或assign的变量。即就是对一个变量实施use和store操作之前必须先执行过了assign和load操作。一个变量在同一时刻只允许一条线程对其进行lock操作但lock操作可以被同一条线程重复执行多次多次执行lock后只有执行相同次数的unlock操作变量才会被解锁。lock和unlock必须成对出现如果对一个变量执行lock操作将会清空工作内存中此变量的值在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值如果一个变量事先没有被lock操作锁定则不允许对它执行unlock操作也不允许去unlock一个被其他线程锁定的变量。对一个变量执行unlock操作之前必须先把此变量同步到主内存中执行store和write操作。
3引入volatile
在了解完这个jmm内存模型之后知道java线程之间是如何进行线程通信的再回到这个 JmmTest 方法中现在可以大胆的猜测一下是不是因为线程B修改完值后没有人去通知线程A所以才导致值没有发生变化
因此接下来继续验证就是直接在这个flag变量前面增加一个关键字 volatile
private volatile boolean flag true;其结果如下可以得出结论线程A跳出了循环就是意味着线程A接收到了这个最新的值
threadB修改flag:false
threadA跳出循环: count399766740因此查阅了一些资料以及看了一下hotspot里面关于这个volatile关键字的源码可以发现这个关键字是通过一个JVM的内存屏障来实现的。
storeload(); //jvm内存屏障在汇编指令中对应着lock关键字内存屏障可以禁止该指令与前面和后面的读写指令重排序并且可以使其他线程中的本地内存中的该值直接失效这样其他内存就需要去主内存中获取改值就能拿到最新的值了。因此volatile是通过内存屏障的方式来实现数据的可见性和有序性的。
除了这个volatile关键字之外另外像synchronizedlock等这些锁底层都是采用了这个内存屏障来实现因此这些重量级锁肯定也是可以保证可见性和有序性的同时由于是重量级操作除了这两种之外他们同时还能保证原子性。
除了内存屏障可以保证可见性之外关键字final也是可以保证可见性的。总而言之能保证可见性的方式只有两种一种是内存屏障一种是上下文切换
4cpu缓存架构
在cpu中主要由寄存器程序计数器高速缓存逻辑运算单元组成高速缓存又分了三级缓存分别是一级缓存、二级缓存和三级缓存一级缓存中又分为两部分一个用于存储指令一个用于存储数据。在inter处理器中一个cpu又分为两个处理器因此会存在两个cpu共享一个三级缓存的情况。 使用高速缓存主要是减少等待内存的时间提升CPU的计算能力
接下来根据这个缓存架构再举一个例子现在有两个线程分别是线程thread1和线程thread2假设主内存中有一个值x100接下来两个线程同时去读这个100线程1加对这个值加10线程2对这个值加20那么根据JMM的八大原子操作此时线程1的CPU的值为110线程2的CPU的值为120最终会将这个值写回主内存中。
那么此时主内存就会出现两种情况如果线程1先写回线程2后写回那么线程2会将线程1写回的值覆盖掉此时如果线程2先写回线程后写回那么线程会将线程2写回的值给覆盖掉这就是经典的线程不安全问题 造成这种原因的主要问题是因为缓存不一致的问题。 即线程1的高速缓存的值和线程2的高速缓存的值不一致所导致的因此为了解决这种缓存一致性的问题主要有两种解决方式嗅探机制、基于目录的机制
5嗅探机制
再了解完这个导致数据不安全的原因是由于缓存不一致的问题因此为了解决这个硬件层面的缓存一致性最流行的还是使用这种嗅探机制。
其工作原理如下就是说如果存在多个缓存被共享的时候如果有处理器修改了共享变量的值那么必须传播到其他所有具有该变量的副本中通过这种传播机制来防止系统违反缓存的一致性。就是说数据的变更通知是通过总线来完成的。当其他缓存接收到这个通知信息之后可以选择重新的在主内存中刷新数据也可以直接让当前缓存中的值直接失效具体是哪种做法还得取决于使用哪种缓存一致性协议。
写失效就是某个处理器将值改完之后直接通知其他处理器让其他处理器的缓存值失效
写更新就是处理器将值修改完之后在通知其他处理器的时候直接将值携带上让其他的处理器缓存值更新
总线的带宽是有效的因此写失效的使用范围是最广的。MSI、MESI、MOSI、MOESI等是最常见的缓存一致性协议
6解决缓存一致性的MESI
为了解决缓存一致性使用最多的方式是这种MESI的方式总共有四种状态分别是
Mmodify修改状态EExclusive独占状态SShare共享状态IInvalid失效状态 当工作内存将主内存的值加载到高速缓存之后假设此时只有当前线程thread1加载了X5那么此时X是一个Exclusive独占状态如果此时线程thread2也加载了这个值那么此时该值则会从一个独占状态变成一个Share共享状态如果此时线程thread1要修改这个值那么在修改这个值后X就会从一个共享状态变为一个Modify修改状态并且在回显的时候被总线窥探到总线就会发起请求告诉其他的线程这个被修改的值让其他的线程缓存里面的改值直接失效Invalid那么其他线程就可以去获取最新的值。
但是该协议并不是会直接生效而是需要在特定的时候生效就是需要一个lock前缀指令才可以满足该协议如一些常见的volatilesynchronizedlock等关键字。这样才能解决这种缓存一致性的问题。但是volatile并不能保证原子性。
并且在某个线程更新了某个值之后刷新主内存的线程会立即执行这样才能让其他已经处于失效的线程立马的回到主内存中去更新改值从而线程在获取值时减少数据的脏读问题以及长时间等待的问题。
除了缓存一致性协议之外还有总线一致性协议由于总线一致性的性能问题缓存一致性协议才得以出现。
7JMM内存可见性的保证
在单线程中由于需要保证 else-if-serial 规范即不管如何进行指令重排都必须要保证最终结果的一致性因此单线程不存在内存可见性的问题不管是编译器还是及时处理器等都必须保证和原始顺序所执行的结果值相同
在正确同步的多线程中如在加锁的情况下JMM在内部会禁止指令重排的操作并且在底层会通过内存屏障的操作来操作底层硬件从而实现可见性和有序性的操作。
未同步的多线程JMM不能保证未同步的执行结果与顺序一致性的结果一致。由于在JVM中存在一些JIT即时编译器以及解释器的一些优化等因此就会出现指令重排的情况。
x 10; y 100;
y 100; x 10;
z x 10; z x 10;举个例子如在单例模式加锁的双重检测中需要在对象的前面加一个关键字 volatile如果不加的话在new对象的时候会经历以下步骤开辟内存空间堆内存初始化栈中对象指向堆中对象。这里就会出现一个问题由于new对象并没有保证这个原子操作因此就会出现指令重排的情况就是可能会先指向堆中的对象再在堆内存中初始化就是第二步和第三步的顺序可能会发生改变。
public class SingletonTest{private volatile static SingletonTest instance null;private SingletonTest() {}public static SingletonTest getInstance() {if (instance null) {synchronized (SingletonTest.class) {if (instance null) {//在不加volatile或者其他锁的情况下//可能会出现指令重排的情况instance new Singleton();}}}return instance;}
}那在多线程的情况下在第一个线程正好执行到发生指令重排的第二步就是指向了一个堆中的对象但还没有初始化只是经历了实例化而第二个线程进行第一个if判断的时候此时并没有加锁所以发现不为null就直接return了但是return的是一个你有进行初始化的一个值因此返回的对象肯定是有问题的
所以为了解决这个指令重排的问题就需要在这个对象上面加上volatile这个关键字了这样就能禁止指令重排了
private volatile static SingletonTest instance null;8内存屏障
在jvm和硬件层面都有实现内存屏障的方式。
在jvm层面在JSR规范中定义了四种内存屏障分别是LoadStoreLoadLoadStoreLoadStoreStore。Load操作可以当做成是一个read读取操作Store操作可以当做成是一个写入操作两个操作之间相当于加了一个一堵墙从而保证两个操作的顺序不被打乱
LoadStore在store2指令写入数据之前保证数据一定被load1指令先写入进去
LoadLoad在Load2指令读取数据之前保证数据一定被load1指令先读取出来
StoreLoad在Load2指令读取数据之前保证数据一定被Store指令写入进去
StoreStore在store2指令写入数据之前保证数据一定被load1指令读取出来
并且以上的写入操作都是可以实现所有的处理器都可以感知到数据的变化即保证可见性。当前jvm底层实现内存屏障的方式主要是通过这个StoreLoad方式来实现的。
在硬件层面也提供了一系列的内存屏障的方式保证数据的一致性主要是通过ifence和sfence来实现读写屏障也可以通过Lock前缀来实现这个类似于内存屏障的功能。但是在JMM内存模型中屏蔽了这种底层硬件带来的差异直接由JVM来为不同的平台生成相应的字节码。
9为何多线程的累加值总是小于期待值
了解这个JMM的内存模型之后接下来通过之前的多线程的系列的文章来对上述这个问题做一个初步的了解。
count;由于在java中实现线程的方式是使用的内核态的方式实现的多线程也就是说开发者只能通过内核去调用操作系统再去调用线程因此开发人员并不能控制线程因此就不能控制上下文切换等并且实现线程的方式是抢占式的方式实现所以在累加操作中某个值可能只执行了一半就出现了cpu中时间片的切换导致这个值被其他线程操作如果是在多线程的情况下两个线程同时操作一个值就会出现这种值被覆盖的问题。因此最终出现的结果会小于期待值
其次是通过JMM模型可知每个线程都有属于自己的工作区间但是每个线程在将值修改之后其他线程并不能感知到就是无法保证可见性的问题因此也会出现大量的值被覆盖。所以累加的结构也会小于期待值
因此需要通过加锁的方式强行保证线程间执行顺序以及需要通过实现内存屏障的方式来实现线程间的可见性和有序性以及原子性。