zoukankan      html  css  js  c++  java
  • 原子数组AtomicIntegerArray实现原理简析

    1 前言

    JDK官方提供了3个原子数组,它们提供了原子更新数组中元素的能力,它们主要借助Unsafe类实现其核心功能。

    • AtomicIntegerArray:原子更新整型数组里的元素

    • AtomicLongArray:原子更新长整型数组里的元素。

    • AtomicReferenceArray:原子更新引用类型数组里的元素。

    在分析原子数组之前,我们先来了解Java对象的内存布局,Java对象由对象头和实例数据两部分组成。

    下图中MarkWord包含对象的hashCode、锁信息、垃圾回收的分代信息等,占32/64位;Class Metadata Pointer表示一个此对象数据类型的Class对象(虚拟机中的Klass对象)的指针,占32/64位;ArrayLength是数组对象特有的内容,表示数组的长度,占32位。数组对象的实例数据是各个元素的值或引用,普通对象的实例数据是各实例字段的值或引用。另外为了快速内存分配、快速内存寻址、提高性能,Java语言规范要求Java对象要做内存对齐处理,每个对象占用的内存字节数必须是8的倍数,若不是则要填零补足对齐。

    从下图可以看出,字段与对象头之间的偏移量是固定的,只要知道字段的相对偏移量和对象起始地址,我们就能获取此字段的绝对内存地址(fieldAddress=objAddress+fieldOffset),根据此绝对内存地址,我们就能忽略访问修饰符的限制而可直接读取/修改此字段的值或引用。

    数组对象的元素内存定址,相对对于普通对象的字段定址有些不一样,它要先计算出对象头的长度,作为基础偏移量;由于数组元素的数据类型是相同的,每个元素的值或引用所占内存空间是相同的,因此将元素值或引用或占内存作为每两相邻元素的相对偏移量。根据对象起始位置、基础偏移量、相邻元素相对偏移量及数组下标,就可以获取到某个元素值或引用的绝对内存地址(itemAddress=arrayAddress+baseOffset+index*indexOffset),进而通过绝对内存地址读取或修改此元素的值或引用。

    2 源码分析

    这里以JDK1.8的AtomicIntegerArray为例,对原子数组做出简单的实现原理分析。

    (1) 主要字段

    //Unsafe是所有并发工具的基础工具类
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //数组对象头到数组首元素间的地址偏移量
    private static final int base = unsafe.arrayBaseOffset(int[].class);
    //数组中相邻元素的地址偏移量的位移表示形式(若shift=x,那么相邻元素的偏移量就是"1<<shift")
    private static final int shift;
    //实际存放元素的数组
    private final int[] array;

    静态块主要是对静态常量shift初始化。

    static {
        int scale = unsafe.arrayIndexScale(int[].class);//表示相邻元素的地址偏移量
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");//非法的,scale一定是2的幂次方
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }

    scale表示相邻元素的地址偏移量,因为内存对齐的原因,scale一定是2n。而shift是相邻元素的地址偏移量的位移表示形式,Integer.numberOfLeadingZeros(scale)取scale进制前导零的个数,所以shift是scale二进制有效位数减1,即有效的0的个数。
    如scale=16,即scale=0b00000000_00000000_00000000_00010000 , 那么shift=4。


    (2) 方法

    构造方法

    AtomicIntegerArray(int)创建指定长度的原子数组,AtomicIntegerArray(int)利用克隆(这里是深度克隆,源数组和原子数组互不影响)将指定的普通数组包装成原子数组.

    public AtomicIntegerArray(int length) {
        array = new int[length];
    }
    public AtomicIntegerArray(int[] array) {
        // Visibility guaranteed by final field guarantees
        this.array = array.clone();
    }

    获取指定下标元素get(int)

    get()方法主要利用byteOffset根据公式offset=base+i*scale求出数组对象的起始位置下标元素的偏移量offset,然后利用unsafe.getIntVolatile根据公式indexAddr=arrayAddr+offset求出出指定偏移位置的元素值

        public final int get(int i) {
            return getRaw(checkedByteOffset(i));
        }
        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; //i*2^shift +base 即,"i*元素间距+对象头长度"
        }
    
        private int getRaw(long offset) {
            return unsafe.getIntVolatile(array, offset);//利用unsafe方法求出指定偏移位置的int值
        }

    更新指定下标元素set(int,int)

    set()方法与get方法类似,都要先调用checkedByteOffset计算偏移量offset,不过此set方法要调用putIntVolatile在指定内存位置修改值而已。

    public final void set(int i, int newValue) {
        unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
    }

    getAndSet(int,int)

    先获取值再设置新值,同样先调用checkedByteOffset计算偏移量offset,然后getAndSetInt进行CAS自旋更新指定的元素。

    public final int getAndSet(int i, int newValue) {
        return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
    }
    public final int getAndSetInt(Object o, long offset, int newValue) {//unsafe的方法
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, newValue));
        return v;
    }

    compareAndSet()CAS更新期望的元素,先调用checkedByteOffset计算偏移量offset,然后调用compareAndSwapInt尝试CAS更新元素。

    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);
    }

    3 使用示例

    class AtomicIntArrayDemo {
    
         public static void main(String[] args) {
             AtomicIntegerArray array = new AtomicIntegerArray(new int[]{45,23,13,47,12,42});
             for (int i = 0; i <array.length() ; i++) {
                 final int j=i;
                 new Thread(()->{
                     array.compareAndSet(j, 13, 31);
                     array.getAndAdd(j, 2);
                     array.decrementAndGet(j);
                     array.getAndSet(j, array.get(j) - 1);
                 }).start();
    
             }
             System.out.println(array);
         }
    }



  • 相关阅读:
    IDEA的Debug详解
    websocket学习(转载)
    Shiro授权及注解式开发
    Redis分布式缓存安装和使用
    JEESZ-SSO解决方案
    英语是学习Java编程的基础吗
    深入分析 ThreadLocal 内存泄漏问题
    这些JVM命令配置参数你知道吗?
    安全开发Java动态代理
    学java编程软件开发,非计算机专业是否能学
  • 原文地址:https://www.cnblogs.com/gocode/p/analysis-source-code-AtomicIntegerArray.html
Copyright © 2011-2022 走看看