一、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. QuickTime视频解析问题

    在QuickTime中可以解析出视频并播放视频,解析的格式后缀名为.mov,之后将该视频导入到Unity Project中,显示未解析到视频文件,本来应该会自动生成MovieTexture材质,但是并 ...

  2. css选择器的优先级问题

    当我们写页面的时候,不知道你会不会产生这样的问题,为什么我给他添加的这条样式分明已经选择到我要给的元素了,但是他的样式并没有生效,那是为什么呢? 定义的属性有冲突时,浏览器会选择用那一套样式呢,下面来 ...

  3. Numpy入门 - 数组排序

    本节主要讲解numpy数组的排序方法sort的应用,包括按升序排列和按降序排列. 一.按升序排列 import numpy as np arr = np.array([[3, 1, 2], [6, 4 ...

  4. Spring Cloud 之 Ribbon

    新建Spring Boot工程,命名为ribbon 1.pom.xml添加依赖 <?xml version="1.0" encoding="UTF-8"? ...

  5. String、StringBuilder和StringBuffer

    1.string不可变性 java的docs有这样一句话:Strings are constant; their values cannot be changed after they are cre ...

  6. NYOJ 119 士兵杀敌(三) RMQ ST

    NYOJ 119 士兵杀敌(三) RMQ ST 题目链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=119 思路: ST在线 预处理O(nlog ...

  7. 有关BOM头的一些知识

    在psr开发标准中,有一条是讲的,php只能使用无bom的utf8格式 . 那么这个bom是几个意思.  说一些理论内容 . 在UCS编码中有一个叫做"ZERO WIDTH NO-BREAK ...

  8. 域名和ip不能访问的原因

    centos的话可能默认可能会有firewalld,可以执行 systemctl stop firewalld systemctl disable firewalld 禁用后在看看,前提都是域名得备案 ...

  9. Cesium几个案例介绍

    前言 本文为大家介绍几个Cesium的Demo,通过这几个Demo能够对如何使用Cesium有进一步的了解,并能充分理解Cesium的强大之处和新功能.其他的无需多言,如果还不太了解什么是Cesium ...

  10. day8、 显示Linux路由表、各列信息

    要用到的命令是 route route 命令    显示和设置Linux路由表 -A:设置地址类型: -C:打印将Linux核心的路由缓存: -v:详细信息模式: -n:不执行DNS反向查找,直接显示 ...