Java并发容器总结
-
ConcurrentHashMap: 线程安全的 HashMap
-
CopyOnWriteArrayList: 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector.
-
ConcurrentLinkedQueue: 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
-
BlockingQueue: 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
-
ConcurrentSkipListMap: 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。
1 | public class CopyOnWriteArrayList<E> exends Object implements List<E>, RandomAccess, Cloneable, Serializable |
在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 List 的内部数据,毕竟读取操作是安全的。这和ReentrantReadWriteLock 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了CopyOnWriteArrayList类比相比于在读写锁的思想又更进一步。为了将读的性能发挥到极致,CopyOnWriteArrayList针对读操作是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。
3.2、实现细节
1 2 3 4 5 6 7 8 9 10 11 12 13 | /** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array; public E get( int index) { return get(getArray(), index); } @SuppressWarnings ( "unchecked" ) private E get(Object[] a, int index) { return (E) a[index]; } final Object[] getArray() { return array; } |
CopyOnWriteArrayList 写入操作 add() 方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。添加的逻辑很简单,先将原容器copy一份,然后在新副本上执行写操作,之后再切换引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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(); //释放锁 } } |
移除remove操作:删除操作同添加,将除要删除元素之外的其他元素拷贝到新副本中,然后切换引用,将原容器引用指向新副本。同属写操作,需要加锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public E remove( int index) { //加锁 final ReentrantLock lock = this .lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; E oldValue = get(elements, index); int numMoved = len - index - 1 ; if (numMoved == 0 ) //如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用 setArray(Arrays.copyOf(elements, len - 1 )); else { //否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用 Object[] newElements = new Object[len - 1 ]; System.arraycopy(elements, 0 , newElements, 0 , index); System.arraycopy(elements, index + 1 , newElements, index, numMoved); setArray(newElements); } return oldValue; } finally { //解锁 lock.unlock(); } } |
JDK中并没有提供CopyOnWriteMap,我们可以参考CopyOnWriteArrayList来实现一个,基本代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class CopyOnWriteMap<K, V> { private volatile Map<K, V> internalMap; public CopyOnWriteMap() { internalMap = new HashMap<K, V>(); } public V put(K key, V value) { synchronized ( this ) { Map<K, V> newMap = new HashMap<K, V>(internalMap); V val = newMap.put(key, value); internalMap = newMap; return val; } } public V get(Object key) { return internalMap.get(key); } public void putAll(Map<? extends K, ? extends V> newData) { synchronized ( this ) { Map<K, V> newMap = new HashMap<K, V>(internalMap); newMap.putAll(newData); internalMap = newMap; } } } |
3.4 CopyOnWriteArrayList应用场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class BlackListServiceImpl { private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>( 1000 ); public static boolean isBlackList(String id) { return blackListMap.get(id) == null ? false : true ; } public static void addBlackList(String id) { blackListMap.put(id, Boolean.TRUE); } /** * 批量添加黑名单 * * @param ids */ public static void addBlackList(Map<String,Boolean> ids) { blackListMap.putAll(ids); } } |
代码很简单,但是使用CopyOnWriteMap需要注意两件事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | // 用于存放元素的数组 final Object[] items; // 下一次读取操作的位置 int takeIndex; // 下一次写入操作的位置 int putIndex; // 队列中的元素数量 int count; // 以下几个就是控制并发用的同步器 final ReentrantLock lock; //全局锁 private final Condition notEmpty; private final Condition notFull; //把object加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程阻塞。直到BlockingQueue里面有空间再继续.相对的offer方法则不阻塞 public void put(E e) throws InterruptedException{ checkNotNull(e); // 非空判断 final ReentrantLock lock = this .lock; lock.lockInterruptibly(); // 获取锁 try { while (count == items.length) { // 一直阻塞,知道队列非满时,被唤醒 notFull.await(); } enqueue(e); // 进队 } finally { lock.unlock(); } } //表示如果可能的话,将object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false,可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。 public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException{ checkNotNull(e); long nanos = unit.toNanos(timeout); final ReentrantLock lock = this .lock; lock.lockInterruptibly(); try { while (count == items.length){ // 阻塞,知道队列不满 // 或者超时时间已过,返回false if (nanos <= 0 ) return false ; nanos = notFull.awaitNanos(nanos); } enqueue(e); return true ; } finally { lock.unlock(); } } //实现的方法,如果当前队列为空,返回null public E poll(){ final ReentrantLock lock = this .lock; lock.lock(); try { return (count == 0 ) ? null : dequeue(); } finally { lock.unlock(); } } //实现的方法,如果当前队列为空,一直阻塞 public E take() throws InterruptedException{ final ReentrantLock lock = this .lock; lock.lockInterruptibly(); try { while (count == 0 ) notEmpty.await(); //队列为空,阻塞方法 return dequeue(); } finally { lock.unlock(); } } //带有超时时间的取元素方法,否则返回Null public E poll( long timeout, TimeUnit unit) throws InterruptedException{ long nanos = unit.toNanos(timeout); final ReentrantLock lock = this .lock; lock.lockInterruptibly(); try { while (count == 0 ){ if (nanos <= 0 ) return null ; nanos = notEmpty.awaitNanos(nanos); //超时等待 } return dequeue(); //取得元素 } finally { lock.unlock(); } } /** * 元素出队,注意调用这个方法时都要先加锁 * */ private E dequeue(){ final Object[] items = this .items; @SuppressWarnings ( "unchecked" ) E x = (E) items[takeIndex]; items[takeIndex] = null ; if (++takeIndex == items.length) takeIndex = 0 ; count--; /当前拥有元素个数减 1 if (itrs != null ) itrs.elementDequeued(); notFull.signal(); //有一个元素取出成功,那肯定队列不满 return x; } |
其同步机制是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /*某种意义上的无界队列*/ public LinkedBlockingQueue(){ this (Integer.MAX_VALUE); } /*有界队列*/ public LinkedBlockingQueue( int capacity){ if (capacity <= 0 ) throw new IllegalArgumentException(); this .capacity = capacity; last = head = new Node<E>( null ); } 这个类有几个属性 // 队列容量 private final int capacity; // 队列中的元素数量 private final AtomicInteger count = new AtomicInteger( 0 ); // 队头 private transient Node<E> head; // 队尾 private transient Node<E> last; // take, poll, peek 等读操作的方法需要获取到这个锁,读锁 private final ReentrantLock takeLock = new ReentrantLock(); // 如果读操作的时候队列是空的,那么等待 notEmpty 条件 private final Condition notEmpty = takeLock.newCondition(); // put, offer 等写操作的方法需要获取到这个锁,写锁 private final ReentrantLock putLock = new ReentrantLock(); // 如果写操作的时候队列是满的,那么等待 notFull 条件 private final Condition notFull = putLock.newCondition(); |
同步机制是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | public LinkedBlockingQueue( int capacity) { if (capacity <= 0 ) throw new IllegalArgumentException(); this .capacity = capacity; last = head = new Node<E>( null ); //这里会初始化一个空的头结点,那么第一个元素入队的时候,队列中就会有两个元素。读取元素时,也总是获取头节点后面的一个节点。count 的计数值不包括这个头节点。 } public void put(E e) throws InterruptedException { if (e == null ) throw new NullPointerException(); // 如果你纠结这里为什么是 -1,可以看看 offer 方法。这就是个标识成功、失败的标志而已。 int c = - 1 ; Node<E> node = new Node(e); final ReentrantLock putLock = this .putLock; final AtomicInteger count = this .count; // 必须要获取到 putLock 才可以进行插入操作 putLock.lockInterruptibly(); try { // 如果队列满,等待 notFull 的条件满足。 while (count.get() == capacity) { notFull.await(); } // 入队 enqueue(node); // count 原子加 1,c 还是加 1 前的值 c = count.getAndIncrement(); // 如果这个元素入队后,还有至少一个槽可以使用,调用 notFull.signal() 唤醒等待线程。 // 哪些线程会等待在 notFull 这个 Condition 上呢? if (c + 1 < capacity) notFull.signal(); } finally { // 入队后,释放掉 putLock putLock.unlock(); } // 如果 c == 0,那么代表队列在这个元素入队前是空的(不包括head空节点), // 那么所有的读线程都在等待 notEmpty 这个条件,等待唤醒,这里做一次唤醒操作 if (c == 0 ) signalNotEmpty(); } // 入队的代码非常简单,就是将last属性指向这个新元素,并且让原队尾的next指向这个元素 // 这里入队没有并发问题,因为只有获取到 putLock 独占锁以后,才可以进行此操作 private void enqueue(Node<E> node) { // assert putLock.isHeldByCurrentThread(); // assert last.next == null; last = last.next = node; } // 元素入队后,如果需要,调用这个方法唤醒读线程来读 private void signalNotEmpty() { final ReentrantLock takeLock = this .takeLock; takeLock.lock(); try { notEmpty.signal(); } finally { takeLock.unlock(); } } public E take() throws InterruptedException { E x; int c = - 1 ; final AtomicInteger count = this .count; final ReentrantLock takeLock = this .takeLock; // 首先,需要获取到 takeLock 才能进行出队操作 takeLock.lockInterruptibly(); try { // 如果队列为空,等待 notEmpty 这个条件满足再继续执行 while (count.get() == 0 ) { notEmpty.await(); } // 出队 x = dequeue(); // count 进行原子减 1 c = count.getAndDecrement(); // 如果这次出队后,队列中至少还有一个元素,那么调用 notEmpty.signal() 唤醒其他的读线程 if (c > 1 ) notEmpty.signal(); } finally { // 出队后释放掉 takeLock takeLock.unlock(); } // 如果 c == capacity,那么说明在这个 take 方法发生的时候,队列是满的 // 既然出队了一个,那么意味着队列不满了,唤醒写线程去写 if (c == capacity) signalNotFull(); return x; } // 取队头,出队 private E dequeue() { // assert takeLock.isHeldByCurrentThread(); // assert head.item == null; // 之前说了,头结点是空的 Node<E> h = head; Node<E> first = h.next; h.next = h; // help GC // 设置这个为新的头结点 head = first; E x = first.item; first.item = null ; return x; } // 元素出队后,如果需要,调用这个方法唤醒写线程来写 private void signalNotFull() { final ReentrantLock putLock = this .putLock; putLock.lock(); try { notFull.signal(); } finally { putLock.unlock(); } } |
5.3 PriorityBlockingQueue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 构造方法中,如果不指定大小的话,默认大小为 11 private static final int DEFAULT_INITIAL_CAPACITY = 11 ; // 数组的最大容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 ; // 这个就是存放数据的数组 private transient Object[] queue; // 队列当前大小 private transient int size; // 大小比较器,如果按照自然序排序,那么此属性可设置为 null private transient Comparator<? super E> comparator; // 并发控制所用的锁,所有的 public 且涉及到线程安全的方法,都必须先获取到这个锁 private final ReentrantLock lock; // 这个很好理解,其实例由上面的 lock 属性创建 private final Condition notEmpty; // 这个也是用于锁,用于数组扩容的时候,需要先获取到这个锁,才能进行扩容操作 // 其使用 CAS 操作 private transient volatile int allocationSpinLock; // 用于序列化和反序列化的时候用,对于 PriorityBlockingQueue 我们应该比较少使用到序列化 private PriorityQueue q; |
此类实现了 Collection 和 Iterator 接口中的所有接口方法,对其对象进行迭代并遍历时,不能保证有序性。如果你想要实现有序遍历,建议采用 Arrays.sort(queue.toArray()) 进行处理。PriorityBlockingQueue 提供了 drainTo方法用于将部分或全部元素有序地填充(准确说是转移,会删除原队列中的元素)到另一个集合中。还有一个需要说明的是,如果两个对象的优先级相同(compare 方法返回 0),此队列并不保证它们之间的顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // 默认构造方法,采用默认值(11)来进行初始化 public PriorityBlockingQueue() { this (DEFAULT_INITIAL_CAPACITY, null ); } // 指定数组的初始大小 public PriorityBlockingQueue( int initialCapacity) { this (initialCapacity, null ); } // 指定比较器 public PriorityBlockingQueue( int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1 ) throw new IllegalArgumentException(); this .lock = new ReentrantLock(); this .notEmpty = lock.newCondition(); this .comparator = comparator; this .queue = new Object[initialCapacity]; } // 在构造方法中就先填充指定的集合中的元素 public PriorityBlockingQueue(Collection<? extends E> c) { this .lock = new ReentrantLock(); this .notEmpty = lock.newCondition(); // boolean heapify = true ; // true if not known to be in heap order boolean screen = true ; // true if must screen for nulls if (c instanceof SortedSet<?>) { SortedSet<? extends E> ss = (SortedSet<? extends E>) c; this .comparator = (Comparator<? super E>) ss.comparator(); heapify = false ; } else if (c instanceof PriorityBlockingQueue<?>) { PriorityBlockingQueue<? extends E> pq = (PriorityBlockingQueue<? extends E>) c; this .comparator = (Comparator<? super E>) pq.comparator(); screen = false ; if (pq.getClass() == PriorityBlockingQueue. class ) // exact match heapify = false ; } Object[] a = c.toArray(); int n = a.length; // If c.toArray incorrectly doesn't return Object[], copy it. if (a.getClass() != Object[]. class ) a = Arrays.copyOf(a, n, Object[]. class ); if (screen && (n == 1 || this .comparator != null )) { for ( int i = 0 ; i < n; ++i) if (a[i] == null ) throw new NullPointerException(); } this .queue = a; this .size = n; if (heapify) heapify(); } |
内部自动扩容实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | private void tryGrow(Object[] array, int oldCap) { // 这边做了释放锁的操作 //扩容方法对并发的控制也非常的巧妙,释放了原来的独占锁 lock,这样的话,扩容操作和读操作可以同时进行,提高吞吐量。 lock.unlock(); // must release and then re-acquire main lock Object[] newArray = null ; // 用 CAS 操作将 allocationSpinLock 由 0 变为 1,也算是获取锁 if (allocationSpinLock == 0 && UNSAFE.compareAndSwapInt( this , allocationSpinLockOffset, 0 , 1 )) { try { // 如果节点个数小于 64,那么增加的 oldCap + 2 的容量 // 如果节点数大于等于 64,那么增加 oldCap 的一半 // 所以节点数较小时,增长得快一些 int newCap = oldCap + ((oldCap < 64 ) ? (oldCap + 2 ) : (oldCap >> 1 )); // 这里有可能溢出 if (newCap - MAX_ARRAY_SIZE > 0 ) { // possible overflow int minCap = oldCap + 1 ; if (minCap < 0 || minCap > MAX_ARRAY_SIZE) throw new OutOfMemoryError(); newCap = MAX_ARRAY_SIZE; } // 如果 queue != array,那么说明有其他线程给 queue 分配了其他的空间 if (newCap > oldCap && queue == array) // 分配一个新的大数组 newArray = new Object[newCap]; } finally { // 重置,也就是释放锁 allocationSpinLock = 0 ; } } // 如果有其他的线程也在做扩容的操作 if (newArray == null ) // back off if another thread is allocating Thread.yield(); // 重新获取锁 lock.lock(); // 将原来数组中的元素复制到新分配的大数组中 if (newArray != null && queue == array) { queue = newArray; System.arraycopy(array, 0 , newArray, 0 , oldCap); } } 最重要的的put和take方法: public void put(E e) { // 直接调用 offer 方法,因为前面我们也说了,在这里,put 方法不会阻塞 offer(e); } public boolean offer(E e) { if (e == null ) throw new NullPointerException(); final ReentrantLock lock = this .lock; // 首先获取到独占锁 lock.lock(); int n, cap; Object[] array; // 如果当前队列中的元素个数 >= 数组的大小,那么需要扩容了 while ((n = size) >= (cap = (array = queue).length)) tryGrow(array, cap); try { Comparator<? super E> cmp = comparator; // 节点添加到二叉堆中 if (cmp == null ) siftUpComparable(n, e, array); else siftUpUsingComparator(n, e, array, cmp); // 更新 size size = n + 1 ; // 唤醒等待的读线程 notEmpty.signal(); } finally { lock.unlock(); } return true ; } 对于二叉堆而言,插入一个节点是简单的,插入的节点如果比父节点小,交换它们,然后继续和父节点比较。 // 这个方法就是将数据 x 插入到数组 array 的位置 k 处,然后再调整树 private static <T> void siftUpComparable( int k, T x, Object[] array) { Comparable<? super T> key = (Comparable<? super T>) x; while (k > 0 ) { // 二叉堆中 a[k] 节点的父节点位置 int parent = (k - 1 ) >>> 1 ; Object e = array[parent]; if (key.compareTo((T) e) >= 0 ) break ; array[k] = e; k = parent; } array[k] = key; } |
图示siftup操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public E take() throws InterruptedException { final ReentrantLock lock = this .lock; // 独占锁 lock.lockInterruptibly(); E result; try { // dequeue 出队 while ( (result = dequeue()) == null ) notEmpty.await(); } finally { lock.unlock(); } return result; } private E dequeue() { int n = size - 1 ; if (n < 0 ) return null ; else { Object[] array = queue; // 队头,用于返回 E result = (E) array[ 0 ]; // 队尾元素先取出 E x = (E) array[n]; // 队尾置空 array[n] = null ; Comparator<? super E> cmp = comparator; if (cmp == null ) siftDownComparable( 0 , x, array, n); else siftDownUsingComparator( 0 , x, array, n, cmp); size = n; return result; } } |
dequeue 方法返回队头,并调整二叉堆的树,调用这个方法必须先获取独占锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | private static <T> void siftDownComparable( int k, T x, Object[] array, int n) { if (n > 0 ) { Comparable<? super T> key = (Comparable<? super T>)x; // 这里得到的 half 肯定是非叶节点 // a[n] 是最后一个元素,其父节点是 a[(n-1)/2]。所以 n >>> 1 代表的节点肯定不是叶子节点 // 下面,我们结合图来一行行分析,这样比较直观简单 // 此时 k 为 0, x 为 17,n 为 9 int half = n >>> 1 ; // 得到 half = 4 while (k < half) { // 先取左子节点 int child = (k << 1 ) + 1 ; // 得到 child = 1 Object c = array[child]; // c = 12 int right = child + 1 ; // right = 2 // 如果右子节点存在,而且比左子节点小 // 此时 array[right] = 20,所以条件不满足 if (right < n && ((Comparable<? super T>) c).compareTo((T) array[right]) > 0 ) c = array[child = right]; // key = 17, c = 12,所以条件不满足 if (key.compareTo((T) c) <= 0 ) break ; // 把 12 填充到根节点 array[k] = c; // k 赋值后为 1 k = child; // 一轮过后,我们发现,12 左边的子树和刚刚的差不多,都是缺少根节点,接下来处理就简单了 } array[k] = key; } } |
图示siftDown操作:
1 2 3 4 5 6 7 8 9 10 11 12 | public SynchronousQueue( boolean fair) { transferer = fair ? new TransferQueue() : new TransferStack(); } abstract static class Transferer { // 从方法名上大概就知道,这个方法用于转移元素,从生产者手上转到消费者手上 // 也可以被动地,消费者调用这个方法来从生产者手上取元素 // 第一个参数 e 如果不是 null,代表场景为:将元素从生产者转移给消费者 // 如果是 null,代表消费者等待生产者提供元素,然后返回值就是相应的生产者提供的元素 // 第二个参数代表是否设置超时,如果设置超时,超时时间是第三个参数的值 // 返回值如果是 null,代表超时,或者中断。具体是哪个,可以通过检测中断状态得到。 abstract Object transfer(Object e, boolean timed, long nanos); } |
Transferer 有两个内部实现类,是因为构造 SynchronousQueue 的时候,我们可以指定公平策略。公平模式意味着,所有的读写线程都遵守先来后到,FIFO 嘛,对应 TransferQueue。而非公平模式则对应 TransferStack。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 写入值 public void put(E o) throws InterruptedException { if (o == null ) throw new NullPointerException(); if (transferer.transfer(o, false , 0 ) == null ) { // 1 Thread.interrupted(); throw new InterruptedException(); } } // 读取值并移除 public E take() throws InterruptedException { Object e = transferer.transfer( null , false , 0 ); // 2 if (e != null ) return (E)e; Thread.interrupted(); throw new InterruptedException(); } |
看到,写操作 put(E o) 和读操作 take() 都是调用 Transferer.transfer(…) 方法,区别在于第一个参数是否为 null 值。
1 2 3 4 5 6 7 8 9 10 11 12 | static final class Node<K,V> { final K key; volatile Object value; //value值 volatile Node<K,V> next; //next引用 …… } static class Index<K,V> { final Node<K,V> node; final Index<K,V> down; //downy引用 volatile Index<K,V> righ …… } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | private V doPut(K kkey, V value, boolean onlyIfAbsent) { Comparable<? super K> key = comparable(kkey); for (;;) { // 找到key的前继节点 Node<K,V> b = findPredecessor(key); // 设置n为“key的前继节点的后继节点”,即n应该是“插入节点”的“后继节点” Node<K,V> n = b.next; for (;;) { if (n != null ) { Node<K,V> f = n.next; // 如果两次获得的b.next不是相同的Node,就跳转到”外层for循环“,重新获得b和n后再遍历。 if (n != b.next) break ; // v是“n的值” Object v = n.value; // 当n的值为null(意味着其它线程删除了n);此时删除b的下一个节点,然后跳转到”外层for循环“,重新获得b和n后再遍历。 if (v == null ) { // n is deleted n.helpDelete(b, f); break ; } // 如果其它线程删除了b;则跳转到”外层for循环“,重新获得b和n后再遍历。 if (v == n || b.value == null ) // b is deleted break ; // 比较key和n.key int c = key.compareTo(n.key); if (c > 0 ) { b = n; n = f; continue ; } if (c == 0 ) { if (onlyIfAbsent || n.casValue(v, value)) return (V)v; else break ; // restart if lost race to replace value } // else c < 0; fall through } // 新建节点(对应是“要插入的键值对”) Node<K,V> z = new Node<K,V>(kkey, value, n); // 设置“b的后继节点”为z if (!b.casNext(n, z)) break ; // 多线程情况下,break才可能发生(其它线程对b进行了操作) // 随机获取一个level // 然后在“第1层”到“第level层”的链表中都插入新建节点 int level = randomLevel(); if (level > 0 ) insertIndex(z, level); return null ; } } } |
doPut() 的作用就是将键值对添加到“跳表”中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | private final V doRemove(Object okey, Object value) { Comparable<? super K> key = comparable(okey); for (;;) { // 找到“key的前继节点” Node<K,V> b = findPredecessor(key); // 设置n为“b的后继节点”(即若key存在于“跳表中”,n就是key对应的节点) Node<K,V> n = b.next; for (;;) { if (n == null ) return null ; // f是“当前节点n的后继节点” Node<K,V> f = n.next; // 如果两次读取到的“b的后继节点”不同(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。 if (n != b.next) // inconsistent read break ; // 如果“当前节点n的值”变为null(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。 Object v = n.value; if (v == null ) { // n is deleted n.helpDelete(b, f); break ; } // 如果“前继节点b”被删除(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。 if (v == n || b.value == null ) // b is deleted break ; int c = key.compareTo(n.key); if (c < 0 ) return null ; if (c > 0 ) { b = n; n = f; continue ; } // 以下是c=0的情况 if (value != null && !value.equals(v)) return null ; // 设置“当前节点n”的值为null if (!n.casValue(v, null )) break ; // 设置“b的后继节点”为f if (!n.appendMarker(f) || !b.casNext(n, f)) findNode(key); // Retry via findNode else { // 清除“跳表”中每一层的key节点 findPredecessor(key); // Clean index // 如果“表头的右索引为空”,则将“跳表的层次”-1。 if (head.right == null ) tryReduceLevel(); } return (V)v; } } } |
doRemove()的作用是删除跳表中的节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | public V get(Object key) { return doGet(key); } private V doGet(Object okey) { Comparable<? super K> key = comparable(okey); for (;;) { // 找到“key对应的节点” Node<K,V> n = findNode(key); //doGet()是通过findNode()找到并返回节点的。 if (n == null ) return null ; Object v = n.value; if (v != null ) return (V)v; } } private Node<K,V> findNode(Comparable<? super K> key) { for (;;) { // 找到key的前继节点 Node<K,V> b = findPredecessor(key); // 设置n为“b的后继节点”(即若key存在于“跳表中”,n就是key对应的节点) Node<K,V> n = b.next; for (;;) { // 如果“n为null”,则跳转中不存在key对应的节点,直接返回null。 if (n == null ) return null ; Node<K,V> f = n.next; // 如果两次读取到的“b的后继节点”不同(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。 if (n != b.next) // inconsistent read break ; Object v = n.value; // 如果“当前节点n的值”变为null(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。 if (v == null ) { // n is deleted n.helpDelete(b, f); break ; } if (v == n || b.value == null ) // b is deleted break ; // 若n是当前节点,则返回n。 int c = key.compareTo(n.key); if (c == 0 ) return n; // 若“节点n的key”小于“key”,则说明跳表中不存在key对应的节点,返回null if (c < 0 ) return null ; // 若“节点n的key”大于“key”,则更新b和n,继续查找。 b = n; n = f; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public class TestConcurrentSkipListMapDeno { // map是TreeMap对象时,程序会出错。 //private static Map<String, String> skiplistmap = new TreeMap<String, String>(); private static Map<String, String> skiplistmap = new ConcurrentSkipListMap<String, String>(); public static void main(String[] args) { new ThreadImpl( "Thread1 " ).start(); new ThreadImpl( "线程2 " ).start(); } private static class ThreadImpl extends Thread { private String name; @Override public void run() { int i = 0 ; while (i++ < 6 ) { // “线程名” + "序号" String val = Thread.currentThread().getName() + i; skiplistmap.put(val, "0" ); // 通过“Iterator”遍历map。 printAll(); } } public ThreadImpl(String name) { super (name); } } private static void printAll(){ String key, value; Iterator iterator = skiplistmap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry entry = (Map.Entry) iterator.next(); key = (String) entry.getKey(); value = (String) entry.getValue(); System.out.println( "key:" + key + "--> value:" +value); } } } |
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
· 提示词工程——AI应用必不可少的技术