当前位置: 首页 > 原理解释

blockingqueue实现原理-阻塞队列实现原理

聊个闲的:那玩意儿到底是如何排队子的 起初别一听“BlockingQueue”就认定自己是个数学系高材生,反正我连公式都没背过。
这就好比你去商场买打折的洗发水,商场那边有个大屏幕在喊:“要买,要买,要买!”你要是忒急,抢着要,结局前面的人根本排不到,得往前挪;挪到前面的人也排不到,就往前挪。等前面的人挪开,你才能上。
这就是我在心里给这玩意儿起的名字——“缓冲区”。
说白了,就是把排队的人往里面一塞,塞满了就自动把最前面的几个人放出去,新的人再塞进来。 别光听我乱讲,咱们得看看人家底层到底是如何干的。在 Java 的源码里,这玩意儿实际上是“阻塞队列”,名字听起来挺唬人,实际上是个死命令。它有个特征,就是一旦容器满了,所有的请求都得在那里乖乖等着。到时候只能去队列的头,得前面的人先走,你才能上。
这个机制的核心就是“阻塞”。 你能够想象一下那个著名的 `LinkedBlockingQueue`,它就是个庞大的蓄水池。平时水流进来,能跑;但一旦池子满了,水流就停住了,只能原地转圈。
这时候,要是我就是那个水龙头,那我就得自己停着,直到前面的人挪开一点,水才能持续流。 咱们把数据装进来,也就是往队列里扔东西。想象一下,我把一堆乱码扔进这个水里,往水面上蹦跶,这时候队列里就有东西了。
这时候你能够直接去取,要么给别人取。
可是,一旦你问它:“我要取个东西?”它就得说:“没水了,带着你的水,你走,我让你走。”这时候你就被强制阻塞了。 你会发现,这不只是是等待,出于一旦你释放了同步锁,它可能早就有人取走了。
这就像你去排队买票,出来一看前面全是人,你被拦在那儿。
这时候要是你想持续操作,就得等前面的人走了。
这就是所谓的“线程阻塞”,你也别想从队列里拿东西出来,要不就前面的都拿走了。 那它到底是如何管理这堆“水”的呢?这实际上就是个双链表。咱们能够用个好办的例子,想象两个队列连在一起,一个在左边,一个在右边。左边的人代表旧的,右边的人代表新的。当新的人进来了,它直接插到“头”后面;当旧的人要走了,它就从“头”要么“尾”被删掉。
这样不管哪位来拿东西,都能直接操作。 咱们再换个角度想,这实际上是个“双向链表 + 同步锁”的变种。Java 的 `BlockingQueue` 底层并不是一个好办的链表,而是用 `Synchronized` 锁住了一个双链表结构。
每次往里面塞东西,都是先拿一个锁,在锁保护下做一次加操作;每次取东西,也是先拿一个锁,在锁保护下做一次减操作。 不过,这里有个挺关键的细节,就是线程保险。当你被阻塞的时候,意味着你想取东西,但目前的队列里全是别的线程在取。
这时候,你不需求自己写一段代码去判断“有没有人拿”,出于 `Synchronized` 锁就把一切变成了“死等”。你只能乖乖停下来,等锁一释放,你就能够上去取。 这里的“死等”是有代价的。你不能去猜“前面有人没取”要么“前面有人取完没取完”。你只能等。等你终于取到了东西,你就把自己重新释放出来,持续去和其他线程竞争。 再说说容量。
要是这个队列装不下东西如何办?比如你能够去存一个庞大的数据库表,要么存一个无穷大的内存池。
这时候,`LinkedBlockingQueue` 就会报错,提示“队列已满,填不满,无法添加元素”。
这个报错的意思就是:这个队列有固定的上限,一旦满了,想装进更多东西,就得暂停,直到有人把东西拿走。 要是要增添容量呢?你能够故意把队列填满,然后通过 `put` 方式把那些已经塞进去的旧元素一个个取出来,这样就腾出了空间。
这时候队列就又能持续接收新元素了。
这个逻辑在底层实际上挺巧妙的,就是把“空间不足”这个难题,转化成了“显式移除元素”的难题。 那关于容量本身呢?要是你想要一个无界的队列,要么有限但挺广容量的队列,实际上还有更好的选择。
比如你能够用 `LinkedBlockingQueue`,它指定了一个最大容量,这个容量拍板了它能塞进多少元素,一旦塞满,就得强制等待。 要是你想做一个无限容量的队列,要么大容量的队列,实际上你能够用 `ArrayBlockingQueue` 配合 `Queue` 接口来实现。
不过这样做的话,你得自己写一个逻辑,每次往队列里塞东西的时候,先看队列里是不是满了。
要是满了,你就先取一个元素出来,腾出空位,然后再塞进去。别看逻辑上没难题,但编程复杂度确实高了不少,并且还得保证取元素的线程和塞元素的线程不会冲突。 再说说线程阻塞这个动作。线程阻塞意味着这行代码在执行的时候,进程暂停了,其他线程持续执行。
要是线程被阻塞了忒久,比如一直等着取东西,一旦进程里的工夫到了,要么线程被唤醒,它就会持续执行,这时候再取东西。 这里有个贼关键的点,就是死锁的难题。别看 `BlockingQueue` 是线程保险的,但它不是无锁的。
要是你加了锁,把队列里的东西都取光了,就没人再取东西了。
这时候,要是你自己再去加锁,要么让别个线程去加锁,那就形成了死锁。
这时候整个进程可能就卡住了,无法持续运行。
故此在使用的时候,要注意不要把所有的逻辑都锁在这里。 再说到这里,实际上还有一个概念叫“饿得慌”。假设有一个线程一直拿着锁,不去取东西,一直在那里等,这时候那堆东西就一辈子取不到,直到那个线程释放了锁,哪怕队列里还有元素在等着呢。
这就是“饿得慌”。 要是队列里满了,你想取东西,但前面的人已经在拿挺久了,这时候要是你一直拿,那前面的人可能一辈子拿不到。
这时候,最稳妥的办法就是定期去检查一下队列。
要是队头元素还在,你就把它拿过来,然后释放锁,持续去和其他线程竞争。 实际上,阻挡队列的原理挺好办的,就是利用双链表结构,配合同步锁,来实现线程之间的排队。它就像一个蓄水池,满了就自动放水(从队列头取出),快满了就自动放一个出来腾位置。它不是靠复杂的算法去排序,而是靠“排队”这个机制,好办粗暴地实现了线程同步。 故此的时候,要是这玩意儿还不够你中意,你就连能够自己去写一个自定义的实现,用 `java.util.concurrent.ArrayBlockingQueue` 来组合 `java.util.concurrent.SynchronousQueue`,自己搞个“取一个放一个”的逻辑。别看费事点,但灵活度最高,用起来最像真正的“水”。 最终总结一下,阻塞队列实际上就是个蓄水池,满了就自动放水,快满了就自动放一个腾位置。它线程保险,利用双链表和同步锁,实现了线程之间的排队。
要是你需求的是无限容量的队列,那就得自己去造个逻辑。
相关标签:

猜你喜欢

热门阅读

  • 赖柴尔定理-赖柴尔定理
  • 迪拜哪个国家的城市?-迪拜在哪国城市
  • 李毅吧番号及出处-李毅吧番号及出处
  • 贴春联的由来简介50字-春联由来简述
  • 思乡的名言和出处-思乡名言及出处

其他分站