理解
同样的数组是存在线程安全问题的,线程安全问题我们可以使用无锁或者加锁的方式实现,那想想数组各位觉得用那种方式比较效率高?
我觉得这得分情况了, 如果如果业务维度是按照下标区分的,那么使用锁的话锁住整个数组那么效率肯定没有根据下标偏移量使用cas代理锁的方式效率高,这种业务维度以下标,使用cas的话类似了分段锁的性质,也就是找到了最小的锁粒度。
但是如果业务维度是整个数组,那么就不能使用cas,cas数组下标只保证了单个小标的线程安全,不保证组合下标的线程安全。
数组如何找到下标偏移量
🆗 上图:
那就开始来进行计算:
假设整个一个存int类型的数组:即 int[] arr = new int[7]
使用Unsafe提供的API:
baseOffSet = unsafe.arrayBaseOffset(int[].class)
scale = unsafe.arrayIndexScale(int[].class);
那么如果要得到 3号下标的偏移量:
baseOffSet + scale * 3
好的正常来说问题已经解决了,因为cas的核心就是获取内存中的地址偏移量。
但是: scale 一般来说是基本数据类型的字节大小,int 为 4个字节, long 8个字节
一个知识前提就是 计算机做位移运算是比乘法快的
所以scale*3 可以表示为 3<<2, 因为int为4个字节 所以3*4就是左移两位。
AtomicIntegerArray 如何计算下标
有了上面的理解看看大神怎么设计的吧。。。。。。
public class AtomicIntegerArray implements java.io.Serializable {
private static final long serialVersionUID = 2862133569453604235L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
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); //获取int数组中int元素直接地址的偏移量
if ((scale & (scale - 1)) != 0) //判断是不是2的幂次
throw new Error("data type scale not a power of two");
shift = 31 - Integer.numberOfLeadingZeros(scale);
}
private long checkedByteOffset(int i) {
if (i < 0 || i >= array.length)
throw new IndexOutOfBoundsException("index " + i);
return byteOffset(i);
}
private static long byteOffset(int i) {//获取便宜量
return ((long) i << shift) + base;
}
}
可见和我们推理的是一样 ((long) i << shift) + base;,使用了位移代替了乘法。
那整个shift怎么来呢, 能直接说是4吗,有没有可能不同的jdk上或者机器上 int的字节大小是不一样的,所以我们需要一种算法根据字节大小得出偏移量。。。。也就是shift.
static {
int scale = unsafe.arrayIndexScale(int[].class); //获取int数组中int元素直接地址的偏移量
if ((scale & (scale - 1)) != 0) //判断是不是2的幂次
throw new Error("data type scale not a power of two");
shift = 31 - Integer.numberOfLeadingZeros(scale); //关键代码。。
}
如果是x是2的幂次,那么a*x 也就是等于 a<<(x的幂次), 而x的幂次如何得到,即用x的bit位长度减去最左开始连续的组最大0bit长度,呃这是我的理解。。。。
所以关键代码就在于 Integer.numberOfLeadingZeros(scale),如何获取scale的从最左边开始的0bit长度
public static int numberOfLeadingZeros(int i) { //不能为负数 获取 二进制的32 从最左边开始有多少个连续的0
// HD, Figure 5-6
if (i == 0) //i 为零说明 有32个零
return 32;
int n = 1; //零的个数
if (i >>> 16 == 0) { n += 16; i <<= 16; } //直接二分,如果无符号右移16为零说明,高16位全是0, 所以 零的个数加上16, 然后低16左移到高16位
if (i >>> 24 == 0) { n += 8; i <<= 8; }//低16继续二分也就是8位,如果还为0 说明低16位中的高8位都是0, n加上8,i继续左移8位,移动到高8位
if (i >>> 28 == 0) { n += 4; i <<= 4; }//低8位继续二分也就是4,如果还位0,说明低8位中的高四位都是0,n加上4,i左移4移动到高四位
if (i >>> 30 == 0) { n += 2; i <<= 2; }//第四位继续二分也就是2,移动到高二位
n -= i >>> 31;//判断最高位是不是1 ,那不是还剩2位吗,应为i在传值的时候就做了2的幂次判断,如果为二的幂次 第二位是不会为1的, 例如2, 4 ,8 他们的32为比特位,最低位也就是最右边的比特是0
return n;
}
呃, 建议自己32位比特位带着演示一下。。。。。。。。。。。,这里笔者就不上图了。。
示例运用
public class TestAtomicArray {
public static void main(String[] args) throws InterruptedException {
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
for (int i2 = 0; i2 < 5; i2++) {
atomicIntegerArray.addAndGet(i2, 1);
}
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
for (int s = 0; s < 5; s++) {
System.out.printf(atomicIntegerArray.get(s) + " ");
}
}
}
100000 100000 100000 100000 100000
可见数组安全性得到了保证。
总结
呃,cas需要得到下标偏移量,数组下标偏移量的获取 在AtomicIntegerArray 设计的很巧妙。
还有一个点就是AtomicIntegerArray里面的数组元素的可见性如何保证,cas的话Lock保证了,如果是正常的数组set呢?
public final void set(int i, int newValue) {
unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
}
可见,putIntVolatile 当作volatile变量来赋值,保证了可见性