前言
相信各位在遇到并发场景处理数据时都碰到过该选什么数据结构进行存储的问题,本文就Java中常用的四种数据结构进行简单的讨论
正文
ConcurrentLinkedQueue
ConcurrentLinkedQueue 是 java.util.concurrent(JUC)包下的一个线程安全的队列实现。基于非阻塞算法(Michael-Scott 非阻塞算法的一种变体),这意味着 ConcurrentLinkedQueue 不再使用传统的锁机制来保护数据安全,而是依靠底层原子的操作(如 CAS)来实现。
Collections.synchronizedList
JDK提供了一个Collections.synchronizedList静态方法将一个非线程安全的List(并不仅限ArrayList)包装为线程安全的List。迭代操作必须加锁,可以使用synchronized关键字修饰;synchronized持有的监视器对象必须是synchronized (list),即包装后的list,使用其他对象如synchronized (new Object())会使add,remove等方法与迭代方法使用的锁不一致,无法实现完全的线程安全性。
CopyOnWriteArrayList
CopyOnWriteArrayList 是线程安全的,可以在多线程环境下使用。CopyOnWriteArrayList 遵循写时复制的原则,每当对列表进行修改(例如添加、删除或更改元素)时,都会创建列表的一个新副本,这个新副本会替换旧的列表,而对旧列表的所有读取操作仍然可以继续。
由于在修改时创建了新的副本,所以读取操作不需要锁定。这使得在多读取者和少写入者的情况下读取操作非常高效。当然,由于每次写操作都会创建一个新的数组副本,所以会增加存储和时间的开销。如果写操作非常频繁,性能会受到影响。
ConcurrentSkipListSet
ConcurrentSkipListSet是线程安全的有序的集合,适用于高并发的场景。
ConcurrentSkipListSet和TreeSet,它们虽然都是有序的集合。但是,第一,它们的线程安全机制不同,TreeSet是非线程安全的,而ConcurrentSkipListSet是线程安全的。第二,ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,而TreeSet是通过TreeMap实现的。
下面是四种数据结构在1000w数据量下进行插入操作,以及10w数据量的遍历读取的性能对比。(其中,由于CopyOnWriteArrayList插入性能较低,故只使用了10w数据量进行插入操作,但依然可见写时复制带来的性能消耗)
AtomicLong sum = new AtomicLong();
long start = 0;
long end = 0;
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
List<Integer> list1 = Collections.synchronizedList(new ArrayList<>());
List<Integer> list2 = new CopyOnWriteArrayList<>();
ConcurrentSkipListSet<Integer> set = new ConcurrentSkipListSet<>();
start = new Date().getTime();
IntStream.rangeClosed(1, 10000000).parallel().forEach(queue::offer);// ConcurrentLinkedQueue
end = new Date().getTime();
System.out.println("ConcurrentLinkedQueue write cost time : " + (end - start) / 1000.0 + "秒");
start = new Date().getTime();
IntStream.rangeClosed(1, 10000000).parallel().forEach(list1::add);// synchronizedList
end = new Date().getTime();
System.out.println("synchronizedList write cost time : " + (end - start) / 1000.0 + "秒");
start = new Date().getTime();
IntStream.rangeClosed(1, 100000).parallel().forEach(list2::add);// CopyOnWriteArrayList
end = new Date().getTime();
System.out.println("CopyOnWriteArrayList write cost time : " + (end - start) / 1000.0 + "秒");
start = new Date().getTime();
IntStream.rangeClosed(1, 10000000).parallel().forEach(set::add);// ConcurrentSkipListSet
end = new Date().getTime();
System.out.println("ConcurrentSkipListSet write cost time : " + (end - start) / 1000.0 + "秒");
System.out.println("***************************");
start = new Date().getTime();
queue.stream().limit(100000).forEach(sum::addAndGet);// ConcurrentSkipListSet
end = new Date().getTime();
System.out.println("ConcurrentLinkedQueue read cost time : " + (end - start) / 1000.0 + "秒");
start = new Date().getTime();
queue.stream().limit(100000).forEach(sum::addAndGet);// ConcurrentSkipListSet
end = new Date().getTime();
System.out.println("synchronizedList read cost time : " + (end - start) / 1000.0 + "秒");
start = new Date().getTime();
queue.stream().limit(100000).forEach(sum::addAndGet);// ConcurrentSkipListSet
end = new Date().getTime();
System.out.println("CopyOnWriteArrayList read cost time : " + (end - start) / 1000.0 + "秒");
start = new Date().getTime();
queue.stream().limit(100000).forEach(sum::addAndGet);// ConcurrentSkipListSet
end = new Date().getTime();
System.out.println("ConcurrentSkipListSet read cost time : " + (end - start) / 1000.0 + "秒");
ConcurrentLinkedQueue write cost time : 2.339秒
synchronizedList write cost time : 4.125秒
CopyOnWriteArrayList write cost time : 4.828秒
ConcurrentSkipListSet write cost time : 0.214秒
***************************
ConcurrentLinkedQueue read cost time : 0.004秒
synchronizedList read cost time : 0.002秒
CopyOnWriteArrayList read cost time : 0.003秒
ConcurrentSkipListSet read cost time : 0.001秒
从上面的演示程序可以看到,基于跳表结构实现的 ConcurrentSkipListSet 具有最高的插入和遍历读取性能,所以,当我们的数据具有需要排序的需求时,我们可以使用该数据结构进行存储。
而无界非阻塞队列 ConcurrentLinkedQueue 具有第二高的插入性能,但遍历读取性能最低,故在多写少读的场景下我们可以选择该数据结构,但由于其无界的特性,我们不应该无限制的进行插入操作,这样会给内存带来较大负担,最好是我们对数据量有一个良好估计的情况下进行使用。
对于 CopyOnWriteArrayList ,我们可以看到它的插入性能和遍历查询性能悬殊较大,但正是因为牺牲了写入性能,才能确保它在高并发下的安全性,故其只适合多读少写的场景。
最后,Collections.synchronizedList 的读写性能都是中间水平,那我们应该什么时候使用呢,当我们想要使用非线程安全的数组列表时(如ArrayList和LinkedList),我们就可以使用Collections.synchronizedList对其进行包装,将其转换为线程安全的列表。但还要注意的是,使用 Iterator 遍历列表时,Collections.synchronizedList 可能发生错误。官方文档给出的解决办法是:我们在迭代器遍历返回列表时,增加手动同步处理,即使用 synchronized 关键字锁住这个 list。
标签:Java,getTime,ConcurrentSkipListSet,end,start,cost,new,数据结构,四种 From: https://www.cnblogs.com/jwcat313/p/18616247需要注意的是,如果我们的数据是一个对象类型,且需要排序的字段有重复值,那我们就要慎重考虑 ConcurrentSkipListSet 了,(如数据库查询包含时间字段的场景),因为mysql5.7使用堆排序对排序性能进行了优化,故每次取的相同值的顺序可能不一样。而当我们使用跳表进行存储时,在初始化时需要指定排序规则,即先对重复字段进行排序,再对不重复字段进行排序,显然,这又是一个不稳定排序,两次不稳定排序就可能导致分页场景下,前后两页出现多个重复值的情况,这样的结果显然是不友好的。