前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >原 【JDK并发基础】Java内存模型详解

原 【JDK并发基础】Java内存模型详解

作者头像
我叫刘半仙
发布2018-04-16 16:18:25
9640
发布2018-04-16 16:18:25
举报
文章被收录于专栏:我叫刘半仙

       无论你是Java还是C,或者其他编程语言编写高并发程序时,都或多或少的会涉及内存模型。高并发程序下数据访问的一致性和安全性受到挑战,为了保证程序正确执行,Java内存模型(以下简称JMM)由此而诞生。如果不理解JMM,就会对内存可见性,有序性等问题出现时无从下手。本文将从以下几个方面进行JMM的说明:

       1.内存模型的相关概念

       2.可见性

       3.有序性

       4.原子性

1.内存模型的相关概念

1.1 Java虚拟机运行时数据区

       在共享内存模型里,线程间的是通过读-写内存中的公共状态进行隐式通信的,那线程在Java虚拟机里是什么位置呢?Java虚拟机运行时数据区:

       堆:解决的是对象数据存储问题。

方法区:是先决条件,储存已经被虚拟机加载过的类信息,常量,静态变量等信息。它和堆描述的是Java共享数据(主)内存模型。

       虚拟机栈:栈解决的是程序运行的问题,即程序如何处理数据。从图中可以看栈是线程私有的,函数的调用要用栈实现,函数运算并改变栈中存放数据的引用。所以虚拟机栈描述的是Java执行的内存模型: 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、  操作数栈、 动态链接、 方法出口等信息。

       本地方法栈:和虚拟机栈类似,虚拟机栈为Java方法服务,而本地方法栈为虚拟机使用到的Native方法服务。程序计数器:cpu中的寄存器,它包含当前正在执行的指令的地址(位置)。(寄存器是cpu组成部分,暂存指令数据地址--百度百科)。

1.2  缓存一致性协议

       CPU在执行指令过程中,势必会牵扯到变量的读和写。共享变量存储到内存中,CPU通过高速缓存(Cache)与内存进行通信,但是CPU执行指令比CPU从内存读写共享变量要快的多。而且在多核CPU时代,每条线程可能运行在不同的CPU里。比如:

代码语言:javascript
复制
i=i+1;

      两个线程读取i的值并在自己的CPU的高速缓存中+1操作,当第一个线程运算完了存到高速缓存还没有刷新到内存中,线程二读取到i还是0,也做了+1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

       最终结果i的值是1,而不是2。这就是缓存一致性问题。怎么解决呢?当CPU写数据时,发现其他CPU也在操作这个共享变量,会让其他CPU的缓存无效且从内存中重新获取。这个缓存一致性协议最出名的就是Intel 的MESI协议。

2.可见性

  • 可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

       程序在执行过程中任何时候都可能产生可见性问题,这里只讨论JMM相关的可见性问题。Java线程之间的通信由JMM控制,JMM的抽象示意图如下:

       本地内存A和B有主内存中共享变量x的副本。假设初始时三个内存中的x值都为0。线程A修改x=1后-->主内存修改为x=1-->最后由B修改x=1。这些步骤实质上就是A在给B发消息,且要通过主内存,及JMM通过控制主内存在控制线程之间的内存可见性。

       我们来看一个Java虚拟机层面产生的可见性问题:

代码语言:javascript
复制
public class Visibility implements Runnable{
	private  boolean isStoped = false;
	 
	public void run() {
	    int i = 0;
	    while(!isStoped) {
	        i++;
	    }
	    System.out.println("end and print,i=" + i);
	}
	 
	public void stopFunction() {
	    	isStoped = true;
	}
	 
	public boolean getStopStatus(){
	        return isStoped;
	}
	public static void main(String[] args) throws Exception {
	    Visibility visibility = new Visibility();
	    new Thread(visibility,"visibility").start();
	    Thread.sleep(1000);
        //当主线程直到主线程调用stop方法,改变了v线程中的stop变量的值使循环停止。
	    visibility.stopFunction();
	    Thread.sleep(1000);
	    System.out.println("finish main");
	    System.out.println(visibility.getStopStatus());
	}
}

        运行结果:

       为什么循环没停止,且没有打印System.out.println("end and print,i=" + i);这句话?原因是主线程调用把isStoped值修改为true对visibility线程并不可见,所以主线程走完了,而visibility线程一直在做i++操作。解决上述变量不可见性的方法:用volatile关键字修饰isStoped变量,它会强制性的从主内存中取值,从而避免本地内存变量不可见问题。

3.有序性

  •  即程序执行的顺序按照代码的先后顺序执行。

       很奇怪?其实我们写代码时只是看上去有序,代码在编译成指令执行时会进行重排序保证程序运行的高效率。 因为下面的语句1和语句2没有什么数据依赖性,可能会打乱顺序执行:

代码语言:javascript
复制
a=b+c;    //语句1
d=e-f;    //语句2
g=a+3;    //语句3

       CPU执行一条指令的时候一般有:取指IF-->译码和读取寄存器操作数ID-->执行或者有效地址计算EX(用到CPU中逻辑运算单元)-->存储器访问MEM-->写回WB(用到寄存器)

       比如两条指令执行顺序由上到下:

       当我们运行语句1和语句2时有:

       上图产生气泡X会严重影响效率,所以会把没有相关性的操作加入到有气泡操作里,这就是处理器为了提高程序运行效率,对输入代码进行优化:

       一般认为CPU的指令都是原子操作,虽然重排序不会影响单个线程内程序执行的结果,但是多线程会有影响:

代码语言:javascript
复制
public class VolatileSort {
	int a =0;
	boolean flag = false;
	public void write(){
		a=1;
		flag =true;
	}
	public void read(){
		if(flag){
			int i=a+1;
			//...
		}
	}
}

       一个线程执行write方法,另一个线程检查flag=true时,a=1还未执行,会导致程序运算错误。解决方法还是使用volatile关键字修饰变量。

4.原子性

  • 原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰,要么直接就不执行。

       volatile关键字最致命的缺点是不支持原子性。比如count++这样的操作就不是原子性的:

代码语言:javascript
复制
public class VolatileTest {
    private volatile int count= 0;
       //解决方法之一
//    Lock lock = new ReentrantLock();
//    public  void increase() {
//        lock.lock();
//        try {
//            count++;
//        } finally{
//            lock.unlock();
//        }
//    }
    //解决方法之二
//  public synchronized  void increase() {
    public void increase() {
        count++;
    }
    public static void main(String[] args) {
        final VolatileTest test = new VolatileTest();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
       //保证前面的线程都执行完
        while(Thread.activeCount()>1){  
            Thread.yield();
        }
        //执行结果并不是10000
        System.out.println(test.count);
    }
}

       count++其实有三个操作:读取count-->做count+1操作-->然后把count+1后的值写回到count里。

       假设有两个线程,当第一个线程读取count=1时,还没进行+1操作,切换到第二个线程,此时第二个线程也读取的是count=1。随后两个线程进行后续+1操作,再赋值回去以后,count不是3,而是2。显然数据出现了不一致性,可以使用上面代码的方法一和方法二加锁或原子类操作:

代码语言:javascript
复制
public class VolatileTest2 {
    private AtomicInteger count= new AtomicInteger();
    public  void increase() {
        count.getAndIncrement();
    }
    public static void main(String[] args) {
        final VolatileTest2 test = new VolatileTest2();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        while(Thread.activeCount()>1){  
            Thread.yield();
        }
        System.out.println(test.count.get());
    }
}

其实前面或多或少提到了volatile关键字,这里做一些扩展,volatile是如何保证可见性的呢?

代码语言:javascript
复制
volatile User user =new User();

上述代码在x86处理器通过工具获取JIT编译器生成的汇编指令如下:

       0x01a3deld:movb 》$0×0,0×1104800(%esi);0x01a3de4>:lock add1 $0×0,(%esp);

lock指令在多核处理器会引发两件事:

       1.将当前缓存行的数据写回到系统内存。

       2.这个写回内存操作会使其他CPU里缓存了该内存地址的数据无效。

参考:

       《深入理解Java虚拟机》

       《Java并发编程的艺术》

       《Java高并发程序设计》

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.内存模型的相关概念
    • 1.1 Java虚拟机运行时数据区
      • 1.2  缓存一致性协议
      • 2.可见性
      • 3.有序性
      • 4.原子性
      相关产品与服务
      数据保险箱
      数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档