Java原子类剖析

简介

通常情况下,在Java中,++i这类自增/自减运算符在并发环境中不能保证并发安全。需要通过加锁才能解决并发环境下的原子性问题。Atomic原子类通过CAS方式来解决线程安全问题,CAS是一种无锁算法(乐观锁),乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。

Atomic原子类分为以下几类:

  • 基本类型:AtomicInteger,AtomicLong,AtomicBoolean
  • 数组类型:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • 引用类型:AtomicReference,AtomicStampedRerence,AtomicMarkableReference
  • 更新器类:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
  • Java8 新增类:DoubleAccumulator,DoubleAdder,LongAccumulator,LongAdder

使用

原子更新基本类型

使用原子的方式更新基本类型。

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

以下以AtomInteger举例:

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
//以原子方式将给定值与当前值相加,线程安全的i = i + delta
int addAndGet(int delta);
//如果当前值== except,则以原子方式将当前值设置为update,成功返回true
boolean compareAndSet(int expect, int update);
//以原子方式将当前值减1,相当于线程安全的i--
int decrementAndGet();
//以原子方式将当前值加1,相当于线程安全的i++
int incrementAndGet()


private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

/**
* Atomically decrements by one the current value.
*
* @return the updated value
*/
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}

从上面代码可以看出,AtomicInteger底层使用volatile关键字和CAS来保证线程安全。其中: - volatile保证线程的可见性 - CAS保证原子性

原子更新数组

以原子方式更新数组中的某个元素。 - AtomicIntegerArray: 原子更新整型数组里的元素。 - AtomicLongArray: 原子更新长整型数组里的元素。 - AtomicReferenceArray: 原子更新引用类型数组里的元素。

1
2
3
4
5
6
public static void main(String[] args) throws InterruptedException {
AtomicIntegerArray array = new AtomicIntegerArray(new int[] { 0, 0 });
System.out.println(array);
System.out.println(array.getAndAdd(1, 2));
System.out.println(array);
}

原子更新引用类型

  • AtomicReference: 原子更新引用类型。
  • AtomicStampedReference: 原子更新引用类型,内部使用Pair来存储元素值及其版本号。
  • AtomicMarkableReferce: 原子更新带有标记位的引用类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建两个Person对象,它们的id分别是101和102。
Person p1 = new Person(101);
Person p2 = new Person(102);
// 新建AtomicReference对象,初始化它的值为p1对象
AtomicReference ar = new AtomicReference(p1);
// 通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。
ar.compareAndSet(p1, p2);
Person p3 = (Person)ar.get();
System.out.println("p3 is "+p3);
System.out.println("p3.equals(p1)="+p3.equals(p1));


//output

p3 is id:102
p3.equals(p1)=false

原子更新字段类

  • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
  • AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
  • AtomicReferenceFieldUpdater: 原子更新引用的更新器。

这四个类通过反射更新字段的值,使用字段类如下: 1. 使用静态方法newUpdater()创建一个更新器,并需要设置想要更新的类和属性。 2. 更新类的字段必须使用public volatile修饰。

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
public class AtomicIntegerFieldUpdaterTest {


private static Class<Person> cls;

public static void main(String[] args) {
AtomicIntegerFieldUpdater<Person> mAtoLong = AtomicIntegerFieldUpdater.newUpdater(Person.class, "id");
Person person = new Person(1000);
mAtoLong.compareAndSet(person, 1000, 1001);
System.out.println("id="+person.getId());
}
}

class Person {
volatile int id;
public Person(int id) {
this.id = id;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
}

使用该类有如下约束: - 字段必须由volatile修饰 - 字段的访问修饰符能让调用方访问到。 - 只能是实例变量,不能加static - 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。

CAS

CAS(Compare-And-Swap):是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值。一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则返回false。CAS的基本思路就是循环进行CAS操作直到成功为止。虽然乐观锁通常情况下比悲观锁性能更优,但是还存在以下一些问题。

ABA问题

CAS自旋需要在操作值得时候检查是否发生变化,但是如果一个值是A,变成B,然后又变成A,CAS检查会发现没有变化。AtomicStampedReference来解决ABA问题:这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

循环时间长开销大

CAS自旋如果长时间不成功,会给CPU带来较大得执行开销。

只能保证一个共享变量的原子操作

对多个共享变量操作时,CAS无法保证多个操作的原子性,但是可以使用锁来解决。 当然可以将多个共享变量合并成一个共享变量来操作,比如i = 2;j = a,合并为ij = 2a,然后CAS操作ij,从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

Unsafe-CAS解析

Java中原子类中的自旋CAS操作通过Unsafe类实现,Unsafe类是位于sun.misc下的一个类,提供的功能主要如下: - 数组相关 - 返回数组元素内存大小 - 返回数组首元素偏移大小 - 内存屏障 - 禁止load, store重排序 - 系统相关 - 返回内存页的大小 - 返回系统指针大小 - 线程调度 - 线程挂起, 恢复 - 获取,释放锁 - 内存操作 - 分配、拷贝、扩充、释放堆外内存 - CAS - Class - 动态创建类 - 获取field的内存地址偏移量 - 检测、确保类初始化 - 对象操作 - 获取对象成员属性的偏移量 - 非常规对象实例化 - 存储、获取指定偏移地址的变量值。

下文对Unsafe的CAS实现做简要分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));

return var5;
}

public final long getAndSetLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var4));

return var6;
}

Unsafe调用compareAndSwapInt进行CAS操作,使用while操作进行自旋。这是一个native方法,实现位于unsafe.cppcmpxchg在不同平台上有不同的实现。

1
2
3
4
5
6
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!