首页 > 编程语言 >Java四种同步数据结构对比

Java四种同步数据结构对比

时间:2024-12-19 09:42:58浏览次数:4  
标签:Java getTime ConcurrentSkipListSet end start cost new 数据结构 四种

前言

相信各位在遇到并发场景处理数据时都碰到过该选什么数据结构进行存储的问题,本文就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。

需要注意的是,如果我们的数据是一个对象类型,且需要排序的字段有重复值,那我们就要慎重考虑 ConcurrentSkipListSet 了,(如数据库查询包含时间字段的场景),因为mysql5.7使用堆排序对排序性能进行了优化,故每次取的相同值的顺序可能不一样。而当我们使用跳表进行存储时,在初始化时需要指定排序规则,即先对重复字段进行排序,再对不重复字段进行排序,显然,这又是一个不稳定排序,两次不稳定排序就可能导致分页场景下,前后两页出现多个重复值的情况,这样的结果显然是不友好的。

标签:Java,getTime,ConcurrentSkipListSet,end,start,cost,new,数据结构,四种
From: https://www.cnblogs.com/jwcat313/p/18616247

相关文章

  • [java]高级技术
    单元测试概述:针对最小的功能单元(方法),编写测试代码对其进行正确性测试手动测试的问题只能在main方法中编写测试代码无法实现自动化测试,一个方法测试失败,会影响其他方法的测试没有测试报告,需要自己观察测试结果Junit框架用来对方法进行测试,第三方公司开源......
  • java位运算实现加减乘除
    纯用位运算实现加减乘除,涉及一些基础的位运算知识,代码注释里都已经写清楚。publicclassBitOperationCalculate{publicintadd(inta,intb){//a+b=(a^b)+(a&b)<<1=a`+b`=(a`^b`)+(a`&b`)<<1直到b`为0,此时的a就是结果//a`=a^b(a异或b)b`=(a&b)<......
  • Java学习,查找数组重复元素
    Java中查找数组中的重复元素可以通过多种方法实现,包括使用额外的数据结构(如 HashSet)来跟踪已经遇到的元素,或者使用嵌套循环来比较数组中的每一对元素。使用 HashSet查找:publicclassFindDuplicates{  publicstaticvoidmain(String[]args){    int[]......
  • Java学习,删除数组元素
    Java中数组的长度是固定的,这意味着不能,直接从一个数组中删除元素并期望数组自动调整其大小。可以通过几种方式,来实现删除数组元素的效果。创建一个新数组:publicclassRemoveArrayElement{  publicstaticvoidmain(String[]args){    int[]array={1,2......
  • 浅谈Java注解之Component
    一、Component的介绍@Component是Spring框架中的一个注解,用于将一个类标识为Spring容器中的一个组件,通常用于定义一个服务、工具类或者帮助类。这个注解会告诉Spring框架这个类需要被纳入Spring的IoC(控制反转)容器进行管理。二、Component的特点1、自动注册:被@Component注解......
  • 浅谈Java注解之Autowired
    一、Autowired的介绍@Autowired是Spring框架中的一个注解(Annotation),用于实现依赖注入(DependencyInjection,DI)。它告诉Spring容器在创建bean的时候,自动注入相关的依赖。二、Autowired的特点1、自动注入:@Autowired允许Spring容器在运行时自动将bean的依赖项注入到bean中。......
  • 浅谈Java注解之Builder
    一、Builder的介绍@Builder是Lombok库提供的一个注解,用于自动生成建造者模式(BuilderPattern)所需的代码。建造者模式是一种设计模式,用于创建复杂对象,它将对象的构造与表示分离,使得同样的构造过程可以创建不同的表示。通过@Builder注解,可以简化对象的构建过程,避免手动编写大量......
  • Java学习,数组反转
    Java反转数组,既将数组中的元素顺序颠倒,可以通过创建一个新的数组来存储反转后的元素,或者原地(in-place)反转数组,即不使用额外的数组空间。使用新数组创建一个新的数组,并按照原数组的反向顺序将元素复制到新数组中:publicclassReverseArrayExample1{  publicstaticvo......
  • 2024实测验证可用的股票数据接口集合.:python、JavaScript 、JAVA等实例代码演示教你如
    实测可用的股票数据接口,可以直接点击在浏览器中验证:沪深两市股票列表API接口链接(可点击验证):https://api.mairui.club/hslt/list/b997d4403688d5e66a【实时数据接口】沪深两市实时交易数据接口API接口链接(可点击验证):https://api.mairui.club/hsrl/ssjy/000001/b997d4403......
  • java集合框架的详细学习
     集合框架和数组的区别为什么引入集合概念使用数组具有局限性:是一种固定大小的数据结构,其元素类型和数量在创建时就已经确定,并且无法更改,不使用就浪费了。为了解决数组的局限性,引入容器类的概念。容器可以根据需要动态地增加或减少元素。此外,集合框架还提供了丰富的操作方......