• JUC并发编程系列详解篇十(Synchronized底层原理分析)


    synchronized 底层语义原理

    synchronized 锁机制在 Java 虚拟机中的同步是基于进入和退出监视器锁对象 monitor 实现的(无论是显示同步还是隐式同步都是如此),每个对象的对象头都关联着一个 monitor 对象,当一个 monitor 被某个线程持有后,它便处于锁定状态。在 HotSpot 虚拟机中,monitor 是由 ObjectMonitor 实现的,每个等待锁的线程都会被封装成 ObjectWaiter 对象,ObjectMonitor 中有两个集合,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表 ,owner 区域指向持有 ObjectMonitor 对象的线程。当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合尝试获取 moniter,当线程获取到对象的 monitor 后进入 _Owner 区域并把 _owner 变量设置为当前线程,同时 monitor 中的计数器 count 加1;若线程调用 wait() 方法,将释放当前持有的 monitor,count自减1,owner 变量恢复为 null,同时该线程进入 _WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor 并复位变量的值,以便其他线程获取 monitor。

    _EntryList:存储处于 Blocked 状态的 ObjectWaiter 对象列表。
    _WaitSet:存储处于 wait 状态的 ObjectWaiter 对象列表。

    Synchronized原理分析

    首先来看一个例子,深入JVM看字节码,创建如下的代码:

    public class SynchTestDemo {
        public static void printlnTest(){
            synchronized (SynchTestDemo.class){
                System.out.println("hhhhh");
            }
        }
        public static void main(String[] args) {
            printlnTest();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    使用javac命令进行编译生成.class文件:

    javac SynchTestDemo.java
    
    • 1

    使用javap命令反编译查看.class文件的信息

    javap -verbose SynchTestDemo.class
    
    • 1

    执行完毕可以得到如下信息,如图所示:
    在这里插入图片描述
    请红色方框里的monitorentermonitorexit。其中monitorenter指令执行;了一次,而monitorexit指令实际上是执行了两次,第一次是正常情况下释放锁,第二次为发生异常情况时释放锁,这样做的目的在于保证线程不死锁。

    monitor指令

    Monitorenter和Monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得。

    monitorenter指令

    在JVM规范中有提到对monitorenter指令的描述: 任何一个对象都有一个monitor与其相关联,当且有一个monitor被持有后,它将处于锁定的状态,其他线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,他会尝试去获取当前对应的monitor的所有权。

    一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:

    • monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
    • 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加。
    • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

    monitorexit指令

    在JVM规范中同样有提到对monitorenter指令的描述:能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程;执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

    字节码分析

    synchronized关键字被编译成字节码后会被翻译成monitorentermonitorexit两条指令分别在同步块逻辑代码的起始位置与结束位置,如下图所示:
    在这里插入图片描述
    每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:
    在这里插入图片描述
    synchronized的实现原理:synchronized的底层实际是通过一个monitor对象来实现的,其实wait/notify方法也是依赖于monitor对象来实现的,这就是为什么只有在同步代码块或者方法中才能调用该方法,否则就会抛出出java.lang.IllegalMonitorStateException的异常的原因。再看一个例子:

    public class SynchTestDemo {
    
        public synchronized void printlnTest(){
            System.out.println("hhhhh");
        }
        public static void main(String[] args) {
            new SynchTestDemo().printlnTest();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    根据上面的方法编译运行然后反编译获取字节码文件,可得下面的内容:
    在这里插入图片描述
    从字节码反编译的可以看出,同步方法并没有通过指令monitorenter和monitorexit来实现的,但是相对于普通方法来说,其常量池多了了 ACC_SYNCHRONIZED 标示符。JVM实际就是根据该标识符来实现方法的同步的。

    当方法被调用时,会检查ACC_SYNCHRONIZED标志是否被设置,若被设置,线程会先获取monitor,获取成功才能执行方法体,方法执行完成后会再次释放monitor。在方法执行期间,其他线程都无法获得同一个monitor对象。

    其实两种同步方式从本质上看是没有区别的,两个指令的执行都是JVM调用操作系统的互斥原语mutex来实现的,被阻塞的线程会被挂起、等待重新调度,会导致线程在“用户态”和“内核态”进行切换,就会对性能有很大的影响。

    monitor详解

    monitor通常被描述为一个对象,可以将其理解为一个同步工具,或者可以理解为一种同步机制。所有的Java对象自打new出来的时候就自带了一把锁,就是monitor锁,也就是对象锁,存在于对象头(Mark Word),锁标识位为10,指针指向的是monitor对象起始地址。

    在Java虚拟机(HotSpot)中,Monitor是由其底层实际是由C++对象ObjectMonitor实现的:

    ObjectMonitor() {
        _header = NULL;
        _count = 0;            //用来记录该线程获取锁的次数
        _waiters = 0,
        _recursions = 0;         // 线程的重入次数 
        _object = NULL;         // 存储该monitor的对象
        _owner = NULL;           // 标识拥有该monitor的线程
        _WaitSet = NULL;         // 处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock = 0 ;
        _Responsible = NULL;
        _succ = NULL;
        _cxq = NULL;           // 多线程竞争锁时的单向队列
        FreeNext = NULL;
        _EntryList = NULL;         // 处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq = 0;
        _SpinClock = 0;
        OwnerIsThread = 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    1. _owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的;
    2. _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。cxq是一个临界资源,JVM通过CAS原子指令来修改cxq队列。修改前cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈);
    3. _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中;
    4. _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。

    举个例子具体分析一下_cxq队列与_EntryList队列的区别:

    public void print() throws InterruptedException {
        synchronized (obj) {
            System.out.println("Hello World");
            //obj.wait();
        }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    若多线程执行上面这段代码,刚开始t1线程第一次进同步代码块,能够获得锁,之后马上又有一个t2线程也准备执行这段代码,t2线程是没有抢到锁的,t2这个线程就会进入_cxq这个队列进行等待,此时又有一个线程t3准备执行这段代码,t3当然也会没有抢到这个锁,那么t3也就会进入_cxq进行等待。

    接着,t1线程执行完同步代码块把锁释放了,这个时候锁是有可能被t1、t2、t3中的任何一个线程抢到的。

    假如此时又被t1线程给抢到了,那么上次已经进入_cxq这个队列进行等待的线程t2、t3就会进入_EntryList进行等待,若此时来了个t4线程,t4线程没有抢到锁资源后,还是会先进入_cxq进行等待。

    具体分析一下_WaitSet队列与_EntryList队列:(图片来源微信公众号 - 得物技术:精选文章|深入理解synchronzied底层原理 )
    在这里插入图片描述
    每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象。ObjectWaiter 对象里存放thread(线程对象) 和unpark的线程, 每一个等待锁的线程都会有一个ObjectWaiter对象,而objectwaiter是个双向链表结构的对象。

    结合上图monitor的结构图可以分析出,当线程的拥有者执行完线程后,会释放锁,此时有可能是阻塞状态的线程去抢到锁,也有可能是处于等待状态的线程被唤醒抢到了锁。在JVM中每个等待锁的线程都会被封装成ObjectMonitor对象,_owner标识拥有该monitor的线程,而_EntryList和_WaitSet就是用来保存ObjectWaiter对象列表的,_EntryList和_WaitSet最大的区别在于前者是用来存放等待锁block状态的线程,后者是用来存放处于wait状态的线程。

    当多个线程同时访问同一段代码时:

    • 首先会进入_EntryList集合每当线程获取到对象的monitor后,会将monitor中的_ower变成设置为当前线程,同时会将monitor中的计数器_count加1。
    • 若线程调用wait()方法时,将释放当前持有的monitor对象,将_ower设置为null,_count减1,同时该线程进入_WaitSet中等待被唤醒。
    • 若当前线程执行完毕,也将释放monitor锁,并将_count值复原,以便于其他线程获取锁。

    monitor对象存在于每个Java对象的对象头(Mark Word)中,所以Java中任何对象都可以作为锁,由于notify/notifyAll/wait等方法会使用到monitor锁对象,所以必须在同步代码块中使用。

    多线程情况下,线程需要同时访问临界资源,监视器monitor可以确保共享数据在同一时刻只会有一个线程在访问。

    可重入原理:加锁次数计数器

    可重入:(来源于维基百科)若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。

    可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。

    public class SynchronizedDemo {
    
        public static void main(String[] args) {
            SynchronizedDemo demo =  new SynchronizedDemo();
            demo.method1();
        }
    
        private synchronized void method1() {
            System.out.println(Thread.currentThread().getId() + ": method1()");
            method2();
        }
    
        private synchronized void method2() {
            System.out.println(Thread.currentThread().getId()+ ": method2()");
            method3();
        }
    
        private synchronized void method3() {
            System.out.println(Thread.currentThread().getId()+ ": method3()");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    执行monitorenter获取锁

    • (monitor计数器=0,可获取锁)
    • 执行method1()方法,monitor计数器+1 -> 1 (获取到锁)
    • 执行method2()方法,monitor计数器+1 -> 2
    • 执行method3()方法,monitor计数器+1 -> 3

    执行monitorexit命令

    • method3()方法执行完,monitor计数器-1 -> 2
    • method2()方法执行完,monitor计数器-1 -> 1
    • method2()方法执行完,monitor计数器-1 -> 0 (释放了锁)
    • (monitor计数器=0,锁被释放了)

    参考文章

    • https://www.pdai.tech/md/java/thread/java-thread-x-key-synchronized.html
    • 微信公众号(得物技术) :精选文章|深入理解synchronzied底层原理
    • https://blog.csdn.net/a745233700/article/details/119923661
    • 《深入理解Java虚拟机》+《Java并发编程的艺术》
    • https://juejin.im/post/5ae6dc04f265da0ba351d3ff
    • https://www.cnblogs.com/javaminer/p/3889023.html
    • https://www.jianshu.com/p/dab7745c0954
    • https://www.cnblogs.com/wuchaodzxx/p/6867546.html
    • https://www.cnblogs.com/xyabk/p/10901291.html
    • https://www.jianshu.com/p/64240319ed60
  • 相关阅读:
    Revisiting Large Language Models as Zero-shot Relation Extractors
    腾讯云4核8G云服务器够用吗?支持多少人访问?
    vue 生命周期钩子函数 mounted()实例
    Spring Boot 整合邮件服务
    Toronto Research Chemicals 6α-羟基乙炔雌二醇参数说明
    东方博宜11月月赛(A,B,C三组的题解)
    K8S核心概念之SVC(易混淆难理解知识点总结)
    dyld: Symbol not found: __ZNSt3__113basic_filebufIcNS_11char_traitsIcEEEC1Ev
    【Harmony OS】【JAVA UI】abilitySlice和ability跳转方式
    Python并发执行(未完待续)
  • 原文地址:https://blog.csdn.net/m0_46198325/article/details/126800917