copyOnWriteArrayList & concurrentHashMap &ConcurrentLinkedQueue

1.1 CopyOnWriteArrayList

       ArrayList,LinkList都不是线程安全的,我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。

      CopyOnWriteArrayList 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWriteArrayList容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWriteArrayList容器也是一种读写分离的思想,读和写不同的容器。

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;
    final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;
/**核心方法*/
}

       如上代码所示我们重点看一下内部实际是封装了一个 volatile修饰的数组,volatile Object[] array;那么我们都知道volatile的作用是.当对volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存 ,写操作会导致其他线程中的缓存无效,这样工作内存中对volatile变量的修改就能及时同步到主内存.好那么

private transient volatile Object[] array;

如上数组在 CopyOnWriteArrayList 中已经定义了一个volatile变量了,那么多线程操作的时候只要写了这个数组,不管是什么线程去读都能读到最新的数据.

1 getArray()

final Object[] getArray() {
        return array;
}

2 setArray(Object[] a)

final void setArray(Object[] a) {
        array = a;
}

3 get(Object[] a, int index)

private E get(Object[] a, int index) {
      return (E) a[index];
}

4 get(int index)

public E get(int index) {
    return get(getArray(), index);
}

5 add(E e) 

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        //写时复制
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
  }

6 addAll(Collection<? extends E> c)

public boolean addAll(Collection<? extends E> c) {
    Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ?
        ((CopyOnWriteArrayList<?>)c).getArray() : c.toArray();
    if (cs.length == 0)
        return false;
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        if (len == 0 && (c.getClass() == CopyOnWriteArrayList.class ||
                         c.getClass() == ArrayList.class)) {
            setArray(cs);
        } else {
            Object[] newElements = Arrays.copyOf(elements, len + cs.length);
            //把cs数组从0的位置拷贝到newElements的(len,cs.length)
            System.arraycopy(cs, 0, newElements, len, cs.length);
            setArray(newElements);
        }
        return true;
    } finally {
        lock.unlock();
    }

如上所以 copyOnWriteArrayList 为了保证线程安全使用了 ReentrantLock锁,这个锁的作用就是保证了写的线程安全问题,当我们要对数组做变更的时候大致的步奏如下

1 获取到这把锁.

2 定义一个新数组拷贝内存中的数据到工作内存,然后把原数组拷贝到新数组中

3 对工作内存中的数据做一系列的修改操作

4 把修改好的数组重新填值回旧数组中.

特性:

①、CopyOnWriteArrayList,写数组的拷贝,支持高效率并发且是线程安全的,读操作无锁的ArrayList。所有可变操作都是通过对底层数组进行一次新的复制来实现。

②、CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变Array引用。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差。

③、CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用 ,因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

缺点:

1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc。

  • young gc :年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。
  • 年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC)

2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

1.2 ConcurrentHashMap

      第一种方法,使用Hashtable线程安全类;

      第二种方法,使用Collections.synchronizedMap方法,对方法进行加同步锁;

      第三种方法,使用并发包中的ConcurrentHashMap类

     Hashtable 是一个线程安全的类,Hashtable 几乎所有的添加、删除、查询方法都加了synchronized同步锁!相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差,所以 Hashtable 不推荐使用!

     Collections.synchronizedMap 里面使用对象锁来保证多线程场景下,操作安全,本质也是对 HashMap 进行全表锁!使用Collections.synchronizedMap方法,在竞争激烈的多线程环境下性能依然也非常差,所以不推荐使用!

1.2.1 JDK1.7 中的 ConcurrentHashMap

ConcurrentHashMap 类所采用的是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment, 如下图:

 

      

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        Segment本身就相当于一个HashMap对象。同HashMap一样,Segment包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。 Segment对象,在ConcurrentHashMap集合中有多少个呢?有2的N次方个,共同保存在一个名为segments的数组当中。

      可以说,ConcurrentHashMap 是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。

Get()

public V get(Object key) {
        Segment<K,V> s; 
        HashEntry<K,V>[] tab;
        int h = hash(key);  //1
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&  //2
            (tab = s.table) != null) {
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

Put()

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);       // 计算Hash值
        int j = (hash >>> segmentShift) & segmentMask;      //计算下标j
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);       //若j处有segment就返回,若没有就创建并返回
        return s.put(key, hash, value, false);  //将值put到segment中去
}


 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);        //如果tryLock成功,就返回null,否则。。。
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;        //根据table数组的长度 和 hash值计算index小标
                HashEntry<K,V> first = entryAt(tab, index); //找到table数组在 index处链表的头部
                for (HashEntry<K,V> e = first;;) {      //从first开始遍历链表
                    if (e != null) {                    //若e!=null
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {        //如果key相同
                            oldValue = e.value;                 //获取旧值
                            if (!onlyIfAbsent) {                //若absent=false
                                e.value = value;                //覆盖旧值
                                ++modCount;                     //
                            }
                            break;      //若已经找到,就退出链表遍历
                        }
                        e = e.next;     //若key不相同,继续遍历
                    }
                    else {              //直到e为null
                        if (node != null)   //将元素放到链表头部
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first); //创建新的Entry
                        int c = count + 1;      //count 用来记录元素个数
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)     //如果hashmap元素个数超过threshold,并且table长度小于最大容量
                            rehash(node);       //rehash跟resize的功能差不多,将table的长度变为原来的两倍,重新打包entries,并将给定的node添加到新的table
                        else        //如果还有容量
                            setEntryAt(tab, index, node);   //就在index处添加链表节点
                        ++modCount;     //修改操作数
                        count = c;      //将count+1
                        oldValue = null;    //
                        break;
                    }
                }
            } finally {
                unlock();           //执行完操作后,释放锁
            }
            return oldValue;        //返回oldValue
}

private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset   获取下标k处的offset,
        Segment<K,V> seg;
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {    //如果下标k处没有元素
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            int cap = proto.table.length;   //根据proto 获得 cap参数
            float lf = proto.loadFactor;    //。。。
            int threshold = (int)(cap * lf);    //计算threshold
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck   //如果下标k处仍然没有元素
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);  //创建segment
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {   //若下标k处仍然没有元素,自旋
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))  //若通过CAS更新成功,则退出
                        break;
                }
            }
        }
        return seg;
    }

        从上述源码我们可以看出,Segment 本身是基于 ReentrantLock 实现的加锁和释放锁的操作,这样就能保证多个线程同时访问 ConcurrentHashMap 时,同一时间只有一个线程能操作相应的节点,这样就保证了 ConcurrentHashMap 的线程安全了。 也就是说 ConcurrentHashMap 的线程安全是建立在 Segment 加锁的基础上的,所以我们把它称之为分段锁或片段锁 .

1.2.2 JDK1.8 中的 ConcurrentHashMap

当然,JDK1.7 和 JDK1.8 对 ConcurrentHashMap 的实现有很大的不同!

JDK1.8 对 HashMap 做了改造,当冲突链表长度大于8时,会将链表转变成红黑树结构,上图是 ConcurrentHashMap 的整体结构,参考 JDK1.7!

我们再来看看 JDK1.8 中 ConcurrentHashMap 的整体结构,内容如下:

JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于8时,链表结构转为红黑二叉树)结构。

ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!

说了这么多,我们再一起来看看 ConcurrentHashMap 的源码实现。

JDK1.8 中的 ConcurrentHashMap

虽然 JDK1.7 中的 ConcurrentHashMap 解决了 HashMap 并发的安全性,但是当冲突的链表过长时,在查询遍历的时候依然很慢!

在 JDK1.8 中,HashMap 引入了红黑二叉树设计,当冲突的链表长度大于8时,会将链表转化成红黑二叉树结构,红黑二叉树又被称为平衡二叉树,在查询效率方面,又大大的提高了不少。

JDK1.8 中的ConcurrentHashMap 相比 JDK1.7 中的 ConcurrentHashMap, 它抛弃了原有的 Segment 分段锁实现,采用了 CAS + synchronized 来保证并发的安全性。

JDK1.8 中的 ConcurrentHashMap 对节点Node类中的共享变量,和 JDK1.7 一样,使用volatile关键字,保证多线程操作时,变量的可见性!

类定义

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
   implements ConcurrentMap<K,V>, Serializable {
private static final long serialVersionUID = 7249069246763182397L;
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private static final float LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
}
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    Node(int hash, K key, V val, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }
    public final K getKey()       { return key; }
    public final V getValue()     { return val; }
    public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
    public final String toString(){ return key + "=" + val; }
    public final V setValue(V value) {
        throw new UnsupportedOperationException();
    }

    public final boolean equals(Object o) {
        Object k, v, u; Map.Entry<?,?> e;
        return ((o instanceof Map.Entry) &&
                (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                (v = e.getValue()) != null &&
                (k == key || k.equals(key)) &&
                (v == (u = val) || v.equals(u)));
    }
 
}

核心方法

1 get(Object key) 方法

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;}
         else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
  1. 判断数组是否为空,通过key定位到数组下标是否为空;
  2. 判断node节点第一个元素是不是要找到,如果是直接返回;
  3. 如果是红黑树结构,就从红黑树里面查询;
  4. 如果是链表结构,循环遍历判断。

 find() 方法

   Node<K,V> find(int h, Object k) {
        Node<K,V> e = this;
        if (k != null) {
            do {
                K ek;
                if (e.hash == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
            } while ((e = e.next) != null);
        }
        return null;
    }

put() 方法

public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}
  1. 当进行 put 操作时,流程大概可以分如下几个步骤:
  2. 首先会判断 key、value是否为空,如果为空就抛异常;
  3. 接着会判断容器数组是否为空,如果为空就初始化数组;
  4. 进一步判断,要插入的元素f,在当前数组下标是否第一次插入,如果是就通过 CAS 方式插入;
  5. 在接着判断f.hash == -1是否成立,如果成立,说明当前f是ForwardingNode节点,表示有其它线程正在扩容,则一起进行扩容操作;
  6. 其他的情况,就是把新的Node节点按链表或红黑树的方式插入到合适的位置;
  7. 节点插入完成之后,接着判断链表长度是否超过8,如果超过8个,就将链表转化为红黑树结构;

最后,插入完成之后,进行扩容判断。

1.2.3小结

虽然HashMap 在多线程环境下操作不安全,但是在 java.util.concurrent 包下,java 为我们提供了 ConcurrentHashMap 类,保证在多线程下 HashMap 操作安全!

在JDK1.7 中,ConcurrentHashMap 采用了分段锁策略,将一个 HashMap 切割成 Segment 数组,其中 Segment 可以看成一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操作的时候给 Segment 赋予了一个对象锁,从而保证多线程环境下并发操作安全。

但是 JDK1.7 中,HashMap 容易因为冲突链表过长,造成查询效率低,所以在 JDK1.8 中,HashMap 引入了红黑树特性,当冲突链表长度大于8时,会将链表转化成红黑二叉树结构。

在 JDK1.8 中与此对应的 ConcurrentHashMap 也是采用了与 HashMap 类似的存储结构,但是 JDK1.8 中 ConcurrentHashMap 并没有采用分段锁的策略,而是在元素的节点上采用 CAS + synchronized 操作来保证并发的安全性,源码的实现比 JDK1.7 要复杂的多。

1.3 CopyOnWriteArraySet

Hashset是非线程安全的, CopyOnWriteArraySet是并发下Hashset的替代方案,CopyOnWriteArraySet 是为 “读多写少” 场景设计的并发容器,其特点如下:

  1. 适用于高并发场景
  2. 列表型容器
  3. 没有重复的数据
  4. 利用 “写时复制” 思想,保证读/写同时进行不会冲突
  5. 只有当多个线程同时进行写操作的时候,才需要同步
  6. 内部使用CopyOnWriteArrayList 对象实现功能

CopyOnWriteArraySet 内部引用了一个 CopyOnWriteArrayList 对象,以 “组合” 方式,委托CopyOnWriteArrayList对象实现了所有API功能。

public class CopyOnWriteArraySet<E> extends AbstractSet<E>
        implements java.io.Serializable {
    private static final long serialVersionUID = 5457747651344034263L;
    private final CopyOnWriteArrayList<E> al;
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }
    public CopyOnWriteArraySet(Collection<? extends E> c) {
        if (c.getClass() == CopyOnWriteArraySet.class) {
            @SuppressWarnings("unchecked") CopyOnWriteArraySet<E> cc =
                (CopyOnWriteArraySet<E>)c;
            al = new CopyOnWriteArrayList<E>(cc.al);
        }else {
            al = new CopyOnWriteArrayList<E>();
            al.addAllAbsent(c);
        }
    }
/**省略部分代码*/
}

核心方法

可以看到,所有的方法都是通过调用CopyOnWriteArrayList对象al的方法来实现的. CopyOnWriteArraySet中的数据是不重复的,具有Set的特性。调用的是al的addIfAbsent方法。该方法是当集合中不存在这个数据的时候将该数据添加到集合中,保证了数据不重复。

总结

  1. 由于使用了 “写时复制” 的思想,适用于 “读多写少” 的场景;
  2. 由于在进行写操作的时候会复制原数组,对内存的占用会比较大,不适用于大数据量的场景;
  3. 只能保证最终的数据一致性,不能保证实时的数据一致性——读操作只能从旧数组中读取数据,而此时可能已经复制了一个新数组,并且正在修改新数组的数据。
  4. 迭代是对快照进行的,不会抛出 ConcurrentModificationException,且迭代过程中不支持修改操作。

1.4 ConcurrentLinkedQueue

         在并发编程中我们有时候需要使用线程安全的队列。如果我们要实现一个线程安全的队列有两种实现方式

   一种是使用阻塞算法:例如LinkedBlockingQueue(无界),ArrayBlockintQueue(有界) 使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现.

   一种是使用非阻塞算法:而非阻塞的实现方式则可以使用循环CAS的方式来实现,下面我们一起来研究下Doug Lea是如何使用非阻塞的方式来实现线程安全队列ConcurrentLinkedQueue的。

         ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法来实现,该算法在Michael & Scott算法上进行了一些修改。

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

1.4.1 构造方法

head = tail = new Node<E>(null);

private static class Node<E> {
    volatile E item;
    volatile Node<E> next;
    Node(E item) {
        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);
        }
    }
}

从如上代码可以看出 ConcurrentLinkedQueue是一个单链表的结构.

1.4.2 入队

入队列就是将入队节点添加到队列的尾部。为了方便理解入队时队列的变化,以及head节点和tail节点的变化,每添加一个节点我就做了一个队列的快照图:

上图所示的元素添加过程如下:

添加元素1:队列更新head节点的next节点为元素1节点。又因为tail节点默认情况下等于head节点,所以它们的next节点都指向元素1节点。

添加元素2:队列首先设置元素1节点的next节点为元素2节点,然后更新tail节点指向元素2节点。

添加元素3:设置tail节点的next节点为元素3节点。

添加元素4:设置元素3的next节点为元素4节点,然后将tail节点指向元素4节点。

入队操作主要做两件事情

     第一是: 将入队节点设置成当前队列尾节点的下一个节点。

     第二是: 更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成尾节点,如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点.

      上面的分析让我们从单线程入队的角度来理解入队过程,但是多个线程同时进行入队情况就变得更加复杂,因为可能会出现其他线程插队的情况。如果有一个线程正在入队,那么它必须先获取尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另外一个线程插队了,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新获取尾节点。

public boolean add(E e) {
    return offer(e);
}
 
public boolean offer(E e) {
    // 如果e为null,则直接抛出NullPointerException异常
    checkNotNull(e);
    // 创建入队节点
    final Node<E> newNode = new Node<E>(e);
 
    //循环CAS直到入队成功
    1、根据tail节点定位出尾节点(last node);
    2、将新节点置为尾节点的下一个节点;
    3、casTail更新尾节点

    for (Node<E> t = tail, p = t;;) {
        // p用来表示队列的尾节点,初始情况下等于tail节点
        // q是p的next节点
        Node<E> q = p.next;
        // 判断p是不是尾节点,tail节点不一定是尾节点,判断是不是尾节点的依据是该节点的next是不是null
        // 如果p是尾节点
        if (q == null) {
            // p is last node
            // 设置p节点的下一个节点为新节点,设置成功则casNext返回true;否则返回false,说明有其他线程更新过尾节点
            if (p.casNext(null, newNode)) {
                // Successful CAS is the linearization point
                // for e to become an element of this queue,
                // and for newNode to become "live".
                // 如果p != t,则将入队节点设置成tail节点,更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        // 多线程操作时候,由于poll时候会把旧的head变为自引用,然后将head的next设置为新的head
        // 所以这里需要重新找新的head,因为新的head后面的节点才是激活的节点,寻找尾节点
        else if (p == q)
            // We have fallen off list.  If tail is unchanged, it
            // will also be off-list, in which case we need to
            // jump to head, from which all live nodes are always
            // reachable.  Else the new tail is a better bet.
            p = (t != (t = tail)) ? t : head;
        
        else  // 寻找尾节点
            // Check for tail updates after two hops.
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

从源代码角度来看整个入队过程主要做两件事情:

第一是: 定位出尾节点

第二是: 使用CAS算法能将入队节点设置成尾节点的next节点,如不成功则重试。

1.4.3 出队

出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用。让我们通过每个节点出队的快照来观察下head节点的变化

从上图可知,并不是每次出队时都更新head节点,当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点。只有当head节点里没有元素时,出队操作才会更新head节点。采用这种方式也是为了减少使用CAS更新head节点的消耗,从而提高出队效率。让我们再通过源码来深入分析下出队过程。

public E poll() {
    restartFromHead:
    for (;;) {
        // p节点表示首节点,即需要出队的节点
        for (Node<E> h = head, p = h, q;;) {
            E item = p.item;
 
            // 如果p节点的元素不为null,则通过CAS来设置p节点引用的元素为null,如果成功则返回p节点的元素
            if (item != null && p.casItem(item, null)) {
                // Successful CAS is the linearization point
                // for item to be removed from this queue.
                // 如果p != h,则更新head
                if (p != h) // hop two nodes at a time
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            // 如果头节点的元素为空或头节点发生了变化,这说明头节点已经被另外一个线程修改了。
            // 那么获取p节点的下一个节点,如果p节点的下一节点为null,则表明队列已经空了
            else if ((q = p.next) == null) {
                // 更新头结点
                updateHead(h, p);
                return null;
            }
            // p == q,则使用新的head重新开始
            else if (p == q)
                continue restartFromHead;
            // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点
            else
                p = q;
        }
    }
}

该方法的主要逻辑就是首先获取头节点的元素,然后判断头节点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走,如果不为空,则使用CAS的方式将头节点的引用设置成null,如果CAS成功,则直接返回头节点的元素,如果不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取头节点。

1.4.4 总结

ConcurrentLinkedQueue 的非阻塞算法实现可概括为下面 5 点:

使用 CAS 原子指令来处理对数据的并发访问,这是非阻塞算法得以实现的基础。

head/tail 并非总是指向队列的头 / 尾节点,也就是说允许队列处于不一致状态。 这个特性把入队 / 出队时,原本需要一起原子化执行的两个步骤分离开来,从而缩小了入队 / 出队时需要原子化更新值的范围到唯一变量。这是非阻塞算法得以实现的关键。

由于队列有时会处于不一致状态。为此,ConcurrentLinkedQueue 使用三个不变式来维护非阻塞算法的正确性。

以批处理方式来更新 head/tail,从整体上减少入队 / 出队操作的开销。

为了有利于垃圾收集,队列使用特有的 head 更新机制;为了确保从已删除节点向后遍历,可到达所有的非删除节点,队列使用了特有的向后推进策略。

1.5 ConcurrentLinkedDeque

 Deque是非线程安全的,所以ConcurrentLinkedDeque基于双向链表实现的并发队列,可以分别对头尾进行操作,因此除了先进先出 (FIFO),也可以先进后出(FILO),当然先进后出的话应该叫它栈了。

 ConcurrentLinkedDeque是线程安全的非阻塞队列,内部结构跟LinkedBlockingQueue一样使用双向链表,最大的区别就是LinkedBlockingDeque使用CSA原子操作,没有用lock。使用它的时候一样需要注意,头节点和尾节点不保证一定是头和尾。继承层次如下图所示

 1.5.1 构造方法

public class ConcurrentLinkedDeque<E>
    extends AbstractCollection<E>
    implements Deque<E>, java.io.Serializable {
    private static final long serialVersionUID = 876323262645176354L;
    private transient volatile Node<E> head;
    private transient volatile Node<E> tail;
    private static final Node<Object> PREV_TERMINATOR, NEXT_TERMINATOR;
    @SuppressWarnings("unchecked")
    Node<E> prevTerminator() {
        return (Node<E>) PREV_TERMINATOR;
    }
    @SuppressWarnings("unchecked")
    Node<E> nextTerminator() {
        return (Node<E>) NEXT_TERMINATOR;
    }
    static final class Node<E> {
        volatile Node<E> prev;
        volatile E item;
        volatile Node<E> next;
        Node() {  
        }
        Node(E item) {
            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);
        }
        void lazySetPrev(Node<E> val) {
            UNSAFE.putOrderedObject(this, prevOffset, val);
        }

        boolean casPrev(Node<E> cmp, Node<E> val) {
            return UNSAFE.compareAndSwapObject(this, prevOffset, cmp, val);
        }
        private static final sun.misc.Unsafe UNSAFE;
        private static final long prevOffset;
        private static final long itemOffset;
        private static final long nextOffset;

        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> k = Node.class;
                prevOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("prev"));
                itemOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("item"));
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }
}

如上代码所示可以看出 LinkedBlockingDeque是一个双端队列.

1.5.1 出队

public E pollFirst() {
    for (Node<E> p = first(); p != null; p = succ(p)) {
        E item = p.item;
        if (item != null && p.casItem(item, null)) {
            unlink(p);
            return item;
        }
    }
    return null;
}

public E pollLast() {
    for (Node<E> p = last(); p != null; p = pred(p)) {
        E item = p.item;
        if (item != null && p.casItem(item, null)) {
            unlink(p);
            return item;
        }
    }
    return null;
}

 1.5.2 入队

private void linkFirst(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    restartFromHead:
    for (;;)
        for (Node<E> h = head, p = h, q;;) {
            if ((q = p.prev) != null &&
                (q = (p = q).prev) != null)
                p = (h != (h = head)) ? h : q;
            else if (p.next == p) // PREV_TERMINATOR
                continue restartFromHead;
            else {
                // p is first node
                newNode.lazySetNext(p); // CAS piggyback
                if (p.casPrev(null, newNode)) {
                    if (p != h) // hop two nodes at a time
                        casHead(h, newNode);  // Failure is OK.
                    return;
                }
            }
        }
}

private void linkLast(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    restartFromTail:
    for (;;)
        for (Node<E> t = tail, p = t, q;;) {
            if ((q = p.next) != null &&
                (q = (p = q).next) != null)
                p = (t != (t = tail)) ? t : q;
            else if (p.prev == p) // NEXT_TERMINATOR
                continue restartFromTail;
            else {
                newNode.lazySetPrev(p); // CAS piggyback
                if (p.casNext(null, newNode)) {
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.
                    return;
                }
            }
        }
}

    linkFirst是插入新节点到队列头的主函数,执行流程如下:
首先从 head 节点开始向前循环找到 first 节点(p.prev==null&&p.next!=p);然后通过lazySetNext设置新节点的 next 节点为 first;然后 CAS 修改 first 的 prev 为新节点。注意这里 CAS 指令成功后会判断 first 节点是否已经跳了两个节点,只有在跳了两个节点才会 CAS 更新 head,这也是为了节省 CAS 指令执行开销。linkLast是插入新节点到队列尾,执行流程与linkFirst一致,不多赘述,具体见源码

1.5.3 小结

ConcurrentLinkedDeque使用了自旋+CAS的非阻塞算法来保证线程并发访问时的数据一致性。由于队列本身是一种双链表结构,所以虽然算法看起来很简单,但其实需要考虑各种并发的情况,实现复杂度较高,并且ConcurrentLinkedDeque不具备实时的数据一致性,实际运用中,如果需要一种线程安全的栈结构,可以使用ConcurrentLinkedDeque。

另外,关于ConcurrentLinkedDeque还有以下需要注意的几点:

ConcurrentLinkedDeque的迭代器是弱一致性的,这在并发容器中是比较普遍的现象,主要是指在一个线程在遍历队列结点而另一个线程尝试对某个队列结点进行修改的话不会抛出ConcurrentModificationException,这也就造成在遍历某个尚未被修改的结点时,在next方法返回时可以看到该结点的修改,但在遍历后再对该结点修改时就看不到这种变化。

size方法需要遍历链表,所以在并发情况下,其结果不一定是准确的,只能供参考。

1.6 ConcurrentSkipListMap

1.6.1类结构图

TreeMap是非线程安全的,所以ConcurrentSkipListMap, SkipList 即跳表,跳表是一种空间换时间的数据结构,通过冗余数据,将链表一层一层索引,达到类似二分查找的效果。可以在并发的条件下对键值对实现排序.

对于单链表,即使链表是有序的,如果想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整;而对跳表的插入和删除,只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,需要一个全局锁,来保证整个平衡树的线程安全;而对于跳表,则只需要部分锁即可。这样,在高并发环境下,就可以拥有更好的性能。就查询的性能而言,跳表的时间复杂度是 O(logn), 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。

跳表的本质,是同时维护了多个链表,并且链表是分层的:

最低层的链表,维护了跳表内所有的元素,每上面一层链表,都是下面一层的子集。跳表内所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素18。

查找18 的时候,原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引,查找效率的提升就会非常明显。

从上面很容易看出,跳表是一种利用空间换时间的算法

使用跳表实现 Map,和使用哈希算法实现 Map 的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此,在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择.

1.6.2跳表的数据结构

普通结点——Node,也就是ConcurrentSkipListMap最底层链表中的结点,保存着实际的键值对,如果单独看底层链,其实就是一个按照Key有序排列的单链表:

static final class Node<K,V> {
    final K key;
    volatile Object value;
    volatile Node<K,V> next;

    Node(K key, Object value, Node<K,V> next) {
        this.key = key;
        this.value = value;
        this.next = next;
    }

    Node(Node<K,V> next) {
        this.key = null;
        this.value = this;
        this.next = next;
    }
    boolean casValue(Object cmp, Object val) {
        return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);
    }
    boolean casNext(Node<K,V> cmp, Node<K,V> val) {
        return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
    }
    boolean isMarker() {
        return value == this;
    }
    boolean isBaseHeader() {
        return value == BASE_HEADER;
    }
    boolean appendMarker(Node<K,V> f) {
        return casNext(f, new Node<K,V>(f));
    }
    void helpDelete(Node<K,V> b, Node<K,V> f) {
        if (f == next && this == b.next) {
            if (f == null || f.value != f) // not already marked
                casNext(f, new Node<K,V>(f));
            else
                b.casNext(this, f.next);
        }
    }
    V getValidValue() {
        Object v = value;
        if (v == this || v == BASE_HEADER)
            return null;
        @SuppressWarnings("unchecked") V vv = (V)v;
        return vv;
    }
    AbstractMap.SimpleImmutableEntry<K,V> createSnapshot() {
        Object v = value;
        if (v == null || v == this || v == BASE_HEADER)
            return null;
        @SuppressWarnings("unchecked") V vv = (V)v;
        return new AbstractMap.SimpleImmutableEntry<K,V>(key, vv);
    }
    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;
    private static final long nextOffset;

    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = Node.class;
            valueOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("value"));
            nextOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("next"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

Index结点是除底层链外,其余各层链表中的非头结点(见示意图中的蓝色结点)。每个Index结点包含3个指针: down、 right、 node。down和right指针分别指向下层结点和后继结点,node指针指向其最底部的node结点。

static class Index<K,V> {
    final Node<K,V> node;//指向最底层链表中节点的指针
    final Index<K,V> down;//指向下一层节点的指针
    volatile Index<K,V> right;//指向右侧节点的指针
    Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
        this.node = node;
        this.down = down;
        this.right = right;
    }
    final boolean casRight(Index<K,V> cmp, Index<K,V> val) {
        return UNSAFE.compareAndSwapObject(this, rightOffset, cmp, val);
    }

    final boolean indexesDeletedNode() {
        return node.value == null;
    }

    final boolean link(Index<K,V> succ, Index<K,V> newSucc) {
        Node<K,V> n = node;
        newSucc.right = succ;
        return n.value != null && casRight(succ, newSucc);
    }
    final boolean unlink(Index<K,V> succ) {
        return node.value != null && casRight(succ, succ.right);
    }
    private static final sun.misc.Unsafe UNSAFE;
    private static final long rightOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = Index.class;
            rightOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("right"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

 HeadIndex结点是各层链表的头结点,它是Index类的子类,唯一的区别是增加了一个 level字段,用于表示当前链表的级别,越往上层,level值越大。

//头索引节点的指针
static final class HeadIndex<K,V> extends Index<K,V> {
    final int level;
    HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
        super(node, down, right);
        this.level = level;
    }
}

 如上所示跳表有两个数据结构一个是HeadIndex,一个是实际的node,一个是index,都是使用了cas操作.

核心方法

1.6.3 put() 方法

public V put(K key, V value) {
    if (value == null)
        throw new NullPointerException();
    return doPut(key, value, false);
}
private V doPut(K key, V value, boolean onlyIfAbsent) {
    Node<K,V> z;             // added node
    if (key == null)
        throw new NullPointerException();
    Comparator<? super K> cmp = comparator;
    outer: for (;;) {

//b是小是小于且最接近给定key”的Node结点(或底层链表头结点)
        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
            if (n != null) {
                Object v; int c;
                Node<K,V> f = n.next;
                if (n != b.next)               // inconsistent read
                    break;
                if ((v = n.value) == null) {   // n is deleted
                    n.helpDelete(b, f);
                    break;
                }
                if (b.value == null || v == n) // b is deleted
                    break;
                if ((c = cpr(cmp, key, n.key)) > 0) {
                    b = n;
                    n = f;
                    continue;
                }
                if (c == 0) {
                    if (onlyIfAbsent || n.casValue(v, value)) {
                        @SuppressWarnings("unchecked") V vv = (V)v;
                        return vv;
                    }
                    break; // restart if lost race to replace value
                }
                // else c < 0; fall through
            }

            z = new Node<K,V>(key, value, n);
            if (!b.casNext(n, z))
                break;         // restart if lost race to append to b
            break outer;
        }
    }

//第一个循环,作用就是找到底层链表的插入点,然后插入结点(在查找过程中可能会删除一些已标记的删除结点)。插入完成后,doPut方法并没结束,我们之前说过ConcurrentSkipListMap的分层数是通过一个随机数生成算法来确定,doPut的后半段,就是这个作用:判断是否需要增加层级,如果需要就在各层级中插入对应的Index结点。
    int rnd = ThreadLocalRandom.nextSecondarySeed();
    if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
        int level = 1, max;
        while (((rnd >>>= 1) & 1) != 0)
            ++level;
        Index<K,V> idx = null;
        HeadIndex<K,V> h = head;
        if (level <= (max = h.level)) {
            for (int i = 1; i <= level; ++i)
                idx = new Index<K,V>(z, idx, null);
        }
        else { // try to grow by one level
            level = max + 1; // hold in array and later pick the one to use
            @SuppressWarnings("unchecked")Index<K,V>[] idxs =
                (Index<K,V>[])new Index<?,?>[level+1];
            for (int i = 1; i <= level; ++i)
                idxs[i] = idx = new Index<K,V>(z, idx, null);
            for (;;) {
                h = head;
                int oldLevel = h.level;
                if (level <= oldLevel) // lost race to add level
                    break;
                HeadIndex<K,V> newh = h;
                Node<K,V> oldbase = h.node;
                for (int j = oldLevel+1; j <= level; ++j)
                    newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
                if (casHead(h, newh)) {
                    h = newh;
                    idx = idxs[level = oldLevel];
                    break;
                }
            }
        }
        // find insertion points and splice in
        splice: for (int insertionLevel = level;;) {
            int j = h.level;
            for (Index<K,V> q = h, r = q.right, t = idx;;) {
                if (q == null || t == null)
                    break splice;
                if (r != null) {
                    Node<K,V> n = r.node;
                    // compare before deletion check avoids needing recheck
                    int c = cpr(cmp, key, n.key);
                    if (n.value == null) {
                        if (!q.unlink(r))
                            break;
                        r = q.right;
                        continue;
                    }
                    if (c > 0) {
                        q = r;
                        r = r.right;
                        continue;
                    }
                }

                if (j == insertionLevel) {
                    if (!q.link(r, t))
                        break; // restart
                    if (t.node.value == null) {
                        findNode(key);
                        break splice;
                    }
                    if (--insertionLevel == 0)
                        break splice;
                }

                if (--j >= insertionLevel && j < level)
                    t = t.down;
                q = q.down;
                r = q.right;
            }
        }
    }
    return null;
}

private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
    if (key == null)
        throw new NullPointerException(); // don't postpone errors
    for (;;) {   //从最上层开始往下查找 
        for (Index<K,V> q = head, r = q.right, d;;) {
            if (r != null) { //存在右节点
                Node<K,V> n = r.node;
                K k = n.key;
                if (n.value == null) {
                    if (!q.unlink(r))
                        break;           // restart
                    r = q.right;         // reread r
                    continue;
                }
                if (cpr(cmp, key, k) > 0) { //比较继续往右找
                    q = r;
                    r = r.right;
                    continue;
                }
            }
            if ((d = q.down) == null) //不存在向下的节点 证明已经是第一层了
                return q.node;
            q = d;
            r = d.right;
        }
    }
}

findPredecessor用于查找“小于且最接近给定key”的Node结点,并且这个Node结点必须有上层结点.

上图中,假设要查找的Key为72,则步骤如下:

  1. 从最上方head指向的结点开始,比较①号标红的Index结点的key值,发现3小于72,则继续向右;
  2. 比较②号标红的Index结点的key值,发现62小于72,则继续向右
  3. 由于此时右边是null,则转而向下,一直到⑥号标红结点;
  4. 由于⑥号标红结点的down字段为空(不能再往下了,已经是level1最低层了),则直接返回它的node字段指向的结点,即⑧号结点。

1.6.4 get() 方法

public V get(Object key) {
    return doGet(key);
}
private V doGet(Object key) {
    if (key == null)
        throw new NullPointerException();
    Comparator<? super K> cmp = comparator;
    outer: for (;;) {
        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
            Object v; int c;
            if (n == null)
                break outer;
            Node<K,V> f = n.next;
            if (n != b.next)                // inconsistent read
                break;
            if ((v = n.value) == null) {    // n is deleted
                n.helpDelete(b, f);
                break;
            }
            if (b.value == null || v == n)  // b is deleted
                break;
            if ((c = cpr(cmp, key, n.key)) == 0) {
                @SuppressWarnings("unchecked") V vv = (V)v;
                return vv;
            }
            if (c < 0)
                break outer;
            b = n;
            n = f;
        }
    }
    return null;
}

首先找到“小于且最接近给定key”的Node结点,然后用了三个指针:b -> n -> f, n用于定位最终查找的Key,然后顺着链表一步步向下查,比如查找KEY==45,则最终三个指针的位置如下:

1.7 ConcurrentSkipListSet

  HashSet是非线程安全的,类似 HashSet 和 HashMap 的关系,ConcurrentSkipListSet 里面就是一个 ConcurrentSkipListMap,就不细说了

1.7.1 类结构图

 如上图所示在ConcurrentSkipListSet的底层主要使用ConcurrentNavigableMap<E,Object> 的子类,通过如下代码所示的构造可以看出ConcurrentSkipListSet主要是对ConcurrentSkipListMap的封装,底层也是用了ConcurrentSkipListMap同样是一个跳表结构.只是相比于ConcurrentSkipListMap类 ConcurrentSkipListSet这个类主要是用了key作为集合.

public class ConcurrentSkipListSet<E>
    extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable {

    private static final long serialVersionUID = -2479143111061671589L;

    private final ConcurrentNavigableMap<E,Object> m;
    public ConcurrentSkipListSet() {
        m = new ConcurrentSkipListMap<E,Object>();
    }
    public ConcurrentSkipListSet(Comparator<? super E> comparator) {
        m = new ConcurrentSkipListMap<E,Object>(comparator);
    }
    public ConcurrentSkipListSet(Collection<? extends E> c) {
        m = new ConcurrentSkipListMap<E,Object>();
        addAll(c);
    }

    public ConcurrentSkipListSet(SortedSet<E> s) {
        m = new ConcurrentSkipListMap<E,Object>(s.comparator());
        addAll(s);
    }
    ConcurrentSkipListSet(ConcurrentNavigableMap<E,Object> m) {
        this.m = m;
    }
    public ConcurrentSkipListSet<E> clone() {
        try {
            @SuppressWarnings("unchecked")
            ConcurrentSkipListSet<E> clone =
                (ConcurrentSkipListSet<E>) super.clone();
            clone.setMap(new ConcurrentSkipListMap<E,Object>(m));
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    }
}

1.7.2 add 方法

public boolean add(E e) {
    return m.putIfAbsent(e, Boolean.TRUE) == null;
}

1.7.3 总结

  • 基于ConcurrentSkipListMap实现,同样是跳跃表的数据结构,保存对象是Map中的key。
  • 值不会重复。如果跳跃表中有同样的值,则不会插入新对象。
  • 遍历也是有序的。
  • putAll()、size()、遍历等方法是弱一致性的,不保证原子性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
├─第一阶段 │      源码+ppt.rar │      高并发编程第一阶段01讲、课程大纲及主要内容介绍.wmv │      高并发编程第一阶段02讲、简单介绍什么是线程.wmv │      高并发编程第一阶段03讲、创建并启动线程.mp4 │      高并发编程第一阶段04讲、线程生命周期以及start方法源码剖析.mp4 │      高并发编程第一阶段05讲、采用多线程方式模拟银行排队叫号.mp4 │      高并发编程第一阶段06讲、用Runnable接口将线程的逻辑执行单元从控制中抽取出来.mp4 │      高并发编程第一阶段07讲、策略模式在Thread和Runnable中的应用分析.mp4 │      高并发编程第一阶段08讲、构造Thread对象你也许不知道的几件事.mp4 │      高并发编程第一阶段09讲、多线程与JVM内存结构的关系,虚拟机栈实验.mp4 │      高并发编程第一阶段10讲、Thread构造函数StackSize详细讲解.mp4 │      高并发编程第一阶段11讲、Thread构造函数StackSize详细讲解-续.mp4 │      高并发编程第一阶段12讲、Daemon线程的创建以及使用场景分析.mp4 │      高并发编程第一阶段13讲、线程ID,优先级讲解.mp4 │      高并发编程第一阶段14讲、Thread的join方法详细介绍,结合一个典型案例.mp4 │      高并发编程第一阶段15讲、Thread中断Interrupt方法详细讲解.mp4 │      高并发编程第一阶段16讲、采用优雅的方式结束线程生命周期.mp4 │      高并发编程第一阶段17讲、Thread API综合实战,编写ThreadService实现暴力结束线程的综合实战.mp4 │      高并发编程第一阶段18讲、数据同步的引入与Synchronized的简单介绍.mp4 │      高并发编程第一阶段19讲、结合jconsole,jstack以及汇编指令认识synchronized关键字.mp4 │      高并发编程第一阶段20讲、同步代码块以及同步方法之间的区别和关系.mp4 │      高并发编程第一阶段21讲、通过实验分析This锁的存在.mp4 │      高并发编程第一阶段22讲、通过实验分析Class锁的存在.mp4 │      高并发编程第一阶段23讲、多线程死锁分析,案例介绍.mp4 │      高并发编程第一阶段24讲、线程间通信快速入门,使用wait和notify进行线程间的数据通信.mp4 │      高并发编程第一阶段25讲、多Produce多Consume之间的通讯导致出现程序假死的原因分析.mp4 │      高并发编程第一阶段26讲、多线程下的生产者消费者模型,以及详细介绍notifyAll方法.mp4 │      高并发编程第一阶段27讲、wait和sleep的本质区别是什么,深入分析(面试常见问题).mp4 │      高并发编程第一阶段28讲、线程生产者消费者的综合实战结合Java8语法.mp4 │      高并发编程第一阶段29讲、如何实现一个自己的显式锁Lock精讲上.mp4 │      高并发编程第一阶段30讲、如何实现一个自己的显式锁Lock精讲下(让锁具备超时功能).mp4 │      高并发编程第一阶段31讲、如何给你的应用程序注入钩子程序,Linux下演示.mp4 │      高并发编程第一阶段32讲、如何捕获线程运行期间的异常.mp4 │      高并发编程第一阶段33讲、ThreadGroup API介绍之一.mp4 │      高并发编程第一阶段34讲、ThreadGroup API介绍之二.mp4 │      高并发编程第一阶段35讲、线程池原理与自定义线程池.mp4 │      高并发编程第一阶段36讲、自定义个简单的线程池并且测试.mp4 │      高并发编程第一阶段37讲、给线程池增加拒绝策略以及停止方法.mp4 │      高并发编程第一阶段38讲、给线程池增加自动扩充线程数量,以及闲时自动回收的功能.mp4 │      高并发编程第一阶段39讲、课程结束,内容回顾,下季内容预告.mp4 │ ├─第二阶段 │       Java并发编程.png │       ppt+源码.rar │       高并发编程第二阶段01讲、课程大纲及主要内容介绍.wmv │       高并发编程第二阶段02讲、介绍四种Singleton方式的优缺点在多线程情况下.wmv │       高并发编程第二阶段03讲、介绍三种高效优雅的Singleton实现方式.wmv │       高并发编程第二阶段04讲、多线程的休息室WaitSet详细介绍与知识点总结.mp4 │       高并发编程第二阶段05讲、一个解释volatile关键字作用最好的例子.mp4 │       高并发编程第二阶段06讲、Java内存模型以及CPU缓存不一致问题的引入.mp4 │       高并发编程第二阶段07讲、CPU以及CPU缓存的结构,解决高速缓存一致性问题的两种方案介绍.mp4 │       高并发编程第二阶段08讲、并发编程的三个重要概念,原子性,可见性,有序性.mp4 │       高并发编程第二阶段09讲、指令重排序,happens-before规则精讲.mp4 │       高并发编程第二阶段10讲、volatile关键字深入详解.mp4 │       高并发编程第二阶段11讲、volatile关键字总结.mp4 │       高并发编程第二阶段12讲、观察者设计模式介绍.mp4 │       高并发编程第二阶段13讲、使用观察者设计模式观察线程的生命周期.mp4 │       高并发编程第二阶段14讲、单线程执行设计模式,有一个门,始终只能一个人通过-上.mp4 │       高并发编程第二阶段15讲、单线程执行设计模式,有一个门,始终只能一个人通过-下.mp4 │       高并发编程第二阶段16讲、多线程读写锁分离设计模式讲解-上.mp4 │       高并发编程第二阶段17讲、多线程读写锁分离设计模式讲解-中.mp4 │       高并发编程第二阶段18讲、多线程读写锁分离设计模式讲解-下.mp4 │       高并发编程第二阶段19讲、多线程不可变对象设计模式Immutable-上.mp4 │       高并发编程第二阶段20讲、多线程不可变对象设计模式Immutable-下.mp4 │       高并发编程第二阶段21讲、多线程Future设计模式详细介绍-上.mp4 │       高并发编程第二阶段22讲、多线程Future设计模式详细介绍-下.mp4 │       高并发编程第二阶段23讲、第二阶段课程答疑学员问题.mp4 │       高并发编程第二阶段24讲、Guarded Suspension设计模式-上.mp4 │       高并发编程第二阶段25讲、Guarded Suspension设计模式-下.mp4 │       高并发编程第二阶段26讲、ThreadLocal使用详解,深入原理介绍.mp4 │       高并发编程第二阶段27讲、多线程运行上下文设计模式介绍.mp4 │       高并发编程第二阶段28讲、使用ThreadLocal重新实现一个上下文设计模式.mp4 │       高并发编程第二阶段29讲、多线程Balking设计模式-上.mp4 │       高并发编程第二阶段30讲、多线程Balking设计模式-下.mp4 │       高并发编程第二阶段31讲、多线程Producer and Consumer设计模式.mp4 │       高并发编程第二阶段32讲、多线程Count Down设计模式.mp4 │       高并发编程第二阶段33讲、多线程Thread-Per-Message设计模式.mp4 │       高并发编程第二阶段34讲、多线程Two Phase Termination设计模式-上.mp4 │       高并发编程第二阶段35讲、多线程Two Phase Termination设计模式-下.mp4 │       高并发编程第二阶段36讲、多线程Worker-Thread设计模式-上.mp4 │       高并发编程第二阶段37讲、多线程Worker-Thread设计模式-上.mp4 │       高并发编程第二阶段38讲、多线程Active Objects设计模式(接受异步消息的主动对象)-上.mp4 │       高并发编程第二阶段39讲、多线程Active Objects设计模式(接受异步消息的主动对象)-中.mp4 │       高并发编程第二阶段40讲、多线程Active Objects设计模式(接受异步消息的主动对象)-下.mp4 │       高并发编程第二阶段41讲、多线程设计模式内容回顾与总结.mp4 │       高并发编程第二阶段42讲、ClassLoader课程大纲介绍.mp4 │       高并发编程第二阶段43讲、类加载的过程以及类主动使用的六种情况详细介绍.mp4 │       高并发编程第二阶段44讲、被动引用和类加载过程的练习巩固训练题.mp4 │       高并发编程第二阶段45讲、ClassLoader加载阶段发生的故事.mp4 │       高并发编程第二阶段46讲、ClassLoader链接阶段(验证,准备,解析)过程详细介绍.mp4 │       高并发编程第二阶段47讲、ClassLoader初始化阶段详细介绍clinit.mp4 │       高并发编程第二阶段48讲、JVM内置三大类加载器的详细介绍.mp4 │       高并发编程第二阶段49讲、自定义类加载器ClassLoader顺便问候了一下世界.mp4 │       高并发编程第二阶段50讲、ClassLoader父委托机制详细介绍.mp4 │       高并发编程第二阶段51讲、加密解密类加载实战演示.mp4 │       高并发编程第二阶段52讲、加密解密类加载实战演示-续.mp4 │       高并发编程第二阶段53讲、ClassLoader打破双父亲委托机制,重写loadClass实战练习.mp4 │       高并发编程第二阶段54讲、ClassLoader命名空间,运行时包,类卸载详细介绍.mp4 │       高并发编程第二阶段55讲、线程上下文类加载器以及数据库驱动案例分析.mp4 │       └─第三阶段        Java并发编程.png        Java高并发第三阶段(JUC).png        高并发编程第三阶段01讲 AtomicInteger多线程下测试讲解.mkv        高并发编程第三阶段02讲 AtomicInteger API详解,以及CAS算法详细介绍.mkv        高并发编程第三阶段03讲 利用CAS构造一个TryLock自定义显式锁.mp4        高并发编程第三阶段04讲 利用CAS构造一个TryLock自定义显式锁-增强并发情况下.mp4        高并发编程第三阶段05讲 AtomicBoolean源码分析.mp4        高并发编程第三阶段06讲 AtomicLong源码分析.mp4        高并发编程第三阶段07讲 AtomicReference详解,CAS算法带来的ABA问题详解.mp4        高并发编程第三阶段08讲 AtomicStampReference详解,解决CAS带来的ABA问题.mp4        高并发编程第三阶段09讲 AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray讲解.mp4        高并发编程第三阶段10讲 AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater讲解.mp4        高并发编程第三阶段11讲 AtomicXXXFieldUpdater源码分析及使用场景分析.mp4        高并发编程第三阶段12讲 sun.misc.Unsafe介绍以及几种Counter方案性能对比.mp4        高并发编程第三阶段13讲 一个JNI程序的编写,通过Java去调用C,C++程序.mp4        高并发编程第三阶段14讲 Unsafe中的方法使用,一半是天使,一半是魔鬼的Unsafe.mp4        高并发编程第三阶段15讲 Unsafe背后的汇编指令,牛逼男人背后的女人_.mp4        高并发编程第三阶段16讲 CountDownLatch经典案例讲解-上_.mp4        高并发编程第三阶段17讲 CountDownLatch经典案例讲解及API精讲-中_.mp4        高并发编程第三阶段18讲 CountDownLatch经典案例讲解如何给离散平行任务增加逻辑层次关系-下_.mp4        高并发编程第三阶段19讲 CyclicBarrier工具的使用场景介绍_.mp4        高并发编程第三阶段20讲 CyclicBarrier vs CountDownLatch_.mp4        高并发编程第三阶段21讲 Exchanger工具的使用以及常见问题分析-上_.mp4        高并发编程第三阶段22讲 Exchanger工具的使用以及常见问题分析-下_.mp4        高并发编程第三阶段23讲 Semaphore工具的介绍以及借助于Semaphore构造一个Lock_.mp4        高并发编程第三阶段24讲 Semaphore工具API详细介绍-上_.mp4        高并发编程第三阶段25讲 Semaphore工具API详细介绍-下_.mp4        高并发编程第三阶段26讲 Lock&ReentrantLock详细讲解_.mp4        高并发编程第三阶段27讲 ReadWriteLock&ReentrantReadWriteLock详细讲解_.mp4        高并发编程第三阶段28讲 Condition初步使用,提出几个疑问_.mp4        高并发编程第三阶段29讲 关于Condition疑问的几个小实验,对比Wait&Notify_.mp4        高并发编程第三阶段30讲 使用Condition实现一个多线程下的Producer-Consumer_.mp4        高并发编程第三阶段31讲 JDK8-StampedLock详细介绍-上_.mp4        高并发编程第三阶段32讲 JDK8-StampedLock详细介绍-下.mp4        高并发编程第三阶段33讲 ForkJoin框架之RecursiveTask_.mp4        高并发编程第三阶段34讲 ForkJoin框架之RecursiveAction_.mp4        高并发编程第三阶段35讲 Phaser工具的实战案例使用第一部分_.mp4        高并发编程第三阶段36讲 Phaser工具的实战案例使用第二部分_.mp4        高并发编程第三阶段37讲 Phaser工具的实战案例使用第三部分_.mp4        高并发编程第三阶段38讲 Executor&ExecutorService讲解_.mp4        高并发编程第三阶段39讲 ThreadPoolExecutor七大构造参数详细讲解_.mp4        高并发编程第三阶段40讲 ThreadPoolExecutor关闭(很重要)精讲_.mp4        高并发编程第三阶段41讲 newCache&newFixed&single ExecutorService详解_.mp4        高并发编程第三阶段42讲 newWorkStealingPool ExecutorService详解_.mp4        高并发编程第三阶段43讲 Scheduler的前奏Timer&Linux Crontab & quartz比较_.mp4        高并发编程第三阶段44讲 ExecutorService API详细讲解-上_.mp4        高并发编程第三阶段45讲 ExecutorService 四大内置拒绝策略深入探究_.mp4        高并发编程第三阶段46讲 ExecutorService API详细讲解-中_.mp4        高并发编程第三阶段47讲 ExecutorService API详细讲解-下_.mp4        高并发编程第三阶段48讲 Future&Callable详细讲解-上_.mp4        高并发编程第三阶段49讲 Future&Callable详细讲解-下_.mp4        高并发编程第三阶段50讲 CompletionService详细介绍_.mp4        高并发编程第三阶段51讲 ScheduledExecutorService详细讲解-上_.mp4        高并发编程第三阶段52讲 ScheduledExecutorService详细讲解-下_.mp4        高并发编程第三阶段53讲 知识回顾与串联_.mp4        高并发编程第三阶段54讲 课程问题答疑,ExecutorService中的陷阱_.mp4        高并发编程第三阶段55讲 CompletableFuture的使用精讲(体验)-1_.mp4        高并发编程第三阶段56讲 CompletableFuture的使用精讲(构建)-2_.mp4        高并发编程第三阶段57讲 CompletableFuture的使用精讲(熟练)-3_.mp4        高并发编程第三阶段58讲 CompletableFuture的使用精讲(深入)-4_.mp4        高并发编程第三阶段59讲 CompletableFuture的使用精讲(掌握)-5_.mp4        高并发编程第三阶段60讲 LinkedList和有序LinkedList的实现_.mp4        高并发编程第三阶段61讲 跳表数据结构的Java实现-1_.mp4        高并发编程第三阶段62讲 跳表数据结构的Java实现-2_.mp4        高并发编程第三阶段63讲 跳表数据结构的Java实现(解决Bug)-3_.mp4        高并发编程第三阶段64讲 ArrayBlockingList详细讲解_.mp4        高并发编程第三阶段65讲 PriorityBlockingQueue详细讲解_.mp4        高并发编程第三阶段66讲 LinkedBlockingQueue详细讲解_.mp4        高并发编程第三阶段67讲 SynchronousQueue详细讲解_.mp4        高并发编程第三阶段68讲 DelayQueue详细讲解_.mp4        高并发编程第三阶段69讲 LinkedBlockingDeque详细讲解_.mp4        高并发编程第三阶段70讲 LinkedTransferQueue详细讲解_.mp4        高并发编程第三阶段71讲 七大BlockingQueue的特点总结,可以不用详细看_.mp4        高并发编程第三阶段72讲 ConcurrentHashMap性能测试以及JDK1.7原理讲解_.mp4        高并发编程第三阶段73讲 ConcurrentHashMap性能测试以及JDK1.8原理讲解_.mp4        高并发编程第三阶段74讲 ConcurrentSkipListMap详细讲解_.mp4        高并发编程第三阶段75讲 ConcurrentSkipListMap vs ConcurrentHashMap_.mp4        高并发编程第三阶段76讲 ConcurrentLinkedQueue&ConcurrentLinkedDeque_.mp4        高并发编程第三阶段77讲 CopyOnWriteArrayList&CopyOnWriteArraySet源码分析_.mp4        高并发编程第三阶段78讲 ConcurrentLinkedList vs CopyOnWriteArrayList vs SynchronizedList性能对比_.mp4        高并发编程第三阶段79讲 实现一个高并发的无锁队列(Lock-Free).mp4        高并发编程第三阶段80讲 总结与回顾,闲聊与感谢.mp4

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值