【<synchronized锁升级>】

[1] Java对象头

我们以 Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据:Mark Word(标记字段) 和 Klass Pointer(类型指针)

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

在上面中我们知道了,synchronized 用的锁是存在Java对象头里的,那么具体是存在对象头哪里呢?答案是:存在锁对象的对象头的Mark Word中,那么MarkWord在对象头中到底长什么样,它到底存储了什么呢?

在64位的虚拟机中:

【<synchronized锁升级>】
在32位的虚拟机中:
【<synchronized锁升级>】

https://*.com/questions/26357186/what-is-in-java-object-header

[2] Mark word 结构

默认(Normal):hashcode(地址码);age(分代中的年龄);biased—lock(是不是偏向锁),01(加锁状态:表示没有和任何monitor关联)

重量级锁(Normal):ptr_to_heavyweight_moniter(指向锁的地址),10(加锁状态:已经与monitor关联)轻轻量级锁(Normal):ptr_to_lock_record(锁记录的地址),00(加锁状态:轻量级锁)

锁状态 存储内容 标志位
无锁 对象的hashCode、对象分代年龄、是否是偏向锁(0) 01
偏向锁 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量的指针 11

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FGEqmXmt-1596596841013)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200714201707252.png)]

[3] Monitor

Monitor 被翻译为监视器管程每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k2X8IIR1-1596596841015)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200714200132363.png)]

  • 刚开始 Monitor 中 Owner 为 null

  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,

  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入

EntryList BLOCKED (Monitor中只能有一个 Owner)

  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的

  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲

wait-notify 时会分析

https://www.jianshu.com/p/c3313dcf2c23

① owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
② _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
③ _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中
④ _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中

synchronized 必须是进入同一个对象的 monitor 才有上述的效果

不加 synchronized 的对象不会关联监视器,不遵从以上规则

[4] synchronized锁升级
无锁

不通过阻塞的方式来访问并修改资源。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功,CAS就是一个无锁的形式。

偏向锁(可关闭)

偏向锁是指一段同步代码仅仅被一个线程所访问时,那么会给对象加偏向锁。简单来说:第一次使用时,使用CAS将线程ID存储到mark word上,之后测试这个锁是自己的就不需要再竞争了。

加锁

  1. 访问Mark Word中偏向锁的标识,如果锁标志位(biased_lock)是为0,则说明无锁。将对象头的markword当前线程ID,并执行同步代码块。
  2. 如果锁标志位是否为01,为可偏向状态,则测试将Mark Word中线程ID是否指向当前线程 。
    • 如果是,执行同步代码;
    • 如果否,则通过CAS操作竞争偏向锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行同步代码块,如果竞争失败,则说明有其他线程在使用,执行撤销操作。

撤销:有竞争时进行撤销,一旦有了竞争就升级为轻量级锁,他会当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,撤销偏向锁的时候会导致stop the word操作。

关闭:有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;

  • 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 关闭偏向锁:-XX:-UseBiasedLocking

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GBLnjmdB-1596596841015)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715094211946.png)]

轻量级锁

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

加锁:这里涉及到一个锁记录的概念,线程在执行同步代码块之前,每个的栈桢都新建一个锁记录的结构,提前将对象的markword复制到锁记录中,官方称为displaced mark word。然后尝试使用CAS(displaced mark word==markword)将对象头的Markword替换为指向锁记录的指针。

  • 如果成功,则执行代码块
  • 如果失败,则使用CAS自旋操作来获取锁

解锁:使用原子操作的CAS将displaced mark word替换回对象头

  • 如果成功,则表示没有竞争发生

    如果失败,则表明当前存在竞争,则升级

重量级锁

升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。详细见monitor

[5] synchronized锁流程

【<synchronized锁升级>】

[6] synchronized锁对比
优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到索竞争的线程,使用自旋会消耗CPU 追求响应速度,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较慢