synchronized同步锁原理详解

Java对象头

JVM中对象头的结构有以下两种(以32位JVM为例):
普通对象的对象头结构
在这里插入图片描述
数组对象的对象头结构
在这里插入图片描述
其中Mark Word结构
在这里插入图片描述
64位虚拟机 Mark Word的结构
在这里插入图片描述
Mark Word这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄、锁状态标记位等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
在这里插入图片描述
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向管程Monitor的指针。

Monitor

Monitor被翻译为监视器或管程。
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级锁)之后,该对象头中的Mark Word就被设置为指向Monitor对象的指针 。
在这里插入图片描述
Owner:用于记录当前Monitor的所属线程
EntryList:是一个链表结构,用于记录阻塞在当前锁对象上的线程
当对象锁发生锁竞争时,在同一时刻只有一个线程能够获取到锁,其他线程会进入阻塞(BLOCKED)状态,此时这些被阻塞的线程就会进入EntryList中等待锁持有者释放锁后被唤醒,再次参与锁竞争(非公平)。当Thread-2持有锁时,Thread-3、Thread-4等均无法获取锁从而进入阻塞队列,等待Thread-2执行完同步代码块之后通知阻塞队列中等待的线程重新竞争锁,竞争成功的线程成为锁拥有者,失败的线程继续在阻塞队列中阻塞。
WaitSet:用于记录获取锁之后进入Waiting状态的线程
当对象获取到锁之后,由于某些资源并未准备完成,需要等待其他线程去准备资源,此时线程会通过wait()/notify()等方法进入等待/通知模式,在这种情况下线程释放锁之后会进入WaitSet,当其他线程准备好资源之后会通知WaitSet中等待的线程,WaitSet中的线程会进入到EntryList中,重新参与锁竞争。
注意:
● synchronized 必须是进入同一个对象的 monitor 才有上述的效果
● 不加 synchronized 的对象不会关联监视器,不遵从以上规则

synchronized的作用

synchronized 通过当前线程持有对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,从而保证线程安全。synchronized 可以保证线程的可见性,synchronized 属于隐式锁,锁的持有与释放都是隐式的,我们无需干预。synchronized最主要的三种应用方式:
● 修饰实例方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁
● 修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
● 修饰代码块:指定加锁对象,进入同步代码库前要获得给定对象的锁

synchronized的可重入性

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

JVM对synchronized锁的优化

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,只能从低到高升级,不会出现锁的降级。重量级锁基于从操作系统的互斥量实现的,而偏向锁与轻量级锁不同,他们是通过CAS并配合Mark Word一起实现的。

轻量级锁

如果一个对象虽然有多个线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
假如有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
 synchronized( obj ) {
 // 同步块 A
 method2();
 }
}
public static void method2() {
 synchronized( obj ) {
 // 同步块 B
 }
}
  1. 在栈中创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word 。
    在这里插入图片描述
  2. 让锁记录中Object reference指向锁对象,并尝试用cas操作替换Object的Mark Word,将Mark Word的值存入Lock Record锁记录 。
    在这里插入图片描述
  3. 如果cas操作替换成功,对象头Mark Word中存储了锁记录地址和锁状态 00 ,表示由该线程给对象加锁,这时图示如下
    在这里插入图片描述
    如果cas操作失败,有两种情况:
    ● 如果是其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程升级为重量级锁
    ● 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数,并指向锁对象
  4. 当退出synchronized代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
    在这里插入图片描述
  5. 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
    ● 成功,则解锁成功
    ● 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
适用于连续多次都是同一个线程申请相同的锁的场景。偏向锁只有初始化的时候需要一次CAS操作,但如果出现其他线程竞争锁资源,那么偏向锁就会被撤销,并升级为轻量级锁。
在这里插入图片描述

偏向状态

● 一个对象创建时: 如果开启了偏向锁(默认开启),那么对象创建后,MarkWord值的最后3位为101,这时它的 thread、epoch、age 都为 0
● 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 - XX:BiasedLockingStartupDelay=0 来禁用延迟
● 如果没有开启偏向锁,那么对象创建后,markword 值的最后3位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值

偏向锁的废除

在 JDK6 中引入的偏向锁能够减少竞争锁定的开销,使得 JVM 的性能得到了显著改善,但是 JDK15 却将决定将偏向锁禁用,并在以后删除它,这是为什么呢?主要有以下几个原因:
● 为了支持偏向锁使得代码复杂度大幅度提升,并且对 HotSpot 的其他组件产生了影响,这种复杂性已成为理解代码的障碍,也阻碍了对同步系统进行重构
● 在更高的 JDK 版本中针对多线程场景推出了性能更高的并发数据结构,所以过去看到的性能提升,在现在看来已经不那么明显了。
● 围绕线程池队列和工作线程构建的应用程序,性能通常在禁用偏向锁的情况下变得更好。

锁自旋

在轻量级锁升级成为重量级锁之前,虚拟机会让当前想要获取锁的线程做几个空循环,在经过若干次循环后,如果得到锁(即这个时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况
在这里插入图片描述
自旋重试失败的情况
在这里插入图片描述
自旋会占用 CPU 时间,单核CPU自旋就是浪费,多核 CPU 自旋才能发挥优势。
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
Java 7 之后不能控制是否开启自旋功能

锁膨胀

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

  1. 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
    在这里插入图片描述
  2. 这时Thread-1加轻量级锁失败,进入锁膨胀流程
    ● 即为Object对象申请Monitor锁,让Object锁对象的Mark Word指向Monitor对象
    ● Monitor的Owner记录Thread-0线程
    ● 然后自己(Thread-1线程)进入Monitor的EntryList阻塞队列中
    在这里插入图片描述
  3. 当Thread-0退出同步块解锁时,使用cas操作将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor地址找到Monitor对象,设置Owner为 null,唤醒EntryList中BLOCKED线程

重量级锁

适用于多个线程同时执行同步代码块的场景,且锁竞争时间长。在这个状态下,未抢到锁的线程都会进入到 Monitor 中并阻塞在_WaitSet 中。

锁消除

消除锁属于编译器对锁的优化,JIT 编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译)会使用逃逸分析技术,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。

锁粗化

JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Java中,synchronized锁的升级是指在不同的场景下,锁的实现方式会有所不同,以提高性能和并发控制的效率。下面是synchronized锁的升级详解: 1. 偏向锁(Biased Locking):当一个线程访问同步块时,首先会尝试获取偏向锁。如果偏向锁未被其他线程占用,则当前线程会获得偏向锁,并标记为偏向线程ID。这样,在后续进入同步块时,无需再进行锁的竞争,提高了性能。只有当其他线程尝试获取偏向锁时,才会撤销偏向锁状态。 2. 轻量级锁(Lightweight Locking):当多个线程尝试竞争同一个锁时,偏向锁会升级为轻量级锁。轻量级锁使用CAS操作来实现加锁和解锁,避免了线程阻塞和唤醒的开销。如果CAS操作失败,表示存在竞争,锁会升级为重量级锁。 3. 重量级锁(Heavyweight Locking):当轻量级锁竞争失败时,锁会升级为重量级锁。重量级锁使用操作系统的互斥量(Mutex)来实现,被阻塞的线程会进入等待状态,直到锁被释放。重量级锁的竞争会导致线程的上下文切换和调度开销增加,性能较低。 synchronized锁的升级过程是根据实际情况和竞争情况动态进行的。在大多数情况下,锁的升级是逐级升级的,即从偏向锁到轻量级锁,再到重量级锁。这种锁的升级机制是为了在减少锁竞争时提供更好的性能,并在存在竞争时保证线程安全性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值