AtomicXFieldUpdater,属性原子修改的外部工具类

前言

最近在看资料的时候偶然间看到了AtomicLongFieldUpdater这个工具类,觉得新鲜就查阅了相关的资料,发现居然是jdk1.5就有的工具类,不禁感叹自己对Java的理解还是太浅了,于是在此整理一下该类的资料,作为知识储备。本篇博客原文地址AtomicXFieldUpdater,属性原子修改的外部工具类

AtomicXFieldUpdater

根据名字,我们可以知道AtomicLongFieldUpdater是对long型field进行原子update的,它是一个工具类,那么有long型的话是不是就有其他类型?通过查看相应的包,发现还有AtomicIntegerFieldUpdaterAtomicReferenceFieldUpdater两个“同胞兄弟”,它们3个加起来,支持了对Integer、long、Reference的原子操作,分别对应的原子类为AtomicInteger、AtomicLong、AtomicReference,那么updater和对应的原子类有什么区别呢?

AtomicLongFieldUpdater来说,通过API文档,其中写了一句:“通过反射技术来对volatile修饰的long型属性进行原子更新”。这里面有几个关键词:反射volatile原子更新

本着talk is cheap, show me the code的原则,直接上代码来展示它的用法,代码来自博客3

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
// LongTest.java的源码
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
public class LongFieldTest {
public static void main(String[] args) {
// 获取Person的class对象
Class cls = Person.class;
// 新建AtomicLongFieldUpdater对象,传递参数是“class对象”和要update的属性名"id"
AtomicLongFieldUpdater mAtoLong = AtomicLongFieldUpdater.newUpdater(cls, "id");
Person person = new Person(12345678L);
// 比较person的"id"属性,如果id的值为12345678L,则设置为1000。
mAtoLong.compareAndSet(person, 12345678L, 1000);
System.out.println("id="+person.getId());
}
}
class Person {
volatile long id;
public Person(long id) {
this.id = id;
}
public void setId(long id) {
this.id = id;
}
public long getId() {
return id;
}
}

上面代码最终输出的为1000,有几个需要关注的点

  1. updater通过AtomicLongFieldUpdater.newUpdater来构造,通过传入类对应的class和要修改的属性名”id”指定该updater所要修改的类的属性,此处为Person的id
  2. 通过compareAndSet(CAS)来进行原子的修改,12345678L是expectedValue,如果传入的person对象的id不为12345678L,则修改失败,这里保证了线程安全,试想在多线程环境下,threadA和threadB同时执行CAS操作,12345678L是传入的expectedValue,只有第一个到达的线程可以成功的执行CAS,将该id修改成希望修改的值

根据Java API的说法,这个工具类不能保证完全的原子性,它只能保证相同updater上执行CAS和set操作的原子性,因此它的原子性是弱与AtomicLong的。为什么呢?因为AtomicLong是对long型属性加了一层原子引用,任何想要修改该long值的操作都需要先获得该原子引用,而updater不会为属性增加原子引用,它是通过反射技术,通过外部操作去修改long型属性值,因此它的原子保证也是通过外部限制的,因此只能保证同一updater进行CAS和set的原子性。

可以说上面的特性既是AtomicLongFieldUpdater的优点,也是它的缺点,为什么说呢?试想对于下面的类(代码来源于博客1),Record是一个记录类,保存了系统中的一条记录信息,version属性是版本的计数。

1
2
3
4
5
6
7
public class Record {
private final AtomicLong version = new AtomicLong(0);
public long update() {
return version.incrementAndGet();
}
}

update方法更新版本号使其加一。这个类看起来逻辑十分清晰,但有一个隐藏的缺陷,就是每个Record都有一个对应的AtomicLong与之对应,如果系统有上亿条Record(夸张而谈),而我们对于update方法的调用并不是很多,更多的是读取version的值,那么这会对堆空间造成严重的污染,大量的AtomicLong存在于堆上(栈上存引用,堆上存实际的对象值),造成了内存的浪费,且AtomicLong的读取需要使用get方法,效率比正常变量的读取要慢,那么如何优化呢?

使用AtomicLongFieldUpdater,我们可以把Record类定义如下:

1
2
3
4
5
6
7
8
9
10
public class Record {
private static final AtomicLongFieldUpdater<Record> VERSION =
AtomicLongFieldUpdater.newUpdater(Record.class, "version");
private volatile long version = 0;
public long update() {
return VERSION.incrementAndGet(this);
}
}

通过改造,对于version属性正常的读取操作可以像普通属性读取那样进行,而当需要update的时候,使用AtomicLongFieldUpdaterincrementAndGet方法来进行,这样内存空间节省了下来(1. AtomicLong对象的引用和值统统不需要,对于拥有大量Record的系统无疑是很大的内存空间 2. version的读取和普通属性读取相同),还有一个好处是AtomicLongFieldUpdater是一个静态常量,它在Record类加载的时候就放在了堆空间的常量池中,对于N个Record,只需要一个AtomicLongFieldUpdater即可(类静态常量),如何还不清晰的话,可以把static final修饰的看作一个”全局常量”,整个系统只存在一个。

因此,虽然原子性不如AtomicLong,但它的效率很高,在特定的场景下有着很好的应用。Stack Overflow有一个回答比较好引文3,对于CPU的消耗来说,其从小到大依次为:

  • long: 最小, 多线程不安全
  • volatile long: 消耗>long, 多线程读取安全,但无法进行原子操作
  • AtomicLong:消耗>volatile long,多线程读安全,可进行原子操作
  • AtomicLongFieldUpdate:消耗>AtomicLong,因为其使用反射技术,多线程安全和可原子操作

因此AtomicLongFieldUpdate的场景是,对于long的正常读写是占比很大的操作,原子操作只占很小的比例,并且多线程读取时必要的,此时可以使用AtomicLongFieldUpdate+volatile的组合。

AtomicReference

如果我们有一个双向链表,想要对链表中节点的替换进行原子操作,此时我们可以使用如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Node {
private volatile Node left, right;
private static final AtomicReferenceFieldUpdater<Node, Node> leftUpdater =
AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "left");
private static AtomicReferenceFieldUpdater<Node, Node> rightUpdater =
AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "right");
Node getLeft() { return left; }
boolean compareAndSetLeft(Node expect, Node update) {
return leftUpdater.compareAndSet(this, expect, update);
}
// ... and so on
}

compareAndSetLeft方法原子的将left指针从expect修改为update。我们知道,通过CAS实现的并发链表,其并发访问性能是最好的,因为无需给节点进行加锁,上面的代码是一个多线程访问的双向链表,拥有良好的读性能。

总结

通过上文的分析,总结AtomicLongFieldUpdate的使用场景主要如下(对另外两个也适用):

  • 多数情况下对属性操作为正常的读写,偶尔需要原子操作(CAS)
  • 该对象在系统中大量存在(如Record),需要节省AtomicLong的内置原子引用所带来的内存消耗.

据说ConcurrentHashMap, ConcurrentLinkedQueue和ConcurrentSkipListMap中有对AtomicXFieldUpdater工具类的使用,有机会可以通过源码加深一下理解,也可以读一下AtomicXFieldUpdater类的源码,对于AtomicLong和AtomicLongFieldUpdate的内存空间比较可以参考引文6

参考博客

  1. sun.misc.Unsafe的后启示录
  2. AtomicLongFieldUpdater API
  3. Real life use and explanation of the AtomicLongFieldUpdate class
  4. AtomicLongFieldUpdater原子类
  5. Atomic field updaters
  6. Lesser known concurrent classes - Atomic*FieldUpdater