尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java并发队列全解析:从BlockingQueue到线程池选型实战

Java并发队列全解析:从BlockingQueue到线程池选型实战 聊到Java里的Queue很多人第一反应是“这不就是个队列嘛LinkedList也能用”。但真正到了并发场景或者面试被问到“线程池的阻塞队列怎么选”时才发现这里面水很深。我见过不少生产事故明明线程池参数看着没问题结果任务全堆在队列里内存直接被打爆也见过有人把SynchronousQueue当成普通队列用线程池直接废掉一半。其实队列选型这件事本质上是在回答三个问题数据怎么进、怎么出、进不来出不去的路上该谁等谁。搞懂了这三个问题无论是JDK自带的Queue还是Dubbo、Netty里那些高性能队列你都能一眼看穿它的设计意图。这篇文章我想把JDK里的Queue家族从头到尾梳理一遍重点放在并发场景和阻塞队列上。内容包括每个队列的底层结构、适用场景、常见坑点以及线程池里队列怎么配、爬虫并发任务怎么分、延迟任务怎么做这类实战问题。不管你是刚学Java基础还是在准备面试八股文或者是线上出了队列相关问题正在排查这篇都值得你收藏慢慢看。1. 先摸清家底JDK里的Queue到底分了哪几路1.1 非阻塞队列LinkedList、ArrayDeque、PriorityQueue怎么用才不踩坑先聊最简单的。JDK里有一类队列它只实现了Collection和Queue接口没有实现BlockingQueue也就是说它本身不具备线程阻塞的语义。这类队列包括 LinkedList、ArrayDeque、PriorityQueue以及它们的并发安全版本 ConcurrentLinkedQueue。LinkedList是最常被拿来当队列用的add往尾部加poll从头部取看起来没什么问题。但你要知道LinkedList的底层是双向链表节点是离散分配的CPU缓存命中率不高。在单线程高频入队出队的场景下LinkedList往往不如ArrayDeque快因为ArrayDeque底层是循环数组内存是连续的扩容也是整体搬迁局部性更好。我自己测过在百万级入队出队的基准测试里ArrayDeque比LinkedList能快出不少虽然单次操作差距很小但积少成多之后差异就明显了。ArrayDeque还有一个特点它不允许存null这一点和LinkedList不同。为什么不允许null因为null在队列里经常被当作特殊返回值比如poll()在队列为空时返回null如果允许入队null那取出来的null到底是元素还是空队列的标志语义就乱了。所以JDK的设计者干脆禁止了null入队。这个细节面试偶尔会考实际使用中也要注意。PriorityQueue则完全不同它虽然名字里有Queue但出队顺序不遵循FIFO而是按元素的自然顺序或者你传入的Comparator来排序。每次poll()返回的是当前优先级最高的元素。它的底层是二叉堆具体来说是数组实现的小顶堆。这里有两个坑第一PriorityQueue不是线程安全的多线程并发修改会出问题第二它的迭代器不保证顺序如果你用for-each遍历元素拿到的顺序和每次poll()的顺序没有任何关系。想按优先级处理任务时优先考虑它但记得加锁或者用下面的PriorityBlockingQueue。再说说ConcurrentLinkedQueue。它是真正线程安全的非阻塞队列底层是CAS实现的Michael-Scott无锁队列。它的特点是不锁整个队列每次入队出队都通过CAS操作完成所以在高并发读多写少、且对实时性要求高的场景下表现很好。但注意它的size()方法是一个O(n)遍历操作在并发环境下返回的也只是个近似值不要拿它来做精确统计。我见过有人用ConcurrentLinkedQueue.size() 0做判断结果高并发下偶尔判断失误就是因为这个原因。如果你想判断队列是否为空优先使用isEmpty()。1.2 阻塞队列真正的并发利器非阻塞队列不解决“等待”的问题。生产者往里塞数据如果容器满了直接失败或者自己忙等消费者来取数据如果容器空了也是直接返回null或抛异常。这在大多数生产消费模型里是不够用的。你总不能让生产者一直死循环重试吧这时候就需要阻塞队列登场。BlockingQueue接口在Queue的基础上增加了阻塞语义核心就是put()和take()两个方法。put()在队列满时会一直等待直到队列有空间take()在队列空时会一直等待直到有新元素进来。这个“等待”不是空转而是通过Lock和Condition实现的线程挂起与唤醒既不浪费CPU又能做到精确的通知。JDK里BlockingQueue的实现类一共有7个ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue、DelayQueue、LinkedTransferQueue、LinkedBlockingDeque。我先把它们放在一张表里对比后面再一个个展开讲。实现类是否有界底层结构线程安全机制典型场景ArrayBlockingQueue有界循环数组一把ReentrantLock 两个Condition有界缓冲、线程池队列LinkedBlockingQueue默认无界可改有界单向链表两把锁take锁/put锁线程池默认队列、生产消费SynchronousQueue无容量栈或队列结构CAS LockSupport直接交接、CachedThreadPoolPriorityBlockingQueue无界二叉堆数组一把ReentrantLock优先级任务调度DelayQueue无界PriorityQueue内部封装一把ReentrantLock Condition延迟任务、定时关闭LinkedTransferQueue无界链表CAS无锁高并发数据传递LinkedBlockingDeque默认无界可改有界双向链表两把锁工作窃取、ForkJoinPool这些队列的设计思路差异很大。ArrayBlockingQueue用一把锁管所有操作入队出队互相排斥LinkedBlockingQueue用两把锁分别管入队和出队所以生产者消费者可以同时操作队列两端。SynchronousQueue干脆不存数据生产者put时直接阻塞等待消费者take。每一种设计的背后都是对特定场景的取舍。2. API和原理阻塞等待背后到底做了什么2.1 add/offer/put/remove/poll/take这一组方法千万别用混很多初学者学Queue时被这一堆方法搞晕了。其实它们的区别只有一个核心那就是操作失败时的行为不同。我直接列一张表把这几个方法按“失败处理方式”分组你对比着看就清楚了。操作类型失败时抛异常失败时返回特殊值失败时阻塞等待超时等待插入add(e)队列满抛IllegalStateExceptionoffer(e)返回falseput(e)offer(e, timeout, unit)移除remove()队列空抛NoSuchElementExceptionpoll()返回nulltake()poll(timeout, unit)检查element()队列空抛异常peek()返回null不支持不支持为什么JDK要提供这么多语义因为不同场景对“失败”的容忍度不一样。比如你写一个异步日志系统日志任务满了你希望直接丢弃还是阻塞等待如果选择丢弃用offer(e)加返回值判断最合适如果希望背压让生产者慢下来就得用put()让它阻塞在队列满的位置上。用错API最常见的后果是用offer(e)做队列满了就丢弃结果日志静默丢失排查半天才发现或者用put()却期待它不要阻塞结果线程全部卡死。我自己的使用习惯是能用带超时的offer(e, timeout, unit)就不用裸的put()。因为put()一旦阻塞就是无限期等待如果没有消费者或者消费者挂了生产者会永久挂起。带超时版本可以设置一个合理的等待时间超时后走降级逻辑比如打印告警、写入本地文件、丢弃并计数这样系统至少还有自愈的可能。并发编程里有一条铁律无限期阻塞的依赖链条越长系统越容易在极端情况下瘫痪。还有一个容易忽略的点peek()只查看队首元素但不移除在队列为空时返回null。而element()同样只查看但为空时抛NoSuchElementException日常开发中用得很少。我建议统一用peek()并且做null判断因为element()的异常语义在实际代码里几乎没有正面价值。2.2 底层阻塞原理Condition和Lock是怎么配合的搞懂阻塞队列的原理最直接的方法是看ArrayBlockingQueue的源码。它的内部有几个关键字段一个Object[]数组当存储容器一个ReentrantLock还有两个从锁上派生出来的ConditionnotEmpty和notFull。put(e)的流程大致是public void put(E e) throws InterruptedException { final ReentrantLock lock this.lock; lock.lockInterruptibly(); try { while (count items.length) notFull.await(); enqueue(e); } finally { lock.unlock(); } }注意这个while循环而不是if判断。为什么因为线程被唤醒后队列可能又被其他生产者塞满了也就是说发生了“伪唤醒”或者竞争。所以必须用while重新检查条件这也是Condition使用的标准姿势。take()的流程是对称的队列为空时在notEmpty上等待有元素被enqueue时通过notEmpty.signal()唤醒等待的消费者。LinkBlockingQueue和ArrayBlockingQueue在这方面有个重要差异ArrayBlockingQueue只有一把锁所有操作串行化LinkedBlockingQueue有两把锁takeLock和putLock互相独立所以一个生产者入队的同时另一个消费者可以出队。这也是为什么很多场景下LinkedBlockingQueue的吞吐量要好于ArrayBlockingQueue但代价是它默认是无界的容易OOM。这和数据库里的读写锁设计是一个思路。锁的粒度越细并发度越高但实现的复杂度也越高。LinkedBlockingQueue通过两把锁把“队头操作”和“队尾操作”解耦前提是链表结构天然支持头尾分离。ArrayBlockingQueue的循环数组入队出队都要维护同一个count变量用一把锁反而更简单可靠。理解了这层设计你就不难明白为什么JDK里会有这么多“看起来功能重复”的队列了。2.3 有界无界怎么选容量设置多少才合理无界队列最大的风险是内存溢出。LinkedBlockingQueue默认构造时容量是Integer.MAX_VALUE等于没设上限。你用Executors.newFixedThreadPool()时它内部就是这么干的。如果生产速度长期大于消费速度任务会在队列里无限堆积最终堆内存被打爆JVM直接OOM。这个问题太经典了几乎每个面试官都会问。那有界队列的容量怎么定我给你一个可以落地参考的公式思路平均每秒任务量 乘以 单个任务平均处理耗时秒得到稳态下的积压量再乘以你愿意承受的缓冲倍数就是容量。举例来说系统每秒产生1000个任务每个任务平均处理100毫秒那么稳态下处理中的任务大约是100个。如果你希望即使在短暂流量尖峰下也能容忍3倍积压那容量设在300到500比较合理。如果容量太大流控形同虚设太小则可能频繁触发拒绝策略导致任务大量失败。另外一个容易被忽略的点是有界队列不仅要设容量还要配合合理的拒绝策略。如果队列满了任务怎么办四种策略各有各的坑。AbortPolicy直接抛RejectedExecutionException适合对失败敏感的业务CallerRunsPolicy让提交任务的线程自己执行实际上是反向背压适合不想丢任务的场景DiscardPolicy和DiscardOldestPolicy都是静默丢弃适合能容忍少量数据丢失的日志、埋点类任务。我在生产环境最常用的是CallerRunsPolicy因为线上重要任务不能随便丢让调用方线程去执行反而是最稳妥的兜底。3. 线程池里的阻塞队列怎么配最常被问也最容易出事的点3.1 为什么newFixedThreadPool会堆积到OOM你先看一段常见的反面教材ExecutorService executor Executors.newFixedThreadPool(10);这行代码看着人畜无害但它内部创建的ThreadPoolExecutor队列用的是无界LinkedBlockingQueue容量是Integer.MAX_VALUE。这意味着无论核心线程是否空闲新任务都会先塞进队列只有当队列满了之后才会创建非核心线程。而队列永远不可能满所以线程池里的线程数永远只有10个。如果这10个线程处理不过来任务就无限排队最终内存耗尽。网上很多文章把它归因于“Executors不靠谱”其实更准确地说是默认的无界队列设计放大了风险。你把队列换成有界的问题就解决了一大半。Executors里几个工厂方法的默认队列配置我列出来你感受一下工厂方法线程数队列类型问题newFixedThreadPool(n)固定n无界LinkedBlockingQueue任务堆积OOMnewSingleThreadExecutor()1无界LinkedBlockingQueue同上newCachedThreadPool()0核心Integer.MAX_VALUE非核心SynchronousQueue任务过多时线程爆炸newScheduledThreadPool(n)核心n无界DelayedWorkQueue堆积延迟任务OOMCachedThreadPool的问题和固定线程池正好相反。它用SynchronousQueue不缓存任何任务每个任务都必须立刻有一个空闲线程来接走。一旦没有空闲线程就创建新线程。如果任务提交速率持续很高线程数会一直涨直到把内存和文件描述符耗尽。面试八股文里常说的“Executors四大坑”基本都离不开队列和线程数的匹配问题。所以生产环境我强烈建议直接使用ThreadPoolExecutor手动指定核心线程数、最大线程数、工作队列、拒绝策略和线程工厂。这样每个参数都是自己深思熟虑过的不会因为框架默认值踩坑。3.2 生产环境线程池队列的几种配置参考线程池的队列怎么选前提是想清楚你的任务特性是什么。先说IO密集型任务比如网络请求、文件读写、数据库操作。这类任务大部分时间在等待IOCPU利用率不高所以可以适度调大线程数避免线程太少导致队列积压。队列我一般选有界LinkedBlockingQueue容量在几百到几千之间具体按前面的公式估算。线程数公式很多人背过IO密集型线程数 CPU核数 * 2或者用CPU核数 / (1 - 阻塞系数)实际操作时还会加上压测调整的空间。参考配置ThreadPoolExecutor pool new ThreadPoolExecutor( 16, 32, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(1000), new ThreadFactoryBuilder().setNameFormat(io-worker-%d).get(), new ThreadPoolExecutor.CallerRunsPolicy() );这段代码里我把核心线程设为16最大线程32队列容量1000。当任务量超过“16个线程处理不过来”时新任务先进入队列排队队列填满1000个后再扩容到最多32个线程。如果32个线程加1000个排队的任务都扛不住就触发CallerRunsPolicy让提交线程自己执行实现自然的背压。这个思路保证系统在极端流量下不会被击穿最多是提交任务的线程卡顿一些。再有一种场景是CPU密集型任务比如图片处理、加解密、数值计算。这类任务几乎不等待线程数超过CPU核数反而会因为线程切换增大开销。线程数我一般设在CPU核数加1左右队列也用小容量比如100到200让任务尽快进入执行而不是排队。因为排队对CPU密集型任务没有意义反而增加了调度延迟。SynchronousQueue在什么场景下用如果你希望任务提交后立刻交给一个工作线程执行不允许排队那么SynchronousQueue是唯一选择。它不存储元素put()必须等take()take()必须等put()两边同时准备好才能完成交接中间不存在缓冲。CachedThreadPool就是基于这个设计来的但你需要自己控制最大线程数避免无限创建线程。参考配置ThreadPoolExecutor pool new ThreadPoolExecutor( 0, 200, 60L, TimeUnit.SECONDS, new SynchronousQueue(), new ThreadPoolExecutor.AbortPolicy() );这里的思路是核心线程为0意味着任务到来时优先创建新线程并尝试放入SynchronousQueue。一旦有空闲线程它会在队列上等待并立刻接走任务没有空闲线程就新建线程直到达到200上限。注意配合AbortPolicy因为SynchronousQueue永远“满”如果线程数也到上限再提交任务必然会触发拒绝策略你需要在catch里做好兜底。3.3 线程池任务提交顺序核心线程、队列、最大线程很多人以为线程池处理任务的顺序是“先核心线程满了就扩容到最大再满了走拒绝策略”这个理解是错误的。ThreadPoolExecutor真正执行execute()时的逻辑是这样的先判断当前工作线程数是否小于核心线程数小于则创建新线程执行任务否则尝试把任务放入队列如果队列也满了才判断当前线程数是否小于最大线程数小于则创建新线程执行任务否则走拒绝策略。注意这个顺序先试着进队列队列满了才创建非核心线程。这就是为什么无界队列会让最大线程数形同虚设。很多人调优时把最大线程数设得很大但队列是无界的结果非核心线程一个都没创建过等于白设。理解了执行顺序你就能明白为什么核心线程数、最大线程数、队列容量三者必须联动设计单独调某一个参数往往没有效果。这里还藏着一个参数坑allowCoreThreadTimeOut。默认情况下核心线程即使空闲也会一直活着不被回收。如果你希望线程池在空闲一段时间后收缩到0个线程可以设置allowCoreThreadTimeOut(true)搭配keepAliveTime使用。这对按需创建、用完回收的CachedThreadPool风格线程池很重要否则即使没有任务核心线程也会占用系统资源。4. 不同业务场景下的队列实战选择4.1 生产者-消费者日志异步写、请求削峰怎么做生产消费者模型是阻塞队列最经典的落地场景。我给你拆一个日志异步写的例子。假设你的业务系统每秒钟产生大量操作日志如果每条都同步写数据库或文件主流程会被IO拖慢。常规做法是引入一个阻塞队列业务线程只负责把日志对象offer到队列里后台一个或几个消费者线程take出来批量写入。我用ArrayBlockingQueue还是LinkedBlockingQueue这是个好问题。如果日志写入频率比较均衡队列长度固定ArrayBlockingQueue足够了因为它的内存占用更小数组是预分配的没有链表节点的离散开销。而且ArrayBlockingQueue支持公平锁模式构造时传入true能避免某些生产者线程长期等待。缺点是单锁设计并发入队出队会互相竞争。LinkedBlockingQueue在“同时入队同时出队”的场景下吞吐量略高一筹因为两把锁分离。如果你的消费者有多个且队列经常同时有生产者和消费者操作LinkedBlockingQueue体验更好。但它默认无界一定要记得在构造时传入容量比如new LinkedBlockingQueue(5000)否则就是给自己埋雷。请求削峰的逻辑也类似。网关层接收到突发请求先把请求封装成任务对象塞进有界队列后端worker按自己的处理能力消费。队列在这里充当了缓冲池的角色消费者按恒定速率消费相当于把突刺流量“熨平”了。这类场景我特别建议用带超时的offer()不要用put()。原因很简单队列满时put()会让请求线程无限期阻塞此时如果后端已经过载阻塞只会让故障传导到更多线程。而offer(e, 1, TimeUnit.SECONDS)超时返回false后可以直接返回“系统繁忙请稍后重试”把压力挡在入口起到熔断的效果。4.2 爬虫并发设计任务队列怎么选才既高效又不被反爬很多人做爬虫上来就开几十个线程去抓页面结果要么被封IP要么内存暴涨。热搜里“爬虫并发设计到底哪个好”这个问题本质上就是线程池和队列的选型问题。我的建议是用有界阻塞队列做任务池用固定线程池消费队列容量按目标网站的抓取频率限制来设定。举一个实际例子。假设你要抓取一个商品列表页每页返回100条数据详情页需要单独请求。你可以设计一个两级队列第一级队列存放待抓取的商品ID列表第二级队列存放待解析的HTML源码或JSON数据。因为详情页请求比较耗时第一级队列的消费速率决定了整体抓取效率而第二级队列的消费速率决定了解析入库的效率。它们之间的缓冲容量起到解耦作用某个环节慢了不至于直接拖垮另一个环节。队列容量怎么设关键看目标网站允许的请求频率。如果网站要求每秒最多10个请求那么你开10个线程每个请求耗时1秒队列容量设为50到100就够了。为什么不能设太大因为一旦其中一个页面异常导致消费者卡住比如网络连接超时设置过长队列里的任务会迅速堆积占用的内存可能非常可观。爬虫任务里每个URL或HTML都可能达到几百KB几千个任务堆积就是上百MB内存。还有一个细节爬虫任务里最好不要用无界队列也不要让队列过深。因为爬虫任务的失败率比普通业务高很多如果消费者线程因反爬被限制而停止消费无界队列会在很短时间内把内存吃满。我用爬虫线程池时队列通常设成100到200拒绝策略用CallerRunsPolicy这样当队列满时提交任务的主线程会自己执行抓取天然放慢了整体抓取速度等价于一种简单的限流。实测下来这比直接丢弃任务或者抛异常要优雅得多。4.3 延迟任务与优先级任务DelayQueue和PriorityBlockingQueue说到DelayQueue我第一个想到的是订单超时自动关闭。一个订单创建后如果30分钟内未支付系统要自动将其置为关闭状态。用户可能创建大量订单总不能每分钟全表扫一遍。DelayQueue就是为此设计的它内部封装了一个PriorityQueue元素按照“剩余延迟时间”排序只有当一个元素的delay时间到了之后take()才能取到它。这里有一点必须注意DelayQueue里存放的元素必须实现Delayed接口这个接口有两个方法getDelay(TimeUnit unit)返回剩余延迟时间compareTo(Delayed o)定义排序规则。我写一个简单示例你就明白用法了。public class OrderDelayTask implements Delayed { private final String orderId; private final long triggerTime; // 毫秒时间戳 public OrderDelayTask(String orderId, long delayMs) { this.orderId orderId; this.triggerTime System.currentTimeMillis() delayMs; } Override public long getDelay(TimeUnit unit) { return unit.convert(triggerTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } Override public int compareTo(Delayed o) { return Long.compare(this.triggerTime, ((OrderDelayTask) o).triggerTime); } public String getOrderId() { return orderId; } }使用方式很简单一个后台单线程不断执行taskQueue.take()取到就处理取不到就阻塞在那里。每来一个新订单就offer一个延迟30分钟的OrderDelayTask进去。这样你就用很少的代码实现了一个可靠的延迟任务调度器不需要引入额外的中间件。但要注意两点第一DelayQueue是无界的如果任务大量堆积同样有OOM风险使用时要限制任务总量或者配合定时清理第二如果服务重启内存里的延迟任务会全部丢失所以它只适合做单机轻量级延迟任务真正需要持久化和分布式能力时还是得靠RocketMQ延迟消息或者Redis ZSet这类方案。PriorityBlockingQueue则是PriorityQueue的线程安全版本。它内部的堆是按优先级排序的每次take()都会取出优先级最高的元素。注意它的比较器是构造时传入的如果不传就要求元素实现Comparable接口。它有个特点是无界队列虽然不会因为“满了”而阻塞生产者但同样可能因持续入队导致OOM。另外它的遍历顺序和出队顺序也不一致从队列里poll()出来的是有序的但直接迭代队列看到的是乱序的。这属于二叉堆的数据结构特性不算bug但面试时经常被拿出来当陷阱题。5. 常见问题与排查技巧实录5.1 队列任务堆积不消费先看这四件事线上如果发现消息积压或者线程池任务排队越来越多按照这个顺序排查通常能快速定位问题。第一确认消费者线程是否都活着。用jstack抓一下线程栈看看消费者线程是处于WAITING状态正常阻塞等待任务还是BLOCKED状态死锁或锁竞争又或者是RUNNABLE状态但卡在某个IO调用上。如果是第三种大概率是下游接口响应变慢导致消费者吞吐量暴跌。第二确认队列是有界还是无界。如果是无界检查当前堆内存使用率以及队列深度的监控指标。很多框架都暴露了队列大小的JMX指标比如ThreadPoolExecutor可以通过getQueue().size()拿到实时值。队列持续增长说明生产速度长期大于消费速度要么扩容消费者要么限流生产者。第三确认拒绝策略是否生效。有界队列打满后拒绝策略如果设置不当任务会在你完全没有感知的情况下被丢弃。我建议生产环境至少把拒绝次数做成一个监控指标比如在拒绝策略里加一个AtomicLong计数器或者日志埋点方便在告警平台上看到丢弃量。第四确认单个任务是否存在异常死循环或者无限重试。有一种隐蔽的情况某个任务处理时抛出异常而你在catch里没有正确结束任务导致它被重新提交到队列头部形成一个永不结束的循环其他任务永远没有机会执行。这种问题通常不会让线程池挂掉但会让系统“假死”。排查办法是看日志里同一个任务ID是否反复出现或者在任务里加执行次数的最大限制。5.2 我踩过的坑和面试高频题速查先说坑。第一个坑是SynchronousQueue配错线程数导致死等。我有一次写生产者消费者程序生产者用put()往SynchronousQueue里放数据消费者只有一个线程但生产速率远远大于消费速率结果生产者线程全部阻塞在put()上UI卡死。后来才意识到SynchronousQueue没有任何缓冲能力生产者必须和消费者“手递手”交接线程数不匹配时要么生产者等要么消费者等。用它之前一定要想清楚你的生产消费速率是否匹配。第二个坑是poll()空转导致CPU飙高。有人想让消费者不阻塞就用while(true) poll()去拿任务队列为空时poll()立刻返回null于是循环就变成了一次高性能空转一个线程就能占满一个CPU核心。用阻塞队列的意义就在于让线程在无任务时挂起而不是空转。如果你真的需要非阻塞轮询至少要在拿不到任务时Thread.sleep一小段时间或者直接用take()。第三个坑是DelayQueue内存泄漏。延迟任务如果长期不触发会一直留在堆里。比如你放了一万个30分钟后执行的延迟任务但服务在10分钟后重启这些任务全部丢失重启后不会有任何补偿机制。就算不重启大量长期延迟的任务也会占据内存。因此延迟任务一定要有总量上限和持久化补偿方案不能用完就忘。至于面试高频题整理几个我常被问到的顺便把回答要点给你ArrayBlockingQueue和LinkedBlockingQueue的区别一个是数组有界单锁一个是链表默认无界双锁。LinkedBlockingQueue并发度更高但要注意默认无界的隐患。SynchronousQueue有什么特点没有容量put和take必须同时准备好才能完成交接一般配CachedThreadPool或需要直接转交的场景。阻塞队列哪些方法会阻塞put和take会无限期阻塞offer(e, timeout, unit)和poll(timeout, unit)会超时返回add和offer(e)不会阻塞只会立刻失败。DelayQueue怎么实现的内部用PriorityQueue按延迟时间排序take时通过Condition等待队首元素到期所以注意元素需要实现Delayed接口的getDelay和compareTo。ConcurrentLinkedQueue和LinkedBlockingQueue怎么选前者是无锁非阻塞的适合对实时性要求高、任务量可控的场景后者有阻塞语义适合做生产者消费者缓冲队列。前者没有阻塞等待的能力后者可以挂起线程节省CPU。这些题目看起来是八股文但如果你能把每个队列的底层结构和设计意图讲清楚面试官基本不会再深挖。真正拉开差距的是你是否能结合项目里的实际场景说清楚“我为什么选这个队列”。我在实际项目中的习惯是先画出生产者和消费者的速率模型再决定用有界还是无界用单锁还是双锁最后把拒绝策略和监控指标一起加上。队列选型没有银弹真正重要的不是你用了哪个队列而是你有没有想清楚每一个关键参数背后的代价。把LinkedBlockingQueue默认无界这件事刻在脑子里把put()有可能永久阻塞这件事刻在脑子里线上会少很多事故。
返回列表