<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試侃集合 | PriorityBlockingQueue篇

          共 12380字,需瀏覽 25分鐘

           ·

          2021-06-17 19:49

          面試官:來了啊小伙子,以前經(jīng)常有小菜鳥被我虐個(gè)兩三輪就不敢來了,看你忍耐力還不錯(cuò),以后應(yīng)該挺能加班的樣子。

          Hydra:那可是,我卷起來真的是連我自己都害怕?。?/p>

          面試官:那咱們今天就繼續(xù)死磕隊(duì)列,聊聊PriorityBlockingQueue吧。

          Hydra:沒問題啊,PriorityBlockingQueue是一個(gè)支持優(yōu)先級(jí)的無界阻塞隊(duì)列,之前介紹的隊(duì)列大多是FIFO先進(jìn)先出或LIFO后進(jìn)先出的,PriorityBlockingQueue不同,可以按照自然排序或自定義排序的順序在隊(duì)列中對(duì)元素進(jìn)行排序。

          我還是先寫一個(gè)例子吧,使用offer方法向隊(duì)列中添加5個(gè)隨機(jī)數(shù),然后使用poll方法從隊(duì)列中依次取出:

          PriorityBlockingQueue<Integer> queue=new PriorityBlockingQueue<Integer>(5);

          Random random = new Random();
          System.out.println("add:");
          for (int i = 0; i < 5; i++) {
              int j = random.nextInt(100);
              System.out.print(j+"  ");
              queue.offer(j);
          }

          System.out.println("\r\npoll:");
          for (int i = 0; i < 5; i++) {
              System.out.print(queue.poll()+"  ");
          }

          查看運(yùn)行結(jié)果,可以看到輸出順序與插入順序是不同的,默認(rèn)情況下最終會(huì)按照自然排序的順序進(jìn)行輸出:

          add:
          68 34 40 31 44
          poll:
          31 34 40 44 68

          PriorityBlockingQueue隊(duì)列就像下面這個(gè)神奇的容器,不管你按照什么順序往里塞數(shù)據(jù),在取出的時(shí)候一定是按照排序完成后的順序出隊(duì)的。

          試官:怎么感覺這功能有點(diǎn)雞肋啊,很多情況下我不想用自然排序怎么辦?

          Hydra:一看你就沒仔細(xì)聽我前面講的,除了自然排序外,也可以自定義排序順序。如果我們想改變排序算法,也可以在構(gòu)造器中傳入一個(gè)Comparator對(duì)象,像下面這么一改就可以變成降序排序了:

          PriorityBlockingQueue queue=new PriorityBlockingQueue<Integer>(10new Comparator<Integer>() {
              @Override
              public int compare(Integer o1, Integer o2) {
                  return o2-o1;
              }
          });

          試官:我就隨口問一句你還真以為我不知道啊,說一下底層是怎么實(shí)現(xiàn)的吧?

          Hydra:在講底層的原理之前,就不得不先提一下二叉堆的數(shù)據(jù)結(jié)構(gòu)了。二叉堆是一種特殊的堆,它的結(jié)構(gòu)和完全二叉樹非常類似。如果父節(jié)點(diǎn)的值總小于子節(jié)點(diǎn)的值,那么它就是一個(gè)最小二叉堆,反之則是最大二叉堆,并且每個(gè)節(jié)點(diǎn)的左子樹和右子樹也是一個(gè)二叉堆。

          以一個(gè)最小二叉堆為例:

          這個(gè)最小二叉堆保存在數(shù)組中的順序是這樣的:

          [1,2,3,4,5,6,7,8,9]

          根據(jù)它的特性,可以輕松的計(jì)算出一個(gè)節(jié)點(diǎn)的父節(jié)點(diǎn)或子節(jié)點(diǎn)在數(shù)組中對(duì)應(yīng)的位置。假設(shè)一個(gè)元素在數(shù)組中的下標(biāo)是t,那么父節(jié)點(diǎn)、左右子節(jié)點(diǎn)的下標(biāo)計(jì)算公式如下:

          parent(t) = (t - 1) >>> 1 
          left(t) = t << 1 + 1
          right(t) = t << 1 + 2

          以上面的二叉堆中的元素6為例,它在數(shù)組中的下標(biāo)是5,可以計(jì)算出它的父節(jié)點(diǎn)下標(biāo)為2,對(duì)應(yīng)元素為3:

          parent(5) = 100 >>> 1 = 2

          如果要計(jì)算元素4的左右子節(jié)點(diǎn)的話,它的下標(biāo)是3,計(jì)算出的子節(jié)點(diǎn)坐標(biāo)分別為7,8,對(duì)應(yīng)的元素為8,9:

          left(3) = 11 << 1 + 1 = 7
          right(3) = 11 << 1 + 2 = 8

          在上面計(jì)算元素的數(shù)組位置過程中使用了左移右移操作,是不是感覺非??犰??

          面試官:行了別貧了,鋪墊了半點(diǎn),趕緊說隊(duì)列的底層原理。

          Hydra:別急,下面就講了,在PriorityBlockingQueue中,關(guān)鍵的屬性有下面這些:

          private transient Object[] queue;
          private transient int size;
          private transient Comparator<? super E> comparator;
          private final ReentrantLock lock;
          private final Condition notEmpty;

          前面我們也說了,二叉堆可以用數(shù)組的形式存儲(chǔ),所以隊(duì)列的底層仍然是使用數(shù)組來存放元素的。在無參構(gòu)造函數(shù)中,隊(duì)列的初始容量是11,comparator為空,也就是使用元素自身的compareTo方法來進(jìn)行比較排序。和ArrayBlockingQueue類似,底層通過ReentrantLock實(shí)現(xiàn)線程間的并發(fā)控制, 并使用Condition實(shí)現(xiàn)線程的等待及喚醒。

          試官:這么一看,屬性和ArrayBlockingQueue還真是基本差不多啊,那結(jié)構(gòu)就介紹到這吧,說重點(diǎn),元素是怎么按照排序方法插入的?

          Hydra:我們先對(duì)offer方法的執(zhí)行流程進(jìn)行分析,如果隊(duì)列中元素未滿,且在默認(rèn)情況下comparator為空時(shí),按照自然順序排序,會(huì)執(zhí)行siftUpComparable方法:

          private static <T> void siftUpComparable(int k, T x, Object[] array) {
              Comparable<? super T> key = (Comparable<? super T>) x;
              while (k > 0) {
                  int parent = (k - 1) >>> 1;
                  Object e = array[parent];
                  if (key.compareTo((T) e) >= 0)
                      break;
                  array[k] = e;
                  k = parent;
              }
              array[k] = key;
          }

          如果隊(duì)列為空,那么元素直接入隊(duì),如果隊(duì)列中已經(jīng)有元素了,那么就需要判斷插入的位置了。首先獲取父節(jié)點(diǎn)的坐標(biāo),將自己的值和父節(jié)點(diǎn)進(jìn)行比較,可以分為兩種情況:

          • 如果新節(jié)點(diǎn)的值比父節(jié)點(diǎn)大,那么說明當(dāng)前父節(jié)點(diǎn)就是較小的元素,不需要進(jìn)行調(diào)整,直接將元素添加到隊(duì)尾
          • 如果新節(jié)點(diǎn)的值比父節(jié)點(diǎn)小的話,那么就要進(jìn)行上浮操作。先將父節(jié)點(diǎn)的值復(fù)制到子節(jié)點(diǎn)的位置,下一次將新節(jié)點(diǎn)的值與父節(jié)點(diǎn)的父節(jié)點(diǎn)進(jìn)行比較。這一上浮過程會(huì)持續(xù)進(jìn)行,直到新節(jié)點(diǎn)的值比父節(jié)點(diǎn)大,或新節(jié)點(diǎn)上浮成為根節(jié)點(diǎn)為止

          還是以上面數(shù)據(jù)插入過程為例,來演示二叉樹的構(gòu)建過程:

          在將新元素添加到隊(duì)列中后,隊(duì)列中元素的計(jì)數(shù)加1,并且去喚醒阻塞在notEmpty上的等待線程。

          試官:那么如果不是自然排序的時(shí)候,邏輯會(huì)發(fā)生改變嗎?

          Hydra:如果comparator不為空的話,邏輯與上面的方法基本一致,唯一不同的是在進(jìn)行比較時(shí)調(diào)用的是傳入的自定義comparatorcompare方法。

          試官:剛才你在講offer方法的時(shí)候,強(qiáng)調(diào)了隊(duì)列中元素未滿這一個(gè)條件,開始的時(shí)候不是說PriorityBlockingQueue是一個(gè)無界隊(duì)列么,那為什么還要加這一個(gè)條件?

          Hydra:雖然說它是一個(gè)無界隊(duì)列,但其實(shí)隊(duì)列的長(zhǎng)度上限是Integer.MAX_VALUE - 8,并且底層是使用的數(shù)組保存元素,在初始化數(shù)組的時(shí)候也會(huì)指定一個(gè)長(zhǎng)度,如果超過這個(gè)長(zhǎng)度的話,那么就需要進(jìn)行擴(kuò)容,執(zhí)行tryGrow方法:

          private void tryGrow(Object[] array, int oldCap) {
              lock.unlock(); // 釋放鎖
              Object[] newArray = null;
              if (allocationSpinLock == 0 &&
                  //cas 加鎖
                  UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,01)) {
                  try {
                      //計(jì)算擴(kuò)容后的容量
                      int newCap = oldCap + ((oldCap < 64) ?
                                             (oldCap + 2) : // grow faster if small
                                             (oldCap >> 1));
                      // 避免超出上限
                      if (newCap - MAX_ARRAY_SIZE > 0) {    
                          int minCap = oldCap + 1;
                          if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                              throw new OutOfMemoryError();
                          newCap = MAX_ARRAY_SIZE;
                      }
                      if (newCap > oldCap && queue == array)
                          //申請(qǐng)新的數(shù)組
                          newArray = new Object[newCap];
                  } finally {
                      //釋放cas鎖標(biāo)志位
                      allocationSpinLock = 0;
                  }
              }
              //其他線程正在擴(kuò)容,讓出CPU
              if (newArray == null// back off if another thread is allocating
                  Thread.yield();
              //加獨(dú)占式鎖,拷貝原先隊(duì)列中的數(shù)據(jù)
              lock.lock();
              if (newArray != null && queue == array) {
                  queue = newArray;
                  System.arraycopy(array, 0, newArray, 0, oldCap);
              }
          }

          先說鎖的操作,在進(jìn)行擴(kuò)容前,會(huì)先釋放獨(dú)占式的lock,因?yàn)閿U(kuò)容操作需要一定的時(shí)間,如果在這段時(shí)間內(nèi)還持有鎖的話會(huì)降低隊(duì)列的吞吐量。因此這里使用cas的方式保證擴(kuò)容這一操作本身是排他性的,即只有一個(gè)線程來實(shí)現(xiàn)擴(kuò)容。在完成新數(shù)組的申請(qǐng)后,會(huì)釋放cas鎖的標(biāo)志位,并在拷貝隊(duì)列中原有數(shù)據(jù)到新數(shù)組前,再次加獨(dú)占式鎖lock,保證線程間的數(shù)據(jù)安全。

          至于擴(kuò)容操作也很簡(jiǎn)單,假設(shè)當(dāng)前數(shù)組長(zhǎng)度為n,如果小于64的話那么數(shù)組長(zhǎng)度擴(kuò)為2n+2,如果大于64則擴(kuò)為1.5n,并且擴(kuò)容后的數(shù)組不能超過上面說的上限值。申請(qǐng)完成新的數(shù)組空間后,使用native方法實(shí)現(xiàn)數(shù)據(jù)的拷貝。

          假設(shè)初始長(zhǎng)度為5,當(dāng)有新元素要入隊(duì)時(shí),就需要進(jìn)行擴(kuò)容,如圖所示:

          試官:ok,講的還不賴,該說出隊(duì)的方法了吧?

          Hydra:嗯,有了前面的基礎(chǔ),出隊(duì)過程理解起來也非常簡(jiǎn)單,還是以自然排序?yàn)槔?,看一?code style="font-size: 14px;word-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;background-color: rgba(27, 31, 35, 0.0470588);">dequeue方法(省略了部分不重要的代碼):

          private E dequeue() {
              int n = size - 1;
              // ...
              Object[] array = queue;
              E result = (E) array[0];
              E x = (E) array[n];
              array[n] = null;
           // ...
              siftDownComparable(0, x, array, n);
           // ...
              size = n;
              return result;    
          }

          如果隊(duì)列為空,dequeue方法會(huì)直接返回null,否則返回?cái)?shù)組中的第一個(gè)元素。在將隊(duì)尾元素保存后,清除隊(duì)尾節(jié)點(diǎn),然后調(diào)用siftDownComparable方法,調(diào)整二叉堆的結(jié)構(gòu),使其成為一個(gè)新的最小二叉堆:

          private static <T> void siftDownComparable(int k, T x, Object[] array,int n) {
              if (n > 0) {
                  Comparable<? super T> key = (Comparable<? super T>)x;
                  int half = n >>> 1;           // loop while a non-leaf
                  while (k < half) {
                      int child = (k << 1) + 1// assume left child is least
                      Object c = array[child];
                      int right = child + 1;
                      if (right < n &&
                          ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                          c = array[child = right];
                      if (key.compareTo((T) c) <= 0)
                          break;
                      array[k] = c;
                      k = child;
                  }
                  array[k] = key;
              }
          }

          首先解釋一下half的作用,它用來尋找隊(duì)列的中間節(jié)點(diǎn),所有非葉子節(jié)點(diǎn)的坐標(biāo)都不會(huì)超過這個(gè)half值。分別以樹中含有奇數(shù)個(gè)節(jié)點(diǎn)和偶數(shù)個(gè)節(jié)點(diǎn)為例:

          [n=9]  1001 >>> 1 =100 =4
          [n=8] 1000 >>> 1 =100 =4

          可以看到,奇數(shù)和偶數(shù)的情況下計(jì)算出的half值都是4,即非葉子節(jié)點(diǎn)的下標(biāo)不會(huì)超過4,對(duì)應(yīng)上圖中的元素為5。

          面試官:計(jì)算二叉樹最后非葉子節(jié)點(diǎn)坐標(biāo)這點(diǎn)知識(shí),大一學(xué)過數(shù)據(jù)結(jié)構(gòu)的新生都知道,趕緊說正題!

          Hydra:著什么急啊,前面我們也說了,在將堆頂元素取出后,堆頂位置的元素出現(xiàn)空缺,需要調(diào)整堆結(jié)構(gòu)使二叉堆的結(jié)構(gòu)特性保持不變。這時(shí)候比較簡(jiǎn)單的方法就是將尾結(jié)點(diǎn)直接填充到堆頂,然后從堆頂開始調(diào)整結(jié)構(gòu)。

          因此在代碼中,每次執(zhí)行堆頂節(jié)點(diǎn)的出隊(duì)后,都將尾節(jié)點(diǎn)取出,然后從根節(jié)點(diǎn)開始向下比較,這一過程可以稱為下沉。下沉過程從根節(jié)點(diǎn)開始,首先獲取左右子節(jié)點(diǎn)的坐標(biāo),并取出存儲(chǔ)的元素值較小的那個(gè),和key進(jìn)行比較:

          • 如果key比左右節(jié)點(diǎn)都要小,那么說明找到了位置,比較結(jié)束,直接使用它替換父節(jié)點(diǎn)即可
          • 否則的話,調(diào)整二叉堆結(jié)構(gòu),將較小的子節(jié)點(diǎn)上浮,使用它替換父節(jié)點(diǎn)。然后將用于比較的父節(jié)點(diǎn)坐標(biāo)k下移調(diào)整為較小子節(jié)點(diǎn),準(zhǔn)備進(jìn)行下一次的比較

          別看我白話這么一大段,估計(jì)你還是不明白,給你畫個(gè)圖吧,以上面的隊(duì)列執(zhí)行一次poll方法為例:

          后面的操作也是以此類推,分析到這出隊(duì)操作也就結(jié)束了,PriorityBlockingQueue也沒什么其他好講的了。

          面試官:我發(fā)現(xiàn)你現(xiàn)在開始偷懶了,前面的面試?yán)锬氵€分一下阻塞和非阻塞方法,現(xiàn)在不說一下這兩種方式的區(qū)別就想蒙混過關(guān)了?

          Hydra:嗨,在PriorityBlockingQueue里阻塞和非阻塞的區(qū)別其實(shí)并不大,首先因?yàn)樗且粋€(gè)無界的隊(duì)列,因此添加元素的操作是不會(huì)被阻塞的,如果看一下源碼,你就會(huì)發(fā)現(xiàn)其他的添加方法addput也是直接調(diào)用的offer方法。

          而取出元素操作會(huì)受限制于隊(duì)列是否為空,因此可能會(huì)發(fā)生阻塞,阻塞方法take和非阻塞的poll會(huì)稍有不同,如果出現(xiàn)隊(duì)列為空的情況,poll會(huì)直接返回null,而take會(huì)將線程在notEmpty上進(jìn)行阻塞,等待隊(duì)列中被添加元素后喚醒。

          面試官:嗯,優(yōu)先級(jí)隊(duì)列我們也聊的差不多了,反正都聊了這么久的隊(duì)列了,不介意我們把剩余的幾個(gè)也說完吧?

          Hydra:沒問題啊,畢竟我能有什么選擇呢?

          我已經(jīng)更新了我的《10萬字Springboot經(jīng)典學(xué)習(xí)筆記》中,點(diǎn)擊下面小卡片,進(jìn)入【Java開發(fā)寶典】,回復(fù):筆記,即可免費(fèi)獲取。

          點(diǎn)贊是最大的支持 

          瀏覽 50
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  蜜臀日韩免费 | AV色中色 | 夜夜操天天日 | 天天射天天日天天 | 嫩草久久 |