Android并发编程 原子类与并发容器 (2)

以上几个类提供的方法几乎一样,所以本节仅以AtomicIntegerArray为例进行讲解

public class AtomicIntegerArrayTest { static int[] value = new int[]{2, 3}; static AtomicIntegerArray ai = new AtomicIntegerArray(value); public static void main(String[] args) { ai.getAndSet(0, 4); System.out.println(ai.get(0)); System.out.println(value[0]); } }

以下是输出的结果。
4
2

更快的原子操作基本类LongAdder DouleAdder

JDK1.8为我们提供了更快的原子操作基本类LongAdder DouleAdder,

LongAdder的doc部分说明如下

This class is usually preferable to {@link AtomicLong} when
multiple threads update a common sum that is used for purposes such
as collecting statistics, not for fine-grained synchronization
control. Under low update contention, the two classes have similar
characteristics. But under high contention, expected throughput of
this class is significantly higher, at the expense of higher space
consumption

上面那段话翻译过来就是

当我们的场景是为了统计计数,而不是为了更细粒度的同步控制时,并且是在多线程更新的场景时,LongAdder类比AtomicLong更好用。 在小并发的环境下,论更新的效率,两者都差不多。但是高并发的场景下,LongAdder有着明显更高的吞吐量,但是有着更高的空间复杂度。

从LongAdder的doc文档上我们就可以知道LongAdder更适用于统计求和场景,而不是细粒度的同步控制。

并发容器

我们在开发中遇到比较简单的并发操作像自增自减,求和之类的问题,上一节原子类已经能比较好的解决了,但对于本篇文章来说只是开胃小菜,下面正菜来喽

ConcurrentLinkedQueue(并发的队列)

ConcurrentLinkedQueue是一个基于链表的无界线程安全队列,它采用先进先出的规则对节点进行排序,我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。

我们先来看一下ConcurrentLinkedQueue的类图

Android并发编程 原子类与并发容器

ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点(next)的引用组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tail节点等于head节点

以下源码来自JDK1.8

public ConcurrentLinkedQueue() { //默认情况下head节点存储的元素为空,tail节点等于head节点,哨兵节点 head = tail = new Node<E>(null); } private static class Node<E> { volatile E item; volatile Node<E> next; Node(E item) { //设置item值 //这种的设置方式类似于C++的指针,直接操作内存地址, //例如此行代码,就是以CAS的方式把值(item)赋值给当前对象即Node地址偏移itemOffset后的地址 //下面出现的casItem以及casNext也是同理 UNSAFE.putObject(this, itemOffset, item); } boolean casItem(E cmp, E val) { return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); } void lazySetNext(Node<E> val) { UNSAFE.putOrderedObject(this, nextOffset, val); } boolean casNext(Node<E> cmp, Node<E> val) { return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); } private static final sun.misc.Unsafe UNSAFE; private static final long itemOffset; private static final long nextOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> k = Node.class; itemOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("item")); nextOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("next")); } catch (Exception e) { throw new Error(e); } } }

看完了初始化,我们来看一下这个线程安全队列的进队和出队方法

offer(E e) 以及 poll() 方法 offer(E e) public boolean offer(E e) { checkNotNull(e);// 检查,为空直接异常 // 创建新节点,并将e 作为节点的item final Node<E> newNode = new Node<E>(e); // 这里操作比较多,将尾节点tail 赋给变量 t,p for (Node<E> t = tail, p = t;;) { // 并获取q 也就是 tail 的下一个节点 Node<E> q = p.next; // 如果下一个节点是null,说明tail 是处于尾节点上 if (q == null) { // 然后用cas 将下一个节点设置成为新节点 // 这里用cas 操作,如果多线程的情况,总会有一个先执行成功,失败的线程继续执行循环。 // <1> if (p.casNext(null, newNode)) { // 如果p.casNext有个线程成功了,p=newNode // 比较 t (tail) 是不是 最后一个节点 if (p != t) // 如果不等,就利用cas将,尾节点移到最后 // 如果失败了,那么说明有其他线程已经把tail移动过,也是OK的 casTail(t, newNode); return true; } // 如果<1>失败了,说明肯定有个线程成功了, // 这时候失败的线程,又会执行for 循环,再次设值,直到成功。 } else if (p == q) // 有可能刚好插入一个,然后P 就被删除了,那么 p==q // 这时候在头结点需要从新定位。 p = (t != (t = tail)) ? t : head; else // 这里是为了当P不是尾节点的时候,将P 移到尾节点,方便下一次插入 // 也就是一直保持向前推进 p = (p != t && t != (t = tail)) ? t : q; } } private boolean casTail(Node<E> cmp, Node<E> val) { return UNSAFE.compareAndSwapObject(this, tailOffset, cmp, val); }

从上述代码可知入队列过程可归纳为3步

定位出尾节点;

使用CAS算法将入队节点设置成尾节点的next节点,如不成功则重试

更新尾节点

1.定位尾节点

tail节点并不总是尾节点,所以每次入队都必须先通过tail节点来找到尾节点。
尾节点可能是tail节点,也可能是tail节点的next节点。

2.设置入队节点为尾节点

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpdjfz.html