互斥同步-锁

简介: synchronized原理。 对象头与Monitor结构。 锁优化技术:锁粗化、锁消除、自旋与适应性自旋、偏向锁、轻量级锁


本文主要从字节码角度、原理角度讨论synchronized的实现原理,以及jvm的锁优化技术。

因为代码、编译后的字节码内容较多,仅截取了与本博客讨论内容相关的代码。重点看标红部分的代码。

一 synchronized

synchronized既可以用在方法上,也可以使用在代码块上,虚拟机对这两种铜鼓使用的是不同的处理逻辑,接下来我们就根据示例的字节码来聊聊虚拟机的处理方式。

1  同步指令

方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作中。方法上添加synchronized以后,编译出来的字节码中会给方法加上ACC_SYNCHRONIZED标识,见后面的示例。虚拟机从方法区常量池的方法表数据结构中判断方法是否拥有ACC_SYNCHRONIZED标识,如果拥有此标识,那么执行线程就要求先持有锁(也称为管程);方法执行过程中其他线程无法获取锁;方法执行结束或者异常退出时释放锁。

代码块的同步是显示的,由monitorenter和monitorexit两条指令完成synchronized块的语义,见后面的示例。

 

2  静态方法同步

静态同步方法,锁是当前类的class对象。对这一点也比较好理解,静态方法调用前不需要为此类创建一个实例,所以也就不可能锁此类的实例对象。

1)   源码

    public static synchronized void syn1() {
        System.out.println("syn1");
    }

2)   字节码

  public static synchronized void syn1();

    descriptor: ()V

    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED

    Code:

      stack=2, locals=0, args_size=0

         0: getstatic     #2       // Field java/lang/System.out:Ljava/io/PrintStream;

         3: ldc           #3       // String syn1

         5: invokevirtual #4       // Method java/io/PrintStream.println:(Ljava/lang/String;)V

         8: return

      LineNumberTable:

        line 6: 0

        line 7: 8

 

3  普通方法同步

普通同步方法,锁的是当前实例对象。这也很好理解,因为存在多态性,一个类可能有非常多的实例、子类,所以普通方法执行前需要通过一个被称之为“分派”的过程确定调用方法的版本,所以不可能锁此类的class对象,从这个角度上看,其实很容理解这里锁的必然就只能是当前类的某一个实例。

1)   源码

    public synchronized void syn2() {
        System.out.println("syn2");
    }

2)   字节码

  public synchronized void syn2();

    descriptor: ()V

    flags: ACC_PUBLIC, ACC_SYNCHRONIZED

    Code:

      stack=2, locals=1, args_size=1

         0: getstatic     #2       // Field java/lang/System.out:Ljava/io/PrintStream;

         3: ldc           #5       // String syn2

         5: invokevirtual #4       // Method java/io/PrintStream.println:(Ljava/lang/String;)V

         8: return

      LineNumberTable:

        line 10: 0

        line 11: 8

 

4  代码块同步

同步代码块,锁是括号中的对象,示例中是this这个实例。从字节码的代码中可以看到,synchronized被转换成monitorenter和monitorexit两个指令。编译器为了保证无论方法通过何种方式完成,执行过monitorenter指令以后必须执行monitorexit指令,所以编译器生成了一段异常处理流程的指令。

1)   源码

    public void syn3() {
        synchronized (this) {
            System.out.println("syn3");
        }
    }

2)   字节码

  public void syn3();

    descriptor: ()V

    flags: ACC_PUBLIC

    Code:

      stack=2, locals=3, args_size=1

         0: aload_0              // 将this加载到栈顶

         1: dup                 // 复制栈顶元素this,并将复制的元素入栈。栈顶有2个this的引用

         2: astore_1             // 将栈顶元素this取出,存放到局部变量表第2个slot中

         3: monitorenter         // 获取栈顶元素this的锁

         4: getstatic     #2      // 调用静态方法System.out;

         7: ldc           #6    // 将常量池中常量syn3推送到栈顶

         9: invokevirtual #4      // 执行静态方法println()

        12: aload_1             // 将局部变量表第2个slot中的数据this,加载到栈顶

        13: monitorexit          // 释放栈顶元素this的锁

        14: goto          22    // 跳转到22行

        17: astore_2             // (异常时执行)将栈顶元素存到局部变量表第3个slot中

        18: aload_1             // (异常时执行)将局部变量表第2个slot中的数据this,加载到栈顶

        19: monitorexit          // (异常时执行)释放栈顶元素this的锁  

        20: aload_2

        21: athrow

        22: return

      Exception table:

         from    to  target type

             4    14    17   any

            17    20    17   any

      LineNumberTable:

        line 14: 0

        line 15: 4

        line 16: 12

        line 17: 22

      StackMapTable: number_of_entries = 2

        frame_type = 255 /* full_frame */

          offset_delta = 17

          locals = [ class com/wzf/greattruth/thread/SynchronizedTester, class java/lang/Object ]

          stack = [ class java/lang/Throwable ]

        frame_type = 250 /* chop */

          offset_delta = 4


二 对象头与Monitor

1  Mark Word

在内存中,对象包括:对象头、实例数据、对齐填充三部分。其中对象头分为Mark Word、类型指针、数组长度(如果是数组的话)。32位虚拟机的对象头中Mark word结构如下:

锁状态

25bit

4bit

1bit

2bit

23bit

2bit

 

是否是偏向锁

锁标志位

无锁态

对象的hashCode

分代年龄

0

01

偏向锁

线程ID

epoch

分代年龄

1

01

轻量级锁

指向栈中锁标记的指针

00

重量级锁

指向互斥量(重量级锁)的指针

10

GC标记

空

11

从此表中可以看到,JVM通过对象头中Mark word中锁标志位记录对象的锁状态。

如果锁标志位是01,则通过是否偏向标志位判断是偏向锁还是无锁状态。

如果锁标志位是00,则表明目前对象处于轻量级锁定状态。

如果锁标志位是10,则表明目前对象处于重量级锁定状态。

如果锁标志位是11,则表明前30bit不需要记录信息。

2  Monitor Record

Monitor Record是线程私有的,每一个线程都有一个可用Monitor record列表,同时还有一个全局的可用列表。以32位虚拟机为例,对象被锁住以后,其对象头的Mark word中前30bit是指向重量级锁的指针,即这里的Monitor Record;同时Monitor Record中owner字段中也存放了持有锁的线程的id。

Monitor Record主要包括以下信息:Owner、EntryQ、RcThis、Nest、HashCode、Candidate,其结构如下:

Owner

初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;

EntryQ

关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。

RcThis

表示blocked或waiting在该monitor record上的所有线程的个数。

Nest

用来实现重入锁的计数。

HashCode

保存从对象头拷贝过来的HashCode值

Candidate

0表示没有需要唤醒的线程。

1表示要唤醒一个继任线程来竞争锁。

用来避免不必要的阻塞或等待线程唤醒。如果每次释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换,从而导致性能严重下降。

 

3  Monitor数据结构

typedef struct monitor {  
    pthread_mutex_t lock;  
    Thread *owner;  
    Object *obj;  
    int count;  
    int in_wait;  
    uintptr_t entering;  
    int wait_count;  
    Thread *wait_set;  
    struct monitor *next;  
} Monitor;
 
 


三 锁优化

1  锁优化

1)  锁优化技术

  • 锁粗化(Lock Coarsening)
  • 锁消除(Lock Elimination)
  • 自旋与适应性自旋(Spinning & Adaptive Spinning)
  • 偏向锁(Biased Locking)
  • 轻量级锁(Lightweight Locking)

2)  锁粗化

将多个连续的锁扩展成一个范围更大的锁,用以减少频繁互斥同步导致的性能损耗。

以下代码中,反复对obj加锁、释放锁,会产生不必要的性能损失。

    Object obj = new Object() ;
    private void  wide(){
        for( int i = 0 ; i< 10000 ; i++){
            synchronized (obj) {
                //TODO 业务逻辑
            }
        }
    }

3)  锁消除

JVM及时编译器在运行时,通过逃逸分析,如果判断一段代码中,堆上的所有数据不会逃逸出去从来被其他线程访问到,就可以去除这些锁。

4)  自旋与适应性自旋

一般而言,共享数据的锁定状态只会持续很短的一段时间,为了这个很短的时间去挂起和恢复线程并不值得。如果物理机有多个处理器,能够支持两个以上的线程同时并行执行,那么可以让后面请求锁的线程“稍微等待一会”,即让线程执行一个忙循环(自旋),看看持有锁的线程是否能够很快的释放锁。这种技术被称之为:自旋锁。

JDK1.4引入自旋锁,可以-XX:+UseSpinning来开启自旋锁,JDK1.6中默认开启。自旋过程会占用处理器时间,所以不能无限制自旋,默认自旋次数为10次,可以通过-XX:PreBlockSpin来修改。

JDK1.6引入了适应性自旋。适应性自旋的自旋时间根据之前同一个锁的自旋时间和持有者(线程)的状态来决定,用以期望能减少阻塞的时间。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机会认为这次自旋很有可能再次成功,进而允许他自旋等待更长时间;如果某个锁,自旋很少成功获得锁,那么以后获取这个锁时可能省略掉自旋过程。

 

5)   轻量级锁

轻量级锁是在无竞争情况下,使用CAS操作去消除重量级锁因为使用的系统互斥量而产生的性能损耗。JDK1.6引入。

a)    上锁

线程(例如线程A)在进入同步代码块的时候,如果此同步对象没有被锁定(Mark word中锁标志位=01),虚拟机首先在当前线程的栈帧中建立一个锁记录(Lock Record),用于存储锁对象目前的Mark Word的拷贝(此拷贝被称之为Displaced Mark Word)。

接着,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向线程(A)栈帧中Lock Record的指针。如果CAS操作成功,那么这个线程(A)就拥有了该对象的锁,并且对象的Mark Word的锁标志位将被改为00,即处于轻量级锁定状态。

轻量级锁CAS操作前堆栈与对象状态如下:

aac9e3288b12ed440b78190f42b5c3850d8aeb9e

轻量级锁CAS操作后堆栈与对象状态如下:

5f2231da3fd535821eba32eea8fc235659cdd3f5

如果CAS操作失败,虚拟机首先检查对象的Mark Word是否指向当前线程(A)的栈帧,如果是指向当前线程(A)的栈帧,则说明当前线程(A)已经拥有了这个对象的锁,那么可以直接进入同步块继续执行;如果不是指向当前线程(A)的栈帧,说明这个锁对象已经被其他线程抢占了。

如果另外一个线程(例如B线程)进来尝试获取对象的锁,首先按照上面的过程尝试加轻量级锁,即通过CAS操作,尝试将对象头Mark Word更新为指向当前线程(B)栈帧中Lock Record的指针,因为已经有线程A占用对象锁,所以B线程会进入自旋。如果自旋结束线程B依然无法取得锁,那么对象的锁将膨胀为重量级锁,此时自旋线程B进入阻塞状态,线程A执行完毕以后唤醒阻塞线程。

b)    解锁

轻量级锁的解锁过程也是通过CAS来实现的。如果对象的Mark Word仍然指向线程的锁记录,那么就用CAS操作把对象当前的Mark Word替换回来(使用线程栈帧的锁记录中的Displaced Mark Word进行替换);如果替换成功,整个同步过程就完成了;如果替换失败,说明有其他线程尝试过获取该锁,那么在释放锁的同时,唤醒被挂起的线程(如果替换失败这一段描述,总感觉不正确,不过没有阅读过源码、书上、博客上都是这么写的,所以暂时未做改动)。

6)   偏向锁

偏向锁是在无竞争的情况下将整个同步都消除掉,连CAS操作都省去。JDK1.6引入。

当锁对象第一次被线程获取的时候,虚拟机将对象头中“是否偏向锁”的标志位改为1;同时通过CAS操作将获取锁的线程Id写入到锁对象Mark Word的前23bit中(32位虚拟机)。

如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不进行任何同步操作(例如Locking、Unlocking、以及对mark word的update等)。具体过程如下:下次获取锁的时候,检查当Mark Word中的ThreadId是否和当前线程的Id一致。如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段,提高了效率。

当有另外一个线程去尝试获取这个锁时,偏向模式宣告结束。根据对象目前是否处于被锁定状态,撤销“是否偏向锁”标识,恢复到未锁定(锁标识=01)或者轻量级锁定(锁标识=00)的状态,后续过程和轻量级锁加锁过程一致。

偏向锁、轻量级锁的状态转化及对象Mark Word关系如下图所示:

c9615f637edb463e601683cc9fd10ec1e314a26b

 

 

 

 

相关文章
|
运维 资源调度 Kubernetes
阿里巴巴超大规模Kubernetes基础设施运维体系揭秘
在过去近三年时间,Kubernetes架构在阿里巴巴集团经历了飞速发展:既支持了集团统一调度超大规模场景,又支持了大批云上云产品用户,还在支持OXS区容器化。经过这些年的发展,我们也逐渐锤炼出了K8S Serverless全托管稳定性运维体系。这些运维和稳定性能力不是K8S原生就具备的,而是在大量实践和失败过程中的沉淀出来的系统稳定性加固能力。
阿里巴巴超大规模Kubernetes基础设施运维体系揭秘
|
7月前
|
人工智能 监控 Linux
AI小龙虾OpenClaw开源Agent生态变现全指南:六大盈利路径+全平台部署与大模型配置实战
OpenClaw(社区昵称“小龙虾”)自2025年底推出以来,已成为全球增长最快的开源自主AI Agent项目,GitHub星标在两个月内突破20万,生态内创业项目超过150个,月度整体收益规模稳定在250万至270万人民币区间。作为一款支持本地优先部署、可通过主流即时通讯工具操控的AI执行框架,OpenClaw真正实现了从自然语言对话到真实环境操作的跨越,可直接操控文件、浏览器、API与脚本执行,具备极强的落地与变现潜力。
2409 0
|
9月前
|
数据采集 缓存 NoSQL
天眼查 item_get - 获取企业详情接口对接全攻略:从入门到精通
天眼查item_get接口(baseinfoV2)通过企业名称、信用代码等关键字,获取企业工商信息、联系方式、经营状态及变更记录等结构化数据,广泛应用于征信、风控、供应商管理。接口基于HTTPS+Token认证,数据源自权威渠道,字段完整、更新实时。本指南涵盖权限申请、Python对接、调试排错与生产优化,提供全链路实操支持,助力企业高效稳定集成。
|
传感器 运维 数据可视化
AR眼镜巡检系统在工业互联网的应用:AR+IoT
AR与IoT融合构建虚实闭环,IoT采集实时数据,AR直观呈现并交互,形成感知-分析-决策-行动高效闭环,提升运维效率。
|
存储 人工智能 弹性计算
AI计算加速渗透、基础设施全面升级…云栖大会重磅发布全览
阿里云全面展示了全新升级后的AI Infra系列产品及能力。通过全栈优化,阿里云打造出一套稳定和高效的AI基础设施,连续训练有效时长大于99%,模型算力利用率提升20%以上。
1370 28
|
SQL 人工智能 供应链
《AI协同供应链调度困局:从需求拆解到落地增效的全流程实践》
本文记录某制造业供应链调度系统升级的AI协同开发实践:面对旧系统“信息流滞后、决策流固化、响应流迟缓”困境及10周重构需求,团队构建“Cursor+Tabnine+Diagrams AI等”工具矩阵,以AI承接规则性工作、人聚焦核心决策。需求拆解3天完成(效率提130%),架构设计2天规避数据迁移风险,编码5天压缩重复工作,联调2小时定位性能瓶颈。项目提前3周落地,调度响应延迟2.8秒(优于目标30%),供应链成本降8%,订单延误率从15%降至3%。核心认知为AI是“认知延伸器”,需“AI生成+人工校验”闭环,工具矩阵最大化协同价值,同时需避免AI主导需求与核心编码。
656 8
|
机器学习/深度学习 传感器 编解码
LFM信号的回波与脉冲压缩附Matlab代码
LFM信号的回波与脉冲压缩附Matlab代码
752 0
|
Python
深入理解 Python 中的异步操作:async 和 await
Python 的异步编程通过 `async` 和 `await` 关键字处理 I/O 密集型任务,如网络请求和文件读写,显著提高性能。`async` 定义异步函数,返回 awaitable 对象;`await` 用于等待这些对象完成。本文介绍异步编程基础、`async` 和 `await` 的用法、常见模式(并发任务、异常处理、异步上下文管理器)及实战案例(如使用 aiohttp 进行异步网络请求),帮助你高效利用系统资源并提升程序性能。
1712 7
|
消息中间件 存储 NoSQL
消息中间件之MQ详解及四大MQ比较
消息中间件之MQ详解及四大MQ比较
3427 127
消息中间件之MQ详解及四大MQ比较
|
Java 索引
substring方法基本使用
`substring` 方法是 Java 中 `String` 类的重要方法,用于从字符串中提取子串。它有两种常用形式:`substring(int beginIndex)` 从指定索引开始截取至字符串末尾;`substring(int beginIndex, int endIndex)` 从指定起始索引截取到结束索引(不包括)。注意索引超出范围会抛出异常。
1018 0