Hello everyone welcome back to 小护士青铜上分系列之《Java源码阅读》,今天小护士将会进入java.util包的源码讲解。
So where to start?
这个很难选,因为util包有太多有趣的东西,例如,集合家族(Collection)、并发家族(Concurrency)、流式家族(Stream)、压缩家族(Zip)、正则家族(Regex)。
How about concurrency-family first?
Emmmm, I say yes.
并发家族内部有很多“小团队”。例如,原子操作、锁、并发集合、线程池、同步工具、ForkJoin模型等等。而并发家族的根基却是不太起眼的Unsafe
。它来自Java内部API,因为可以直接操作内存,所以它的方法都很危险,以此得名Unsafe
。
现在进入正题,小护士打算先是介绍Unsafe
大概有哪些方法,根据它们的逻辑进行归类;然后是介绍Unsafe
各个方法集具体都干了些啥,因为Unsafe
的方法大部分是本地方法,所以直接深入到JVM的C++源码是必须的。就酱紫。
从这里分类中,小护士大概可以了解到,其实Unsafe
是负责做内存访问和操作用的,把内存看成一个类似Map的结构;同时支持CAS原子操作(CompareAndSwap),用它来优雅完成多线程环境下值更新操作。Unsafe
还负责线程执行控制,利用park\monitor操作和load\store内存屏障操作完成多线程之间同步互斥处理。同时,它还提供了内存资源的分配、释放和拷贝等操作。
这里先给出源码位置,避免一脸懵逼的你一脸懵逼地看完小护士的博文啊。
Unsafe.java
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/java.base/share/classes/jdk/internal/misc/Unsafe.java
Unsafe.cpp
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/prims/unsafe.cpp
getXxx()系列方法是用来读取(fetch)Java对象所在内存地址对应的内存值。
以getInt()
为例:
@HotSpotIntrinsicCandidate
public native int getInt(Object o, long offset);
Object
,拥有一个对象的内存地址。getInt()
方法会利用第一个参数Object o
来获取该对象所在的内存地址,再根据这个内存地址的基础上,加上偏移量,得出一个真实的内存地址,最后从这个内存地址中加载对应的内存值。
如果把内存比喻为一个Map
的话,其中,key为地址值,value为内存值。
那么这个getInt()
方法就相当于:
long addr = o.getAddress()+offset;
int ret = map.get(addr);
后面会具体讲getXxx()系列方法中的getInt()
方法的实现细节。
putXxx()系列方法与getXxx()系列方法相对应,前者是写入内存值,后者是读取内存值。
以putInt()
方法为例:
@HotSpotIntrinsicCandidate
public native void putInt(Object o, long offset, int x);
Object
,拥有一个对象的内存地址。如果把内存再次比喻为一个Map
的话,其中,key为地址值,value为内存值。
那么这个putInt()
方法就相当于:
long addr = o.getAddress()+offset;
map.put(addr, x);
后面会具体讲putXxx()系列方法中的putInt()
方法的实现细节。
Well,这就是传说中的CAS(compare and swap);在JDK8版本中,方法名就是compareAndSwapXxx;但在JDK11里面,则是浪子回头,把方法名改成compareAndExchangeXxx,它的含义与Intel-x86指令集中的cmpxchg指令以及Linux内核中的cmpxchg函数相同。恐怕以后都要改口叫CAX(compare and exchange)了。
以compareAndSwapInt()
方法为例
public final native
boolean compareAndSwapInt(Object o, long offset, int expected, int x);
Object
,拥有一个对象的内存地址。如果把内存再次比喻为一个Map
的话,其中,key为地址值,value为内存值。
那么这个compareAndSwapInt()
方法就相当于:
long addr = o.getAddress() + offset;
int origin = map.get(addr);
boolean ret = origin == expect;
if(ret) {
map.put(addr, x);
}
也不是很难理解嘛,但里面还会有其他技术细节需要注意,例如ABA问题;这个在待会儿讲C++源码时会详细描述。BTW,除了compareAndExchange()
方法以外,JDK11还增加了compareAndSet()
方法。
park和monitor都是实现多线程同步互斥的手段。其中,park有park()
和unpark()
两个方法分别实现挂起和恢复功能。同理,monitor也有monitorEnter()
和monitorExit()
两个方法实现类似的功能,不过主要是上锁和解锁。遗憾的是,JDK11已经删除了monitor的机制,也就是说,假设小护士打算对JVM进行二次开发;就不可以通过Unsafe
类的monitor来模拟synchronized
语义了。
public native void unpark(Object thread);
public native void park(boolean isAbsolute, long time);
public native void monitorEnter(Object o);
public native void monitorExit(Object o);
fence,中文意思是栅栏,专业一点的翻译可以是内存屏障。小护士猜测这个xxxFence系列的方法就是用来模拟volatile
语义的。fence有三个方法,分别是loadFence()
、storeFence()
、fullFence()
。
public native void loadFence();
public native void storeFence();
public native void fullFence();
这三个方法都是为了防止指令重排序的。指令重排序的意思是操作系统在运行程序时,会执行一系列程序指令,而指令的执行顺序可以根据具体硬件平台(CPU、内存、32位字长、64位字长)的特点来做执行顺序上的优化,这种优化主要是编译时处理的。
到底如何加内存屏障呢,一会儿看个究竟。
memory,内存。Unsafe
有好几个管理内存的方法,分别是:
public native long allocateMemory(long bytes);
public native long reallocateMemory(long address, long bytes);
public native void setMemory(Object o, long offset, long bytes, byte value);
@HotSpotIntrinsicCandidate
private native void copyMemory0(Object srcBase, long srcOffset,
Object destBase, long destOffset,
long bytes);
private native void copySwapMemory0(Object srcBase, long srcOffset,
Object destBase, long destOffset,
long bytes, long elemSize);
public native void freeMemory(long address);
这里先说明一下,copyMemory方法在JDK11上的实现与JDK8的不同,加了一些内存检查,小护士觉得直接讲讲JDK11的实现吧,至于JDK8,who would care? 除了monitor机制会讲讲JDK8的以外,其他都是基于JDK11。而你看到的copySwapMemory0()
方法就是JDK11新增的。
Unsafe
还有其他很有趣的方法,例如计算offset
值的方法。小护士为了让大家不用翻源码就大概了解这些方法的分布情况,就把JDK11的C++源码中的方法数组声明部分贴在这里:
static JNINativeMethod jdk_internal_misc_Unsafe_methods[] = {
{CC "getObject", CC "(" OBJ "J)" OBJ "", FN_PTR(Unsafe_GetObject)},
{CC "putObject", CC "(" OBJ "J" OBJ ")V", FN_PTR(Unsafe_PutObject)},
{CC "getObjectVolatile",CC "(" OBJ "J)" OBJ "", FN_PTR(Unsafe_GetObjectVolatile)},
{CC "putObjectVolatile",CC "(" OBJ "J" OBJ ")V", FN_PTR(Unsafe_PutObjectVolatile)},
{CC "getUncompressedObject", CC "(" ADR ")" OBJ, FN_PTR(Unsafe_GetUncompressedObject)},
DECLARE_GETPUTOOP(Boolean, Z),
DECLARE_GETPUTOOP(Byte, B),
DECLARE_GETPUTOOP(Short, S),
DECLARE_GETPUTOOP(Char, C),
DECLARE_GETPUTOOP(Int, I),
DECLARE_GETPUTOOP(Long, J),
DECLARE_GETPUTOOP(Float, F),
DECLARE_GETPUTOOP(Double, D),
{CC "allocateMemory0", CC "(J)" ADR, FN_PTR(Unsafe_AllocateMemory0)},
{CC "reallocateMemory0", CC "(" ADR "J)" ADR, FN_PTR(Unsafe_ReallocateMemory0)},
{CC "freeMemory0", CC "(" ADR ")V", FN_PTR(Unsafe_FreeMemory0)},
{CC "objectFieldOffset0", CC "(" FLD ")J", FN_PTR(Unsafe_ObjectFieldOffset0)},
{CC "objectFieldOffset1", CC "(" CLS LANG "String;)J", FN_PTR(Unsafe_ObjectFieldOffset1)},
{CC "staticFieldOffset0", CC "(" FLD ")J", FN_PTR(Unsafe_StaticFieldOffset0)},
{CC "staticFieldBase0", CC "(" FLD ")" OBJ, FN_PTR(Unsafe_StaticFieldBase0)},
{CC "ensureClassInitialized0", CC "(" CLS ")V", FN_PTR(Unsafe_EnsureClassInitialized0)},
{CC "arrayBaseOffset0", CC "(" CLS ")I", FN_PTR(Unsafe_ArrayBaseOffset0)},
{CC "arrayIndexScale0", CC "(" CLS ")I", FN_PTR(Unsafe_ArrayIndexScale0)},
{CC "addressSize0", CC "()I", FN_PTR(Unsafe_AddressSize0)},
{CC "pageSize", CC "()I", FN_PTR(Unsafe_PageSize)},
{CC "defineClass0", CC "(" DC_Args ")" CLS, FN_PTR(Unsafe_DefineClass0)},
{CC "allocateInstance", CC "(" CLS ")" OBJ, FN_PTR(Unsafe_AllocateInstance)},
{CC "throwException", CC "(" THR ")V", FN_PTR(Unsafe_ThrowException)},
{CC "compareAndSetObject",CC "(" OBJ "J" OBJ "" OBJ ")Z", FN_PTR(Unsafe_CompareAndSetObject)},
{CC "compareAndSetInt", CC "(" OBJ "J""I""I"")Z", FN_PTR(Unsafe_CompareAndSetInt)},
{CC "compareAndSetLong", CC "(" OBJ "J""J""J"")Z", FN_PTR(Unsafe_CompareAndSetLong)},
{CC "compareAndExchangeObject", CC "(" OBJ "J" OBJ "" OBJ ")" OBJ, FN_PTR(Unsafe_CompareAndExchangeObject)},
{CC "compareAndExchangeInt", CC "(" OBJ "J""I""I"")I", FN_PTR(Unsafe_CompareAndExchangeInt)},
{CC "compareAndExchangeLong", CC "(" OBJ "J""J""J"")J", FN_PTR(Unsafe_CompareAndExchangeLong)},
{CC "park", CC "(ZJ)V", FN_PTR(Unsafe_Park)},
{CC "unpark", CC "(" OBJ ")V", FN_PTR(Unsafe_Unpark)},
{CC "getLoadAverage0", CC "([DI)I", FN_PTR(Unsafe_GetLoadAverage0)},
{CC "copyMemory0", CC "(" OBJ "J" OBJ "JJ)V", FN_PTR(Unsafe_CopyMemory0)},
{CC "copySwapMemory0", CC "(" OBJ "J" OBJ "JJJ)V", FN_PTR(Unsafe_CopySwapMemory0)},
{CC "setMemory0", CC "(" OBJ "JJB)V", FN_PTR(Unsafe_SetMemory0)},
{CC "defineAnonymousClass0", CC "(" DAC_Args ")" CLS, FN_PTR(Unsafe_DefineAnonymousClass0)},
{CC "shouldBeInitialized0", CC "(" CLS ")Z", FN_PTR(Unsafe_ShouldBeInitialized0)},
{CC "loadFence", CC "()V", FN_PTR(Unsafe_LoadFence)},
{CC "storeFence", CC "()V", FN_PTR(Unsafe_StoreFence)},
{CC "fullFence", CC "()V", FN_PTR(Unsafe_FullFence)},
{CC "isBigEndian0", CC "()Z", FN_PTR(Unsafe_isBigEndian0)},
{CC "unalignedAccess0", CC "()Z", FN_PTR(Unsafe_unalignedAccess0)}
};
小护士精力有限,博文篇幅有限,不能全部方法都介绍到,只能挑一些经典的来讲讲了。其中,对于内存修改操作的方法,小护士选了compareAndExchangeInt()
、getInt()
以及putInt()
方法来细讲。
先来看看Java代码的方法声明:
@HotSpotIntrinsicCandidate
public final native int compareAndExchangeInt(Object o, long offset,
int expected, int x);
注意这里有个注解@HotSpotIntrinsicCandidate
。这个注解会根据运行平台,把原有的实现替换为更加优化的实现,发挥所在平台的优势特性。
Object o
参数代表需要做CAS处理的目标对象,long offset
参数是该目标对象的内存地址偏移量。int expect
参数是期望值,int x
参数是更新值。当原值等于期望值,则把原值替换为更新值。下面就是C++源码实现了:
UNSAFE_ENTRY(jint, Unsafe_CompareAndExchangeInt(JNIEnv *env, jobject unsafe,
jobject obj, jlong offset,
jint e, jint x)) {
oop p = JNIHandles::resolve(obj);
if (p == NULL) {
volatile jint* addr = (volatile jint*)index_oop_from_field_offset_long(p, offset);
return RawAccess<>::atomic_cmpxchg(x, addr, e);
} else {
assert_field_offset_sane(p, offset);
return HeapAccess<>::atomic_cmpxchg_at(x, p, (ptrdiff_t)offset, e);
}
} UNSAFE_END
resolve()
方法可以先不看实现细节,姑且认为是jobject obj
转为oop p
。index_oop_from_field_offset_long()
方法用来计算:对象地址+偏移量=字段地址。RawAccess<>::atomic_cmpxchg()
方法是物理内存地址(原生地址)的CAS方法实现。assert_field_offset_sane()
方法用来校验偏移量是否正常HeapAccess<>::atomic_cmpxchg_at()
方法是JVM内存地址(堆地址)的CAS方法实现。Do not go any deeper further.
再往下深入看的话,你会发现,在access.hpp
中,JVM的Access机制设计得相当复杂,还会跟GC屏障扯上关系。小护士觉得如果要搞清楚access.hpp
、access.inline.hpp
、accessBackend.hpp
、accessBackend.inline.hpp
和accessDecorators.hpp
这几个头文件共同构成的内存访问机制,就要花更多的时间去研究了。
由此可见,JVM的真实CAS实现比常人想象的更要复杂得多;至少在还没搞懂什么是GC Barrier(GC屏障)机制之前,小护士是拒绝继续往下研究的。一旦看源码看到超纲部分,就会容易做无用功,因为你根本就看不懂超纲代码在做什么。
超纲代码:超出你知识范围的代码。
当然,小护士看不懂,不代表你看不懂。下面是Access机制的源码地址:
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/access.hpp
另外附上关于@HotSpotIntrinsicCandidate
的源码地址,里面有大量注释描述这项汇编代码替换字节码机制:
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/java.base/share/classes/jdk/internal/HotSpotIntrinsicCandidate.java
getInt()
方法通过目标对象的地址获取整型值。
@HotSpotIntrinsicCandidate
public native int getInt(Object o, long offset);
Object o
参数代表目标对象。long offset
参数代表目标对象的内存地址偏移量。下面是C++源码跟踪:
ctrl+F
搜索getInt
,未果。直接看1.7其他方法那里介绍的函数声明数组发现这段声明:static JNINativeMethod jdk_internal_misc_Unsafe_methods[] = {
...
DECLARE_GETPUTOOP(Int, I),
...
}
DECLARE_GETPUTOOP()
的函数数组声明:#define DECLARE_GETPUTOOP(Type, Desc) \
{CC "get" #Type, CC "(" OBJ "J)" #Desc, FN_PTR(Unsafe_Get##Type)}, \
{CC "put" #Type, CC "(" OBJ "J" #Desc ")V", FN_PTR(Unsafe_Put##Type)}, \
{CC "get" #Type "Volatile", CC "(" OBJ "J)" #Desc, FN_PTR(Unsafe_Get##Type##Volatile)},\
{CC "put" #Type "Volatile", CC "(" OBJ "J" #Desc ")V", FN_PTR(Unsafe_Put##Type##Volatile)}
其中,小护士发现这里除了有getInt()
以外还有getIntVolatile()
,下面也会讲到。
Unsafe_Get##Type
:UNSAFE_ENTRY(java_type, Unsafe_Get##Type(JNIEnv *env, jobject unsafe,
jobject obj, jlong offset)) {
return MemoryAccess(thread, obj, offset).get();
} UNSAFE_END
这里小护士屏蔽掉了宏定义的语法,因为定义Unsafe_Get##Type()
是用#define
语法糖实现的。如果你真的会去看这块源码,就会明白小护士的用意啦。
Unsafe_Get##Type##Volatile
:UNSAFE_ENTRY(java_type, Unsafe_Get##Type##Volatile(JNIEnv *env, jobject unsafe,
jobject obj, jlong offset)) {
return MemoryAccess(thread, obj, offset).get_volatile();
} UNSAFE_END
C++真的是一门比Java神奇得多的语言,JVM本身就是C++的经典代表作,太多高级用法,但目前我们(小护士+你)并不需要了解那些C++语法糖,毕竟还没到妨碍理解代码逻辑的层度。
在上面两个Unsafe_Get
方法中,你会发现它们都是由MemoryAccess
实现的。小护士继续深入看个究竟。
MemoryAccess(thread, obj, offset).get();
MemoryAccess(thread, obj, offset).get_volatile();
MemoryAccess
,发现类定义就在Unsafe.cpp
:template T>
class MemoryAccess : StackObj {
...
}
get()
函数定义:T get() {
if (_obj == NULL) {
GuardUnsafeAccess guard(_thread);
T ret = RawAccess<>::load(addr());
return normalize_for_read(ret);
} else {
T ret = HeapAccess<>::load_at(_obj, _offset);
return normalize_for_read(ret);
}
}
先不管guard()是干嘛的,从字面意思理解就是保护这条线程对象,也许是加个标记不让垃圾回收吧。
重点是下面两个方法:
RawAccess<>::load(addr());
HeapAccess<>::load_at(_obj, _offset);
你看的没错,还是要跟JVM的Access机制打交道,里面的源码实现就没必要纠结了。
T get_volatile() {
if (_obj == NULL) {
GuardUnsafeAccess guard(_thread);
volatile T ret = RawAccess::load(addr());
return normalize_for_read(ret);
} else {
T ret = HeapAccess::load_at(_obj, _offset);
return normalize_for_read(ret);
}
}
小护士将会在不久的将来有能力阅读JVM的C++源码,到时候再告诉你什么是MO_SEQ_CST
。
关于putInt()
的实现,小护士决定直接跳到MemoryAccess
类的实现,毕竟查找步骤与getInt()
相同。
put()
函数定义:void put(T x) {
if (_obj == NULL) {
GuardUnsafeAccess guard(_thread);
RawAccess<>::store(addr(), normalize_for_write(x));
} else {
HeapAccess<>::store_at(_obj, _offset, normalize_for_write(x));
}
}
void put_volatile(T x) {
if (_obj == NULL) {
GuardUnsafeAccess guard(_thread);
RawAccess<MO_SEQ_CST>::store(addr(), normalize_for_write(x));
} else {
HeapAccess<MO_SEQ_CST>::store_at(_obj, _offset, normalize_for_write(x));
}
}
小护士发现,这里的方法名字与x86汇编中的load
系列、store
系列指令有点联系,只是猜测。
@HotSpotIntrinsicCandidate
public native void park(boolean isAbsolute, long time);
park()
的java代码注释上,大概是说这方法用来阻塞掉当前线程执行。至于两个参数怎么用的,就要看看C++源码了。
UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) {
HOTSPOT_THREAD_PARK_BEGIN((uintptr_t) thread->parker(), (int) isAbsolute, time);
EventThreadPark event;
JavaThreadParkedState jtps(thread, time != 0);
thread->parker()->park(isAbsolute != 0, time);
if (event.should_commit()) {
post_thread_park_event(&event, thread->current_park_blocker(), time);
}
HOTSPOT_THREAD_PARK_END((uintptr_t) thread->parker());
} UNSAFE_END
C++源码中主要是两部分构成park()
方法的逻辑:
thread->parker()->park()
post_thread_park_event()
发送阻塞事件主要是与Java Flight Recorder有关系,如果要讨论JFR机制的话,估计要超纲了,暂时不讨论。
主要还是park()
函数,这里的Parker类有个方法声明,但真正实现是在posix上。
Thread有Parker* _parker
字段
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/runtime/thread.hpp
Parker的parker()
方法只声明不实现
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/runtime/park.hpp
POSIX实现
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/os/posix/os_posix.cpp
void Parker::park(bool isAbsolute, jlong time) {
...
if (time == 0) {
_cur_index = REL_INDEX; // arbitrary choice when not timed
status = pthread_cond_wait(&_cond[_cur_index], _mutex);
assert_status(status == 0, status, "cond_timedwait");
}
else {
_cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime);
assert_status(status == 0 || status == ETIMEDOUT,
status, "cond_timedwait");
}
...
}
重点,POSIX两个接口:
pthread_cond_wait()
pthread_cond_timedwait()
如果有兴趣了解linux编程的话可以继续查POSIX接口规范深入了解。而至于,两个参数bool isAbsolute
和jlong time
,主要是用于创建timespecs
用的。time
是原始时间,isAbsolute
为false时会忽略nanosecond(纳秒)。
unpark()
就在park()
隔壁,源码路径也相同。
@HotSpotIntrinsicCandidate
public native void unpark(Object thread);
注意@HotSpotIntrinsicCandidate
注解,这里不再多说。
UNSAFE_ENTRY(void, Unsafe_Unpark(JNIEnv *env, jobject unsafe, jobject jthread)) {
Parker* p = NULL;
if (jthread != NULL) {
ThreadsListHandle tlh;
JavaThread* thr = NULL;
oop java_thread = NULL;
(void) tlh.cv_internal_thread_to_JavaThread(jthread, &thr, &java_thread);
if (java_thread != NULL) {
// This is a valid oop.
jlong lp = java_lang_Thread::park_event(java_thread);
if (lp != 0) {
p = (Parker*)addr_from_java(lp);
} else {
// Not cached in the java.lang.Thread oop yet
if (thr != NULL) {
// The JavaThread is alive.
p = thr->parker();
if (p != NULL) {
// Cache the Parker in the java.lang.Thread oop for next time.
java_lang_Thread::set_park_event(java_thread, addr_to_java(p));
}
}
}
}
}
if (p != NULL) {
HOTSPOT_THREAD_UNPARK((uintptr_t) p);
p->unpark();
}
} UNSAFE_END
方法要点:
cv_internal_thread_to_JavaThread()
,校验并分解jobject
到oop
和JavaThread
;java_lang_Thread::park_event()
,返回parker
字段地址偏移量;if (lp != 0) else
,如果地址不等于0,则从内存加载parker
,否则从JavaThread
中加载并缓存地址值。java_lang_Thread::set_park_event()
,缓存parker
地址值,以待下次使用。HOTSPOT_THREAD_UNPARK()
,JVM层面做unpark处理。(暂时不确定,只是猜测。)p->unpark();
,操作系统层面做unpark处理。下面是p->unpark()
的POSIX实现:
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/os/posix/os_posix.cpp
void Parker::unpark() {
int status = pthread_mutex_lock(_mutex);
assert_status(status == 0, status, "invariant");
const int s = _counter;
_counter = 1;
// must capture correct index before unlocking
int index = _cur_index;
status = pthread_mutex_unlock(_mutex);
assert_status(status == 0, status, "invariant");
if (s < 1 && index != -1) {
// thread is definitely parked
status = pthread_cond_signal(&_cond[index]);
assert_status(status == 0, status, "invariant");
}
}
POSIX接口调用:
pthread_mutex_lock()
pthread_mutex_unlock()
pthread_cond_signal()
当小护士重新正式捡起C++的时候再来看看POSIX接口细节吧。如果你有兴趣的话可以直接查阅上面列出的接口函数。
monitorEnter()
、tryMonitorEnter()
、monitorExit()
三个方法在JDK11里面已经完全移除了,但是在JDK8里面还在。小护士觉得这三个方法应该阅读一下,仍未过时;因为目测JDK8在国内还会持续十几年时间,而且JDK6还有部分国企\互联网老系统扔在投产使用。
这里直接跳过Java代码声明吧,直接C++源码三连:
UNSAFE_ENTRY(void, Unsafe_MonitorEnter(JNIEnv *env, jobject unsafe, jobject jobj))
UnsafeWrapper("Unsafe_MonitorEnter");
{
if (jobj == NULL) {
THROW(vmSymbols::java_lang_NullPointerException());
}
Handle obj(thread, JNIHandles::resolve_non_null(jobj));
ObjectSynchronizer::jni_enter(obj, CHECK);
}
UNSAFE_END
UNSAFE_ENTRY(jboolean, Unsafe_TryMonitorEnter(JNIEnv *env, jobject unsafe, jobject jobj))
UnsafeWrapper("Unsafe_TryMonitorEnter");
{
if (jobj == NULL) {
THROW_(vmSymbols::java_lang_NullPointerException(), JNI_FALSE);
}
Handle obj(thread, JNIHandles::resolve_non_null(jobj));
bool res = ObjectSynchronizer::jni_try_enter(obj, CHECK_0);
return (res ? JNI_TRUE : JNI_FALSE);
}
UNSAFE_END
UNSAFE_ENTRY(void, Unsafe_MonitorExit(JNIEnv *env, jobject unsafe, jobject jobj))
UnsafeWrapper("Unsafe_MonitorExit");
{
if (jobj == NULL) {
THROW(vmSymbols::java_lang_NullPointerException());
}
Handle obj(THREAD, JNIHandles::resolve_non_null(jobj));
ObjectSynchronizer::jni_exit(obj(), CHECK);
}
UNSAFE_END
粗略看看上面的入口代码,其实很容易发现,synchronized
语义的真正实现者是ObjectSynchronizer
。说到这里,小护士不禁想到那些讲Java并发的书籍都会提到synchronized
的四种锁状态。嗯,小护士带你看清这个真相。
头文件声明:
https://github.com/unofficial-openjdk/openjdk/blob/jdk8u/jdk8u/hotspot/src/share/vm/runtime/synchronizer.hpp
cpp实现:
https://github.com/unofficial-openjdk/openjdk/blob/jdk8u/jdk8u/hotspot/src/share/vm/runtime/synchronizer.cpp
真相1:
在JDK11中,没有所谓四个锁状态,只有五个mark states(标记状态)。
五个mark
状态:
BiasedLocking
实现,译为“偏向锁机制”。有些书本会把inflating翻译为锁升级,这是错误的。
五种enter
方式,按照性能损耗程度从小到大排列:
1. quick_enter() // 无锁,无线程竞争过。
2. fast_enter() // 使用偏向锁机制竞争资源, BiasedLocking
3. slow_enter() // 当fast_enter()竞争资源失败时调用,
// 若是无锁状态则CAS
// 否则竞争mark->locker(), 若扔失败则调用inflate()
// 把“slow_enter()”函数处理视作“轻量级锁处理”是个极大的错误
4. jni_enter() // JNI locks on java objects
// JNI代码中竞争Java对象,以该Java对象作为互斥标记(mutex)
// 使用heavy weight monitor实现,重量级监视器
// 把“重量级监视器”翻译为“重量级锁”是个极大的错误
5. inflate() // Inflate light weight monitor to heavy weight monitor
// mark状态膨胀处理,从轻量级监视器转为重量级监视器
// 把“mark状态膨胀处理”翻译为“锁升级处理”是个极大的错误
真相2:
没有锁升级,只有mark状态膨胀,膨胀后监视器从轻量级转为重量级。
真相3:
无锁状态检查不是必须的;JDK8甚至没有quick_enter()函数。而JDK11则有。
真相4:
jni_enter()一直被那些国内讲并发编程的书籍忽略,这是不对的。
真相5:
Stack-locked,栈锁,才是真正的锁;BiasedLocking,偏向锁机制只是机制不是锁。
真相6:
没有轻量级锁和重量级锁,只有轻量级监视器和重量级监视器;锁和监视器是两码事。
真相7:
ObjectSynchronizer仍然有很多FIXME和TODO,连大神都要费尽脑汁修八阿哥。
如果你对监视器和锁机制感兴趣的话,可以方便加群一起讨论,加群方式在底部。毕竟,这两个东西要描述起来还挺费劲的。
还记得volatile
么?这个语义是实现了变量的原子访问,同时保证了happen-before规则的实施,而实现这种机制是非常依赖内存屏障的。
Unsafe有三个内存屏障方法:
@HotSpotIntrinsicCandidate
public native void loadFence();
@HotSpotIntrinsicCandidate
public native void storeFence();
@HotSpotIntrinsicCandidate
public native void fullFence();
对应到C++源码是这样的:
UNSAFE_LEAF(void, Unsafe_LoadFence(JNIEnv *env, jobject unsafe)) {
OrderAccess::acquire();
} UNSAFE_END
UNSAFE_LEAF(void, Unsafe_StoreFence(JNIEnv *env, jobject unsafe)) {
OrderAccess::release();
} UNSAFE_END
UNSAFE_LEAF(void, Unsafe_FullFence(JNIEnv *env, jobject unsafe)) {
OrderAccess::fence();
} UNSAFE_END
你肯定听说过Load\Store模型,load就是读操作,store就是写操作,load store barrier抽象来说就是读写操作内存屏障。
网上也充斥着load与store之间的四个组合关系,loadload、loadstore、storeload、storestore。
再翻查一下真正的实现类OrderAccess
,就看到里面有注释介绍这四个组合:
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/runtime/orderAccess.hpp
部分注释:
// Memory Access Ordering Model
//
// This interface is based on the JSR-133 Cookbook for Compiler Writers.
// We define four primitive memory barrier operations.
//
// LoadLoad: Load1(s); LoadLoad; Load2
//
// Ensures that Load1 completes (obtains the value it loads from memory)
// before Load2 and any subsequent load operations. Loads before Load1
// may *not* float below Load2 and any subsequent load operations.
小护士翻译:
确保在Load2操作及其任意次序列load操作完成之前,Load1操作先完成。
而那些在Load1操作之前的多个Load操作也不会重排序到Load2操作及其任意次序列load操作之后。
* 先读后读
//
// StoreStore: Store1(s); StoreStore; Store2
//
// Ensures that Store1 completes (the effect on memory of Store1 is made
// visible to other processors) before Store2 and any subsequent store
// operations. Stores before Store1 may *not* float below Store2 and any
// subsequent store operations.
小护士翻译:
确保在Store2操作及其任意次序列store操作完成之前,Store1操作先完成。(能让Store1的内存操作对其他核可见)
而那些在Store1操作之前的多个Store操作也不会重排序到Store2操作及其任意次序列store操作之后。
* 先写后写
//
// LoadStore: Load1(s); LoadStore; Store2
//
// Ensures that Load1 completes before Store2 and any subsequent store
// operations. Loads before Load1 may *not* float below Store2 and any
// subsequent store operations.
小护士翻译:
确保在Store2操作及其任意次序列store操作完成之前,Load1操作先完成。
而那些在Load1操作之前的多个Load操作也不会重排序到Store2操作及其任意次序列store操作之后。
* 先读后写
//
// StoreLoad: Store1(s); StoreLoad; Load2
//
// Ensures that Store1 completes before Load2 and any subsequent load
// operations. Stores before Store1 may *not* float below Load2 and any
// subsequent load operations.
小护士翻译:
确保在Load2操作及其任意次序列load操作完成之前,Store1操作先完成。
而那些在Store1操作之前的多个Store操作也不会重排序到Load2操作及其任意次序列load操作之后。
* 先写后读
OrderAccess
还提供了其他几个函数实现load\store模型,受限于篇幅这里只能先把所有公有函数的声明列出来。
class OrderAccess : private Atomic {
public:
// barriers
static void loadload();
static void storestore();
static void loadstore();
static void storeload();
static void acquire();
static void release();
static void fence();
template <typename T>
static T load_acquire(const volatile T* p);
template <typename T, typename D>
static void release_store(volatile D* p, T v);
template <typename T, typename D>
static void release_store_fence(volatile D* p, T v);
...
}
上面这些方法都是根据具体平台各自实现的,会有差异性。
以Linux_x86平台为例,实现上面OrderAccess
的方法在如下源码中:
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/os_cpu/linux_x86/orderAccess_linux_x86.hpp
static inline void compiler_barrier() {
__asm__ volatile ("" : : : "memory");
}
inline void OrderAccess::loadload() { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore() { compiler_barrier(); }
inline void OrderAccess::storeload() { fence(); }
inline void OrderAccess::acquire() { compiler_barrier(); }
inline void OrderAccess::release() { compiler_barrier(); }
inline void OrderAccess::fence() {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
compiler_barrier();
}
可以看到的是,在linux_x86平台下,每个函数都统一使用了compiler_barrier()
函数实现。
而它里面则是直接一行代码:
__asm__ volatile ("" : : : "memory");
这个函数貌似是用来在C++里面执行汇编指令的,但这种用法直接让小护士不明觉厉,查查维基百科果真有专门介绍。
这行代码必须再贴一次。
__asm__ volatile ("" : : : "memory");
维基百科链接:
https://en.wikipedia.org/wiki/Memory_ordering
这里引入了一个计算机术语:Memory ordering。
小护士姑且把它翻译为“内存排序机制”。
维基里面大概是说,内存屏障是分为编译时和运行时两种;
细致看看,在Linux_x86平台实现中,fence()
函数有这么一段注释:
inline void OrderAccess::fence() {
// always use locked addl since mfence is sometimes expensive
...
}
而mfence()
就是运行时的内存屏障,开销有点大,毕竟要控制多核执行有序。在刚才的维基中,下面讲解的硬件实现内存屏障里面就讲到了x86、x86-64平台的函数就有mfence()
。
凡事没有绝对,load\store模型在理论上是一回事,而在实际平台实现上则是另外一回事,为什么linux_x86平台中放弃用mfence()
而改用执行汇编指令lock;addl $0,0(%%rsp)
这种直接操作栈指针寄存器地址的方式。具体原因,还需要小护士在日后的成长曲线中研读 Intel-x86 手册才慢慢了解。
内存空间管理主要是使用C++的标准库函数malloc()
、realloc()
、free()
;对于copy的处理上,JVM则主要用位运算的技巧来实现具备原子操作的拷贝处理。受限于篇幅,小护士只能介绍allocateMemory()
、reallocateMemory()
以及setMemory()
源码实现,但不会太深入。
Java声明:
public long allocateMemory(long bytes) {
allocateMemoryChecks(bytes);
if (bytes == 0) {
return 0;
}
long p = allocateMemory0(bytes);
if (p == 0) {
throw new OutOfMemoryError();
}
return p;
}
private native long allocateMemory0(long bytes);
allocateMemoryChecks()
方法只是用来检查申请的内存大小是否超过2的32次方byte大小,也就是4GB。allocateMemory0()
方法才是真正做内存分配的本地方法。下面看看allocateMemory0()
的C++实现:
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory0(JNIEnv *env, jobject unsafe, jlong size)) {
size_t sz = (size_t)size;
sz = align_up(sz, HeapWordSize);
void* x = os::malloc(sz, mtOther);
return addr_to_java(x);
} UNSAFE_END
核心处理在于os::malloc()
,这里JVM封装了C++的标准库std::malloc()
。其他细节不用太过强迫自己去了解,只需要关心核心实现就好。
下面是os::malloc()
实现:
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/runtime/os.cpp
void* os::malloc(size_t size, MEMFLAGS flags) {
return os::malloc(size, flags, CALLER_PC);
}
void* os::malloc(size_t size, MEMFLAGS memflags, const NativeCallStack& stack) {
...
u_char* ptr;
ptr = (u_char*)::malloc(alloc_size);
...
}
::malloc(alloc_size)
就是非常熟悉的C++标准库函数,传入空间大小,尝试分配该大小的内存空间。
Java声明:
public long reallocateMemory(long address, long bytes) {
reallocateMemoryChecks(address, bytes);
if (bytes == 0) {
freeMemory(address);
return 0;
}
long p = (address == 0) ? allocateMemory0(bytes) : reallocateMemory0(address, bytes);
if (p == 0) {
throw new OutOfMemoryError();
}
return p;
}
private native long reallocateMemory0(long address, long bytes);
这里做了一个兼容处理,如果传入的地址是0,也就是意味着这是无效地址,则走正常的内存分配申请;否则,进入重新分配内存申请,相当于数组扩容。
下面是reallocateMemory0()
的C++源码:
UNSAFE_ENTRY(jlong, Unsafe_ReallocateMemory0(JNIEnv *env, jobject unsafe, jlong addr, jlong size)) {
void* p = addr_from_java(addr);
size_t sz = (size_t)size;
sz = align_up(sz, HeapWordSize);
void* x = os::realloc(p, sz, mtOther);
return addr_to_java(x);
} UNSAFE_END
和上面的malloc()
的一样,JVM也是封装了C++的一个标准库函数realloc()
。
下面是os::realloc()
的实现:
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/runtime/os.cpp
void* os::realloc(void *memblock, size_t size, MEMFLAGS flags) {
return os::realloc(memblock, size, flags, CALLER_PC);
}
void* os::realloc(void *memblock, size_t size, MEMFLAGS memflags, const NativeCallStack& stack) {
...
#ifndef ASSERT
...
void* ptr = ::realloc(membase, size + nmt_header_size);
...
}
又是一个C++的标准库函数,::realloc()
按照新的size重新分配内存空间。
Java声明:
public void setMemory(Object o, long offset, long bytes, byte value) {
setMemoryChecks(o, offset, bytes, value);
if (bytes == 0) {
return;
}
setMemory0(o, offset, bytes, value);
}
private native void setMemory0(Object o, long offset, long bytes, byte value);
setMemory0()
的C++源码:
UNSAFE_ENTRY(void, Unsafe_SetMemory0(JNIEnv *env, jobject unsafe, jobject obj,
jlong offset, jlong size, jbyte value)) {
size_t sz = (size_t)size;
oop base = JNIHandles::resolve(obj);
void* p = index_oop_from_field_offset_long(base, offset);
Copy::fill_to_memory_atomic(p, sz, value);
} UNSAFE_END
这里核心方法就是Copy::fill_to_memory_atomic()
,Copy机制在另外一个独立的C++文件中,有兴趣可以深入研究一下里面的位运算操作,这里不展开细讲了,已经不知不觉了两万多字了,含代码和文字描述。
下面是Copy::fill_to_memory_atomic()
的源码位置:
https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/utilities/copy.cpp
写到这,小护士相信大家内心会有个疑虑:
为什么需要研究这么深入?有什么用?
小护士统一回答:
因为好奇心,因为想知道潘多拉盒子里面装着什么妖怪,想摸清自己的知识边界,想知道如何去努力才能提高自己的技术水平。
小护士写这篇博文时,已经从业三年。小护士身上背负着太多焦虑,虽然学习新技术不成问题,但只停留在表面使用的层度,勉强能应付产品经理的业务需求,前端后端都可以,对当前主流Vue+SpringBoot信手拈来。对于程序异常处理,都是直接把异常信息复制粘贴到百度谷歌搜索输入框。
小护士在想:难道就这样么,这就是程序员的生活?学习使用新技术?复制粘贴异常信息?在技术潮流中疲于奔命?然后等着哪天学不动了,被人才市场无情地淘汰掉?
No way.
好吧,说太多内心话。希望大家喜欢阅读小护士青铜上分系列文章,由于小护士在本系列花太多时间,以至于《深入理解计算机系统》这本书的阅读进度落下不少,但是在Thread
、ThreadLocal
、Unsafe
这三个类的源码阅读中,小护士越发觉得掌握扎实的计算机基础非常重要,因此阅读《深入理解计算机系统》、《算法导论》等优秀经典名著非常有必要。下周开始回归读书笔记系列;同时计划开始工程实践系列的文章。小护士动手能力还是可以的,希望写写代码做做调优,然后写份报告,最后通过报告结果引导小护士和大家去看源码找原因。三个系列,《读书笔记》、《青铜上分》、《工程实践》互相关联,映射出小护士对程序员职业发展的蓝图。
BTW,写在最后,如果你想对小护士说些啥,可以在下面评论,或者加QQ群讨论。
小护士推荐的高质量QQ技术群:
QQ群:JAVA高级交流(329019348)
QQ群:大宽宽的技术交流群(317060090)
本文耗时11天,共写25728个字。