Java并发包-atomic包-让您彻底掌握AtomicIntegerArray源码

本文详细探讨了Java并发包中的AtomicIntegerArray,通过实例展示了其在多线程环境下的原子操作特性。文章深入解析了AtomicIntegerArray的源码,包括重要成员变量base、scale和shift的作用,以及如何确保元素的线程安全性。此外,还对比了AtomicIntegerArray与普通数组的区别,强调了其对数组元素的原子操作而非整个数组的原子性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇文章的主要内容如下:

1:AtomicIntegerArray实例
2:AtomicIntegerArray的源码解析
3:AtomicInLongArray
1、AtomicIntegerArray实例
接下来的例子是定义一个普通的整型数组和一个AtomicIntegerArray在多线程下添加元素,看看会出现什么效果。

public class AtomicIntegerArrayTest {
private static int[] intArr = new int[5];
private static AtomicIntegerArray aiArr = new AtomicIntegerArray(5);
private static ThreadPoolExecutor pool;
static {
pool = new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));
}
public static void main(String[] args) throws InterruptedException {
intArrTest();
aiArrTest();
pool.awaitTermination(3, TimeUnit.SECONDS);
System.out.println(“AtomicInteger=” + aiArr);
StringBuilder sb = new StringBuilder(“整形数组=[”);
for(int i=0;i<intArr.length;i++){
sb.append(intArr[i]).append(" “);
}
sb.append(”]");
System.out.println(sb.toString());
pool.shutdown();
}
//普通整型数组多线程下添加元素
public static void intArrTest() {
for (int i = 0; i < 5; i++) {
pool.execute(() -> {
for (int j = 0; j < 10000; j++) {
intArr[j % 5]++;
}
});
}
}
//AtomicIntegerArray多线程下添加元素
public static void aiArrTest() {
for (int i = 0; i < 5; i++) {
pool.execute(() -> {
for (int j = 0; j < 10000; j++) {
aiArr.getAndIncrement(j % 5);
}
});
}
}
}
运行结果如下:

Java并发包-atomic包-让您彻底掌握AtomicIntegerArray源码
Java并发包-atomic包-让您彻底掌握AtomicIntegerArray源码
通过上面的运行结果可以看出,普通的int[]数组每一次运行结果都是不同的,而AtomicIntegerArray无论运行多少次,运行结果都是一样且正确的。所以大家是不是明白了AtomicIntegerArray是保证数组中元素的原子操作,而不是数组本身的原子操作。

2、AtomicIntegerArray的源码解析
首先说一下AtomicIntegerArray的特点:

1:AtomicIntegerArray是保证整形数组的元素进行原子操作,而不是数组本身
好多同学刚接触AtomicIntegerArray时都认为是原子性操作数组,其实它是保证整形数组元素的原子操作,我们从源码中可以得到这个答案,那接下来我们进入源码看一下。

2.1、重要的成员变量

//CAS的包装类
private static final Unsafe unsafe = Unsafe.getUnsafe();
//获取对象头的长度
private static final int base = unsafe.arrayBaseOffset(int[].class);
private static final int shift;
//底层的整形数组,但是没有被volatile修饰
private final int[] array;
//静态代码块
static {
//数组中元素字节长度,int类型是4个字节
int scale = unsafe.arrayIndexScale(int[].class);
if ((scale & (scale - 1)) != 0)
throw new Error(“data type scale not a power of two”);
shift = 31 - Integer.numberOfLeadingZeros(scale);
}
好多同学对上面的base、scale、shift都不太明白,接下来我详细分析以下。在将这些内容之前,我首先简单介绍以下关于JVM怎样定义对象的

一个对象主要包含三个部分:
第一个部分:对象头:主要是包含象本身定义的信息(如:hashCode,锁,GC的分代年龄)和类型指针(通过这个指针可以获取是属于哪一个类)
第二部分:实例数据:我们定义的各种字段。
第三部分:对齐填充:使对象占内存是8字节的整数倍
上面只是对对象的定义做了简单的介绍,关于JVM详细的内容我会有一个专题去介绍,这里就不在展开说明了。简单的理解对象的构造后下面的内容就非常的好理解了。

base:数组第一个元素的偏移地址。也就是数组对象头所占的字节数。

这个base是通过调用Unsafe中的arrayBaseOffset获取的。我们跟进这个方法中,发现是一个本地方法:

public native int arrayBaseOffset(Class<?> var1);
scale:获取数组元素所占字节长度。

这个scale也是通过调用Unsafe中的arrayIndexScale()方法获取的,跟进这个方法发现也是一个本地方法:

public native int arrayIndexScale(Class<?> var1);
通过下面的大白话说明更能让大家理解:

1:int类型,4个字节,那么通过上面方法算出的就是scale=4
2:long类型,8个字节,那么通过上面方法算出的就是scale=8
通过跟踪断点,如图:

Java并发包-atomic包-让您彻底掌握AtomicIntegerArray源码
int类型获取的scale=4

Java并发包-atomic包-让您彻底掌握AtomicIntegerArray源码
long类型获取的scale=8

所以通过arrayBaseOffset()方法和arrayIndexScale()方法可以获取数组元素的内存地址

shift:左移的基数

shift = 31 - Integer.numberOfLeadingZeros(scale);
在这一篇文章我已经介绍过了numberOfLeadingZeros(),它的大概意思就是获取指定数据二进制第一次出现1前的0的个数。如图:

Java并发包-atomic包-让您彻底掌握AtomicIntegerArray源码
我们知道scale=4,所以

Java并发包-atomic包-让您彻底掌握AtomicIntegerArray源码
所以shift=31-29=2

通过上面的讲解,大家对上面的base/scale/shift是非常清楚了,他们是为查找到数组中某一个元素的内存偏移地址服务的。那接下来我们看一下怎样获取元素的偏移地址。

private long checkedByteOffset(int i) {
if (i < 0 || i >= array.length)
//走到这一步:说明给定的数组下标不合法
throw new IndexOutOfBoundsException("index " + i);
//返回指定i在内存的偏移量
return byteOffset(i);
}
private static long byteOffset(int i) {
return ((long) i << shift) + base;
}
上面的byteOffset就是获取下标为i在数组内存的偏移量,因为int类型的scale=4,所以下标为i的内存偏移地址就是base+(i-1)*4,和((long)i<<2)+base意义一样。举个例子如下:

1:i=4
2:base+4*4 = base+16
3:((long) i << shift) + base = (4<<2)+base = 16+base
2.2、构造函数

上面讲解了在AtomicIntegerArray的成员变量,但是大家发现底层数组array用关键字final修饰,并没有用关键字volatile修饰,因为volatile只对单个变量起作用,从这也可以看出AtomicIntegerArray能够保证数组中的元素线程安全,而不是数组本身。

//第一个构造函数:传递底层数组的长度,因为array被final修饰,所以数组本身不能修改。
public AtomicIntegerArray(int length) {
array = new int[length];
}
//第二个构造函数:传递一个数组,从代码可以看出是复制一份,然后赋值给底层数组,当底层数组改变时不影响传递的那个参数。
public AtomicIntegerArray(int[] array) {
// Visibility guaranteed by final field guarantees
this.array = array.clone();
}
2.3、重要的方法

第一类方法:对数组中的元素单一操作

//获取底层数组的长度,因为array是final修饰,所以数组长度不能变化,无需加锁。
public final int length() {
return array.length;
}
//获取指定下标的元素
public final int get(int i) {
return getRaw(checkedByteOffset(i));
}
//通过unsafe原子获取数组下标为i的元素。
private int getRaw(long offset) {
return unsafe.getIntVolatile(array, offset);
}
//通过unsafe原子设置数组下标为i的元素的值。
public final void set(int i, int newValue) {
unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
}
第二类方法:对数组中元素的符合操作

//比较数组下标为i的元素,成功则设置,失败则自旋。
public final boolean compareAndSet(int i, int expect, int update) {
return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
private boolean compareAndSetRaw(long offset, int expect, int update) {
return unsafe.compareAndSwapInt(array, offset, expect, update);
}
//原子设置数组下标为i的值为newValue,然后返回旧值
public final int getAndSet(int i, int newValue) {
return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
}
//通过CAS原子设置下标为i的值+delta,然后返回旧值
public final int getAndAdd(int i, int delta) {
return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}
//通过CAS原子设置下标为i的值+1,并返回+1前的旧值
public final int getAndIncrement(int i) {
return getAndAdd(i, 1);
}
//通过CAS原子设置下标为i的值+1,并返回新值
public final int incrementAndGet(int i) {
return getAndAdd(i, 1) + 1;
}
//通过CAS原子设置下标为i的值+delta,并返回新值
public final int addAndGet(int i, int delta) {
return getAndAdd(i, delta) + delta;
}
//通过CAS原子设置下标为i的值-1,并返回旧值
public final int getAndDecrement(int i) {
return getAndAdd(i, -1);
}
//通过CAS原子设置下标为i的值-1,并返回新值
public final int decrementAndGet(int i) {
return getAndAdd(i, -1) - 1;
}
第三类方法:对数组中元素的更加复杂操作。

//获取下标为i的元素值pre,然后将pre传递给功能函数,并返回旧值pre
public final int getAndUpdate(int i, IntUnaryOperator updateFunction) {
long offset = checkedByteOffset(i);
int prev, next;
do {
prev = getRaw(offset);
next = updateFunction.applyAsInt(prev);
} while (!compareAndSetRaw(offset, prev, next));
return prev;
}
//获取下标为i的元素值pre,然后将pre传递给功能函数,并返回新值next
public final int updateAndGet(int i, IntUnaryOperator updateFunction) {
long offset = checkedByteOffset(i);
int prev, next;
do {
prev = getRaw(offset);
next = updateFunction.applyAsInt(prev);
} while (!compareAndSetRaw(offset, prev, next));
return next;
}
//获取下标为i的元素值pre,将x和pre传递给功能函数,并返回旧值pre
public final int getAndAccumulate(int i, int x,
IntBinaryOperator accumulatorFunction) {
long offset = checkedByteOffset(i);
int prev, next;
do {
prev = getRaw(offset);
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSetRaw(offset, prev, next));
return prev;
}
//获取下标为i的元素值pre,将x和pre传递给功能函数,并返回新值next
public final int accumulateAndGet(int i, int x,
IntBinaryOperator accumulatorFunction) {
long offset = checkedByteOffset(i);
int prev, next;
do {
prev = getRaw(offset);
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSetRaw(offset, prev, next));
return next;
}
从上面的所有方法中,可以看出几乎所有的方法都是通过CAS对数组中的元素进行操作,而不是数组本身,所以要记住AtomicIntegerArray和AtomicLongArray都是对数组中的元素进行操作的。

3、AtomicLongArray
AtomicLongArray和AtomicIntegerArray机制是一样的,大家可以自行观看源码,这里就不在介绍了。接下来一篇文章将对atomic包中另外一个类型:AtomicIntegerFieldUpdater进行详细分析。

还有就是这我总结出了一些架构视频资料和互联网公司java程序员面试涉及到的绝大部分面试题和答案做成了文档和架构视频资料还有完整高清的java进阶架构学习思维导图免费分享给大家(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术资料),希望能帮助到您面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

料领取方式:加群:374308445填写【优快云 资料】即可免费获取!!!
如果您喜欢本文章,可以点击关注,每天将有更多精彩文章与您分享!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值