互斥同步-锁

简介: 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

 

 

 

 

相关文章
|
4月前
|
缓存 算法
同步与互斥(一)
同步与互斥(一)
40 1
|
4月前
|
算法 Java 程序员
同步与互斥(二)
同步与互斥(二)
87 0
|
机器学习/深度学习 存储 机器人
LLM系列 | 19: ChatGPT应用框架LangChain实践速成
本文以实践的方式将OpenAI接口、ChatOpenAI接口、Prompt模板、Chain、Agent、Memory这几个LangChain核心模块串起来,从而希望能够让小伙伴们快速地了解LangChain的使用。
|
缓存 Oracle 关系型数据库
【数据设计与实现】第5章:同步与互斥
同步与互斥设计原则数据库的一个重要能力就是为多个用户提供并发访问服务,并发度是考察数据库性能的重要指标之一。事务隔离级别定义了并发控制算法的正确性,并让用户通过选择隔离级别在正确性和高性能之间进行平衡。事务重点考虑的是数据层面的并发控制,是属于较上层的同步与互斥。实际上,数据库系统是由大量进程、线程、数据结构构成的,进程、线程会并发地访问、修改数据结构,还需要在较底层级解决数据结构的同步与互斥问题
【数据设计与实现】第5章:同步与互斥
|
安全 测试技术 开发工具
a.gray.PiggyGoldcoin.a病毒(已解决)
a.gray.PiggyGoldcoin.a病毒(已解决)
|
Java
从JVM角度看对象创建的过程
对象创建过程如下: 通过new指令参数查找常量池中类的符号引用,检查此 符号引用代表的类是否已经被加载、准备、解析、初始化过,如果没有则先执行这些操作。 计算并为对象分配内存。 将分配的内存空间初始化为零值。 设置对象头 执行构造函数。
3056 0
|
安全 Java
Java内存模型
本博客为《深入理解java虚拟机》的学习笔记,所以大部分内容来自此书,另外一部分内容来自网络其他博客和源码分析。 主要内容探讨以下问题: Ø  Java内存模型、协议、规则。 Ø  volatile的可见性和禁止指令重排序是什么意思? Ø  Synchronized是如何做到线程安全的? Ø  先行发生原则。
20593 0
|
1天前
|
存储 运维 安全
云上金融量化策略回测方案与最佳实践
2024年11月29日,阿里云在上海举办金融量化策略回测Workshop,汇聚多位行业专家,围绕量化投资的最佳实践、数据隐私安全、量化策略回测方案等议题进行深入探讨。活动特别设计了动手实践环节,帮助参会者亲身体验阿里云产品功能,涵盖EHPC量化回测和Argo Workflows量化回测两大主题,旨在提升量化投研效率与安全性。
云上金融量化策略回测方案与最佳实践
|
15天前
|
人工智能 自动驾驶 大数据
预告 | 阿里云邀您参加2024中国生成式AI大会上海站,马上报名
大会以“智能跃进 创造无限”为主题,设置主会场峰会、分会场研讨会及展览区,聚焦大模型、AI Infra等热点议题。阿里云智算集群产品解决方案负责人丛培岩将出席并发表《高性能智算集群设计思考与实践》主题演讲。观众报名现已开放。
|
7天前
|
自然语言处理 数据可视化 API
Qwen系列模型+GraphRAG/LightRAG/Kotaemon从0开始构建中医方剂大模型知识图谱问答
本文详细记录了作者在短时间内尝试构建中医药知识图谱的过程,涵盖了GraphRAG、LightRAG和Kotaemon三种图RAG架构的对比与应用。通过实际操作,作者不仅展示了如何利用这些工具构建知识图谱,还指出了每种工具的优势和局限性。尽管初步构建的知识图谱在数据处理、实体识别和关系抽取等方面存在不足,但为后续的优化和改进提供了宝贵的经验和方向。此外,文章强调了知识图谱构建不仅仅是技术问题,还需要深入整合领域知识和满足用户需求,体现了跨学科合作的重要性。