Java集合底层源码剖析-HashMap扩展

LinkedHashMap有顺序的map数据结构

概述

LinkedHashMap是HashMap的子类,它的大部分实现与HashMap相同,两者最大的区别在于,HashMap的对哈希表进行迭代时是无序的,而 LinkedHashMap对哈希表迭代是有序的,LinkedHashMap默认的规则是,迭代输出的结果保持和插入key-value pair的顺序一致(当然具体迭代规则可以修改)。LinkedHashMap除了像HashMap一样用数组、单链表和红黑树来组织数据外,还额外维护了一个 双向链表,每次向linkedHashMap插入键值对,除了将其插入到哈希表的对应位置之外,还要将其插入到双向循环链表的尾部。它是由 数组 + 一个单项链表+一个双向链表组成。在原HashMap的数据结构基础上加一个双向链表

  • 底层是散列表和双向链表
  • 允许为null,不同步
  • 插入的顺序是有序的(底层链表致使有序)
  • 装载因子和初始容量对LinkedHashMap影响是很大的~

LinkedHashMap比HashMap多了一个双向链表的维护,在数据结构而言它要复杂一些,阅读源码起来比较轻松一些,因为大多都由HashMap实现了…
阅读源码的时候我们会发现多态是无处不在的~子类用父类的方法,子类重写了父类的部分方法即可达到不一样的效果!
比如:LinkedHashMap并没有重写put方法,而put方法内部的newNode()方法重写了。LinkedHashMap调用父类的put方法,里面回调的是重写后的newNode(),从而达到目的!

LinkedHashMap可以设置两种遍历顺序:

  • 访问顺序(access-ordered)
  • 插入顺序(insertion-ordered)
  • 默认是插入顺序的
    对于访问顺序,它是LRU(最近最少使用)算法的实现,要使用它要么重写LinkedListMap的几个方法(removeEldestEntry(Map.Entry<K,V> eldest)和afterNodeInsertion(boolean evict)),要么是扩展成LRUMap来使用,不然设置为访问顺序(access-ordered)的用处不大,对于LinkedHashMap的LRU这边没有深入了,因为确实用的不多,但是大家有兴趣的可以深入研究。
    LinkedHashMap遍历的是内部维护的双向链表,所以说初始容量对LinkedHashMap遍历是不受影响的
    一句话。LinkedHashMap就是继承HashMap的加上双向链表的HashMap

基本属性

         //双向链表的头节点
    transient LinkedHashMap.Entry<K,V> head;
    
     //双向链表的尾节点
    transient LinkedHashMap.Entry<K,V> tail;

   //排序的规则,false按插入顺序排序,true访问顺序排序 
    final boolean accessOrder;
    
    //所以说accessOrder的作用就是控制访问顺序,设置为true后每次访问一个元素,就将该元素所在的Node变成最后一个节点,
    改变该元素在LinkedHashMap中的存储顺序。

构造方法

//LinkedHashMap的构造方法,都是通过调用父类的构造方法来实现,大部分accessOrder默认为false
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}
public LinkedHashMap() {
    super();
    accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super(m);
    accessOrder = false;
}
public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

上面是LinkedHashMap的构造方法,通过传入初始化参数和代码看出,LinkedHashMap的构造方法和父类的构造方法,是一一对应的。也是通过super()关键字来调用父类的构造方法来进行初始化,唯一的不同是最后一个构造方法,提供了AccessOrder参数,用来指定LinkedHashMap的排序方式,accessOrder =false -> 插入顺序进行排序 , accessOrder = true -> 访问顺序进行排序。

Entry定义

这个比较重要

private static class Entry<K,V> extends HashMap.Entry<K,V> {
    //定义Entry类型的两个变量,或者称之为前后的两个指针    
    Entry<K,V> before, after;
    //构造方法与HashMap的没有区别,也是调用父类的Entry构造方法
    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
    }

    //删除
    private void remove() {
        before.after = after;
        after.before = before;
    }

    //插入节点到指定的节点之前
    private void addBefore(Entry<K,V> existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
    }

    //方法重写,HashMap中为空
    void recordAccess(HashMap<K,V> m) {
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) {
            lm.modCount++;
            remove();
            addBefore(lm.header);
        }
    }
    //方法重写 ,HashMap中方法为空
    void recordRemoval(HashMap<K,V> m) {
        remove();
    }
}

到这里已经可以知道,相对比HashMap,LinkedHashMap内部不光是使用HashMap中的哈希表来存储Entry对象,还另外维护了一个LinkedHashMapEntry,这些LinkedHashMapEntry内部又保存了前驱跟后继的引用,可以确定这是个双向链表。而这个LinkedHashMapEntry提供了对象的增加删除方法都是去更改节点的前驱后继指向。

put()方法

LinkedHashMap并没有重写父类的put()方法,说明调用put方法时实际上调用的是父类的put方法。HashMap的put这里就不多说了中有这样一个方法

LinkedHashMap 重写了2个方法

ode<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) {
        //用临时变量last记录尾节点tail
        LinkedHashMap.Entry<K,V> last = tail;
        //将尾节点设为当前插入的节点p
        tail = p;
        //如果原先尾节点为null,表示当前链表为空
        if (last == null)
            //头结点也为当前插入节点
            head = p;
        else {
            //原始链表不为空,那么将当前节点的上节点指向原始尾节点
            p.before = last;
            //原始尾节点的下一个节点指向当前插入节点
            last.after = p;
        }
    }
    //把当前节点放到双向链表的尾部
    void afterNodeAccess(HashMap.Node<K,V> e) { // move node to last
       LinkedHashMap.Entry<K,V> last;
       //当 accessOrder = true 并且当前节点不等于尾节点tail。这里将last节点赋值为tail节点
     if (accessOrder && (last = tail) != e) {
           //记录当前节点的上一个节点b和下一个节点a
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
          //释放当前节点和后一个节点的关系
           p.after = null;
             //如果当前节点的前一个节点为null
           if (b == null)
            //头节点=当前节点的下一个节点
              head = a;
          else
             //否则b的后节点指向a
           b.after = a;
           //如果a != null
           if (a != null)
             //a的前一个节点指向b
             a.before = b;
            else
              //b设为尾节点
           last = b;
          //如果尾节点为null
           if (last == null)
                //头节点设为p
                head = p;
         else {
             //否则将p放到双向链表的最后
               p.before = last;
              last.after = p;
            }
        //将尾节点设为p
            tail = p;
          //LinkedHashMap对象操作次数+1,用于快速失败校验
          ++modCount;
      }
   }

通过重写put方法,来维护一个双向链表,使得存取有序。

get()方法

public V get(Object key) {
    Entry<K,V> e = (Entry<K,V>)getEntry(key); //调用父类的getEntry()方法
    if (e == null)
        return null;
    e.recordAccess(this);  //判断排序方式,如果accessOrder = true , 删除当前e节点
    return e.value;
}

相比于 HashMap 的 get 方法,这里多出了第 5,6行代码,当 accessOrder = true 时,即表示按照最近访问的迭代顺序,会将访问过的元素放在链表后面。

TreeMap 自定义排序规则的红黑树map数据结构

  • TreeMap集合结构特点
    • 键的数据结构是红黑树,可保证键的排序和唯一性
    • 排序分为自然排序和比较器排序,如果使用的是自然排序,对元素有要求,要求这个元素需要实现 Comparable 接口
    • 线程是不安全的效率比较高
    public TreeMap(): 自然排序
    public TreeMap(Comparator<? super K> comparator):  使用的是比较器排序
    
    之前已经学习过HashMap和LinkedHashMap了,HashMap不保证数据有序,LinkedHashMap保证数据可以保持插入顺序,而如果我们希望Map可以保持key的大小顺序的时候,我们就需要利用TreeMap了。

put函数源码

  • 如果存在的话,old value被替换;如果不存在的话,则新添一个节点,然后对做红黑树的平衡操作。
    public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
            // 如果该节点存在,则替换值直接返回
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        // 如果该节点未存在,则新建
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        
        // 红黑树平衡调整
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
    

get获取函数源码

  • get函数则相对来说比较简单,以log(n)的复杂度进行get。博客
    final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
            // 按照二叉树搜索的方式进行搜索,搜到返回
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }
    public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
    }
    

如何保证有序性

  • TreeMap是如何保证其迭代输出是有序的呢?
    • 其实从宏观上来讲,就相当于树的中序遍历(LDR)。我们先看一下迭代输出的步骤
    for(Entry<Integer, String> entry : tmap.entrySet()) {
        System.out.println(entry.getKey() + ": " + entry.getValue());
    }
    
    • for语句会做如下转换为:
    for(Iterator<Map.Entry<String, String>> it = tmap.entrySet().iterator() ; tmap.hasNext(); ) {
        Entry<Integer, String> entry = it.next();
        System.out.println(entry.getKey() + ": " + entry.getValue());
    }
    
    • it.next()的调用中会使用nextEntry调用successor这个是过的后继的重点。
  • 然后看一下successor函数
    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) {
            // 有右子树的节点,后继节点就是右子树的“最左节点”
            // 因为“最左子树”是右子树的最小节点
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            // 如果右子树为空,则寻找当前节点所在左子树的第一个祖先节点
            // 因为左子树找完了,根据LDR该D了
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            // 保证左子树
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }
    
    • 怎么理解这个successor呢?只要记住,这个是中序遍历就好了,L-D-R。具体细节如下:
      • a. 空节点,没有后继
      • b. 有右子树的节点,后继就是右子树的“最左节点”
      • c. 无右子树的节点,后继就是该节点所在左子树的第一个祖先节点
      • 有右子树的节点,节点的下一个节点,肯定在右子树中,而右子树中“最左”的那个节点则是右树中最小的一个,那么当然是右子树的“最左节点”
      • 无右子树的节点,先找到这个节点所在的左子树(右图),那么这个节点所在的左子树的父节点(绿色节点),就是下一个节点。

HashSet

- HashSet 实现了 Set 接口,不允许插入重复的元素,允许包含 null 元素,且不保证元素迭代顺序,特别是不保证该顺序恒久不变
- HashSet 的代码十分简单,去掉注释后的代码不到两百行。HashSet 底层是通过 HashMap 来实现的。
- HashSet是根据hashCode来决定存储位置的,是通过HashMap实现的,所以对象必须实现hashCode()方法,存储的数据无序不能重复,可以存储null,但是只能存一个。

HashSet如何去重

  • 在向 HashSet 添加元素时,HashSet 会将该操作转换为向 HashMap 添加键值对,如果 HashMap 中包含 key 值与待插入元素相等的键值对(hashCode() 方法返回值相等,通过 equals() 方法比较也返回 true),则待添加的键值对的 value 会覆盖原有数据,但 key 不会有所改变,因此如果向 HashSet 添加一个已存在的元素时,元素不会被存入 HashMap 中,从而实现了 HashSet 元素不重复的特征。

源码分析

    public class HashSet<E>
        extends AbstractSet<E>
        implements Set<E>, Cloneable, java.io.Serializable{
    
        //序列化ID
        static final long serialVersionUID = -5024744406713321676L;
    
        //HashSet 底层用 HashMap 来存放数据
        //Key值由外部传入,Value则由 HashSet 内部来维护
        private transient HashMap<E,Object> map;
    
        //HashMap 中所有键值对都共享同一个值
        //即所有存入 HashMap 的键值对都是使用这个对象作为值
        private static final Object PRESENT = new Object();
    
        //无参构造函数,HashMap 使用默认的初始化大小和装载因子
        public HashSet() {
            map = new HashMap<>();
        }
    
        //使用默认的装载因子,并以此来计算 HashMap 的初始化大小
        //+1 是为了弥补精度损失
        public HashSet(Collection<? extends E> c) {
            map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
            addAll(c);
        }
    
        //为 HashMap 自定义初始化大小和装载因子
        public HashSet(int initialCapacity, float loadFactor) {
            map = new HashMap<>(initialCapacity, loadFactor);
        }
    
        //为 HashMap 自定义初始化大小
        public HashSet(int initialCapacity) {
            map = new HashMap<>(initialCapacity);
        }
    
        //此构造函数为包访问权限,只用于对 LinkedHashSet 的支持
        HashSet(int initialCapacity, float loadFactor, boolean dummy) {
            map = new LinkedHashMap<>(initialCapacity, loadFactor);
        }
    
        //将对 HashSet 的迭代转换为对 HashMap 的 Key 值的迭代
        public Iterator<E> iterator() {
            return map.keySet().iterator();
        }
    
        //获取集合中的元素数量
        public int size() {
            return map.size();
        }
    
        //判断集合是否为空
        public boolean isEmpty() {
            return map.isEmpty();
        }
    
        //判断集合是否包含指定元素
        public boolean contains(Object o) {
            return map.containsKey(o);
        }
    
        //如果 HashSet 中不包含元素 e,则添加该元素,并返回 true
        //如果 HashSet 中包含元素 e,则不会影响 HashSet ,并返回 false
        //该方法将向 HashSet 添加元素 e 的操作转换为向 HashMap 添加键值对
        //如果 HashMap 中包含 key 值与 e 相等的结点(hashCode() 方法返回值相等,通过 equals() 方法比较也返回 true)
        //则新添加的结点的 value 会覆盖原有数据,但 key 不会有所改变
        //因此如果向 HashSet 添加一个已存在的元素时,元素不会被存入 HashMap 中
        //从而实现了 HashSet 元素不重复的特征
        public boolean add(E e) {
            return map.put(e, PRESENT)==null;
        }
    
        //移除集合中的元素 o
        //如果集合不包含元素 o,则返回 false
        public boolean remove(Object o) {
            return map.remove(o)==PRESENT;
        }
    
        //清空集合中的元素
        public void clear() {
            map.clear();
        }
    
        @SuppressWarnings("unchecked")
        public Object clone() {
            try {
                HashSet<E> newSet = (HashSet<E>) super.clone();
                newSet.map = (HashMap<E, Object>) map.clone();
                return newSet;
            } catch (CloneNotSupportedException e) {
                throw new InternalError(e);
            }
        }
    
        private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException {
            // Write out any hidden serialization magic
            s.defaultWriteObject();
    
            // Write out HashMap capacity and load factor
            s.writeInt(map.capacity());
            s.writeFloat(map.loadFactor());
    
            // Write out size
            s.writeInt(map.size());
    
            // Write out all elements in the proper order.
            for (E e : map.keySet())
                s.writeObject(e);
        }
    
        /**
         * Reconstitute the <tt>HashSet</tt> instance from a stream (that is,
         * deserialize it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            // Read in any hidden serialization magic
            s.defaultReadObject();
    
            // Read capacity and verify non-negative.
            int capacity = s.readInt();
            if (capacity < 0) {
                throw new InvalidObjectException("Illegal capacity: " +
                                                 capacity);
            }
    
            // Read load factor and verify positive and non NaN.
            float loadFactor = s.readFloat();
            if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
                throw new InvalidObjectException("Illegal load factor: " +
                                                 loadFactor);
            }
    
            // Read size and verify non-negative.
            int size = s.readInt();
            if (size < 0) {
                throw new InvalidObjectException("Illegal size: " +
                                                 size);
            }
    
            // Set the capacity according to the size and load factor ensuring that
            // the HashMap is at least 25% full but clamping to maximum capacity.
            capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
                    HashMap.MAXIMUM_CAPACITY);
    
            // Create backing HashMap
            map = (((HashSet<?>)this) instanceof LinkedHashSet ?
                   new LinkedHashMap<E,Object>(capacity, loadFactor) :
                   new HashMap<E,Object>(capacity, loadFactor));
    
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                @SuppressWarnings("unchecked")
                    E e = (E) s.readObject();
                map.put(e, PRESENT);
            }
        }

        //为了并行遍历数据源中的元素而设计的迭代器
        public Spliterator<E> spliterator() {
            return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
        }
        
    }

LinkedHashSet

  • 想要理解LinkedHashSet,则需要对 HashMap 、HashSet 和 LinkedHashMap 的源码有所了解,因为 LinkedHashSet 的内部实现都是来自于这三个容器类,其内部源码十分简单,简单到它只有一个成员变量、四个构造函数、一个 Set 接口的方法。

  • LinkedHashSet的所有源码

    public class LinkedHashSet<E>
        extends HashSet<E>
        implements Set<E>, Cloneable, java.io.Serializable {
    
        //序列化ID
        private static final long serialVersionUID = -2851667679971038690L;
    
        //自定义初始容量与装载因子
        public LinkedHashSet(int initialCapacity, float loadFactor) {
            super(initialCapacity, loadFactor, true);
        }
    
        //自定义初始容量
        public LinkedHashSet(int initialCapacity) {
            super(initialCapacity, .75f, true);
        }
    
        //使用默认的初始容量以及装载因子
        public LinkedHashSet() {
            super(16, .75f, true);
        }
    
        //使用初始数据、默认的初始容量以及装载因子
        public LinkedHashSet(Collection<? extends E> c) {
            super(Math.max(2*c.size(), 11), .75f, true);
            addAll(c);
        }
    
        //并行遍历迭代器
        @Override
        public Spliterator<E> spliterator() {
            return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
        }
    
    }
    
  • LinkedHashSet 继承于 HashSet,而 LinkedHashSet 调用的父类构造函数均是

    private transient HashMap<E,Object> map;
    
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
    
  • 即 LinkedHashSet 底层是依靠 LinkedHashMap 来实现数据存取的,而 LinkedHashMap 继承于 HashMap,在内部自己维护了一条双向链表用于保存元素的插入顺序。

  • 因此使得 LinkedHashSet 也具有了存取有序,元素唯一的特点。

TreeSet

TreeSet是根据二叉树实现的,也就是TreeMap, 放入数据不能重复且不能为null,可以重写compareTo()方法来确定元素大小,从而进行升序排序。TJava中的TreeSet是基于红黑树实现的Set集合。它继承自AbstractSet类,实现了NavigableSet接口,提供了按自然顺序或自定义比较器排序的元素迭代器。
TreeSet提供了一些常用的方法,例如add()、remove()、contains()、size()等方法。这些方法的时间复杂度都是O(log n),其中n是集合中元素的数量。
除了基本的集合操作,TreeSet还提供了一些高级的API,例如descendingSet()、tailSet()、subSet()等方法,它们返回的是一个新的TreeSet对象,但是按照不同的顺序进行排序。
需要注意的是,TreeSet中的元素必须实现Comparable接口或者提供一个Comparator对象来进行比较。如果元素没有实现Comparable接口,那么默认情况下会使用元素的自然顺序进行排序。如果需要自定义排序规则,则需要提供一个Comparator对象来实现比较逻辑。
常用方法:
TreeSet():默认构造函数,创建一个空的TreeSet集合。
TreeSet(Collection<? extends E> c):构造函数,创建一个包含指定集合元素的TreeSet集合。
add(E e):添加一个元素到TreeSet集合中。如果该元素已经存在,则不进行任何操作;否则,将该元素插入到合适的位置以保持有序性。
remove(Object o):删除指定元素,如果该元素存在,则删除并返回true;否则,返回false。
contains(Object o):检查TreeSet集合中是否包含指定元素,如果存在则返回true;否则,返回false。
first():返回TreeSet集合中的最小元素(按自然顺序排列)。如果该集合为空,则返回null。
last():返回TreeSet集合中的最大元素(按自然顺序排列)。如果该集合为空,则返回null。
headSet(E toElement):返回一个视图,包含TreeSet集合中小于指定元素的子集。该方法可以用于截取子集。
tailSet(E fromElement):返回一个视图,包含TreeSet集合中大于等于指定元素的子集。该方法可以用于截取子集。
subSet(E fromElement, E toElement):返回一个视图,包含TreeSet集合中在指定范围内的子集。该方法可以用于截取子集。
iterator():返回一个迭代器,用于遍历TreeSet集合中的元素。迭代器支持add操作,但每次迭代只能修改一次集合。
size():返回TreeSet集合中元素的数量。
isEmpty():检查TreeSet集合是否为空,如果为空则返回true;否则,返回false。
clear():清空TreeSet集合中的所有元素。

Iterator迭代器应对多线程并发修改的fail_fast机制

Iterator迭代器应对多线程并发修改的fail_fast机制是指在迭代过程中,如果集合的结构发生了改变(例如添加、删除元素),则迭代器会抛出ConcurrentModificationException异常。

fail_fast机制是一种避免多线程并发修改时出现不可预知结果的机制。当迭代器检测到集合结构发生改变时,它会立即抛出ConcurrentModificationException异常,而不是继续迭代下去。

这种机制可以保证数据的一致性和正确性,因为在多线程并发修改的情况下,可能会出现数据不一致的情况,例如一个线程正在遍历集合,而另一个线程在同时修改集合,这样就会导致迭代器遍历到不正确的数据。

需要注意的是,fail_fast机制并不是绝对安全的,它只能在一定程度上避免多线程并发修改时出现不可预知的结果。如果需要更加安全的数据操作,可以考虑使用线程安全的集合类,例如CopyOnWriteArrayList、ConcurrentHashMap等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿与禅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值