上集说到,Java的对象头里可以标记对象锁的状态:无锁,偏向锁,轻量级锁,重量级锁,我们进一步分析四种锁的状态
Java 中的锁
在 Java 中主要2种加锁机制:
-
synchronized 关键字
上集分析过是通过一对字节码指令 monitorenter/monitorexit 实现, 这对指令被 JVM 规范所描述。 -
java.util.concurrent.Lock
Lock是一个接口,ReentrantLock是该接口一个很常用的实现
通过 Java 代码搭配sun.misc.Unsafe 中的本地调用实现的
CAS 指令(Compare And Swap)
Synchronized在底层用汇编指令cmpxchg来实现CAS指令:语义可以用伪代码表示。CAS指令是原子性的
// 入参三个参数p(目的地址),old(值1),new(值2)
// 指令会比较目的地址和值1是否一致
// 如果不一致,返回false
// 如果一致,将值2填写到目的地址,返回true
function cas(p, old, new) returns bool {
if(*p != old) {
return false
}
*p = new
return true
}
锁升级--- 偏向锁,轻量级锁,重量级锁
java对象头标志位表示了3种锁的状态,根据竞争的程度,偏向锁会变成轻量级锁,轻量级锁再更激烈的竞争下回变成重量级锁。
回忆上集提到过的Java对象头,下图体现了三种锁状态切换
无锁->偏向锁(Normal ->Biased)
偏向锁的获取方式是将MarkWord部分,标记上线程ID,赋值逻辑:
- 获取MarkWord,判断是否可以处于可偏向的状态
找到了openjdk的源码MarkWord.hpp
// Indicates that the mark has the bias bit set but that it has not
// yet been biased toward a particular thread
bool is_biased_anonymously() const {
return (has_bias_pattern() && (biased_locker() == NULL));
}
// Biased Locking accessors.
// These must be checked by all code which calls into the
// ObjectSynchronizer and other code. The biasing is not understood
// by the lower-level CAS-based locking code, although the runtime
// fixes up biased locks to be compatible with it when a bias is
// revoked.
bool has_bias_pattern() const {
return (mask_bits(value(), biased_lock_mask_in_place) == biased_lock_pattern);
}
JavaThread* biased_locker() const {
assert(has_bias_pattern(), "should not call this otherwise");
return (JavaThread*) mask_bits(value(), ~(biased_lock_mask_in_place | age_mask_in_place | epoch_mask_in_place));
}
has_bias_pattern() 返回true代表可偏向标志(biased_lock)为1,lock标志位01
biased_locker() == Null 返回true 表示MardWord里thread是空
如果为可偏向状态,尝试CAS操作。
- CAS操作成功,变为偏向的状态
- CAS失败,有另外一个线程 Thread B 抢先获取了偏向锁。 这种状态说明该对象的竞争比较激烈, 此时需要撤销 Thread B 获得的偏向锁,将 Thread B 持有的锁升级为轻量级锁。 该操作需要等待全局安全点 JVM safepoint ( 此时间点, 没有线程在执行字节码) 。
如果是已偏向状态, 则检测 MarkWord 中存储的 thread ID 是否等于当前 thread ID
- 如果相等, 则证明本线程已经获取到偏向锁, 可以直接继续执行同步代码块
- 如果不等, 则证明该对象目前偏向于其他线程, 需要撤销偏向锁
偏向锁撤销流程
1.在一个安全点停止拥有锁的线程。
2.遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态。
3.唤醒当前线程,将当前锁升级成轻量级锁。
所以,如果某些同步代码块大多数情况下都是有两个及以上的线程竞争的话,系统开销大,我的可以关闭偏向锁,JVM有参数可以设置
-XX:-UseBiasedLocking
偏向锁->轻量级锁(Biased->Lightweight)
前面提到,多个线程竞争一个对象时,发生偏向锁的撤销操作。对象可能处于两种状态:
不可偏向的无锁状态,原来已经执行了同步快代码,对象处于闲置状态:
可以偏向的已锁状态(轻量级锁),原来已经获取了偏向锁的线程尚未执行同步代码块,偏向锁依旧有效:
轻量级加锁过程:
通过标志位判断出对象状态处于不可偏向的无锁状态
当前线程的栈帧创建存储锁记录的空间,将对象头复制到锁记录中
-
CAS操作来将对象头的MarkWord替换为指向锁记录的记录的指针
- 成功,当前线程获得锁
- 失败,已经加锁,自旋,再次尝试CAS操作,仍未抢到,升级为重量级锁,就是锁膨胀
轻量级锁也被称为非阻塞同步、乐观锁,因为这个过程并没有把线程阻塞挂起,而是让线程空循环等待,串行执行。
锁膨胀示意
引用大佬的图展示锁竞争,最终膨胀为重量级锁的过程。
重量级锁
Synchronized没有优化之前,是重量级锁,依赖对象内部的monitor锁来实现,而monitor锁依赖操作系统的MutexLock(互斥锁)来实现
为什么重量级锁开销大?
主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
互斥锁(重量级锁)也称为阻塞同步,悲观锁
下面总结一下三个形态锁的优缺点:
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外消耗 | 线程间存在锁竞争,会带来额外锁撤销的消耗 | 只有一个线程访问同步快 |
轻量级锁 | 竞争的线程不会阻塞,提高程序相信速度 | 如果始终得不到锁竞争的线程,消耗CPU | 追求响应时间,同步块实行速度特别快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量,同步块时间时间长 |
总结
通过上面的分析,synchronized关键字并非一开始就该对象加上重量级锁,也是从偏向锁,轻量级锁,再到重量级锁的过程。