synchronized原理_synchronized 底层原理与内存屏障

点击?蓝色“ 深入原理”,关注并“设为星标”

技术干货,第一时间推送

锁概述

我们知道线程安全问题的产生前提是多个线程并发访问共享变量、共享资源(以下统称为共享数据)。于是,我们很容易想到保障线程安全的方法将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问,该线程访问结束后其他线程才能对其进行访问。锁(Lock)就是利用这种思路以保障线程安全的线程同步机制。

按照上述思路,锁可以理解为对共享数据进行保护的许可证。对于同一个许可证所保护的共享数据而言,任何线程访问这些共享数据前必须先持有该许可证。一个线程只有在持有许可证的情况下才能够对这些共享数据进行访问;并且,一个许可证一次只能够被一个线程持有;许可证的持有线程在其结束对这些共享数据的访问后必须让出(释放)其持有的许可证,以便其他线程能够对这些共享数据进行访问。

一个线程在访问共享数据前必须申请相应的锁(许可证),线程的这个动作被称为锁的获得(Acquire)。一个线程获得某个锁( 持有许可证 ),我们就称该线程为相应锁的持有线程 ( 线程持有许可证 ),一个锁一次只能被一个线程持有。锁的持有线程可以对该锁所保护的共享数据进行访问,访问结束后该线程必须释放 ( Release ) 相应的锁。锁的持有线程在其获得锁之后和释放锁之前这段时间内所执行的代码被称为临界区( Critical Section )。因此,共享数据只允许在临界区内进行访问,临界区一次只能被一个线程执行。

锁具有排他性(Exclusive),即一个锁一次只能被一个线程持有。因此,这种锁被称为排他锁或者互斥锁 ( Mutex )。这种锁的实现方式代表了锁的基本原理,如图所示。 

3fc82c2505ac2449fee27d563bf521a6.png

按照Java虚拟机对锁的实现方式划分,Java 平台中的锁包括内部锁( Intrinsic  Lock )和显式锁 ( Explicit Lock )。内部锁是通过synchronized关键字实现的;显式锁是通过java.concurrent.locks.Lock接口的实现类(如 java.concurrent.locks.ReentrantLock 类 ) 实现的。

锁的作用

锁能够保护共享数据以实现线程安全,其作用包括保障原子性、保障可见性和保障有序性。

锁是通过互斥保障原子性的。所谓互斥 ( Mutual Exclusion ),就是指一个锁一次只能被一个线程持有。因此一个线程持有一个锁的时候,其他线程无法获得该锁,而只能等待其释放该锁后再申请。这就保证了临界区代码一次只能够被一个线程执行。因此,一个线程执行临界区期间没有其他线程能够访问相应的共享数据,这使得临界区代码所执行的操作自然而然地具有不可分割的特性,即具备了原子性。

从互斥的角度来看,锁其实是将多个线程对共享数据的访问由本来的并发( 未使用锁的情况下 )改为串行( 使用锁之后 )。因此,虽然实现并发是多线程编程的目标,但是这种并发往往是并发中带有串行的局部并发。这好比公路维修使得多股车道在某处被合并成一股小车道,从而使原本在多股车道上并驾齐驱的车辆不得不“鱼贯而行”。

我们知道,可见性的保障是通过写线程冲刷处理器缓存和读线程刷新处理器缓存这两个动作实现的。在Java平台中,锁的获得隐含着刷新处理器缓存这个动作,这使得读线程在执行临界区代码前( 获得锁之后 ) 可以将写线程对共享变量所做的更新同步到该线程执行处理器的高速缓存中;而锁的释放隐含着冲刷处理器缓存这个动作,这使得写线程对共享变量所做的更新能够被“推送” 到该线程执行处理器的高速缓存中,从而对读线程可同步。因此,锁能够保障可见性。

锁能够保障有序性。写线程在临界区中所执行的一系列操作在读线程所执行的临界区看起来像是完全按照源代码顺序执行,即读线程对这些操作的感知顺序与源代码顺序一致。这是暂且对原子性和可见性的保障的结果。设写线程在临界区中更新了b 、c 和 flag 这 3 个共享变量,如下代码片段所示 :

3a4fc4b5d192fac5e1549a4c50e4a0f2.png

由于锁对可见性的保障,写线程在临界区中对上述任何一个共享变量所做的更新都对读线程可见。并且,由于临界区内的操作具有原子性,因此写线程对上述共事变量的更新会同时对读线程可见,即在读线程看来这些变量就像是在同一刻被更新的。因此读线程并无法(也没有必要)区分写线程实际上是以什么顺序更新上述变量的,这意味着读线程可以认为写线程是依照源代码顺序更新上述共享变量的,即有序性得以保障。

尽管锁能够保障有序性,但是这并不意味着临界区内的内存操作不能够被重排序。临界区内的任意两个操作依然可以在临界区之内被重排序(即不会重排到临界区之外)。由于临界区内的操作具有的原子性,写线程在临界区内对各个共享数据的更新同时对读线程可见,因此这种重排序并不会对其他线程产生影响。

在理解,以及使用锁保证线程安全的时候,需要注意锁对可见性、原子性和有序性的保障是有条件的,我们要同时保证以下两点得以满足。

• 这些线程在访问同一组共享数据的时候必须使用同一个锁。

• 这些线程中的任意一个线程,即使其仅仅是读取这组共享数据而没有对其进行更新的话,也需要在读取时持有相应的锁。

上述任意一个条件未满足都会使原子性、可见性和有序性没有保障。可见,我们说锁能够保护共享数据其实是一种“协议” 的结果,这个协议就是任何访问该共享数据的写线程、 读线程都要满足上述条件。只要有任何一个线程没有遵守这个协议实际上就被打破,从而无法保障线程安全。这就好比交通规则( “协议” ) 要靠人人都遵守才能保障交通安全一样。

Java平台中的任何一个对象都有唯一一个与之关联的锁。这种锁被称为监视器 ( Monitor ) 或者内部锁 ( Intrinsic Lock )。内部锁是一种排他锁,它能够保障原子性、可见性和有序性。

内部锁是通过synchronized关键字实现的。synchronized 关键字可以用来修饰方法以及代码块( 花括号 “ { } ” 包裹的代码 )。

synchronized关键字修饰的方法就被称为同步方法( Synchronized Method )。synchronized  修饰的静态方法就被称为同步静态方法,synchronized  修饰的实例方法就被称为同步实例方法。同步方法的整个方法体就是一个临界区。

synchronized关键字所引导的代码块就是临界区。锁句柄是一个对象的引用(它或者能够返回对象的表达式)。例如,锁句柄可以填写为this关键字( 表示当前对象 )。习惯上我们也直接称锁句柄为锁。锁句柄对应的监视器就被称为相应同步块的引导锁。相应地,我们称呼相应的同步块为该锁引导的同步块。

作为锁句柄的变量通常采用final修饰。这是因为锁句柄变量的值一旦改变,会导致执行同一个同步块的多个线程实际上使用不同的锁,从而导致竞态。有鉴于此,通常我们会使用 private 修饰作为锁句柄的变量。

线程在执行临界区代码的时候必须持有该临界区的引导锁。一个线程执行到同步块(同步方法也可看作同步块)时必须先申请该同步块的引导锁,只有申请成功(获得)该锁的线程才能够执行相应的临界区。一个线程执行完临界区代码后引导该临界区的锁就会被自动释放。在这个过程中,线程对内部锁的申请与释放的动作由Java虚拟机负责代为实施,这也正是 synchronized 实现的锁被称为内部锁的原因。

内部锁的使用并不会导致锁世漏。这是因为Java编译器 ( javac ) 在将同步块代码编译为字节码的时候,对临界区中可能抛出的而程序代码中又未捕获的异常进行了特殊( 代为 )处理,这使得临界区的代码即使抛出异常也不会妨碍内部锁的释放。

内部锁的调度

Java虚拟机会为每个内部锁分配一个入口集 ( Entry  Set ),用于记录等待获得相应内部锁的线程。多个线程申请同一个锁的时候,只有一个申请者能够成为该锁的持有线程( 即申请锁的操作成功 ),而其他申请者的申请操作会失败。这些申请失败的线程并不会抛出异常,而是会被暂停( 生命周期状态变为 BLOCKED ) 并被存入相应锁的入口集中等待再次申请锁的机会 。入口集中的线程就被称为相应内部锁的等待线程。当这些线程申请的锁被其持有线程释放的时候,该锁的入口集中的一个任意线程会被Java虚拟机唤醒,从而得到再次申请锁的机会。由于Java 虚拟机对内部锁的调度仅支持非公平调度,被唤醒的等待线程占用处理器运行时可能还有其他新的活跃线程 ( 处于RUNNABLE 状态,且未进入过入口集 ) 与该线程抢占这个被释放锁,因此被唤醒的线程不一定就能成为该锁的持有线程。另外,Java 虚拟机如何从一个锁的入口集中选择一个等待线程,作为下一个可以参与再次申请相应锁的线程,这个细节与 Java 虚拟机的具体实现有关:这个被选中的线程有可能是入口集中等待时间最长的线程,也可能是等待时间最短的线程,或者完全是随机的一个线程。因此,我们不能依赖这个具体的选择算法。

前文我们讲解锁是如何保证可见性的时候提到了线程获得和释放锁时所分别执行的两个动作:刷新处理器缓存和冲刷处理器缓存。对于同一个锁所保护的共享数据而言,前一个动作保证了该锁的当前持有线程能够读取到前一个持有线程对这些数据所做的更新,后一个动作保证了该锁的持有线程对这些数据所做的更新对该锁的后续持有线程可见。那么,这两个动作是如何实现的呢?弄清楚这个问题有助于我们学习和掌握包括锁在内的所有Java线程同步机制 。

Java虚拟机底层实际上是借助内存屏障( Memory Barrier ,也称 Fence )来实现上述两个动作的。内存屏障是对一类仅针对内存读、写操作指令 ( Instruction ) 的跨处理器架构 ( 比如 x86 、ARM )的比较底层的抽象( 或者称呼 )。内存屏障是被插入到两个指令之间进行使用的,其作用是禁止编译器、处理器重排序从而保障有序性。它在指令序列 ( 如指令 1 ;指令2 ;指令3 )中就像是一堵墙 ( 因此被称为屏障 )一样使其两侧 ( 之前和之后 )的指令无法“穿越”它 ( 一旦穿越了就是重排序了 )。但是,为了实现禁止重排序的功能,这些指令也往往具有一个副作用刷新处理器缓存、冲刷处理器缓存,从而保证可见性。不同微架构的处理器所提供的这样的指令是不同的,并且出于不同的目的使用的相应指令也是不同的。例如对于 “写-写” ( 写后写 ) 操作,如果仅仅是为了防止 ( 禁止 ) 重排序而对可见性保障没有要求,那么在x86架构的处理器下使用空操作就可以了(  因为 x86处理器不会对 “写-写” 操作进行重排序 )。而如果对可见性有要求(比如前一个写操作的结果要在后一个写操作执行前对其他处理器可见),那么在x86    处理器下需要使用LOCK 前缀指令或者sfence 指令、mfence 指令;在 ARM 处理器下则需要使用 DMB 指令。

按照内存屏障所起的作用来划分,将内存屏障划分为以下几种。

按照可见性保障来划分。内存屏障可分为加载屏障(Load Barrier)和存储屏障(Store      Barrier)。加载屏障的作用是刷新处理器缓存,存储屏障的作用冲刷处理器缓存。Java虚拟机会在 MonitorExit ( 释放锁 ) 对应的机器码指令之后插入一个存储屏障,这就保障了写线程在释放锁之前在临界区中对共享变量所做的更新对读线程的执行处理器来说是可同步的。相应地,Java 虚拟机会在 MonitorEnter ( 申请锁 ) 对应的机器码指令之后临界区开始之前的地方插入一个加载屏障,这使得读线程的执行处理器能够将写线程对相应共享变量所做的更新从其他处理器同步到该处理器的高速缓存中。因此,可见性的保障是通过写线程和读线程成对地使用存储屏障和加载屏障实现的。

按照有序性保障来划分,内存屏障可以分为获取屏障(Acquire Barrier)和释放屏障 ( Release Barrier )。获 取 屏 障 的 使 用 方 式 是 在 一 个 读 操 作 ( 包括 Read-Modify-Write 以及普通的读操作 )之后插入该内存屏障,其作用是禁止该读操作与其后的任何读写操作之间进行重排序,这相当于在进行后续操作之前先要获得相应共享数据的所有权 ( 这也是该屏障的名称来源 )。释放屏障的使用方式是在一个写操作之前插入该内存屏障,其作用是禁止该写操作与其前面的任何读写操作之间进行重排序。这相当于在对相应共享数据操作结束后释放所有权( 这也是该屏障的名称来源 )。 Java虚拟机会在 MonitorEnter( 它包含了读操作 ) 对应的机器码指令之后临界区开始之前的地方插入一个获取屏障,并在临界区结束之后 MonitorExit ( 它包含了写操作 ) 对应的机器码指令之前的地方插入一个释放屏障。因此,这两种屏障就像是三明治的两层面包片把火腿夹住一样把临界区中的代码(指令序列)包括起来,如图所示。

08035ed1675c20b2bc3e4450f5a2f922.png

由于获取屏障禁止了临界区中的任何读、写操作被重排序到临界区之前的可能性。而释放屏障又禁止了临界区中的任何读、写操作被重排序到临界区之后的可能性。因此临界区内的任何读、写操作都无法被重排序到临界区之外。在锁的排他性的作用下,这使得临界区中执行的操作序列具有原子性。因此,写线程在临界区中对各个共享变量所做的更新会同时对读线程可见,即在卖线程看来各个共享变量就像是“一下子” 被更新的,于是这些线程无从 ( 也无必要 ) 区分这些共享变量是以何种顺序被更新的。这使得写线程在临界区中执行的操作自然而然地具有有序性读线程对这些操作的感知顺序与源代码顺序一致。

可见,锁对有序性的保障是通过写线程和读线程配对使用释放屏障与加载屏障实现的。

https://www.jianshu.com/p/39ecb11d41d7

-深入原理-  

   知其然并知其所以然    

04ac83bc3a28fa8fad17511ad57ff6e0.png

89377a1d0b8b4d83062172771a5404ed.png

37776b34c6f8639fd170dd0d8ac08ba1.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/434985.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python处理ncdc气象数据并利用arcgis可视化

作者已经处理好的数据如下 中国2020年均气温数据点加栅格.zip-讲义文档类资源-CSDN下载 数据格式如下 所有文件 对2020年文件进行查看(共有412个站点数据) 打开其中一个进行查看共有12列数据

动态分区分配的“首次适应算法_动态图划分复制算法:Leopard

数据管理和系统实现课程上要分享的论文:《LEOPARD: Lightweight Edge-Oriented Partitioning and Replication for Dynamic Graphs》背景目前分析处理图数据已经成为一项重要的任务,例如,研究互联网结构,分析社会关系,…

微软工程院院长:1万多应聘者挑不出100人

微软工程院院长:1万多应聘者挑不出100人“过去两三个月,我最主要的精力都花在了雇人上。遗憾的是,1万多名应聘者中,居然招不到足够合适的人。”今天,在北京中关村希格玛大厦微软亚洲工程院总部,新任院长张宏…

广义典型相关分析_重复测量数据分析及结果详解(之二)——广义估计方程

上一篇文章主要介绍了重复测量方差分析的基本思想是什么、它能做什么、怎么做、结果怎么解释,这几个问题。最后同时指出重复测量方差分析还是有一定局限,起码不够灵活。所以本文在上一篇文章基础上继续介绍医学重复测量数据中第二种常用方法:…

机器学习复制粘贴笔记要点

逻辑回归对特征的规模很敏感。重新调整数据使每个特征的均值为 0 和方差为 1 通常被认为是好的做法 zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。 如果各个迭代器的元素个数不一致&#x…

YUV422格式信号格式(以备学习之用)

YUV信号有很多种,一般YUV420和YUV422用的比较多, YUV422格式,又分为很多小类,按照U、V的排列可以有YUYV,YVYU,UYVY,VYUY四种,其中,YUYVY一般又称作yuv2格式。 而这四种YUV422格式,每种又可以分为…

excel怎么添加diy工具箱_这些Excel插件让你的Excel更好用!

Excel基本功不扎实临时学习也没时间这时候比起各种操作技巧Excel插件更实际更能切实提高你的效率今天就给大家推荐几款插件!方方格子说起Excel插件,就不能不提到它。方方格子支持Excel2007到2016各版本,而且对于WPS版本,也有专门的…

Arcgis自下而上从左到右进行编号

添加字段xmin和ymax !shape.extent.Xmin! !shape.extent.Ymax! 计算结果 对处理好的矢量按这个两个字段进行排序 排序结果后的objectid即可做为斜坡单元编号(如果项目中对编号有其他要求,请继续往下查看,如果没有的话,就不用往下…

Linux之V4L2基础编程

1. 定义 V4L2(Video For Linux Two) 是内核提供给应用程序访问音、视频驱动的统一接口。 2. 工作流程: 打开设备-> 检查和设置设备属性-> 设置帧格式-> 设置一种输入输出方法(缓冲 区管理)&…

ArcGIS如何在一个矢量上用不同颜色进行标注

在图层属性--标注里 选择“定义要素类并且为每个类加不同的标注” 点击添加 在类里面就会选择红色,此处作者添加了红色和黑色两个类 注意选择类是,查看是否标注次类 点击SQL查询,不同类进行不同的SQL查询

基于web的工作流设计器(多比图形控件)

多比图形控件是一款基于Web的矢量图形控件, 类似于网页上的Visio控件,是目前国内外最佳的基于web的工作流设计器、工作流流程监视器解决方案。 可广泛应用于包括:电力、军工、煤炭、化工、科研、能源等各种监控软件、基于web的工作流设计器&a…

earthdata数据的.nc4如何使用

原始数据 打开ArcGIS软件 参数如下,只需改变变量参数,选择自己所需变量,其他默认参数,点击确定 更具个人电脑性能,本人电脑反应比较慢

EXT.NET复杂布局(二)——报表

前面提到过工作台(《EXT.NET复杂布局(一)——工作台》)了,不知道各位看过之后有什么感想。这次就介绍介绍使用EXT.NET画几个报表。 看图写作从小学就开始了,如图: 图一 图二 图三(1&…

arcgis导出access数据库能打开的文件

arcgis有两种数据库 1.个人地理数据库(.mdb)(Access数据库可以打开查看属性) 2.文件地理数据库(.gdb) 其中个人地理数据库 (.mdb)可以用Access数据库打开 在文件中显示如下 打开文件如下 文件地理数据库如下 个人地理数据库(地质灾害建立数据库就要用这个数据库…

如何使用网上下载的arcgis工具箱,报错汇总

执行网格表达式错误 解决方法 更改环境变量里的并行处理设置成0即可

基于 Android NDK 的学习之旅-----环境搭建

工欲善其事 必先利其器 , 下面介绍下 Eclipse SDK NDK Cygwin CDT 集成开发环境的搭建。 1、Android 开发环境搭建 Android开发环境搭建不是重点,相信看此文章的很多人都已经搭建成功,这里随便概述性的说说。 1) 下载 JDK 2) 下载 Eclipse 3) 下载 Android…

arcgis将小于0的数值设置成0.01

原始范围 打开栅格计算器 主要利用的是栅格计算器的con条件函数 con用法 con(条件,满足条件的部分赋值赋值,不满足条件的部分赋值) 运行完的范围 0.0008<0.01所以显示0.0008

vivo怎么调时间_卡西欧手表怎么调时间 怎么评估卡西欧手表的价格档次

在以前的手表是戴在手上方便看时间的&#xff0c;但是随着科技的发展&#xff0c;手表也越来越智能&#xff0c;很多的手表都是多功能的&#xff0c;但是我们的知道&#xff0c;一样东西越好用就会显得它的复杂性越高&#xff0c;慢慢着就会使很多人都不会使用&#xff0c;就拿…