首页 > 其他分享 >看到就是赚到!当 LinkedList 不是列表时,速度快的兔子都追不上!

看到就是赚到!当 LinkedList 不是列表时,速度快的兔子都追不上!

时间:2022-10-17 12:31:28浏览次数:54  
标签:方法 LinkedList 追不上 队列 lock 列表 数据 final

作者:小姐姐味道

ArrayList和LinkedList有什么区别?

这种侮辱人的问题,默认就把这两者限定在了同一个场景之中,它甚至连八股文都算不上。

一旦你被问到这种问题,也证明面试基本上泡汤了--面试官已经实在是找不到其他问题与你交流了。

你Over了。

但当我们细看一下LinkedList的class定义,就会发现,它并不像是ArrayList的那样具有纯洁的列表精神。

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable

//VS

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList除了能够当作普通的列表,它还是一个Deque。双向链表,听着就比较唬人,这就是一个既能当做队列、又能当做栈的存在。

有了这种双重Buff的叠加,LinkedList的应用场景比ArrayList丰富的多。除了能做最简单的LRU缓存,LinkedList在刷题的时候也是充满了正能量。

关于类似Deque的API,xjjdog以前有专门的文章来介绍这些爆炸性的方法。

《带你见识一下,JAVA中的方法爆炸!》

王者ConcurrentLinkedQueue,一个阻塞的双向队列,它的基本操作方法有:(3[基本]x2[异常与返回值]+4[阻塞加超时])x3[队头队尾]=5x2x3=30,足足有30个方法。看完上面的文章,这30个方法可以很快手到擒来。

不过我们今天要聊的一个重点,是使用Deque来实现更快的延迟队列。

延迟队列

如果你想要某些数据延迟一段时间再进行处理,或者要再某段时间内按照分组进行一些计算,那延迟队列无疑是非常合适的。

在Java的Concurrent包里,就静悄悄的躺着DelayQueue。只要你的数据实现了Delayed接口,那么就可以放心大胆的把它们往里面塞。

可惜的是,DelayQueue的底层存储,使用的是PriorityQueue。

PriorityQueue是堆实现的,offer和poll数据的时间复杂度是O(logN)。这就意味着,当DelayQueue中的数据比较多的时候,它的性能就会下降。

除了把数据分片,使用多个DelayQueue来完成工作,我们有没有速度更快的方法?比如把PriorityQueue使用LinkedList来替换?

这要看场景。

如果你向DelayQueue中添加数据,是与当前添加的时间相关的,那完全可以使用LinkedList来替换PriorityQueue。

关键代码

要了解DelayQueue的运行原理,我们可以需要先看一下Delayed接口。任何想要存储到DelayQueue中的数据,都需要实现这个接口。

其中,getDelay就是用来判断当前数据是否超时的方法。而compareTo,则是PriorityQueue用来排序的,如果我们是按照当前塞入数据的,则compareTo方法就不是必要的。

public long getDelay(@NotNull {
return MAX_CACHE_DUAL + this.enqueueTimeNs - System.nanoTime();
}
public int compareTo(@NotNull {
long d = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return (d == 0) ? 0 : (d < 0 ? -1 : 1);
}

按照以上的思路,我们把DelayQueue的代码拷贝一份,仅保留关键代码,如下。

public class LightweightDelayedQueue<E extends Delayed> {
private final transient ReentrantLock lock = new ReentrantLock();
private final LinkedList<E> q = new LinkedList<>();
private final Condition available = lock.newCondition();
private Thread leader;

public void put(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
if (q.peek() == e) {
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
}

public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (; ; ) {
E first = q.peek();
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0L)
return q.poll();
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}

public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.size();
} finally {
lock.unlock();
}
}

public int drainTo(Collection<? super E> c, int {
Objects.requireNonNull(c);
if (c == this)
throw new IllegalArgumentException();
if (maxElements <= 0)
return 0;
final ReentrantLock lock = this.lock;
lock.lock();
try {
int n = 0;
for (E first;
n < maxElements
&& (first = q.peek()) != null
&& first.getDelay(NANOSECONDS) <= 0; ) {
c.add(first); // In this order, in case add() throws.
q.poll();
++n;
}
return n;
} finally

主要方法

轻量级的延迟队列,如果一旦采用了LinkedList,那么它的入队、出队方法,就都变成了O(1)的时间复杂度。在延迟队列中的数据增加时,时间复杂度也能维持不变,可以说是速度快的连兔子都追不上了。

一般,在java中,put和take方法,都是代表阻塞性方法。比如,take方法,我们可以将其安全的阻塞在某个线程上,而不用担心太多的资源浪费。

final Thread thread = Thread.currentThread();
while (!this.close && !thread.isInterrupted()) {
Data data =

这一切都是Condition办到的,它和wait、notify的作用是一样的。

当我们通过put方法添加新的数据到队列中,会通过signal方法,来通知等待的线程获取数据。

相同的,如果take方法发现队列中的数据为空,它将进入等待状态。如果有数据,也并不是马上把这些数据取出来,因为数据还没到期。比如最老的数据还剩下5秒才到期,代码也对这种情况进行了处理,它会尝试​​awaitNanos​​对应的时间。

这样,我们就可以使用这种简单的轮询方式来实现延迟队列,而不需要单独的线程去检测队列中的数据。

增加take方法的效率

但是这样还不够。

当数据量比较大的时候,队列的数据可能有多条已经到期。如果我们通过take方法来一条一条获取的话,效率自然不如批量获取高。

drainTo方法,可以一股脑的把到期的数据转移到其他的集合中,但它并不是一个阻塞性的方法。

我们可以先使用take来阻塞线程,然后再批量取出所有数据。

下面代码,会一次性获取100条数据作为一个批量。

final Data takeItem = q.take();
final List<Data> buckets = new ArrayList<>(100);
q.drainTo(buckets, 99);
buckets.add(takeItem);

End

实际上,我们的某个业务,当采用LinkedList来替代PriorityQueue,并进行批量操作后,CPU的使用直接降低了1/3。

Deque是xjjdog最喜欢的一个接口。每当使用offerFirst、offerLast这样精准的API进行操作,都会体验到多巴胺的乐趣。LinkedList作为它的儿子,自然拥有了这些所有的功能。

当我们使用concurrent包里的基本API,对这些淳朴的工具进行封装,它们就会焕发出新的活力。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。

标签:方法,LinkedList,追不上,队列,lock,列表,数据,final
From: https://blog.51cto.com/u_14355948/5762220

相关文章

  • LinkedList集合和Vectir集合
    LinkedList集合LinkedList集合数据存储的结构是链表结构,方便元素添加删除集合LinkedList集合特点:1.底层是一个链表结构:查询慢,增删快2.里边包含了大量操作首尾元素的......
  • 基于element-ui upload 二次封装,可拖拽上传列表
    1<template>2<div>3<el-upload4ref="upload"5class="upload"6:drag="drag"7:disabled......
  • js列表合并以及处理响应数据
    JS实现列表元素合并 Vue做一个穿梭框的功能,需要用到合并列表元素,左列表合并到右列表。核心思路是右三个数据列表,左、右、选中method:{toRight:function(){......
  • Redis数据结构之列表
    目录Redis数据结构之列表查看命令帮助创建列表从左边插入元素从右边插入数据若list存在,则从左边依次追加元素,不存在则忽略若list存在,则从右边依次追加元素,不存在则忽略从li......
  • 2、将一个数据列表写入到本地一个txt文件内
    列表如下:a=[{"name1":"123"},{"name2":"456"},{"name3":"789"},] 解题思路:打开文件循环列表,提取字典提取key,value写入文件代码如下:......
  • 数据库列表的建立,添加数据和删除
    --建立数据库createdatabasedbname;createdatabaseifnotexists`dbshop`defaultchatsetutf8;--数据库列表showdatabases;--使用数据库usedbname;--查询账号......
  • js实现列表自动滚动循环播放
    1.实现效果图鼠标移入,暂停滚动;鼠标移出,继续滚动;2.原理要实现无缝衔接,在原有ul后面还要有一个一样内容的ul;最外层div为可视区域,设overflow:hidden;2个ul的高度>外层......
  • self.countries是一个列表,list(sorted(set(self.countries)))
    set()函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,还可以计算交集、差集、并集等。x=set('runoob')y=set('google')print(x)print(y)a=x&y#交集p......
  • 链表(列表)
    线性结构:一对一非线性结构:一对多(树),多对多(图)链表节点:数据,指针域#include<stdio.h>#include<stdlib.h>#include<time.h>#defineCOLOR(b,a)"\033["#b"m"a"\0......
  • F5 LTM 常用oid列表
    所属层级指标输出类型OIDltmVirtualServersltmVirtualServNumberINTEGER.1.3.6.1.4.1.3375.2.2.10.1.1ltmVirtualServNameLongDisplayString.1.3.6.1.4......