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

摘要:
轻量级锁利用线程栈中的锁标记来加锁。线程之间不会竞争,如果出现竞争,依然会使用重量级锁,所以轻量级锁就是用来优化重量级锁性能问题的。偏向锁轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作,CAS也会影响性能,所以引入偏向锁。当两个及以上线程使用同一个对象时,偏向锁将会升级为轻量级锁,如果这些线程会产生资源竞争,则进一步升级为重量级锁。
轻量级锁,偏向锁,重量级锁

参考视频: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(即内核态),自旋最大的特点是避免了线程从用户态进入到内核态。

免责声明:文章转载自《轻量级锁,偏向锁,重量级锁》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇shell 命令行参数(基本)iOS判断一些权限是否被禁止下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

网络编程,从socket到epoll

网络编程,从socket到epoll 参考链接:https://www.bilibili.com/video/BV11Z4y157RY?p=2&spm_id_from=pageDriver socket基本知识: socket分类: socekt提供了流和数据报两种通信机制,即流socket和数据报socket。 简单的socket通信流程: 先...

WebService处理大数据量数据

在通过WebService处理大数据量数据时出现如下错误: soap fault: 运行配置文件中指定的扩展时出现异常。 ---> 超过了最大请求长度。 解决方法: 因为上传的文件大于系统默认配置的值,asp.net web service默认的请求长度是4M。 1、针对单个项目,只需修改Web.config就可以了: 修改配置可以在web.conf...

高德APP启动耗时剖析与优化实践(iOS篇)

前言最近高德地图APP完成了一次启动优化专项,超预期将双端启动的耗时都降低了65%以上,iOS在iPhone7上速度达到了400毫秒以内。就像产品们用后说的,快到不习惯。算一下每天为用户省下的时间,还是蛮有成就感的,本文做个小结。 (文中配图均为多才多艺的技术哥哥手绘)   启动阶段性能多维度分析 要优化,首先要做到的是对启动阶段的各个性能纬度做分析,...

epoll惊群原因分析

考虑如下情况(实际一般不会做,这里只是举个例子): 在主线程中创建一个socket、绑定到本地端口并监听 在主线程中创建一个epoll实例(epoll_create(2)) 将监听socket添加到epoll中(epoll_ctl(2)) 创建多个子线程,每个子线程都共享步骤2里创建的同一个epoll文件描述符,然后调用epoll_wait(2)等待事...

进程的Binder线程池工作过程

copy from : http://gityuan.com/2016/10/29/binder-thread-pool/ 基于Android 6.0源码剖析,分析Binder线程池以及binder线程启动过程。 frameworks/base/cmds/app_process/app_main.cpp frameworks/native/libs/bin...

springboot配置 Druid , yml格式

datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai u...