关于Set集合不安全问题的源码和解决分析

Java中的Set集合在并发修改下可能抛出`ConcurrentModificationException`。解决方案包括使用`Collections.synchronizedSet`创建线程安全的Set,或者通过`CopyOnWriteArraySet`实现读写复制的安全操作。HashSet的底层实现是HashMap,利用key的唯一性存储数据。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

   java中的Set集合和List集合一样都继承Collection的接口,Set集合是一个不包含重复元素的集合,主要包含三种存放数据类型的变量,分别是HashSet、LinkedHashSet和TreeSet,和List集合一样,Set集合也是不安全类,在并发情况修改下会报:java.util.ConcurrentModificationException(并发修改异常);

/**
 * Adds the specified element to this set if it is not already present.
 * More formally, adds the specified element <tt>e</tt> to this set if
 * this set contains no element <tt>e2</tt> such that
 * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
 * If this set already contains the element, the call leaves the set
 * unchanged and returns <tt>false</tt>.
 *
 * @param e element to be added to this set
 * @return <tt>true</tt> if this set did not already contain the specified
 * element
 */
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

查看HashSet的源码可知,HashSet并不是一个线程安全的方法,这里还发现了一个有趣的事情,就是HashSet的地城是通过HashMap来实现,这个最后再说。因为HashSet为线程不安全的方法,在并发情况下就会产生java.util.ConcurrentModificationException,之所以add方法会是线程不安全方法,我猜想应该和List集合是一样的,为了提高在对线程安全要求不苛刻情况下,集合处理数据效率。

解决方案:

1.通过使用Collections提供的synchronizedSet方法来获取Set集合:

/**
 * Returns a synchronized (thread-safe) set backed by the specified
 * set.  In order to guarantee serial access, it is critical that
 * <strong>all</strong> access to the backing set is accomplished
 * through the returned set.<p>
 *
 * It is imperative that the user manually synchronize on the returned
 * set when iterating over it:
 * <pre>
 *  Set s = Collections.synchronizedSet(new HashSet());
 *      ...
 *  synchronized (s) {
 *      Iterator i = s.iterator(); // Must be in the synchronized block
 *      while (i.hasNext())
 *          foo(i.next());
 *  }
 * </pre>
 * Failure to follow this advice may result in non-deterministic behavior.
 *
 * <p>The returned set will be serializable if the specified set is
 * serializable.
 *
 * @param  <T> the class of the objects in the set
 * @param  s the set to be "wrapped" in a synchronized set.
 * @return a synchronized view of the specified set.
 */
public static <T> Set<T> synchronizedSet(Set<T> s) {
    return new SynchronizedSet<>(s);
}

我们通过源码可以看到,synchronizedSet方法是生成一个叫SynchronizedSet集合类。

/**
 * @serial include
 */
static class SynchronizedSet<E>
      extends SynchronizedCollection<E>
      implements Set<E> {
    private static final long serialVersionUID = 487447009682186044L;

    SynchronizedSet(Set<E> s) {
        super(s);
    }
    SynchronizedSet(Set<E> s, Object mutex) {
        super(s, mutex);
    }

    public boolean equals(Object o) {
        if (this == o)
            return true;
        synchronized (mutex) {return c.equals(o);}
    }
    public int hashCode() {
        synchronized (mutex) {return c.hashCode();}
    }
}

但是我们会奇怪的发现这个类它没有最集合操作的方法,但是它继承了一个叫SynchronizedCollection。这个父类才是具体对集合操作的类,也是真正实现线程安全的类。

public boolean add(E e) {
    synchronized (mutex) {return c.add(e);}
}

以add方法为例,通过synchronized来实现线程操作的安全。

2.通过通过读写复制的方法来创建Set集合:

通过new CopyOnWriteArraySet方法来创建Set集合,这种方法之所以能实现线程安全,和List的读写复制的原理是一样的,我们通过底层源码可知,它就是复用List的CopyOnWriteArrayList类。

/**
 * Creates an empty set.
 */
public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}

补充:

之前谈到的HashSet的底层其实是通过HashMap实现。

/**
 * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
 * default initial capacity (16) and load factor (0.75).
 */
public HashSet() {
    map = new HashMap<>();
}

从HashSet的构造方法来看,我们new一个HashSet方法实际上是new了一个HashMap,在我们的印象里Set集合相比较List集合来说,Set集合不允许有重复,其他和List差不多,跟Map差很多,但是HashSet却使用的是HashMap实现的。

/**
 * Adds the specified element to this set if it is not already present.
 * More formally, adds the specified element <tt>e</tt> to this set if
 * this set contains no element <tt>e2</tt> such that
 * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
 * If this set already contains the element, the call leaves the set
 * unchanged and returns <tt>false</tt>.
 *
 * @param e element to be added to this set
 * @return <tt>true</tt> if this set did not already contain the specified
 * element
 */
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

我们可以从add方法来看,其实HashSet只是利用了HashMap的key唯一这个特性,也就是只用了它一半,使用key来存储数据,而value统一存储成了PRESENT,而PRESENT就是一个Object的对象。

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值