前面我们分析了ArrayList的并发替代品CopyOnWriteArrayList。它使用了写时复制的策略来提高并发问题。
今天我们研究下LinkedList的并发替代品ConcurrentLinkedQueue. 并发链表。
要实现并发安全,一般2种策略。一种是加锁,一种是cas。那么我们看下ConcurrentLinkedQueue用的哪种策略。
我们先看下它的类结构。
该类内部定义了一个Node类。拥有item(存的元素),next(下一节点)。其中我们看到一个Unsafe属性。
Unsafe提供了很多方法直接对内存进行读和写操作。其中大部分方法很底层,对应到了硬件指令。效率比较高。它提供一个Unsafe.getUnsafe()方法来获取unsafe实例。
但只能在JDK中的源码中使用,因为JDK之外的代码被认为是不被信任的,因此不能通过这种方式使用Unsafe。
但可以通过反射来使用它。
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
复制代码
虽然如此,但我们还是不建议在应用代码里使用Unsafe的。因为对内存的操作是非常危险的。Unsafe中有以下常用方法 objectFieldOffset:给出指定字段的偏移量,这个偏移量是不变的,同一个类中不同的字段不会存在相同的偏移量; putObject:存储一个值到指定变量; compareAndSwapObject:如果当前值是期望的值,则原子的更新该值到新值; putOrderedObject:存储值到指定字段,但不提供可见性,如果需要具备可见性,则需要指定字段为volatile。
其中,add方法将指定元素插入队列的尾部。poll获取并移除队列的头节点。如果队列为null,则返回null。
add方法源码分析
public boolean add(E e) {
return offer(e);
}
复制代码
public boolean offer(E e) {
//检测是否为null,若为null,抛npe
checkNotNull(e);
//用cas创建一个对象
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
if (p.casNext(null, newNode)) {
if (p != t)
casTail(t, newNode);
return true;
}
}
else if (p == q)
p = (t != (t = tail)) ? t : head;
else
p = (p != t && t != (t = tail)) ? t : q;
}
}
复制代码
下面是jdk1.6版本
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
Node<E> n = new Node<E>(e);
retry:
for (;;) {
//创建一个tail节点的引用
Node<E> t = tail;
//p表示尾节点,默认等于tail节点
Node<E> p = t;
for (int hops = 0; ; hops++) {
//1.获取p的后继节点。(如果p的next指向自身,返回head节点)
Node<E> next = succ(p);
2.如果next不为null ,说明不是尾节点。更新p为next节点
if (next != null) {
if (hops > HOPS && t != tail)
continue retry;
p = next; 4.如果自旋字数小于HOPS或者t是尾节点,将p指向next。
//5.如果next为null,尝试将p的next节点设置为n
} else if (p.casNext(null, n)) {
//6.如果tali节点大于等于1个next节点,将入队节点设为tail节点
if (hops >= HOPS)
casTail(t, n); // 失败了没关系。说明别的线程已经更新
return true; // 7.添加成功。
} else {
//8.说明5中设置fail,p有next节点。那么获取next节点
p = succ(p); //
}
}
}
}
final Node<E> succ(Node<E> p) {
Node<E> next = p.getNext();
//如果p节点的next节点指向自身,那么返回head节点;否则返回p的next节点。
return (p == next) ? head : next;
}
/**
* 允许头尾节点的指针滞后,所以当头尾节点离"实际位置"的距离
* (按节点)小于HOPS时,不会去更新头尾指针。这里是假设volatile写代价比volatile读高。
*/
private static final int HOPS = 1;
复制代码
从上面代码看出,整个入队过程干2件事:一件是定位尾节点,一件是设置成尾节点的next节点。
第一步定位尾节点:因为tail节点不一定是尾节点,有可能是tail的next节点。所以代码开始就判断是否有尾节点。
获取tail节点的next节点需要注意的是p节点等于p的next节点的情况,只有一种可能就是p节点和p的next节点都等于空,表示这个队列刚初始化,正准备添加第一次节点,所以需要返回head节点
第二步设置入队节点为尾节点。p.casNext(null,n)方法用于将入队节点设置为当前队列尾节点的next节点,p如果是null表示p是当前队列的尾节点,如果不为null表示有其他线程更新了尾节点,则需要重新获取当前队列的尾节点。
为什么不让tail一直指向尾节点?
这样做行不行?
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
Node<e> n = new Node<>(e);
for (;;) {
Node<> t = tail;
if (t.casNext(null, n) && casTail(t, n)) {
return true;
}
}
}
复制代码
这样做,逻辑不是非常清楚么。
是这么做有个缺点就是每次都需要使用循环CAS更新tail节点。如果能减少CAS更新tail节点的次数,就能提高入队的效率。
再看下poll方法
public E poll() {
Node</e><e> h = head;
// p表示头节点,需要出队的节点
Node</e><e> p = h;
for (int hops = 0;; hops++) {
// 获取p节点的元素
E item = p.getItem();
//如果p节点的元素不为空,使用CAS设置p节点引用的元素为null,如果成功则返回p节点的元素。
if (item != null && p.casItem(item, null)) {
if (hops >= HOPS) {
//将p节点下一个节点设置成head节点
Node</e><e> q = p.getNext();
updateHead(h, (q != null) ? q : p);
}
return item;
}
// 如果头节点的元素为空或头节点发生了变化,这说明头节点已经被另外一个线程修改了。那么获取p节点的下一个节点
Node</e><e> next = succ(p);
// 如果p的下一个节点也为空,说明这个队列已经空了
if (next == null) {
// 更新头节点。
updateHead(h, p);
break;
}
// 如果下一个元素不为空,则将头节点的下一个节点设置成头节点
p = next;
}
return null;
}
复制代码