Synchronized 都用过,那么它是怎么做到简单修饰一下就能做到并发场景下资源同步的呢?让我们来揭开它神秘的面纱~
JDK6以前Synchronized 只有重量级锁,很笨重,就是当一个线程t1访问同步资源时,立马上锁,其它线程过来竞争锁的话直接阻塞,然后等待t1释放锁,线程唤醒再去加重量级锁,
线程的阻塞和唤醒需要OS切换CPU(内核态与用户态)的状态来完成,这个状态转换消耗处理器时间,开销大,影响性能,
so~ 在JDK 6 引入了偏向锁(默认开启偏向锁) 和 轻量级锁
所以现在有四种锁状态,无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 只能升级,不能降级
synchronized 的锁是加在对象头的,
我们都知道对象是在堆内存分配空间的,,在JVM中,对象在内存中的布局又分为:对象头、实例数据、对齐填充,盗图如下,
我们专门来看看 对象头
对象头
盗图如下:
普通对象的 Header = Mark Word (标记字段)+ Klass Word(类型指针)
数组对象的 Header = Mark Word (标记字段)+ Klass Word(类型指针) + array length(数组长度)
![在这里插入图片描述](https://img-blog.csdnimg.cn/e7b88385f54a43cda5cda1ed632d1f12.png
所以**对象在堆内存的存储结构
**应该如下:
Klass Pointer (类型指针):简单说一下,就是指向类元数据的指针,JVM通过这个指针确定当前对象是属于哪个类的实例
Mark Word(标记字段):重点说这个标记字段,它是和对象本身数据没有关系的,用来存储对象运行时数据,存储数据和结构是变化的
结构如下盗图:
可以看到,针对不同锁的状态,存储的数据不同,翻译过来就是盗图如下(32bit):
下面我们逐个状态来康康:
NORMAL - 无锁状态
:
Mark Word - 标记字段 里边存了
Hash Code
: 对象标识Hash码
age
: GC年龄,年轻代中s1、s2复制一次,年龄+1,阈值15,固定4bit,4位也就意味着最大1111 = 15
biased_lock
: 是否是偏向锁,0-否 1-是,JDK6以上默认开启偏向锁,可以通过参数来禁用偏向锁
-XX: -UseBiasedLocking //禁用
-XX: +UseBiasedLocking //启用
lock
: 锁标记,盗图如下
无锁和偏向锁需要 biased_lock 和 lock组合来判断
Biased - 偏向锁状态
首先来说下什么是偏向锁,顾名思义,它很偏向,锁老给一个线程获取,嘎嘎~
引入偏向锁是为了在没有多线程竞争资源时减少CAS(硬件实现原子性)次数从而提高性能,不细说了
thread: 持有偏向锁的线程id
epoch: 线程获取偏向锁时的时间戳
age: GC年龄
Biased_lock: 是否是偏向锁, 1-是
lock: 锁标记 01 同无锁
偏向锁获取
- 当一个线程访问同步变量(Synchronized修饰),先去对象header中判断biased_lock = 1 ,是否为偏向锁
- 然后判断thread = 当前线程id,判断是否偏向当前线程
- 如果偏向当前线程,说明当前线程持有锁,直接执行同步代码
- 如果同步对象的header中mark word 中thread 与当前线程不等,则通过CAS操作竞争锁,将当前线程id赋值给thread
- 如果竞争成功则持有锁,执行同步代码,如果竞争失败,则说明锁被其它线程占有,偏向锁同一时刻有竞争就会在safepoint(全局安全点)挂起线程,升级偏向锁为轻量级锁,然后挂起线程继续执行同步代码,(我觉这点很好理解,偏向锁的出现本来就是为在无多线程竞争的情况下放置锁获取和释放依赖多次CAS导致系统开销大问题,现在有并发竞争了你还偏向,就给一个线程执行,那其它线程猴年能获取到锁啊)
- 持有偏向锁的线程不会主动释放锁
panda白话总结
:
线程访问同步对象,先康康对象头里说它是偏向锁了吗,是的话康康偏向的是我吗,是我就放肆了,执行代码,不是我的话那我试试(CAS)我能加锁不,加锁成功,我又放肆了,后面这个对象偏向我了,我再来请求就不用做CAS这样无用功了,加锁不成功的话,害~别人来早偏向他了,那不行啊,我也得用呢,自旋等会吧,对象一看,哦?同时又来一个,那好吧,我火了,不能偏向了,找个安全时间点(safepoint)升级为轻量级锁你们去竞争吧,嘎嘎
LightWeighed Locked - 轻量级锁
ptr_to_lock_record : 指向栈帧中Lock Record的指针
lock: 锁标记 00
上面说了偏向锁,和它相同锁标志的还有无锁呢,无锁就是没有现成上锁呗,当有现成访问时也会直接升级为轻量级锁
轻量级锁是什么呢??
是针对重量级锁来的,重量级锁竞争现成直接阻塞,锁释放了再去环境阻塞现成,系统进行现成阻塞和唤醒,恢复现场,开销比较大,所以出了轻量级锁,轻量级锁竞争现成不直接阻塞,而是自旋等待(等一会),是通过自旋锁机制实现的,自选到一定次数再阻塞,这是轻量级锁也升级为重量级锁,如果自旋过程中获取到锁,那就很美滋滋啦。
无锁 -> 轻量级锁获取
1、首先一个对象创建老实巴交地呆在堆内存中, 此时对象头是无锁状态,如下:
2、此时有个线程请求过来要上=加锁,我们都知道每个线程分配一个栈空间,那这个线程能否加锁成功,要做几件事
- 栈帧(每个方法创建一个栈帧压栈)中创建一个
Lock Record
(锁记录)空间,锁记录中有俩属性_displaced_header
和owner
- 拷贝一份对象头中
Mark Word
存放到_displaced_header
- 此时
biased_locked
-偏向锁标志值为 0 ,即不是偏向锁,所以它要上一把轻量级锁,上面说了轻量级锁mark word
中除了锁标记lock
,还存了**ptr_to_lock_record :** 指向栈帧中Lock Record的指针
,所以第三件事做的就是:JVM通过CAS操作将对象中的mark word 存储的ptr_to_lock_record
更新为指向栈帧中Lock Record
的指针 - 将栈帧里Lock Record 中的
owner
指针指向对象,代表这个对象被我占了,i am the owner,嘎嘎 - 修改堆中对象头的Mark Word 中lock 标志为
00
,告诉其他线程这个对象被轻量级锁锁了,其他线程竞争锁时可以自旋等待,不用立即释放CPU时间片进入阻塞,不赘述了。。 - 上锁成功
结果如下盗图:
panda白话总结
:
针对以对象来说,我只要知道两件事,1、lock=00,告诉竞争线程我被轻量级线程锁了,你们该自旋自旋
2、ptr_to_lock_record = 持有锁线程LockRecord指针,我被谁给锁了
针对于持有锁线程来说,我也只要知道两件事1、对象mark word副本(duck不必)2、owner = 对象指针 知道我锁的是哪个对象
HeavyWeight Locked - 重量级锁
ptr_to_heavyweight_monitor: 指向monitor对象的起始地址,monitor对象稍后说
lock: 锁标记 11
重量级锁一直在说,不赘述了。。
轻量级锁 -> 重量级锁获取
上面说过,重量级锁对象头mark word中除了lock锁标记(10)还存储了**ptr_to_heavyweight_monitor:** 指向monitor对象的起始地址,
,看来重量级锁是Monitor
对象来实现的啊,
那我们就来康康什么是Monitor
Monitor - 监控器 or 管程
每个java对象都有一把看不见的锁,成为内部锁或者Monitor锁,同对象生命周期一致
来看它的数据结构:
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
我觉有用的就是:
_WaitSet : 等待wait线程集合
_EntryList :等待block线程集合
owner : 哪个线程正在持有锁
加锁步骤
:
- 当一个线程尝试获取重量级锁,把同步对象与OS提供的
Monitor
关联(只有访问synchrized同步对象加锁时才关联),即上面说的ptr_to_heavyweight_monitor
记录Monitor的起始地址 - 将Monitor中
owner
指向当前线程 - 即当前线程获取锁成功,其它线程请求锁进入阻塞队列EntryList,等待锁释放被唤醒进行非公平竞争锁
- 持有锁线程调用wait()中断进入waitSet
如下图
panda白话总结:
针对对象,1、lock=10,重量级锁 2、ptr_to_heavyweight_monitor = monitor地址,我关联的monitor是谁,有事请找它
针对monitor,1、owner = 持有锁线程id,我管的这个对象被哪个线程占了,2、其它这些请求的线程该进哪个队列进哪个队列waitset 、entryset
锁状态转换
这个大哥图画的不错,简单明了
panda总结:
创建一个对象在堆内存分配一块空间,此时对象锁状态可能为无锁 or 偏向锁
当对象为无锁状态被synchronized修饰时,代表并发情况下只能同步访问,
此时一个线程请求过来,直接升级为轻量级锁,上锁成功,其它线程请求自旋等待,自旋超过一定次数进入阻塞队列,此时轻量级锁升级为重量级锁,此时交给os创建的monitor来处理锁的获取和释放
当对象为偏向锁状态被synchronized修饰时,代表并发情况下只能同步访问,
此时一个线程请求过来,偏向锁将偏向这个线程,下次访问直接执行代码即可,此时有其它线程同步请求,则释放偏向,找个安全点将持有线程挂起,此时如果线程存活则直接获升级后的轻量级锁,如果线程不存活则变为无锁状态。
就到这吧~唉呀妈呀 脑瓜疼~