聊聊AtomicIntegerFieldUpdater的使用

3,105 阅读6分钟

1. 背景

最近在dubbo、netty和druid(阿里开源连接池)的源码中看到了AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater的身影:

1.1. dubbo源码

public class AtomicPositiveInteger extends Number {

    private static final long serialVersionUID = -3038533876489105940L;

    private static final AtomicIntegerFieldUpdater<AtomicPositiveInteger> indexUpdater =
            AtomicIntegerFieldUpdater.newUpdater(AtomicPositiveInteger.class, "index");

    @SuppressWarnings("unused")
    private volatile int index = 0;

    public AtomicPositiveInteger() {
    }
    
    ......
}

1.2. netty源码

public class HashedWheelTimer implements Timer {

    static final InternalLogger logger =
            InternalLoggerFactory.getInstance(HashedWheelTimer.class);

    private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();
    private static final AtomicBoolean WARNED_TOO_MANY_INSTANCES = new AtomicBoolean();
    private static final int INSTANCE_COUNT_LIMIT = 64;
    private static final long MILLISECOND_NANOS = TimeUnit.MILLISECONDS.toNanos(1);
    private static final ResourceLeakDetector<HashedWheelTimer> leakDetector = ResourceLeakDetectorFactory.instance()
            .newResourceLeakDetector(HashedWheelTimer.class, 1);

    private static final AtomicIntegerFieldUpdater<HashedWheelTimer> WORKER_STATE_UPDATER =
            AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimer.class, "workerState");

    private final ResourceLeakTracker<HashedWheelTimer> leak;
    private final Worker worker = new Worker();
    private final Thread workerThread;

    public static final int WORKER_STATE_INIT = 0;
    public static final int WORKER_STATE_STARTED = 1;
    public static final int WORKER_STATE_SHUTDOWN = 2;
    @SuppressWarnings({ "unused", "FieldMayBeFinal" })
    private volatile int workerState; 
    
}

1.3. druid

我们再来瞧瞧druid中的一个issue:

pr内容参见地址:github.com/alibaba/dru…

因为平时开发中很少使用到,不禁好奇这几个类到底有什么神奇之处,让底层中间件框架如此青睐?其实这三个类都是Java大神Doug Lea写的,在jdk1.5中就已经提供了,位于J.U.C包下面。我们知道J.U.C是java提供的并发编程包,那么这三个类一定与并发编程有关了。

2. 代码测试

看下面一个例子:假如有200个线程,同时对一个成员变量进行修改,如何保证线程安全呢?

public class Test {
    private static int a = 0;
    public static void main(String[] args){
        for(int i=0; i<200; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                   a++;
                }
            });
            t.start();
        }
    }
  }

以上代码肯定非线程安全的了。实现线程安全有很多方法,比如最简单的方式给对象加锁,或者使用J.U.C包下提供的AtomicInteger

public class Test {
    private static AtomicInteger a = new AtomicInteger();

    public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    a.incrementAndGet();
                }
            });
            t.start();
        }
    }
}

除此之外,我们还可以使用AtomicIntegerFieldUpdater来实现。

public class Test {

    private volatile int a = 0;
    private static final AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(Test.class, "a");

    public static void main(String[] args) {
        Test test = new Test();
        for (int i = 0; i < 200; i++) {
            final int j = i;
            Thread t = new Thread(() -> {
                System.out.println("i=" + j + ", a=" + updater.incrementAndGet(test));
            });
            t.start();
        }
    }
}

本质上AtomicIntegerFieldUpdater 是基于反射机制实现对volatile对象进行原子更新(AtomicLongFieldUpdaterAtomicReferenceFieldUpdater作用类似)。

既然已经有了AtomicInteger,为什么又多此一举弄出个AtomicIntegerFieldUpdater来呢?其实主要有两方面原因:

  1. 要使用AtomicInteger需要修改代码,将原来int类型改造成AtomicInteger,使用该对象的地方都要进行调整(多进行一次get()操作获取值),但是有时候代码不是我们想改就能改动的。
  2. 也是比较重要的一个特性,AtomicIntegerFieldUpdater可以节省内存消耗。

第一点比较好理解,对于一些三方包或外部依赖进来的类,我们通常并不具备修改源代码的权限。第二点节省内存是什么鬼?要说清这个问题,我们需要回顾下java中对象的内存占用问题。

3. Java对象占用的内存

一个对象由以下几部分组成:对象头 + 实例数据 + 对齐填充。

3.1. 对象头

虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如 hashCodeGC分代年龄锁状态标志线程持有的锁偏向线程ID偏向时间戳等。这部分数据的长度在 32 位和 64 位的虚拟机(未开启指针压缩)中分别为 4 bytes 和 8 bytes ,官方称之为 ”Mark Word”

对象的另一部分是类型指针(kclass),即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。另外如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中却无法确定数组的大小。同样,这部分数据的长度在 32 位和 64 的虚拟机(未开启指针压缩,下文会讲到)中分别为 4 bytes 和 8 bytes。

如果是数组对象再增加4 bytes记录数组长度。

注意:从上图我们可以知道分代年龄为4 bit,这就可以解释为什么对象的分代年龄最大值是15了。

3.2. 实例数据

一个 Java 对象中的实例数据可能包括两种,一是 8 种基本类型,二是实例数据也是个对象,那么实例数据就是个引用reference指针。8 种基本类型和 reference 大小在虚拟机上都是固定的,见下表:

类型大小(字节)备注
byte1基本类型
boolean1基本类型
char2基本类型
short2基本类型
int4基本类型
float4基本类型
long8基本类型
double8基本类型
reference4引用类型实际上是一个地址指针

这里还要说明下,从 JDK 1.6 开始,64 bit JVM 正式支持了 -XX:+UseCompressedOops 这个可以压缩指针,起到节约内存占用的新参数。如果 UseCompressedOops 是打开的(默认开启),则以下对象的指针会被压缩:

  • 所有对象的 klass 属性
  • 所有对象指针实例的属性
  • 所有对象指针数组的元素

由此我们可以计算出对象头大小:

  • 32位虚拟机对象头大小 = Mark Word(4 bytes)+ kclass(4 bytes) = 8 bytes
  • 64位虚拟机对象头大小 = Mark Word(8 bytes)+ kclass(4 bytes) = 12 bytes
  • 64位虚拟机引用指针大小 = 4 bytes

3.3. 对齐填充

由于虚拟机内存管理体系要求 Java 对象内存起始地址必须为 8 的整数倍,换句话说,Java 对象大小必须为 8 的整数倍,当对象头 + 实例数据大小不为 8 的整数倍时,将会使用Padding机制进行填充,譬如, 64 位虚拟机上 new Object() 实际大小为:

Mark Word(8 bytes)+ kclass(4 bytes)[开启指针压缩] = 12 bytes

但由于Padding机制,实际占用空间为: Mark Word(8 bytes)+ kclass(4 bytes)[开启指针压缩] + Padding(4 bytes)= 16 bytes。

4. 刨根问底

回顾了对象内存占用问题后,我们继续看AtomicIntegerAtomicIntegerFieldUpdater的区别。

打开AtomicInteger的源码看,里边只有一个成员变量:

那么我们可以计算出其内存占用大小 = Mark Word(8 bytes)+ kclass(4 bytes)[开启指针压缩] + 实例数据(4 bytes) + Padding(0 bytes)= 16 bytes。

实际是否如此呢?我们使用lucene提供的工具来计算其真实大小:

<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>4.0.0</version>
</dependency>

再来看一个自定义对象的例子:

 public class ObjectSize {

    private int i; // 4
    private int j; // 4
    private String s; // 4 reference指针
    private boolean aBoolean; // 1
    private char c; // 2

}

对象头大小:(Mark Word + kclass)= 8 + 4 = 12 bytes

实例数据大小:4 + 4 + 4 + 2 +1 = 15 bytes

最终对象大小为:12 + 15 + 5(padding)= 32 bytes

这里推荐一款openjdk提供的更直观好用的工具ClassLayout

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.8</version>
</dependency>

我们使用这个工具分别测试下对象拥有AtomicIntegerAtomicIntegerFieldUpdater属性时的大小。

public class FieldUpdaterTest {

    public volatile int a = 0;
    private static final AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(FieldUpdaterTest.class, "a");
}

public class AtomicTest {

    public volatile int a = 0;
    private AtomicInteger atomicInteger = new AtomicInteger(0);
}

public class T {
     public static void main(String[] args) {
        FieldUpdaterTest test = new FieldUpdaterTest();
        System.out.println(ClassLayout.parseInstance(test).toPrintable());

        AtomicTest test2 = new AtomicTest();
        System.out.println(ClassLayout.parseInstance(test2).toPrintable());
    }
}

最终输出结果:

可以看到,FieldUpdaterTestAtomicTest少了8 bytes。仔细分析上图结果,我们发现后者比前者多了4 bytes 大小的AtomicInteger对象,这也正是AtomicInteger中的成员变量int value,而AtomicIntegerFieldUpdaterstaic final类型,即类变量,并不会占用当前对象的内存。正是基于AtomicIntegerFieldUpdater该使用特性,当字段所属的类会被创建大量的实例时,如果用AtomicInteger每个实例里面都要创建AtomicInteger对象,从而多出很多不必要的内存消耗。

5. 结尾

通过今天的学习,我们知道了 AtomicIntegerFieldUpdater(包括AtomicLongFieldUpdaterAtomicReferenceFieldUpdater)在底层框架或中间件代码中还是比较广泛被应用的,我们也有必要深入学习和掌握它。