synchronized 的锁类型

news2024/11/23 20:12:11

        之前的文章有讲过对同步锁的理解,实现同步锁的方式无非是多个线程抢占一个互斥变量,如果抢占成功则表示获得了锁,而没有获得锁的线程则阻塞等待,直到获得锁的线程释放锁

        如图所示,在Mark Word中,我们发现锁的类型有偏向锁、轻量级锁、重量级锁,那么
    
        其实,在JDK 1.6之前,synchronized只提供了重量级锁的机制,重量级锁的本质就是我们前面对于锁的认知,也就是没有获得锁的线程会通过park方法阻塞,接着被获得锁的线程唤醒后再次抢占锁,直到抢占成功。
        重量级锁依赖于底层操作系统的Mutex Lock来实现,而使用Mutex Lock需要把当前线程挂起,并从用户态切换到内核态来执行,这种切换带来的性能开销是非常大的。因此,如何在性能和线程安全性之间做好平衡,就是一个值得探讨的话题了。
        在JDK1.6之后,synchronized做了很多优化,其中针对锁的类型增加了偏向锁和轻量级锁,这两种锁的核心设计理念就是如何让线程在不阻塞的情况下达到线程安全的目的。

偏向锁的原理分析


        偏向锁其实可以认为是在没有多线程竞争的情况下访问synchronized修饰的代码块的加锁场景,也就是在单线程执行的情况下。
        很多读者可能会有疑问,没有线程竞争,那为什么要加锁呢?实际上对程序开发来说,加锁是为了防范线程安全性的风险,但是是否有线程竞争并不由我们来控制,而是由应用场景来决定。假设这种情况存在,就没有必要使用重量级锁基于操作系统级别的Mutex Lock来实现锁的抢占,这样显然很耗费性能。
        所以偏向锁的作用就是,线程在没有线程竞争的情况下去访问synchronized 同步代码块时。会尝试先通过偏向锁来抢占访问资格,这个抢占过程是基于CAS来完成的,如果抢占锁成功,则直接修改对象头中的锁标记。其中,偏向锁标记为1,锁标记为01,以及存储当前获得锁的线程ID。而偏向的意是就是,如果线程X获得了偏向锁,那么当线程x后续再访问这个同步方法时,只需要判断对象头中的线程ID和线程X是否相等即可。如果相等,就不需要再次去抢占锁,直接获得访问资格即可,其实现原理如图所示,


        结合前面关于对象头部分的说明及偏向锁的原理,我们通过一个例子来看一下偏向锁的实现。

public class BiasedLockExample {
    public static void main(String[] args) {
        BiasedLockExample example=new BiasedLockExample(); 
        System.out.println("加锁之前");
        System.out.println(ClassLayout.parseInstance(example).toPrintable()); 
        synchronized (example){
            System.out.println("加锁之后");
            System,out.print1n(ClassLayout .parseInstance(example).toPrintable());
        }
    }
}

        在上述代码中,BiasedLockExample演示了针对example这个锁对象,在加锁之前和加锁之后分别打印对象的内存布局的过程,来看一下输出结果。

        从上述输出结果中我们发现:

  • 在加锁之前,对象头中的第一个字节00000001最后三位为[001],其中低位的两位表示锁标记,它的值是[01],表示当前为无锁状态。
  • 在加锁之后,对象头中的第一个字节01111000最后三位为[000],其中低位的两位是[00],对照前面介绍的MarkWord中的存储结构的含义,它表示轻量级锁状态。

        当前的程序并不存在锁竞争,基于前面的理论分析,此处应该是获得偏向锁,但是为什么变成了轻量级锁呢?
        原因是,JVM在启动的时候,有一个启动参数-XX:BiasedLockingStartupDelay,这个参数表示偏向锁延迟开启的时间,默认是4秒,也就是说在我们运行上述程序时,偏向锁还未开启,导致最终只能获得轻量级锁。之所以延迟启动,是因为JVM在启动的时候会有很多线程运行,也就是说会存在线程竞争的场景,那么这时候开启偏向锁的意义不大。


        如果我们需要看到偏向锁的实现效果,那么有两种方法:

  • 添加JVM启动参数-XX:BiasedLockingStartupDelay=0,把延迟启动时间设置为0。
  • 抢占锁资源之前,先通过Thread.sleep)方法睡眠4秒以上。

        最终得到如下输出结果。


        从上面输出结果我们发现,加锁之后,第一个字节低位部分的3位变成了[101],高位[1]表示当前是偏向锁状态,低位[01]表示当前是偏向锁状态,这显然达到了我们的预期效果。细心的读者会发现,加锁之前的锁标记位也是[101] -------- 这里并没有加偏向锁呀?
        我们来分析一下,加锁之前并没有存储线程ID,加锁之后才有一个线程ID(45889541)。因此,在获得偏向锁之前,这个标记表示当前是可偏向状态,并不代表已经处于偏向状态。

05 00 00 00  (00000101 000000000000000000000000)        (5)
05 38 bc 02  (00000101 001110001011110000000010)        (45889541)

轻量级锁的原理分析

        在线程没有竞争时,使用偏向锁能够在不影响性能的前提下获得锁资源,但是同一时刻只允许一个线程获得锁资源,如果突然有多个线程来访问同步方法,那么没有抢占到锁资源的线程要怎么办呢?很显然偏向锁解决不了这个问题。
        正常情况下,没有抢占到锁的线程肯定要阻塞等待被唤醒,也就是说按照重量级锁的逻辑来实现,但是在此之前,有没有更好的平衡方案呢?于是就有了轻量级锁的设计。
        所谓的轻量级锁,就是没有抢占到锁的线程,进行一定次数的重试(自旋)。比如线程第一次没抢到锁则重试几次,如果在重试的过程中抢占到了锁,那么这个线程就不需要阻塞,这种实现方式我们称为自旋锁,具体的实现流程如图所示。


        当然,线程通过重试来抢占锁的方式是有代价的,因为线程如果不断自旋重试,那么CPU会一直处于运行状态。如果持有锁的线程占有锁的时间比较短,那么自旋等待的实现带来性能的提升会比较明显。反之,如果持有锁的线程占用锁资源的时间比较长,那么自旋的线程就会浪费CPU资源,所以线程重试抢占锁的次数必须要有一个限制。
        在JDK1.6中默认的自旋次数是10次,我们可以通过-XX:PreBlockSpin参数来调整自旋次数。同时开发者在JDK1.6中还对自旋锁做了优化,引入了自适应自旋锁,自适应自旋锁的自旋次数不是固定的,而是根据前一次在同一个锁上的自旋次数及锁持有者的状态来决定的。如果在同一个锁对象上,通过自旋等待成功获得过锁,并且持有锁的线程正在运行中,那么JVM会认为此次自旋也有很大的机会获得锁,因此会将这个线程的自旋时间相对延长。反之,如果在一个锁对象中,通过自旋锁获得锁很少成功,那么JVM会缩短自旋次数。
轻量级锁的演示在上面中有,默认不修改偏向锁的延期开启参数,加锁得到的锁状态就是轻量级锁。

重量级锁的原理分析

        轻量级锁能够通过一定次数的重试让没有获得锁的线程有可能抢占到锁资源,但是轻量级锁
得销节省积巷有销的时间较短的情况下才能起到提升同步锁性能的效果。如果持有锁的线程占用锁资源的时间较长,那么不能让那些没有抢占到锁资源的线程不断自旋,否则会占用过多的CPU资源,这反而是一件得不偿失的事情。
        如果没抢占到锁资源的线程通过一定次数的自旋后,发现仍然没有获得锁,就只能阻塞等待了,所以最终会升级到重量级锁,通过系统层面的互斥量来抢占锁资源。重量级锁的实现原理如图 所示。


        整体来看,我们发现,如果在偏向锁、轻量级锁这些类型中无法让线程获得锁资源,那么这些没获得锁的线程最终的结果仍然是阻塞等待,直到获得锁的线程释放锁之后才能被唤醒。而在整个优化过程中,我们通过乐观锁的机制来保证线程的安全性。
        下面这个例子演示了在加锁之前、单个线程抢占锁、多个线程抢占锁的场景中,对象头中的锁的状态变化。

public class HeavyLockExample {

    public static void main(String[] args) throws InterruptedException {
        HeavyLockExample heavy = newHeavyLockExample(); 
        System,out.println("加锁之前");
        System.out.println(ClassLayout.parseInstance(heavy),toPrintable()); 
        Thread t1=new Thread(()->{ 
            synchronized (heavy){
                try{
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printstackTrace( );
                }
            }
         });
        t1.start();
        //确保t1 线程已经运行
        TimeUnit.MILLISECONDS.sleep(500); 
        system.out.println("t1 线程抢占了锁");
        System.out,println(ClassLayout.parseInstance(heavy).toPrintable()); 
        synchronized (heavy){
            system.out.println("main 线程来抢占锁");
            system. out.println(ClassLayout.parseInstance(heavy).toprintable());
        }
    }
}

上述程序打印的结果如下:

        从上述打印结果来看,对象头中的锁状态一共经历了三个类型。

  • 加锁之前,对象头中的第一个字节是00000001,表示无锁状态。
  • 当t1线程去抢占同步锁时,对象头中的第一个字节变成了11011000,表示轻量级锁状态。
  • 接着 main 线程来抢占同一个对象锁,由于t1线程睡眠了2秒,此时锁还没有被释放,main线程无法通过轻量级锁自旋获得锁,因此它的锁的类型是重量级锁,锁标记为10。

        注意,在这个案例演示中,开发者并没有开启偏向锁的参数,如果开启了,那么第一个加锁之后得到的锁状态应该是偏向锁,然后直接到重量级锁(因为tI线程有一个sleep,所以轻量级锁肯
定无法获得)。

        由此可以看到,synchronized同步锁最终的底层加锁机制是JVM层面根据线程的竞争情况逐步升级来实现的,从而达到同步锁性能和安全性平衡的目的,而这个过程并不需要开发者干预。

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

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

相关文章

软考系列(系统架构师)- 2009年系统架构师软考案例分析考点

试题一 软件架构设计 【问题1】(9分) 软件质量属性是影响软件架构设计的重要因素。请用200字以内的文字列举六种不同的软件质量属性名称并解释其含义。 常见的软件质量属性有多种,例如性能(Performance)、可用性(Ava…

037-第三代软件开发-系统音量设置

第三代软件开发-系统音量设置 文章目录 第三代软件开发-系统音量设置项目介绍系统音量设置QML 实现C 实现 总结一下 关键字: Qt、 Qml、 volume、 声音、 GPT 项目介绍 欢迎来到我们的 QML & C 项目!这个项目结合了 QML(Qt Meta-Obj…

【C++】类与对象 第二篇(构造函数,析构函数,拷贝构造,赋值重载)

目录 类的6个默认成员函数 初始化和清理 1.构造函数 2.析构函数 3.共同点 拷贝复制 1.拷贝构造 使用细节 2.赋值重载 运算符重载 < < > > ! 连续赋值 C入门 第一篇(C关键字&#xff0c; 命名空间&#xff0c;C输入&输出)-CSDN博客 C入门 第二篇( 引…

036-第三代软件开发-系统时间设置

第三代软件开发-系统时间设置 文章目录 第三代软件开发-系统时间设置项目介绍系统时间设置演示效果QML 实现小伙伴自创 TumblerQt 家 Tumbler C 端实现 总结一下 关键字&#xff1a; Qt、 Qml、 Time、 时间、 系统 项目介绍 欢迎来到我们的 QML & C 项目&#xff01;…

用Python实现感知机学习算法及其对偶算法实验报告

实验目的 1.理解感知机学习算法的基本思想&#xff1a;感知机是一种简单的线性分类模型&#xff0c;其基本思想是通过不断调整权重&#xff0c;使得分类超平面能够将不同类别的样本正确分开。 2.掌握感知机学习算法的实现方法&#xff1a;感知机学习算法基于随机梯度下降法&am…

【斗罗二】霍雨浩迷惑审查,戴华斌故意挑衅,惨败者屈服下跪

【侵权联系删除】【文/郑尔巴金】 深度爆料&#xff0c;自《绝世唐门》宣布问世以来&#xff0c;其在国漫圈引发的关注和热议便如火如荼。作为《斗罗大陆》的续作&#xff0c;这部作品无疑继承了前作的荣光&#xff0c;甚至被无数粉丝期待着能再创辉煌。在各大社交媒体和国漫论…

前端移动web高级详细解析二

移动 Web 第二天 01-空间转换 空间转换简介 空间&#xff1a;是从坐标轴角度定义的 X 、Y 和 Z 三条坐标轴构成了一个立体空间&#xff0c;Z 轴位置与视线方向相同。 空间转换也叫 3D转换 属性&#xff1a;transform 平移 transform: translate3d(x, y, z); transform…

搞定蓝牙——第五篇(SMP)

搞定蓝牙——第五篇&#xff08;SMP&#xff09; 原理Security Manager(简称SM&#xff0c;不要想歪)秘钥配对秘钥生成特定秘钥分发原理总结 原理 Security Manager(简称SM&#xff0c;不要想歪) 按照前面的试验&#xff0c;两个设备可以通过ble通讯了&#xff0c;但是&#…

(a /b)*c的值

系列文章目录 进阶的卡莎C++_睡觉觉觉得的博客-CSDN博客数1的个数_睡觉觉觉得的博客-CSDN博客双精度浮点数的输入输出_睡觉觉觉得的博客-CSDN博客足球联赛积分_睡觉觉觉得的博客-CSDN博客大减价(一级)_睡觉觉觉得的博客-CSDN博客小写字母的判断_睡觉觉觉得的博客-CSDN博客纸币(…

ETL工具Kettle

1 Kettle的基本概念 一个数据抽取过程&#xff0c;主要包括创建一个作业&#xff08;Job&#xff09;&#xff0c;每个作业由一个或多个作业项&#xff08;Job Entry&#xff09;和连接作业项的作业跳&#xff08;Job Hop&#xff09;组成。每个作业项可以是一个转换&#xff…

Cookie技术

Cookie中文名称为小型文本文件&#xff0c;指某些网站为了辨别用户身份、进行会话跟踪而储存在用户本地终端上的数据。 Cookie是由服务器端生成&#xff0c;发送给User-Agent&#xff08;—般是浏览器&#xff09;&#xff0c;浏览器会将Cookie的key/value保存到某个目录下的文…

wiresharak捕获DNS

DNS解析&#xff1a; 过滤项输入dns&#xff1a; dns查询报文 应答报文&#xff1a; 事务id相同&#xff0c;flag里 QR字段1&#xff0c;表示响应&#xff0c;answers rrs变成了2. 并且响应报文多了Answers 再具体一点&#xff0c;得到解析出的ip地址&#xff08;最底下的add…

【Linux】虚拟机安装Linux、客户端工具,MobaXterm的使用,Linux常用命令

目录 一&#xff0c;安装Linux的centos7版本 具体安装步骤&#xff1a; 二&#xff0c;Linux常见的命令&#xff1a; 三、安装客户端工具 1、介绍 2、安装MobaXterm 3、换源 四、拍照功能 一&#xff0c;安装Linux的centos7版本 介绍&#xff1a; 具体安装步骤&#…

PTA L1-8 静静的推荐

PTA L1-8 静静的推荐 分数 20 全屏浏览题目 切换布局 作者 陈越 单位 浙江大学 天梯赛结束后&#xff0c;某企业的人力资源部希望组委会能推荐一批优秀的学生&#xff0c;这个整理推荐名单的任务就由静静姐负责。企业接受推荐的流程是这样的&#xff1a; 只考虑得分不低于 175 …

软考系列(系统架构师)- 2012年系统架构师软考案例分析考点

试题一 软件架构&#xff08;架构风格对比、架构风格选取、架构设计过程&#xff09; 【问题1】&#xff08;12分&#xff09; 请用200字以内的文字解释什么是软件架构风格&#xff0c;并从集成开发环境与用户的交互方式、集成开发环境的扩展性、集成开发环境的数据管理三个方…

Anaconda下载和安装

1.概述 1&#xff09;包含conda&#xff1a;conda是一个环境管理器&#xff0c;其功能依靠conda包来实现&#xff0c;该环境管理器与pip类似。 2&#xff09;安装大量工具包&#xff1a;Anaconda会自动安装一个基本的python&#xff0c;该python的版本Anaconda的版本有关。该…

进程(详解)

进程 进程PCB进程的定义进程的组成进程模式进程的状态进程的运行进程的创建进程的结束孤儿进程僵尸进程僵尸进程的危害 进程的创建pidforkwait案例 进程 PCB 从操作系统理解进程概念-------先描述&#xff0c;后组织 为了使参与并发执行的程序能独立的运行&#xff0c;必须为之…

经常遇到的问题

一个前端经常会遇到的问题 例如&#xff0c;我想要在一个项目里&#xff0c;监听所有的fetch请求&#xff0c;应该怎么办&#xff1f;又或者说&#xff0c;我想用别人封装好的方法&#xff0c;但是在它之前&#xff0c;需要经过一层处理、判断&#xff0c;然后再看情况是否调用…

如何隐藏woocommerce 后台header,woocommerce-layout__header

如何隐藏woocommerce 后台header&#xff0c;woocommerce-layout__header WooCommerce |Products Store Activity| Inbox| Orders| Stock| Reviews| Notices| breadcrumbs 在 functions.php 里添加如下代码即可&#xff1a; // Disable WooCommerce Header in WordPress Admi…

开启CETOS 裸奔了一年的服务器开启firewall防火墙

记录一下关于firewall&#xff0c;博主非运维专家或服务器专家。 背景 客户有一台裸奔运行了一年多的系统有公网但发现没有开防火墙&#xff0c;iptables和firewall均是关闭状态&#xff0c;通过扫描发现很多漏洞。根据客户要求对端口进行重新梳理且关闭不必要或有潜在风险的…