前段时间找工作- -,一直没有什么时间写博客,断更了好长时间,不好意思了。
1.锁带来的问题
前面在3.Java并发synchronized关键字解析中提到了synchronized实现的原理是通过对象的对象头中的标记来实现的,而且如果出现多线程争夺锁的时候后面还有一些系列的所膨胀的过程。这些过程都是比较消耗性能的。因此可以通过CAS来实现无锁的方式处理并发访问的问题。
2.CAS
CAS是CompareAndSwap的简写,就是比较交换的意思。用一段代码来说明
public static void casDemo(int expect,int update){
while (original==expect){
original=update;
}
}
这里的expect是期望值,update是想要赋予的新的值,其中判断中的original表示的是原始值。大概的意思就是,如果期望值等于原始值,那么就将原始值设置成要更新的值。
示意图
在Java中大量的用到了CAS操作,其中CAS操作是在Unsafe类中定义的,这个类在java9以后貌似换到别的实现类了。不过现在大部分都是使用的java8。在juc包下的atomic包中都使用到了Unsafe,在AQS跟其他类中也大量使用到了Unsafe的cas操作。
3.CAS的ABA问题
关于CAS的ABA问题网上有很多描述,这里就借用一篇文章来简单说明ABA问题。但是对于ABA问题,对于不同的场景其产生的影响可能重要也可能不重要。
4.CAS的底层实现
在JVM中跟计算机中CAS的指令执行是原子性的,那么怎么保证这个原子性的实现的呢。这里我们先进入JVM源码进行查看,这里用Unsafe
类的compareAndSwapInt
方法进行举例。
@HotSpotIntrinsicCandidate
public final native int compareAndExchangeInt(Object o, long offset,
int expected, int x);
其中HotSpotIntrinsicCandidate这个表情的作用是根据jdk运行的平台,比如64位32位和其他信息,来选择最佳的底层实现方式。
进入到jvm的源码中
#define ADR "J"
#define LANG "Ljava/lang/"
#define OBJ LANG "Object;"
#define CLS LANG "Class;"
#define FLD LANG "reflect/Field;"
#define THR LANG "Throwable;"
#define DC_Args LANG "String;[BII" LANG "ClassLoader;" "Ljava/security/ProtectionDomain;"
#define DAC_Args CLS "[B[" OBJ
#define CC (char*) /*cast a literal from (const char*)*/
#define FN_PTR(f) CAST_FROM_FN_PTR(void*, &f)
static JNINativeMethod jdk_internal_misc_Unsafe_methods[] = {
......
{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 "compareAndSetInt", CC "(" OBJ "J""I""I"")Z", FN_PTR(Unsafe_CompareAndSetInt)},
{CC "compareAndSetLong", CC "(" OBJ "J""J""J"")Z", FN_PTR(Unsafe_CompareAndSetLong)},
{CC "compareAndExchangeReference", CC "(" OBJ "J" OBJ "" OBJ ")" OBJ, FN_PTR(Unsafe_CompareAndExchangeReference)},
{CC "compareAndExchangeInt", CC "(" OBJ "J""I""I"")I", FN_PTR(Unsafe_CompareAndExchangeInt)},
{CC "compareAndExchangeLong", CC "(" OBJ "J""J""J"")J", FN_PTR(Unsafe_CompareAndExchangeLong)},
......
};
在jvm的unsafe.cpp
文件中声明了很多java中的字段,比如用LANG表示“Ljava/lang/”,用OBJ表示“Object”等。还声明了很多跟java中Unsafe类中方法相关的方法数组。其中可以看到unsafe
类中的compareAndExchangeInt
方法关联到的方法是Unsafe_CompareAndSetInt
,接下来进入这个方法
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
//获取obj中的oop对象,这里的oop是对象结构中的一部分
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) == e;
} else {
assert_field_offset_sane(p, offset);
//如果以前的值与比较值匹配,则在内部指针地址原子地比较并交换一个新值。
return HeapAccess<>::atomic_cmpxchg_at(x, p, (ptrdiff_t)offset, e) == e;
}
} UNSAFE_END
笔者这里对于RawAccess
跟HeapAccess
这两种CAS的操作区别不是特别清楚,然后关于oop对象大家可以在网上看看《揭秘Java虚拟机:JVM设计原理与实现》这本书里面有介绍,这里就不多介绍(其实我记得也不太清楚,只是大概记得有这么个概念),因为对C++也是不太懂- -。有明白的读者可以告知一下。但是其中index_oop_from_field_offset_long
跟assert_field_offset_sane
都是极端对应的偏移量,然后进行CAS操作。
其中CAS的操作全部都定义在access.hpp
文件中,这里进入access.hpp
进行查看
// * atomic_cmpxchg: Atomically compare-and-swap a new value at an address if previous value matched the compared value.
//如果以前的值与比较值匹配,则原子地在一个地址上比较并交换一个新值。
// * atomic_cmpxchg_at: Atomically compare-and-swap a new value at an internal pointer address if previous value matched the compared value.
//如果以前的值与比较值匹配,则在内部指针地址原子地比较并交换一个新值。
static inline T atomic_cmpxchg(T new_value, P* addr, T compare_value) {
verify_primitive_decorators<atomic_cmpxchg_mo_decorators>();
return AccessInternal::atomic_cmpxchg<decorators>(new_value, addr, compare_value);
}
static inline T atomic_cmpxchg_at(T new_value, oop base, ptrdiff_t offset, T compare_value) {
verify_primitive_decorators<atomic_cmpxchg_mo_decorators>();
return AccessInternal::atomic_cmpxchg_at<decorators>(new_value, base, offset, compare_value);
}
到了这里我已经感觉深入很难了,因为JVM的底层CAS实现并不像我们想象的那么简单,只知道最后会用到内存屏障这种机制,而内存屏障这个东西主要也是保证读写的顺序性防止指令重排。关于jvm的内存屏障种类很多的比如跟内存排序有关的屏障,跟GC有关的内存屏障。很复杂,我也不是特别的了解这里用屏障的原因,难道是为了防止读写的顺序性吗?
在汇编层面用到的cmpxchg
指令进行比较交换操作。而由于不同的cpu厂商可能有不同的指令集,而不同的操作系统实现某个操作的方式也可能不同,所以jvm根据不同的操作系统进行不同的实现,这也是java
代码能够在不同的操作平台运行的原因因为在jvm层次实现了兼容。
通过观察不同的平台的原子操作文件,发现了一个共同点。比如在windows的x86平台下原子Cmpxchg
操作的实现使用如下的内嵌汇编完成的
int
类型的原子交换操作
inline T Atomic::PlatformCmpxchg<4>::operator()(T exchange_value,
T volatile* dest,
T compare_value,
atomic_memory_order order) const {
STATIC_ASSERT(4 == sizeof(T));
// alternative for InterlockedCompareExchange
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
lock cmpxchg dword ptr [edx], ecx
}
}
long
类型的原子交换操作
inline T Atomic::PlatformCmpxchg<8>::operator()(T exchange_value,
T volatile* dest,
T compare_value,
atomic_memory_order order) const {
STATIC_ASSERT(8 == sizeof(T));
//计算要设置的值低4为的地址
int32_t ex_lo = (int32_t)exchange_value;
//计算要设置的值高4为的地址
int32_t ex_hi = *( ((int32_t*)&exchange_value) + 1 );
//计算原始值低4为的地址
int32_t cmp_lo = (int32_t)compare_value;
//计算原始值高4为的地址
int32_t cmp_hi = *( ((int32_t*)&compare_value) + 1 );
__asm {
push ebx
push edi
mov eax, cmp_lo
mov edx, cmp_hi
mov edi, dest
mov ebx, ex_lo
mov ecx, ex_hi
lock cmpxchg8b qword ptr [edi]
pop edi
pop ebx
}
}
比较上面的操作发现在cmpxchg
跟cmpxchg8b
命令前面都加了一个lock
修饰。这个Lock在我们前面有讲到过Java并发volatile关键字的作用和汇编原理。这里就不多说,所以可以知道原子交换的操作最后底层也是用了给总线加锁的方式,来避免数据的可见性问题的。
这里说一下cmpxchg8b
指令,这个指令是8字节的比较交换指令。因为long类型的长度是int的两倍8字节而一般的地址存储单元是4字节的,所以这里不是cmpxchg
这个普通的4字节的比较交换指令。
接下来继续看Linux
平台的原子比较交换的实现,这里只是截取部分代码,没有将不同cpu的实现表现出来
inline T Atomic::PlatformCmpxchg<1>::operator()(T exchange_value,
T volatile* dest,
T compare_value,
atomic_memory_order /* order */) const {
STATIC_ASSERT(1 == sizeof(T));
__asm__ volatile ("lock cmpxchgb %1,(%3)"
: "=a" (exchange_value)
: "q" (exchange_value), "a" (compare_value), "r" (dest)
: "cc", "memory");
return exchange_value;
}
template<>
template<typename T>
inline T Atomic::PlatformCmpxchg<4>::operator()(T exchange_value,
T volatile* dest,
T compare_value,
atomic_memory_order /* order */) const {
STATIC_ASSERT(4 == sizeof(T));
__asm__ volatile ("lock cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest)
: "cc", "memory");
return exchange_value;
}
可以看到这里使用的也是内嵌汇编的实现形式,在指令前面也有加了lock指令,而__asm__ volatile
这个是表示不允许对该内联汇编优化。
5.总结
在这里大概已经清楚了jdk内部的原子类使用Unsafe
类中的Api实现的,而Unsafe
类中的CAS操作底层又是在JVM中实现的。上面看到了在实现的过程中,
- 我们有看到屏障相关的东西(由于对于C++不熟悉,所以不知道具体怎么使用的),前面关于屏障也说过了,个人理解为这里是为了数据操作的顺序保证。
- 底层的比较交换用到的是对于的汇编指令,并且在前面加上的
LOCK
汇编指令,给总线加锁。
最后
这篇文章还需要深入,日后复习C++之后再看看,这里推荐一篇文章https://blog.csdn.net/lwg040814025/article/details/54645599可以瞅瞅,
😔深感无奈和自己C++不懂的难处。果然是想学好一门语言知道还需要实现其功能的语言。