深入理解 synchronized 关键字(关于对象头和锁的升级)


深入理解 volatile 关键字

synchronized 关键字

1. 概述

⾸先需要明确的⼀点是:Java多线程的锁都是基于对象的,Java中的每⼀个对象都可以作为⼀个锁。
还有⼀点需要注意的是,我们常听到的类锁其实也是对象锁。Java类只有⼀个Class对象(可以有多个实例对象,多个实例共享这个Class对象),而Class对象也是特殊的Java对象。所以我们常说的类锁,其实就是Class对象的锁。

说到锁,我们通常会谈到 synchronized 这个关键字。它翻译成中⽂就是“同步”的意思。
我们通常使⽤ synchronized 关键字来给⼀段代码或⼀个⽅法上锁。它通常有以下三种形式:

	
// 关键字在实例⽅法上,锁为当前实例
public synchronized void instanceLock() {
 // code
}
// 关键字在静态⽅法上,锁为当前Class对象
public static synchronized void classLock() {
 // code
}
// 关键字在代码块上,锁为括号⾥⾯的对象
public void blockLock() {
 Object o = new Object();
 synchronized (o) {
 // code
 }
}

2. synchronized 的四种锁状态

Java 6 为了减少获得锁和释放锁带来的性能消耗,引⼊了“偏向锁”和“轻量级锁“。

在Java 6 以前,所有的锁都是”重量级“锁。所以在Java 6 及其以后,⼀个对象其实有四种锁状态,它们级别由低到⾼依次是:

  1. ⽆锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

⽆锁就是没有对资源进⾏锁定,任何线程都可以尝试去修改它,⽆锁在这⾥不再细讲。
⼏种锁会随着竞争情况逐渐升级,锁的升级很容易发⽣,但是锁降级发⽣的条件会⽐较苛刻,锁降级发⽣在 Stop The World 期间,当JVM进⼊安全点的时候,会检查是否有闲置的锁,然后进⾏降级。

关于锁降级有两点说明:
1.不同于⼤部分⽂章说锁不能降级,实际上HotSpot JVM 是⽀持锁降级的,
2.上⾯提到的Stop The World期间,以及安全点,这些知识是属于JVM的知识范畴,本⽂不做细讲。
2.1 Java对象头

前⾯我们提到,Java的锁都是基于对象的。⾸先我们来看看⼀个对象的“锁”的信息是存放在什么地⽅的。
每个Java对象都有对象头。如果是⾮数组类型,则⽤2个字宽来存储对象头如果是数组,则会⽤3个字宽来存储对象头。在32位处理器中,⼀个字宽是32位;在64位虚拟机中,⼀个字宽是64位。对象头的内容如下表:
在这里插入图片描述
我们主要来看看Mark Word的格式:
在这里插入图片描述

可以看到,当对象状态为偏向锁时, Mark Word 存储的是偏向的线程ID;当状态为轻量级锁时, Mark Word 存储的是指向线程栈中 Lock Record 的指针;当状态为重量级锁时, Mark Word 为指向堆中的 monitor 对象的指针。

2.2 偏向锁

Hotspot的作者经过以往的研究发现⼤多数情况下锁不仅不存在多线程竞争,⽽且总是由同⼀线程多次获得,于是引⼊了偏向锁。

偏向锁会偏向于第⼀个访问锁的线程,如果在接下来的运⾏过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,偏向锁在资源⽆竞争情况下消除了同步语句,连CAS操作都不做了,提⾼了程序的运⾏性能。

⼤⽩话就是对锁置个变量,如果发现为true,代表资源⽆竞争,则⽆需再⾛各种加锁/解	
锁流程。如果为false,代表存在其他线程竞争资源,那么就会⾛后⾯的流程。

偏向锁实现原理

⼀个线程在第⼀次进⼊同步块时,会在对象头和栈帧中的锁记录⾥存储锁的偏向的线程ID。当下次该线程进⼊这个同步块时,会去检查锁的Mark Word⾥⾯是不是放的⾃⼰的线程ID。

如果是,表明该线程已经获得了锁,以后该线程在进⼊和退出同步块时不需要花费CAS操作来加锁和解锁 ;

如果不是,就代表有另⼀个线程来竞争这个偏向锁。这个时候会尝试使⽤CAS来替换Mark Word⾥⾯的线程ID为新线程的ID,这个时候要分两种情况:

  • 成功,表示之前的线程不存在了, Mark Word⾥⾯的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;

  • 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的⽅式进⾏竞争锁。

CAS: Compare and Swap
⽐较并设置。⽤于在硬件层⾯上提供原⼦性操作。在 Intel 处理器中,⽐较并
交换通过指令cmpxchg实现。 ⽐较是否和给定的数值⼀致,如果⼀致则修
改,不⼀致则不修改。

线程竞争偏向锁
在这里插入图片描述

图中涉及到了lock record指针指向当前堆栈中的最近⼀个lock record,是轻量级锁按照先来先服务的模式进⾏了轻量级锁的加锁。

偏向锁的升级

偏向锁会等到有竞争情况下时才会进行升级。

在偏向锁升级的过程中,会先暂停持有偏向锁的线程,将偏向锁升级为轻量级锁,最后再将其唤醒,这个过程还是比较耗费资源的。

  1. 在⼀个安全点(在这个时间点上没有字节码正在执⾏)停⽌拥有锁的线程。
  2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和Mark Word,使其变成
    ⽆锁状态。
  3. 唤醒被停⽌的线程,将当前锁升级成轻量级锁。

所以如果应用程序中的锁一般处于竞争状态,可以关闭偏向锁,让其默认开始时为轻量级锁
-XX:UseBiasedLocking=false

在这里插入图片描述

2.3 轻量级锁

轻量级锁主要使用自旋的方式来进行加锁。

⾃旋:不断尝试去获取锁,⼀般⽤循环来实现。

轻量级锁的加锁

JVM会为每个线程在当前线程的栈帧中创建⽤于存储锁记录的空间,我们称为Displaced Mark Word。如果⼀个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到⾃⼰的Displaced Mark Word⾥⾯。

然后线程尝试⽤CAS将锁的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使⽤⾃旋来获取锁。

⾃旋是需要消耗CPU的,如果⼀直获取不到锁的话,那该线程就⼀直处在⾃旋状态,⽩⽩浪费CPU资源。解决这个问题最简单的办法就是指定⾃旋的次数,例如让其循环10次,如果还没获取到锁就进⼊阻塞状态。

但是JDK采⽤了更聪明的⽅式——适应性⾃旋,简单来说就是线程如果⾃旋成功了,则下次⾃旋的次数会更多,如果⾃旋失败了,则⾃旋的次数就会减少。

⾃旋也不是⼀直进⾏下去的,如果⾃旋到⼀定程度(和JVM、操作系统相关),依然没有获取到锁,称为⾃旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。

在这里插入图片描述

2.4 重量级锁

重量级锁依赖于操作系统的互斥量(mutex) 实现的,⽽操作系统中线程间状态的转换需要相对⽐较⻓的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。

前⾯说到,每⼀个对象都可以当做⼀个锁,当多个线程同时请求某个对象锁时,对象锁会设置⼏种状态⽤来区分请求的线程:

Contention List:所有请求锁的线程将被⾸先放置到该竞争队列
Entry List:Contention List中那些有资格成为候选⼈的线程被移到Entry List
Wait Set:那些调⽤wait⽅法被阻塞的线程被放置到Wait Set
OnDeck:任何时刻最多只能有⼀个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner
!Owner:释放锁的线程

当⼀个线程尝试获得锁时,如果该锁已经被占⽤,则会将该线程封装成⼀
个 ObjectWaiter 对象插⼊到Contention List的队列的队⾸,然后调⽤ park 函数挂起当前线程。

当线程释放锁时,会从Contention List或EntryList中挑选⼀个线程唤醒,被选中的线程叫做 Heir presumptive 即假定继承⼈,假定继承⼈被唤醒后会尝试获得锁,但 synchronized 是⾮公平的,所以假定继承⼈不⼀定能获得锁。这是因为对于重量级锁,线程先⾃旋尝试获得锁,这样做的⽬的是为了减少执⾏操作系统同步操作带来的开销。如果⾃旋不成功再进⼊等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平,还有⼀个不公平的地⽅是⾃旋线程可能会抢占了Ready线程的锁。

如果线程获得锁后调⽤ Object.wait ⽅法,则会将线程加⼊到WaitSet中,当
被 Object.notify 唤醒后,会将线程从WaitSet移动到Contention List或EntryList中去。需要注意的是,当调⽤⼀个锁对象的 wait 或 notify ⽅法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

2.5 总结锁的升级流程
  1. 首先检查自己 Mark Word 中的 ThreadId 是否是自己的,如果是就表示当前是“偏向锁”
  2. 如果当前的 ThreadId 不是自己的,就进行锁升级,新的线程根据当前的ThreadId 来通知之前的线程,将其 ThreadId 置为空
  3. 两个线程都把锁对象的HashCode复制到⾃⼰新建的⽤于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarKword的内容修改为⾃⼰新建的记录空间的地址的⽅式竞争MarkWord。
  4. 第三步中成功执⾏CAS的获得资源,失败的则进⼊⾃旋 。
  5. ⾃旋的线程在⾃旋过程中,成功获得资源(即之前获的资源的线程执⾏完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果⾃旋失败。
  6. 进⼊重量级锁的状态,这个时候,⾃旋的线程进⾏阻塞,等待之前线程执⾏完成并唤醒⾃⼰
2.6 各个锁的优缺点对比

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_CX_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值