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

  • 前言
    • java对象内存布局
    • (1)重量级锁(Moniter)
    • (2)轻量级锁
    • 锁膨胀、自旋、自适应自旋
    • (3)偏向锁(以64位虚拟机为例)
    • 2.偏向锁撤销的3种情况
      • (1)当一个对象调用hashcode()方法时:会禁用偏向锁???
      • (2) 当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
      • (3)当使用wait(),notify()方法时,偏向锁也会撤销
    • 3.批量重偏向和批量撤销

在开始介绍今天的内容之前,我们先来认识java中的对象,对象想必大家都已经很熟悉,但是大家有想过java对象在内存中"长"什么样吗??它是怎么组成的吗??

前言

java对象内存布局

java对象在内存中分为3部分,分别是对象头(Oject Header),实例数据(Instance Data),补齐填充(Padding)

对象头(Object Header)

注意:
(1)这里暂不考虑指针压缩的场景
(2)这里都是针对HotSpot虚拟机进行介绍

  • MarkWord :用来存放一些标志位(比如锁的标记位)和hashcode,分代年龄等,在32位虚拟机下,MarkWord占4个字节,在64位虚拟机下,占8个字节
  • Klass Pointer(或者Class Pointer) :用来存放该对象所对应的class类对象在方法区中的地址,在32位虚拟机下占4字节,在64位虚拟机下占8字节;
  • Length :如果对象是数组对象,那么对象头中还有一个Length字段,用来记录数组的长度,占4个字节;

(这里以HotSpot虚拟机为32位的情况举例)

普通对象的对象头:(包括MarkWordklass word两部分)

数组对象的对象头:(包括MarkWordklass word数组长度三部分)

JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

MarkWord的状态变化(32位Hotspot虚拟机下)

在正常状态下(也即是无锁状态下):
32位虚拟机中的MarkWord包含了25位的hashcode,4位的分代年龄,1位偏向锁标志位,2位锁标志位;


MarkWord的状态变化(64位Hotspot虚拟机下)

实例数据(Instance Data)

实例数据部分是对象真正存储的有效信息,也就是我们在代码里面所定义的各种类型的字段内容(成员变量),无论是从父类继承下来的,还是在子类中定义的都需要记录下来;

对齐填充(Padding)

第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。对象头正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

在认识了对象头之后,我们接下来介绍JAVA中的几种锁以及优化,在此之前,我们有必要知道以下几点:

三者 优先级:
(上锁消耗的资源越少,优先级越高)

(1)偏向锁 > 轻量级锁 > 重量级锁 (这3个锁都是用synchronized关键字来上锁)

(2)轻量级锁的使用场景:一个对象有多个线程需要加锁,但是加锁的时间是错开的(即没有竞争),此时可以使用轻量级锁来优化;

(3)偏向锁的使用场景:轻量级锁在没有竞争,每次重入仍然需要CAS操作,Java 6 之后引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

1 .在Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗,引入了"偏向锁"和"轻量级锁",在Java SE 1.6中,锁一共有4中状态,级别从低到高依次是:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态,这几个状态会随着竞争情况逐渐升级;锁可以升级但是不能降级,意味着偏向锁升级为轻量级锁后不能降级成偏向锁.这种锁升级确不能降级的策略,目的是为了提高获得锁和释放锁的效率;

2.所谓的重量级锁和轻量级锁,是从性能消耗方面来命名的;轻量级锁是JDK1.6引入的,“轻量级"是相对于使用操作系统互斥量来实现的传统锁(重量级锁)而言的;重量级锁由于加锁和解锁性能消耗大,并且如果发生锁竞争,会发生线程的阻塞和唤醒,这个操作是借助操作系统的系统调用,然而系统调用会涉及到内核态和用户态的切换,因此正是由于重量级锁的性能消耗大,我们称作"重”,而轻量级锁相比较而言性能消耗较少,我们称作"轻";
轻量级锁并不是用来替代重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗

3.轻量级锁不会发生自旋,重量级锁才会有自旋
点击链接查看源码分析

(1)重量级锁(Moniter)

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中前62位就会被设置为 Monitor 对象的地址,后两位表示此时对象的状态,对象头中的信息暂时保存在Moniter中,等到解锁时才取出;

图解:

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一
    个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入
    EntryList进而BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲
    wait-notify 时会分析

注意:
synchronized 必须是进入同一个对象的 monitor 才有上述的效果
不加 synchronized 的对象不会关联监视器,不遵从以上规则

(2)轻量级锁

线程在执行同步代码块之前,JVM会在当前线程的中栈帧中创建用于存储锁记录(Lock Record)的空间,并将对象
轻量级锁对使用者是透明的,即语法仍然是 synchronized

  • (1)创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
  • (2)让锁记录中 Object reference 指向锁对象,并尝试用 CAS(原子操作) 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
  • (3)如果 cas 替换成功,对象头中存储了锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
  • (4)如果 cas 失败,有两种情况
    1 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    2 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

解释:所谓锁重入,就是给一个对象上锁多次,每一次重入都要进行CAS操作,当发现对象头中已经有锁记录的地址了,则添加一条Lock Record 作为重入的计数

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();public static void method1() {synchronized( obj ) {// 同步块 Amethod2();}
}public static void method2() {synchronized( obj ) {// 同步块 B}
}

  • (5)当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

    当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头;
    如果操作成功,则解锁成功
    如果操作失败,说明轻量级锁进行了锁膨胀或已经升级为了重量级锁,那么此时进入重量级锁解锁流程

锁膨胀、自旋、自适应自旋

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

自旋

自旋是让CPU执行一些没有意义的指令,目的是让线程不放弃对CPU的占用

重量级锁竞争的时候,还可以使用自旋来进行优化;

正常情况下锁获取失败就应该阻塞入队,但是有时候可能刚一阻塞,别的线程就释放锁了,然后再唤醒刚刚阻塞的线程,这就没必要了。
所以在线程竞争不是很激烈的时候,稍微自旋一会儿,指不定不需要阻塞线程就能直接获取锁,这样就避免了不必要的开销,提高了锁的性能。
如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞

自旋重试成功的情况

自旋重试失败的情况

注意:自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

自适应自旋

但是自旋的次数又是一个难点,在竞争很激烈的情况,自旋就是在浪费 CPU,因为结果肯定是自旋一会让之后阻塞。

在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
Java 7 之后不能控制是否开启自旋功能

(3)偏向锁(以64位虚拟机为例)

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

几点说明:

  1. 在JDK1.6中.偏向锁是默认开启的,我们可以通过在VMoptions中设置参数(-XX:-UseBiasedLocking)来关闭偏向锁;

  2. 偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以在VMoptions中设置参数:-XX:BiasedLockingStartupDelay=0来禁用延迟

  3. 如果我们禁用了偏向锁,那么给对象上锁时,会加轻量级锁

我们通过以下实验来测试一下相关说明:

这里利用 jol 第三方工具来查看对象头信息(注意这里扩展了 jol 让它输出更为简洁)

不设置延迟为0
如果不设置延迟为0,那么新建的对象是处于正常状态,因为此时偏向锁还未生效,后3位为001,如果使当前线程睡眠4s,再新建一个对象,会发现此时偏向锁已经生效,后3位为101

设置延迟为0
禁用延迟后,偏向锁在程序启动时就生效,因此新建的两个对象都是处于可偏向状态

但我们发现,以上除了后3位有值外,其余的61位都是0,那是因为此时的对象处于可偏向状态,我们还没有给对象上锁,所以这时它的 thread、epoch、age 都为 0

现在给对象上锁:
// 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0

class Dog {}public static void main(String[] args) throws IOException {Dog d = new Dog();ClassLayout classLayout = ClassLayout.parseInstance(d);new Thread(() -> {log.debug("synchronized 前");System.out.println(classLayout.toPrintableSimple(true)); synchronized (d) {log.debug("synchronized 中");System.out.println(classLayout.toPrintableSimple(true));}log.debug("synchronized 后");System.out.println(classLayout.toPrintableSimple(true));     }, "t1").start();
}

输出:

11:08:58.117 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101

注意:这里我们发现解锁后,MarkWord没有变化,这里就体现的是偏向锁的"偏"的特性:这个被上锁的对象,会偏向于第一次给他上锁的线程,即使退出了同步代码块,该对象的MarkWord中仍然会存储该线程ID(注意:这里的线程ID是JVM中的ID,不同与操作系统中线程ID),直到有其他线程使用该对象或者其他条件发生时才会改变

测试禁用偏向锁:在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

11:13:10.018 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
11:13:10.021 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
11:13:10.021 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

会发现此时进入Synchronized代码块,打印的后3位为000,说明此时给对象加的是轻量级锁

所以:如果我们禁用了偏向锁,那么给对象上锁时,会加轻量级锁

2.偏向锁撤销的3种情况

(1)当一个对象调用hashcode()方法时:会禁用偏向锁???


通过图可以发现确实禁用了偏向锁,因为在加锁之前,Markword的后3位是001,代表正常状态,并且填充了31位的哈希码

那这是为什么呢??为什么调用了对象的hashcode方法后,偏向锁就失效了呢???

我们知道在对象头的MarkWord中有一个字段是用来存放线程ID的;
当对象进入偏向状态的时候,MarkWord的中的54bit会拿来存放持有锁的线程ID,那原来对象的哈希码怎么办呢???

如果是轻量级锁会在锁记录中记录 hashCode
如果是重量级锁会在 Monitor 中记录 hashCode
如果是偏向锁会在当前对象的Markword中记录hashcode

对象新建时哈希码默认是0;当对象第一次调用hashcode()方法时才会产生哈希码,并填充到MarkWord中;
此时64位的MarkWord已经被占用了31位了,如果此时要开启偏向锁,那么就还得在MarkWord拿54位来存储线程ID号,显然空间不够,因此JVM规定了如果一个可偏向对象调用了hashcode方法,会使偏向锁失效;

我们这里是可偏向对象调用hashcode方法,那如果是已经处于偏向状态的对象,又收到需要计算其一致性哈希码请求时1。它的偏向状态会立即被撤销,并且锁会膨胀为重量级锁;在重量级锁的实现中,代表重量级锁的Moniter对象会存放非加锁状态下(标志位为"01")的MarkWord,其中自然可以存放原来的hashcode

(2) 当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

当一个对象加锁时,默认是加的偏向锁,当解锁时(syncronized中代码执行完),此时线程的ID号仍然会留在对象头的Markword中,因此当另一个线程再给这个对象加锁时,偏向锁就会失效,并升级为轻量级锁

(3)当使用wait(),notify()方法时,偏向锁也会撤销

wait()和notify()是在重量级锁中使用的方法,因此如果使用了这两个方法,表明偏向锁已经撤销

3.批量重偏向和批量撤销

JVM 基于一种启发式的做法判断是否应该触发批量撤销或批量重偏向

依赖三个阈值作出判断:

# 批量重偏向阈值
-XX:BiasedLockingBulkRebiasThreshold=20
# 重置计数的延迟时间
-XX:BiasedLockingDecayTime=25000
# 批量撤销阈值
-XX:BiasedLockingBulkRevokeThreshold=40

简单总结,对于一个类,按默认参数来说:
单个偏向撤销的计数达到 20,就会进行批量重偏向。
距上次批量重偏向 25 秒内,计数达到 40,就会发生批量撤销。

每隔 (>=) 25 秒,会重置在 [20, 40) 内的计数为0,这意味着可以发生多次批量重偏向。

注意:对于一个类来说,批量撤销只能发生一次,因为批量撤销后,该类禁用了可偏向属性,后面该类的对象都是不可偏向的,包括新创建的对象。

启发式源码:

static HeuristicsResult update_heuristics(oop o, bool allow_rebias) {markOop mark = o->mark();// 如果不是偏向模式直接返回if (!mark->has_bias_pattern()) {return HR_NOT_BIASED;}// 获取锁对象的类元数据Klass* k = o->klass();// 当前时间jlong cur_time = os::javaTimeMillis();// 该类上一次批量重偏向的时间jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time();// 该类单个偏向撤销的计数int revocation_count = k->biased_lock_revocation_count();// 按默认参数来说:// 如果撤销计数大于等于 20,且小于 40,// 且距上次批量撤销的时间大于等于 25 秒,就会重置计数。if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&(revocation_count <  BiasedLockingBulkRevokeThreshold) &&(last_bulk_revocation_time != 0) &&(cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {// This is the first revocation we've seen in a while of an// object of this type since the last time we performed a bulk// rebiasing operation. The application is allocating objects in// bulk which are biased toward a thread and then handing them// off to another thread. We can cope with this allocation// pattern via the bulk rebiasing mechanism so we reset the// klass's revocation count rather than allow it to increase// monotonically. If we see the need to perform another bulk// rebias operation later, we will, and if subsequently we see// many more revocation operations in a short period of time we// will completely disable biasing for this type.k->set_biased_lock_revocation_count(0);revocation_count = 0;}if (revocation_count <= BiasedLockingBulkRevokeThreshold) {// 自增计数revocation_count = k->atomic_incr_biased_lock_revocation_count();}// 此时,如果达到批量撤销阈值,则进行批量撤销。if (revocation_count == BiasedLockingBulkRevokeThreshold) {return HR_BULK_REVOKE;}// 此时,如果达到批量重偏向阈值,则进行批量重偏向。if (revocation_count == BiasedLockingBulkRebiasThreshold) {return HR_BULK_REBIAS;}// 否则,仅进行单个对象的撤销偏向return HR_SINGLE_REVOKE;
}

  1. 这里说的请求应该来自于Object::hashcode()或者System::identityHashCode(Object)方法的调用,如果重写了对象的hashcode方法,计算哈希码是并不会产生这里所说的请求 ↩︎

(JUC)图文并茂!!!! 超详细 偏向锁VS轻量级锁VS重量级锁VS自旋相关推荐

  1. 【剧前爆米花--爪哇岛寻宝】常见的锁策略——乐观锁、读写锁、重量级锁、自旋锁、公平锁、可重入锁等

    作者:困了电视剧 专栏:<JavaEE初阶> 文章分布:这是关于操作系统锁策略的文章,包括乐观锁.读写锁.重量级锁.自旋锁.公平锁.可重入锁等,希望对你有所帮助! 目录 乐观锁和悲观锁 悲 ...

  2. 轻量级锁_一句话撸完重量级锁、自旋锁、轻量级锁、偏向锁、悲观、乐观锁等各种锁 不看后悔系列...

    重量级锁?自旋锁?自适应自旋锁?轻量级锁?偏向锁?悲观锁?乐观锁?执行一个方法咋这么辛苦,到处都是锁. 今天这篇文章,给大家普及下这些锁究竟是啥,他们的由来,他们之间有啥关系,有啥区别. 重量级锁 如 ...

  3. 前缀和与差分 (图文并茂 超详细整理)

    可以看一下这篇文章,链接如下.博主超级厉害,关于前缀和与差分整理的特别详细. 版权声明:本文转载自CSDN博主「林深时不见鹿」的原创文章,遵循CC 4.0 BY-SA版权协议,再次转载请附上原文出处链 ...

  4. 前缀和与差分 图文并茂 超详细整理(全网最通俗易懂)

    目录 1.前缀和 2.前缀和算法有什么好处? 3.二维前缀和 4.差分 5.一维差分 6.二维差分 1.前缀和 前缀和是指某序列的前n项和,可以把它理解为数学上的数列的前n项和,而差分可以看成前缀和的 ...

  5. java多线程之锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁

    转载至:https://blog.csdn.net/zqz_zqz/article/details/70233767 之前做过一个测试,详情见这篇文章<多线程 +1操作的几种实现方式,及效率对比 ...

  6. 锁升级过程(偏向锁/轻量级锁/重量级锁)

    锁的前置知识 如果想要透彻的理解java锁的来龙去脉,需要先了解锁的基础知识:锁的类型.java线程阻塞的代价.Markword. 锁的类型 锁从宏观上分类,分为悲观锁与乐观锁. 乐观锁 乐观锁是一种 ...

  7. Java锁---偏向锁、轻量级锁、自旋锁、重量级锁

    Java锁-偏向锁.轻量级锁.自旋锁.重量级锁 之前做过一个测试,反复执行过多次,发现结果是一样的: 单线程下synchronized效率最高(当时感觉它的效率应该是最差才对): AtomicInte ...

  8. 12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁

    小陈:呼叫老王...... 老王:来了来了,小陈你准备好了吗?今天我们来讲synchronized的锁重入.锁优化.和锁升级的原理 小陈:早就准备好了,我现在都等不及了 老王:那就好,那我们废话不多说 ...

  9. 偏向锁、轻量级锁、重量级锁的区别和解析

    为了换取性能,JVM在内置锁上做了非常多的优化,膨胀式的锁分配策略就是其一.理解偏向锁.轻量级锁.重量级锁的要解决的基本问题,几种锁的分配和膨胀过程,有助于编写并优化基于锁的并发程序. 内置锁的分配和 ...

最新文章

  1. Java集合LinkedHashMap
  2. openssl qt 生成秘钥_关于openssl作的rsa生成密钥及加解密
  3. MySQL FIND_IN_SET(s1,s2) 返回在字符串s2中与s1匹配的字符串的位置
  4. 【Hibernate】could not instantiate class.. from tuple] with root cause
  5. 2_5 BridgeMode.cpp 桥接模式
  6. 环球网-王坚《在线》:用20万字讲清楚三个词
  7. Python爬虫利器一Requests库的用法
  8. oracle 加全文索引,Oracle创建全文索引
  9. 数码管显示实验一 编写程序让8只数码管同时显示零
  10. SpringSecurity自定义登陆页面和跳转页面
  11. 新来个专家吐槽我们:连qps都不懂,靠谱吗?
  12. scala 学习笔记一 列表List
  13. 认真学习系列:编译原理——B站笔记
  14. 图片以base64格式显示出来
  15. Matlab中imhist函数的使用及图像直方图的概念
  16. 算法-数组拆分为奇偶两部分
  17. CHM格式的帮助文档制作与代码调用 Visual Studio C#
  18. 大学生面试 4个问题千万别撒谎
  19. 掌握Android图像显示原理(上)
  20. 物联网信息安全复习笔记(从头开始,两天速成)

热门文章

  1. 爬取裁判文书网(一)
  2. “AI+教育”假套路还是真功夫,本质还是对AI能力的拷问
  3. mysql常用的tamper脚本,Sqlmap Tamper绕过脚本详解
  4. 路由器和电脑IP地址、端口号、网卡mac查询方式
  5. Nginx之URL重写
  6. 2012二级c语言上机,2012全国计算机等级考试-二级C语言-上机考试-填空题-分类总结...
  7. 图解电动汽车:电动汽车电控系统
  8. try {}里有一个 return 语句,那么紧跟在这个 try 后的 finally {}里的 code 会不会被执行,什么时候被执行,在 return 前还是后?
  9. Office Web apps可以利用Excel Web JavaScript编程
  10. 根据已经commit的数据,进行leader和peon之间的同步