一、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. Foundation基础框架

    自己总结的 // // main.m // 01-结构体 // // Created by Mac-ZhangXiaoMeng on 14/12/29. // Copyright (c) 2014年 ...

  2. jstl--->Core 核心标签库->流程控制

    jstl--->Core 核心标签库->流程控制 -->if.choose.when.otherwise  <c:if>条件判断 语法1:没有本体内容  <c:if ...

  3. node实现一个WEBSOCKET服务器

    早点时候翻译了篇实现一个websocket服务器-理论篇,简单介绍了下理论基础,本来打算放在一起,但是感觉太长了大家可能都看不下去.不过发现如果拆开的话,还是不可避免的要提及理论部分.用到的地方就简要 ...

  4. hdu 1150 Machine Schedule 最小覆盖点集

    题意:x,y两台机器各在一边,分别有模式x0 x1 x2 ... xn, y0 y1 y2 ... ym, 现在对给定K个任务,每个任务可以用xi模式或者yj模式完成,同时变换一次模式需要重新启动一次 ...

  5. 智能合约语言Solidity教程系列2 - 地址类型介绍

    智能合约语言Solidity教程系列第二篇 - Solidity地址类型介绍. 写在前面 Solidity是以太坊智能合约编程语言,阅读本文前,你应该对以太坊.智能合约有所了解,如果你还不了解,建议你 ...

  6. 基于 HTML5 Canvas 的 3D 压力器反序列化

    在实际应用中,我觉得能够通过操作 JSON 文件来操作 3D 上的场景变化是非常方便的一件事,尤其是在做编辑器进行拖拽图元并且在图元上产生的一系列变化的时候,都能将数据很直观地反应给我们,这边我们简单 ...

  7. Arrays.asList()生成的List抛UnsupportedOperationException分析

    一.背景:使用工具类 Arrays.asList()把数组转换成集合时,使用其修改集合相关的方 法,它的 add/remove/clear 方法会抛出 UnsupportedOperationExce ...

  8. Django模型中value函数运用

    values(*fields) 这个方法返回的是ValuesQuerySet,是QuerySet 的子类,也就是说,你可以用QuerySet里的方法. 需要注意的是,返回的不是list,不要直接当li ...

  9. 分布式文件系统及FastDFS

    1.前言 今天来谈谈分布式文件系统,侧重点是文件系统,分布式稍微带一下.然后聊下我用的FastDFS的例子. 2.从小需求开始 我的博客的编辑器用的是markdown,它内嵌了一个文件上传功能,不过后 ...

  10. ML—高斯判别分析

    华电北风吹 天津大学认知计算与应用重点实验室 日期:2015/12/11 高斯判别分析属于生成模型,模型终于学习一个特征-类别的联合概率. 0 多维正态分布 确定一个多维正态分布仅仅须要知道分布的均值 ...