深入分析synchronized的实现原理_java中synchronized实现原理-程序员宅基地

技术标签: Android学习之java进阶  

记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字“同步”,也成为了我们解决多线程情况的百试不爽的良药。但是,随着我们学习的进行我们知道synchronized是一个重量级锁,相对于Lock,它会显得那么笨重,以至于我们认为它不是那么的高效而慢慢摒弃它。 
诚然,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。下面跟随LZ一起来探索synchronized的实现机制、Java是如何对它进行了优化、锁优化机制、锁的存储结构和升级过程;

实现原理

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: 
1. 普通同步方法,锁是当前实例对象 
2. 静态同步方法,锁是当前类的class对象 
3. 同步方法块,锁是括号里面的对象

当一个线程访问同步代码块时,它首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁,那么它是如何来实现这个机制的呢?我们先看一段简单的代码:

public class SynchronizedTest {
    
    public synchronized void test1(){

    }

    public void test2(){
        synchronized (this){

        }
    }
}
 
 
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

利用javap工具查看生成的class文件信息来分析Synchronize的实现 
这里写图片描述 
从上面可以看出,同步代码块是使用monitorenter和monitorexit指令实现的,同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。 
同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁; 
同步方法:synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。(摘自:http://www.cnblogs.com/javaminer/p/3889023.html)

下面我们来继续分析,但是在深入之前我们需要了解两个重要的概念:Java对象头,Monitor。

Java对象头、monitor

Java对象头和monitor是实现synchronized的基础!下面就这两个概念来做详细介绍。

Java对象头

synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,所以下面将重点阐述

Mark Word。 
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。下图是Java对象头的存储结构(32位虚拟机): 
这里写图片描述 
对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机): 
这里写图片描述

简单介绍了Java对象头,我们下面再看Monitor。

Monitor

什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。 
与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。 
Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下: 
这里写图片描述 
Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL; 
EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。 
RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。 
Nest:用来实现重入锁的计数。 
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。 
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。 
摘自:Java中synchronized的实现原理与应用) 
我们知道synchronized是重量级锁,效率不怎么滴,同时这个观念也一直存在我们脑海里,不过在jdk 1.6中对synchronize的实现进行了各种优化,使得它显得不是那么重了,那么JVM采用了那些优化手段呢?

锁优化

jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

自旋锁

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。 
何谓自旋锁? 
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。 
自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。 
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整; 
如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

适应自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。 
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

锁消除

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。 
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:

    public void vectorTest(){
        Vector<String> vector = new Vector<String>();
        for(int i = 0 ; i < 10 ; i++){
            vector.add(i + "");
        }

        System.out.println(vector);
    }
 
 
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。

锁粗化

我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。 
在大多数的情况下,上述观点是正确的,LZ也一直坚持着这个观点。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。 
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。

轻量级锁

引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下: 
获取锁 
1. 判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3); 
2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3); 
3. 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;

释放锁 
轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下: 
1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据; 
2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行(3); 
3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢;


下图是轻量级锁的获取和释放过程 
这里写图片描述

偏向锁

引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。上面提到了轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的。那么偏向锁是如何来减少不必要的CAS操作呢?我们可以查看Mark work的结构就明白了。只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下: 
获取锁 
1. 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01; 
1. 若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3); 
1. 如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4); 
4. 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块; 
5. 执行同步代码块

释放锁 
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下: 
1. 暂停拥有偏向锁的线程,判断锁对象石是否还处于被锁定状态; 
2. 撤销偏向苏,恢复到无锁状态(01)或者轻量级锁的状态;


下图是偏向锁的获取和释放流程 
这里写图片描述

重量级锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

参考资料

  1. 周志明:《深入理解Java虚拟机》
  2. 方腾飞:《Java并发编程的艺术》
  3. Java中synchronized的实现原理与应用)
本文转载自:http://blog.csdn.net/chenssy/article/details/54883355 。感谢楼主!
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/shandian000/article/details/54927876

智能推荐

springboot自动配置与application.yml联系_spring中为啥yml文件里能联想-程序员宅基地

文章浏览阅读356次。我们要知道springboot启动的时候会进行自动配置,找到META-INF/spring.factories,并进行加载到ioc容器中!!!进入到该文件中,随便找个类!点进去!!!看见@EnableConfigurationProperties({ServerProperties.class})这个注解点进ServerProperties.class这表示的意思就是类与application.yml文件进行绑定其中的prefix的值代表yml文件中的这个server后面能够点出来的各_spring中为啥yml文件里能联想

GEE11:2个土地覆盖数据(LUCC)分享和下载_lucc数据-程序员宅基地

文章浏览阅读6.3k次,点赞11次,收藏55次。地表覆盖分布是气候变化研究、生态环境评估及地理国情监测等不可或缺的重要基础信息。近年来,随着遥感科学技术以及计算机存储和计算能力的不断提升,地表覆盖应用需求也逐步从公里尺度(如 1 公里,500 米)向更高分辨率的米级尺度(30 米和 10 米)过渡。目前,已公开的三套 30 米全球地表覆盖产品(GLC_FCS30,FROM_GLC 和 GlobeLand30)能够很好地服务了全球/区域尺度的应用需求。_lucc数据

脂肪酸摄取试剂盒应用:快速和敏感,简单方便-程序员宅基地

文章浏览阅读40次。长链脂肪酸(LCFA)作为β-氧化的底物是动物重要的燃料来源,是许多不同细胞结构的基石。Assay Genie的基于细胞的脂肪酸摄取检测试剂盒(cat#BA0184)利用荧光脂肪酸类似物,该类似物被脂肪酸转运蛋白摄取并在细胞内积累。细胞摄取脂肪酸类似物,通过检测λex/em = 488/523nm处荧光信号的增加评估细胞中的脂肪酸摄取活性,并进行激活剂和抑制剂的筛选。图. 3T3-L1细胞脂肪酸摄取测定:将3T3-L1细胞以每孔60,000个细胞的密度接种,并进行饥饿处理。① 全细胞长链脂肪酸摄取的测定。

Python计算机视觉-第9章_img_as_uint()-程序员宅基地

文章浏览阅读622次。图像分割是将一幅图像分割成有意义区域的过程。区域可以是图像的前景与背景或 图像中一些单独的对象。这些区域可以利用一些诸如颜色、边界或近邻相似性等特 征进行构建。本章中,我们将看到一些不同的分割技术。1、图割(1)从图像创建图割(2)用户交互式分割2、利用聚类进行分割3、变分法当优化的对象是函数时,该问题称为变分问题,解决这类问题的算法称为变分法。 我们看一个简单而有效的变分模型。Chan-Vese 分割模型由分片常数图像 U=χ1c1+χ2c2,我们可以将上式重写为:_img_as_uint()

pixel 3 android 12下root简易过程_pixel3安卓12root-程序员宅基地

文章浏览阅读1k次。介绍Android 12 系统下的pixel 3进行root的过程。提示:以下是本篇文章正文内容,下面案例可供参考。_pixel3安卓12root

python将字典数据保存为 excle或csv文件-程序员宅基地

文章浏览阅读1.6w次,点赞14次,收藏45次。参考:python对CSV、Excel、txt、dat、mat文件的处理dict_data = [{'Address': 'CKYsrEiJBrUGXfKYGrr4PkCLbRQn5TiXCm', 'miner_balance': 197561652, 'miner_recieved': 197561652, 'miner_sent': 0, 'top': 98, 'turn..._pf.to_csv

随便推点

TFLearn 在给定模型精度时候提前终止训练-程序员宅基地

文章浏览阅读692次。拿来主义:看我的代码,我是在模型acc和验证数据集val_acc都达到99.8%时候才终止训练。import numpy as npimport tflearnfrom tflearn.layers.core import dropoutfrom tflearn.layers.normalization import batch_normalizationfrom..._tflearn中断训练

1009 FatMouse’ Trade 简单贪心算法_fatmouse' trade-程序员宅基地

文章浏览阅读112次。思路:简单的贪心问题,先算出性价比num[i].cost=(num[i].food*1.0)/num[i].javabean;老鼠用M猫粮换仓库里的鼠粮,仓库有n个房间,每个房间有num[i].javabean鼠粮,需要num[i].food猫粮换,if(m>=num[cnt].food){ //加上等于,则有免费时候回继续提取。//直到所有房间遍历完毕。//这个不理,是用来方便输入输出的东西,利用文本输入流来读取数据。0.000//虽然有钱,但是没房间,你要设置退出机制。=0)//没房间时候退出机制。_fatmouse' trade

HyperLynx(八)带状线串扰的仿真_hyperlynx 窜扰-程序员宅基地

文章浏览阅读5k次,点赞2次,收藏12次。TL8 和 TL11为底层的传输线,传输线到参考层的距离为5m il,线宽为8.25mil,传输线的阻抗都为单端 50Ω。在电阻R3上测得的近端串扰值分别为:62.31mV,61.37mV,58.37mV,54.05mV,48.24mV,从测得的数据和仿真波形来看,说明相邻布线层相互耦合的距离越近,串扰越大。在高速、高密度的 PCB 设计中普遍存在串扰,而串扰对系统的影响一般都是负面的但串扰无法被彻底消除,那么就需要找到相应的优化或减小串扰的措施,在不影响系统设计成本和功能的情况下,使串扰尽量变小。..._hyperlynx 窜扰

Spring Boot 日志文件_springboot打印日志-程序员宅基地

文章浏览阅读7.5k次。我们日常在写代码的时候,出现了异常,都会在编译器的控制台上,找出问题。这些问题并不是我们盲猜的,而是控制台上有明显的日志打印,我们就可以根据日志打印发现问题、解决问题。所以说,日志是调试程序重要的一环。_springboot打印日志

【python】判断字符串是否包含子串,居然有七种方法?_python字符串包含子串-程序员宅基地

文章浏览阅读1.1k次。1、使用 in 和 not inin和not in在 Python 中是很常用的关键字,我们将它们归类为成员运算符。_python字符串包含子串

使用Python识别滑块验证码中的缺口位置_python 滑块验证码 没有缺口的位置-程序员宅基地

文章浏览阅读79次。首先使用imread函数读取背景图片和缺口图片,然后使用Canny函数识别出图片的边缘。最后,选出其中“概率最高”的点,即为缺口匹配的位置。缺口的X轴坐标就是匹配结果中“概率最高”的点的X坐标。而识别滑块验证码中的缺口位置是破解滑块验证码的一种常见方式。min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) # 寻找最优匹配。cv2.rectangle(bg_img, tl, br, (0, 0, 255), 2) # 绘制矩形。_python 滑块验证码 没有缺口的位置

推荐文章

热门文章

相关标签