Typecho合集站

一个喜欢Typecho站长搭建的站点,为能服务更多typecho用户

[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....

在开始介绍今天的内容之前,我们先来认识java中的

对象
,对象想必大家都已经很熟悉,但是大家有想过java对象在内存中"长"什么样吗??它是怎么组成的吗??

前言

java对象内存布局

java对象在内存中分为3部分,分别是对象头(Oject Header),实例数据(Instance Data),补齐填充(Padding)
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....
(1)对象头(Object Header)

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

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

普通对象的对象头:(包括

MarkWord
klass word
两部分)
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....

数组对象的对象头:(包括

MarkWord
klass word
数组长度
三部分)

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

[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....

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

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

[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....
MarkWord的状态变化(64位Hotspot虚拟机下)
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....

(2)实例数据(Instance Data)

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

(3)对齐填充(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中,等到解锁时才取出;
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....
图解:

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

(2)轻量级锁

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

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

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

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

[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....

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

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....

自旋

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

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

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

自旋重试成功的情况
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....
自旋重试失败的情况
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....
注意:自旋会占用 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 让它输出更为简洁)
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....
不设置延迟为0
如果不设置延迟为0,那么新建的对象是处于正常状态,因为此时偏向锁还未生效,后3位为001,如果使当前线程睡眠4s,再新建一个对象,会发现此时偏向锁已经生效,后3位为101
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....
设置延迟为0
禁用延迟后,偏向锁在程序启动时就生效,因此新建的两个对象都是处于可偏向状态
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....
但我们发现,以上除了后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()方法时:会禁用偏向锁???

[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....
通过图可以发现确实禁用了偏向锁,因为在加锁之前,Markword的后3位是001,代表正常状态,并且填充了31位的哈希码
[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....

那这是为什么呢??为什么调用了对象的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()是在重量级锁中使用的方法,因此如果使用了这两个方法,表明偏向锁已经撤销

[博客大赛]图文并茂!!讲解JUC重量级锁、轻量级锁、自旋、锁膨胀....

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;
}


上一篇 : javascript中的闭包closure详解
下一篇 : Nginx共享内存剖析及开源项目分享

发表新评论