JUC并发编程基础学习之Set集合线程安全问题

前言

上一篇博客我们发现并发情况下使用ArrayList线程不安全,那么Set集合在多线程环境下,是否线程安全呢?后面还会对HashSet的源码进行浅析,这就是今天我们所要学习和讨论的问题!

1.1 测试Set集合是否线程安全

1. 首轮Set集合安全测试
1-1 测试代码
package com.kuang.unsafe;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @ClassName SetTest
 * @Description Set安全性测试
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/30
 */
public class SetTest {

    public static void main(String[] args) {
        //获取一个HashSet集合
        Set<String> set = new HashSet<>();
        //模拟多线程环境(首轮测试中我们现将循环次数设置为10)
        for (int i = 1; i <= 10; i++) {
            //使用Lambda表达式创建线程
            new Thread(()->{
               //向set集合中添加随机字符串元素(截取下标0到5的元素)
               set.add(UUID.randomUUID().toString().substring(0,5));
               //打印set集合的输出结果
               System.out.println(set);
               //获取下标为i的字符串,并启动线程
            },String.valueOf(i)).start();
        }
    }

}
1-2 测试结果

在这里插入图片描述

结果没有出现异常,并不符合预期结果!

1-3 结果分析

似乎是我们的循环次数太少,导致没有出现异常,所以我们加大循环次数重新测试,看一看会不会抛出异常!

2. 加大循环次数重新测试
2-1 测试代码
package com.kuang.unsafe;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @ClassName SetTest
 * @Description Set安全性测试
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/30
 */
public class SetTest {

    public static void main(String[] args) {
        //获取一个HashSet集合
        Set<String> set = new HashSet<>();
        //模拟多线程环境(加大循环次数为20)
        for (int i = 1; i <= 20; i++) {
            //使用Lambda表达式创建线程
            new Thread(()->{
               //向set集合中添加随机字符串元素(截取下标0到5的元素)
               set.add(UUID.randomUUID().toString().substring(0,5));
               //打印set集合的输出结果
               System.out.println(set);
               //获取下标为i的字符串,并启动线程
            },String.valueOf(i)).start();
        }
    }

}
2-2 测试结果

在这里插入图片描述

结果抛出ConcurrentModificationException(并发修改异常)!

2-3 测试结论

使用Set集合时也抛出ConcurrentModificationException(并发修改异常),
因此我们可以得出结论: Set集合线程不安全

3.解决方案
  • 使用Collections集合工具类的 synchronizedSet()方法
  • 使用 CopyOnWriteArraySet(写时复制数组Set集合)

1.2 解决Set集合线程不安全问题

1.使用Collections工具类的synchronizedList方法
1-1 源码分析
  • 查看Collections工具类synchronizedSet()方法的相关源码
package java.util;

/**
 *
 * @author  Josh Bloch
 * @author  Neal Gafter
 * @see     Collection
 * @see     Set
 * @see     List
 * @see     Map
 * @since   1.2
 */
//Collections工具类
public class Collections {
    // 抑制默认的构造函数, 确保其非实例化
    private Collections() {
    }
    
  // ...(省略前面部分代码)...
    
    //静态的SynchronizedCollection(同步集合类)
    static class SynchronizedCollection<E> implements Collection<E>, Serializable {
        //连续版本UID
        private static final long serialVersionUID = 3053995032091335093L;
        //返回的Collection集合
        final Collection<E> c;
        //同步互斥量
        final Object mutex;
        
        /**
         * 同步Collection集合构造方法(只有一个参数)
         * @param c Collection集合
         */
        SynchronizedCollection(Collection<E> c) {
            //参数c(Collection集合)不能为空
            this.c = Objects.requireNonNull(c);
            mutex = this;
        }
        
        /**
         * 同步Collection集合构造方法(包含两个参数)
         * @param c Collection集合
         * @param mutex 同步互斥量
         */
        SynchronizedCollection(Collection<E> c, Object mutex) {
            //参数c(Collection集合)不能为空
            this.c = Objects.requireNonNull(c);
            //参数mutex(同步互斥量)不能为空
            this.mutex = Objects.requireNonNull(mutex);
        }
        
        //...(省略后面部分代码)...
        
    }
    
   //  ...(省略中间部分代码)...
    
    /**
    *  synchronizedSet构造方法(只包含一个参数)
     * 如果指定的Set集合是可序列化的,则返回的Set集合将会被序列化
     * @param  <T> set集合中对象的类型
     * @param  s 被包装的在同步Set中的Set集合
     * @return a 指定set集合的同步视图
     */
    public static <T> Set<T> synchronizedSet(Set<T> s) {
        return new SynchronizedSet<>(s);
    }
    
     /**
     * synchronizedSet构造方法(包含两个参数)
     * @param  <T> set集合中对象的类型
     * @param  s 被包装的在同步set中的set集合
     * @param  mutex 同步互斥量
     * @return a 指定set集合的同步视图
     */
    static <T> Set<T> synchronizedSet(Set<T> s, Object mutex) {
        return new SynchronizedSet<>(s, mutex);
    }

    /**
     * @serial include
     */
    //SynchronizedSet(同步Set集合类)
    static class SynchronizedSet<E>
          //继承SynchronizedCollection(同步Collection集合)
          extends SynchronizedCollection<E>
          //实现了Set集合接口
          implements Set<E> {
        //私有静态最终的长整型的serialVersionUID(连续版本UID)
        private static final long serialVersionUID = 487447009682186044L;
        /**
         * 同步Set集合有参构造(一个参数)
         * @param s Set集合
         */
        SynchronizedSet(Set<E> s) {
            super(s);
        }
        /**
         * 同步Set集合有参构造(两个参数)
         * @param s Set集合
         * @param mutex 互斥量/锁
         */
        SynchronizedSet(Set<E> s, Object mutex) {
            super(s, mutex);
        }
        //equals方法 
        public boolean equals(Object o) {
		   //判断对象值是否相等	
            if (this == o)
                return true;
            //同步代码块
            synchronized (mutex) {
                //返回集合和对象值是否相等
                return c.equals(o);
            }
        }
        //hashCode方法
        public int hashCode() {
            synchronized (mutex) {return c.hashCode();}
        }
    }
    
    // ...(省略后面部分代码)...
    
}
1-2 测试代码
package com.kuang.unsafe;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @ClassName SetTest
 * @Description Set安全性测试
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/30
 */
public class SetTest {

    public static void main(String[] args) {
        //获取一个HashSet集合
//        Set<String> set = new HashSet<>();
        //方案1: 使用Collections集合工具类的synchronizedSet方法
        Set<String> set = Collections.synchronizedSet(new HashSet<>());

        //模拟多线程环境
        for (int i = 1; i <= 20; i++) {
            //使用Lambda表达式创建线程
            new Thread(()->{
               //向set集合中添加随机字符串元素(截取下标0到5的元素)
               set.add(UUID.randomUUID().toString().substring(0,5));
               //打印set集合的输出结果
               System.out.println(set);
               //获取下标为i的字符串,并启动线程
            },String.valueOf(i)).start();
        }
    }

}
1-3 测试结果

在这里插入图片描述

结果执行成功,没有出现异常!

2. 使用CopyOnWriteArrayList集合
2-1 CopyOnWriteArraySet简单了解
  • JDK 8 API文档中CopyOnWriteArraySet类的位置

在这里插入图片描述

  • API文档中对CopyOnWriteArraySet类的简单介绍

在这里插入图片描述

2-2 CopyOnWriteArraySet源码分析
  • 查看CopyOnWriteArraySet集合类源码
package java.util.concurrent;
import java.util.Collection;
import java.util.Set;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Predicate;
import java.util.function.Consumer;

/**
 * 此类属于Java集合框架的一员
 * @see CopyOnWriteArrayList
 * @since 1.5
 * @author Doug Lea
 * @param <E> 容纳这个集合中所有元素类型
 */
//CopyOnWriteArraySet(写时复制数组Set集合)
public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements java.io.Serializable {
    //连续版本UID
    private static final long serialVersionUID = 5457747651344034263L;
    //写时复制数组List集合
    private final CopyOnWriteArrayList<E> al;

    /**
     * 创建一个空的Set集合.
     */
    public CopyOnWriteArraySet() {
        //本质上还是创建了一个CopyOnWriteArrayList(写时复制数组List集合)
        al = new CopyOnWriteArrayList<E>();
    }
    
    //...(省略中间部分代码)...

    /**
     * 创建一个包含所有指定集合的元素的Set集合
     * @param c 初始化包含的元素集合
     * @throws 如果指定集合为空,抛出NullPointerException空指针异常
     */
    public CopyOnWriteArraySet(Collection<? extends E> c) {
        //判断c(包含初始化元素的集合)的类是否和CopyOnWriteArraySet(写时发复制数组Set集合)相同
        if (c.getClass() == CopyOnWriteArraySet.class) {
            //抑制不检查警告
            @SuppressWarnings("unchecked")
            //如果相同,将c进行强制类型转换为CopyOnWriteArraySet<E>
            CopyOnWriteArraySet<E> cc = (CopyOnWriteArraySet<E>)c;
            al = new CopyOnWriteArrayList<E>(cc.al);
        }
        else {
            //如果不相同,创建一个新的CopyOnWriteArrayList<E>集合
            al = new CopyOnWriteArrayList<E>();
            //添加所有的缺席(不包含在当前list集合中)元素到al集合中去
            al.addAllAbsent(c);
        }
    }
    
    /**
     * 在指定的集合中,追加所有的不包含在当前list集合中的元素,到list集合的尾部,
     * 通过指定集合的迭代器,按照顺序将它们进行返回
     * @param c 包含被添加到list集合中元素的集合
     * @return 添加元素的数量
     * @throws 如果指定集合为空,抛出NullPointerException空指针异常  
     * @see #addIfAbsent(Object)
     */
    public int addAllAbsent(Collection<? extends E> c) {
        //转换成cs数组
        Object[] cs = c.toArray();
        //判断长度是否为0
        if (cs.length == 0)
            //若为0,返回0
            return 0;
        //获取一个ReentrantLock重入锁
        final ReentrantLock lock = this.lock;
        //首先上锁
        lock.lock();
        try {
            //获取元素数组
            Object[] elements = getArray();
            //元素长度
            int len = elements.length;
            //添加的元素数量
            int added = 0;
            //在cs数组中实例唯一化且紧凑的元素
            for (int i = 0; i < cs.length; ++i) {
                //获取数组中下标为i的元素
                Object e = cs[i];
                //判断
                if (indexOf(e, elements, 0, len) < 0 &&
                    indexOf(e, cs, 0, added) < 0)
                    //如果条件满足,将e(元素)添加到cs
                    cs[added++] = e;
            }
            //判断添加元素数量是否大于0
            if (added > 0) {
                //复制一份原数组中元素(包括原数量+添加数量)到新数组(newElements)中
                Object[] newElements = Arrays.copyOf(elements, len + added);
                //
                System.arraycopy(cs, 0, newElements, len, added);
                //设置数组
                setArray(newElements);
            }
            //返回添加的元素数量
            return added;
        } finally {
            //最终解锁
            lock.unlock();
        }
    }
    
  //  ...(省略中间部分代码)...
    
    /**
     * indexOf的静态版本, 允许重复调用,不需要每次重新获取数组
     * @param o 搜索的元素
     * @param elements 数组
     * @param index 搜索的第一个元素的索引
     * @param fence 过去的最后一个元素的索引 
     * @return 元素的索引下标, 如果元素不存在返回-1
     */
    private static int indexOf(Object o, Object[] elements, int index, int fence) {
        //判断搜索的元素o是否为空
        if (o == null) {
            //若为空,遍历之前的元素(范围为index(第一个元素索引)到fence(过去的最后一个元素索引))
            for (int i = index; i < fence; i++)
                //判断下标为i的元素是否为空
                if (elements[i] == null)
                    //返回索引下标
                    return i;
        } else {
            //如果搜素的元素o不为空
            //遍历之前的元素
            for (int i = index; i < fence; i++)
                //判断搜素的元素o是否等于数组中下标为i的元素
                if (o.equals(elements[i]))
                    //若相等,将下标i进行返回
                    return i;
        }
        //如果搜索的元素不存在,则返回-1
        return -1;
    } 
    
    //...(省略后面部分代码)...
    
}

Set集合使用CopyOnWriteArrayList(写时数组List集合)所有的内部操作,因此它们有相同的基本属性

使用案例

//Handler处理器类
class Handler { void handle(); ... }

//测试类X
class X {
    
    //创建一个泛型为Handler的CopyOnWriteArraySet集合
    private final CopyOnWriteArraySet<Handler> handlers = new CopyOnWriteArraySet<Handler>();

    /**
     * 添加处理器方法
     * @param h Handler处理器
     */
    public void addHandler(Handler h) { handlers.add(h); }

    //长整型的internalState(内部状态)
    private long internalState;

    //使用了synchronized同步锁修饰的changeState(改变状态)方法
    private synchronized void changeState() {
         //"..."表示可变长参数,可以传入任意个该类型参数,简单来说就是个数组
             internalState = ...; 
    }

    //更新
    public void update() {
        //改变状态
        changeState();
        //遍历handlers集合
        for (Handler handler : handlers) {
        	//调用集合子元素handler的handle方法
        	handler.handle();
      }
   }
        
}
2-3 CopyOnWriteArraySet集合的使用
  • 测试代码
package com.kuang.unsafe;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @ClassName SetTest
 * @Description Set安全性测试
 * @Author 狂奔の蜗牛rz
 * @Date 2021/7/30
 */
public class SetTest {

    public static void main(String[] args) {
        //获取一个HashSet集合
//        Set<String> set = new HashSet<>();
        //方案2: 使用CopyOnWriteArraySet(写时复制数组Set集合)
        Set<String> set = new CopyOnWriteArraySet<>();

        //模拟多线程环境
        for (int i = 1; i <= 20; i++) {
            //使用Lambda表达式创建线程
            new Thread(()->{
               //向set集合中添加随机字符串元素(截取下标0到5的元素)
               set.add(UUID.randomUUID().toString().substring(0,5));
               //打印set集合的输出结果
               System.out.println(set);
               //获取下标为i的字符串,并启动线程
            },String.valueOf(i)).start();
        }
    }

}
  • 测试结果

在这里插入图片描述

结果执行成功,没有出现异常!

最后,我们来浅析一下HashSet的源码,HashSet底层也是面试会经常问到的,所以不看过源码,你怎么能说你很懂HashSet?

3. HashSet源码分析
3-1 查看HashSet集合源码
package java.util;

/**
 * <p>该类是Java集合框架中的一员</p>
 * @param <E> 该Set集合维护的元素类型
 * @author  Josh Bloch
 * @author  Neal Gafter
 * @see     Collection
 * @see     Set
 * @see     TreeSet
 * @see     HashMap
 * @since   1.2
 */
//HashSet集合类
public class HashSet<E>
    extends AbstractSet<E> //继承AbstractSet<E>(抽象Set集合)类
    implements Set<E>, Cloneable, java.io.Serializable //实现Set集合接口和序列化接口
{ 
    //静态最终的持续版本UID
    static final long serialVersionUID = -5024744406713321676L;
    //获取一个HashMap对象
    /**
     * 与常用的HashMap中Key-Value(键值对)不同的是,
     * 这里的HashMap的key是E(即Element,表示集合中存放的元素)
     * 这里的Value是Object类(它是所有类的父类,数组也是Object类的子类) 
     */
    //使用transient修饰HashMap(表示序列化对象的时候,这个属性就不会被序列化)
    private transient HashMap<E,Object> map;
    
    //在支持的Map中,和一个object对象相关联的虚拟值(PRESENT, 即已存在对象)
    private static final Object PRESENT = new Object();

    /**
     * 构造一个新的、空的set集合,背后的HashMap实例有默认的初始容量(16)和装载因子(0.75)
     */
    public HashSet() {
        map = new HashMap<>();
    }

    /**
     * 构造有个新的set集合包含指定集合中的元素
     * HashMap被创建时默认装载因子为(0.75),并且初始容量足够包含指定集中合的元素
     * @param c 数组元素被放置在当前Set集合中的Collection集合
     * @throws 如果指定集合为空,则抛出NullPointerException(空指针异常)
     */
    public HashSet(Collection<? extends E> c) {
        //创建一个HashMap(最大值是集合大小/0.75,初始数组大小为16)
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        //添加所有的元素
        addAll(c);
    }

    /**
     * 构造一个新的、空的Set集合,后面的HashMap实例有指定初始容量和指定装载因子
     * @param  initialCapacity  HashMap的初始容量
     * @param  loadFactor       HashMap的装载因子
     * @throws IllegalArgumentException 如果初始容量小于0,或者装载因子为负数, 抛出IllegalArgumentException(不合法的参数异常)
     */
    public HashSet(int initialCapacity, float loadFactor) {
        //创建HashMap对象,并设置其初始容量和装载因子大小
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    /**
     * 构造一个新的、空的Set集合,后面的HashMap实例有指定初始容量和默认装载因子为0.75
     * @param  initialCapacity 哈希表的初始容量
     * @throws 如果初始容量小于0,抛出IllegalArgumentException(非法参数异常)
     */
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

    /**
     * 构建一个新的、空的LinkedHashSet(这个包私有的构造器只被LinkedHashSet使用) The backing
     * 后面的HashMap实例是一个有初始容量和转载因子的LinkedHashMap
     * @param  initialCapacity  HashMap的初始容量
     * @param  loadFactor       HashMap的装载因子
     * @param  dummy 忽略(将此构造器与其他的int和float型构造器区分开)
     * @throws 如果初始容量小于0, 或者装载因子为负数, 抛出IllegalArgumentException(不合法异常)
     */
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        //创建HashMap对象,并设置其初始容量和装载因子大小
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

    /**
     * 返回该Set集合中迭代的元素, 元素未按指定顺序返回
     * @return 该Set集合中迭代的元素
     * @see ConcurrentModificationException(并发修改异常)
     */
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }

    /**
     * 返回set集合中元素的数量(它的基数)
     * @return set集合中的元素数量(它的基数)
     */
    public int size() {
        //map的大小
        return map.size();
    }

    /**
     * 如果set集不包含元素, 则返回值为true
     * @return 布尔型值true或false, 如果set集合不包含元素,则返回true
     */
    public boolean isEmpty() {
        //map为空
        return map.isEmpty();
    }

    /**
     * 如果Set集合中包含指定元素,则返回布尔型值true.
     * 更正式的讲, 如果该Set集合包含一个元素e, 
     * 例如(o==null ? e==null : o.equals(e)), 则返回true
     * 使用三元运算符: 判断o(要测试的元素)是否为空, 若为空, 则e元素不存在, 否则测试元素o的值等于元素e
     * More formally, returns <tt>true</tt> if and only if this set
     * contains an element <tt>e</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.
     *
     * @param o 要测试在该集合中存在的元素
     * @return 布尔型值true或者false 如果包含指定元素, 则返回true
     */
    public boolean contains(Object o) {
        //map中是否包含测试元素的key
        return map.containsKey(o);
    }

    /**
     * 如果指定元素尚未出现, 则将其添加到该Set集合中
     * 更正式的讲, 如果该Set集合不包含元素e2, 添加指定元素e到该Set集合中
     * 例如:(e==null ? e2==null : e.equals(e2)) 使用三元运算符表达式: 
     * 判断e(添加到该集合中元素)是否为空, 若为空, 则e2(集合中未包含元素)为空, 否则e元素值等于e2
     * 如果Set集合已经包含该元素, 该调用使集合保持不变并返回false
     * @param e 被添加到该集合中的元素
     * @return 布尔型值true或者false 如果该Set集合中不包含指定的元素, 则返回true
     */
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

    /**
     * 如果指定元素已存在, 将其从该Set集合中移除
     * Removes the specified element from this set if it is present.
     * 更正式的讲, 移除一个元素e, 例如: (o==null ? e==null : o.equals(e))
     * 使用三元运算符: 如果测试元素o为空, 则元素e为空, 否则测试元素o的值等于元素e
     * 如果该Set集合包含元素(或为等效值, 如果该Set集合因调用结果发生改变)
     * (一旦返回调用, 该Set集合将不能包含元素.)
     * @param o 如果存在, object对象将从该Set集合中移除
     * @return 布尔型值true或false 如果Set集合包含指定元素, 则返回true
     */
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

    /**
     * 移除该Set集合中的所有元素
     * 返回调用后该Set集合将清空
     */
    public void clear() {
        map.clear();
    }

    /**
     * 返回该HashSet实例的浅拷贝: 元素本身没有被克隆
     * @return 该Set集合的浅拷贝
     */
    //抑制未检查警告
    @SuppressWarnings("unchecked")
    public Object clone() {
        try {
            HashSet<E> newSet = (HashSet<E>) super.clone();
            newSet.map = (HashMap<E, Object>) map.clone();
            return newSet;
        //捕获CloneNotSupportedException(克隆不被支持异常)
        } catch (CloneNotSupportedException e) {
            throw new InternalError(e);
        }
    }

   ...(省略后面部分代码,如果想了解更多,请去查阅相关源码)...
       
}
3-2 HashSet的底层是什么?

HashSet的底层实际上是一个HashMap

与常用的HashMapKey-Value(键值对)不同的是:它的keyE,即Element表示集合中存放的元素 ; 而它的ValueObject类它是所有类的父类,数组也是Object类的子类

3-3 HashSet的主要特点是什么?
  • HashSetSet集合接口的实现类,由哈希表支持 (实际上是一个HashMap实例);
  • 它不保证set集合的迭代顺序,特别是,它不能保证顺序会随时间保持不变
  • HashSet允许元素为空,对基础操作 (例如add, remove, contains和size方法) 提供持续时间表现;
3-4 为什么说不能设置初始容量太高(或者装载因子太低)?

假设哈希函数正确的在桶中分散元素, 那么遍历该Set集合所需的时间和
HashSet实例大小(元素的数量)加上支持的HashMap实例的“容量”(桶的数量)之和成正比例;
因此,如果重视迭代性能,不设置初始容量太高(或者装载因子太低)是至关重要的

3-5 HashSet是同步的吗?

HashSet实现类是不同步的

如果多个线程并发访问一个HashSet,并且至少有一个线程修改了该Set集合,它必须从外部同步; 这通常在一些对象上同步来实现,自然的封装了Set集合.

3-6 对象不存在怎么实现同步?

如果object对象不存在, Set集合应该使用Collections集合工具类的synchronizedSet方法进行包装,最好在创建时完成,以防意外的非同步访问Set集合:

Set s = Collections.synchronizedSet(new HashSet(...));
3-7 迭代器的快速失效行为是指什么?

通过HashSet的迭代器方法返回的迭代器们是fail-fast(快速失效)的; 在迭代器创建后,如果Set集合在任何时间被修改, 使用任何方法(除通过迭代器自己的remove方法外),迭代器将抛出一个ConcurrentModificationException(并发修改异常);

因此,在面对并发修改时,迭代器更加的快速、干净的失效, 而不是在将来的不确定时间,冒着任意的, 不确定行为的风险.

fail-fast(快速失效)行为会力所能及的抛出ConcurrentModificationException(并发修改异常), 因此为了编写程序的正确性而依赖这个异常是错误的;迭代器的fail-fast(快速失效)行为应该只用于去检测错误

到这里,今天的有关Set集合线程安全的学习就结束了,欢迎大家学习和讨论!

参考视频链接:https://www.bilibili.com/video/BV1B7411L7tE (B站UP主遇见狂神说的JUC并发编程基础)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

狂奔の蜗牛rz

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

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

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

打赏作者

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

抵扣说明:

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

余额充值