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

    先引入几个概念

    1. 对象头

    Java中,每个对象都含有一个对象头,用于保存一些额外信息,以32位JDK为例,对象头长度为4byte,其结构如下

    锁状态

    25 bit

    4bit

    1bit

    2bit

    23bit

    2bit

    是否是偏向锁

    锁标志位

    轻量级锁

    指向栈中锁记录的指针

    00

    重量级锁

    指向互斥量(重量级锁)的指针

    10

    GC标记

    11

    偏向锁

    线程ID

    Epoch

    对象分代年龄

    1

    01

    无锁

    对象的hashCode

    对象分代年龄

    0

    01

    参见hotspotsrcsharevmoopsmarkOop.hpp

    2. lock record 

    参见hotspotsrcsharevm untimeasicLock.hpp

    其关键代码如下

    class BasicLock VALUE_OBJ_CLASS_SPEC {
      friend class VMStructs;
     private:
      volatile markOop _displaced_header;//对象头
    .........................
    }
    
    class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
      friend class VMStructs;
     private:
      BasicLock _lock;                                    // the lock, must be double word aligned
      oop       _obj;                                     // object holds the lock;
    ..........................
    }

    也就是BasicObjectLock这个class了,其中记录了锁对象的mark word,与锁对象的指针

    重量级锁

    这是最重的锁了,直接使用操作系统自带的互斥量来实现

    当线程A拥有针对某个对象的重量级锁时,对象头的mark word中保存了指向互斥量的指针,锁状态也被设置为10

    如果此时线程B试图获取这个对象的锁,在检测到mark word的状态后,线程B会找到对应的互斥量,将自己注册到这个互斥量的等待队列中,然后挂起自身

    线程A解锁时,会唤醒互斥量中等待队列里的线程B,使其可以占用这个对象的锁

    轻量级锁

    利用系统互斥量是一个很重的操作,根据经验规律我们可以知道:大部分的锁竞争都不激烈,很多情况下锁对象虽然会被多线程使用,但是线程之间不会发生冲突,针对这种情况就有了轻量级锁的优化

    所谓的轻量级锁,就是当一个线程试图获取某个Object上的锁时,不是直接调用很重的mutex,而是

    1. 先在这个线程的栈帧中创建一个lock record,然后将对象头的mark word复制到这个lock word里

    2. cas的修改对象头的mark word,在mark word里写入指向这个线程的指针,并将锁标记位改写成00。写入成功,表示这个对象上加了轻量级的锁,跳转至3。如果写入不成功,表明这个对象被其他的线程以轻量级锁锁住,跳转至5

    3. 执行操作,跳转至4

    4. 检查对象头的mark word,如果锁状态还是00,那么表明占用期间没有发生争用,可以放心解锁,也就是把栈帧中保存的mark word用cas替换到对象头中。如果锁状态不为00,说明发生了锁争用,轻量级锁已经膨胀成为了重量级锁,现在对象头的mark word里保存的是指向互斥量的指针,走一般的重量级锁的解锁流程即可。

    5. 考虑到大部分的锁争用只发生很短时间,先原地自旋若干次,如果还是不能获取锁,就执行锁膨胀操作,将这个对象的轻量级锁升级为重量级锁。具体操作是先申请一个mutex,将对象头的mark word中的指针指向这个mutex,锁状态改写成10,然后将自己放入等待队列,然后挂起自己。从第4步中我们可以知道,当前占有锁的线程在执行完毕之后,会发现这个锁已经膨胀,这个等待中的线程也就会被唤醒。

    ps. 如果发生锁重入,线程会在栈帧中再次创建lock record,此时lock record中只记录指向锁对象的指针,mark word位直接置为0。重入锁解锁时,发现mark word位为0的情况,只会删除这个lock record,不会对对象头做任何操作

    可以看到,在低竞争的情况下,轻量级锁用轻量级的cas操作替代了重量级的mutex操作,减少了系统开销

    偏向锁

    根据统计,在实际情况下,大部分的锁对象永远只被一个线程占用,那么在这种情况下,轻量级锁在每次monitorenter和monitorexit的时候(非重入),都会进行一次cas操作,为了进一步减少cas操作,偏向锁(biased locking)诞生了。

    所谓的偏向锁,是指某个对象一经加锁,在不发生争用的情况下,mark word里的指针永远是偏向这个线程的,那么在不发生锁争用的情况下,线程每次进入临界区之前,只需要检查一下对象头中的mark word是否是指向自己即可,如果还是指向自己,那么在栈帧中申请一个lock record即可。这样就只用进行一次cas操作了。

    但是如果发生竞争该怎么办呢?那就要走revoke biased流程了:

    1. 看一眼持有锁的线程是否还活着,如果已经死了,那将对象设置为无锁状态就可以了

    2. 如果这个线程还活着,那需要遍历持有这个锁的线程的栈帧中的所有lock record,如果所有lock record都不指向锁对象,那么这个线程实际上不持有这个对象锁。同1,将对象设置为无锁状态即可

    3. 如果这个线程正在占用这个锁对象,那么需要修改线程中的锁记录与对象头,将它们都修改为轻量级锁状态,然后正常走轻量级锁的流程即可

    总结:

    偏,轻,重锁,分别解决三个问题

    偏:只有一个线程进入临界区

    轻:多个线程交替进入临界区

    重:多线程同时进入临界区

    ps.

    1. 如果对象被调用过native的hashCode方法,那么这个对象的对象头中的hashcode字段就有值了,那么这个对象就无法进入偏向锁状态,就算正处于偏向锁状态,那也要revoke baised了

    2. revoke baised是一个相当昂贵的操作,如果应用程序的锁争用极其激烈,偏向锁经常被revoke,那么直接关闭偏向锁可能反而会提高性能

    参考资料

    java锁优化

    JVM内部细节之一:synchronized关键字及实现细节(轻量级锁Lightweight Locking)

    聊聊并发(二)Java SE1.6中的Synchronized

    虚拟机中的锁优化简介(适应性自旋/锁粗化/锁削除/轻量级锁/偏向锁)

    openjdk对biased locking的官方文档

    一篇详细讲解了biased locking的论文

    LockSupport.park()实现分析

  • 相关阅读:
    sql 删除默认索引,对象 依赖于 列,由于一个或多个对象访问此列
    sql 重复数据查询
    Sql 查询结果 根据某个字段值 变更另外一个字段值 case when
    使用ABP框架踩过的坑系列3
    使用ABP框架踩过的坑系列5
    使用ABP框架踩过的坑系列4
    使用ABP框架踩过的坑系列2
    使用ABP框架踩过的坑系列1
    java rest框架jersey数组单记录问题解决
    测试工程师面试常见逻辑题
  • 原文地址:https://www.cnblogs.com/stevenczp/p/6278522.html
Copyright © 2011-2022 走看看