一、JAVA内存模型
关于Java内存模型的文章,网上真的数不胜数。在这里我就不打算说的很详细、很严谨了。只力求大家能更好的理解和运用,为后边的技术点做铺垫。
 
内存模型并不是Java独有的概念,而是我们的计算机硬件平台的一个概念。内存模型描述了程序中变量如何在从内存读出、以及何时写会内存的底层细节。
 
我们知道,程序运行其实就是CPU和内存的频繁交互的过程。随着CPU的快速发展,CPU的执行速度越来越快,但是内存却很难跟上CPU的执行速度,为了解决这一矛盾,CPU厂商就为每颗CPU加了高速缓存,用来缓解这个速度不匹配的问题。因此,CPU和内存的交互变成了这个样子:
以上只是在CPU和内存之间加了个高速缓存,其实也还没什么问题。那内存模型这个概念是怎么产生的呢?继续往下看。
 
CPU虽然在不停的发展,但单个CPU的主频速度不可能无限制的增长,为了进一步提高计算性能就引入了多核技术。由于每个cpu都有自己的高速缓存,当多个CPU操作同一个内存数据时,就产生了缓存不一致的问题。如下图:
为了解决这个不一致的问题,就需要处理器在运行时要遵循某些协议,这类协议包括MSI、MESI、MOSI等等。到这里就有了内存模型这个概念,它就是用来描述数据在各个高级缓存以及内存之间的交互细节。不同的硬件处理器架构,就会有不同的内存模型。所以用c/c++开发多线程程序时,就需要考虑不同操作平台下的内存模型。
 
所幸我们是学Java的,Java平台为了屏蔽不同硬件平台的不同内存模型给开发人员带来的成本,引入了Java内存模型,即JAVA Memory Model,简称JMM。
 
要想深入掌握JAVA多线程并发编程,Java内存模型是必须要了解的。Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。直白点说就是:同一个变量,被多个cpu上执行的多个线程访问,每个cpu的高速缓存都缓存了这个变量,当某个线程修改了高速缓存里的变量,何时通知给其他的cpu线程让它可见,以及何时将变量同步回内存(主存)。如下图:
Java虚拟机的内存模型和计算机硬件的内存模型基本一致。在Java内存模型中,分为线程私有的本地内存和线程共享的主内存,线程在读写变量时会把主内存里的变量缓存到本地内存,换句话说,本地内存存放了主内存中变量的副本。主内存和本地内存其实是一种逻辑上的划分,并不是实际的物理内存。
 
这里需要强调一下,这里的变量指的是分配到堆上的变量,即线程之间可以共享的变量。本地变量是线程私有的,所以不会有可见性问题。
 
二、volatile
Java内存模型中说到了线程间共享变量的可见性问题。可见性问题其实就是缓存不一致的问题。如下图:
线程B读取变量X,并缓存到了自己的本地内存中,线程A也将变量X缓存到本地内存中并修改为2,这时线程B并不知道变量X修改为2。这就是线程间不可见的问题。为了解决这个问题,就引入了volatile关键字,被volatile修饰的变量将不会在本地内存缓存,线程直接通过主内存来读写变量。虽然解决了不可见的问题,但也是以牺牲性能为代价的。
 
volatile关键字相信你已经理解了,但是在Java中volatile并不仅仅是这个功能。在这里我通过与c语言中的volatile对比扩展下。
有的时候我们可能会面临这么个场景,线程1执行某些业务逻辑,线程2判断线程1是否执行完,执行完了则线程2执行另一个逻辑,如下伪代码:
我们通过一个flag变量来标识线程1是否执行完相关逻辑,为了保证flag的改变对线程2可见,这里使用了volatile关键字修饰。如果这个伪代码采用Java实现,这是没问题的,如果c实现,则就会有坑。
这个坑主要是源于指令重排。为了提高执行效率减少内存的交互,编译器会根据情况对执行的指令做一个重排序。所以线程1中执行相关业务逻辑后,再将flag设置为true的逻辑,极有可能重排为:先设置flag=true然后再执行相关业务逻辑。这也是c语言为啥不提倡使用volatile的原因。
 
但是为什么在Java中就不会有这个坑呢,难道Java没有指令重排序吗?
当然不是,Java也会有重排序,不过Java对volatile做了如下的极大增强:
  • 所有对volatile变量的写操作之前的针对其他变量的读写操作,经过编译器、cpu优化后,都不会被重排到对voltile变量的写操作之后。
  • 所有对volatile变量的读操作之后的针对其他变量的读写操作,经过编译器、cpu优化后,都不会被重排到对voltile变量的读操作之前。
 
面试中,有面试官比较喜欢问这么一个问题:能否用volatile修饰的整数变量n,通过n++操作实现计数的功能?这个问题就是考查应试者对volatile的理解。我这里简单地说一下。
答案肯定是不能。volatile实现的是线程间共享变量的可见性,并不是原子性操作。++操作其实可以拆分为这么几个步骤:
  1. 读取主内存里的变量
  2. cpu完成变量的++,然后写会主内存。
所以可以想象这么一个执行顺序:
  1. 线程A读取volatile变量X=0
  2. 线程B读取volatile变量X=0
  3. 线程A完成++操作,然后将X=1写回主存。
  4. 线程B也完成++操作将X=1写回主存。
在这么一个执行顺序下,对X进行了++两次,但值却只增加了1。
 
 
关于如何实现原子性操作,我将在下一节进行讨论。
 
 
 

自己动手写把”锁”之---JMM和volatile的更多相关文章

  1. 自己动手写把”锁”---LockSupport介绍

    本篇是<自己动手写把"锁">系列技术铺垫的最后一个知识点.本篇主要讲解LockSupport工具类,它用来实现线程的挂起和唤醒. LockSupport是Java6引入 ...

  2. 自己动手写把”锁”---LockSupport深入浅出

    本篇是<自己动手写把"锁">系列技术铺垫的最后一个知识点.本篇主要讲解LockSupport工具类,它用来实现线程的挂起和唤醒. LockSupport是Java6引入 ...

  3. 死磕 java同步系列之自己动手写一个锁Lock

    问题 (1)自己动手写一个锁需要哪些知识? (2)自己动手写一个锁到底有多简单? (3)自己能不能写出来一个完美的锁? 简介 本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁. ...

  4. 自己动手写java锁

    1.LockSupport的park和unpark方法的基本使用,以及对线程中断的响应性 LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语.java锁和同步器 ...

  5. Java并发编程:自己动手写一把可重入锁

    关于线程安全的例子,我前面的文章Java并发编程:线程安全和ThreadLocal里面提到了,简而言之就是多个线程在同时访问或修改公共资源的时候,由于不同线程抢占公共资源而导致的结果不确定性,就是在并 ...

  6. 动手写一个简单版的谷歌TPU-指令集

    系列目录 谷歌TPU概述和简化 基本单元-矩阵乘法阵列 基本单元-归一化和池化(待发布) TPU中的指令集 SimpleTPU实例: (计划中) 拓展 TPU的边界(规划中) 重新审视深度神经网络中的 ...

  7. 自己动手写SQL执行引擎

    自己动手写SQL执行引擎 前言 在阅读了大量关于数据库的资料后,笔者情不自禁产生了一个造数据库轮子的想法.来验证一下自己对于数据库底层原理的掌握是否牢靠.在笔者的github中给这个database起 ...

  8. 60行自己动手写LockSupport是什么体验?

    60行自己动手写LockSupport是什么体验? 前言 在JDK当中给我们提供的各种并发工具当中,比如ReentrantLock等等工具的内部实现,经常会使用到一个工具,这个工具就是LockSupp ...

  9. 【原创】自己动手写控件----XSmartNote控件

    一.前面的话 在上一篇博文自己动手写工具----XSmartNote [Beta 3.0]中,用到了若干个自定义控件,其中包含用于显示Note内容的简单的Label扩展控件,用于展示标签内容的labe ...

随机推荐

  1. Android Studio 升级到3.0 提示 java.lang.NoClassDefFoundError

    Android Studio 升级到3.0 提示 java.lang.NoClassDefFoundError 这个问题折腾了2个小时,最后解决了,Stack Overflow 上也有一次类似的问题, ...

  2. Servlet中web.xml 以及 <url-pattern>总结

    web.xml中添加Servlet配置信息 使用Eclipse创建Servlet,会自动的在WEB-INF下的web.xml中声明,但是有的时候需要我们手动的写入配置信息,以下就是Servlet在we ...

  3. javascript语言基础

    js的基本语法 /* 多行注释 * */ //单行注释 // 变量赋值 默认以换行符作为结束符,有分号以分号作为结束符号 var i; i=10; s="hello"; var b ...

  4. LoadRunner性能测试过程中报Error(-17998):Failed to get [param not passed in call] thread TLS entry.

    最近与其他公司一起合作使用loadrunner11进行性能测试,在此过程中,遇到Error(-17998)的错误,从网上搜索,找到的答案基本上都是说没有定义事务,但检查所有测试代码,发现都已经定义了事 ...

  5. (翻译)使用Api分析器与Windows兼容包来编写智能的跨平台.NET Core应用

    本文翻译自Scott Hanselman博客: https://www.hanselman.com/blog/WritingSmarterCrossplatformNETCoreAppsWithThe ...

  6. centos 下安装pptp (vpn) 的方法

    废话少说     01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 3 ...

  7. 【下一代核心技术DevOps】:(五)微服务CI与Rancher持续集成

    1. 引言 DevOps的核心魅力是快速的持续集成交付,降低研发和实施运维之间的交互,使得传统的各种扯皮现象统统消失.最重要的是降低成本 保障产品交付可靠性. 使用Rancher作为持续集成的关键环节 ...

  8. 如何编写一个稳定的网络程序(TCP)

    本节我们看一下怎样才能编写一个基于TCP稳定的客户端或者服务器程序,主要以试验抓包的方式观察数据包的变化,对网络中出现的多种情况进行分析,分析网络程序中常用的技术及它们出现的原因,在之后的编程中能早一 ...

  9. Spring任务调度之SpringTask基于XML和基于注解的使用示例

    使用Spring的环境要求是:JDK1.8以上.Maven3.0以上. Maven依赖 SpringTask集成在SpringContext中,所以只需要SpringContext即可. 可以使用ma ...

  10. 如何使用 Secret?- 每天5分钟玩转 Docker 容器技术(108)

    我们经常要向容器传递敏感信息,最常见的莫过于密码了.比如: docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql 在启动 MySQL 容器时我 ...