锁的升级、降级
- 所谓所的升级、降级,就是JVM 优化synchronized的运行机制,当jvm检测到不同的竟态条件,会自动切换到合适的锁实现,这种切换情况就是锁的升级、降级
- synchronized代码块有一对monitorenter/monitorenter指令实现,monitor对象是同步实现的基本单元
- java6之前,monitor实现完全依靠操作系统内部的互斥锁,因为需要完成用户态到内核态的切换,所以同步操作是一个无差别的重量级操作
- 现代(oracle)JDK 中,JVM 对此进行重大改进,提供欧冠你三种不同monitor实现,也就是常说的三种不同锁,偏斜锁(Biased Locking),轻量级锁和重量级所,根据不同竟态条件去选择不同锁
jvm 锁切换策略
- 当无竟态条件出现时,默认是偏斜锁。jvm会使用CAS (compare and swap)操作,在对象头上的Mark Word位置 设置线程ID ,以表示这个对象偏向于当前线程,并不涉及真正的互斥锁。这样做的假设适用多数应用场景(竟态条件),因为大部分对象生命周期内最多会被一个线程锁定。
- 如果有另外线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖CAS 操作Mark Word来试图获取锁。若重试成功,就使用轻量级级锁;否则进一步升级为重量级锁。
- JVM在进入安全点(safepoint)时,会检查是否有闲置Monitor ,然后试图进行降级。
知识扩展
JVM synchronized源码分析
- synchronized是JVM 内部的Instrinsic Lock。所以偏斜锁、轻量级锁、重量级锁的代码实现不在核心类库部分,而是JVM 代码中。
- java代码运行分两种模式,解释型: javac经编译成字节码后,运行时字节码被jvm加载、解释为机器码进行解释执行。编译型:javac编译成字节码后,jvm的JIT(java 即时编译器)会根据jvm代码情况,执行效率使用内联、虚化等规则,在运行时会将热点代码编译成机器码进行优化,提高执行效率
1.[解释器版本-最新代码](http://hg.openjdk.java.net/jdk/jdk/file/6659a8f57d78/src/hotspot/share/interpreter/interpreterRuntime.cpp)
2.为便于理解,这里专注于[通用基类-最新代码](http://hg.openjdk.java.net/jdk/jdk/file/6659a8f57d78/src/hotspot/share/runtime/)实现
偏斜锁代码分析
- 1.首先,synchronized行为是JVM runtime的一部分,所以我们需要先找到Runtime相关功能实现,通过在代码搜索“monitor_enter”或”monitor Enter”,很直观的就可以定位到:
a.解释器和编译器运行时的基类:sharedRuntime.cpp/hpp,UseBiasedLocking是一个检查,因为,在JVM 启动时,我们可以指定是否开启偏斜锁。偏斜锁不适合所有场景,撤销(revoke)是比较重的行为,只有当存在较多真正竞争的synchronized块时,才能体现出明显改善。
Handle h_obj(THREAD, obj);
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, lock, true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, lock, CHECK);
}
b.另一方面,偏斜锁会延缓JIT 预热进程,因此大多数性能测试会显式地关闭偏斜锁
命令:开启 -XX:+UseBiasedLocking 关闭:-XX:+UseBiasedLocking
- 2.faster_enter是我们熟悉的完整锁的获取路径,slow_enter则是绕过偏斜锁,直接进入轻量级所获取逻辑,核心实现逻辑如下
(1)类似faster_enter实现,解释器或动态编译器,都是拷贝这的基础逻辑,所以如果我们修改这个部分逻辑,要保证一致性。修改要谨慎,微小问题都可能造成死锁或正确性问题。
(2)逻辑如下:bisedLocking定义了偏斜锁的相关操作,revoke_and_rebias是获取偏斜锁的入口方法,revoke_at_safepoint则定义了当检测到安全点是的处理逻辑;如果获取偏斜锁失败,则进入slow_enter;该方法同样检查是否开启偏斜锁,但从代码看,如果关闭偏斜锁,不会进入该方法,所以其是个额外保障性检查
(3)核心代码如下:
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock,
bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
slow_enter(obj, lock, THREAD);
}
- 3.JVM同步相关的各种逻辑:synchronized.cpp/hpp
仔细查看 synchronized.cpp里,会发现不仅仅是synchronized的逻辑,包括从本地代码(JNI),出发Monitor 动作,都可以看到
- 4.对象头的Mark Word
- 5.随着线程竞争加剧,偏斜锁升级为轻量级锁,即slow_enter
流程:设置 Displaced Header,然后利用cas_set_mark设置对象Mark Word,如果成功就获取轻量级锁;否则Displaced Header,然后进入锁膨胀阶段,具体实现在inflated方法中,
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
if (mark->is_neutral()) {
// 将目前的Mark Word复制到Displaced Header上
lock->set_displaced_header(mark);
// 利用CAS设置对象的Mark Word
if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
TEVENT(slow_enter: release stacklock);
return;
}
// 检查存在竞争
} else if (mark->has_locker() &&
THREAD->is_lock_owned((address)mark->locker())) {
// 清除
lock->set_displaced_header(NULL);
return;
}
// 重置Displaced Header
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD,
obj(),
inflate_cause_monitor_enter)->enter(THREAD);
}
- 6。deflate_idle_monitors(参考源码是分析锁降级逻辑的入口,这部分行为会进行持续改进,因为其逻辑实在安全点内进行,处理不当会托长JVM停顿时间(STW,stop-the-world)的时间
- 7. fast_exit或slow_exit是对应的锁释放逻辑(参考源码)
Lock设计
- 类图
- 上述显示,这些锁都未实现Lock接口。ReadWriteLock是一个单独接口,通常代表一对锁(共享读锁,互斥写锁)。标准库提供了再入版本的读写锁(ReentrantReadWriteLock),对应的语义同ReentrantLock相似。
- StampedLock也是单独类型,其不支持再入性予以,即不是以持有锁的线程为单位。其在提供类似读写锁的同时,还支持基于假设的优化读模式。逻辑是先试着读,然后通过validate方法确认是否进入写模式,如果未进入,则成功避免开销;如果进入,则尝试获取读锁。例子如下
public class StampedSample {
private final StampedLock sl = new StampedLock();
void mutate() {
long stamp = sl.writeLock();
try {
write();
} finally {
sl.unlockWrite(stamp);
}
}
Data access() {
long stamp = sl.tryOptimisticRead();
Data data = read();
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
data = read();
} finally {
sl.unlockRead(stamp);
}
}
return data;
}
// …
}
- 读写锁的需求场景
1.ReentrantLock和synchronized简单实用,但行为有一定局限性,通俗说就是”霸道”,要么不占用,要么独占(写时独占保证数据一致性,读时独占则严重影响线程并发).实际场景,不需要大量竞争的写操作,而是以并发读取为主。因此出现ReentrantReadWriteLock等
2.java并发包提供的读写锁扩展了锁的能力,其原理是多个读操作不需要互斥,读操作不会更改数据,不存在相互干扰,写操作则会导致并发一致性问题。因此,写线程之间、读写线程之间,需要精心设计互斥逻辑
3.以下是使用ReentrantReadWriteLock,解决数据量打,并发读多,并发写少,能够比纯同步版本凸显优势。运行过程中,读锁试图锁定时,写锁是被某个线程持有,读锁则无法获取,只能等待对方操作结束,这样保证不会读取到有争议的数据。读锁可以并发访问。
4.这里writeLock和unLockWrite一定保证成对调用
5.例子如下:
public class RWSample {
private final Map<String, String> m = new TreeMap<>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public String get(String key) {
r.lock();
System.out.println("读锁锁定!");
try {
return m.get(key);
} finally {
r.unlock();
}
}
public String put(String key, String entry) {
w.lock();
System.out.println("写锁锁定!");
try {
return m.put(key, entry);
} finally {
w.unlock();
}
}
// …
}