小护士青铜上分系列之《Java源码阅读》第五篇Unsafe

小护士青铜上分系列之《Java源码阅读》第五篇Unsafe

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++源码是必须的。就酱紫。

1. Unsafe有哪些方法?

  • getXxx
  • putXxx
  • compareAndSwapXxx
  • park\monitor
  • xxxMemory
  • 其他杂七杂八的方法

从这里分类中,小护士大概可以了解到,其实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

1.1 getXxx

getXxx()系列方法是用来读取(fetch)Java对象所在内存地址对应的内存值。

getInt()为例:

@HotSpotIntrinsicCandidate
public native int getInt(Object o, long offset);
  • 第一个参数,是一个Object,拥有一个对象的内存地址。
  • 第二个参数,是一个偏移量。
  • 返回值,是一个该内存地址对应的内存值。

getInt()方法会利用第一个参数Object o来获取该对象所在的内存地址,再根据这个内存地址的基础上,加上偏移量,得出一个真实的内存地址,最后从这个内存地址中加载对应的内存值。

如果把内存比喻为一个Map map的话,其中,key为地址值,value为内存值。
那么这个getInt()方法就相当于:

long addr = o.getAddress()+offset;
int ret = map.get(addr);

后面会具体讲getXxx()系列方法中的getInt()方法的实现细节。

1.2 putXxx

putXxx()系列方法与getXxx()系列方法相对应,前者是写入内存值,后者是读取内存值。

putInt()方法为例:

@HotSpotIntrinsicCandidate
public native void putInt(Object o, long offset, int x);
  • 第一个参数,是一个Object,拥有一个对象的内存地址。
  • 第二个参数,是一个偏移量。
  • 第三个参数,是一个将会更新到内存中的值。

如果把内存再次比喻为一个Map map的话,其中,key为地址值,value为内存值。
那么这个putInt()方法就相当于:

long addr = o.getAddress()+offset;
map.put(addr, x);

后面会具体讲putXxx()系列方法中的putInt()方法的实现细节。

1.3 compareAndSwapXxx

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 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()方法。

1.4 park \ monitor

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);

1.5 xxxFence

fence,中文意思是栅栏,专业一点的翻译可以是内存屏障。小护士猜测这个xxxFence系列的方法就是用来模拟volatile语义的。fence有三个方法,分别是loadFence()storeFence()fullFence()

public native void loadFence();

public native void storeFence();

public native void fullFence();

这三个方法都是为了防止指令重排序的。指令重排序的意思是操作系统在运行程序时,会执行一系列程序指令,而指令的执行顺序可以根据具体硬件平台(CPU、内存、32位字长、64位字长)的特点来做执行顺序上的优化,这种优化主要是编译时处理的。

到底如何加内存屏障呢,一会儿看个究竟。

1.6 xxxMemory

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新增的。

1.7 其他方法

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)}
};

2. Unsafe的内存修改操作

小护士精力有限,博文篇幅有限,不能全部方法都介绍到,只能挑一些经典的来讲讲了。其中,对于内存修改操作的方法,小护士选了compareAndExchangeInt()getInt()以及putInt()方法来细讲。

2.1 compareAndExchangeInt()

先来看看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.hppaccess.inline.hppaccessBackend.hppaccessBackend.inline.hppaccessDecorators.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

2.2 getInt()

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机制打交道,里面的源码实现就没必要纠结了。

  • 对于volatile版本的写法,小护士发现它只是加了个泛型:
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

2.3 putInt()

关于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));
  }
}
  • 这是不远处volatile版本的函数定义:
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系列指令有点联系,只是猜测。

3. Unsafe的同步互斥机制

  • park()
  • unpark()
  • monitorEnter()
  • tryMonitorEnter
  • monitorExit()

3.1 park()

@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()方法的逻辑:

  • 操作系统层面park,thread->parker()->park()
  • JVM层面发送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 isAbsolutejlong time,主要是用于创建timespecs用的。time是原始时间,isAbsolute为false时会忽略nanosecond(纳秒)。

3.2 unpark()

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(),校验并分解jobjectoopJavaThread
  • 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接口细节吧。如果你有兴趣的话可以直接查阅上面列出的接口函数。

3.3 monitorEnter(), tryMonitorEnter(), monitorExit()

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状态:

  • Inflated,已膨胀状态,不是非要用heavy weight monitor实现(重量级监视器,不是重量级锁)。
  • Stack-locked,栈锁状态,其他线程把整个栈都锁住,该栈中包含该竞争资源,迫使其膨胀。
  • INFLATING,膨胀中状态,意味着任意非膨胀状态正在转换为已膨胀状态。
  • Neutral,中立状态,无线程竞争,又称无锁状态。
  • BIASED,偏向状态,使用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,连大神都要费尽脑汁修八阿哥。

如果你对监视器和锁机制感兴趣的话,可以方便加群一起讨论,加群方式在底部。毕竟,这两个东西要描述起来还挺费劲的。

4. Unsafe的内存屏障机制

还记得volatile么?这个语义是实现了变量的原子访问,同时保证了happen-before规则的实施,而实现这种机制是非常依赖内存屏障的。

4.1 Unsafe的三个内存屏障方法

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);
...
}

上面这些方法都是根据具体平台各自实现的,会有差异性。

4.2 Linux_X86实现的内存屏障

以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++里面执行汇编指令的,但这种用法直接让小护士不明觉厉,查查维基百科果真有专门介绍。

4.3 Memory ordering Wiki

这行代码必须再贴一次。

__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 手册才慢慢了解。

5. Unsafe的内存空间管理

  • allocateMemory()
  • reallocateMemory()
  • setMemory()
  • freeMemory()
  • copyMemory()
  • copySwapMemory()

内存空间管理主要是使用C++的标准库函数malloc()realloc()free();对于copy的处理上,JVM则主要用位运算的技巧来实现具备原子操作的拷贝处理。受限于篇幅,小护士只能介绍allocateMemory()reallocateMemory()以及setMemory()源码实现,但不会太深入。

5.1 allocateMemory()

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++标准库函数,传入空间大小,尝试分配该大小的内存空间。

5.2 reallocateMemory()

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重新分配内存空间。

5.3 setMemory()

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

6. Unsafe小结

写到这,小护士相信大家内心会有个疑虑:

为什么需要研究这么深入?有什么用?

小护士统一回答:

因为好奇心,因为想知道潘多拉盒子里面装着什么妖怪,想摸清自己的知识边界,想知道如何去努力才能提高自己的技术水平。

小护士写这篇博文时,已经从业三年。小护士身上背负着太多焦虑,虽然学习新技术不成问题,但只停留在表面使用的层度,勉强能应付产品经理的业务需求,前端后端都可以,对当前主流Vue+SpringBoot信手拈来。对于程序异常处理,都是直接把异常信息复制粘贴到百度谷歌搜索输入框。

小护士在想:难道就这样么,这就是程序员的生活?学习使用新技术?复制粘贴异常信息?在技术潮流中疲于奔命?然后等着哪天学不动了,被人才市场无情地淘汰掉?

No way.

好吧,说太多内心话。希望大家喜欢阅读小护士青铜上分系列文章,由于小护士在本系列花太多时间,以至于《深入理解计算机系统》这本书的阅读进度落下不少,但是在ThreadThreadLocalUnsafe这三个类的源码阅读中,小护士越发觉得掌握扎实的计算机基础非常重要,因此阅读《深入理解计算机系统》、《算法导论》等优秀经典名著非常有必要。下周开始回归读书笔记系列;同时计划开始工程实践系列的文章。小护士动手能力还是可以的,希望写写代码做做调优,然后写份报告,最后通过报告结果引导小护士和大家去看源码找原因。三个系列,《读书笔记》、《青铜上分》、《工程实践》互相关联,映射出小护士对程序员职业发展的蓝图。

BTW,写在最后,如果你想对小护士说些啥,可以在下面评论,或者加QQ群讨论。

小护士推荐的高质量QQ技术群:
QQ群:JAVA高级交流(329019348)
QQ群:大宽宽的技术交流群(317060090)

本文耗时11天,共写25728个字。

你可能感兴趣的:(青铜上分,Java源码)