CAS 源码分析

摘要:CAS全称为compare and swap,是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。
from Wikipedia

正文:

在使用上,通常会记录下某块内存中的旧值,通过对旧值进行一系列的操作后得到新值,然后通过CAS操作将新值与旧值进行交换。如果这块内存的值在这期间内没被修改过,则旧值会与内存中的数据相同,这时CAS操作将会成功执行 使内存中的数据变为新值。如果内存中的值在这期间内被修改过,则一般来说旧值会与内存中的数据不同,这时CAS操作将会失败,新值将不会被写入内存。

CAS的实现

接下来我们去看CAS在java中的实现,sun.misc.Unsafe提供了compareAndSwap系列函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
*/
public final native boolean compareAndSwapObject(Object o, long offset,
Object expected,
Object x);

/**
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
*/
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);

/**
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
*/
public final native boolean compareAndSwapLong(Object o, long offset,
long expected,
long x);

可以看到native发现这是一个本地方法调用,可以去查看对应的OpenJDK中调用代码atomic_linux_x86.inline.hpp / atomic_windows_x86.inline.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//linux(int 类型)
inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}

//windows(int 类型)
inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
return (*os::atomic_cmpxchg_func)(exchange_value, dest, compare_value);
}

可以看到其实现方式是基于硬件平台的汇编指令cmpxchg指令完成的,JVM只是封装了汇编调用。以linux x86处理器为例子,int mp = os::is_MP()中的MP是multiprocessor,即多处理器,当遇到是多处理器的情况下加上LOCK。cmpxchgl指的应该是compare and exchange指令。

CAS在Java中的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
	public class UnsafeTest {

private static Unsafe unsafe;

static {
try {
// 通过反射获取rt.jar下的Unsafe类
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
System.out.println("Get Unsafe instance occur error" + e);
}
}

public static void main(String[] args) throws Exception {
Class clazz = Target.class;
Field[] fields = clazz.getDeclaredFields();
Target target = new Target();
Field intFiled = clazz.getDeclaredField("intParam");
Field longFiled = clazz.getDeclaredField("longParam");
Field strFiled = clazz.getDeclaredField("strParam");
Field strFiled2 = clazz.getDeclaredField("strParam2");

// intParam
System.out.print(unsafe.compareAndSwapInt(target, 12, 3, 10) + ":");
System.out.println((Integer) intFiled.get(target));
// longParam
System.out.print(unsafe.compareAndSwapLong(target, 16, 1l, 2l) + ":");
System.out.println((Long) longFiled.get(target));
// strParam
System.out.print(unsafe.compareAndSwapObject(target, 24, null, "5")
+ ":");
System.out.println((String) strFiled.get(target));
// strParam2
System.out.print(unsafe.compareAndSwapObject(target, 28, null, "6")
+ ":");
System.out.println((String) strFiled2.get(target));

}
}

class Target {
int intParam = 3;
long longParam = 1l;
String strParam;
String strParam2;
}

如代码所示,compareAndSwapXx方法会根据第二个参数”偏移量”去拿偏移量这么多的属性的值和第三个参数对比,如果相同则将该属性值替换为第四个参数。该偏移量是指某个字段相对Java对象的起始位置的偏移量,可以通过unsafe.objectFieldOffset(param)去获取对应属性的偏移量。

顺便介绍个查看对象的属性位置分布的一个小工具:jol

使用Demo:

首先引用jol-core包

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>

然后在项目里简单使用下:

1
2
3
4
5
6
public class TestOffset {
public static void main(String[] args) {
out.println(VM.current().details());
out.println(ClassLayout.parseClass(Throwable.class).toPrintable());
}
}

结果如下,根据偏移量界面化的显示属性分布的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

java.lang.Throwable object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (alignment/padding gap)
16 4 java.lang.String Throwable.detailMessage N/A
20 4 java.lang.Throwable Throwable.cause N/A
24 4 java.lang.StackTraceElement[] Throwable.stackTrace N/A
28 4 java.util.List Throwable.suppressedExceptions N/A
Instance size: 32 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

CAS存在的ABA问题

CAS普遍存在的一个问题就是ABA问题,即是当线程一将变量A修改为B,之后又修改为A,线程二去对比A发现没变化就会判断出错。目前很多都是使用加上版本号来解决,加个version字段,每次修改就++,每次判断时候多判断下版本号是否变化来确定某变量是否被修改。

下班啦,暂且到这里,祝大家十一玩的开心~~~