深入理解Java内存模型JMM与volatile关键字

并发编程三大特性:可见性、原子性、有序性
volatile保证可见性与有序性,但是不保证原子性,保证原子性需要借助synchronized这样的锁机制
多核并发缓存架构:
深入理解Java内存模型JMM与volatile关键字_第1张图片
Java线程内存模型跟cpu缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
深入理解Java内存模型JMM与volatile关键字_第2张图片

以下测试程序,测试线程会在工作内存中保存一个共享变量的副本。

package demo6;

public class VolatileTest {
 private static boolean initFlag = false;
 public static void main(String[] args)throws InterruptedException {
	 new Thread(new Runnable(){
		 public void run() {
			 System.out.println("waiting data ....");
		 //死循环
		   while(!initFlag) {
		   }
		   System.out.println("*************sucess!!!");
		 }
		 
	 }).start();
	 
	 Thread.sleep(2000);
	 new Thread(new Runnable() {
		 public void run() {
			 prepareData();
		 }
	 }).start();
 }
 public static void prepareData() {
	 System.out.println("prepareing data .....");	
	 initFlag = true;
	 System.out.println("prepare data end.....");	
} 
}

打印如下:
深入理解Java内存模型JMM与volatile关键字_第3张图片
由上打印结果可知在prepareData()方法中initFlag的值改变并未影响另一个线程while(initFlag)的值。这是由于多个线程中都有一个副本值,当另一个线程修改了这个值后对其它线程是不可见的。
解决办法:(1.用 volatile 修饰2.synchronized(也会刷新副本中工作内存中的值))

 private static volatile boolean initFlag = false;

所以:volatile 作用之一: 用来修饰共享变量可以达到数据在多线程之间的可见性,就是一个线程修改了这个变量,其它线程知道它修改了。
方法:使用的是MESI缓存一致性协议
JMM数据原子操作

  • read(读取):从主内存读取数据
  • load(载入):将主内存读取到的数据写入工作内存
  • use(使用):从工作内存读取数据来计算
  • assign(赋值):将计算好的值重新赋值到工作内存中
  • store(存储):将工作内存数据写入主内存
  • write(写入):将store过去的变量赋值给主内存中的变量
  • lock(锁写):将主内存变量加锁,标识为线程独占状态
  • unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
    下面我们来看一下上面加了volatile 关键字后代码在硬件中原子操作的运行流程(JMM内存模型):
    深入理解Java内存模型JMM与volatile关键字_第4张图片

volatile 缓存可见性实现原理:
底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存
IA-32架构软件开发者手册对lock指令的解释

  1. 会将当前处理缓存行的数据立即写回到系统内存
  2. 这个写回操作会引起在其它cpu里缓存了该内存地址的数据无效(即MESI协议)

我们来看一下volatile不支持的原子性

package demo6;

public class VolatileTest1 {
    public static volatile int num =0;
    public static void increase() {
    	num++;
    }
	public static void main(String[] args) throws InterruptedException {
	   Thread[] threads = new Thread[10];
	   for(int i=0;i<threads.length;i++) {
		     threads[i] = new Thread(new Runnable(){
				@Override
				public void run() {
					for(int i=0;i<1000;i++) { 
						increase();
					}
				}
		     });
		     threads[i].start();
	   }
         for(Thread t:threads) {
        	 t.join();
         }
         System.out.println(num);
	}
}

如果volatile保证原子性则打印结果应为10000
但结果打印如下:

深入理解Java内存模型JMM与volatile关键字_第5张图片
每一次打印的结果都不一样,这是因为其它线程同时也做自增操作时从新去主存read时,原来自增的值会丢失(出现了写覆盖)。
解决办法:1.加synchronized(2.AtomicInteger 封装类)

package demo6;

import java.util.concurrent.atomic.AtomicInteger;

public class VolatileTest1 {
    public static volatile int num =0;
    public static void increase() {
    	num++;
    }
    static AtomicInteger atomicInteger = new AtomicInteger();
    public static void addMyAtomic() {
    	atomicInteger.getAndIncrement(); //每次自增1
    	
    }
	public static void main(String[] args) throws InterruptedException {
	   Thread[] threads = new Thread[10];
	   for(int i=0;i<threads.length;i++) {
		     threads[i] = new Thread(new Runnable(){
				@Override
				public void run() {
					for(int i=0;i<1000;i++) { 
						increase();
						addMyAtomic();
					}
				}
		     });
		     threads[i].start();
	   }
         for(Thread t:threads) {
        	 t.join();
         }
         System.out.println("不保证原子性:"+num);
         System.out.println("保证原子性:"+atomicInteger);
	}
}

打印结果为:
深入理解Java内存模型JMM与volatile关键字_第6张图片

本博文内容出自原作者链接

你可能感兴趣的:(JMM)