結合多線程實例談一談LinkedBlockingQueue的原理

之前工作時候寫了點多線程的東西,當時自己用了LinkedBlockingQueue這個隊列,最近看了看這個源碼,這裏就算做個筆記吧。
一般我們涉及到和服務器交互的時候,比如java和C++服務器進行交互,這時候肯定需要使用socket。通常做法是維護兩個隊列,三個線程。A隊列裏面放需要發送給服務器的數據包,B隊列裏面存放服務器返回過來的數據包。三個線程功能如下:A線程專門負責從A隊列裏取數據發送到服務端,B線程專門負責從socket通道中讀數據放入到B隊列。C線程專門負責從B隊列中取數據進行分包後將數據包轉發給相應的數據處理模塊進行數據處理。部分代碼如下
兩個隊列:
    BlockingQueue<byte[]> A = new LinkedBlockingQueue<byte[]>();
    BlockingQueue<byte[]> B = new LinkedBlockingQueue<byte[]>();
線程A:
    Thread A = new Thread(new Runnable()
    {
        @Override
        public void run() {
            try {
                socket = new Socket(IP,Port);
                Log.e("連接服務器成功");
                B.start();
                C.start();
                while(true){
                    byte[] data = A.take();
                    socket.getOutputStream().write(data);
                }
            } catch (IOException e) {
                Log.e("連接服務器失敗");
                e.printStackTrace();
            } catch (InterruptedException e) {
                Log.e("隊列沒有數據");
                e.printStackTrace();
            }

        }
    });
線程B:
    Thread B = new Thread(new Runnable()
    {
        @Override
        public void run() {
            try {
                byte[] receive = new byte[1000];
                InputStream is = socket.getInputStream();
                while(true){
                    is.read(receive);
                    B.put(receive);
                    receive = new byte[1000];
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
線程C:
    Thread C = new Thread(new Runnable()
    {
        @Override
        public void run() {
              while(true){
                    byte[] packet = B.take();
                     //分包,調用業務邏輯接口
               }
        }
    });
這裏A線程可以在構造函數中直接開啓,由於socket這類最好用單例,所以可以這麼寫
    private  TCP(){
        A.start();
    }
多線程的邏輯大概就是這樣,當然實際情況中可能更加複雜,這個按照業務場景改變即可。上述代碼中用了LinkedBlockingQueue這個類,查閱相關資料可以知道它是一個阻塞型隊列,put方法在隊列滿的時候會阻塞直到有隊列成員被消費,take方法在隊列空的時候會阻塞,直到有隊列成員被放進來。相關代碼如下(jdk1.8版本):
  private final AtomicInteger count = new AtomicInteger();
  transient Node<E> head;
  private transient Node<E> last;
  private final ReentrantLock takeLock = new ReentrantLock();
  private final Condition notEmpty = this.takeLock.newCondition();
  private final ReentrantLock putLock = new ReentrantLock();
  private final Condition notFull = this.putLock.newCondition();
以上代碼可以看出LinkedBlockingQueue含有一個鏈表,兩把重入鎖,兩個condition。condition其實是監視器接口,類似於object的notify、wait這些方法,需要和lock配合,可以實現等待通知模式,這裏有兩把鎖分別創建兩個condition。
put方法源碼:
  public void put(E paramE)
    throws InterruptedException
  {
    if (paramE == null) {
      throw new NullPointerException();
    }
    int i = -1;
    Node localNode = new Node(paramE);
    ReentrantLock localReentrantLock = this.putLock;
    AtomicInteger localAtomicInteger = this.count;
    localReentrantLock.lockInterruptibly();
    try
    {
      while (localAtomicInteger.get() == this.capacity) {
        this.notFull.await();
      }
      enqueue(localNode);
      i = localAtomicInteger.getAndIncrement();
      if (i + 1 < this.capacity) {
        this.notFull.signal();
      }
    }
    finally
    {
      localReentrantLock.unlock();
    }
    if (i == 0) {
      signalNotEmpty();
    }
  }
可以看到當往隊列裏插入元素時,如果隊列不可用,會進入this.notFull.await()代碼。我們查看await源碼可以發現阻塞這一動作主要通過LockSupport.park(this)來實現:
public final void await()
      throws InterruptedException
    {
      if (Thread.interrupted()) {
        throw new InterruptedException();
      }
      AbstractQueuedSynchronizer.Node localNode = addConditionWaiter();
      int i = AbstractQueuedSynchronizer.this.fullyRelease(localNode);
      int j = 0;
      while (!AbstractQueuedSynchronizer.this.isOnSyncQueue(localNode))
      {
        LockSupport.park(this);//這行代碼!!!!!!
        if ((j = checkInterruptWhileWaiting(localNode)) != 0) {
          break;
        }
      }
      if ((AbstractQueuedSynchronizer.this.acquireQueued(localNode, i)) && (j != -1)) {
        j = 1;
      }
      if (localNode.nextWaiter != null) {
        unlinkCancelledWaiters();
      }
      if (j != 0) {
        reportInterruptAfterWait(j);
      }
    }
至於這個park這個方法實現原理我現在也不是很懂,以後這塊研究有點眉目了再寫一寫吧(看書上說這個方法是jvm實現的,調用了native方法)。回到put方法,可以看出當隊列不滿的時候將數據放入到隊列中,然後判斷count+1是否小於this.capacity,如果小於就喚起生產者線程,最後在finally模塊中釋放鎖,判斷隊列是否爲空,不爲空喚起消費者線程,告知可以取元素。
take方法源碼:
  public E take()
    throws InterruptedException
  {
    int i = -1;
    AtomicInteger localAtomicInteger = this.count;
    ReentrantLock localReentrantLock = this.takeLock;
    localReentrantLock.lockInterruptibly();
    Object localObject1;
    try
    {
      while (localAtomicInteger.get() == 0) {
        this.notEmpty.await();
      }
      localObject1 = dequeue();
      i = localAtomicInteger.getAndDecrement();
      if (i > 1) {
        this.notEmpty.signal();
      }
    }
    finally
    {
      localReentrantLock.unlock();
    }
    if (i == this.capacity) {
      signalNotFull();
    }
    return localObject1;
  }
take方法和put方法類似,如果爲空就是await方法,不爲空是讓進行出列操作,最後釋放鎖,返回object。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章