目录
- 对象内存布局
- 对象头
- 实例数据
- 对齐填充
- 锁在内存布局中的标志位
- 锁升级
- 无锁
- 偏向锁
- 偏向锁升级
- 轻量级锁
- 重量级锁
- 锁消除和锁粗化
- 锁消除
- 锁粗化
- 锁升级总结
对象内存布局
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头
对象头里面有两部分内容:对象标记Mark Word和类元信息(类型指针)
对象标记Mark Word存储是什么数据?
- 对象哈希码、对象分代年龄
- GC标记
- GC次数
- 同步锁标记
- 偏向锁持有者
类元信息存储的是指向该对象类元数据的首地址
对象头有多大?
在64位系统中,MarkWord占了8个字节,类型指针占了8个字节,一共是16字节
案例说明:
也就是说一个简单的object对象的话,一共是16字节大小
对象头很重要,后面讲到锁升级和降级的时候,就是锁的标志位,在对象头里演变
实例数据
存放类的属性数据信息,包括父类的属性信息
对齐填充
虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐。
锁在内存布局中的标志位
锁升级
锁升级背景:
用锁能够实现数据的安全性,但是会带来性能下降,无锁能够基于线程并行提升程序性能,但是会带来安全性下降,那么怎么平衡呢?
基于Java对象内存布局得知,对象头中的Mark Word根据锁标志位的不同可以完成我们的锁升级策略
锁升级过程:无锁->偏向锁->轻量锁->重量级锁
偏向锁:MarkWord存储的是指向偏向的线程ID
轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针
重量级锁:MarkWord存储的是指向堆中的monitor对象的指针
Java5之前是直接使用Synchronized重量级锁,计算机底层的用户态和内核态之间切换的原理,假如锁的竞争比较激烈的话,性能变低,所以java6之后,为了减少获得锁和释放锁带来的性能消耗,引入了轻量级锁和偏向锁
接下来根据锁的种类和锁演化顺序,给大家来讲解
无锁
初始状态,一个对象被实例化后,如果还没有被任何线程竞争锁,那么它就为无锁状态(001)
偏向锁
在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。
那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检査锁的MarkWord里面是不是放的自己的线程ID)。
-
如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程!D是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
-
如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID
- 竞争成功,表示之前的线程不存在了, MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁
- 竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
编向锁在IDK1.6之后是默认开启的,但是启动时间有延迟,所以需要添加参数-XX:BiasedLockingstartupDelay=0,让其在程序启动时立刻启动
开启编向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:关闭之后程序默认会直接进入轻量级锁状态。
-XX:-UseBiasedLocking
偏向锁升级
偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行
1)第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。
此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
2)第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。
但是 java15逐步废弃偏量锁 这个需要注意下
轻量级锁
多线程竞争,但是任意时刻最多只有一个线程竞争,不存在锁竞争太过激烈的情况,也就没有线程阳塞,轻量级锁是为了在线程近乎交替执行同步块时提高性能。
主要目的:在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞
升级时机: 当关闭偏向锁功能或多线程竞争偏向锁时会导致偏向锁升级为轻量级锁
假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。此时线程B操作中有两种情况:
- 如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A - B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;
- 如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
重量级锁
Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitorenter指令,在结束位置插入monitor exit指令。
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。
锁消除和锁粗化
简单来说,两种错误使用锁的案例,编译不会出问题,但是是无实际意义的代码,可以简单看看
锁消除
从JIT角度看相当于无视它,synchronized(o)不存在了这个锁对象并没有被共用扩散到其它线程使用,极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
锁粗化
假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请锁使用即可,避免次次的申请和释放,提升了性能
上面的代码等同于一个锁里面的执行块
锁升级总结
Synchronized锁升级过程总结:先自旋,不行再阻塞
实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式
Synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级),而不是无论什么情况都使用重量级锁。
- 偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
- 轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似),存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
- 重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。
就先说到这
\color{#008B8B}{ 就先说到这}
就先说到这
在下
A
p
o
l
l
o
\color{#008B8B}{在下Apollo}
在下Apollo
一个爱分享
J
a
v
a
、生活的小人物,
\color{#008B8B}{一个爱分享Java、生活的小人物,}
一个爱分享Java、生活的小人物,
咱们来日方长,有缘江湖再见,告辞!
\color{#008B8B}{咱们来日方长,有缘江湖再见,告辞!}
咱们来日方长,有缘江湖再见,告辞!