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

简介: 前言最近在看资料的时候偶然间看到了AtomicLongFieldUpdater这个工具类,觉得新鲜就查阅了相关的资料,发现居然是jdk1.5就有的工具类,不禁感叹自己对Java的理解还是太浅了,于是在此整理一下该类的资料,作为知识储备。

前言

最近在看资料的时候偶然间看到了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

// 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属性是版本的计数。

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类定义如下:

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

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

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
目录
相关文章
|
11天前
|
前端开发 开发者 数据格式
|
4月前
|
算法 编译器 C语言
【C++模板的派生】探索C++模板中的依赖名称问题及其解决方案
【C++模板的派生】探索C++模板中的依赖名称问题及其解决方案
50 0
|
3月前
|
存储 Swift
Swift开发——属性检查器
Swift中的属性检查器(willSet, didSet)允许在设置存储属性值前后执行代码。在类`Circle`中,属性`radius`使用属性观察器:willSet在赋值前检查值,若值为负则打印警告;didSet在赋值后比较新旧值,根据变化输出相应信息。在实例`c`中,`radius`从-5变为0时,输出“Input value is negative.”和“The circle gets smaller.”;从0变为10时,输出“Input value is normal.”和“The circle gets larger.”。
207 4
Swift开发——属性检查器
|
3月前
|
监控 Java
记录页面修改差异(java注解实现)
记录页面修改差异(java注解实现)
|
4月前
|
程序员 C++
【C++14保姆级教程】带默认成员初始化器的聚合类
【C++14保姆级教程】带默认成员初始化器的聚合类
|
JavaScript 前端开发
vvue基础指令和基础属性
vue基础指令和基础属性
修饰类属性失败。请确保提案类属性已启用并设置为使用松散模式。要在规范模式下将提案类属性与修饰器一起使用,请在阶段 2 中等待下一个主要版本的装饰器。 #80
修饰类属性失败。请确保提案类属性已启用并设置为使用松散模式。要在规范模式下将提案类属性与修饰器一起使用,请在阶段 2 中等待下一个主要版本的装饰器。 #80
42 0
index.js:39 错误:修饰类属性失败。请确保提案类属性已启用并设置为使用松散模式。要在规范模式下将提案类属性与修饰器一起使用,请在阶段 2 中等待下一个主要版本的装饰器。 #79
index.js:39 错误:修饰类属性失败。请确保提案类属性已启用并设置为使用松散模式。要在规范模式下将提案类属性与修饰器一起使用,请在阶段 2 中等待下一个主要版本的装饰器。 #79
71 0
西门子S7-1200PLC变量表如何使用?如何声明、选用、显示、定义、更改变量?变量保持性如何设置?
在S7-1200 CPU的编程理念中,特别强调符号寻址的使用,在开始编写程序之前,用户应当为输入、输出、中间变量定义相应的符号名,也就是标签。具体步骤如下:
西门子S7-1200PLC变量表如何使用?如何声明、选用、显示、定义、更改变量?变量保持性如何设置?
定义一个事件需要单独新建一个文件吗?底层原理是什么?
定义一个事件需要单独新建一个文件吗?底层原理是什么?