Java-并发-锁-synchronized
Java-并发-锁-synchronized
摘要
本文会详细说下synchronized的底层实现原理。
0x01 基本概念
- 每次只能有一个线程进入临界区
- 保证临界区内共享变量的可见性和有序性
- 成功进入
synchronized
区域的线程可以拿到对象的Object-Monitor。具体有3种用法,作用域不同,在后面例子中介绍。 - 对于拿到锁的线程来说,同一个对象的
synchronized
具有可重入性 - 不要将可变对象作为synchronized
- 如果相互等待对方的synchronized 对象,可能出现死锁
- synchronized锁是非公平的
注意:最近发现本文所讲偏向锁和轻量级锁的代码分析章节有误,请大家移驾参阅死磕Synchronized底层实现–概论系列文章,查看源码分析。待后续有时间我会改正本文内容。
前置知识,是锁优化升级:
- Java中4种synchronized锁状态,随着锁竞争开始,这几种锁之间有锁升级的关系:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
- 锁只能升级不能降级。这么做的原因是缩短锁的获取释放周期,提升效率。
关于锁优化和锁升级知识的更多详细内容请点击此文:Java-并发-关于锁的一切
0x02 实现原理
2.1 引子-synchronized用于对象同步块
可以用一个简单代码试试:
public class SimpleTest2
{
public static void main(String[] args)
{
Object chengc = new Object();
synchronized (chengc){
int i = 1;
System.out.println("");
}
}
}
然后就是用命令编译:
$ javac SimpleTest2.java
然后进行反汇编:
$ javap -c SimpleTest2.class
结果如下:
11: monitorenter
12: iconst_1
13: istore_3
14: aload_2
15: monitorexit
synchronized
代码块通过javap生成的字节码中包含 monitorenter
和 monitorexit
指令。也就是说,synchronized
指令可以尝试获取对象锁(object-monitor):
- monitor
每个对象都关联着一个monitor,只能被唯一获取monitor权限的线程锁定。锁定后,其他线程请求会失败,进入等待集合,线程随之被阻塞。 - monitorenter
这个命令就是用来获取监视器的权限,每进入1次就记录次数加1,也就是同一线程说可重入。而其他未获取到锁的只能等待。 - monitorexit
拥有该监视器的线程才能使用该指令,且每次执行都会将累计的计数减1,直到变为0就释放了所有权。在此之后其他等待的线程就开始竞争该监视器的权限
在jdk8/hotspot/src/share/vm/interpreter/interpreterRuntime.hpp
中可以找到如下方法:
static void monitorenter(JavaThread* thread, BasicObjectLock* elem);
static void monitorexit (JavaThread* thread, BasicObjectLock* elem);
因为前置知识还不够,我们代码分析先说到这里。请看完第四章后,在这里继续看monitorenter底层实现。
2.2 synchronized用于方法
先说结论,将关键字修饰方法,那就会使得该方法的flags
中多出一个ACC_SYNCHRONIZED
。当线程执行时就需要去获取对象监视器,拿到的再开始执行方法。
实例如下:
public class SynchronizedMethodTest
{
public synchronized void synMethod(){
int j = 11;
}
public static synchronized void method() {
System.out.println("Hello World!");
}
}
编译后再使用javap -verbose SynchronizedMethodTest.class
,部分结果如下:
public synchronized void synMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=1, locals=2, args_size=1
0: bipush 11
2: istore_1
3: return
LineNumberTable:
line 9: 0
line 10: 3
public static synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 13: 0
line 14: 8
可以看到两个synchronized
方法的flags
区域都多了ACC_SYNCHRONIZED
。有这种标识的,会被JVM要求执行方法之前先获取到锁,否则等待阻塞。
2.3 synchronized用于静态方法或类对象
这种情况的ObjectMonitor就是该Class对象。
2.4 ObjectMonitor
每个对象都有一个对象监视器。
Object-Monitor在HotSpot中实现主要在
/Users/chengc/cc/work/projects/jdk8/hotspot/src/share/vm/runtime/objectMonitor.hpp
-
/Users/chengc/cc/work/projects/jdk8/hotspot/src/share/vm/runtime/objectMonitor.cpp
这里摘录一些重要的部分:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
注意_WaitSet
t和_EntryList
,他们被用来存储该Object的等待对象的集合,具体来说:
-
_count
记录已获取锁的次数 -
_WaitSet
存放处于wait状态的线程 -
_EntryList
存放那些等待synchronized
同步锁的线程 -
_owner
是指向获得了该对象的Monitor
权限的线程的指针。 -
_WaitSetLock
用来对WaitSet进行同步保护
他们的关系示意图如下:
- 执行
synchronized
,进入EntrySet
,线程处于等待状态 - 每次都只能有一个线程获取到
ObjectMonitor
,_owner
指向该线程,处于Active
状态。同时,将monitor计数器加1(退出一个同步块时,monitor计数器减1) - 执行
wait
方法,会释放ObjectMonitor,进入WaitSet
,线程处于等待状态。同时,_owner
置为空,monitor计数器归0。 - 执行当拥有ObjectMonitor权限的线程释放后会,如果调用了notify或notifyAll方法,会唤醒WaitSet中的线程。被唤醒的线程重新竞争同步锁。
- 当退出
synchronized
块后,完全释放ObjectMonitor
2.5 ObjectWaiter
而EntryList和WaitSet中的等待对象ObjectWaiter
实现如下:
class ObjectWaiter : public StackObj {
public:
enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;
enum Sorted { PREPEND, APPEND, SORTED } ;
ObjectWaiter * volatile _next;
ObjectWaiter * volatile _prev;
Thread* _thread;
jlong _notifier_tid;
ParkEvent * _event;
volatile int _notified ;
volatile TStates TState ;
Sorted _Sorted ; // List placement disposition
bool _active ; // Contention monitoring is enabled
public:
ObjectWaiter(Thread* thread);
void wait_reenter_begin(ObjectMonitor *mon);
void wait_reenter_end(ObjectMonitor *mon);
};
- TStates
存放当前线程状态 - _next和_prev
可以看到该对象有两个分别指向前一个元素和后一个元素的指针,也就是说是一个双向链表。 - _thread
指向的线程
2.6 monitorenter源码分析
2.6.1 InterpreterRuntime::monitorenter
对应到jdk8/hotspot/src/share/vm/interpreter/interpreterRuntime.cpp
继续看看monitorenter
方法实现,这里摘录部分对我们分析有意义的代码如下:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
// 这个h_obj封装了该线程和拥有锁的对象
Handle h_obj(thread, elem->obj());
if (UseBiasedLocking) {
// 当开启了偏向锁模式时,就优先使用快速进入模式避免不必要的锁膨胀
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
2.6.2 BasicObjectLock
上面的monitorenter方法有两个参数:
- thread:
JavaThread* thread:指向我们当前的线程 - BasicObjectLock* elem:
找到BasicObjectLock
位于jdk8/hotspot/src/share/vm/runtime/basicLock.hpp
:
// 一个BasicObjectLock将一个特定的Java对象与一个BasicLock相关联,
// 它目前被嵌入到解释器框架中。
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
// 锁对象,必须双字(一般一个字是4字节)对齐
BasicLock _lock;
// 持有该锁的对象
oop _obj;
- 继续看
BasicLock
,摘录部分代码如下:
class BasicLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
// 该私有变量_displaced_header就是用来描述对象头信息的
volatile markOop _displaced_header;
public:
markOop displaced_header() const { return _displaced_header; }
// 保存对象头的方法
void set_displaced_header(markOop header) { _displaced_header = header; }
}
2.6.3 锁升级
注意前面我们提到的monitorenter
方法中核心代码:
if (UseBiasedLocking) {
// 当开启了偏向锁模式时,就优先使用快速进入模式避免不必要的锁膨胀
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
也就是说,从这里开始,我们的synchronized就要从偏向锁开启锁升级之旅(前提是开启了偏向锁设置,否则从轻量级锁开始)。
2.7 偏向锁
2.7.1 偏向锁的获取
2.7.1.1 fast_enter
// 快速获取/释放monitor所有权
// 该方法在竞争场景下十分敏感
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
// 判断是否使用偏向锁
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
// 不在安全点,调用revoke_and_rebias方法获取偏向锁
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
// 处于安全点,调用revoke_at_safepoint释放偏向锁
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轻量级锁
slow_enter (obj, lock, THREAD) ;
}
2.7.1.2 biasedLocking.revoke_and_rebias
revoke_and_rebias
方法代码位于jdk8/hotspot/src/share/vm/runtime/biasedLocking.cpp
,主要做的就是获取偏向锁。确实因为能力有限看不懂了 - - 。这里转载占小狼大神JVM源码分析之synchronized实现一文对该过程的描述,略有改动:
- 通过
markOop mark = obj->mark()
获取对象的对象头的Mark Word
; - 判断
mark
是否为可偏向状态,即mark的偏向锁标志位为 1且锁标志位为 01; - 判断
mark
中JavaThread
指针的状态:- 如果为空,则进入步骤(4);
- 如果指向当前线程,则执行同步代码块;
- 如果指向其它线程,进入步骤(5);
- 通过CAS原子指令设置mark中的JavaThread为当前线程ID。如果执行CAS成功,则执行同步代码块,否则进入步骤(5);
- 如果执行CAS失败,表示当前存在多个线程竞争锁。当达到全局安全点(safepoint),获得偏向锁的线程被挂起,撤销偏向锁,并升级为轻量级锁。升级完成后被阻塞在安全点的线程继续执行同步代码块。
2.7.2 偏向锁的释放
前面提到过,当已经位于安全点(is_at_safepoint),才可以释放偏向锁。
2.7.2.1 BiasedLocking::revoke_at_safepoint
void BiasedLocking::revoke_at_safepoint(Handle h_obj) {
//又一次断言,只允许在安全点调用该方法
assert(SafepointSynchronize::is_at_safepoint(), "must only be called while at safepoint");
// 封装了线程和锁对象
oop obj = h_obj();
// 这一步作用是更新释放偏向锁的计数
HeuristicsResult heuristics = update_heuristics(obj, false);
if (heuristics == HR_SINGLE_REVOKE) {
// 单撤销模式
revoke_bias(obj, false, false, NULL);
} else if ((heuristics == HR_BULK_REBIAS) ||
(heuristics == HR_BULK_REVOKE)) {
// 批量撤销模式(因为heuristics == HR_BULK_REBIAS为false)
bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL);
}
// 遍历线程列表,清除所有monitor锁缓存信息
clean_up_cached_monitor_info();
}
2.7.2.2 BiasedLocking::Condition revoke_bias
核心代码如下:
// 参数1为锁对象和线程组合
// 参数2为是否允许偏向
// 参数3为是否批量模式
// 参数4申请线程
static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias, bool is_bulk, JavaThread* requesting_thread) {
markOop mark = obj->mark();
if (!mark->has_bias_pattern()) {
// 非偏向锁模式直接返回无偏向
return BiasedLocking::NOT_BIASED;
}
uint age = mark->age();
// 该值代表MarkWord最后3bit101
markOop biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
// 该值代表MarkWord最后3bit001
markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);
// 拥有偏向锁的线程
JavaThread* biased_thread = mark->biased_locker();
if (biased_thread == NULL) {
// 此时偏向锁没有指向任何线程,可能是hashcode误差等原因
if (!allow_rebias) {
// 不允许偏向,就设MarkWord为非偏向模式(无锁,倒数第三bit设为0,最后2bit 01)
obj->set_mark(unbiased_prototype);
}
// 返回已撤销偏向锁
return BiasedLocking::BIAS_REVOKED;
}
bool thread_is_alive = false;
if (requesting_thread == biased_thread) {
// 看当前申请偏向锁线程是否就是拥有偏向锁的线程
thread_is_alive = true;
} else {
// 单撤销偏向锁的时候,requesting_thread为null
// 就遍历所有线程
for (JavaThread* cur_thread = Threads::first(); cur_thread != NULL; cur_thread = cur_thread->next()) {
if (cur_thread == biased_thread) {
// 找到了拥有偏向锁的线程,跳出循环
thread_is_alive = true;
break;
}
}
}
if (!thread_is_alive) {
// 拥有偏向锁的线程已经挂了
if (allow_rebias) {
// 允许申请,就直接设为匿名可偏向模式,即MarkWord中线程id设为null,最后2bit 01
obj->set_mark(biased_prototype);
} else {
// 否则设为非偏向模式(无锁,倒数第三bit设为0,最后2bit 01)
obj->set_mark(unbiased_prototype);
}
// 返回偏向锁已经撤销
return BiasedLocking::BIAS_REVOKED;
}
// 来到这里,说明拥有偏向锁的线程还活着
// 后面一系列代码做的事
// 先检查该线程是否拥有monitor,如果是就将所需的`displaced headers`写入线程的栈中
// 否则恢复锁对象的头信息为
if (allow_rebias) {
// 设置为匿名偏向状态
obj->set_mark(biased_prototype);
} else {
// 设为无锁
obj->set_mark(unbiased_prototype);
}
// 最后返回锁已经撤销
return BiasedLocking::BIAS_REVOKED;
}
2.8 轻量级锁
2.8.1 slow_enter-获取锁
摘录核心代码如下:
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
// 获取对象头的MarkWord部分
markOop mark = obj->mark();
// 此时不应该是偏向锁模式
assert(!mark->has_bias_pattern(), "should not see bias pattern here");
if (mark->is_neutral()) {
// is_neutral代表无锁
// Anticipate successful CAS -- the ST of the displaced mark must
// be visible <= the ST performed by the CAS.
// lock保存该对象头的MarkWord
lock->set_displaced_header(mark);
// CAS方式将目标对象的MarkWord替换为lock
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
// CAS方式成功,代表该线程成功获取锁,可以返回执行同步块代码了
TEVENT (slow_enter: release stacklock) ;
return ;
}
// Fall through to inflate() ...
} else
if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
// 此时MarkWord为轻量级锁状态,且该锁拥有者就是当前Thread
// 轻量级锁不可重入,也就是说每次都需CAS方式获取
assert(lock != mark->locker(), "must not re-lock the same lock");
assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
lock->set_displaced_header(NULL);
return;
}
// 搜了下,下面这段代码因为#if 0的,相当于注释永不运行。。。
#if 0
// The following optimization isn't particularly useful.
if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
lock->set_displaced_header (NULL) ;
return ;
}
#endif
// The object header will never be displaced to this lock,
// so it does not matter what the value is, except that it
// must be non-zero to avoid looking like a re-entrant lock,
// and must not look locked either.
lock->set_displaced_header(markOopDesc::unused_mark());
// 到了这里,说明轻量级锁存在竞争,需要膨胀为重量级锁
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
2.8.2 slow_exit-释放锁1
void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) {
fast_exit (object, lock, THREAD) ;
}
2.8.3 fast_exit-释放锁2
摘录核心代码如下:
void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
// dhw指向MarkWord副本
markOop dhw = lock->displaced_header();
markOop mark ;
// mark指向真实MarkWord
mark = object->mark() ;
if (mark == (markOop) lock) {
assert (dhw->is_neutral(), "invariant") ;
// 如果当前线程拥有锁,就CAS方式还原MarkWord
if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
TEVENT (fast_exit: release stacklock) ;
return;
}
}
// CAS失败,说明其他线程尝试过获取该锁。
// 此时不仅要释放锁,还需要膨胀为重量级锁
ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
}
2.9 重量级锁-膨胀
实现主要在以下方法中:
ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object)
关于重量级锁的膨胀、锁enter/exit等细节待补充,可以参见占小狼的JVM源码分析之synchronized实现。
2.10 monitorexit源码分析
2.10.1 InterpreterRuntime::monitorexit
核心代码如下:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
// 这个h_obj封装了该线程和拥有锁的对象
Handle h_obj(thread, elem->obj());
if (elem == NULL || h_obj()->is_unlocked()) {
// 锁对象关联为空或是已经解锁,就抛出IllegalMonitorStateException
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
// 调用slow_exit方法解除该线程的锁拥有权
ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
elem->set_obj(NULL);
2.10.2 ObjectSynchronizer::slow_exit
void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) {
// 直接调用fast模式退出
fast_exit (object, lock, THREAD) ;
}
2.10.3 ObjectSynchronizer::fast_exit
核心代码如下:
void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
// if displaced header is null, the previous enter is recursive enter, no-op
// dhw暂存MrkWord副本
markOop dhw = lock->displaced_header();
markOop mark ;
if (dhw == NULL) {
// Recursive stack-lock.
// Diagnostics -- Could be: stack-locked, inflating, inflated.
// 对象头的MarkWord
mark = object->mark() ;
if (mark->has_locker() && mark != markOopDesc::INFLATING()) {
assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ;
}
if (mark->has_monitor()) {
ObjectMonitor * m = mark->monitor() ;
assert(((oop)(m->object()))->mark() == mark, "invariant") ;
assert(m->is_entered(THREAD), "invariant") ;
}
return ;
}
}
0x03 synchronized对比ReentrantLock
可重入性 | 等待可中断 | 公平性 | 绑定对象数 | 性能优化 | |
---|---|---|---|---|---|
synchronized | 支持 | 不支持 | 非公平 | 只能1个 | 较多 |
ReentrantLock | 支持 | 支持 | 非公平/公平 | 可以多个 | - |
0x04 对象锁和类锁
ObjectMonitor分为对象锁和类锁,控制范围不同,可以查看这篇文章:Java-并发-锁-synchronized之对象锁和类锁
0x05 总结
synchronized在JDK和用户代码中大量使用,JDK作者们以后的优化方向肯定也会是synchronized。因为其底层采用cpp所写,既然现在都能提出那么多锁优化的内容,那可见优化空间还是有的,想必以后synchronized会越来越快。
关于锁优化和锁升级知识的更多详细内容请点击此文:Java-并发-关于锁的一切
0xFF 参考文档
Java 8 并发篇 - 冷静分析 Synchronized(下)
Synchronized的原理及自旋锁,偏向锁,轻量级锁,重量级锁的区别