一、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. Error when sending message to topic test with key: null, value: 2 bytes with error: (org.apache.kafka.clients.producer.internals.ErrorLoggingCallback)

    windows下使用kafka遇到这个问题: Error when sending message to topic test with key: null, value: 2 bytes with ...

  2. JavaScript练习网站收集

    在学习的过程中会发现很多知识点如果不在工作中运用或者手写带验证的话,很容易忘记.任何技能的掌握都是需要不断练习的.在此收集一些自己遇到的JavaScript练习的网站. codewars 国外的一个练 ...

  3. WebRTC介绍及简单应用

    WebRTC介绍及简单应用 WebRTC,即Web Real-Time Communication,web实时通信技术.简单地说就是在web浏览器里面引入实时通信,包括音视频通话等. WebRTC实时 ...

  4. Code Kata:大整数比较大小&大整数四则运算---加减法 javascript实现

    大整数的四则运算已经是老生常谈的问题了.很多的库也已经包含了各种各样的解决方案. 作为练习,我们从最简单的加减法开始. 加减法的核心思路是用倒序数组来模拟一个大数,然后将两个大数的利用竖式进行运算. ...

  5. 实践作业3DAY1

    今天,老师又布置了新的学习任务,关于白盒测试.感觉黑盒测试,我们用的比较多,白盒测试就相对陌生了.上课的时候老师虽然也进行了一定的点拨,外加我们学习了SPOC视频,但是并没有看到什么具体的项目,所以实 ...

  6. Undefined index: HTTP_RAW_POST_DATA的解决办法

    $postStr = $GLOBALS["HTTP_RAW_POST_DATA"]; 替换为 $postStr = isset($GLOBALS['HTTP_RAW_POST_DA ...

  7. FastDFS教程Ⅲ-文件服务器扩容

    1.简介     FastDFS文件服务器在设计时,为了支持大容量,存储节点(服务器)采用了分卷(或分组)的组织方式.存储系统由一个或多个卷组成,卷与卷之间的文件是相互独立的,所有卷的文件容量累加就是 ...

  8. Java分布式锁之数据库实现

    之前的文章<Java分布式锁实现>中列举了分布式锁的3种实现方式,分别是基于数据库实现,基于缓存实现和基于zookeeper实现.三种实现方式各有可取之处,本篇文章就详细讲解一下Java分 ...

  9. PHPstorm 函数时间注释的修改

    正常的PHPstorm里面函数方法的注释是没有动态时间设置的,但是看了PHP file里面有时间日期的注释,而PHP Function Doc Comment 却没有,让很多PHPer很头疼,今天在搜 ...

  10. 关于在linux下清屏的几种技巧(转载-备忘)

    原文地址:http://www.cnblogs.com/5201351/p/4208277.html 在windows的DOS操作界面里面,清屏的命令是cls,那么在linux 里面的清屏命令是什么呢 ...