案例引入
不使用任何锁机制
public class MyLock {
private static Unsafe unsafe;
public Unsafe getUnsafe() {
return unsafe;
}
public int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
public void inc(){
value++;
}
public static void main(String[] args) {
MyLock myLock = new MyLock();
for (int i = 0; i < 100; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
myLock.inc();
}
}).start();
}
System.out.println(myLock.getValue());
}
}
9882
使用
CAS + 自旋的方式
的方式执行
public class MyLock {
private static long stateOffset;
private static Unsafe unsafe;
public Unsafe getUnsafe() {
return unsafe;
}
public int value;
public int getValue() {
return value;
}
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
stateOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
public void casInc(){
int intVolatile = 0;
do{
intVolatile = unsafe.getIntVolatile(this, stateOffset);
}while(!unsafe.compareAndSwapInt(this,stateOffset,intVolatile,intVolatile+1));
}
public static void main(String[] args) {
MyLock myLock = new MyLock();
for (int i = 0; i < 100; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
myLock.casInc();
}
}).start();
}
LockSupport.parkNanos(1*1000*1000);
System.out.println(myLock.getValue());
}
}
10000
没加屏障,程序不会停止
class TestFence_{
private static int a = 0;
public static void main(String[] args) {
new Thread(()->{
while(a == 0){
}
}).start();
LockSupport.parkNanos(1*1000*1000);
TestFence_.a = 1;
}
}
// 不会停止
加了屏障,程序会停止
class TestFence_{
private static int a = 0;
public static void main(String[] args) {
new Thread(()->{
while(a == 0){
//加屏障: 禁止使用编译器进行优化
MyLock.getUnsafe().loadFence();
}
}).start();
LockSupport.parkNanos(1*1000*1000);
TestFence_.a = 1;
}
}
CAS 基本介绍
Java
中的CAS
操作本质上还是通过sun.misc.Unsafe
类来实现,因为CAS
操作需要直接操作内存,而Unsafe
类恰好提供了直接操作内存的方法。
主要提供了三个参数 : 内存地址V ; 旧值 A,新值B;如果当前内存地址的值等于 原始值 A,则将内存地址的值修改为 新值 B,否则不进行任何操作。
cas 的操作是原子的,就是在同一个时刻只有一个线程能够成功执行这个操作。
CAS 源码解析
java
Unsafe.java
所在目录 :openjdk-jdk8u-master/jdk/src/share/classes/sun/misc/Unsafe.java
public final class Unsafe {
...
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
...
}
cpp
Unsafe.cpp
所在目录 :openjdk-jdk8u-master/hotspot/src/share/vm/prims/unsafe.cpp
//作用是比较和交换整型值,用于实现线程安全的原子操作。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
//代码首先使用JNIHandles::resolve函数将传入的obj对象转换为一个oop对象
oop p = JNIHandles::resolve(obj);
//然后使用index_oop_from_field_offset_long函数根据偏移量offset来获取 整型地址addr
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
//使用Atomic::cmpxchg函数来比较地址addr处的值和e的值,如果相等,则将x的值赋给addr。
//新值,地址值,旧值
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //代码将比较结果与旧的值e进行比较,并返回比较结果的布尔值。
UNSAFE_END
linux -内联汇编
- exchange_value——新值
- dest——目标地址
- compare_value ——旧值
// 使用cmpxchgl指令比较并交换两个整数值。
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
//将exchange_value与dest指向的整数值进行比较,如果相等,则将exchange_value存储到dest指向的内存位置,并返回原本在dest中的值
//通过__asm__ volatile语句 表示 ASM汇编,禁止编译器优化,将汇编代码嵌入到C++代码中,实现了比较并交换的原子操作
/*
ASM的内敛汇编格式:
asm( assembler template:就是cmpxchgl%1,(%3)表示汇编模板
:output operands:表示输出操作数,=a对应eax寄存器
:input operands:表示输入参数,%1就是exchange_value(新值),%3是dest(变量地址),%4就是mp(多处理器标志),r表示任意寄存器,a是eax寄存器(存的是compare_value的值,原值)
:list of clobbered registers:就是些额外参数,cc表示编译器cmpxchgl的执行将影响到标志寄存器,memory告诉
编译器要重新从内存中读取变量的最新值,这点实现了volatile
)
*/
//LOCK_IF_MP宏 根据系统是否为多处理器(MP)来决定是否在指令前加上lock前缀,以确保操作的原子性。
//#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: " 多核才会加 lock 锁总线
__asm__ volatile (LOCK_IF_MP(%4)
//cmpxchgl指令就是比较eax寄存器的值(compare_value预期值)和目的操作数((dest))的值是否相同
//%1 和 %3 比较替换,将 /%1 的exchange_value 更新到 %3dest的
"cmpxchgl %1,(%3)"
/*
:"=a" (exchange_value):这是输出操作数列表,表示将%eax寄存器中的值(即exchange_value)作为输出。
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp):这是输入操作数列表,
表示将exchange_value、compare_value、dest和mp分别赋值给 %eax、 %ecx、 %edx和 %ebx寄存器。
*/
: "=a" (exchange_value) // 0%
: "r" (exchange_value), // 1%
"a" (compare_value), // 2%
"r" (dest),// 3%
"r" (mp)// 4%
//如果相同,就把原操作数(exchange_value)的值赋值给目的操作数((dest)),并且设置标志位ZF=1(影响的cc);这种情况下,就成功的修改了内存中的数据,后续的操作成功就是指的这种情况
//如果不同,就把源操作数(exchange_value)的值赋值给eax寄存器,并且设置ZF=0
/*
"cc", "memory":这是指令的副作用列表,表示该指令可能会修改条件码寄存器(cc)和内存。
*/
: "cc", "memory");
return exchange_value;
}
windows-内联汇编
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest 获取内存地址 dest 数据放至 edx 寄存器中
mov ecx, exchange_value 将 新值exchange_value 放入到 ecx 寄存器中
mov eax, compare_value 将 原值compare_value 放入到 eax 寄存器中
LOCK_IF_MP(mp) 根据当前cpu是否为多核 决定 是不是要 加锁
cmpxchg dword ptr [edx], ecx
// dword doubleword 双字
// ptr pointer,与前面的 dword 连起来使用,表明访问的内存单元是一个双字单元
// [edx] 表示一个内存单元,edx 是寄存器,dest 指针值存放在 edx 中
// [edx] 表示内存地址为 dest 的内存单元
// 访问内存地址为 dest 的双字内存单元,
// 就是说:将 EAX 寄存器中的值与 内存地址为 dest 的双字内存单元 进行比较。如果两个值相等,则将 ecx寄存器中的值 加载到目标操作数中。
}
}
CAS的问题
1、ABA 问题
基本介绍:
在执行CAS操作时,如果内存中的值从A变为B,然后再变回A,那么CAS操作会错误地认为内存中的值没有发生变化,导致可能出现意外的结果。
后果:
可能会导致一些潜在的问题,例如在无锁数据结构中,一个线程可能在执行CAS操作时,另一个线程修改了内存中的值,然后又恢复为原始值,这样第一个线程就无法察觉到这个变化。
解决:
1、使用带有版本号的CAS操作。每当对内存进行修改时,都将版本号递增,这样即使值发生了变化,但版本号也会发生变化,从而避免了ABA问题的发生。
2、JDK的解决:AtomicStampedReference
//如果当前引用==到预期引用 并且 当前标记等于预期标记,则以原子方式将引用和标记的值设置为给定更新值。
public boolean weakCompareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
return compareAndSet(expectedReference, newReference,
expectedStamp, newStamp);
}
//如果当前引用==到预期引用 并且 当前标记等于预期标记,则以原子方式将引用和标记的值设置为给定更新值。
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
2、循环开销
在底层汇编或者cpp代码的层面并没有循环开销的问题;
但是在java 应用层面,程序员书写代码的时候就会产生这样的循环开销的问题,就是说CAS操作通常是通过一个循环来实现的,因为它需要不断地重试,直到成功为止。也就是在高并发情况下
,多个线程同时对同一个变量进行CAS操作时,可能会发生竞争,导致大量的重试
。这种情况下,CAS操作的循环会不断地重试,消耗了大量的CPU资源和时间
解决
使用指数退避策略,即在每次重试时增加等待时间,以减少对共享资源的竞争。另外,可以考虑使用其他并发控制手段,如锁机制,来避免过多的CAS操作
内存屏障源码解析
Java
public native void loadFence();
cpp
// 总的来说,这段代码的作用是执行一个加载屏障操作,以确保后续的读取操作能够看到之前的最新值。
在这段代码中,Unsafe_LoadFence函数被标记为UNSAFE_ENTRY,这表示它是一个不安全的入口点,可能会涉及到一些底层的操作或者绕过一些安全检查。
UNSAFE_ENTRY(void, Unsafe_LoadFence(JNIEnv *env, jobject unsafe))
//UnsafeWrapper("Unsafe_LoadFence")是一个函数调用,它可能是用来处理一些与安全性相关的操作或者包装底层的不安全代码。
UnsafeWrapper("Unsafe_LoadFence");
//通过调用OrderAccess::acquire()函数来执行一个加载屏障(load fence)操作。
//加载屏障是一种同步原语,用于确保在 加载操作之后 的所有 读取操作都能 看到加载操作之前 的最新值。
OrderAccess::acquire();
UNSAFE_END
内联汇编
// 编译器屏障
//目的是通过内联汇编语句来实现内存顺序的同步,以确保在多线程环境下的正确性和一致性。
//acquire函数通过使用内联汇编语句来实现内存顺序的同步。
inline void OrderAccess::acquire() {
//声明了一个volatile intptr_t类型的本地变量local_dummy,用于存储从内存中读取的值。
volatile intptr_t local_dummy;
//根据编译器宏定义的架构类型,使用不同的汇编指令来将栈顶或栈指针指向的内存数据加载到local_dummy变量中。
#ifdef AMD64
//在x86架构中(非AMD64),使用movl指令将栈顶(ESP)指向的内存数据加载到local_dummy中。
__asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
//在AMD64架构中,使用movq指令将栈指针(RSP)指向的内存数据加载到local_dummy中。
__asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
//通过使用volatile关键字修饰local_dummy变量,可以确保编译器不会对该变量的读取进行优化,从而保证了内存顺序的同步操作。
#endif // AMD64
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/180219.html