本文简述 Synchronized 关键字,从以下几个方面介绍:
前置知识
Synchronized 横切面讲解
锁升级过程
锁消除
锁粗化
锁降级
持续更新中~
JDK 早起,synchronized 叫做重量级锁,因为申请锁资源必须通过 kernel,系统调用。
;hello.asm
;write(int fd, const void *buffer, size_t nbytes)
section data
msg db "Hello", 0xA
len equ $ - msg
section .text
global _start
_start:
mov edx, len
mov ecs, msg
mov ebx, 1 ;文件描述符1 std_out
mov eax, 4 ;write 函数系统调用号 4
int 0x80
mov ebx, 0
mov eax, 1 ;exit 函数系统调用号
int 0x80
CAS
CAS
即 Compare And Swap
(Compare And Exchange
/ Compare And Set
) / 自旋 / 自旋锁 / 无锁(无重量锁)。因为经常配合循环操作,直到完成为止,所以泛指一类操作。
cas(v, a, b)
变量 v,期待值 a,修改值 b。
自旋就是一直在空转等待,一直等到 CAS
操作成功为止。
ABA
问题形象化解释:你的女朋友离开你的时间中经历了别人。
如何解决:版本号 AtomicStampedReference
,基础类型简单值不需要版本号。
跟踪 CAS
调用链,我们以 JDK 1.8
的 AtomicInteger
的 incrementAndGet
方法为例,进行跟踪:
Step1
:java.util.concurrent.atomic.AtomicInteger
的 incrementAndGet
方法
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
Step2
:sun.misc.Unsafe
的 getAndAddInt
方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Step3
:sun.misc.Unsafe
的 compareAndSwapInt
方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Step4
:unsafe.cpp
(jdk8u
)中的 cmpxchg
,即 compare and exchange
。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
Step5
:atomic_linux_x86.inline.hpp
(jdk8u
)is_MP
= Multi Processor
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
Step6
:os.hpp
(jdk8u
)is_MP()
static inline bool is_MP() {
// During bootstrap if _processor_count is not yet initialized
// we claim to be MP as that is safest. If any platform has a
// stub generator that might be triggered in this phase and for
// which being declared MP when in fact not, is a problem - then
// the bootstrap routine for the stub generator needs to check
// the processor count directly and leave the bootstrap routine
// in place until called after initialization has ocurred.
return (_processor_count != 1) || AssumeMP;
}
Step7
:atomic_linux_x86.inline.hpp
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
Step8
:最终实现: cmpxchg
= cas
修改变量值
lock cmpxchg 指令
Step9
:lock
指令在执行后面指令的时候锁定一个北桥信号,不采用锁总线的方式。
Markword
详见 JVM
中的 Java 对象内存布局。
JOL
工具(Java Object Layout
)JOL
可以查看 Java
对象内存布局。
<dependencies>
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol-coreartifactId>
<version>0.9version>
dependency>
dependencies>
markOop.hpp
(jdk8u
)
// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 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)
Synchronzied
橫切面讲解/**
* synchronized 底层实现
*/
package com.xiumei.multithreadinghighconcurrency.basicconcept.sync;
import org.openjdk.jol.info.ClassLayout;
public class T02_Sync1 {
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
synchronized 修饰方法:ACC_SYNCHRONIZED 访问修饰符
synchronized 同步语句块:monitorenter monitorexit
0 new #2
3 dup
4 invokespecial #1 >
7 astore_1
8 getstatic #3
11 aload_1
12 invokestatic #4
15 invokevirtual #5
18 invokevirtual #6
21 aload_1
22 dup
23 astore_2
24 monitorenter
25 getstatic #3
28 aload_1
29 invokestatic #4
32 invokevirtual #5
35 invokevirtual #6
38 aload_2
39 monitorexit
40 goto 48 (+8)
43 astore_3
44 aload_2
45 monitorexit
46 aload_3
47 athrow
48 return
JVM
层级(Hotspot
)C++ 调用了操作系统提供的同步机制。
Java 对象内存布局:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) c8 f7 70 02 (11001000 11110111 01110000 00000010) (40957896)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
interpreterRuntime.cpp
的 InterpreterRuntime:: monitorenter
方法
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
synchronizer.cpp
的 revoke_and_rebias
fast_enter
slow_enter
方法
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) ;
}
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
assert(!mark->has_bias_pattern(), "should not see bias pattern here");
if (mark->is_neutral()) {
// Anticipate successful CAS -- the ST of the displaced mark must
// be visible <= the ST performed by the CAS.
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
// Fall through to inflate() ...
} else
if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
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
// 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);
}
inflate 方法:膨胀为重量级锁
X86:lock cmpxchg xxx[1]
JDK8
markword
实现表synchronized 优化与 markword 相关,使用 markword 中最低三位表示锁状态,其中一位是偏向锁,剩余两位是普通锁(具体见 markword 实现表)。
偏向锁和自旋锁属于用户空间锁,都在用户空间完成的。
重量级锁是需要向内核申请。
Object o = new Object()
锁 = 001 无锁态
注意:如果偏向锁打开,默认是匿名偏向状态。
o.hashCode()
001 + hashcode
00000001 10101101 00110100 00110110
01011001 00000000 00000000 00000000
little endian big endian
默认 synchronized(o)
00 -> 轻量级锁
默认情况下,偏向锁有时延,默认是 4 秒(可通过 -XX:BaisedLockingStartupDelay
参数进行设置)。因为,JVM
虚拟机有自己一些默认启动的线程,有很多 sync 代码,这些 sync 代码启动时,肯定会有竞争。如果使用偏向锁,就会造成偏向锁不断地进行锁撤销和锁升级的操作,效率较低。
-XX:BaisedLockingStartupDelay=0
若设置了上述参数,则标识立即启动偏向锁,则上述 new 出的对象,会上为 101 的匿名偏向锁。
若线程上锁
上偏向锁,指的是,把 markword 的线程 ID 改为自己的线程 ID。
偏向锁不可重偏向,批量偏向,批量撤销。
如果线程有竞争
撤销偏向锁,升级为轻量级锁
线程在自己的线程栈生成 LockRecord,用 CAS 操作将 markword 设置为指向自己的线程的 LockRecord 的指针,设置成功者得锁。
如果竞争加剧
为什么有自旋锁还需要重量级锁?
自旋消耗 CPU 资源,如果锁的时间长,或者自旋线程多,CPU 会被大量消耗。
重量级锁有等待队列(参考 Hotspot 的 objectMonitor.hpp 实现),所有拿不到锁的线程进入等待队列,不需要消耗 CPU 资源。
偏向锁是否一定比自旋效率高?
不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时直接使用自旋锁。
例如,JVM 启动过程,会有很多线程竞争(明确知道),所以默认情况下不打开偏向锁,过段时间再打开。
轻量级锁重量级锁的 hashCode 存在于什么地方?
线程栈中,轻量级锁的LR中,或是代表重量级锁的 ObjectMonitor 的成员中。
关于epoch: (不重要)
批量重偏向与批量撤销渊源:从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到 safe point 时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。
原理以 class 为单位,为每个 class 维护解决场景批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。
一个偏向锁撤销计数器,每一次该 class 的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM 就认为该 class 的偏向锁有问题,因此会进行批量重偏向。每个 class 对象会有一个对应的epoch字段,每个处于偏向锁状态对象的 Mark Word 中也有该字段,其初始值为创建该对象时 class 中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历 JVM 中所有线程的栈,找到该 class 所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的 epoch 值和 class 的 epoch 不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过 CAS 操作将其 Mark Word 的 Thread Id 改成当前线程 Id。当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM 就认为该 class 的使用场景存在多线程竞争,会标记该 class 为不可偏向,之后,对于该 class 的锁,直接走轻量级锁的逻辑。
synchronized 是可重入锁
重入次数必须记录,因为要解锁几次必须得对应
偏向锁 自旋锁 -> 线程栈 -> LR + 1
Synchronized
VS Lock
(CAS
)在高争用、高耗时的环境下 synchronized
效率更高
在低争用 低耗时的环境下 CAS
效率更高
synchronized
到重量级之后是等待队列(不消耗 CPU
)CAS
(等待期间消耗 CPU
)lock eliminate
)public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
StringBuffer
是线程安全的,因为它的关键方法都是被 synchronized
修饰过的,但我们看上面这段代码,我们会发现,sb
这个引用只会在 append
方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb
是不可能共享的资源,JVM
会自动消除 StringBuffer
对象内部的锁。
lock coarsening
)public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}
JVM
会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM
就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。
锁降级参考:https://www.zhihu.com/question/63859501
文章从多方面介绍了 synchronized 关键字,包括底层原理层面破解,横切面剖析,以及锁升级,锁消除,锁粗化,锁降级,帮助大家更好地理解 synchronized 关键字。
[1] Hotspot 术语汇编
[2] 锁降级
[3] Java 使用字节码和汇编语言同步分析 volatile,synchronized 的底层实现