1、工具:JOL = Java Object Layout
1.1、JOL maven 配置
<dependencies>
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
</dependencies>
1.2、jdk8u: markOop.hpp 代码片段
// 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)
1.3、测试代码
import org.openjdk.jol.info.ClassLayout;
/**
* @description: 锁测试
* @author: erlang
* @since: 2020-12-24 23:46
*/
public class SyncTest {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized(SyncTest.class) {
System.out.println(2);;
}
}
public synchronized void syncMethod() {
System.out.println(1);
}
public static synchronized void syncStaticMethod() {
System.out.println(1);
}
public void sync() {
synchronized (this) {
System.out.println(1);
}
}
}
2、synchronized
2.1、互斥同步
互斥同步(Mutual Exclusion & Synchronization)是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。因此,在这四个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
2.2、实现原理与应用
JVM 规范中可以看到 synchronized 在 JVM 里的实现原理,JVM 基于进入和退出的 Monitor 对象来实现方法同步和代码块同步,但是两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,在静态方法和方法上加锁是在方法的 flags 中加入 ACC_SYNCHRONIZED。 JVM 运行方法时检查方法的 flags,遇到同步标识开始启动前面的加锁流程;在方法内部遇到 monitorenter 指令开始加锁。如图所示,我们把上面测试代码编译成 class 文件,然后通过 javap -v SyncTest.class。
上图可以看到 synchronized 关键字经过汇编之后,会在同步代码块的前后形成 monitorenter 和 monitorexit 这两个字节码指令,当一个线程试图访问同步代码块时,它首选必须得到锁,退出或抛异常时必须释放锁。
这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,Java 中的每一个对象都可以作为锁,具体表现为以下三种情况:
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的 Class 对象
- 对于同步方法快,锁是 synchronized 括号里配置的对象
这里的 reference 对象尽量不要用 String、Integer、Long 等类型的,否则有可能存在其他未知的 bug。
JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之匹配。任何对象都有一个 Monitor 与之关联,当且一个 Monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 Monitor 的所有权,即尝试获得对象的锁。
根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加 1,相应的在执行 monitorexit 指令时,会将计数器减 1,当计数器为 0 时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等地啊,直到锁对象被另外一个线程释放为止。
在虚拟机规范对 monitorenter 和 monitorexit 的行为描述中,需要注意两点:
- synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
- 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
Java 的线程是映射到操作系统的原生线程之上的,如果阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到内核态中,因此状态转换需要消耗很多的处理器时间。对于代码简单的代码块,状态转换消耗的时间可能比用户代码执行的时间还要长。所以 synchronized 是 Java 语言中一个重量级的(Heavyweight)操作。通常都是在确实有必要的情况下才使用这种操作。而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待的过程,避免频繁地切入到内核空间。
2.3、Mark Word 实现表
synchronized 用的锁是存在 Java 对象头(Header)里的,HotSpot 虚拟机的对象头把包括两部分信息,第一部分用于存储对象自身的运行是数据,如 HashCode、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳,这部分数据的长度在 32 位(4字节)和 64 位(8字节)的虚拟机(未开启压缩指针 COOPs)中分别是 32 位和 64 位,官方称为 Mark Word。
另一部分是类型指针(Klass Word),即对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。并不是所有的虚拟机实现都是必须在对象数据上保留类型指针;换句话说,查找对象的元数据信息并不一定要经过对象本身。另外,如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象大小,但是从数组的元数据中却无法确定数组的大小。
在 64 位虚拟机下, Mark Word 是 64 位大小的,其存储结构如下
在 32 位虚拟机下,Mark Word 是 32 位大小的,其存储结构如下:
锁标识如下
锁分类 | 30bit/62bit | lock 锁标识 2bit | |
---|---|---|---|
无锁 | biased_lock 0 | 0 | 1 |
偏向锁 | biased_lock 1 | 0 | 1 |
轻量级锁 | ptr_to_lock_record | 0 | 0 |
重量级锁 | ptr_to_heavyweight_monitor | 1 | 0 |
进入 GC | 空 | 1 | 1 |
2.4、Hotspot JVM 层级
2.4.1、ObjectMonitor 结构
每个对象都有一个与之关联的 ObjectMonitor,属性如下所示
ObjectMonitor() {
_header = NULL;
_count = 0; // 用来记录该线程获取锁的次数
_waiters = 0, // 等待线程数
_recursions = 0; // 锁的重入次数
_object = NULL;
_owner = NULL; // 当前持有 ObjectMonitor 的线程
_WaitSet = NULL; // 调用了 wait 方法的线程被阻塞 放置在这里
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 等待锁 处于block的线程 有资格成为候选资源的线程
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
_WaitSet
protected:
ObjectWaiter * volatile _WaitSet; // LL of threads wait()ing on the monitor
_EntryList
protected:
ObjectWaiter * volatile _EntryList ; // Threads blocked on entry or reentry.
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);
};
可以看出来 EntryList 和 WaitSet 都是 ObjectWaiter 类型,可以看出是一个双向链表的集合( next,prev)。对象关联的 ObjectMonitor 对象有一个线程内部竞争锁的机制,如下图所示:
2.4.2、JDK6 以前 synchronized 实现步骤如下
- 当有两个线程 1、2 都要开始对我们的共享变量进行操作的时候,发现方法上加了 synchronized 锁,这时线程调度到线程 1 执行,线程 1 就抢先拿到了锁。拿到锁的步骤为:
- 将 MonitorObject 中的 _owner 设置成线程 1
- 将 Mark Word 设置为 Monitor 对象地址,锁标志位改为 10(重量级锁)
- 将线程 2 阻塞放到 ContentionList 竞争队列
- JVM 每次从 Waiting Queue 的尾部取出一个线程放到 OnDeck 作为候选者,但是如果并发比较高,Waiting Queue 会被大量线程执行 CAS 操作。为了降低对尾部元素的竞争,将 Waiting Queue 拆分 ContentionList 和 EntryList 两个队列, JVM 将一部分线程移到 EntryList 作为准备进入 OnDeck 的预备线程。
- 所有请求锁的线程,首先被放在 ContentionList 这个竞争队列中
- ContentionList 中那些有资格成为候选资源的线程被移动到 EntryList 中
- 任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck
- 当前已经获取到锁资源的线程被称为 Owner
- 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(在 Linux 内核下采用 pthread_mutex_lock 内核函数实现的);这里的线程被阻塞后,便进入内核调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能
- 作为 Owner 的线程 1 在执行过程中,可能调用 wait 方法释放锁,这时候线程 1 进入 WaitSet, 等待被唤醒。
2.4.3、synchronized 是非公平的
- synchronized 在线程竞争锁时,首先做的不是直接进 ContentionList 队列排队,而是尝试自旋获取锁(这时 ContentionList 中有别的线程在等锁),如果获取不到才进入 ContentionList,这明显对于已经进入队列的线程是不公平的;
- 另一个不公平的是,自旋获取锁的线程,还可能直接抢占 OnDeck 线程的锁资源。
2.4.4、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
2.4.5、偏向锁代码
从下面的代码中可以看出,偏向锁的实现具体代码在 BiasedLocking::revoke_and_rebias 中
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");
// 批量撤销,底层调用 bulk_revoke_or_rebias_at_safepoint
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
slow_enter (obj, lock, THREAD) ;
}
2.4.6、轻量级锁代码
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);
}
当线程 1 和线程 2 同时执行到临界区 if (mark->is_neutral()) 时,会出现下面的情况
- 线程 1 和线程 2 都把对象头的 Mark Word 保存到各自的 _displaced_header 字段,该数据保存在线程的栈帧上,是线程私有的
- Atomic::cmpxchg_ptr 属于原子操作,保障了只有一个线程可以把 Mark Word 中替换成指向自己线程栈上的 _displaced_header(官方称为 Displaced Mark Word )。假设线程 1 执行成功,相当于线程 1 获取到了锁,开始继续执行同步代码块。
- 线程 2 执行失败,退出临界区,通过 ObjectSynchronizer::inflate 膨胀为重量级锁