Java 中隊列和優先隊列的使用

     

隊列的使用(點擊打開鏈接

今天跟大家來看看如何在項目中使用隊列。首先我們要知道使用隊列的目的是什麼?一般情況下,如果是一些及時消息的處理,並且處理時間很短的情況下是不需要使用隊列的,直接阻塞式的方法調用就可以了。但是,如果在消息處理的時候特別費時間,這個時候如果有新的消息來了,就只能處於阻塞狀態,造成用戶等待。這個時候在項目中引入隊列是十分有必要的。當我們接受到消息後,先把消息放到隊列中,然後再用新的線程進行處理,這個時候就不會有消息的阻塞了。下面就跟大家介紹兩種隊列的使用,一種是基於內存的,一種是基於數據庫的。

     首先,我們來看看基於內存的隊列。在Java的併發包中已經提供了BlockingQueue的實現,比較常用的有ArrayBlockingQueue和LinkedBlockingQueue,前者是以數組的形式存儲,後者是以Node節點的鏈表形式存儲。至於數組和鏈表的區別這裏就不多說了。

BlockingQueue 隊列常用的操作方法:

      1.往隊列中添加元素: add(), put(), offer()

      2.從隊列中取出或者刪除元素: remove() element()  peek()   poll()  take()

每個方法的說明如下:

      offer()方法往隊列添加元素如果隊列已滿直接返回false,隊列未滿則直接插入並返回true;

      add()方法是對offer()方法的簡單封裝.如果隊列已滿,拋出異常new IllegalStateException("Queue full");

       put()方法往隊列裏插入元素,如果隊列已經滿,則會一直等待直到隊列爲空插入新元素,或者線程被中斷拋出異常.

       remove()方法直接刪除隊頭的元素:

       peek()方法直接取出隊頭的元素,並不刪除.

       element()方法對peek方法進行簡單封裝,如果隊頭元素存在則取出並不刪除,如果不存在拋出異常NoSuchElementException()

       poll()方法取出並刪除隊頭的元素,當隊列爲空,返回null;

       take()方法取出並刪除隊頭的元素,當隊列爲空,則會一直等待直到隊列有新元素可以取出,或者線程被中斷拋出異常

offer()方法一般跟poll()方法相對應, put()方法一般跟take()方法相對應.日常開發過程中offer()與poll()方法用的相對比較頻繁.

下面用一個例子來看看是怎麼使用的。

[java] view plain copy
  1. import java.util.concurrent.BlockingQueue;  
  2. import java.util.concurrent.Executors;  
  3. import java.util.concurrent.LinkedBlockingQueue;  
  4. import java.util.concurrent.ScheduledExecutorService;  
  5. import java.util.concurrent.TimeUnit;  
  6.   
  7. public class UserTask {  
  8.     //隊列大小  
  9.     private final int QUEUE_LENGTH = 10000*10;  
  10.     //基於內存的阻塞隊列  
  11.     private BlockingQueue<String> queue = new LinkedBlockingQueue<String>(QUEUE_LENGTH);  
  12.     //創建計劃任務執行器  
  13.     private ScheduledExecutorService es = Executors.newScheduledThreadPool(1);  
  14.   
  15.     /** 
  16.      * 構造函數,執行execute方法 
  17.      */  
  18.     public UserTask() {  
  19.         execute();  
  20.     }  
  21.       
  22.     /** 
  23.      * 添加信息至隊列中 
  24.      * @param content 
  25.      */  
  26.     public void addQueue(String content) {  
  27.         queue.add(content);  
  28.     }  
  29.       
  30.     /** 
  31.      * 初始化執行 
  32.      */  
  33.     public void execute() {  
  34.         //每一分鐘執行一次  
  35.         es.scheduleWithFixedDelay(new Runnable(){  
  36.             public void run() {  
  37.                 try {  
  38.                     String content = queue.take();  
  39.                     //處理隊列中的信息。。。。。  
  40.                     System.out.println(content);  
  41.                 } catch (InterruptedException e) {  
  42.                     e.printStackTrace();  
  43.                 }  
  44.             }  
  45.               
  46.         }, 01, TimeUnit.MINUTES);  
  47.     }  
  48. }  
 
        以上呢,就是基於內存的隊列的介紹,基於內存的隊列,隊列的大小依賴於JVM內存的大小,一般如果是內存佔用不大且處理相對較爲及時的都可以採用此種方法。如果你在隊列處理的時候需要有失敗重試機制,那麼用此種隊列就不是特別合適了。下面就說說基於數據庫的隊列。

       基於數據庫的隊列,很好理解,就是接收到消息之後,把消息存入數據庫中,設置消費時間、重試次數等,再用新的線程從數據庫中讀取信息,進行處理。首先來看看數據庫的設計。

字段
類型
說明
queue_id
bigint
隊列ID,唯一標識
create_time
bigint
創建時間
type
int
業務類型
status
int
處理狀態位 : 1:有效可處理(active) 3:臨時被佔用 (locked) 5:處理完畢 標記刪除(deleted)
consume_status
int
消費狀態:1:未消費  2:消費成功 3:消費失敗,等待下次消費 4:作廢
update_time
bigint
更新時間
locker
varchar
佔用標籤
last_consume_time
bigint
最後一次消費時間
next_consume_time
bigint
可消費開始時間
consume_count
int
消費次數
json_data
text
數據信息 json格式


代碼示例如下:

[java] view plain copy
  1. /** 
  2.      * 批量獲取 可以消費的消息 
  3.      * 先使用一個時間戳將被消費的消息鎖定,然後再使用這個時間戳去查詢鎖定的數據。 
  4.      * @param count 
  5.      * @return 
  6.      */  
  7.     public List<Queue> findActiveQueueNew(int count) {  
  8.         //先去更新數據  
  9.         String locker = String.valueOf(System.currentTimeMillis())+random.nextInt(10000);  
  10.         int lockCount = 0;  
  11.         try {  
  12.                         //將status爲1的更新爲3,設置locker,先鎖定消息  
  13.             lockCount = queueDAO.updateActiveQueue(PayConstants.QUEUE_STATUS_LOCKED,  
  14.                     PayConstants.QUEUE_STATUS_ACTIVE, count, locker);  
  15.         } catch (Exception e) {  
  16.             logger.error(  
  17.                     "QueueDomainRepository.findActiveQueueNew error occured!"  
  18.                             + e.getMessage(), e);  
  19.             throw new TuanRuntimeException(  
  20.                     PayConstants.SERVICE_DATABASE_FALIURE,  
  21.                     "QueueDomainRepository.findActiveQueue error occured!", e);  
  22.         }  
  23.           
  24.         //如果鎖定的數量爲0,則無需再去查詢  
  25.         if(lockCount == 0){  
  26.             return null;  
  27.         }  
  28.                   
  29.         //休息一會在再詢,防止數據已經被更改  
  30.         try {  
  31.             Thread.sleep(1);  
  32.         } catch (Exception e) {  
  33.             logger.error("QueueDomainRepository.findActiveQueue error sleep occured!"  
  34.                     + e.getMessage(), e);  
  35.         }  
  36.         List<Queue> activeList = null;  
  37.         try {  
  38.             activeList = queueDAO.getByLocker(locker);  
  39.         } catch (Exception e) {  
  40.             logger.error("QueueDomainRepository.findActiveQueue error occured!"  
  41.                     + e.getMessage(), e);  
  42.             throw new TuanRuntimeException(  
  43.                     PayConstants.SERVICE_DATABASE_FALIURE,  
  44.                     "QueueDomainRepository.findActiveQueue error occured!",e);  
  45.         }  
  46.         return activeList;  
  47.     }  

獲取到消息之後,還需要再判斷消息是否合法,如是否達到最大消費次數,消息是否已被成功消費,等,判斷代碼如下:

[java] view plain copy
  1. /** 
  2.      * 驗證隊列modle 的合法性 
  3.      *  
  4.      * @param model 
  5.      * @return boolean true,消息還可以消費。false,消息不允許消費。 
  6.      */  
  7.     public boolean validateQueue(final QueueModel model){  
  8.         int consumeCount = model.getConsumeCount();  
  9.         if (consumeCount >= PayConstants.QUEUE_MAX_CONSUME_COUNT) {  
  10.             //消費次數超過了最大次數  
  11.             return false;  
  12.         }  
  13.         int consumeStatus = model.getConsumeStatus();  
  14.         if(consumeStatus == PayConstants.QUEUE_STATUS_CONSUMER_SUCCESS){  
  15.             //消息已經被成功消費  
  16.             return false;  
  17.         }  
  18.         QueueStatusEnum queueStatusEnum  = model.getQueueStatusEnum();  
  19.         if(queueStatusEnum == null || queueStatusEnum != QueueStatusEnum.LOCKED){  
  20.             //消息狀態不正確  
  21.             return false;  
  22.         }  
  23.         String jsonData = model.getJsonData();  
  24.         if(StringUtils.isEmpty(jsonData)){  
  25.             //消息體爲空  
  26.             return false;  
  27.         }  
  28.         return true;  
  29.     }  

消息處理完畢之後,根據消費結果修改數據庫中的狀態。

[java] view plain copy
  1. public void consume(boolean isDelete, Long consumeMinTime,  
  2.             String tradeNo,int consumeCount) {  
  3.         QueueDO queueDO  = new QueueDO();  
  4.         if (!isDelete) {  
  5.             //已經到了做大消費次數,消息作廢 不再處理  
  6.             if (consumeCount >= PayConstants.QUEUE_MAX_CONSUME_COUNT) {  
  7.                 //達到最大消費次數的也設置爲消費成功  
  8.                                 queueDO.setConsumeStatus(PayConstants.QUEUE_STATUS_CONSUMER_SUCCESS);  
  9.                 queueDO.setStatus(PayConstants.QUEUE_STATUS_CANCEL);  
  10.             } else {  
  11.                 queueDO.setConsumeStatus(PayConstants.QUEUE_STATUS_CONSUMER_FAILED);      
  12.                 //設置爲可用狀態等待下次繼續發送  
  13.                 queueDO.setStatus(PayConstants.QUEUE_STATUS_ACTIVE);  
  14.             }  
  15.         } else {  
  16.             //第三方消費成功  
  17.             queueDO.setConsumeStatus(PayConstants.QUEUE_STATUS_CONSUMER_SUCCESS);  
  18.             queueDO.setStatus(PayConstants.QUEUE_STATUS_DELETED);  
  19.         }  
  20.         queueDO.setNextConsumeTime(consumeMinTime == null ? QueueRuleUtil  
  21.                 .getNextConsumeTime(consumeCount) : consumeMinTime);  
  22.         if (StringUtils.isNotBlank(tradeNo)) {  
  23.             queueDO.setTradeNo(tradeNo);  
  24.         }  
  25.         long now = System.currentTimeMillis();  
  26.         queueDO.setUpdateTime(now);  
  27.         queueDO.setLastConsumeTime(now);  
  28.         queueDO.setConsumeCount(consumeCount);  
  29.         queueDO.setQueueID(id);  
  30.         setQueueDOUpdate(queueDO);  
  31.     }  
下次消費時間的計算如下:根據消費次數計算,每次消費存在遞增的時間間隔。

[java] view plain copy
  1. /** 
  2.  * 隊列消費 開始時間 控制 
  3.  */  
  4. public class QueueRuleUtil {  
  5.       
  6.     public static long getNextConsumeTime(int consumeCount) {  
  7.         return getNextConsumeTime(consumeCount, 0);  
  8.     }  
  9.   
  10.     public static long getNextConsumeSecond(int consumeCount) {  
  11.         return getNextConsumeTime(consumeCount, 0);  
  12.     }  
  13.       
  14.     public static long getNextConsumeTime(int cousumeCount, int addInteval) {  
  15.         int secends = getNextConsumeSecond(cousumeCount,addInteval);  
  16.         return System.currentTimeMillis()+secends*1000;  
  17.     }  
  18.       
  19.     public static int getNextConsumeSecond(int cousumeCount, int addInteval) {  
  20.         if (cousumeCount == 1) {  
  21.             return  addInteval + 10;  
  22.         } else if (cousumeCount == 2) {  
  23.             return  addInteval + 60;  
  24.         } else if (cousumeCount == 3) {  
  25.             return  addInteval + 60 * 5;  
  26.         } else if (cousumeCount == 4) {  
  27.             return  addInteval + 60 * 15;  
  28.         } else if (cousumeCount == 5) {  
  29.             return addInteval + 60 * 60;  
  30.         } else if (cousumeCount == 6){  
  31.             return addInteval + 60 * 60 *2;  
  32.         } else if(cousumeCount == 7){  
  33.             return addInteval + 60 * 60 *5;  
  34.         } else {  
  35.             return addInteval + 60 * 60 * 10;  
  36.         }  
  37.     }  

除此之外,對於消費完成,等待刪除的消息,可以將消息直接刪除或者是進行備份。最好不要在該表中保留太多需要刪除的消息,以免影響數據庫的查詢效率。

我們在處理消息的時候,首先對消息進行了鎖定,設置了locker,如果系統出現異常的時候,也會產生消息一直處於被鎖定的狀態,此時可能還需要定期去修復被鎖定的消息。

[java] view plain copy
  1. /** 
  2.      * 批量獲取 可以消費的消息 
  3.      *  
  4.      * @param count 
  5.      * @return 
  6.      */  
  7.     public void repairQueueByStatus(int status) {  
  8.         List<QueueDO> activeList = null;  
  9.         try {  
  10.             Map<String,Object> params = new HashMap<String,Object>();  
  11.             params.put("status", status);  
  12.             //下次消費時間在當前時間3小時以內的消息  
  13.                         params.put("next_consume_time", System.currentTimeMillis()+3*60*1000);  
  14.             activeList =  queueDAO.findQueueByParams(params);  
  15.         } catch (Exception e) {  
  16.             logger.error("QueueDomainRepository.repairQueueByStatus find error occured!"  
  17.                     + e.getMessage(), e);  
  18.             throw new TuanRuntimeException(  
  19.                     PayConstants.SERVICE_DATABASE_FALIURE,  
  20.                     "QueueDomainRepository.findQueueByStatus error occured!",e);  
  21.         }  
  22.         if (activeList == null || activeList.size() == 0) {  
  23.             return ;  
  24.         }  
  25.         for (QueueDO temp : activeList) {  
  26.             try {  
  27.                 //status=1,可被消費  
  28.                                 queueDAO.update(temp.getQueueID(), PayConstants.QUEUE_STATUS_ACTIVE);  
  29.             } catch (Exception e) {  
  30.                 logger.error("QueueDomainRepository.repairQueueByStatus  update error occured!"  
  31.                         + e.getMessage(), e);  
  32.                 throw new TuanRuntimeException(  
  33.                         PayConstants.SERVICE_DATABASE_FALIURE,  
  34.                         "QueueDomainRepository.repairQueueByStatus update error occured!",e);  
  35.             }  
  36.               
  37.         }  
  38.          }  

以上就是對兩種隊列的簡單說明。在使用基於數據庫的隊列的時候,其中還使用到了事件處理機制,這部分的內容,就下次的時候再去介紹。



優先隊列的使用(點擊打開鏈接

我們知道優先隊列其實內部實現就是一個堆的數據結構,java默認的是一個小跟堆,每次取出最小的元素,因爲堆的性質他可以做到O(logn)級別的插入和刪除操作。

我們知道堆的性質是有: 
1.堆中某個結點的值總是不大於(或不小於)其父結點的值; 
2.堆總是一棵完全二叉樹。

將根結點最大的堆叫做大根堆,根結點最小的堆叫做小根堆。常見的堆有二叉堆、斐波那契堆等

插入:向堆中插入一個新元素;在數組的最末尾插入新結點。然後自下而上調整子結點與父結點:比較當前結點與父結點,不滿足堆性質則交換,使得當前子樹滿足二叉堆的性質。時間複雜度爲 O(logn)。

彈出:刪除堆頂元素,再把堆存儲的最後那個結點填在根結點處。再從上而下調整父結點與它的子結點。時間複雜度爲 O(logn)。

刪除:使該元素與堆尾元素交換,調整堆容量,再由原堆尾元素的當前位置自頂向下調整。時間複雜度爲 O(logn)。

如果經常需要合併兩個堆的操作,那麼使用二項堆、斜堆、左偏樹等數據結構會更好。 
可並堆支持合併操作,使得合併後的堆也能保持堆的性質。左偏樹是可並堆的一種,保證左子樹的深度大於右子樹的深度,再用右子樹與另一個堆合併。 
因此,堆支持查詢最值、插入、刪除操作。

堆排序,通過堆維護最值,對最值逐個彈出,使得得到的序列有序。 
下面看下一般的堆我們都直接用優先隊列實現如下


import java.io.BufferedInputStream;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Scanner;

class Dog
{
    public int x;
}
public class Main
{
    Scanner cin = new Scanner(new BufferedInputStream(System.in));
    Queue<Integer> queue = new PriorityQueue<>();///優先隊列默認的小根堆
    void solve()
    {
        queue.add(5);
        queue.add(3);
        queue.add(56);
        while(!queue.isEmpty())
        {
            System.out.println(queue.poll());///維護一個堆保證每次取出的都是最小的並出堆
        }
        Comparator<Integer> cmp;///可以new一個重載器;
        cmp = new Comparator<Integer>()
        {
            public int compare(Integer e1,Integer e2)
            {
                return e2 - e1;///重載優先級使其變爲大根堆
            }
        };
        Queue<Integer> que = new PriorityQueue<>(cmp);///篩入一個重載器使其變爲大跟堆
        que.add(5);
        que.add(6);
        que.add(7);
        que.add(1);
        que.add(11);
        System.out.println("*****");
        while(!que.isEmpty())
        {
            System.out.println(que.poll());///維護一個堆保證每次取出的都是最大的並出堆
        }
        Comparator<Dog> dogcmp;
        dogcmp = new Comparator<Dog>() {
            @Override
            public int compare(Dog o1, Dog o2) {
                return o1.x-o2.x;///重載優先級使其變爲小根堆
            }
        };
        Queue<Dog> q = new PriorityQueue<Dog>(dogcmp);
        Dog g = new Dog();
        g.x = 1;
        Dog g1 = new Dog();
        g1.x = 100;
        Dog g2 = new Dog();
        g2.x = 50;
        Dog g3 = new Dog();
        g3.x = 11;
        q.add(g);
        q.add(g1);
        q.add(g2);
        q.add(g3);
        System.out.println("*****");
        while(!q.isEmpty())
        {
            System.out.println(q.poll().x);///維護一個堆保證每次取出的都是最小的並出堆
        }
    }
    public static void main(String[] args)
    {
        new Main().solve();
    }
}

發佈了497 篇原創文章 · 獲贊 623 · 訪問量 37萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章