Java中的原子操作类 - Sanarous的博客

Java中的原子操作类

概述

在多线程环境下,如果多个线程同时更新一个变量,可能并不会得到预期的结果。往往我们会使用加锁机制如 Synchrnized 等解决这个问题,但是仅仅对于更新基本变量来说,加锁的开销太大了,从 JDK 1.5 开始,java.util.concurrent.atomic(简称 J.U.C 原子类)提供一种用法简单、性能高效、线程安全的更新一个变量的方式。因为变量的类型有很多,所以在 Atomic 包中一共提供了 13 个类,属于 4 种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性。Atomic 包基本都是使用的 Unsafe 实现的包装类。

原子更新基本类型

使用原子方式更新基本类型,原子类包中提供了三个类:

  • AtomicBoolean:原子更新布尔类型
  • AtomicInteger:原子更新整形
  • AtomicInteger:原子更新长整形

上面三个类提供的方法基本一模一样,所以这里我们只以 AtomicInteger 作为代表进行讨论。

AtomicInteger 的常用方法如下:

方法作用
int addAndGet(int delta)以原子方式将输入的数值与实例中值相加,并返回结果
boolean compareAndSet(int expect,int update)如果输入的数值等于预期值,则以原子的方式将该数值设置为输入的值
int getAndIncrement()以原子的方式将当前值加1,注意,这里返回的是自增前的值
void lazySet(int newValue)最终会设置成newValue,使用lazySet后,可能导致其它线程在之后的一小段时间内还是可以访问到旧值
int getAndSet()以原子方式设置为newValue的值,并返回旧值

使用方法如下:

1
2
3
4
5
6
7
8
9
10
import java.util.concurent.atomic.AtomicInteger;

public class AtomicIntegerTest{
static AtomicInteger ai = new AtomicInteger(1);

public static void main(String[] args){
System.out.println(ai.getAndIncrement()); //返回旧值
System.out.println(ai.get()); //返回新值
}
}

测试结果如下:

1
2
1
2

那么 AtomicInteger 底层是如何实现的呢?我们扒一下 JDK 1.8 中的源码:

1
2
3
4
5
6
7
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

其中 getAndAddInt 的源码如下:

1
2
3
4
5
6
7
8
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

这里面的 this 指示的是 unsafe 对象,可以看到最终都是调用 unsafe.compareAndSwapInt 方法,也就是 CAS 方法。

底层调用的都是 Unsafe 类的方式,类权限定名为sun.misc.Unsafe,由于这个包是 JDK 的 native 方法,底层使用 C++ 实现,用户不能直接通过 new Unsafe() 或者 Unsafe.getUnsafe() 等方法创建对象。

所以换种方法,我们通过查看 openjdk-8 的源码来查看底层操作,目录是openjdk-8-src-b132-03_mar_2014\openjdk\jdk\src\share\classes\sun\misc。此类包含了低级(native 硬件级别的原子操作)、不安全的操作集合。

获取 Unsafe 实例静态方法:

1
2
3
4
5
6
7
8
9
10
11
private Unsafe() {}

private static final Unsafe theUnsafe = new Unsafe();

@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}

我们再看一下其中的 public native 方法,扩展的 C++ 就不看了:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//扩充内存  
public native long reallocateMemory(long address, long bytes);

//分配内存
public native long allocateMemory(long bytes);

//释放内存
public native void freeMemory(long address);

//在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes, byte value);

//从一个内存块拷贝到另一个内存块
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);

//获取值,不管java的访问限制,其他有类似的getInt,getDouble,getLong,getChar等等
public native Object getObject(Object o, long offset);

//设置值,不管java的访问限制,其他有类似的putInt,putDouble,putLong,putChar等等
public native void putObject(Object o, long offset);

//从一个给定的内存地址获取本地指针,如果不是allocateMemory方法的,结果将不确定
public native long getAddress(long address);

//存储一个本地指针到一个给定的内存地址,如果地址不是allocateMemory方法的,结果将不确定
public native void putAddress(long address, long x);

//该方法返回给定field的内存地址偏移量,这个值对于给定的filed是唯一的且是固定不变的
public native long staticFieldOffset(Field f);

//报告一个给定的字段的位置,不管这个字段是private,public还是保护类型,和staticFieldBase结合使用
public native long objectFieldOffset(Field f);

//获取一个给定字段的位置
public native Object staticFieldBase(Field f);

//确保给定class被初始化,这往往需要结合基类的静态域(field)
public native void ensureClassInitialized(Class c);

//可以获取数组第一个元素的偏移地址
public native int arrayBaseOffset(Class arrayClass);

//可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用, 可以定位数组中每个元素在内存中的位置
public native int arrayIndexScale(Class arrayClass);

//获取本机内存的页数,这个值永远都是2的幂次方
public native int pageSize();

//告诉虚拟机定义了一个没有安全检查的类,默认情况下这个类加载器和保护域来着调用者类
public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);

//定义一个类,但是不让它知道类加载器和系统字典
public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);

//锁定对象,必须是没有被锁的
public native void monitorEnter(Object o);

//解锁对象
public native void monitorExit(Object o);

//试图锁定对象,返回true或false是否锁定成功,如果锁定,必须用monitorExit解锁
public native boolean tryMonitorEnter(Object o);

//引发异常,没有通知
public native void throwException(Throwable ee);

//CAS,如果对象偏移量上的值=期待值,更新为x,返回true.否则false.类似的有compareAndSwapInt,compareAndSwapLong,compareAndSwapBoolean,compareAndSwapChar等等。
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);

// 该方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。类似的方法有getIntVolatile,getBooleanVolatile等等
public native Object getObjectVolatile(Object o, long offset);

//线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。
public native void park(boolean isAbsolute, long time);

//终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,也正是使用这两个方法
public native void unpark(Object thread);

//获取系统在不同时间系统的负载情况
public native int getLoadAverage(double[] loadavg, int nelems);

//创建一个类的实例,不需要调用它的构造函数、初使化代码、各种JVM安全检查以及其它的一些底层的东西。即使构造函数是私有,我们也可以通过这个方法创建它的实例,对于单例模式,简直是噩梦,哈哈
public native Object allocateInstance(Class cls) throws InstantiationException;

上面这些有兴趣可以仔细看看,我们主要看 compareAndSwapObject 的使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Atomically exchanges the given reference value with the current
* reference value of a field or array element within the given
* object <code>o</code> at the given <code>offset</code>.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param newValue new value
* @return the previous value
* @since 1.8
*/
public final Object getAndSetObject(Object o, long offset, Object newValue) {
Object v;
do {
v = getObjectVolatile(o, offset); //获取对象内存地址偏移量上的数值v
} while (!compareAndSwapObject(o, offset, v, newValue)); //如果现在还是v,设置为newValue,否则返回false,!false=true,一直循环直到等于v退出循环返回v.
return v;
}

到这里我们就可以明白了,更新数值都是基于 CAS 实现的,

CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。这一系列的操作是原子的。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”。(这段描述引自《Java并发编程实践》)

简单的来说,CAS有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则返回 V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而 Synchronized 是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。

原子更新数组

通过原子的方式更新数组里某个元素,Atomic 包中提供以下类:

  • AtomicIntegerArray:原子更新整型数组中元素
  • AtomicLongArray:原子更新长整型数组中的元素
  • AtomicReferenceArray:原子更新引用类型数组中的元素

其中 AtomicIntegerArray 类主要是提供原子的方式更新数组中的整形,其常用方法如下:

  1. int addAndGet(int i,int delta):以原子方式将输入值与数组中索引 i 的元素相加
  2. boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置 i 的元素设置为 update 值。

以上几个类提供的方法几乎一样,我们只以 AtomicIntegerArray 讲解:

1
2
3
4
5
6
7
8
9
10
public class AtomicIntegerArraytest{
static int[] value = {1,2};
static AtomicIntegerArray ai = new AtomicIntegerArray(value);

public static void main(String[] args){
ai.getAndSet(0,3);
System.out.println(ai.get(0));
System.out.println(value[0]);
}
}

测试结果:

1
2
3
1

需要注意的是,AtomicIntegerArray 传入数组的构造方法仅仅是将原数组复制了一份,所以原数组中内容不会被影响。

原子更新引用类型

原子更新基本类型 AtomicInteger 只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供以下类:

  • AtomicReference:原子更新引用类型
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段
  • AtomicMarkableReference:原子更新带有引用标记的引用类型,可以原子更新一个布尔类型的标记位和引用类型。构造方法是 AtomicMarkableReference(V initialRef,boolean initialMark)

此处仅以 AtomicReference 为例进行讲解:

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
public class AtomicReferenceTest{
public static AtomicReference<user> atomicUserRef = new AtomicReference<user>();

public static void main(String[] args){
User user = new User("conan",15);
atomicUserRef.set(user);
User updateUser = new User("Shinichi",17);
atomicUserRef.compareAndSet(user,updateUser);
System.out.println(atomicUserRef.get().getName());
System.out.println(atomicUserRef.get().getOld());
}

static class User{
private String name;
private int old;

public User(String name,String old){
this.name = name;
this.old = old;
}

public String getName(){
return name;
}

public int getOld(){
return old;
}
}
}

输出结果如下:

1
2
Shinichi
17

原子更新字段类

如果需要原子更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic 包提供了以下 3 个类进行原子字段更新:

  • AtomicintegerFieldUpdater:原子更新整型的字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新出现的 “ABA” 问题。

要想原子的更新字段类需要两步,第一步因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法 newUpdatar() 创建一个更新器,并且需要设置想要更新的类和属性;第二步,更新类的字段(属性)必须使用 public volatile 修饰符。

此处仅以 AtomicIntegerFieldUpdater 为例:

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
public class AtomicIntegerFieldUpdaterTest{
//创建原子更新器,并设置需要更新的对象类和对象的属性
private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class,"old");

public static void main(String[] args){
User conan = new User("conan",10);

//柯南长了一岁,但是还是会输出旧的年龄
System.out.println(a.getAndIncrement(conan));
//输出柯南现在的年龄
System.out.println(a.get(conan));
}

public static class User{
private String name;
public volatile int old;

public User(String name,int old){
this.name = name;
this.old = old;
}

public String getName(){
return name;
}

public int getOld(){
return old;
}
}
}

输出结果如下:

1
2
10
11

参考文章

  1. 方腾飞 等著 《Java 并发编程的艺术》
  2. 在openjdk8下看Unsafe源码
  3. Java线程(十):CAS
如果这篇文章对您很有帮助,不妨
-------------    本文结束  感谢您的阅读    -------------
0%