热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java并发锁与volatile的内存语义

为什么80%的码农都做不了架构师?前言在前面的文章中已经提到过volatile关键字的底层实现原理:处理器的LOCK指令会使得其他处理器将缓存刷新

为什么80%的码农都做不了架构师?>>>   hot3.png

前言

在前面的文章中已经提到过volatile关键字的底层实现原理:处理器的LOCK指令会使得其他处理器将缓存刷新到内存中(确切说是主存)以及会把其他处理器的缓存设置为无效。这里的内存语义则说的是在JMM中的实现,那么为什么要理解volatile和锁在JMM中的内存语义呢?主要原因是这部分内容是与程序开发息息相关的,所以在高并发量的系统中,如果对这块知识的了解欠缺的话将无法设计出优雅支持高并发的系统(之前广被吐槽的12306,现在勉强能够支持千万级别的访问量了)。

volatile的内存语义

简而言之,volatile关键字具有以下两个特性:

  1. 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个变量最后的写入。
  2. 原子性。对任意**单个**volatile变量的读/写具有原子性,但是类似volatile++这样的操作是不具有原子性的。

之前看过一篇文章,将volatile关键字从硬件讲到软件,再从软件讲到JMM,然后从JMM讲到volatile关键字,整个过程显得特别复杂,这里仅仅站在程序员的角度,对volatile关键字在JMM的语义进行说明。上面这个过程的成立时需要一个称为happens-before的原则支持的。

happens-before原则可以概括为三点:

  1. 程序顺序规则:就是每个操作都按照在程序中顺序执行的
  2. 监视器锁规则:锁的释放之后就是锁的获取
  3. volatile变量规则:volatile写操作之后才是任意对这个volatile的读
  4. 传递性规则:这个好理解

这个原则保证了volatile特性的成立。现在问题是,虽然volatile有上面的特性,但是这个与我们程序员有什么关系呢?由于添加volatile关键字修饰一个变量的时候都是一个被共享的变量,那么任意对该共享变量的操作何时对其他线程可见(也就是内存可见性)是我们比较关心的,首先可以从JMM的角度说明volatile:volatile的写-读与锁的释放-获取有相同的效果。比如如下的代码:

package com.rhwayfun.primer.thread;public class VolatileExample {int a = 0; //普通变量volatile boolean flag = false; //共享变量//写线程public void writer(){a = 1; //1.普通写flag = true; //2.volatile写}//读线程public void reader(){if(flag){ //3.volatile读int i = a; //4.普通读}}
}

根据happens-before原则,具有以下的happens-before关系:

  1. 操作1 happens-befoe 操作2;操作3 happens-before 操作4
  2. 操作2 happens-before 操作3
  3. 操作1 happens-before 操作4

上面的这些关系有什么作用呢?其中第一点和第三点比较好理解,关键是第二点,为什么操作2会 happens-before 操作3呢?根据前面重排序的知识,编译器和处理器会对指令进行重排序(在保证正确性的前提下),但是如果一个变量被声明为volatile,那么JMM会插入内存屏障指令来防止重排序,插入内存屏障指令就保证了指令之前的volatile变量与后面的普通读/写发生重排序,所以明白这点就可以理解为什么操作2 happens-before 操作3了。根据前面处理器对volatile的处理,这里讲JMM的volatile语义做一个小结:

  1. 执行volatile写的时候,JMM会把该线程对应的本地内存(并不是实际存在的,也称为TLB,线程本地缓冲区)刷新到主内存中。这个过程可以理解为线程1(执行写方法的线程)向接下来要读取这个变量的线程2(执行读方法的线程)发送了一条消息
  2. 执行volatile读的时候,JMM会把该线程对应的本地内存设置为无效。线程接下来将直接从主内存中读取共享变量的值。这个过程可以理解为线程2接收到了线程1发送的消息

上面提到了内存屏障指令,volatile的语义在JMM中实际上与处理器的重排序规则有些类似,下面是volatile的重排序规则表:

是否能重排序第二个操作第二个操作第二个操作
第一个操作普通读/写volatile读volatile写
普通读/写  NO
volatile读NONONO
volatile写 NONO

第一列表示的是第一个操作,NO的意思是不允许进行重排序。上面的表格有点丑,Markdown的语法不支持跨列(鸡肋啊)。我们可以从上面的表格中得到三个结论:

  1. 当第一个操作为volatile读的时候,不管第二操作是什么,都不允许进行重排序
  2. 当第二个操作是volatile写的时候,不管第一个操作是什么,都不允许进行重排序
  3. 当第一个操作是volatile写的时候,如果第二个操作也是volatile的操作,那么不允许进行从排序

上面的结论怎么理解呢?根据上篇博文中JMM的内存屏障指令,以第一条结论为例,我们可以这么理解:如果volatile读和下面的普通写或读发生了重排序将会读到错误的值。

下面的问题是在volatile读或者写之间该使用什么内存屏障指令呢?具体是这样的:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。这个屏障可以禁止上面的普通写和下面的volatile写发生重排序
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。该屏障可以防止上面的volatile写与下面的volatile读或写操作发生重排序
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。该屏障可以禁止下面所有的普通读操作与上面的volatile读发生重排序
  • 在每个volatile读后面插入一个LoadStore屏障,该屏障可以禁止下面所有的普通写和上面的volatile读发生重排序

这里需要注意的是,处理器会根据具体的情况省略不必要的内存屏障。所以比如当有连续多个volatile读的时候,可能会省略一个LoadStore屏障。

锁的内存语义

锁是实现同步的重要手段,虽然volatile关键字很给力,但是volatile只能保证对单个volatile变量读或写的操作具有原子性,诸如复杂操作或者多个volatile变量的操作就不能保证了。而锁的互斥执行的特性可以确保对整个临界区的代码都具有原子性。

锁中的happens-before关系与volatile中的happens-before关系大致是一样的,现在考虑锁的获取和锁的释放在JMM中的实现,了解volatile之后,可以把锁的获取与释放的内存语义简要概括为以下几句话:

  • 线程释放一个锁,JMM就会把该线程对应的本地内存中共享变量刷新到主内存中。这个过程可以理解为该线程向接下来需要获取这个锁的线程发送一条消息
  • 另一个线程获得该锁,JMM就会把该线程对应的本地内存设置为无效。这个过程可以理解为该线程接收到了之前释放该锁的线程发送的消息

锁的内存语义的实现与可重入锁相关,可以简要总结锁的内存语义的实现包括以下两种方式:

  • 利用volatile变量的内存语义
  • 利用CAS附带的volatile语义

转:https://my.oschina.net/oosc/blog/1620305



推荐阅读
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • (三)多表代码生成的实现方法
    本文介绍了一种实现多表代码生成的方法,使用了java代码和org.jeecg框架中的相关类和接口。通过设置主表配置,可以生成父子表的数据模型。 ... [详细]
  • MySQL中的MVVC多版本并发控制机制的应用及实现
    本文介绍了MySQL中MVCC的应用及实现机制。MVCC是一种提高并发性能的技术,通过对事务内读取的内存进行处理,避免写操作堵塞读操作的并发问题。与其他数据库系统的MVCC实现机制不尽相同,MySQL的MVCC是在undolog中实现的。通过undolog可以找回数据的历史版本,提供给用户读取或在回滚时覆盖数据页上的数据。MySQL的大多数事务型存储引擎都实现了MVCC,但各自的实现机制有所不同。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 关于CMS收集器的知识介绍和优缺点分析
    本文介绍了CMS收集器的概念、运行过程和优缺点,并解释了垃圾回收器的作用和实践。CMS收集器是一种基于标记-清除算法的垃圾回收器,适用于互联网站和B/S系统等对响应速度和停顿时间有较高要求的应用。同时,还提供了其他垃圾回收器的参考资料。 ... [详细]
  • 统一知识图谱学习和建议:更好地理解用户偏好
    本文介绍了一种将知识图谱纳入推荐系统的方法,以提高推荐的准确性和可解释性。与现有方法不同的是,本方法考虑了知识图谱的不完整性,并在知识图谱中传输关系信息,以更好地理解用户的偏好。通过大量实验,验证了本方法在推荐任务和知识图谱完成任务上的优势。 ... [详细]
author-avatar
无敌鸟的秋天
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有