1. 背景
最近在dubbo、netty和druid(阿里开源连接池)的源码中看到了AtomicIntegerFieldUpdater
、AtomicLongFieldUpdater
或AtomicReferenceFieldUpdater
的身影:
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对象进行原子更新(AtomicLongFieldUpdater
和AtomicReferenceFieldUpdater
作用类似)。
既然已经有了AtomicInteger
,为什么又多此一举弄出个AtomicIntegerFieldUpdater
来呢?其实主要有两方面原因:
- 要使用
AtomicInteger
需要修改代码,将原来int
类型改造成AtomicInteger
,使用该对象的地方都要进行调整(多进行一次get()操作获取值),但是有时候代码不是我们想改就能改动的。 - 也是比较重要的一个特性,
AtomicIntegerFieldUpdater
可以节省内存消耗。
第一点比较好理解,对于一些三方包或外部依赖进来的类,我们通常并不具备修改源代码的权限。第二点节省内存是什么鬼?要说清这个问题,我们需要回顾下java中对象的内存占用问题。
3. Java对象占用的内存
一个对象由以下几部分组成:对象头 + 实例数据 + 对齐填充。
3.1. 对象头
虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据
,如 hashCode
、GC分代年龄
、锁状态标志
、线程持有的锁
、偏向线程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
大小在虚拟机上都是固定的,见下表:
类型 | 大小(字节) | 备注 |
---|---|---|
byte | 1 | 基本类型 |
boolean | 1 | 基本类型 |
char | 2 | 基本类型 |
short | 2 | 基本类型 |
int | 4 | 基本类型 |
float | 4 | 基本类型 |
long | 8 | 基本类型 |
double | 8 | 基本类型 |
reference | 4 | 引用类型实际上是一个地址指针 |
这里还要说明下,从 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. 刨根问底
回顾了对象内存占用问题后,我们继续看AtomicInteger
和AtomicIntegerFieldUpdater
的区别。
打开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>
我们使用这个工具分别测试下对象拥有AtomicInteger
和AtomicIntegerFieldUpdater
属性时的大小。
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());
}
}
最终输出结果:
可以看到,FieldUpdaterTest
比AtomicTest
少了8 bytes。仔细分析上图结果,我们发现后者比前者多了4 bytes 大小的AtomicInteger
对象,这也正是AtomicInteger
中的成员变量int value,而AtomicIntegerFieldUpdater
是staic final
类型,即类变量,并不会占用当前对象的内存。正是基于AtomicIntegerFieldUpdater
该使用特性,当字段所属的类会被创建大量的实例时,如果用AtomicInteger
每个实例里面都要创建AtomicInteger对象,从而多出很多不必要的内存消耗。
5. 结尾
通过今天的学习,我们知道了 AtomicIntegerFieldUpdater
(包括AtomicLongFieldUpdater
和AtomicReferenceFieldUpdater
)在底层框架或中间件代码中还是比较广泛被应用的,我们也有必要深入学习和掌握它。