并发ConcurrentSkipListSet与线程安全Collections.SynchronizedSet()区别

ConcurrentSkipListSet是SortedSet的并发版本,当然TreeSet也可以通过Collections.SynchronizedSet(new TreeSet())实现,这两者有什么区别?

下面这段ChatGPT代码:

import java.util.Collections;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentSkipListSet;

public class ConcurrentSortedSetExample {

    public static void main(String[] args) {
        // 使用 ConcurrentSkipListSet 获得排序并发集合
        SortedSet<String> concurrentSortedSet = new ConcurrentSkipListSet<>();

       
// 用 synchronizedSortedSet 对其进行封装,使其线程安全
        Set<String> synchronizedSet = Collections.synchronizedSet(concurrentSortedSet);

       
// Perform operations on the synchronizedSet
       
// ...

       
// Example usage:
        synchronizedSet.add(
"Element1");
        synchronizedSet.add(
"Element2");

       
// Iterating over the set in a thread-safe manner
        synchronized (synchronizedSet) {
            for (String element : synchronizedSet) {
                System.out.println(element);
            }
        }
    }
}


ConcurrentSkipListSet 是 SortedSet 接口的并发实现。为了使其线程安全,我们使用 Collections.synchronizedSet 对其进行了封装。这将确保对集合的所有操作都是同步的,从而提供线程安全。

已经实现了并发以后,还需要再线程安全吗?线程安全应该是并发的一个子集。线程安全可以通过并发实现,也可以悲观锁实现,同时只能一个线程访问。

ConcurrentSkipListSet
ConcurrentSkipListSet 是一个实现 Set 和 NavigableSet 接口的类,它使用并发跳转列表作为底层数据结构来存储元素。并发跳转列表是一种概率数据结构,由按层次结构排列的多个链接列表组成。并发跳转列表中的元素默认按其自然排序或按集合创建时提供的比较器排序,具体取决于使用哪个构造函数。它有很多优点:

  • ConcurrentSkipListSet 允许多个线程同时对集合安全地执行插入、移除和访问操作。当需要多个线程并发修改集合时,与其他集合接口实现相比,ConcurrentSkipListSet 更受青睐。
  • ConcurrentSkipListSet 不会锁定整个集合,只会锁定部分集合,允许多个线程并行访问。这意味着多个线程可以同时访问集合,而不会相互阻塞,从而提高了性能。
  • ConcurrentSkipListSet 不允许集合中出现空元素。这意味着我们不能在不抛出异常的情况下在 ConcurrentSkipListSet 中插入空值。
  • ConcurrentSkipListSet 保留集合中元素的顺序。这意味着我们可以根据自然排序或所提供的比较器对 ConcurrentSkipListSet 中的元素进行排序遍历。
  • ConcurrentSkipListSet 提供了按排序顺序浏览集合的其他方法,如 first()、last()、lower()、higher() 等。通过这些方法,我们可以访问集合中的第一个或最后一个元素,或查找小于或大于给定元素的元素。

Collections.SynchronizedSet()
Collections.synchronizedSet() 是 Collections 类的一个静态方法,用于返回由指定集合支持的同步或线程安全集合。该方法用于在现有集合(如 HashSet、TreeSet 或 LinkedHashSet)周围创建同步封装。与 ConcurrentSkipListSet 相比,它具有一些优势:

  • SynchronizedSet() 会锁定整个集合,阻塞多个线程的并行访问。这意味着一次只能有一个线程访问集合,从而确保数据一致性并防止并发问题。
  • SynchronizedSet() 允许在集合中插入空元素,但前提是后备集合允许。这意味着,如果后备集允许,我们就可以在 SynchronizedSet() 中插入空值。
  • SynchronizedSet() 不会保留集合中元素的顺序,除非后备集保留了顺序。这意味着 SynchronizedSet() 中元素的顺序取决于后备集的类型。如果使用 HashSet 作为后备集,元素的顺序是未定义的。如果使用 TreeSet 作为后备集,则元素的顺序与元素的自然顺序或所提供的比较器的顺序相同。如果使用 LinkedHashSet 作为后备集,则元素顺序与插入顺序相同。
  • SynchronizedSet() 不提供按排序顺序浏览集合的其他方法,除非后备集提供了这种方法。这意味着我们无法访问集合中的第一个或最后一个元素,也无法找到小于或大于给定元素的元素,除非我们使用树集作为后备集。

ConcurrentSkipListSet 与 Collections.SynchronizedSet() 的区别
以下是 Java 中 ConcurrentSkipListSet 与 Collections.SynchronizedSet() 的一些主要区别:

  • ConcurrentSkipListSet 是一个实现 Set 和 NavigableSet 接口的类,而 SynchronizedSet() 是 Collections 类的一个返回 Set 接口的方法。
  • ConcurrentSkipListSet 使用并发跳转列表作为其底层数据结构,而 SynchronizedSet() 使用任何类型的集合作为其底层数据结构。
  • ConcurrentSkipListSet 允许多个线程并行访问而不会阻塞它们,而 SynchronizedSet() 会阻塞多个线程的并行访问,每次只允许一个线程访问集合。
  • ConcurrentSkipListSet 不允许集合中出现空元素,而 SynchronizedSet() 则允许集合中出现空元素,具体取决于后备集。
  • ConcurrentSkipListSet 会根据自然排序或所提供的比较器保留集合中元素的顺序,而 SynchronizedSet() 则不会保留集合中元素的顺序,除非后备集保留了顺序。
  • ConcurrentSkipListSet 提供了按排序顺序浏览集合的其他方法,如第一、最后、较低、较高等,而 SynchronizedSet() 不提供按排序顺序浏览集合的其他方法,除非后备集提供了这些方法。
  • ConcurrentSkipListSet 迭代器是弱一致性的,即使在构建迭代器后修改了集合,也不会抛出 ConcurrentModificationException 异常。相反,SynchronizedSet() 并不保证迭代器在并发修改时不会出错。故障安全或故障快速行为完全取决于支持集合。

在 ConcurrentSkipListSet 和 Collections.SynchronizedSet() 之间做出选择
正如我们所见,ConcurrentSkipListSet 和 Collections.SynchronizedSet() 都是在 Java 中创建同步或线程安全集合的有用方法。下面是关于如何在它们之间做出选择的一些一般性建议:

  • 如果需要创建一个同步集合,多个线程可以并发访问而不会阻塞,那么我们应该使用 ConcurrentSkipListSet。因为这时它允许线程获取集合的不同段上的锁,并同时进行修改。
  • 如果需要创建可由多个线程安全访问且数据一致的同步集,我们应该使用 SynchronizedSet()。因为这时它会为每个线程为读/写操作获取整个对象的锁。

性能上ConcurrentSkipListSet高于SynchronizedSet,因为后者是全局锁,不是对象级别锁。

ConcurrentModificationException
如果一个线程尝试修改它,而另一个线程正在迭代它,ConcurrentSkipListSet则不会抛出。而SynchronizedSet可能会!