zoukankan      html  css  js  c++  java
  • Synchronized底层实现

    之前了解Synchronized不多,只知道同步代码插入了两个字节码指令,同步方法提前获取管程。近来看了很多写的很好的博客,了解了底层的实现方法。这里做一下记录,主要还是拾人牙慧。

    Synchronized底层使用c++实现,在JDK1.6之后,加入了很多优化的技术,减少线程阻塞和唤醒的开销,具体可以参看:https://www.cnblogs.com/walker993/p/14654008.html。

    Synchronized使用对象来当锁,是使用的对象头的Mark Word部分,在32位系统上大小32字节,在64系统大小64字节。下图展示了64位系统中的布局(图片来源:https://www.jianshu.com/p/d99993b52a07)

    最右侧展示的对象加锁状态,分为偏向锁,轻量级锁,重量级锁,顺序按照加锁尝试顺序,一般来讲也是加锁的开销的大小顺序。

    1. 重量级加锁

      介绍:重量级的加锁,会使线程进入阻塞,当偏向锁和轻量级锁加锁失败时采用。内部使用自适应自旋,减少进入阻塞的可能。

      实现:使用ObjectMonitor + CAS + mutex实现。ObjectMonitor也是使用c++定义的对象,其内部重要的状态有:

    ObjectMonitor(){
      _recursions = 0;   //重入次数  
      _owner = NULL;   //拥有该锁的线程
      _object = NULL;   //当做锁的对象
      _head = NULL;  //锁对象的Mark Word
      _WaitSet = NULL;  //调用wait方法进入等待的线程队列,一个双向链表
      _cxq = NULL;        //等待锁的第一队列,一个单向链表
      _EntryList = NULL;  //等待锁的第二队列,一个双向链表
      //...
    }

      ObjectMonitor在轻量级锁膨胀或直接加重量级锁时初始化:(参考:https://www.jianshu.com/p/09de11d71ef8)

      1.1 直接加重量级锁的初始化

        1. 调用omAlloc分配一个ObjectMonitor对象

        2. 初始化ObjectMonitor对象

        3. 设置header字段为锁对象的Mark Word,owner为null,object为锁对象

        4. CAS将锁对象头的Mark Word设置为重量级锁状态,并指向ObjectMonitor对象

      1.2 轻量级锁膨胀时的初始化

        1. 调用omAlloc分配一个ObjectMonitor对象

        2. 初始化ObjectMonitor对象

        3. 将Mark Word的状态设置为膨胀中

        4. 设置header字段为Displaced Mark Word,owner为Lock Record,object为锁对象 (后面会讲Lock Record,重量锁只有这里用到了Lock Record)

        5. CAS将锁对象头的Mark Word设置为重量级锁状态,并指向ObjectMonitor对象

      1.3 加锁流程 ObjectMonitor#enter

        1. CAS将ObjectMonitor对象的owner设置为当前线程,成功将recursions + 1。

        2. 失败开启自适应自旋,循环尝试。失败继续下面的步骤

        3. 将当前线程包装成ObjectWaiter对象,状态设置为TS_CXQ。死循环CAS将自己设置到cxq的头部,每次CAS失败都会再次尝试获取一下锁,知道设置成功或者获取到锁

        4. 一个死循环,尝试获取锁,失败将自己阻塞(park方法,底层使用的mutex),被唤醒则立即尝试获取锁

        5. 成功获取锁后,将自己从cxq或EntryList中移除(下面会讲为什么会在EntryList中)

      1.4 解锁流程 ObjectMonitor#exit

        1. recursions 不为0,将recursions - 1返回

        2. 根据QMode的配置采用不同的唤醒(unpark方法)策略

          2.1 QMode等于2且cxq不为空,唤醒cxq的头部线程

          2.2 QMode等于3并且cxq不为空,将cxq中的线程加到EntryList头部,唤醒EntryList头部节点

          2.3 QMode等于4并且cxq不为空,将cxq中的线程加到EntryList尾部,唤醒EntryList头部节点

      1.5 Object#wait方法

        使用前必须先获得锁,将当前线程包装成ObjectWaiter对象,状态设置为TS_WAIT。将其放到WaitSet中,调用解锁流程释放锁,并将自己阻塞

      1.6 Object#notify方法

        使用前必须先获得锁,从WaitSet中取出第一个对象,根据不同的策略放到EntryList还是cxq的头部还是尾部。并唤醒线程。notifyall则是遍历整个WaitSet,全部唤醒

    2. 轻量级加锁

      介绍:对重量级加锁的优化,旨在竞争不激烈的情况下,减少重量级加锁的开销。不再使用ObjectMonitor,同时线程不再进入阻塞。

      实现:使用Lock Record实现,使用轻量级锁时,会在线程栈上开辟一块锁记录空间,用来存放Lock Record(BasicObjectLock)。其有两个属性:

         1. _obj:存放指向锁对象的指针

         2. _prototype_header:存放锁对象的Mark Word备份(称为Displaced Mark Word)

      2.1 加锁流程

        1. 当锁对象尚未加锁,初始化一个Lock Record,设置prototype_header为锁对象的Mark Word备份,obj指向锁对象

        2. CAS替换锁对象的Mark Word为指向Lock Record的指针,无竞争的情况下替换成功,将备份的Mark Word的锁状态设置为轻量级加锁

        3. CAS交换失败,检查是否已经指向当前栈帧的锁记录空间

          3.1 若是,代表重入,则初始化一个Lock Record,设置prototype_header为null,obj指向锁对象。放入锁空间中

          3.2 否则,有竞争,锁膨胀。可能是偏向锁膨胀为轻量级锁,可能是轻量级锁膨胀为重量级锁。

            3.2.1 膨胀为重量级锁,走1.2初始化一个ObjectMonitor对象,然后执行调用ObjectMonitor#enter加锁

            3.2.2 膨胀为轻量级锁,找到占用锁的线程,匹配其所有的Lock Record,找到对应的Lock Record,将其prototype_header设置为对象的Mark Word的备份,CAS替换锁对象的Mark Word为指向Lock Record的指针。

        网上有说轻量级锁,在CAS失败时会自旋,有博主通过源码已经验证了是不会自旋的:https://www.jianshu.com/p/d99993b52a07

      2.2 解锁流程

        1. 取出锁空间保存的Lock Record,根据obj匹配到对应的Lock Record,判断其prototype_header

          1.2 为空,代表重入,释放Lock Record。(将Lock Record的obj置空,Lock Record是可以重用的)

          1.3 不为空,CAS将备份替换到锁对象头的Mark Word位置,并释放Lock Record。

            1.3.1 替换失败,说明是重量级锁或解锁发生竞争,膨胀为重量级锁,调用ObjectMonitor#exit解锁,并释放Lock Record。

        

    3. 偏向锁加锁

      介绍:对轻量级加锁的再次优化,当线程重入时,不再需要CAS操作。

      实现:通过在Mark Word中设置占用锁的线程ID以标识是哪个线程占有了锁。对象头中有类型信息指针,类型信息中也有prototype_header字段,表示初始的Mark Word,存储了epoch、偏向锁等信息。用这个部分信息加上线程的ID,跟锁对象头的Mark Word做异或运算,可以很快的找到不同的地方。

      3.1 加锁流程

        1. 获取Lock Record,将obj指向锁对象。

        2. 将类信息中的Mark Word加线程ID 跟 锁对象头的Mark Word做异或运算,找不同

          2.1 结果相等,此次为重入,执行一次步骤1。每次重入都有一个Lock Record对象

          2.2 偏向锁标志位不同,说明已经不支持偏向,修改Mark Word为无锁状态,升级为轻量级锁 (修改为无状态的原因是方便轻量级锁解锁时CAS替换回原位置,因为初始就是无锁状态)

          2.3 epoch不同,走批量重偏向逻辑 

          2.4 上述三种都不满足,说明线程ID有问题,可能锁对象的线程ID为null或不指向自己,CAS将锁对象的Mark Word偏向自己。修改失败对偏向锁进行撤销和升级

          2.5 没有开启偏向模式,走轻量级锁,构造一个无锁状态的Mark Word(注意是001,也就是该锁无法再走偏向模式)塞入Lock Record,CAS替换锁对象头的Mark Word为指向Lock Record指针

      注意:《深入理解Java虚拟机》包括一些博客上写,是使用CAS将获取锁的线程记录在对象的Mark Word中,其实这里使用的是整个替换,构建一个线程ID为当前线程的新的Mark Word,再使用CAS替换锁对象的Mark Word。

      3.2 撤销或重偏向相关

        有另外的线程访问处于偏向模式的锁对象,触发撤销。撤销分为安全点撤销和非安全点撤销,非安全点撤销需要CAS,CAS失败则需要等到安全点撤销,效率比较低。

        因此偏向锁引入了批量重偏向和批量撤销。对象的类信息中有一项计数器,统计这个类下所有对象偏向模式下被撤销的次数。注意这个是跟类绑定的

        当对象的锁被撤销次数达到阈值(XX:BiasedLockingBulkRebiasThreshold,默认20),触发批量重偏向逻辑,将类对象的epoch值加一,当其他线程发现类对象的epoch和锁对象的epoch值不一样时,可以避免撤销流程,直接CAS替换锁对象的Mark Word,这个Mark Word中线程ID指向自己,并且epoch为类的epoch。同时,为了保证正在持有偏向锁的其他线程不会因为epoch不同而丢失锁,会遍历所有正持有偏向锁的线程,将其锁对象的epoch也加一。

        当对象的锁被撤销次数达到阈值(XX:BiasedLockingBulkRevokeThreshold,默认40),触发批量撤销逻辑,将偏向标志置为0,废弃此类的偏向功能。

        这两个阈值可以改,还有一个是(XX:BiasedLockingDecayTime,默认25000),这个是两次批量重偏向的时间间隔。

        3.2.1 撤销或重偏向 revoke_and_rebias :

        包含了epoch不同、批量重偏向、批量撤销和单个线程撤销的逻辑。

        3.2.2 单个线程撤销流程 revoke_bias :

        1. 构造两个Mark Word,一个是匿名偏向状态(101,无线程ID),一个是无锁状态(001),线程ID都为空

        2. 根据锁对象的Mark Word中的线程ID,遍历jvm所有线程判断前一个线程是否还存活

          2.1 已回收,若允许重偏向则将锁对象的Mark Word设置为匿名偏向状态,否则设置为无锁状态

          2.2 未回收,遍历该线程所有Lock Record,根据obj指针判断是否有该锁对象的Lock Record(重入锁的情况下有多个)

            2.2.1 有,说明前一个线程还未退出同步,膨胀为轻量级锁。直接修改其Lock Recod,将其prototype_header置空,将第一个Lock Record(重入锁情况)的prototype_header设置为无锁状态。

            2.2.2 无,说明前一个线程已退出同步,同2.1。

        说明一下:进入单个线程撤销的时候,允许重偏向作为入参,并且值为false。也就是一定会升级为轻量级锁

      3.3 解锁流程

        1. 找出对应的Lock Record,将obj置为null。

        没有更改Mark Word中的线程ID,方便下次重入时,Mark Word异或计算结果直接相等。倘若有下个线程获取锁,走到撤销流程,根据obj为空判断之前的线程已经释放锁,会直接CAS将其偏向自己有误,除非上一个持有锁线程被回收,否则会升级轻量级锁,看5.4和5.7

    4. 总结

      偏向锁和轻量级锁的“锁”是Mark Word,偏向锁CAS将Mark Word偏向自己,轻量级锁CAS将Mark Word替换为指向自己栈中的Lock Record的指针。这两都使用Lock Record个数记录重入次数。

      而重量级锁的“锁”是ObjectMonitor,CAS设置owner为当前线程。使用recursions记录重入次数

      

      摘自:https://blog.csdn.net/baidu_38083619/article/details/82527461

    5. 通过查看对象头了解加锁过程

      参考:https://www.cnblogs.com/LemonFive/p/11246086.html

      5.1 测试对象初始化状态

    import org.openjdk.jol.info.ClassLayout;
    
    public class SynchronizedTest {
        private static class Lock {
        }
    
        public static void main(String[] args) throws InterruptedException {
            test1();
        }
    
        private static void test0() {
            Lock lock = new Lock();
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    
        //测试对象初始化Mark Word状态
        private static void test1() throws InterruptedException {
            /**
             * JVM会延迟加载偏向锁,停顿5秒,使得此时对象会被正确加锁
             */
            Thread.sleep(5000);
            Lock lock = new Lock();
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
            //对象初始化完毕,偏向和加锁状态:101,不偏向任何线程
        }
    }

       

       对象初始化完成,默认101,无锁和可偏向状态。

      5.2 计算一次哈希(未改写哈希算法)

      参考:https://blog.csdn.net/qq_43783527/article/details/115183456。如果不想等待,通过虚拟机配置 -XX:BiasedLockingStartupDelay=0。因为在虚拟机启动时,会有大量的锁竞争,并且大多不是偏向锁能解决的,所以采用优化,在这段时间关闭偏向锁功能,即偏向锁延迟加载。

    public class SynchronizedTest {
        private static class Lock {
        }
    
        public static void main(String[] args) throws InterruptedException {
            test2();
        }
    
    
        private static void test2() throws InterruptedException {
            Thread.sleep(5000);
            Lock lock = new Lock();
            System.out.println(Integer.toHexString(lock.hashCode()));
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    }

      

       测试结果:无锁状态,偏向不可用。

      一旦计算了哈希,则无法再次进入偏向模式。注意如果改写了HashCode算法,计算的哈希是不会存在对象头中的,依然是101状态,读者可以自己验证。

      5.3 测试偏向锁

    public class SynchronizedTest {
        private static class Lock {
        }
    
        public static void main(String[] args) throws InterruptedException {
            test3();
        }
    
        //测试偏向锁
        private static void test3() throws InterruptedException {
            Thread.sleep(5000);
            Lock lock = new Lock();
            synchronized (lock) {
                System.out.println(ClassLayout.parseInstance(lock).toPrintable());
            }
        }
    }

      

       测试结果:偏向锁状态,偏向主线程。这里的线程ID不是 Thread.currentThread().getId() 打印的ID。

      5.4 测试轻量级锁

    public class SynchronizedTest {
        private static class Lock {
        }
    
        public static void main(String[] args) throws InterruptedException {
            test4();
        }
    
        //测试轻量级锁
        private static void test4() throws InterruptedException {
            Thread.sleep(5000);
            Lock lock = new Lock();
            new Thread(() -> {
                synchronized (lock) {
                    System.out.println("子线程首先获取锁");
                    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                }
            }).start();
    
            Thread.sleep(5000); //这个等待是留给子线程打印对象头信息的,那个操作比较耗时
            synchronized (lock) {
                //此时子线程已经释放完锁,但是未修改对象头的线程ID,主线程再次获取锁会升级到轻量级锁
                System.out.println("主线程再次获取锁");
                System.out.println(ClassLayout.parseInstance(lock).toPrintable());
            }
        }
    }

      

      测试结果:首次加锁是偏向级锁,线程ID是子线程ID,子线程释放锁,未改写Mark Word。主线程再次加锁时,触发锁膨胀,升级为轻量级锁

      5.5 测试批量重偏向

      参考:https://www.jianshu.com/p/2a25e9954527

    public class SynchronizedTest {
        private static class Lock {
        }
    
        public static void main(String[] args) throws InterruptedException {
            test5();
        }
    
        //测试批量重偏向
        private static void test5() throws InterruptedException {
            Thread.sleep(5000);
            List<Lock> locks = new ArrayList<>();
            int testTimes = 21;
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < testTimes; i++) {
                    //生产新锁
                    Lock lock = new Lock();
                    //使偏向到线程t1
                    synchronized (lock) {
                        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                        locks.add(lock);
                    }
                }
            });
            t1.start();
            t1.join();
            for (int i = 0; i < testTimes; i++) {
                Lock lock = locks.get(i);
                //触发偏向锁撤销
                synchronized (lock) {
                    System.out.println("第" + i + "次取锁");
                    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                }
            }
        }
    }

      1. 初始偏向t1

      

       2. t2获取锁触发撤销,升级为轻量级锁

      

       3. 到达20次阈值之后,再次撤销将会触发批量重偏向,再次获取锁对象时不再升级而是重偏向到t2

      

       5.6 测试批量撤销

    public class SynchronizedTest {
        private static class Lock {
        }
    
        public static void main(String[] args) throws InterruptedException {
            test6();
        }
    
        //测试批量重撤销
        private static void test6() throws InterruptedException {
            Thread.sleep(5000);
            List<Lock> locks = new ArrayList<>();
            int testTimes = 41;
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < testTimes; i++) {
                    //生产新锁
                    Lock lock = new Lock();
                    //使偏向到线程t1
                    synchronized (lock) {
                        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                        locks.add(lock);
                    }
                }
            });
            t1.start();
            t1.join();
            for (int i = 0; i < testTimes; i++) {
                Lock lock = locks.get(i);
                //触发偏向锁撤销
                synchronized (lock) {
                    System.out.println("主线程第" + i + "次取锁");
                    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                }
            }
    
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < testTimes; i++) {
                    Lock lock = locks.get(i);
                    //触发偏向锁撤销
                    synchronized (lock) {
                        System.out.println("子线程第" + i + "次取锁");
                        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                    }
                }
            });
            t2.start();
            t2.join();
            //新锁不再支持偏向模式
            System.out.println(ClassLayout.parseInstance(new Lock()).toPrintable());
    
        }
    }

      1. 主线程触发的批量重偏向:

      

       2. 达到批量撤销阈值,很奇怪并没有触发批量撤销

      

       3. 子线程触发批量撤销,升级为轻量级锁,并且也不再触发批量重偏向

      

       

       4. 新锁不再支持偏向模式

      

       5.7 前加锁线程回收情况下的偏向加锁

    public class SynchronizedTest {
        private static class Lock {
        }
    
        public static void main(String[] args) throws InterruptedException {
            test7();
        }//测试重新获取偏向锁
        private static void test7() throws InterruptedException {
            Thread.sleep(5000);
            Lock lock = new Lock();
            Thread t1 = new Thread(() -> {
                synchronized (lock) {
                    System.out.println("子线程首先获取锁");
                    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                }
            });
            t1.start();
            t1.join();
            //杀死线程
            t1.stop();
    
           Thread t2 = new Thread(() -> {
                synchronized (lock) {
                    System.out.println("子线程再次获取锁");
                    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                }
            });
            t2.start();
            t2.join();
        }
    }

      

      注意:这两线程ID一样是线程ID复用的原因

       5.8 测试重量级加锁

    public class SynchronizedTest {
        private static class Lock {
        }
    
        public static void main(String[] args) throws InterruptedException {
            test8();
        }
    
        //偏向锁最终膨胀为重量级锁
        private static void test8() throws InterruptedException {
    //        Thread.sleep(5000);
            Lock lock = new Lock();
            new Thread(() -> {
                synchronized (lock) {
                    System.out.println("子线程首先获取锁");
                    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                    try {
                        //持有锁并等待,让锁升级
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }).start();
    
            Thread.sleep(3000);
            synchronized (lock) {
                //此时子线程还未释放锁,膨胀到轻量级锁再膨胀到重量级锁
                System.out.println("主线程抢锁");
                System.out.println(ClassLayout.parseInstance(lock).toPrintable());
            }
        }
    
    
    }

      

       测试结果:在偏向线程未释放锁时,产生竞争最终会升级到重量级锁

    参考:

    (1) https://www.jianshu.com/p/d99993b52a07

    (2) https://www.jianshu.com/p/22b5a0a78a9b

    (3) https://www.jianshu.com/p/911c112e0c2f

    (4) https://www.jianshu.com/p/09de11d71ef8

    (5) https://www.jianshu.com/p/4758852cbff4

    (6) https://www.cnblogs.com/LemonFive/p/11248248.html

    人生就像蒲公英,看似自由,其实身不由己。
  • 相关阅读:
    mysql练习
    导航 开发 常用 官方网址 办公 政府 网站 url
    Yii 数据库 连接 Error Info: 向一个无法连接的网络尝试了一个套接字操作。
    xampp Apache Access forbidden! Error 403 解决方法
    MySQL 没有密码 初始化 设置 用户名
    Apache 虚拟机 vhosts C:WINDOWSsystem32driversetchosts
    js 返回上一页 链接 按钮
    MySQL concat concat_ws group_concat 函数(连接字符串)
    PHP的UTF-8中文转拼音处理类(性能已优化至极致)
    原生JavaScript实现金额大写转换函数
  • 原文地址:https://www.cnblogs.com/walker993/p/14664599.html
Copyright © 2011-2022 走看看