每个对象都具有一个ObjectMonitor与之关联, 在C++中的数据结构如下:
ObjectMonitor() {
_count = 0; //记录数
_recursions = 0; //锁的重入次数
_owner = NULL; //指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //调用wait后,线程会被加入到_WaitSet
_EntryList = NULL ; //等待获取锁的线程,会被加入到该列表
}
_count: 记录数
_recursions: 锁 重入次数;
_owner : 指向持有ObjectMonitor对象的线程, 指向持有锁的线程;
_waitSet : 调用wait()之后,线程会被被假如到_waitSet集合中;
_EntryList: 争抢锁失败,等待获取锁的线程,会被假如到该列表;
Synchronized加锁时依赖的monitor就是需要操作系统内核来执行,即从用户态需要切换到内核态;
内核程序还可以做其他事情,如 从磁盘读取数据, 将内存数据copy到Socket缓冲区发送到网卡进行网络传输, 操作monitor加锁、释放锁等;
synchronized 锁升级过程: 无锁 <-> 偏向锁 -> 轻量级锁-> 重量级锁,
锁可以升级,但是不能降级; 特例:偏向锁可以重置为"无锁"
为什么要优化synchronized呢? 因为锁的释放和获取,牵涉到CPU用户态和内核态的切换, 这个切换很耗时!
一、【引入偏向锁的原因?】
主要还是为了 {降低获取锁的代价}!
大部分时候是没有锁竞争的, 都是一个线程重复获取锁, 如果仍然需要去“争抢”一次锁,会耗时较多, 不如直接比较一下是否是偏向锁, 锁是否被当前线程持有,会快很多。
二、【为什么要引入轻量级锁?】
通过{自旋}实现轻量级锁,避免频繁阻塞、唤醒线程;
轻量级锁考虑的是: {竞争锁对象的线程不多},而且{线程持有锁的时间也不长}的情景。
因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
如果存在锁竞争,争抢不到锁的线程会被阻塞,进入_EntryList, blocking状态, CPU从用户态切换到内核态, 如果刚刚被阻塞不就, 锁就被释放了, 此时还要将阻塞的线程“唤醒”(用户线程获取到锁,从内核态又切回了用户态), 这个过程太耗时。
三、MarkWord在对象头中的样子:
32位=》
无锁: 25bit 存储 hashCode值;
4 bit 存储对象的分代年龄;
1 bit 存储是否偏向锁 0;
2 bit 锁标识位 01;
偏向锁:23 bit 偏向锁的{线程ID},
2 bit epoch,
4 bit 分代年龄;
1 bit 是否偏向锁 1;
2 bit 锁标识位 01;
轻量级锁: 30 bit 存指向栈中锁的记录的{指针}, 会在栈内创建锁记录(包括hashCode , 分代年龄等)
2 bit 锁标识位 00;
重量级锁: 30 bit 存储指向重量级锁的指针,
2 bit 锁标识位 10;
GC标志 : 30 bit 空;
2 bit 锁标识位 11 ;
四、【偏向锁的原理和升级过程】
偏向锁的加锁过程:
1、访问MarkWord中偏向锁标识是否设置为1, 锁状态标识是否是01, 确认为可偏向状态;
2、如果为可偏向状态, 则判断线程ID是否为当前线程ID, 如果是,进入 步骤5, 执行同步代码块,end;否则,进入步骤3;
3、如果线程ID不是当前线程的ID, 则通过CAS操作竞争锁,如果竞争成功, 则将MarkWord中的线程ID设置为当前线程的ID,然后执行步骤5; 如果竞争失败,执行步骤4
4、 如果CAS竞争锁失败,则表示有竞争,当到达全局安全点(safePoint)后获取偏向锁的线程会被挂起,偏向锁升级为【轻量级锁】, 会将MarkWord复制一份到帧栈的锁记录空间,同时在MarkWord中记录该空间的地址指针,修改锁状态标识位为00;
5、执行同步代码块;
我们发现, 偏向锁适用于竞争少的场景, 一旦发生锁竞争,就会升级为轻量级锁;升级为轻量级锁时会进入安全点,导致线程停顿STW,导致性能下降
五、【轻量级锁升级为重量级锁的过程】
1、在进入同步块时,如果同步对象锁状态为无锁状态( 偏向标识为 0, 锁标识为 01), JVM首先会在当前线程的帧栈中建立一块名为 LOCK RECORD锁记录的空间, 用于存储锁对象的MarkWord副本, 称之为 Dispaced Mark Word;
2、 拷贝对象Markword到锁记录中
3、 拷贝成功后 , JVM将使用CAS操作尝试将对象的Mark Word中的几位更新为指向Lock Record的指针, 并将Lock Record里的owner指针指向 Object Mark Word,如果CAS成功, 则执行步骤4, 否则执行步骤5;
4、如果CAS更新成功,那么这个线程就拥有了对象锁,并且修改Mark Word的锁标识为00, 标识此对象处于轻量级锁状态;
5、如果CAS更新失败,首先检查Mark Word中的指针是否指向当前线程的帧栈, 如果是就说明当前线程已经拥有; 否则会自旋等待获取锁, 如果再有其他线程争抢锁,就会升级为重量级锁; 对象的Mark Word中记录会修改为指向互斥量的指针, 锁标识位变成10;线程被挂起, 后面的线程也都会被挂起;
线程1获取轻量级锁时,会把锁对象的对象头MarkWord复制一份到线程1的帧栈中, 在帧栈中创建了一个内存空间用于存储锁记录, 简称为 DisPlacedMarkWord, 然后使用CAS把对象头中的内容替换为线程1存储的锁记录的地址, 即使用CAS更新MarkWord中的那30个bit位,指向帧栈中的对象头MarkWrod副本;
如果在线程1复制对象头的同时或在CAS前,线程2也准备获取锁, 赋值了对象头的MarkWord信息到线程2的锁记录空间中, 但是在线程2CAS的时候, 发现线程1已经把对象头的MarkWord的30bit的空间赋值了, 线程2的CAS失败, 那么线程2就会尝试使用自旋来等待线程1释放锁。 但是如果自旋的时间太长也不行, 因为自旋是消耗CPU的, 因此自旋的次数有限制, 如果自旋次数到了线程1扔未释放锁, 或者线程1还在执行,线程2还在自旋等待, 这时又来了个线程3过来竞争这个锁对象, 此时 轻量级锁升级为重量级锁, 会把线程都阻塞,防止CPU空转;
轻量级锁不适用 锁住时间长或等待锁的线程特别多的场景;
JDK 1.6之后引入了 自适应的自旋锁, 他的自旋此时是会变的, 上次获得锁的线程正在执行或持有锁, 那么这一次也极有可能也获取到锁, 此时自旋次数会多些, 如果某个锁很少有自旋成功, 那么以后也极有可能不成功,就减少甚至不自旋;
【锁清除】
如果在一段程序里用了锁, 但是JVM检测到这段程序不存在共享数据竞争,不会有锁争抢,变量没有逃逸出方法外, 这个时间JVM会将锁清除掉,不执行同步操作;