1,简介
关于java的同步,阻塞,唤醒等操作的具体实现原理,最近很想了解一下,但是没有时间去具体去看虚拟机的代码,所以想看看有没有别人已经造好的轮子。感谢分享的互联网的世界。
同步实现包含的东西
我们每次需要同步的时候都是直接使用了synchronized,但是仔细考虑的话,这个里面实际上包含了两层意思。
一方面是包括了互斥,也就是说对于共享资源的操作只能有一个线程能够访问临界区,
另一方面,在一个线程进入了临界区,其他线程一定是处于等待或者阻塞的状态的。
所以在synchronized处肯定是完成了互斥 和 等待/阻塞 的
2,一个synchronized的代码
源码
public class Syntest {
public static void testSyn(String param) {
synchronized (Syntest.class) {
System.out.println(param);
}
}
}
反编译以后的代码
chuangchuang@chuang:~/work/git_lab/cajl-approve/target/test-classes/chen/thread[master*]$ javap -c syn/Syntest.class
Compiled from "Syntest.java"
public class chen.thread.syn.Syntest {
public chen.thread.syn.Syntest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void testSyn(java.lang.String);
Code:
0: ldc #2 // class chen/thread/syn/Syntest
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: aload_0
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
5 14 17 any
17 20 17 any
}
可以看到在编译后的代码中加入了两个指令
在code区的第 4 节有一个 monitorenter 指令
第19节有一个 monitorexit 指令
jvm也就是在monitorenetr指令的执行过程中完成了互斥和等待/阻塞
3, synchronized具体实现
synchronized 在jdk1.5之前听说是性能不太好,主要是只要使用synchronize就会产生操作系统的monitor的获取,但是这个同步块可能只有一个线程会访问,或者根本不会出现竞争的场景(多个线程交替访问),在这样的场景下,使用synchronized频繁的加锁释放锁就有点不是很合适了(开销太大)。
从网上查了很多资料,对synchronized的实现,尤其是重量级锁的实现讲的都是含糊其词,都说是交给操作系统,并没有show the code 到底是怎么和操作系统关联的。看得实在难受,大概翻了几百篇博客,受不了了,尝试看了一下jvm的实现,下面介绍一下自认为看到的东西,希望有人站出来打脸,帮忙斧正。
synchronized 的实现现在优化了很多,增加了偏向锁,轻量级锁,在进入synchronized的代码的时候,有可能会经历从 偏向锁-->轻量级锁--->重量级锁 的升级过程。但是也有可能不会升级,看对临界区的竞争情况。
3.1 monitorenter 对应的jvm代码入口
monitorenter时会进入到InterpreterRuntime.cpp的InterpreterRuntime::monitorenter函数
//%note monitor_1
//参数 JavaThread* thread 指向的是当前线程
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem); //
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
// UseBiasedLocking 标识了虚拟机是否可以使用偏向锁
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else { // 非偏向锁的状态
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
方法入参解析
1、JavaThread thread指向java中的当前线程;
2、BasicObjectLock类型的elem对象包含一个BasicLock类型_lock对象和一个指向Object对象(synchronized包裹的对象)的指针_obj;
class BasicObjectLock {
BasicLock _lock; // object holds the lock;
oop _obj;
}
3、BasicLock类型_lock对象主要用来保存_obj指向Object对象的对象头数据;
class BasicLock {
volatile markOop _displaced_header;
}
4、UseBiasedLocking标识虚拟机是否开启偏向锁功能,如果开启则执行fast_enter逻辑,否则执行slow_enter;
3.2 进入偏向锁
如果开启了偏向锁,则优先通过
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
进入偏向锁
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) ;
}
通过调用BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
来进行偏向锁的设置,老实说这个里面可能用的有些东西还没有弄太明白,偏向锁的获取逻辑有点复杂
只能看的大概的意思
1,获取对象头的Mark Word;
2,判断mark是否为偏向锁状态且mark word中的threadId 为null , 如果是,则进入3,否则进入6
3, 进行一次cas的偏向加锁操作,就是将 markword中存放的threadId替换为当前线程的id
4, cas成功则说明获取到偏向锁,返回,执行同步代码块。
5,cas失败,则等待安全点,释放偏向锁,通过轻量级锁进入。
6,如果mark标示为偏向锁状态,threadId为当前线程,则是重入锁,直接进入临界区。
3.3 偏向锁的撤销
偏向锁的存在的场景是在临界资源不会被两个活着的线程交替访问的状态,它包含了
在整个应用的周期只会有一个线程持有该偏向锁
在整个应用周期当下一个需要访问临界区的线程来临时,上一个线程已经挂掉了
所以偏向锁采取的是不主动撤销偏向锁的策略,线程会一直持有偏向锁,知道其他线程来访问临界资源。
这样的优势是不用撤销锁了,对比轻量级锁减小了退出的时候的cas操作
1,要到达一个全局安全点,safe point
2,暂停拥有偏向锁的线程,看是恢复到无锁还是轻量级锁状态
偏向锁可以通过启动参数关闭
XX:-UseBiasedLocking=false
4, 轻量级锁
偏向锁的存在的场景是在临界资源不会被两个活着的线程交替访问的状态
轻量级锁存在的场景是任意时刻不会有两个线程同时需要持有锁
对应的是多个线程交替的进入和退出临界区,没有竞争。
4.1 轻量级锁的进入
对应了
ObjectSynchronizer::slow_enter
在无法获取偏向锁的时候,会调用slow_enter
-
拷贝锁对象头mark到线程的 BasicLock 当中
-
通过cas尝试将 mark更新为指向 BasicLock的指针
-
2成功则执行同步代码块,失败的话进入4
-
2失败的话说明当前处于加锁状态,查看mark指向的 BasicLock 是否存储在当前线程的栈贞,如果是,则说明是重入,直接执行同步代码即可。否则,说明发生了竞争,则进行锁升级,轻量级锁膨胀为重量级锁。
4.2 轻量级锁的释放
入口是:ObjectSynchronizer::fast_exit
-
将保存在BasicLock中的数据通过cas换回到对象的mark当中
-
如果1 成功,则成功释放锁,否则,说明有其他线程在竞争锁,这个时候需要将该锁升级为重量级锁并后释放。
5, 重量级锁
这一次学习主要关注的也是重量级锁,因为看了很多文章说重量级锁是交给操作系统进行管理的,互斥等都是由操作系统来实现,对性能的影响很大,等等。但是我翻阅了很多文章都很难将这些东西说清楚,后面只能自己看代码来学习一下了。
在重量级锁中用到了一个叫 ObjectMonitor 的对象,这个对象被称为锁,将他绑定到synchronized使用的对象上(对象的mark指向 objectMonitor)
5.1 重量级锁的膨胀
使用的是
ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object)
函数,这个里面的逻辑也是比较多,只能大体的介绍一下逻辑
重量级锁的膨胀的目的是为了拿到一个objectMonitor对象,并把这个objectMonitor绑定到synchronized使用的对象(obj)上
-
jvm会维护一个全局的可用monitor, 叫gfree-monitors, 同时每个线程也会有自己私有的可用的monitor对象列表 omfreemMonitors
-
当前线程会先去omfreeMonitors当中寻找可用的objectMonitor对象,如果没有再去gfree-monitors中去取,如果gree-monitors当中也没有的话,就会new一个objectMonitor对象,然后使obj->mark指向该objectMonitor
3.这样的话,该膨胀就完成了,但是线程还需要对锁进行竞争。
5.2 对重量级锁的竞争
这个需要稍微了解一下objectMonitor的结构,这里做了一些删减,只留下了本次需要用到的字段
ObjectMonitor() {
_owner = NULL; //存放该monitor被持有的线程的id
_WaitSet = NULL; //调用obj.wait()方法的线程会被放入此线程
_cxq = NULL ; // 竞争失败的线程会进入
_EntryList = NULL ;
}
重量级锁涉及到了线程间的阻塞和唤醒,是通过locksupport中的park和unpark进行的支持,其中阻塞和等待锁的线程都在objectMonitor中记录。
先聊一下竞争的过程:
1.通过CAS尝试把monitor的_owner字段设置为当前线程,同时也会判断是否为重入锁等。
2.如果cas成功,或者为重入锁则返回进入到临界区。
3.如果竞争失败,则将当前线程放入_cxq队列,并且将当前线程park()。
5.3 重量级锁的释放
释放重量级锁的入口是ObjectMonitor::exit()
释放的逻辑是
1.把objectMonitor的owner字段设置为null
2.查看_EntryList是否为空,不为空则则从_EntryList中取出一个线程唤醒
3.如果_EntryList为空,则将 _cxq中的元素移动到_EntryList当中,然后再从_EntryList当中唤醒一个
网友评论