写点什么

AtmoicXXX 与 AtmoicXXXArray 源码分析

用户头像
Darren
关注
发布于: 2020 年 09 月 03 日
AtmoicXXX与AtmoicXXXArray源码分析

内存布局

对象内存布局查看

在查看源码之前,必须先熟悉Java对象的内存布局;



如果想要看Java内存的对象布局,可以在POM文件中引入该依赖。

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



使用该语句可以获取对象的内存布局。

ClassLayout.parseInstance(unsafeModel).toPrintable()



对象组成

Java内存对象由3部分组成





  • Mark Word:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。在32位系统占4字节,在64位系统中占8字节;

  • Class Pointer:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节;

  • Length:如果是数组对象,还有一个保存数组长度的空间,占4个字节;



对象数据

对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节(64位系统中是8个字节)。

对齐单元

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。对象头正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

对象头

64位(启用压缩指针)

|--------------------------------------------------------------------------------------------------------------|--------------------|
| Object Header (96 bits) | State |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| Mark Word (64 bits) | Klass Word (32 bits) | |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| unused:25 | identity_hashcode:31 | cms_free:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Normal |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| thread:54 | epoch:2 | cms_free:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Biased |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| ptr_to_lock_record | lock:2 | OOP to metadata object | Lightweight Locked |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| ptr_to_heavyweight_monitor | lock:2 | OOP to metadata object | Heavyweight Locked |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|
| | lock:2 | OOP to metadata object | Marked for GC |
|--------------------------------------------------------------------------------|-----------------------------|--------------------|





指针压缩

这里说明一下32位系统和64位系统中对象所占用内存空间的大小:

  • 在32位系统下,存放Class Pointer的空间大小是4字节,MarkWord是4字节,对象头为8字节;

  • 在64位系统下,存放Class Pointer的空间大小是8字节,MarkWord是8字节,对象头为16字节;

  • 64位开启指针压缩的情况下,存放Class Pointer的空间大小是4字节,MarkWord是8字节,对象头为12字节;

  • 如果是数组对象,对象头的大小为:数组对象头8字节+数组长度4字节+对齐4字节=16字节。其中对象引用占4字节(未开启指针压缩的64位为8字节),数组MarkWord为4字节(64位未开启指针压缩的为8字节);

  • 静态属性不算在对象大小内。



可以看到,64位JVM消耗的内存会比32位的要多大约1.5倍,这是因为对象指针在64位JVM下有更宽的寻址。对于那些将要从32位平台移植到64位的应用来说,平白无辜多了1/2的内存占用,这是开发者不愿意看到的。

从JDK 1.6 update14开始,64位的JVM正式支持了 -XX:+UseCompressedOops 这个可以压缩指针,起到节约内存占用的新参数。

在Java程序启动时增加JVM参数:-XX:+UseCompressedOops来启用。JDK 1.8,默认该参数就是开启的。

java.util.concurrent.atomic源码分析



Atomic单元素操作



以AtomicInteger为例。

下面来看,成员变量及静态代码块,及构造方法。

private static final Unsafe unsafe = Unsafe.getUnsafe(); // Unsafe类
private static final long valueOffset; // 偏移量
private volatile int value; // 数据 volatile类型的
/**
静态代码块,主要获取value属性在AtomicInteger的内存偏移量
*/
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}



valueOffset属性就是value属性在AtomicInteger.class的偏移量,这个偏移量+对象的地址,就可以获取value这个属性在内存中的具体位置,这个具体位置是影响因素很多,肯定不是按照源代码中属性的顺序出现的,因为还有父类。任何class都是有一个或者多个父类的,父类的中属性也是在该类中的,在真实的排序中,要考虑内存使用情况,byte、short、char、boolean等会优先放在一起,这样可以充分的利用空间,所以除非是对内存布局非常熟悉的老司机,可以经过源码分析出一个属性的偏移量,大部分情况下,都是需要经过工具获取进行验证想法的。



AtomicInteger主要使用的方法,主要使用的CAS操作,拿到了地址和偏移量,进行CAS操作就会非常容易,compareAndSwap是原子操作,由CPU直接实现。CPU实现原理可以查看附录。

public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Unsafe类:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// 根据地址及偏移量直接从内存获取指定的地址空间的值
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}



AtomicInteger中其他方法基本是通过CAS实现线程安全的操作,这个类是JDK1.5出现的,在1.6中增加了一个方法。

public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}

调用的是putOrderedXXX方法,这些方法都比较特殊,调用的是volatile修饰的变量的一个写操作, 我们知道volatile的write为了保证对其他线程的可见性会追加以下两个Fence(内存屏障)

1)StoreStore // 在intel cpu中, 不存在[写写]重排序, 这个可以直接省略了

2)StoreLoad // 这个是所有内存屏障里最耗性能的



关于lazyXXX方法,doug lea给出的解释是

As probably the last little JSR166 follow-up for Mustang, we added a "lazySet" method to the Atomic classes (AtomicInteger, AtomicReference, etc). This is a niche method that is sometimes useful when fine-tuning code using non-blocking data structures. The semantics are that the write is guaranteed not to be re-ordered with any previous write, but may be reordered with subsequent operations (or equivalently, might not be visible to other threads) until some other volatile write or synchronizing action occurs).

The main use case is for nulling out fields of nodes in non-blocking data structures solely for the sake of avoiding long-term garbage retention; it applies when it is harmless if other threads see non-null values for a while, but you'd like to ensure that structures are eventually GCable. In such cases, you can get better performance by avoiding the costs of the null volatile-write. There are a few other use cases along these lines for non-reference-based atomics as well, so the method is supported across all of the AtomicX classes.

For people who like to think of these operations in terms of machine-level barriers on common multiprocessors, lazySet provides a preceeding store-store barrier (which is either a no-op or very cheap on current platforms), but no store-load barrier (which is usually the expensive part of a volatile-write).




把最耗性能的StoreLoad拿掉, 性能必然会提高不少(虽然不能禁止写读的重排序了保证不了可见性, 但给其他应用场景提供了更好的选择, 比如上面Doug Lea举例的场景)



volatile的两大作用:禁止指令重排序(为什么要重排序,因为CPU执行是流水线执行的,为了提高CPU的执行速度)及线程间可见问题,上述的问题以后在详细介绍。AtomicXXX类下的lazyXXX操作,咱们的在日常的使用中基本是使用不到的,咱们暂时先跳过。



AtomicBoolean、AtomicLong、AtomicReference都是类似的操作。



Atomic数组类操作



以AtomicIntegerArray为例。

下面来看,成员变量及静态代码块,及构造方法。

private static final Unsafe unsafe = Unsafe.getUnsafe();
/*
第一个数组元素的偏移量,这块本质上指的是int[]对象中,对象头的大小,因为对象体肯定是在对象头之后的
*/
private static final int base = unsafe.arrayBaseOffset(int[].class);
/*
数组元素本身的偏移量
*/
private static final int shift;
/*
数组元素的引用
*/
private final int[] array;
static {
/*
该方法是获取数组中每个元素的字节数
*/
int scale = unsafe.arrayIndexScale(int[].class);
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
/*
这个方法本质上获取2^x = scale,得出x的值,因为这个值在根据数组索引获取数据非常有用
*/
shift = 31 - Integer.numberOfLeadingZeros(scale);
}



主要方法:

public final boolean compareAndSet(int i, long expect, long update) {
/*
索引转偏移量
*/
return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
private boolean compareAndSetRaw(long offset, long expect, long update) {
/*
还是CAS操作
*/
return unsafe.compareAndSwapLong(array, offset, expect, update);
}
private long checkedByteOffset(int i) {
if (i < 0 || i >= array.length)
throw new IndexOutOfBoundsException("index " + i);
return byteOffset(i);
}
/*
索引转偏移量,根据shift进行移位操作,得到下标i的真实偏移量
*/
private static long byteOffset(int i) {
return ((long) i << shift) + base;
}



AtomicLongArray、AtomicReferenceArray也只类似的操作,就不赘述了。



附录

CPU实现CAS原理:

程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。



intel的手册对lock前缀的说明如下:

确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。



发布于: 2020 年 09 月 03 日阅读数: 820
用户头像

Darren

关注

迟飞的笨鸟 2018.09.26 加入

努力学习的码农

评论 (6 条评论)

发布
用户头像
太坏了!大家都是进来看照片的吧~~~
2020 年 09 月 04 日 13:23
回复
哈哈 那以后多多进来,我以后的文章封面都是美女
2020 年 09 月 04 日 14:40
回复
想看帅哥
2020 年 09 月 04 日 17:33
回复
图片不要用这种啊,要不InfoQ要被关门了。请换一个。
2020 年 09 月 07 日 09:38
回复
查看更多回复
用户头像
内容通俗易懂,手动点赞
2020 年 09 月 03 日 23:23
回复
没有更多了
AtmoicXXX与AtmoicXXXArray源码分析