zoukankan      html  css  js  c++  java
  • 轻量级锁,偏向锁,重量级锁

    轻量级锁,偏向锁,重量级锁

    参考视频:https://www.bilibili.com/video/BV16J411h7Rd

    对象头信息

    对象头信息

    • normal,正常对象,使用markwork的最后3bits来标记,001就表示正常对象
    • Biased,偏向锁标记,使用markwork的最后3bits来标记,跟正常对象虽然有区别,但区别不大,101就表示偏向锁
    • 轻量级锁,最后2bits来标记,00就表示轻量级锁
    • 重量级锁,最后2bits来标记,10就表示重量级锁
    • 最后2bits为11就表示需要GC的对象

    重量级锁

    使用monitor对象来实现重量级锁,如果使用重量级锁,加锁过程就需要先去关联monitor对象,然后还需要各种判断。

    monitor对象和asynchronized的关系

    asychronized关键字实现重量级锁的原理:

    从字节码的角度说明asychronized

    monitorenter和monitorexit就是操作monitor对象,会有性能损耗,所以引入轻量级锁。

    自旋优化

    当出现重量级锁竞争的时候,不会马上进入阻塞,阻塞会进入上下文切换,会影响性能的,而是先使用自旋重试,自旋只有多核CPU才有意义,一个核进行资源访问,另一个核进行尝试加锁,如果是一个核,这个核在访问资源,那也没必要花时间去重试,所以自旋必然是多核CPU才有意义。JVM会自动控制自旋重试次数,只有多核才有意义。

    轻量级锁

    利用线程栈中的锁标记来加锁。加锁过程只是替换对象头信息即可,这比重量级锁使用monitor来说性能会有提升,这就是对重量级锁的优化。

    轻量级锁,线程栈的使用

    轻量级锁的加锁过程也就是交换线程栈和对象头信息即可,这样就会优化monitor了。

    轻量级锁的加锁过程

    加锁

    不管加锁成功与否,都会执行一次CAS操作。

    • 成功:对象头最后为01,只需要交换对象头信息,则一定成功
    • 失败:如果已经加锁,对象头最后为00(不可能为10,如果是10则是重量级锁,此时已经不可能使用轻量级锁去加锁),表示已经加锁,加锁失败会有两种情况
      • 升级锁:如果请求加锁的线程是两个线程,升级为重量级锁,引入monitor对象
      • 锁重入:如果请求加锁的线程是同一个线程,则只是锁重入,再在线程栈中添加一条锁记录

    锁重入的情况

    锁重入:一个线程对同一个对象多次加锁

    解锁

    • 如果获取的锁记录是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
    • 如果取值不为 null ,使用CAS操作,交换对象头信息完成解锁
    • 如果解锁失败,则进入重量级锁的解锁过程

    虽然多线程会对一个资源进行加锁,但是如果这些线程访问是错开的,也就是这些线程不会竞争资源,这时候使用轻量级锁能提高性能,毕竟没有引入monitor对象,只是进行CAS操作,这是轻量级锁引入的原因。线程之间不会竞争,如果出现竞争,依然会使用重量级锁,所以轻量级锁就是用来优化重量级锁性能问题的。

    偏向锁

    轻量级锁重入,CAS操作引入偏向锁

    轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作,CAS也会影响性能,所以引入偏向锁。

    偏向锁的加锁

    注意到偏向锁的对象头,里面有线程id,所以偏向锁会减少CAS操作,在一定程度上优化轻量级锁.

    JVM默认是开启偏向锁的,但不会在程序启动时就生效,而是有一点延迟,可以加VM参数-XX:BiasedLockingStartupDelay=0来禁用延迟,使用asychronized加锁,会优先使用偏向锁。

    VM参数 -XX:-UseBiasedLocking 禁用偏向锁,-XX:+UseBiasedLocking 使用偏向锁。

    偏向撤销

    1. 对象调用hashCode()方法会禁用该对象的偏向锁,原因就是调用了hashCode()方法,对象头就没有地方存放线程id了,只能禁用该对象的偏向锁。重量级锁在monitor对象中存储hashCode。
    2. 当两个及以上线程使用同一个对象时,偏向锁将会升级为轻量级锁,如果这些线程会产生资源竞争,则进一步升级为重量级锁。
    3. 对象调用wait/notify,也会撤销对象的偏向状态,原因是只有重量级锁才会有wait/notify机制
    4. 连续撤销偏向超过40次(超过阈值),jvm会认为确实偏向错了,于是所有类都不可偏向,新建的对象也不可以偏向

    批量重偏向

    当撤销偏向超过20次后(超过阈值),JVM 会觉得是不是偏向错了,这时会在给对象加锁时,又会重新开始偏向。

    Vector<Dog> list = new Vector<>();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 30; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {// 导致这30个对象偏向t1线程
                log.debug(i + "	" + ClassLayout.parseInstance(d).toPrintable());
            }
        }
        synchronized (list) {
            list.notify(); // 唤醒线程
        }
    }, "t1");
    t1.start();
    
    Thread t2 = new Thread(() -> {
        synchronized (list) {
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("===============> ");
        for (int i = 0; i < 30; i++) {
            Dog d = list.get(i);
            log.debug(i + "	" + ClassLayout.parseInstance(d).toPrintable());  // 在t2加锁前依然会偏向t1
            synchronized (d) {
                log.debug(i + "	" + ClassLayout.parseInstance(d).toPrintable()); // 前20个会使用轻量级加锁,后面的10个不会使用轻量级加锁了,也就是偏向不会撤销了
            }
            log.debug(i + "	" + ClassLayout.parseInstance(d).toPrintable()); // 前20个处于无偏向状态,两个线程使用,撤销偏向,后面的10个会偏向t2
            // 后面的10个偏向t2,就是因为JVM发现撤销偏向超过20次后(超过阈值),重新的批量偏向另一个线程
        }
    }, "t2");
    t2.start();
    

    锁消除

    -XX:-EliminateLocks,关闭锁消除
    -XX:+EliminateLocks,开启锁消除,默认开启
    

    总结

    https://blog.csdn.net/weixin_50280576/article/details/113033975

    对于 synchronized 锁来说,锁的升级主要是通过 Mark Word 中的锁标记位与是否是偏向锁标记为来达成的;synchronized 关键字所对象的锁都是先从偏向锁开始,随着锁竞争的不断升级,逐步演化至轻量级锁,最后变成了重量级锁。

    1. 偏向锁:针对一个线程来说的,主要作用是优化同一个线程多次获取一个锁的情况, 当一个线程执行了一个 synchronized 方法的时候,肯定能得到对象的 monitor ,这个方法所在的对象就会在 Mark Work 处设为偏向锁标记,还会有一个字段指向拥有锁的这个线程的线程 ID 。当这个线程再次访问同一个 synchronized 方法的时候,如果按照通常的方法,这个线程还是要尝试获取这个对象的 monitor ,再执行这个 synchronized 方法。但是由于 Mark Word 的存在,当第二个线程再次来访问的时候,就会检查这个对象的 Mark Word 的偏向锁标记,再判断一下这个字段记录的线程 ID 是不是跟第二个线程的 ID 是否相同的。如果相同,就无需再获取 monitor 了,直接进入方法体中。
      如果是另一个线程访问这个 synchronized 方法,那么实际情况会如何呢?:偏向锁会被取消掉。
    2. 轻量级锁:若第一个线程已经获取到了当前对象的锁,这是第二个线程又开始尝试争抢该对象的锁,由于该对象的锁已经被第一个线程获取到,因此它是偏向锁,而第二个线程再争抢时,会发现该对象头中的 Mark Word 已经是偏向锁,但里面储存的线程 ID 并不是自己(是第一个线程),那么她会进行 CAS(Compare and Swap),从而获取到锁,这里面存在两种情况:
      • 获取到锁成功(一共只有两个线程):那么它会将 Mark Word 中的线程 ID 由第一个线程变成自己(偏向锁标记位保持不表),这样该对象依然会保持偏向锁的状态
      • 获取锁失败(一共不止两个线程):则表示这时可能会有多个线程同时再尝试争抢该对象的锁,那么这是偏向锁就会进行升级,升级为轻量级锁
    3. 旋锁,若自旋失败,那么锁就会转化为重量级锁,在这种情况下,无法获取到锁的线程都会进入到 moniter(即内核态),自旋最大的特点是避免了线程从用户态进入到内核态。
  • 相关阅读:
    <转> Lua使用心得(2)
    (转) Lua使用心得一 LUA和VC整合
    Highcharts 的实际实践一
    Springmvc4 com/fasterxml/jackson/core/JsonProcessingException
    如何在其他电脑上运行VS2005编译的DEBUG版应用程序
    [转]深入分析 Java 中的中文编码问题
    自动白平衡技术(WhiteBalance)(转自Harri的blog)
    沉思录(1)——EricKing工作的一个月
    图像处理一(BMP的格式说明)
    ios检查版本更新
  • 原文地址:https://www.cnblogs.com/catelina/p/14494565.html
Copyright © 2011-2022 走看看