在JUC中 可以使用synchronized关键字进行加锁
如下所示
Object object = new Object();
synchronized (object){
// TODO
}
synchronized关键字所加的锁是逐步升级的,顺序是
无锁-> 偏向锁 -> 轻量级锁 -> 重量级锁、
随着锁等级的提高,所带来的消耗也会越大。
在介绍锁直接之前,我们需要先引入一些概念。
在java中,对象由对象头+实例数据+填充数据(可选)组成。而对象头=Marklass+KClass+Body。其中Klass被用来存储对象指向类元数据的指针,虚拟机通过这个指针确定该对象是哪个类的实例。MarkWord则用来存储一些对象自身运行时数据,根据机器不同有32位和64位之分。以64位为例,它在不同锁状态下的结构如下所示
下面将逐个介绍这些锁
偏向锁
首先是偏向锁,偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作。
偏向锁是默认开启的,并且是有延迟的,不会在程序启动时默认生效。原因是在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低。
- 使用jvm参数
-XX:BiasedLockingStartupDelay=0
可以禁用掉延迟 - 使用jvm参数
-XX:-UseBiasedLocking
可以禁用掉偏向锁
当锁对象第一次被线程获得进入偏向状态时,会使用CAS操作将线程ID(53位)记录到Markword中 而且Markwod中的biased_lock=1 锁标志位会变成01。也就是MarkWord的后三位会由正常状态下的001变成101。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作。当有其他线程来竞争锁对象时,偏向锁就会被撤销。
偏向锁的撤销
- 当锁对象的hashcode被调用后,偏向锁机会被撤销,原因是在markword中记录线程ID的字段占用了hashcode的字段(在未偏向时使用hashcode会导致直接成为轻量级锁,在已偏向时调用hashcode会直接升级为重量级锁)。
- 当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
- 调用 wait/notify时,notify会升级为轻量级锁,notify则会升级为重量级锁。
- 批量撤销 如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
- 批量重偏向 当撤销偏向锁阈值超过 20 次后,JVM 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程
- 批量撤销 当撤销偏向锁阈值超过 40 次后,JVM 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
轻量级锁
当一个对象有多个对象要加锁,但是时间是错开的,JVM会使用轻量级锁来优化
轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作,Java 6 才引入的偏向锁来优化。
加锁过程
-
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word
-
让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
-
如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁
-
如果CAS 失败 则有两种情况
- 一是锁重入 添加一条 Lock Record 作为重入的计数
- 二是其他线程已经持有该对象的轻量级锁 这时表明存在竞争 会将轻量级锁升级为重量级锁。
- 一是锁重入 添加一条 Lock Record 作为重入的计数
解锁过程
- 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
- 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
重量级锁
当发生多个线程同时竞争时,轻量级锁就会升级为重量级锁,重量级锁的实现依赖于Monitor
Montior
Monitor 被翻译为监视器或管程。每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。
Montior由Owner EntryList WaitSet三部分组成
轻量级锁升级(锁膨胀)
在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
-
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
-
Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED
锁自旋
重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁
注意:
- 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势
- 自旋失败的线程会进入阻塞状态
优点:不会进入阻塞状态,减少线程上下文切换的消耗
缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源
自旋锁说明:
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
- Java 7 之后不能控制是否开启自旋功能,由 JVM 控制
参考
本文内容参考以下内容
https://blog.csdn.net/Xin_101/article/details/117568632