java集合-Map篇

java集合-Map篇

JDK提供的集合类型主要分为四种类型:

  1. List:支持重复元素
  2. Set:不支持重复元素
  3. Map:键/值对的映射集
  4. Queue/Deque(double ended queue):queue是在集合尾部添加元素,在头部删除元素的队列,deque是可在头部和尾部添加或者删除元素的双端队列,deque既可以实现队列又可以实现栈

本文基于JDK8,java version “1.8.0_251”

HashMap

数组+链表/红黑树,非线程安全

  1. 在JDK8之前,hashMap是数组+链表的结构,JDK8引入了红黑树。当链表元素大于等于8,且table数组长度大于64,链表会转换成红黑树(链表元素个数大于等于8但table数组的长度小于64时,只触发扩容),若红黑树的节点个数小于等于6,树结构还原成链表。因为红黑树的平均查找长度是log_2 n,而链表的平均查找长度是n/2。中间有个差值,可以防止链表和树频繁转换。

  2. 默认容量为16,默认负载因子为0.75,最大容量为2^30

     /**
     * 默认的初始容量是16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    /**
     * 最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    /**
     * 默认负载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     * 树化阈值。当添加元素到桶中,如果桶中链表长度被添加到至少8,链表就转换为红黑树
     */
    static final int TREEIFY_THRESHOLD = 8;
    /**
     * 链表还原阈值。
     */
    static final int UNTREEIFY_THRESHOLD = 6;
    /**
     * 最小数形化容量。桶中链表被转换为红黑树的最小容量是64
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
    
  3. 容量总是为2的次幂(数组索引计算为与运算,i = (n - 1) & hash),在计算机中, (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,计算更加高效。

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    
  4. 懒加载,使用空参构造方法初始完后,只是给负载因子赋值,table并没有初始化,执行put方法后,才会将table初始化为默认大小16;且values方法和keySet方法也为懒加载,没有调用values和keySet方法之前,values和keySet为空,调用时才初始化。

    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;
    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    
  5. 动态扩容,当 元素数量 = 容量*负载因子时触发扩容,不存在缩容。新的容量为旧容量的2倍。然后将原来的Node节点放入新的bucket数组中。这个过程叫作再散列,因为它调用hash方法找到新的bucket位置。在JDK8之前,采用的是头插法,多个线程发现需要调整hashmap的大小,他们会同时进行rehashing,这样可能导致链表产生环,rehashing过程也会死循环,其他线程执行get方法也可能会导致死循环。而JDK8采用尾插法,避免了这一点,同时JDK8如果单个链表长度大于8且table数组长度大于64时,链表会转为红黑树,这点也需要计算链表长度,正好一举两得。

  6. 非线程安全,通过**Collections.synchronizedMap()**方法把它转成线程安全的集合,效率较低,推荐使用ConcurrentHashMap。

  7. 支持fail-fast机制,KeyIterator和ValueIterator都继承自HashIterator,而HashIterator的nextNode方法中包含会检查modCount的值,如果modCount != expectedModCount会抛出ConcurrentModificationException异常。

  8. 允许key和value为null,hash函数做了判空处理

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

HashTable

和JDK8之前的HashMap很相似,线程安全

  1. 基于数组+链表实现

  2. 默认容量为11,默认负载因子为0.75,显式定义了数组的长度最大为Integer.MAX_VALUE - 8,没有定义节点的最大数量。

    /**
     * Constructs a new, empty hashtable with a default initial capacity (11)
     * and load factor (0.75).
     */
    public Hashtable() {
        this(11, 0.75f);
    }
    
  3. 无懒加载,默认容量就是11

  4. 动态扩容,当 元素数量 = 容量*负载因子时触发扩容,不存在缩容。新的容量为旧容量的2倍+1。链表新增元素采用头插法,由于put方法使用synchronized修饰,所以不会出现多个线程同时扩容。

  5. 线程安全,get、put、remove等方法均使用synchronized修饰,效率较低,推荐使用ConcurrentHashMap。

  6. 支持fail-fast机制,但是Enumerator无法获取,获取迭代器是私有方法,只能先获取keySet()或values(),再获取iterator

    public T next() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        return nextElement();
    }
    private <T> Iterator<T> getIterator(int type) {
        if (count == 0) {
            return Collections.emptyIterator();
        } else {
            return new Enumerator<>(type, true);
        }
    }
    
  7. key和value都不允许为null,为空会抛出NullPointerException异常

  8. 计算数组索引采用与运算,(hash & 0x7FFFFFFF) % tab.length,hash & 0x7FFFFFFF的作用是为了得到一个正数。

TreeMap

红黑树结构,有序Map集合,非线程安全,

  1. 基于红黑树结构的有序Map集合,元素的顺序基于键的compareTo方法,或者自定义的comparator。
  2. 基于红黑树,无懒加载
  3. 非线程安全,通过**Collections.synchronizedSortedMap()**方法把它转成线程安全的集合,效率较低
  4. 支持fail-fast机制,如果在创建了Interator后,有其他线程修改了集合导致modCount != expectedModCount则会抛出ConcurrentModificationException异常。
  5. key不允许为null,value允许为null,key为空会抛出NullPointerException异常

LinkedHashMap

在这里插入图片描述

继承自HashMap,有序集合,非线程安全,可以用来实现FIFO和LRU缓存淘汰算法

  1. 继承自HashMap的有序集合,根据初始化参数accessOrder是false还是true来选择是按插入元素时的顺序还是按访问顺序进行排序。默认accessOrder=false,按插入元素的顺序进行排序

    public class LinkedHashMap<K,V>
        extends HashMap<K,V>
        implements Map<K,V>
    {
    
  2. 新增双向链表结构,在HashMap结构的基础上,新增了一个双向链表的结构。重写了HashMap的部分方法,比如创建Node节点,会调用linkNodeLast方法,将元素加到链表的尾部;get方法,查询到了元素,会根据accessOrder判断,如果为true,会将元素移动到链表的尾部。

    /**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;
    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;
    /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }
    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }
    
  3. 懒加载,主要是调用HashMap构造函数,HashMap是懒加载,所以将它也定义为懒加载。

    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
    
  4. 动态扩容,JDK8之前重写了部分数据迁移的方法transfer(通过遍历双向链表),JDK8直接使用HashMap的扩容机制。从遍历的效率来说,遍历双向链表的效率要高于遍历table,因为遍历双向链表是N次(N为元素个数);而遍历table是N+table的空余个数(N为元素个数)。

  5. 非线程安全,通过**Collections.synchronizedMap()**方法把它转成线程安全的集合,效率较低

  6. 支持fail-fast机制,nextNode方法中会判断modCount值,如果modCount != expectedModCount,则会抛出ConcurrentModificationException异常。

  7. 允许key和value为null,因为基于HashMap,所以它也允许key和value为null

linkNodeLast

添加一个对象,创建Node节点,然后调用linkNodeLast方法,将元素加到链表的尾部

// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

afterNodeAccess

当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

afterNodeInsertion

在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时(默认返回false)会移除最晚的节点,也就是链表首部节点 first。

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

要实现LRU或者FIFO,首先就需要重写removeEldestEntry(),当元素容量>缓存容量时,返回true。

private final static MAX_ENTRIES = 10;
boolean removeEldestEntry(Map.Entry eldest) {
    return size() > MAX_ENTRIES;
}

LRU实现思路:accessOrder设置为true,最新添加的元素在链表的头部,如果一个元素被访问了,这个元素会被移动到链表尾部,链表头部的元素首先被删除。

FIFO实现思路:accessOrder设置为false,最新添加的元素在链表头,最旧的元素在链表尾,链表头部的元素首先被删除。

LFU实现思路:使用HashMap,key就是缓存key,value包含缓存value,访问次数,创建时间。查询缓存,修改元素的访问次数;添加缓存,先判断当前缓存容量是否等于最大容量,如果是,则遍历values,找出访问次数最少(次数相同则比较创建时间)的元素并将其移除,然后再添加缓存元素。

IdentityHashMap

和JDK8之前的HashMap很相似,IdentityHashMap比较的是引用,HashMap是比较equal方法

  1. 基于数组存储,且key,value是连续存储。

    tab[i] = k;
    tab[i + 1] = value;
    
  2. 线性探测法解决hash冲突

    private static int nextKeyIndex(int i, int len) {
        return (i + 2 < len ? i + 2 : 0);
    }
    
  3. 数组默认长度为32,既容量为16,默认负载因子为2/3(没有显示定义负载因子,而是在代码中写死),最大容量为1 << 29

  4. 容量总是为2的次幂(数组索引计算为与运算,i = hash & (n - 1) ),在计算机中, (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,计算更加高效。

  5. 无懒加载,使用空参构造或指定大小都会初始化table数组。

  6. 动态扩容,当 元素数量 = 容量*负载因子时触发扩容,不存在缩容。新的容量为旧容量的2倍。扩容之后需要再散列

  7. 非线程安全,通过**Collections.synchronizedMap()**方法把它转成线程安全的集合,效率较低

  8. 支持fail-fast机制,nextNode方法中会判断modCount值,如果modCount != expectedModCount,则会抛出ConcurrentModificationException异常。

  9. 允许key和value为null

  10. key 和 value 比较引用判断是否相等(HashMap通过equeal方法)

WeakHashMap

和HashMap很相似,弱引用,实现缓存(Tomcat中的ConcurrentCache)

  1. 基于数组+链表的结构,默认容量为16,默认负载因子为0.75,最大容量为2^30。

  2. 容量总是为2的次幂(数组索引计算为与运算,i = (n - 1) & hash),在计算机中, (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,计算更加高效

  3. 无懒加载,使用空参构造或指定大小都会初始化table数组。

  4. 动态扩容,当 元素数量 = 容量*负载因子时触发扩容。新的容量为旧容量的2倍。插入和再散列过程采用头插法,多个线程执行再散列可能会导致链表产生环(和上文HashMap类似)。将元素从旧Map迁移到新表时,若size小于threshold/2,重新将数据从新表迁移到旧表(因为迁移的时候会删除已经被回收的条目:key为null)。

  5. 非线程安全,通过**Collections.synchronizedMap()**方法把它转成线程安全的集合,效率较低。

  6. 支持fail-fast机制,nextNode方法中会判断modCount值,如果modCount != expectedModCount,则会抛出ConcurrentModificationException异常。

  7. 允许key和value为null

  8. Entry继承自WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。

  9. expungeStaleEntries方法清除过时的条目,当key失效时,GC会自动将对应的Entry添加到ReferenceQueue queue中,在调用getTable size resize方法时会调用expungeStaleEntries方法,这些方法会被几乎所有方法调用。

    /**
     * Reference queue for cleared WeakEntries
     */
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
    /**
     * Expunges stale entries from the table.
     */
    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);
    
                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table[i] = next;
                        else
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }
    

ConcurrentHashMap

线程安全的HashMap,Synchronized + CAS

  1. 基于数组+链表/红黑树,当链表元素大于等于8,且table数组长度大于64,链表会转换成红黑树(链表元素个数大于等于8但table数组长度小于64时,只触发扩容),若红黑树的节点个数小于等于6,树结构还原成链表。因为红黑树的平均查找长度是log_2 n,而链表的平均查找长度是n/2。中间有个差值,可以防止链表和树频繁转换。

  2. 默认容量为16,默认负载因子为0.75,最大容量为2^30。指定的负载因子只在构造方法中有效,用来计算扩容阈值,不会存储负载因子。后续扩容还是使用0.75

  3. 容量总是为2的次幂(数组索引计算为与运算,i = (n - 1) & hash),在计算机中, (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,计算更加高效。

  4. 懒加载,空参构造方法什么也没干,指定容量的构造方法中,计算了 sizeCtl,sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】。如 initialCapacity 为 10,那么得到 sizeCtl 为 16,如果 initialCapacity 为 11,得到 sizeCtl 为 32

    public ConcurrentHashMap() {
    }
    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }
    
  5. 线程安全,JDK7采用分段锁(Segment)设计,而JDK采用Synchronized + CAS设计实现线程安全。

  6. 动态扩容。新的容量是旧容量的2倍。数据迁移。扩容的第一次初始化新表(扩容后的新表)这个动作,只能由一个线程完成。其他线程都是在帮助迁移元素到新数组。数据迁移从后向前进行,通过改变transferIndex的值从n到0,每个线程迁移数组中指定长度Node节点(这个长度叫做步长)。transferIndex到0后,还会将其值改回n,检查一遍是否迁移成功。扩容和数据迁移数concurrentHashMap中最难的一部分,参考下面的资料方便理解。

  7. 添加元素采用尾插法(方便计数)数据迁移时采用头插法

  8. 对于获取size使用CounterCell避免大量的CAS ,当需要修改元素数量时,线程会先去 CAS 修改 baseCount 加1,若成功即返回。若失败,则线程被分配到某个 CounterCell ,然后操作 value 加1。若成功,则返回。否则,给当前线程重新分配一个 CounterCell,再尝试给 value 加1。(这里简略的说,实际更复杂),遍历CounterCell的值和baseCount相加的结果就是容量。

  9. 对于get读操作,如果当前节点有数据,还没迁移完成,此时不影响读,能够正常进行。 如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时get线程会帮助扩容。

  10. 对于put/remove写操作,如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时写线程会帮助扩容,如果扩容没有完成,当前链表的头节点会被锁住,所以写线程会被阻塞,直到扩容完成。

  11. Node类的value和next使用volatile修饰,保证可见性。

  12. sizeCtl,默认为0,用来控制table的初始化和扩容操作。-1 代表table正在初始化,-N 表示有N-1个线程正在进行扩容操作 。如果table未初始化,表示table需要初始化的大小。 如果table初始化完成,表示table的容量,默认是table大小的0.75倍。用来控制扩容操作时,高16位是数据校验标识,低16位代表当前有几个线程正在帮助扩容。扩容时候会判断这个值,如果超过阈值就要扩容,首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素f,初始化一个forwardNode实例fwd,如果f == null,则在table中的i位置放入fwd,否则采用头插法的方式把当前旧table数组的指定任务范围的数据给迁移到新的数组中,然后
    给旧table原位置赋值fwd。直到遍历过所有的节点以后就完成了复制工作,把table指向nextTable,并更新sizeCtl为新数组大小的0.75倍 ,扩容完成。在此期间如果其他线程的有读写操作都会判断head节点是否为forwardNode节点,如果是就帮助扩容。

  13. 红黑树的根节点的哈希值为-2,判断的是否为红黑树直接判断第一个元素的哈希值是否大于0。

  14. 不支持fail-fast机制

  15. key和value都不能为null,否则会抛出空指针异常。

  16. 线程安全,并发粒度细,性能强。

  17. 弱一致性,因为使用了Counter来帮助计算容量,遍历Counter时没有加锁,所以存在弱一致性问题(实际上juc集合里面的size方法所返回的元素个数都是不保证准确的)。另外JDK8之前迭代器也存在弱一致性问题,HashEntry的next属性定义为final,这意味着它是不可变的,如果线程A 删掉了第 7 个元素,线程B 已经遍历到了第 3 个元素,那么 B 还是可以遍历到被删除的第 7 个元素。

笔记:

  1. Thread.yield();让掉当前线程 CPU 的时间片,使正在运行中的线程重新变成就绪状态,并重新竞争 CPU 的调度权。作用:避免一个线程长时间占有 CPU 资源

参考资料

  1. ConcurrentHashMap 底层原理
  2. Java 中的 HashMap 和 ConcurrentHashMap

ConcurrentSkipListMap

在这里插入图片描述

跳跃表,线程安全,CAS,无锁,有序,空间换时间

  1. 基于跳跃表,理论上能够O(log(n))时间内完成查找、插入、删除操作。

  2. 有序Map集合,key是有序的,元素的顺序基于键的compareTo方法,或者自定义的comparator。

  3. 线程安全,基于CAS+volatile实现,并发粒度细,性能强。

  4. 空间换时间,基于跳跃表实现的有序,包含多级索引,每个级别的索引节点按照其关联数据节点的关键字升序排列,且高级别索引是其低级别索引的子集,如果关键字key在级别level=i的索引中出现,则级别level<=i的所有索引中都包含key。

  5. 节点主要由 Node, Index, HeadIndex 构成,横向纵向都是链表。最下面那层链表是Node层(数据节点层), 上面几层都是Index层(索引)。从纵向链表来看, 最左边的是 HeadIndex 层, 右边的都是Index 层, 且每层的最底端都是对应Node, 纵向上的索引都是指向最底端的Node

    static final class Node<K, V>{
        final K key;  // key 是 final 的, 说明节点一旦定下来, 除了删除, 不然不会改动 key 了
        volatile Object value; // 对应的 value
        volatile Node<K, V> next; // 下一个节点
    }
    static class Index<K, V>{
        final Node<K, V> node; // 索引指向的节点, 纵向上所有索引指向链表最下面的节点
        final Index<K, V> down; // 下边level层的 Index
        volatile Index<K, V> right; // 右边的  Index
    }
    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); // 所有的 HeadIndex 都指向同一个 Base_header 节点
            this.level = level;
        }
    }
    
  6. 元素的增删改查都从最上层的head指针指向的结点开始

  7. 头插法创建Index结点,在删除键值对时,不会立即执行删除,而是通过引入标记结点,以懒删除的方式进行,以提高并发效率。

  8. 每次put操作都会生成随机数,然后用来获取level (50%的几率返回0,25%的几率返回1,12.5%的几率返回2…最大返回31),如果level大于最大的level,则会新增level。否则只插入一个Index节点。

  9. key和value都不能为null,否则会抛出空指针异常。

  10. 不支持fail-fast机制。

  11. 无懒加载

参考资料

  1. J.U.C之collections框架:ConcurrentSkipListMap
  2. ConcurrentSkipListMap 源码分析 (基于Java 8)

问题

性能谁更强?ConcurrentSkipListMap VS ConcurrentHashMap

在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。

但ConcurrentSkipListMap有几个ConcurrentHashMap 不能比拟的优点:

1、ConcurrentSkipListMap 的key是有序的。

2、ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值