史上最详细JUC教程之Synchronized与锁升级详解

news2024/12/28 5:38:07

在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成(用户态和内核态之间的切换),这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的synchronized效率低的原因
Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了锁升级

用户态和内核态之间的切换

1 为什么每个Java对象都可以上锁

1.1 C++底层源码

Java的基类Object是由C++底层源码实现的,每一个Object对象都有一个ObjectMonitor实例
Monitor可以理解为一种同步工具,也可理解为一种同步机制,常常被描述为一个Java对象。Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁

image-20230209172341576

Monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。

1.2 Monitor(监视器锁)

Monitor是在jvm底层实现的,底层代码是c++。本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,状态转换需要耗费很多的处理器时间成本非常高。所以synchronized是Java语言中的一个重量级操作。

1.3 Monitor与java对象以及线程是如何关联 ?

  • 如果一个java对象被某个线程锁住,则该java对象的Mark Word字段中LockWord指向monitor的起始地址
  • Monitor的Owner字段会存放拥有相关联对象锁的线程id

1.4 java6以后开始优化Synchronized

我们先来回顾一下锁升级对 synchronized 性能的影响,所谓的锁升级是指 synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫锁升级也叫锁升级。

image-20230209173506791

JDK 1.6 之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁时都会从用户态转换成内核态,而转换的效率是比较低的。但有了锁升级机制之后,synchronized 的状态就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核态的转换了,这样就大幅的提升了 synchronized 的性能。

2. synchronized锁种类及升级步骤

synchronized用的锁是存在Java对象头里的Mark Word中.锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位

64位标记图再看

2.1 无锁

image-20230209173037153

image-20230209173124381

锁消除

锁消除是在JIT编译器级别的事情。

在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。

也许你会觉得奇怪,既然有些对象不可能被多线程访问,那为什么要加锁呢?写代码时直接不加锁不就好了。

但是有时,这些锁并不是程序员所写的,有的是JDK实现中就有锁的,比如Vector和StringBuffer这样的类,它们中的很多方法都是有锁的。当我们在一些不会有线程安全的情况下使用这些类的方法时,达到某些条件时,编译器会将锁消除来提高性能

public class TestLockEliminate {
    public static String getString(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }
 
    public static void main(String[] args) {
        long tsStart = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            getString("TestLockEliminate ", "Suffix");
        }
        System.out.println("一共耗费:" + (System.currentTimeMillis() - tsStart) + " ms");
    }

上述代码中的StringBuffer.append是一个同步操作,但是StringBuffer却是一个局部变量,并且方法也并没有把StringBuffer返回,所以不可能会有多线程去访问它。那么此时StringBuffer中的同步操作就是没有意义的。

开启锁消除是在JVM参数上设置的,当然需要在server模式下:

并且要开启逃逸分析。 逃逸分析的作用呢,就是看看变量是否有可能逃出作用域的范围

当JVM参数为:

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

DEMO 2:

/**
 * 锁消除
 * 从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用,
 * 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
 */
public class LockClearUPDemo
{
    static Object objectLock = new Object();//正常的

    public void m1()
    {
        //锁消除,JIT会无视它,synchronized(对象锁)不存在了。不正常的
        Object o = new Object();

        synchronized (o)
        {
            System.out.println("-----hello LockClearUPDemo"+"\t"+o.hashCode()+"\t"+objectLock.hashCode());
        }
    }

    public static void main(String[] args)
    {
        LockClearUPDemo demo = new LockClearUPDemo();

        for (int i = 1; i <=10; i++) {
            new Thread(() -> {
                demo.m1();
            },String.valueOf(i)).start();
        }
    }
}

锁粗化

锁粗化是指,将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁

我只听说锁“细化”可以提高程序的执行效率,也就是将锁的范围尽可能缩小,这样在锁竞争时,等待获取锁的线程才能更早的获取锁,从而提高程序的运行效率,但锁粗化是如何提高性能的呢?

没错,锁细化的观点在大多数情况下都是成立了,但是一系列连续加锁和解锁的操作,也会导致不必要的性能开销,从而影响程序的执行效率,比如这段代码:

public String method() {
    StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            // 伪代码:加锁操作
            sb.append( + i);
            // 伪代码:解锁操作
        }
    return sb.toString();
}

这里我们不考虑编译器优化的情况,如果在 for 循环中定义锁,那么锁的范围很小,但每次 for 循环都需要进行加锁和释放锁的操作,性能是很低的;但如果我们直接在 for 循环的外层加一把锁,那么对于同一个对象操作这段代码的性能就会提高很多,如下伪代码所示:

public String method() {
    StringBuilder sb = new StringBuilder();
    // 伪代码:加锁操作
    for (int i = 0; i < 10; i++) {
    	sb.append( + i);
    }
    // 伪代码:解锁操作
    return sb.toString();
}

锁粗化的作用:如果检测到同一个对象执行了连续的加锁和解锁的操作,则会将这一系列操作合并成一个更大的锁,从而提升程序的执行效率

DEMO 2:

/**
 * 锁粗化
 * 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,
 * 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
 */
public class LockBigDemo
{
    static Object objectLock = new Object();


    public static void main(String[] args)
    {
        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println("11111");
            }
            synchronized (objectLock) {
                System.out.println("22222");
            }
            synchronized (objectLock) {
                System.out.println("33333");
            }
        },"a").start();

        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println("44444");
            }
            synchronized (objectLock) {
                System.out.println("55555");
            }
            synchronized (objectLock) {
                System.out.println("66666");
            }
        },"b").start();

    }
}

2.2 偏向锁

当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁。会修改偏向锁位置为1,并且前面的54位放入当前线程的指针JavaThread*,标识目前的偏向锁被哪一个线程所持有。

理论落地:

在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程

那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。

如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

技术实现:

一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还
会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的Mark Word 中去判断一下是否有偏向锁指向本身的ID,无需再进入 Monitor 去竞争对象了。

image-20230210094215432

细化案例Account对象举例说明

偏向锁的操作不用直接捅到操作系统,不涉及用户到内核转换,不必要直接升级为最高级,我们以一个account对象的“对象头”为例,

image-20230210094600444

假如有一个线程执行到synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到Mark Word当中,并修改标偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。

image-20230210094746170

这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程ID也在对象头里),JVM通过account对象的Mark Word判断:当前线程ID还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁,这里也就不需要重新加锁。 如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

结论:JVM不用和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID就标示自己获得了当前锁,不用操作系统接入。
上述就是偏向锁:在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行。

JVM参数说明

实际上偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟,所以需要添加参数**-XX:BiasedLockingStartupDelay=0让其在程序启动时立刻启动,只是实验用,平时默认设置就好
开启偏向锁: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁: -XX:-UseBiasedLocking 关闭之后程序默认会直接进入轻量级锁状态。**

偏向锁的撤销

偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销,要升级为轻量级锁。
撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

① 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。
此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
② 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向 。

image-20230210095739642

总体步骤流程图示

image-20230210100055877

2.3 轻量级锁(本质就是CAS自旋)

轻量级锁是为了在线程近乎交替执行同步块时提高性能。

主要目的: 在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,本质就是先进行CAS自旋,实在不行了再去升级为重量级锁。说白了先自旋再阻塞。
升级时机: 当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁

假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。
而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。
此时线程B操作中有两种情况:
如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A → B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;

image-20230210102050756

如果锁获取失败,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
image-20230210102136740

image-20230210102434499

自旋达到一定次数和程度

java6之前

  • 默认启用,默认情况下自旋的次数是 10 次。使用JVM 参数 -XX:PreBlockSpin=10来修改
  • 或者自旋线程数超过cpu核数一半

java6之后(自适应)

  • 自适应意味着自旋的次数不是固定不变的,而是根据:同一个锁上一次自旋的时间, 拥有锁线程的状态来决定。

轻量锁与偏向锁的区别和不同

争夺轻量级锁失败时,自旋尝试抢占锁。轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁

3. 各种锁优缺点、synchronized锁升级和实现原理

image-20230210102921830

  • synchronized锁升级过程总结:一句话,就是先自旋,不行再阻塞。实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式

  • synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。

  • JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。

  • 偏向锁: 适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。

  • **轻量级锁:**适用于竞争较不激烈的情况(这和乐观锁的使用范围类似), 存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。

  • **重量级锁:**适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。

4. 锁升级流程图

java synchronized 锁升级流程图

5. 64位的MarkWord标记图

64位的MarkWord标记图

参考

源码级别的讲解JAVA 中的CAS

ava对象内存布局及对象头详解

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

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

相关文章

JavaSE学习day4_02 数组(超级重点)

3.数组 3.1什么是数组 数组就是存储数据长度固定的容器&#xff0c;存储多个数据的数据类型要一致。 3.2数组定义格式 3.2.1第一种&#xff08;常用&#xff09; 数据类型[] 数组名 示例&#xff1a; int[] arr; double[] arr; char[] arr; 3.2.2第二种(在…

pandas——字符串处理【建议收藏】

pandas——字符串处理 作者&#xff1a;AOAIYI 创作不易&#xff0c;如果觉得文章不错或能帮助到你学习&#xff0c;记得点赞收藏评论一下哦 文章目录pandas——字符串处理一、实验目的二、实验原理三、实验环境四、实验内容五、实验步骤1.cat() 拼接字符串2.split()切片字符串…

Viper+MSF:靶机代理转发上线学习

本文仅为代理转发学习&#xff0c;在本地环境测试验证&#xff0c;无其它目的&#xff0c;请勿用于未经授权的测试 说明&#xff1a; 之前学习的代理转发上线&#xff0c;近期也做实验通过Viper&#xff0c;利用MSF实现了多重代理转发上线靶机&#xff0c;过程有些繁琐&#x…

【Linux】-- 进程信号(认识、应用)

目录 信号初识 生活中的信号 Linux中的信号 产生信号 核心转储 软件条件产生信号的样例 系统调用接口 raise kill abort 闹钟 硬件异常产生信号 如何理解除0错误 如何理解野指针 / 越界错误 总结 信号初识 &#xff08;信号和信号量是两个东西&#xff09; 生…

java web命令学习笔记

本博文源于笔者自学java web&#xff0c;博文包含了jsp脚本代码、jsp声明、jsp表达式、注释、jsp指令等内容说明&#xff0c;并书写对应的案例。即可观看&#xff0c;也可收藏。 文章目录1.jsp脚本代码2.JSP声明3.jsp表达式4.注释5.JSP指令5.1 page指令5.2 include指令1.jsp脚本…

《霍格沃茨之遗》席卷Steam周榜 哈迷们出手了!

自上上周起&#xff0c;《霍格沃茨之遗》就打败了长期霸榜王Steam Deck&#xff0c;夺得了Steam周榜冠军&#xff0c;果然魔法的力量不容小觑。 而在最新公开的周榜&#xff08;2 月 6 日~2 月 12 日&#xff09;中&#xff0c;《霍格沃茨之遗》更是强势席卷榜单&#xff0c;四…

​网易游戏实时 HTAP 计费风控平台建设

本文整理自网易互娱资深工程师, Flink Contributor, CDC Contributor 林佳&#xff0c;在 FFA 实时风控专场的分享。本篇内容主要分为五个部分&#xff1a; 实时风控业务会话会话关联的 Flink 实现HTAP 风控平台建设提升风控结果数据能效发展历程与展望未来 众所周知&#xff…

【GlobalMapper精品教程】045:空间操作(2)——相交(Intersect)

GlobalMapper提供的空间分析(操作)的方法有:交集、并集、单并集、差异、对称差集、相交、重叠、接触、包含、等于、内部、分离等,本文主要讲述相交工具的使用。 文章目录 一、实验数据二、符号化设置三、相交运算四、结果展示五、心灵感悟一、实验数据 加载配套实验数据(…

初始C++(三):引用

文章目录一.引用的概念二.引用的使用1.引用作为输出型参数2. 引用作为函数返回值3.const引用三.引用的一些小问题四.引用和指针五.引用和指针的区别一.引用的概念 引用的作用是给一个已经存在的变量取别名&#xff0c;编译器不会为引用变量开空间&#xff0c;引用变量和被他引…

一般人我劝你不要自学软件测试!!!

本人5年测试经验&#xff0c;在学测试之前对电脑的认知也就只限于上个网&#xff0c;玩个办公软件。这里不能跑题&#xff0c;我为啥说&#xff1a;自学软件测试&#xff0c;一般人我还是劝你算了吧&#xff1f;因为我就是那个一般人&#xff01; 软件测试基础真的很简单&…

九龙证券|“春季躁动”行情要来?1月新增投资者数大增

新增投资者数量在上一年12月触及多年新低后&#xff0c;2023年1月份开端呈现反弹。 在新增投资者数量之外&#xff0c;近段时刻以来&#xff0c;包含A股商场股票成交额、北向资金净买入额、两融资金规划及成交额在内多个商场目标也呈现回暖的特征&#xff0c;目前A股商场交投氛…

SQL数据查询——连接查询

文章目录一、等值和非等值连接查询1.等值连接查询2.非等值连接查询二、自连接与非自连接三、内连接与外连接1.内连接2.外连接使用左外连接还是右外连接&#xff1f;满外连接四、UNION 的使用使用 UNION 还是 UNION ALL &#xff1f;五、各种形式集合关系的SQL实现六、语法差异注…

动态SQL使用【JavaEE】

动态SQL使用 1. if 标签 判断一个参数是否有值&#xff0c;如果没值&#xff0c;那么就会隐藏 if 中的 sql 语法&#xff1a; <if test"username!null">username#{username} </if>表达式&#xff1a;username 的参数是否为空 如果结果为 true&#xff0c…

minio前后端分离上传视频/上传大文件——前后端分离断点续传minio分片上传实现

&#x1f340;&#x1f340;&#x1f340;&#x1f340;分布式文件系统-minio&#xff1a; 第一章&#xff1a;分布式文件系统介绍与minio介绍与使用&#xff08;附minio java client 使用&#xff09;第二章&#xff1a;minio&前后端分离上传视频/上传大文件——前后端分离…

网络协议(六):网络层

网络协议系列文章 网络协议(一)&#xff1a;基本概念、计算机之间的连接方式 网络协议(二)&#xff1a;MAC地址、IP地址、子网掩码、子网和超网 网络协议(三)&#xff1a;路由器原理及数据包传输过程 网络协议(四)&#xff1a;网络分类、ISP、上网方式、公网私网、NAT 网络…

【Spark分布式内存计算框架——Spark Core】10. Spark 内核调度(中)

8.3 Spark Shuffle 首先回顾MapReduce框架中Shuffle过程&#xff0c;整体流程图如下 Spark在DAG调度阶段会将一个Job划分为多个Stage&#xff0c;上游Stage做map工作&#xff0c;下游Stage做reduce工作&#xff0c;其本质上还是MapReduce计算框架。Shuffle是连接map和reduce之…

全国空气质量排行,云贵川和西藏新疆等地空气质量更好

哈喽&#xff0c;大家好&#xff0c;春节刚刚过去&#xff0c;不知道大家是不是都开始进入工作状态了呢&#xff1f;春节期间&#xff0c;允许燃放烟花爆竹的地区的朋友们不知道都去欣赏烟花表演没有&#xff1f;其他地区的朋友们相比烟花表演可能更关心燃放烟花爆竹造成的环境…

浅谈前端设计模式:策略模式和状态模式的异同点

一、策略模式 策略模式是定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。 而且策略模式是重构小能力&#xff0c;特别适合拆分“胖逻辑”。 这个定义乍一看会有点懵&#xff0c;不过通过下面的例子就能慢慢理解它的意思。 先来看一个真实场景 某次活动要做…

测试2年还拿实习生的薪资打发我,你后悔去吧····

20年7月大学毕业&#xff0c;学的计算机科学专业。因为考研之后&#xff0c;秋招结束了。没什么更多的岗位选择&#xff0c;就想找个工作先干着&#xff0c;然后亲戚在一家大厂公司上班说要招测试&#xff0c;所以就来做测试了。 虽然都是属于计算机大类&#xff0c;但自己专业…

记一次 .NET 某游戏网站 CPU爆高分析

一&#xff1a;背景 1. 讲故事 这段时间经常有朋友微信上问我这个真实案例分析连载怎么不往下续了&#xff0c;关注我的朋友应该知道&#xff0c;我近二个月在研究 SQLSERVER&#xff0c;也写了十多篇文章&#xff0c;为什么要研究这东西呢&#xff1f; 是因为在 dump 中发现…