当前位置: 首页 > 科技观察

当LinkedList不是列表的时候,再快的兔子也追不上!

时间:2023-03-15 15:25:04 科技观察

ArrayList和LinkedList有什么区别?这种侮辱性的问题默认把两人限制在同一个场景,连八股文都不算。一旦被问到这种问题,也证明面试基本就毁了——面试官实在找不到其他问题跟你交流了。你结束了。但是当我们仔细看LinkedList的类定义时,会发现它并没有像ArrayList那样纯粹的列表精神。publicclassArrayListextendsAbstractListimplementsList,RandomAccess,Cloneable,java.io.Serializable//VSpublicclassLinkedListextendsAbstractSequentialListimplementsList,Deque,Cloneable,java.io.SerializableLinkedList可以作为一个普通的链表使用,它也是一个双端队列。双向链表听起来很唬人,它是一个既可以当队列又可以当栈的存在。有了这个双Buff的叠加,LinkedList的应用场景比ArrayList要丰富很多。LinkedList除了可以做最简单的LRU缓存,刷题的时候也是满满的正能量。关于Deque-likeAPI,xjjdog之前有专门的文章介绍这些爆款方法。ConcurrentLinkedQueue王,一个阻塞双向队列,它的基本运行方式是:(3[基础]x2[异常及返回值]+4[阻塞加超时])x3[队头队尾]=5x2x3=30,完全有有30种方法。看完上面的文章,这30种方法就可以轻松掌握了。但是我们今天要讲的重点之一就是使用Deque来实现更快的延迟队列。延迟队列如果你想让一些数据延迟一段时间再处理,或者在一定时间内按组进行一些计算,延迟队列无疑是非常适合的。在Java的Concurrent包中,DelayQueue悄无声息地躺着。只要你的数据实现了Delayed接口,你就可以放心大胆的往里面塞。不幸的是,DelayQueue的底层存储使用了PriorityQueue。PriorityQueue是在堆上实现的,offer和poll数据的时间复杂度是O(logN)。这意味着当DelayQueue中有更多数据时,它的性能会下降。除了分片数据和使用多个DelayQueues来完成工作,我们还有没有更快的方法呢?比如把PriorityQueue换成LinkedList?这取决于场景。如果往DelayQueue中添加数据,与当前添加时间相关,可以用LinkedList代替PriorityQueue。关键代码要理解DelayQueue的运行原理,我们可能需要先看一下Delayed接口。任何想要存储在DelayQueue中的数据都需要实现这个接口。其中getDelay是判断当前数据是否超时的方法。而compareTo被PriorityQueue用于排序。如果我们当前正在填充数据,则不需要compareTo方法。根据上面的思路,我们把DelayQueue的代码复制过来,只保留关键代码,如下。publiclonggetDelay(@NotNullTimeUnitunit){returnMAX_CACHE_DUAL+this.enqueueTimeNs-System.nanoTime();}publicintcompareTo(@NotNullDelayedo){longd=getDelay(TimeUnit.NANOSECONDS)-o.getDelay(TimeUnit.纳秒);返回(d==0)?0:(d<0?-1:1);}主要方法是一个轻量级延迟队列。一旦使用了LinkedList,它的enqueue和dequeue方法,都变成了O(1)的时间复杂度。当延迟队列中的数据增加时,时间复杂度也可以保持不变。可以说速度之快,连兔子都追不上。一般在java中,put和take方法代表阻塞方法。比如对于take方法,我们可以放心的将其阻塞在某个线程上,而不用担心浪费太多资源。finalThreadthread=Thread.currentThread();while(!this.close&&!thread.isInterrupted()){Datadata=q.take();}这一切都是Condition完成的,与wait和notify相关效果是一样的。当我们通过put方法向队列中添加新数据时,会通过signal方法通知等待线程去获取数据。同样,如果take方法发现队列为空,就会进入等待状态。如果有数据,不会立即取出,因为数据还没有过期。比如最旧的数据还有5秒过期,代码也会处理这种情况,会尝试awaitNanos对应的时间。这样,我们就可以使用这种简单的轮询方式来实现延迟队列,而不需要单独的线程来检测队列中的数据。提高take方法的效率,但这还不够。当数据量比较大时,队列中可能有多条数据已经过期。如果我们用take方式一个一个获取,效率自然没有批量获取高。drainTo方法可以将过期数据转移到其他集合中,但它不是阻塞方法。我们可以先用take阻塞线程,然后批量取所有数据。下面的代码将一次获取100条数据作为一个批次。finalDatatakeItem=q.take();finalListbuckets=newArrayList<>(100);q.drainTo(buckets,99);buckets.add(takeItem);End其实我们的一项业务,当使用LinkedList代替PriorityQueue进行批处理,CPU占用率直接降低1/3。Deque是xjjdog最喜欢的接口之一。每当你使用offerFirst、offerLast等精准API进行操作时,你都会体验到多巴胺带来的快乐。作为它的儿子,LinkedList自然具备了所有这些功能。当我们使用并发包中的基础API来封装这些简单的工具时,它们就会焕发出新的活力。作者简介:薇薇小姐姐(xjjdog),一个不允许程序员走弯路的公众号。专注于基础架构和Linux。十年架构,每天百亿流量,与你探讨高并发世界,给你不一样的滋味。