内存布局、Synchronized锁与锁升级
一、对象的内存布局
在HotSpot虚拟机中,对象在堆内存中的存储结构可以分为三个部分,对象头、实例数据和对齐填充

1.对象头
对象头分为 对象标记(Mark Word)和类元信息(类型指针)
对象头里存放着:哈希码、GC标记、GC次数、同步锁标记、偏向锁持有者的信息

那么对象头里怎么存放数据呢

那么有一个问题,存放着hashcode的的31位、在上锁之后去了哪里呢?


2.对齐填充:
jvm要求每个对象都是8位的整数倍,如果对象头+实例数据的不是整数倍(如12),那么剩下的就会用对象填充。
二、Synchronized锁
我们首先提出一个问题:为什么每一个对象都可以成为一把锁?
因为java对象是天生的Monitor,每一个Java对象都由成为Monitor的潜质,因为在Java的设计中,每一个Java对象自从出生之后就会带一把锁,它叫做内部锁或者Monitor锁。
为什么要升级成为偏向锁、轻量级锁?
在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的synchronized效率低的原因Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁
(一句话就是尽量减少用户态和内核态的切换)
1.偏向锁:
当只有一个线程在竞争锁,Synchronized锁会先升级成偏向锁
偏向锁的操作不用直接捅到操作系统,不涉及用户到内核转换,不必要直接升级为最高级,
假如有一个线程执行到synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到Mark Word当中,并修改标偏向标示,标示当前线程就获得该锁。
在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。
那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检查锁的MarkWord里面是不是放的自己的线程ID)。
如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程[D与当前线程1D是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
如果不等,表示发生了竞争,锁己经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID,
竞争成功,表示之前的线程不存在了,MarkWord里面的线程1D为新线程的ID,锁不会升级,仍然为偏向锁;
竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。
锁的撤销,
偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。
撇销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:
① 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
②第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。
但是偏向锁并不是立即执行的,当只有一个线程进行的时候,偏向锁会默认等4秒才会开启(可以更改)。
2.轻量级锁
当由两个或以上的线程争抢锁,偏向锁会升级成为轻量级锁

3.重量级锁
当自旋到一定次数之后,轻量级锁会升级成为重量级锁
4.锁消除

5.锁粗化

四个锁会合并成为一个锁

浙公网安备 33010602011771号