引言
通过原子类序章我们知道Java并发包提供的原子类共分5类,这里开始介绍第二类数组类,其实也就是通过数组下标原子更新基本类型和引用类型的数组中的元素,它们是:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray。由于AtomicLongArray与AtomicIntegerArray非常类似,所以我还是重点对AtomicIntegerArray和AtomicReferenceArray进行分析。
AtomicIntegerArray
在分析其具体原子更新方法之前,我们有必要先了解一下AtomicIntegerArray的构造方法和一些确定数组下标的基础方法,这将有利于我们对后面其它所有方法的理解。
首先构造方法:
private final int[] array; //内部维护了一个final修饰的整形数组 public AtomicIntegerArray(int length) { array = new int[length]; //构造一个指定长度的所有元素都是0的整形原子数组 } public AtomicIntegerArray(int[] array) { // Visibility guaranteed by final field guarantees this.array = array.clone();//构造一个和指定数组长度、内容一样的数组 }
它有两个构造方法, 注意第二个有实际内容的构造方法这里并没有使用volatile写来保证可见性,但是由于成员属性array是final关键字修饰的,根据Java内存模型JMM之五final内存语义对final域的JMM规则,只要没有在构造方法中逸出this指针,对final域的写入也能够在构造完成将this引用赋值给其它变量时变得立即可见。
基础方法:
private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final int base = unsafe.arrayBaseOffset(int[].class);//第一个元素的偏移地址 private static final int shift; static { int scale = unsafe.arrayIndexScale(int[].class);//数组中单个元素占用的内存空间 if ((scale & (scale - 1)) != 0) //确保是2的幂,即2的多少次方 throw new Error("data type scale not a power of two"); shift = 31 - Integer.numberOfLeadingZeros(scale); } //更加下标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;//通过移位操作计算指定下标的数组元素偏移量 } //通过下标获取数组元素,最终是将下标计算出偏移量 public final int get(int i) { return getRaw(checkedByteOffset(i)); } //通过偏移量获取数组元素,使用了有volatile特性的get方法,可以立即获取最新的值 private int getRaw(long offset) { return unsafe.getIntVolatile(array, offset); }以上的基础方法分两部分,第一部分准备数组元素的基础偏移量和间隔偏移,按理说通过unsafe的arrayBaseOffset和arrayIndexScale两个方法就能确定需要的数据,但是事实上它的逻辑却更复杂,首先它要求scale必须是2的多少次方,对于int类型的数组其值为4,long类型的数组其值为8,然后它又通过 31 - Integer.numberOfLeadingZeros(scale) 方法计算出了一个新用于确定数组元素偏移地址的常量shift,关于numberOfLeadingZeros方法,可以很容易了解到它是利用二分查找方返回指定int值的二进制补码表示形式中在最高位的1左边的0的位数,比如:4的二进制补码为100,那么它的最高位1的左边的0的位数就是29;又如8的二进制补码为1000,那么其值就是28,然后再用31减该值,其实得到的结果就是二进制补码形式中最高位1右边的所有位数,为4的时候,shift就是2,为8的时候,shift就是4。
第二部分就是利用第一部分得到的常量计算出指定数组下标的元素的偏移量,它最终是通过位移操作完成的,例如,下标0的偏移就是base,int类型时,下标1的偏移1<<2 +4 等于8,下标2的偏移是2<<2 +4=12; long类型时,下标1的偏移1<<3 +8=16,下标2的偏移2<<3 +8=24,所以实际上和直接使用公式scale*i + base计算的结果是一样的,只是这里AtomicIntegerArray使用移位操作来计算结果罢了。
接下来,就是AtomicIntegerArray原子更新操作相关的方法了,其实它也主要由AtomicInteger所划分的那四类方法构成,不同的是AtomicIntegerArray更新的是数组中的特定元素而已。
1. 简单自更新
//简单自更新方法1 public final int getAndIncrement(int i) { return getAndAdd(i, 1); } //简单自更新方法2 public final int getAndDecrement(int i) { return getAndAdd(i, -1); } //简单自更新方法3 public final int incrementAndGet(int i) { return getAndAdd(i, 1) + 1; } //简单自更新方法4 public final int decrementAndGet(int i) { return getAndAdd(i, -1) - 1; } //最终实现方法,都是利用了unsafe public final int getAndAdd(int i, int delta) { return unsafe.getAndAddInt(array, checkedByteOffset(i), delta); } //unsafe的getAndAddInt实现 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的简单自更新几乎一样,不同的时此处需要传入更新的下标,然后对指定下标的数组元素进行加减1,它们的实现都是利用了unsafe的getAndAddInt方法:首先使用getIntVolatile获取最新的值,然后通过CAS原子地更新。
2. 简单外更新
//原子的将指定下标的元素更新为新的值 public final void set(int i, int newValue) { unsafe.putIntVolatile(array, checkedByteOffset(i), newValue); } //延迟可见设置指定下标的元素为新的值 public final void lazySet(int i, int newValue) { unsafe.putOrderedInt(array, checkedByteOffset(i), newValue); } //原子的设置指定下标的元素为新值,返回旧值 public final int getAndSet(int i, int newValue) { return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue); } //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); } public final boolean weakCompareAndSet(int i, int expect, int update) { return compareAndSet(i, expect, update); } //原子的将指定下标的数组元素增加delta,返回旧值。 public final int getAndAdd(int i, int delta) { return unsafe.getAndAddInt(array, checkedByteOffset(i), delta); } //原子的将指定下标的数组元素增加delta,返回新值。 public final int addAndGet(int i, int delta) { return getAndAdd(i, delta) + delta; } //Unsafe的getAndSetInt实现 public final int getAndSetInt(Object o, long offset, int newValue) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, newValue)); return v; }它们的实现同样也是利用了CAS+Volatile关键字,不在熬述。
3. 函数式自更新
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; } //返回新值 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; }
同AtomicInteger一样,AtomicIntegerArray也提供了函数式更新指定下标元素的方法,使用的IntUnaryOperator参数类型也是一样的,就不再熬述。
4. 函数式外更新
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; } //返回新值 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; }
同AtomicInteger一样,AtomicIntegerArray也提供了函数式更新指定下标元素的方法,使用的IntBinaryOperator参数类型也是一样的,就不再熬述。
同 AtomicIntegerArray相比AtomicLongArray不同的是内部维护的是一个long型的数组变量,其他都是一样的方法。
AtomicReferenceArray
说起AtomicReferenceArray,它提供的方法其实和AtomicIntegerArray也几乎类似,不同的是它在内部维护的是一个Object类型的数组,构造方法和通过下标定位数组元素的偏移地址的方式一摸一样。它也和AtomicReference一样分为那四类方法,同样没有AtomicIntegerArray和AtomicLongArray那样可以进行加减的操作方法。
另外,AtomicReferenceArray还有一个arrayFieldOffset变量:
private static final long arrayFieldOffset; private final Object[] array; static { try { unsafe = Unsafe.getUnsafe(); arrayFieldOffset = unsafe.objectFieldOffset (AtomicReferenceArray.class.getDeclaredField("array")); } catch (Exception e) { throw new Error(e); } }它表示内置对象数组的其实偏移地址,它只有一个用处那就是反序列化,就是如下这个方法:
/** * Reconstitutes the instance from a stream (that is, deserializes it). */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException, java.io.InvalidObjectException { // Note: This must be changed if any additional fields are defined Object a = s.readFields().get("array", null); if (a == null || !a.getClass().isArray()) throw new java.io.InvalidObjectException("Not array type"); if (a.getClass() != Object[].class) a = Arrays.copyOf((Object[])a, Array.getLength(a), Object[].class); unsafe.putObjectVolatile(this, arrayFieldOffset, a); }
相关推荐
数据结构与算法 入门基础 基础数据结构 数组&链表 数组&链表进阶 栈 队列 算法思想 数论&枚举&递归&分治&回溯 排序及其源码实现 贪心&动态规划 高级数据结构 树论基础&二叉树 二叉搜索树&红黑树 BTree Trie树&赫夫曼...
在FastDB中,通过原子指令来实现对数据库并发访问的同步,对查询处理几乎不增加任何开销。FastDB假设整个数据库都在当前内存中,并且在这个假设的基础上优化查询算法和结构。另外,数据库缓存管理几乎不会给FastDB...
◆讲解命令式数据并行、命令式任务并行、并发集合以及协调数据结构。 ◆描述PLINQ高级声明式数据并行。 ◆讨论如何使用新的Visual Studio 2010并行调试功能来调试匿名方法、任务和线程。 ◆演示如何对数据源进行...
◆讲解命令式数据并行、命令式任务并行、并发集合以及协调数据结构。 ◆描述PLINQ高级声明式数据并行。 ◆讨论如何使用新的Visual Studio 2010并行调试功能来调试匿名方法、任务和线程。 ◆演示如何对数据源进行...
◆讲解命令式数据并行、命令式任务并行、并发集合以及协调数据结构。 ◆描述PLINQ高级声明式数据并行。 ◆讨论如何使用新的Visual Studio 2010并行调试功能来调试匿名方法、任务和线程。 ◆演示如何对数据源进行...
实例185 自定义泛型化数组类 235 实例186 泛型方法与数据查询 236 实例187 泛型化方法与最小值 238 实例188 泛型化接口与最大值 239 实例189 使用通配符增强泛型 240 实例190 泛型化的折半查找法 241 第9章 编程常用...
10.1.9 万类之祖——Object类 250 10.2 子类对象?父类对象? 251 10.2.1 父随子行 251 10.2.2 当构造方法遇到继承 254 10.2.3 记得给类一个无参数的构造方法 255 10.2.4 调用父类中的构造方法 256 10.2.5 ...
10.1.9 万类之祖——Object类 250 10.2 子类对象?父类对象? 251 10.2.1 父随子行 251 10.2.2 当构造方法遇到继承 254 10.2.3 记得给类一个无参数的构造方法 255 10.2.4 调用父类中的构造方法 256 10.2.5 ...
5.2.6 不恰当数据结构导致内存占用过大 5.2.7 由Windows虚拟内存导致的长时间停顿 5.3 实战:Eclipse运行速度调优 5.3.1 调优前的程序运行状态 5.3.2 升级JDK 1.6的性能变化及兼容问题 5.3.3 编译时间和类加载...
map 成对的数据结构,键值必须具有唯一性 Servlet和CGI的区别? 答:Servlet与CGI的区别在于Servlet处于服务器进程中,它通过多线程方式允许其service方法,一个实例可以服务于多个请求,并且其实例一般不会被销毁...
volatile:volatile变量表示保证它必须是与主内存保持一致,它实际是""变量的同步"", 也就是说对于volatile变量的操作是原子型的,如用在long 或 double变量前,一般用于多线程编程。 abstract:抽象,必须重载,修饰...
1.7 中的数据结构图: 先来看看 1.7 中的实现。 这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思? 初始化桶大小,因为底层是数组,所以这是数组默认的大小。 桶最大值。 默认的负载因子(0.75)...
/ 126 5.3.5 选择收集器降低延迟 / 130 5.4 本章小结 / 133 第三部分 虚拟机执行子系统 第6章 类文件结构 / 136 6.1 概述 / 136 6.2 无关性的基石 / 136 6.3 Class类文件的结构 / 138 6.3.1 魔数与Class文件...
1.4 文档结构 4 第二章编程模型 7 2.1 内核 7 2.2 线程层次 8 2.3 存储器层次 11 2.4 异构编程 11 2.5 计算能力 11 第三章编程接口 15 3.1 用nvcc编译 15 3.1.1 编译流程 16 3.1.1.1 离线编译 16 3.1.1.2 即时编译 ...
堆(Heap-线程共享)-运行时数据区 ...................................................................................... 23 2.2.5. 方法区/永久代(线程共享) ..................................................
Java 虚拟机面试题全面解析,《深入理解Java虚拟机》干货版,自己总结,希望能够帮助大家,免费下载~什么是类加载机制? 虚拟机和物理机的区别是什么? 运行时栈帧结构 Java方法调用 什么是方法调用? Java的方法调用,...