深入理解 synchronized 关键字

深入理解 synchronized 关键字

如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,我们需要对线程进行同步,那么synchronized就是实现线程同步的关键字,是用来保证被锁定了代码同一时间只能有一个线程执行,那么synchronized关键字的实现原理是怎样的呢?

先给出一张图,看看 synchronized 这个关键字有多复杂,先看看能看懂多少,如果看不太懂,希望看完本文能真正理解 synchronized 关键字和这张图。

synchronized 用法

synchronized 在用法上可以分为如下四种:普通方法、对象、静态方法和类。

1、普通方法

public synchronized void method() {}

2、对象

public void method() {
synchronized(obj) {
}
}

3、静态方法

public static synchronized void method() {}

4、类

public static void method() {
synchronized(Obj.class) {
}
}

但是我们要知道,syncnronized 锁的并不是代码块而是对象,锁静态方法也是锁住这个类。

synchronized 特性

  • 原子性

  • 可见性

  • 有序性

  • 可重入性

synchronized 实现原理

字节码

通过查看字节码来看看 synchronized 在字节码程度实现。

1、先看看在 synchronized 修饰在方法上

public synchronized void method() {}

我们进入到编译后的 classes 目录下,找到对应的类,使用 javap -v XX.class

public synchronized void method();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED // 注意这个
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/cuzz/syn/Test;

我们发现在 flags 上有一个 ACC_SYNCHRONIZED 标识。

2、对象上

public void method() {
synchronized(obj) {
}
}

反编译结果,在字节码成面上有 monitorenter 和 monitorexit,其中后面一个 monitorexit 表示异常退出。

public void method();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3
4: dup
5: astore_1
6: monitorenter // ---> 进入
7: aload_1
8: monitorexit // ---> 退出
9: goto 17
12: astore_2
13: aload_1
14: monitorexit // ---> 异常退出
15: aload_2
16: athrow
17: return

montor

1、monitorenter JVM 规范中描述

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

  • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
  • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
  • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

2、monitorexit JVM 规范中描述

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

小结

通过 javap 反汇编我们看到 synchronized 使用编程了 monitorentor 和 monitorexit 两个指令:

  • 每个锁对象都会关联一个 monitor (监视器它才是真正的锁对象)。
  • 它内部有两个重要的成员变量 owner 会保存获得锁的线程,recursions 会保存线程获得锁的次数。
  • 当执行到 monitorenter 时,recursions 会+1;当执行到 monitorexit 时,recursions会-1,当计数器减到 0 时这个线程就会释放锁。
  • 同步方法在反汇编后,会增加 ACC_SYNCHRONIZED 修饰,会隐式调用 monitorenter 和 monitorexit。在执行同步方法前会调用 monitorenter,在执行完同步方法后会调用 monitorexit。

深入 JVM 源码

monitor 监视锁

先下载 JVM 源码, https://github.com/openjdk/jdk 导入 CLion 中,切到 1.8 tag 上。

在HotSpot虚拟机中,monitor 是由 ObjectMonitor 实现的。其源码是用 C++ 来实现的,位于HotSpot虚 拟机源码 ObjectMonitor.hpp 文件中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor 主要数据结构如下:

ObjectMonitor() {
_header = NULL;
_count = 0; // 用来记录该对象被线程获取锁的次数
_waiters = 0,
_recursions = 0; // 线程的重入次数
_object = NULL; // 储存改monitor的对象
_owner = NULL; // 标识拥有该monitor的线程
_WaitSet = NULL; // 处于wait状态的线程,会加入到其中
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 多线程竞争锁的单向列表
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会加入该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

关键属性解释:

  • _owner:初始时为NULL,当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL,owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
  • _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链表)。_cxq是一个临界资 源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的栈。
  • _EntryList_cxq队列中有资格成为候选资源的线程会被移动到该队列中。
  • _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。

具体过程:

  • 当多个线程同时访问该方法,那么这些线程会先被放进_EntryList队列,此时线程处于blocking状态
  • 当一个线程获取到了实例对象的监视器(monitor)锁,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象_owner指向当前线程,_count加1表示当前对象锁被一个线程获取
  • 当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,同时线程进入_WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程重新获取monitor对象进入_Owner区
  • 如果当前线程执行完毕,那么也释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为 null

ObjectMonitor 的数据结构中包含:_owner、_WaitSet 和 _EntryList,它们之间的关系转换可以用下图表示:

每一个 Java 对象都可以与一个监视器 monitor 关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被 synchronized 圈起来的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的monitor。

我们的 Java 代码里不会显示地去创造这么一个monitor对象,我们也无需创建,事实上可以这么理解:

  • monitor并不是随着对象创建而创建的。
  • 我们是通过synchronized修饰符告诉 JVM 需要为我们的某个对象创建关联的monitor对象。
  • 每个线程都存在两个ObjectMonitor 对象列表,分别为free和used列表。 同时JVM中也维护着global locklist。
  • 当线程需要ObjectMonitor对象时,首先从线程自身的free表中申请,若存在则使用,若不存在则从global list中申请。

monitor 竞争

在字节码上 monitorenter,最终会调用 InterpreterRuntime.cpp

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");
// 是否使用偏向锁
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

最终调用 ObjectMonitor::enter 方法

void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
// _owner为NULL表示无锁
// 通过CAS操作吧monitor的_owner字段设置为当前线程
// 这个会根据不同的操作系统有不同的实现如果是Linux,则在atomic_linux_x86.inline.hpp中
// 返回旧值
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
// 设置成功
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
// 线程重入,_recursions++
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
// 如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程
// 当前线程是之前持有轻量级锁的线程。由轻量级锁膨胀且第一次调用enter方法,那cur是指向Lock Record的指针
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}

...

// 在调用系统的同步操作之前,先尝试自旋获得锁
if (Knob_SpinEarly && TrySpin (Self) > 0) {
...
//自旋的过程中获得了锁,则直接返回
Self->_Stalled = 0 ;
return ;
}
// 通过自旋执行ObjectMonitor::EnterI方法等待锁的释放
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don't want to enter
// the monitor while suspended because that would surprise the
// thread that suspended us.
//
_recursions = 0 ;
_succ = NULL ;
exit (Self) ;
jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}

代码具体逻辑:

  • 通过CAS尝试吧monItor的_owner设置为当前线程
  • 如果设置之前的_owner指向当前线程,说明线程再次进入monitor,为重入锁,_recursions++,记录重入得次数
  • 如果当前线程是第一次进入monitor,设置_recursions为1,_owner为当前线程,改线程获得锁
  • 如果获得锁失败,则等待锁的释放

monitor 等待

竞争失败等待调用的是ObjectMonitor对象的EnterI方法。

void ATTR ObjectMonitor::EnterI (TRAPS) {
Thread * Self = THREAD ;
...
// 尝试获得锁
if (TryLock (Self) > 0) {
...
return ;
}

DeferredInitialize () ;

// 自旋
if (TrySpin (Self) > 0) {
...
return ;
}

...

// 将线程封装成node节点中
ObjectWaiter node(Self) ;
Self->_ParkEvent->reset() ;
node._prev = (ObjectWaiter *) 0xBAD ;
node.TState = ObjectWaiter::TS_CXQ ;

// 将node节点插入到_cxq队列的头部,cxq是一个单向链表
ObjectWaiter * nxt ;
for (;;) {
node._next = nxt = _cxq ;
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;

// CAS失败的话 再尝试获得锁,这样可以降低插入到_cxq队列的频率
if (TryLock (Self) > 0) {
...
return ;
}
}

// SyncFlags默认为0,如果没有其他等待的线程,则将_Responsible设置为自己
if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}


TEVENT (Inflated enter - Contention) ;
int nWakeups = 0 ;
int RecheckInterval = 1 ;

for (;;) {
// 线程在被挂起前再尝试一次,看能不能获得到锁
if (TryLock (Self) > 0) break ;
assert (_owner != Self, "invariant") ;

...

// park self
if (_Responsible == Self || (SyncFlags & 1)) {
// 当前线程是_Responsible时,调用的是带时间参数的park
TEVENT (Inflated enter - park TIMED) ;
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
RecheckInterval *= 8 ;
if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
//否则直接调用park挂起当前线程
TEVENT (Inflated enter - park UNTIMED) ;
Self->_ParkEvent->park() ;
}

if (TryLock(Self) > 0) break ;

...

if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ;

...
// 在释放锁时,_succ会被设置为EntryList或_cxq中的一个线程
if (_succ == Self) _succ = NULL ;

// Invariant: after clearing _succ a thread *must* retry _owner before parking.
OrderAccess::fence() ;
}

// 走到这里说明已经获得锁了

assert (_owner == Self , "invariant") ;
assert (object() != NULL , "invariant") ;

// 将当前线程的node从cxq或EntryList中移除
UnlinkAfterAcquire (Self, &node) ;
if (_succ == Self) _succ = NULL ;
if (_Responsible == Self) {
_Responsible = NULL ;
OrderAccess::fence();
}
...
return ;
}

当线程被唤醒时,会从挂起点继续执行,通过ObjectMonitor::TryLock尝试获取锁。

int ObjectMonitor::TryLock (Thread * Self) {
for (;;) {
void * own = _owner ;
if (own != NULL) return 0 ;
if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
// Either guarantee _recursions == 0 or set _recursions = 0.
assert (_recursions == 0, "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert that OwnerIsThread == 1
return 1 ;
}
// The lock had been free momentarily, but we lost the race to the lock.
// Interference -- the CAS failed.
// We can either return -1 or retry.
// Retry doesn't make as much sense because the lock was just acquired.
if (true) return -1 ;
}
}

上面代码具体流程:

  • 将当前线程插入到cxq队列的队首
  • 然后park当前线程
  • 当被唤醒后再尝试获得锁

monitor 释放

当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其它线程机会执行同步代码,在 HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于 ObjectMonitor的exit方法中。

void ATTR ObjectMonitor::exit(TRAPS) {
Thread * Self = THREAD ;

...

// 看看_recursions 是否为0
if (_recursions != 0) {
_recursions--; // this is simple recursive enter
TEVENT (Inflated exit - recursive) ;
return ;
}

// Invariant: after setting Responsible=null an thread must execute
// a MEMBAR or other serializing instruction before fetching EntryList|cxq.
if ((SyncFlags & 4) == 0) {
_Responsible = NULL ;
}

for (;;) {
assert (THREAD == _owner, "invariant") ;

guarantee (_owner == THREAD, "invariant") ;

ObjectWaiter * w = NULL ;
int QMode = Knob_QMode ;

// QMode = 2:直接绕过EntryList队列,从_cxq队列中获取线程用于竞争锁
if (QMode == 2 && _cxq != NULL) {
w = _cxq ;
assert (w != NULL, "invariant") ;
assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
ExitEpilog (Self, w) ;
return ;
}
// Qmode = 3: _cxq队列插入EntryList尾部
if (QMode == 3 && _cxq != NULL) {
w = _cxq ;
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
assert (w != NULL , "invariant") ;

ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}

// Append the RATs to the EntryList
// TODO: organize EntryList as a CDLL so we can locate the tail in constant-time.
ObjectWaiter * Tail ;
for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ;
if (Tail == NULL) {
_EntryList = w ;
} else {
Tail->_next = w ;
w->_prev = Tail ;
}

// Fall thru into code that tries to wake a successor from EntryList
}
// Qmode = 4: _cxq队列插入EntryList头部
if (QMode == 4 && _cxq != NULL) {
w = _cxq ;
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
assert (w != NULL , "invariant") ;

ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}

// Prepend the RATs to the EntryList
if (_EntryList != NULL) {
q->_next = _EntryList ;
_EntryList->_prev = q ;
}
_EntryList = w ;

// Fall thru into code that tries to wake a successor from EntryList
}

w = _EntryList ;
if (w != NULL) {

assert (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
ExitEpilog (Self, w) ;
return ;
}

// If we find that both _cxq and EntryList are null then just
// re-run the exit protocol from the top.
w = _cxq ;
if (w == NULL) continue ;

// Drain _cxq into EntryList - bulk transfer.
// First, detach _cxq.
// The following loop is tantamount to: w = swap (&cxq, NULL)
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
TEVENT (Inflated exit - drain cxq into EntryList) ;

assert (w != NULL , "invariant") ;
assert (_EntryList == NULL , "invariant") ;


// QMode = 1 : 将_cxq中的元素转移到EntryList,并反转顺序
if (QMode == 1) {
// QMode == 1 : drain cxq to EntryList, reversing order
// We also reverse the order of the list.
ObjectWaiter * s = NULL ;
ObjectWaiter * t = w ;
ObjectWaiter * u = NULL ;
while (t != NULL) {
guarantee (t->TState == ObjectWaiter::TS_CXQ, "invariant") ;
t->TState = ObjectWaiter::TS_ENTER ;
u = t->_next ;
t->_prev = u ;
t->_next = s ;
s = t;
t = u ;
}
_EntryList = s ;
assert (s != NULL, "invariant") ;
} else {
// QMode == 0 or QMode == 2
_EntryList = w ;
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}
}

// In 1-0 mode we need: ST EntryList; MEMBAR #storestore; ST _owner = NULL
// The MEMBAR is satisfied by the release_store() operation in ExitEpilog().

// See if we can abdicate to a spinner instead of waking a thread.
// A primary goal of the implementation is to reduce the
// context-switch rate.
if (_succ != NULL) continue;

w = _EntryList ;
if (w != NULL) {
guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
ExitEpilog (Self, w) ;
return ;
}
}
}

退出同步代码块时会让_recursions减1,当_recursions的值减为0时,说明线程释放了锁。

根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过
ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作最终由unpark完成,实现如下:

void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
assert (_owner == Self, "invariant") ;

// Exit protocol:
// 1. ST _succ = wakee
// 2. membar #loadstore|#storestore;
// 2. ST _owner = NULL
// 3. unpark(wakee)

_succ = Knob_SuccEnabled ? Wakee->_thread : NULL ;
ParkEvent * Trigger = Wakee->_event ;

// Hygiene -- once we've set _owner = NULL we can't safely dereference Wakee again.
// The thread associated with Wakee may have grabbed the lock and "Wakee" may be
// out-of-scope (non-extant).
Wakee = NULL ;

// Drop the lock
OrderAccess::release_store_ptr (&_owner, NULL) ;
OrderAccess::fence() ; // ST _owner vs LD in unpark()

if (SafepointSynchronize::do_call_back()) {
TEVENT (unpark before SAFEPOINT) ;
}

DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);
// 唤醒之前被pack()挂起的线程
Trigger->unpark() ;

// Maintain stats and report events to JVMTI
if (ObjectMonitor::_sync_Parks != NULL) {
ObjectMonitor::_sync_Parks->inc() ;
}
}

被唤醒的线程,会回到EnterI方法中的继续执行monitor的竞争。

for (;;) {

if (TryLock (Self) > 0) break ;
assert (_owner != Self, "invariant") ;

if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}

// park self
if (_Responsible == Self || (SyncFlags & 1)) {
TEVENT (Inflated enter - park TIMED) ;
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
RecheckInterval *= 8 ;
if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
TEVENT (Inflated enter - park UNTIMED) ;
Self->_ParkEvent->park() ;
}

if (TryLock(Self) > 0) break ;

...
}

monitor是重量级锁

可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数, 执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就 会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语 言中是一个重量级(Heavyweight)的操作。

用户态和和内核态是什么东西呢?要想了解用户态和内核态还需要先了解一下Linux系统的体系架构:

从上图可以看出,Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。

  • 内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。
  • 用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存 储资源、I/O资源等。
  • 系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);但是当它调用系统调用执行某些操作时,例如 I/O调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(或简称为内 核态)。 系统调用的过程可以简单理解为:

  1. 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈, 以此表明需要操作系统提 供的服务。
  2. 用户态程序执行系统调用。
  3. CPU切换到内核态,并跳到位于内存指定位置的指令。
  4. 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。
  5. 系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。
    由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器 值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在 synchronized未优化之前,效率低的原因。

JVM 对 synchronzied 底层优化

高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和如适应性自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等,这些技术都是为 了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

具体源码分析推荐看这几篇文章:死磕Synchronized底层实现

CAS

CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制。CAS 操作包含三个操作数:内存位置、预期数值和新值。CAS 的实现逻辑是将内存位置处的数值与预期数值想比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作。

在 Java 中,Java 并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的,Java 代码需通过 JNI 才能调用。

CAS实现并发,我们先看一段代码:

public class AtomicTest {
private static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = () -> {
for (int i = 0; i < 1000; i++) {
atomicInteger.incrementAndGet();
}
};

for (int i = 0; i < 10; i++) {
new Thread(r).start();
}
Thread.sleep(1000);
System.out.println(atomicInteger.get()); // 输出 10000
}
}

底层是调用 jdk.internal.misc.Unsafe#getAndAddInt 这个方法

JAVA对象布局

当一个线程尝试访问 synchronized 修饰的代码块时,它首先要获得锁,那么 这个锁到底存在哪里呢?

是存在锁对象的对象头里。

java对象布局 JOL(java object layout)描述对象在堆内存的布局,如图所示:

  • markword: 固定长度8字节,描述对象的 identityhashcode、GC分代年龄、锁的状态标志、线程持有的锁、偏向锁的线程ID和偏向时间戳等等;
  • klasspoint: 固定长度4字节, 指定该对象的class类对象(默认使用-XX:+UseCompressedClassPointers 参数进行压缩,可使用-XX:-UseCompressedClassPointers关闭,则该字段在64位jvm下占用8个字节;可使用java -XX:+PrintCommandLineFlags -version 命令查看默认的或已设置的jvm参数);
  • 基本变量:用于存放java八种基本类型成员变量,以4字节步长进行补齐,使用内存重排序优化空间;
  • 引用变量:存放对象地址,如String,Object;占用4个字节,64位jvm上默认使用-XX:+UseCompressedOops进行压缩,可使用-XX:-UseCompressedOops进行关闭,则在64位jvm上会占用8个字节;
  • 补齐:对象大小必须是8字节的整数倍,用来补齐字节数。Object o = new Object() 在内存中占用16个字节,其中最后4个是补齐;
  • 数组长度:如果是数组,额外占用固定4字节存放数组长度;

jol-core 是 openjdk的一个工具,他可以很方便的让我看到一个对象的布局。

<dependencies>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
</dependencies>

我们查看一下对象和数组

public class HeaderTest {
public static void main(String[] args) {

Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
System.out.println("===================");
int[] arr = new int[1];
System.out.println(ClassLayout.parseInstance(arr).toPrintable());
}
}

打印结果

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

===================
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 10 0c 00 00 (00010000 00001100 00000000 00000000) (3088)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
16 4 int [I.<elements> N/A
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类 型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,另外,arrayOopDesc 的定义对应 arrayOop.hpp 。

class instanceOopDesc : public oopDesc {
public:
// aligned header size.
static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

// If compressed, the offset of the fields of the instance may not be aligned.
static int base_offset_in_bytes() {
return UseCompressedOops ?
klass_gap_offset_in_bytes() :
sizeof(instanceOopDesc);
}

static bool contains_field_offset(int offset, int nonstatic_field_size) {
int base_in_bytes = base_offset_in_bytes();
return (offset >= base_in_bytes &&
(offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
}
};

从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot 源码中的 oop.hpp 文件中。

class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;

...

}
  • 在普通实例对象中,oopDesc的定义包含两个成员,分别是 _mark 和 _metadata。
  • _mark 表示对象标记、属于markOop类型,也就是接下来要讲解的Mark World,它记录了对象和锁有关的信息。
  • _metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针、 _compressed_klass 表示压缩类指针。
  • 对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指针,及对象指向它的类元数据的指针。

我们看一下markOop.hpp 中对的描述:

//  64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//
// - hash contains the identity hash value: largest value is
// 31 bits, see os::random(). Also, 64-bit vm's require
// a hash value no bigger than 32 bits because they will not
// properly generate a mask larger than that: see library_call.cpp
// and c1_CodePatterns_sparc.cpp.
//
// - the biased lock pattern is used to bias a lock toward a given
// thread. When this pattern is set in the low three bits, the lock
// is either biased toward a given thread or "anonymously" biased,
// indicating that it is possible for it to be biased. When the
// lock is biased toward a given thread, locking and unlocking can
// be performed by that thread without using atomic operations.
// When a lock's bias is revoked, it reverts back to the normal
// locking scheme described below.
//
// Note that we are overloading the meaning of the "unlocked" state
// of the header. Because we steal a bit from the age we can
// guarantee that the bias pattern will never be seen for a truly
// unlocked object.
//
// Note also that the biased state contains the age bits normally
// contained in the object header. Large increases in scavenge
// times were seen when these bits were absent and an arbitrary age
// assigned to all biased objects, because they tended to consume a
// significant fraction of the eden semispaces and were not
// promoted promptly, causing an increase in the amount of copying
// performed. The runtime system aligns all JavaThread* pointers to
// a very large value (currently 128 bytes (32bVM) or 256 bytes (64bVM))
// to make room for the age bits & the epoch bits (used in support of
// biased locking), and for the CMS "freeness" bit in the 64bVM (+COOPs).
//
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
// not valid at any other time

根据下面图理解

以及

无锁

先打印一下,如果我们不调用 hashCode 在markword上是不会显示

public class HeaderTest {
public static void main(String[] args) {

Object o = new Object();
System.out.println(Integer.toHexString(o.hashCode()));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}

打印结果

hashCode: 2d209079

OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 79 90 20 (00000001 01111001 10010000 00100000) (546339073)
4 4 (object header) 2d 00 00 00 (00101101 00000000 00000000 00000000) (45)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)

x86都是使用的小端模式,我们转为大端显示更直观一些

  • 我们看最后三位为 001 表示无锁。
  • 其中 hashCode [0 00101101 00100000 10010000 01111001]转换类 16 进制为 0x2d209079。
  • 分代年龄为 4 位,说明 JVM 回收最大存活为 15 代。
小端 00000001 01111001 10010000 00100000 00101101 00000000 00000000 00000000
大端 00000000 00000000 00000000 00101101 00100000 10010000 01111001 00000001

[00000000 00000000 0000000][0 00101101 00100000 10010000 01111001] [0][0000][0][01]
未使用 hashCode cms_free 分代年龄 偏向锁 锁标志

偏向锁

偏向锁是JDK 6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及 ThreadID 即可。

当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操 作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每 次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以 提高带有同步但无竞争的程序性能。

偏向锁在Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活(因为刚启动程序内部一般会有多个竞争),当然可以使用-XX:BiasedLockingStartupDelay=0参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过 -XX:UseBiasedLocking=false参数关闭偏向锁。

public class HeaderTest {
public static void main(String[] args) throws InterruptedException {
// 等待虚拟机开启偏向锁
Thread.sleep(5000);

Object o = new Object();

System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}

打印结果:

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 a0 00 a6 (00000101 10100000 00000000 10100110) (-1509908475)
4 4 (object header) bb 7f 00 00 (10111011 01111111 00000000 00000000) (32699)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

第一次打印为匿名偏向,第二次偏向锁指向了main 线程,第一个线程ID为0,第二有线程ID。

匿名偏向:
小端 00000101 00000000 00000000 00000000 00000000 00000000 00000000 00000000
大端 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

[00000000 00000000 00000000 00000000 00000000 00000000 000000][00] [00000][1][01]
线程ID 偏向时间戳 cms_free 分代年龄 偏向锁 锁标志

偏向main线程:
小端 00000101 10100000 00000000 10100110 10111011 01111111 00000000 00000000
大端 00000000 00000000 01111111 10111011 10100110 00000000 10100000 00000101

[00000000 00000000 01111111 10111011 10100110 00000000 101000][00] [0][0000][1][01]
线程ID 偏向时间戳 cms_free 分代年龄 偏向锁 锁标志

轻量级锁

轻量级锁是JDK 6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的。

引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如 果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要 替代重量级锁。

当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

  • 判断当前对象是否处于无锁状态,如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝(官方把这份拷贝加了一个 Displaced 前缀,即 Displaced Mark Word),将对象的 Mark Word 复制到栈帧中的 Lock Record 中,将 Lock Reocrd 中的 owner 指向当前对象。
  • JVM利用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,如果成功表示竞争到锁,则将锁标志位变成 00,执行同步操作。
  • 如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻 量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

轻量级锁CAS操作之前堆栈与对象的状态

轻量级锁CAS操作之后堆栈与对象的状态

public class HeaderTest {
public static void main(String[] args) throws InterruptedException {
// 等待虚拟机开启偏向锁
Thread.sleep(5000);

Object o = new Object();

synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

new Thread(() -> {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}).start();
}
}

打印结果:

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 c8 00 2f (00000101 11001000 00000000 00101111) (788580357)
4 4 (object header) d7 7f 00 00 (11010111 01111111 00000000 00000000) (32727)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 00 5a a9 03 (00000000 01011010 10101001 00000011) (61430272)
4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

发现从偏向锁转为轻量级锁

偏向锁:
大端 00000101 11001000 00000000 00101111 11010111 01111111 00000000 00000000
小端 00000000 00000000 01111111 11010111 00101111 00000000 11001000 00000101

轻量级锁:
大端 00000000 01011010 10101001 00000011 00000000 01110000 00000000 00000000
小端 00000000 00000000 01110000 00000000 00000011 10101001 01011010 00000000

[00000000 00000000 01110000 00000000 00000011 10101001 01011010 000000][00]
指向轻量级锁指针 锁标志

重量级锁

public class HeaderTest {
public static void main(String[] args) throws InterruptedException {
// 等待虚拟机开启偏向锁
Thread.sleep(5000);

Object o = new Object();

synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
new Thread(() -> {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}).start();

Thread.sleep(1000);

for (int i = 0; i < 2; i++) {
new Thread(() -> {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}).start();
}
Thread.sleep(1000);
}
}

展示了从偏向锁(101)-> 轻量级锁(000)-> 重量级锁(010)

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 80 33 (00000101 10010000 10000000 00110011) (864063493)
4 4 (object header) fc 7f 00 00 (11111100 01111111 00000000 00000000) (32764)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 00 3a d6 06 (00000000 00111010 11010110 00000110) (114702848)
4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 02 3f 21 33 (00000010 00111111 00100001 00110011) (857816834)
4 4 (object header) fc 7f 00 00 (11111100 01111111 00000000 00000000) (32764)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 02 3f 21 33 (00000010 00111111 00100001 00110011) (857816834)
4 4 (object header) fc 7f 00 00 (11111100 01111111 00000000 00000000) (32764)
8 4 (object header) 00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。

自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

锁消除

锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享 数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中, 堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们 是线程私有的,同步加锁自然就无须进行。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确 定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?实际上有 许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。下面这段非常简单的代码仅仅是输出3个字符串相加的结果,无论是源码字面上还是程序语义上 都没有同步。

public class Demo {
public static void main(String[] args) {
contactString("aa", "bb", "cc");
}

public static String contactString(String s1, String s2, String s3) {
return new StringBuffer().append(s1).append(s2).append(s3).toString();
}
}

StringBuffer的append ( ) 是一个同步方法,锁就是this也就是(new StringBuilder())。虚拟机发现它的 动态作用域被限制在concatString( )方法内部。也就是说, new StringBuilder()对象的引用永远不会“逃 逸”到concatString ( )方法之外,其他线程无法访问到它,因此,虽然这里有锁,但是可以被安全地消除 掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。

锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作 用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线 程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对 象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操 作也会导致不必要的性能损耗。

public class Demo {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100; i++) {
sb.append("aa");
}
System.out.println(sb.toString());
}
}

JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大, 放到这串操作的外面,这样只需要加一次锁即可。

写在最后

我看很很多视频和文章整理出来的比较,synchronized比较难,涉及到的知识特别多,包括并发、字节码、JVM、汇编以及计算机系统等。在 Java 1.6 以后 JVM 对 synchronized 进行了优化,存在偏向锁、轻量级锁和重量级锁等形式,尤其是锁的锁的升级和降级比较复杂,现在我们回过头再看看这张图,就很容易看懂了。

参考

Comments