和朱曄一起復習Java併發(二):佇列

lovecindywang發表於2019-07-17

和朱曄一起復習Java併發(二):佇列

老樣子,我們還是從一些例子開始慢慢熟悉各種併發佇列。以看小說看故事的心態來學習不會顯得那麼枯燥而且更容易記憶深刻。

阻塞佇列的等待?

阻塞佇列最適合做的事情就是做為生產消費者的中間儲存,以抵抗生產者消費者速率不匹配的問題,不但是在速率不匹配的時候能夠有地方暫存任務,而且能在佇列滿或空的時候讓執行緒進行阻塞,讓出CPU的時間。這裡對於阻塞兩字加粗,是因為其實Java的執行緒在這個時候是等待(WAITING)狀態而不是阻塞(BLOCKED),這個容易引起歧義。

下面我們來寫一個程式比較一下阻塞和等待:

@Slf4j
public class BlockVsWait {

    Object locker = new Object();
    ArrayBlockingQueue<Integer> arrayBlockingQueue1 = new ArrayBlockingQueue<>(1);
    ArrayBlockingQueue<Integer> arrayBlockingQueue2 = new ArrayBlockingQueue<>(1);

    @Test
    public void test() throws InterruptedException {

        arrayBlockingQueue1.put(1);


        Thread waitOnTake = new Thread(() -> {
            synchronized (locker) {
                try {
                    arrayBlockingQueue2.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        waitOnTake.setName("waitOnTake");
        waitOnTake.start();

        Thread waitOnPut = new Thread(() -> {
            try {
                arrayBlockingQueue1.put(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        waitOnPut.setName("waitOnPut");
        waitOnPut.start();

        Thread block = new Thread(() -> {
            synchronized (locker) {
                log.info("OK");
            }
        });
        block.setName("block");
        block.start();

        block.join();
    }
}

在上面的程式碼裡,我們開啟了三個執行緒:

  • 一個是等待鎖
  • 一個是等待從佇列獲取資料
  • 一個是等待加入資料到佇列

執行程式之後,我們看一下執行緒的狀態,可以看到:

  • 等待鎖的block執行緒,處於BLOCKED狀態
  • 還有兩個被阻塞佇列阻塞的執行緒,處於WAITING狀態

image_1dfuu0lnapb61bek2rbmed19pp9.png-404.3kB

我們來檢視一下執行緒這兩種狀態的定義:

image_1dfuuun4k50e1cg13j017hp15jh16.png-229.8kB

通俗一點說,BLOCKED就是執行緒自己想做事情,但是很無奈只能等別人先把事情幹完,所以說是被阻塞,被動的,WAITING就是執行緒自己主動願意放棄CPU時間進行等待,等別人在合適的時候通知自己來繼續幹活,所以說是等待中,主動的。Blocking Queue其實是讓執行緒Waiting而不是Block。

生產消費

現在,我們使用阻塞佇列嘗試實現生產者消費者的功能。

首先,實現一個基類,通過一個開關來控制生產者消費者的執行:

@Slf4j
public abstract class Worker implements Runnable {
    protected volatile boolean enable = true;
    protected String name;
    protected BlockingQueue<Integer> queue;

    public Worker(String name, BlockingQueue<Integer> queue) {
        this.name = name;
        this.queue = queue;
    }

    public void stop() {
        this.enable = false;
        log.info("Stop:{}", name);
    }
}

然後實現生產者:

@Slf4j
public class Producer extends Worker {
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public Producer(String name, BlockingQueue<Integer> queue) {
        super(name, queue);
    }

    @Override
    public void run() {
        while (enable) {
            try {
                int value = atomicInteger.incrementAndGet();
                queue.put(value);
                log.info("size:{}, put:{}, enable:{}", queue.size(), value, enable);
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
            }
        }
        log.info("{} quit", name);
    }
}

只要開關開啟,生產者會無限進行資料生產,把資料加入佇列,生產者每100ms生產一個資料,這裡有一個計數器來提供要生產的資料。

下面實現消費者:

@Slf4j
public class Consumer extends Worker {

    private static AtomicInteger totalConsumedAfterShutdown = new AtomicInteger();

    public Consumer(String name, BlockingQueue<Integer> queue) {
        super(name, queue);
    }

    public static int totalConsumedAfterShutdown() {
        return totalConsumedAfterShutdown.get();
    }

    @Override
    public void run() {
        while (enable || queue.size() > 0) {
            try {
                Integer item = queue.take();
                log.info("size:{}, got:{}, enable:{}", queue.size(), item, enable);
                if (!enable) {
                    totalConsumedAfterShutdown.incrementAndGet();
                }
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
            }
        }
        log.info("{} quit", name);
    }
}

同樣,消費者也是在開關開啟或佇列中有資料的時候,會不斷進行資料消費。這裡我們有一個計數器用來統計開關關閉之後,消費者還能消費多少資料。消費者消費速度是200ms消費一次,明顯比生產者慢一半。通過這個配置我們可以想到,如果使用有界阻塞佇列的話,因為消費速度比生產速度慢,所以佇列會慢慢堆積一直到佇列滿,然後生產者執行緒被阻塞,我們來寫一個測試程式看看是不是這樣:

@Slf4j
public class ArrayBlockingQueueTest {

    @Test
    public void test() throws InterruptedException {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(50, false);
        List<Worker> workers = new ArrayList<>();
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            String name = "Producer" + i;
            Producer worker = new Producer(name, queue);
            workers.add(worker);
            Thread thread = new Thread(worker);
            thread.setName(name);
            threads.add(thread);
            thread.start();
        }
        for (int i = 0; i < 4; i++) {
            String name = "Consumer" + i;
            Consumer worker = new Consumer(name, queue);
            workers.add(worker);
            Thread thread = new Thread(worker);
            thread.setName(name);
            threads.add(thread);
            thread.start();
        }

        Executors.newSingleThreadScheduledExecutor().schedule(() -> {
            for (Worker worker : workers) {
                worker.stop();
            }
        }, 2, TimeUnit.SECONDS);

        for (Thread thread : threads) {
            thread.join();
        }
        log.info("totalConsumedAfterShutdown:{}", Consumer.totalConsumedAfterShutdown());
    }
}

在這段程式碼裡:

  • 我們使用了容量為50的有界阻塞佇列ArrayBlockingQueue作為容器
  • 生產者10個執行緒
  • 消費者4個執行緒
  • 2秒後關閉生產者和消費者(這個時候生產者應該不會繼續生產,但是消費者還會繼續消費)
  • 主執行緒等待所有生產者消費者執行完成
  • 最後輸出關閉後,消費者還能消費多少資料

部分執行結果如下:

12:59:34.609 [Producer7] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:33, put:40, enable:true
12:59:34.609 [Producer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:36, put:37, enable:true
12:59:34.609 [Producer8] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:35, put:36, enable:true
12:59:34.609 [Producer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:36, put:38, enable:true
12:59:34.609 [Producer6] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:34, put:39, enable:true
12:59:34.683 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:33, got:7, enable:true
12:59:34.683 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:34, got:6, enable:true
12:59:34.683 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:33, got:5, enable:true
12:59:34.687 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:32, got:8, enable:true
12:59:34.701 [Producer5] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:33, put:41, enable:true
12:59:34.701 [Producer4] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:35, put:42, enable:true
12:59:34.701 [Producer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:35, put:44, enable:true
12:59:34.701 [Producer9] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:36, put:43, enable:true
12:59:34.711 [Producer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:37, put:45, enable:true
12:59:34.714 [Producer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:41, put:46, enable:true
12:59:34.714 [Producer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:39, put:48, enable:true
12:59:34.714 [Producer8] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:40, put:50, enable:true
12:59:34.714 [Producer6] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:42, put:49, enable:true
12:59:34.714 [Producer7] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:38, put:47, enable:true
12:59:34.805 [Producer4] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:45, put:53, enable:true
12:59:34.805 [Producer5] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:43, put:51, enable:true
12:59:34.805 [Producer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:44, put:52, enable:true
12:59:34.805 [Producer9] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:46, put:54, enable:true
12:59:34.814 [Producer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:47, put:55, enable:true
12:59:34.818 [Producer8] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:58, enable:true
12:59:34.818 [Producer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:57, enable:true
12:59:34.818 [Producer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:56, enable:true
12:59:34.888 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:47, got:12, enable:true
12:59:34.888 [Producer7] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:60, enable:true
12:59:34.888 [Producer6] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:59, enable:true
12:59:34.887 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:49, got:9, enable:true
12:59:34.887 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:10, enable:true
12:59:34.892 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:11, enable:true
12:59:34.909 [Producer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:62, enable:true
12:59:34.909 [Producer5] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:61, enable:true
12:59:35.093 [Producer9] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:64, enable:true
12:59:35.093 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:49, got:13, enable:true
12:59:35.094 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:47, got:16, enable:true
12:59:35.094 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:49, got:17, enable:true
12:59:35.094 [Producer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:65, enable:true
12:59:35.094 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:49, got:18, enable:true
12:59:35.094 [Producer8] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:66, enable:true
12:59:35.094 [Producer4] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:63, enable:true
12:59:35.297 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:49, got:19, enable:true
12:59:35.298 [Producer7] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:69, enable:true
12:59:35.298 [Producer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:47, put:68, enable:true
12:59:35.298 [Producer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:67, enable:true
12:59:35.298 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:46, got:20, enable:true
12:59:35.298 [Producer6] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:70, enable:true
12:59:35.298 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:47, got:15, enable:true
12:59:35.298 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:14, enable:true
12:59:35.502 [Producer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:74, enable:true
12:59:35.502 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:47, got:24, enable:true
12:59:35.502 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:49, got:22, enable:true
12:59:35.502 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:49, got:23, enable:true
12:59:35.502 [Producer9] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:73, enable:true
12:59:35.502 [Producer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:72, enable:true
12:59:35.502 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:21, enable:true
12:59:35.502 [Producer5] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:71, enable:true
12:59:35.704 [Producer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:77, enable:true
12:59:35.704 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:47, got:30, enable:true
12:59:35.704 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:28, enable:true
12:59:35.704 [Producer8] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:75, enable:true
12:59:35.704 [Producer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:80, enable:true
12:59:35.704 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:27, enable:true
12:59:35.704 [Producer4] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:76, enable:true
12:59:35.704 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:29, enable:true
12:59:35.909 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:32, enable:true
12:59:35.909 [Producer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:84, enable:true
12:59:35.909 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:49, got:25, enable:true
12:59:35.909 [Producer6] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:79, enable:true
12:59:35.909 [Producer7] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:78, enable:true
12:59:35.909 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:47, got:33, enable:true
12:59:35.909 [Producer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:83, enable:true
12:59:35.909 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:49, got:26, enable:true
12:59:36.113 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:34, enable:true
12:59:36.113 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:35, enable:true
12:59:36.113 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:31, enable:true
12:59:36.113 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:47, got:38, enable:true
12:59:36.113 [Producer5] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:81, enable:true
12:59:36.113 [Producer9] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:82, enable:true
12:59:36.114 [Producer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:87, enable:true
12:59:36.114 [Producer8] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:85, enable:true
12:59:36.313 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Producer0
12:59:36.313 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Producer1
12:59:36.313 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Producer2
12:59:36.313 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Producer3
12:59:36.313 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Producer4
12:59:36.313 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Producer5
12:59:36.313 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Producer6
12:59:36.313 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Producer7
12:59:36.313 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Producer8
12:59:36.314 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Producer9
12:59:36.314 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Consumer0
12:59:36.314 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Consumer1
12:59:36.314 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Consumer2
12:59:36.314 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Worker - Stop:Consumer3
12:59:36.317 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:39, enable:false
12:59:36.317 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:47, got:36, enable:false
12:59:36.317 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:46, got:37, enable:false
12:59:36.317 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:40, enable:false
12:59:36.317 [Producer4] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:47, put:86, enable:false
12:59:36.317 [Producer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:88, enable:false
12:59:36.317 [Producer6] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:92, enable:false
12:59:36.317 [Producer7] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:91, enable:false
12:59:36.420 [Producer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - Producer1 quit
12:59:36.420 [Producer6] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - Producer6 quit
12:59:36.420 [Producer7] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - Producer7 quit
12:59:36.420 [Producer4] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - Producer4 quit
12:59:36.522 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:41, enable:false
12:59:36.522 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:47, got:44, enable:false
12:59:36.522 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:43, enable:false
12:59:36.522 [Producer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:96, enable:false
12:59:36.522 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:49, got:42, enable:false
12:59:36.522 [Producer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:49, put:90, enable:false
12:59:36.522 [Producer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:89, enable:false
12:59:36.522 [Producer8] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:50, put:93, enable:false
12:59:36.626 [Producer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - Producer2 quit
12:59:36.626 [Producer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - Producer0 quit
12:59:36.626 [Producer8] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - Producer8 quit
12:59:36.626 [Producer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - Producer3 quit
12:59:36.725 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:49, got:45, enable:false
12:59:36.726 [Producer9] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:95, enable:false
12:59:36.726 [Producer5] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - size:48, put:94, enable:false
12:59:36.726 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:47, got:50, enable:false
12:59:36.725 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:48, got:47, enable:false
12:59:36.726 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:47, got:48, enable:false
12:59:36.829 [Producer5] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - Producer5 quit
12:59:36.829 [Producer9] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Producer - Producer9 quit
12:59:36.930 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:46, got:49, enable:false
12:59:36.930 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:44, got:46, enable:false
12:59:36.930 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:45, got:51, enable:false
12:59:36.930 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:44, got:52, enable:false
12:59:37.133 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:42, got:54, enable:false
12:59:37.133 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:40, got:57, enable:false
12:59:37.133 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:40, got:53, enable:false
12:59:37.133 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:41, got:55, enable:false
12:59:37.334 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:37, got:59, enable:false
12:59:37.334 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:36, got:56, enable:false
12:59:37.334 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:36, got:60, enable:false
12:59:37.334 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:37, got:58, enable:false
12:59:37.538 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:34, got:61, enable:false
12:59:37.538 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:32, got:63, enable:false
12:59:37.538 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:33, got:64, enable:false
12:59:37.539 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:32, got:62, enable:false
12:59:37.742 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:29, got:68, enable:false
12:59:37.742 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:30, got:65, enable:false
12:59:37.742 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:30, got:66, enable:false
12:59:37.742 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:28, got:67, enable:false
12:59:37.948 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:26, got:70, enable:false
12:59:37.948 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:24, got:69, enable:false
12:59:37.948 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:24, got:72, enable:false
12:59:37.948 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:25, got:71, enable:false
12:59:38.149 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:21, got:75, enable:false
12:59:38.149 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:20, got:76, enable:false
12:59:38.149 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:22, got:74, enable:false
12:59:38.149 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:20, got:73, enable:false
12:59:38.350 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:18, got:80, enable:false
12:59:38.350 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:18, got:77, enable:false
12:59:38.350 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:16, got:79, enable:false
12:59:38.350 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:17, got:78, enable:false
12:59:38.553 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:12, got:83, enable:false
12:59:38.553 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:14, got:84, enable:false
12:59:38.553 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:13, got:82, enable:false
12:59:38.553 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:12, got:81, enable:false
12:59:38.759 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:8, got:87, enable:false
12:59:38.759 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:8, got:88, enable:false
12:59:38.759 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:8, got:86, enable:false
12:59:38.759 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:8, got:85, enable:false
12:59:38.960 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:7, got:92, enable:false
12:59:38.963 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:5, got:89, enable:false
12:59:38.963 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:4, got:90, enable:false
12:59:38.963 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:5, got:91, enable:false
12:59:39.161 [Consumer0] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:3, got:96, enable:false
12:59:39.168 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:1, got:93, enable:false
12:59:39.168 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:0, got:94, enable:false
12:59:39.168 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - size:1, got:95, enable:false
12:59:39.168 [Consumer2] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - Consumer2 quit
12:59:39.168 [Consumer1] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - Consumer1 quit
12:59:39.168 [Consumer3] INFO me.josephzhu.javaconcurrenttest.concurrent.queues.Consumer - Consumer3 quit

從結果看到幾個結論:

  • 在佇列滿之前,生產者可以任意按照自己的速度生產,滿了之後只能等消費者消費後才能進行生產,符合預期
  • 關閉開啟設定後,生產者很快就都完成了,但是最後消費者只退出了3個,有一個卡住了,執行緒狀態如下:

image_1dfv50m5tfre14f0hh1k2gtda9.png-133.7kB

當然這個狀態不那麼容易碰巧遇到,我執行了20+次程式碼才遇到一次,你也可以把sleep移到前面去這樣更容易出現這樣的問題。
細細品味一下為什麼有一個消費者卡住了,我們不是判斷了佇列中有資料才繼續執行take()的嗎?問題就出在這裡,在判斷的時候佇列中的確有資料,看看Consumer0最後輸出了3,但是在這之後的瞬間,還有3條資料都被其它執行緒消費完了,等到執行下一行程式碼的時候就卡住了。在編寫多執行緒程式的時候,我們很容易去假設:

  • 兩行靠在一起的程式碼就是能在一個原子操作內完成的,不是這樣的,在之後的文章中我們會繼續看到更有意思的一個錯覺
  • 既然使用了執行緒安全的佇列,那麼所有操作都是執行緒安全的一致的,這個說法也是一個誤區,首先,我們無法確保所有操作都是執行緒安全以及一致的,具體需要參考JDK的文件說明,比如迭代操作,比如size()操作,很對執行緒安全的併發型別也無法提供一致性的保證,有的時候只是估算;其次,所謂所有操作僅限於單個操作,一般而言容器無法確保你兩個操作兩行程式碼之間不能有其它執行緒來繼續操作這個容器

這個Bug是很容易忽略的,我們可以改一下消費者程式碼,利用有超時等待的poll()來解決這個問題:

@Override
public void run() {
    while (enable || queue.size() > 0) {
        try {
            Integer item = queue.poll(1, TimeUnit.SECONDS);
            log.info("size:{}, got:{}, enable:{}", queue.size(), item, enable);
            if (!enable && item != null) {
                totalConsumedAfterShutdown.incrementAndGet();
            }
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
        }
    }
    log.info("{} quit", name);
}

修改主程式後可以得到下面的結果:
image_1dfv633k69a41g901hfvi911a5612.png-592.6kB
值得注意幾點:

  • 這次Consumer3沒有永遠卡住,而是在等待了1秒後超時了,沒有拿到資料
  • 最後輸出的totalConsumedAfterShutdown是60而不是最大佇列50,這個也很容易想到為什麼,enable=false之後,之前那10個生產者當前的迴圈還會繼續執行,把資料加入佇列,但是這個結果永遠只會是60(50+10生產者)嗎?你可以想想

佇列各種方法執行速度比拼

前面我們也看到了,佇列消費的操作可以take()可以poll(),各種操作的區別如下:

image_1dfv6drpq4c3pfu14j254p1m031i.png-39.8kB

  • 丟擲異常就是在操作失敗的時候直接丟擲異常
  • 特殊值就是不能執行操作的時候返回false或null
  • 阻塞就是執行緒進行等待狀態等待可以操作為止
  • 超時就是等待一定時間不行的話再放棄

這些操作之間的效能是否有區別呢,我們寫一個簡單的程式測試一下

@Slf4j
public class QueueBenchmark {

    int taskCount = 20000000;
    int threadCount = 10;

    @Test
    public void test() throws InterruptedException {

        List<Queue<Integer>> queues = getQueues();
        benchmark("add", queues, taskCount, threadCount);
        benchmark("poll", queues, taskCount, threadCount);
        benchmark("offer", queues, taskCount, threadCount);
        benchmark("size", queues, taskCount, threadCount);
        benchmark("remove", queues, taskCount, threadCount);
    }

    private List<Queue<Integer>> getQueues() {
        return Arrays.asList(new ConcurrentLinkedQueue<>(),
                new LinkedBlockingQueue<>(),
                new ArrayBlockingQueue<>(taskCount, false),
                new LinkedTransferQueue<>(),
                new PriorityBlockingQueue<>(),
                new LinkedList<>());
    }

    private void benchmark(String operation, List<Queue<Integer>> queues, int taskCount, int threadCount) throws InterruptedException {
        StopWatch stopWatch = new StopWatch();
        queues.forEach(queue -> {
            stopWatch.start(queue.getClass().getSimpleName() + "-" + operation);
            try {
                tasks(queue, taskCount, threadCount, operation);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stopWatch.stop();
            log.info("queue:{}, operation:{}, size:{}, qps:{}", queue.getClass().getSimpleName(), operation, queue.size(), (long) taskCount * 1000 / stopWatch.getLastTaskTimeMillis());
        });
        log.info(stopWatch.prettyPrint());
    }

    private void tasks(Queue<Integer> queue, int taskCount, int threadCount, String operation) throws InterruptedException {
        ForkJoinPool forkJoinPool = new ForkJoinPool(threadCount);
        forkJoinPool.execute(() -> IntStream.rangeClosed(1, taskCount).parallel().forEach(i -> {
                    IntConsumer opt = task(queue, operation);
                    if (queue instanceof LinkedList) {
                        synchronized (queue) {
                            opt.accept(i);
                        }
                    } else {
                        opt.accept(i);
                    }
                }
        ));
        forkJoinPool.shutdown();
        forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
    }

    private IntConsumer task(Queue<Integer> queue, String name) {
        if (name.equals("add")) return queue::add;
        if (name.equals("offer")) return queue::offer;
        if (name.equals("poll")) return i -> queue.poll();
        if (name.equals("remove")) return i -> queue.remove();
        if (name.equals("size")) return i -> queue.size();

        return i -> {
        };
    }
}

在程式碼裡,我們測試10個執行緒下,對各種佇列的各種方法執行N次操作的耗時。
結論如下,表格中資料的單位毫秒,也就是耗時,數字越小效能越好:
image_1dfv8o0so167fmdf1kticm1r5h2p.png-42.7kB

有幾個地方值得注意:

  • ConcurrentLinkedQueue以及LinkedTransferQueue的size()操作特別慢,見JDK說明:
    image_1dfv8bmqgpg82vf15m4os01qjp1v.png-54.8kB
    所以我們在使用這兩種佇列的時候特別需要注意
  • 總體上來說,add相對於offer,poll相對於remove沒有什麼效能差異,根據自己的需求使用對應的方法即可

下面我們稍微改下程式碼測試一下BlockingQueue的put()和take():

@Slf4j
public class BlockingQueueBenchmark {

    int taskCount = 20000000;
    int threadCount = 10;

    @Test
    public void test() throws InterruptedException {

        List<BlockingQueue<Integer>> queues = getQueues();
        benchmark("put", queues, taskCount, threadCount);
        benchmark("take", queues, taskCount, threadCount);
    }

    private List<BlockingQueue<Integer>> getQueues() {
        return Arrays.asList(
                new LinkedBlockingQueue<>(),
                new LinkedTransferQueue<>(),
                new ArrayBlockingQueue<>(taskCount, false),
                new PriorityBlockingQueue<>());
    }

    private void benchmark(String operation, List<BlockingQueue<Integer>> queues, int taskCount, int threadCount) throws InterruptedException {
        StopWatch stopWatch = new StopWatch();
        queues.forEach(queue -> {
            stopWatch.start(queue.getClass().getSimpleName() + "-" + operation);
            try {
                tasks(queue, taskCount, threadCount, operation);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stopWatch.stop();
            log.info("queue:{}, operation:{}, size:{}", queue.getClass().getSimpleName(), operation, queue.size());
        });
        log.info(stopWatch.prettyPrint());
    }

    private void tasks(BlockingQueue<Integer> queue, int taskCount, int threadCount, String operation) throws InterruptedException {
        ForkJoinPool forkJoinPool = new ForkJoinPool(threadCount);
        forkJoinPool.execute(() -> IntStream.rangeClosed(1, taskCount).parallel().forEach(task(queue, operation)));
        forkJoinPool.shutdown();
        forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
    }

    private IntConsumer task(BlockingQueue<Integer> queue, String name) {
        if (name.equals("put")) return i -> {
            try {
                queue.put(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        if (name.equals("take")) return i -> {
            try {
                queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        return i -> {
        };
    }
}

把結果一起完善到前面表格中:
image_1dfv9l78mf9a1khp1uqgn53199e36.png-51.9kB
可以看到,阻塞的方法和非阻塞的效能差不多,也是根據需要選擇即可。看程式碼實現的話也可以看到很多佇列對於各種存取方法邏輯基本是一致的。
各個佇列之間的效能貌似區別不大,我感覺這個測試寫的不是很好,可能和執行緒池的排程也有關係,我們接下去再重新換一種測試方式來測試下各種佇列的吞吐。

各種場景下各種佇列的吞吐測試

在這次的測試中,我們模擬一下場景:

@Data
@AllArgsConstructor
@NoArgsConstructor
class TestCase {
    private int elementCount;
    private Mode mode;
    private int producerCount;
    private int consumerCount;
}

模擬一下不同的消費者生產者執行緒數量配比的情況下,各種佇列完成一定數量元素的存取操作總共的耗時。我們定義三種模式:

  • ProducerAndConsumerShareThread:也就是存取操作在一個執行緒中完成,先存後取
  • ProducerAndThenConsumer:也就是先把佇列用生產者填充完畢,然後再用消費者去全部讀取出來
  • ConcurrentProducerAndConsumer:也就是生產者和消費者同時操作佇列,同時進行存和取操作
enum Mode {
    ProducerAndConsumerShareThread,
    ProducerAndThenConsumer,
    ConcurrentProducerAndConsumer
}

我們定義的所有測試場景如下:

  List<TestCase> testCases = new ArrayList<>();
        testCases.add(new TestCase(element_count, Mode.ConcurrentProducerAndConsumer, 1, 1));
        testCases.add(new TestCase(element_count, Mode.ConcurrentProducerAndConsumer, 10, 10));
        testCases.add(new TestCase(element_count, Mode.ConcurrentProducerAndConsumer, 100, 100));
        testCases.add(new TestCase(element_count, Mode.ConcurrentProducerAndConsumer, 1000, 1000));
        testCases.add(new TestCase(element_count, Mode.ConcurrentProducerAndConsumer, Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors()));

        testCases.add(new TestCase(element_count, Mode.ConcurrentProducerAndConsumer, 1, 100));
        testCases.add(new TestCase(element_count, Mode.ConcurrentProducerAndConsumer, 100, 1));

        testCases.add(new TestCase(element_count, Mode.ProducerAndConsumerShareThread, 1, 0));
        testCases.add(new TestCase(element_count, Mode.ProducerAndConsumerShareThread, 10, 0));
        testCases.add(new TestCase(element_count, Mode.ProducerAndConsumerShareThread, 100, 0));
        testCases.add(new TestCase(element_count, Mode.ProducerAndConsumerShareThread, 1000, 0));
        testCases.add(new TestCase(element_count, Mode.ProducerAndConsumerShareThread, Runtime.getRuntime().availableProcessors(), 0));


        testCases.add(new TestCase(element_count, Mode.ProducerAndThenConsumer, 1, 1));
        testCases.add(new TestCase(element_count, Mode.ProducerAndThenConsumer, 10, 10));
        testCases.add(new TestCase(element_count, Mode.ProducerAndThenConsumer, 100, 100));
        testCases.add(new TestCase(element_count, Mode.ProducerAndThenConsumer, 1000, 1000));
        testCases.add(new TestCase(element_count, Mode.ProducerAndThenConsumer, Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors()));

十幾種測試,覆蓋這些場景:

  • 同時存取模式下不同生產者和消費者執行緒數量的情況
  • 同時存取模式下生產者和消費者數量不均衡的情況
  • 先存後取模式下不同生產者和消費者執行緒數量的情況
  • 存取操作在一個執行緒依次操作模式下不同執行緒數量的情況

主要測試三種佇列,每一種佇列測試之間GC一次儘量排除干擾:

LinkedBlockingQueue<String> linkedBlockingQueue = new LinkedBlockingQueue<>();
for (TestCase testCase : testCases) {
    System.gc();
    benchmark(linkedBlockingQueue, testCase);
}
linkedBlockingQueue = null;

LinkedTransferQueue<String> linkedTransferQueue = new LinkedTransferQueue<>();
for (TestCase testCase : testCases) {
    System.gc();
    benchmark(linkedTransferQueue, testCase);
}
linkedTransferQueue = null;

ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(element_count);
for (TestCase testCase : testCases) {
    System.gc();
    benchmark(arrayBlockingQueue, testCase);
}
arrayBlockingQueue = null;

生產者:

class ProducerTask implements Runnable {

    private String name;
    private BlockingQueue<String> queue;
    private TestCase testCase;
    private CountDownLatch startCountDownLatch;
    private CountDownLatch finishCountDownLatch;

    public ProducerTask(CountDownLatch startCountDownLatch,
                        CountDownLatch finishCountDownLatch,
                        String name,
                        BlockingQueue<String> queue,
                        TestCase testCase) {
        this.startCountDownLatch = startCountDownLatch;
        this.finishCountDownLatch = finishCountDownLatch;
        this.name = name;
        this.queue = queue;
        this.testCase = testCase;
    }

    @Override
    public void run() {

        try {
            startCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        int count = testCase.elementCount / testCase.getProducerCount();

        if (testCase.mode == Mode.ProducerAndConsumerShareThread) {
            for (int i = 0; i < count; i++) {
                try {
                    queue.put(name + i);
                    queue.take();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        } else {
            for (int i = 0; i < count; i++) {
                try {
                    queue.put(name + i);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        finishCountDownLatch.countDown();
    }
}

這次的測試,我們預先根據執行緒數量算好執行次數,而不是像之前的測試一樣所有的任務統一由執行緒池排程,這樣更容易測試出佇列本身的效能,排除干擾。這裡可以看到如果是存取共享模式的話,生產者直接做存取操作,其它模式的話,生產者僅僅做存的操作。

消費者:

class ConsumerTask implements Runnable {

    private BlockingQueue<String> queue;
    private TestCase testCase;
    private CountDownLatch startCountDownLatch;
    private CountDownLatch finishCountDownLatch;

    public ConsumerTask(CountDownLatch startCountDownLatch,
                        CountDownLatch finishCountDownLatch,
                        BlockingQueue<String> queue,
                        TestCase testCase) {
        this.startCountDownLatch = startCountDownLatch;
        this.finishCountDownLatch = finishCountDownLatch;
        this.queue = queue;
        this.testCase = testCase;
    }

    @Override
    public void run() {
        try {
            startCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int count = testCase.elementCount / testCase.getConsumerCount();

        if (testCase.mode != Mode.ProducerAndConsumerShareThread) {
            for (int i = 0; i < count; i++) {
                try {
                    queue.take();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        finishCountDownLatch.countDown();
    }
}

生產者和消費者我們都用了兩個CountDownLatch來做攔截,一個startCountDownLatch用來在所有執行緒都啟動後由主執行緒通知一下子放開所有的執行緒,一個finishCountDownLatch用來讓主執行緒等待執行緒的執行完畢。

主要的測試程式碼如下:

private void benchmark(BlockingQueue<String> queue, TestCase testCase) throws InterruptedException {

        long begin = System.currentTimeMillis();
        log.info("\r\n==========================\r\nBegin benchmark Queue:[{}], case:{}", queue.getClass().getSimpleName(),
                testCase.toString());
        CountDownLatch startCountDownLatch = new CountDownLatch(1);

        if (testCase.mode == Mode.ProducerAndConsumerShareThread) {
            CountDownLatch finishCountDownLatch = new CountDownLatch(testCase.getProducerCount());
            for (int i = 0; i < testCase.getProducerCount(); i++) {
                new Thread(new ProducerTask(
                        startCountDownLatch,
                        finishCountDownLatch,
                        String.format("Thread_%d_", i),
                        queue,
                        testCase)).start();
            }
            startCountDownLatch.countDown();
            finishCountDownLatch.await();

        } else if (testCase.mode == Mode.ConcurrentProducerAndConsumer) {
            CountDownLatch finishCountDownLatch = new CountDownLatch(testCase.getProducerCount() + testCase.getConsumerCount());
            for (int i = 0; i < testCase.getProducerCount(); i++) {
                new Thread(new ProducerTask(
                        startCountDownLatch,
                        finishCountDownLatch,
                        String.format("Thread_%d_", i),
                        queue,
                        testCase)).start();
            }
            for (int i = 0; i < testCase.getConsumerCount(); i++) {
                new Thread(new ConsumerTask(
                        startCountDownLatch,
                        finishCountDownLatch,
                        queue,
                        testCase)).start();
            }
            startCountDownLatch.countDown();
            finishCountDownLatch.await();
        } else if (testCase.mode == Mode.ProducerAndThenConsumer) {
            CountDownLatch finishCountDownLatch = new CountDownLatch(testCase.getProducerCount());
            for (int i = 0; i < testCase.getProducerCount(); i++) {
                new Thread(new ProducerTask(
                        startCountDownLatch,
                        finishCountDownLatch,
                        String.format("Thread_%d_", i),
                        queue,
                        testCase)).start();
            }
            startCountDownLatch.countDown();
            finishCountDownLatch.await();

            startCountDownLatch = new CountDownLatch(1);
            finishCountDownLatch = new CountDownLatch(testCase.getConsumerCount());
            for (int i = 0; i < testCase.getConsumerCount(); i++) {
                new Thread(new ConsumerTask(
                        startCountDownLatch,
                        finishCountDownLatch,
                        queue,
                        testCase)).start();
            }
            startCountDownLatch.countDown();
            finishCountDownLatch.await();
        }

        long finish = System.currentTimeMillis();
        log.info("Finish benchmark Queue:[{}], case:{}, QPS:{}\r\n==========================\n", queue.getClass().getSimpleName(),
                testCase.toString(),
                (long) element_count * 1000 / (finish - begin));
    }

可以看到三種模式的處理不同:

  • 對於存取共享執行緒的話,我們只有生產者執行緒
  • 對於先存後取模式的話,在所有生產者執行緒執行完成後我們再開啟消費者執行緒
  • 對於併發存取模式的話,我們同時開啟兩組執行緒

整個測試結果彙總如下(這個測試是在12核阿里雲跑出來的,元素數1000萬):

image_1dfvhc5g31vrj1dtbehf35qove4q.png-129.9kB

說實話這個測試的結果不是我想象的那樣,我想象的是隨著併發的增多佇列效能會急劇下降,而且各種佇列之間有顯著的效能差異,這個結果是這樣這也可以說明這些佇列效能都是很不錯的,沒有明顯的短板。

可以大概得出幾個結論:

  • 隨著併發的增多會降低一些吞吐,不過也都還好,併發太小吞吐也上不去
  • ArrayBlockingQueue效能穩定,而且效能也幾乎是最好的
  • 在生產者數量大大小於消費者數量的時候,LinkedBlockingQueue表現出最好的吞吐,而且比其它兩個好很多,這點我還沒細究,有待研究是為什麼

一般而言,阻塞佇列中,無界佇列可以選擇LinkedBlockingQueue,有界佇列可以選擇ArrayBlockingQueue,後者還有公平引數可以開啟公平特性,有關這個特性下面我們也會來觀察。

通過同步佇列觀察公平特性

SynchronousQueue是沒有容量的阻塞佇列,只有等另一個執行緒移出元素後才能插入元素成功。這裡我們寫一段程式碼來測試,沿用之前的消費者和生產者類,只是修改了2秒後關閉佇列的地方,這裡我們加上了interrupt()操作,否則生產者是無法退出的:

@Slf4j
public class SynchronousQueueTest {

    @Test
    public void test() throws InterruptedException {
        SynchronousQueue<Integer> queue = new SynchronousQueue<>(false);
        List<Worker> workers = new ArrayList<>();
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            String name = "Producer" + i;
            Producer worker = new Producer(name, queue);
            workers.add(worker);
            Thread thread = new Thread(worker);
            thread.setName(name);
            threads.add(thread);
            thread.start();
        }
        for (int i = 0; i < 4; i++) {
            String name = "Consumer" + i;
            Consumer worker = new Consumer(name, queue);
            workers.add(worker);
            Thread thread = new Thread(worker);
            thread.setName(name);
            threads.add(thread);
            thread.start();
        }

        Executors.newSingleThreadScheduledExecutor().schedule(() -> {
            for (Worker worker : workers) {
                worker.stop();
            }
            for (Thread thread : threads) {
                thread.interrupt();
            }
        }, 2, TimeUnit.SECONDS);

        for (Thread thread : threads) {
            thread.join();
        }
    }
}

我們先把公平引數設定為false看看輸出:
image_1dfvik2puns51b2b1qnev0o8ni57.png-224kB
搜尋日誌可以發現找不到Producer0~Producer5這6個生產者的蹤跡,因為沒有消費者來拉取它們的資料,它們都卡住了,這些生產者都餓死了,日誌中最小的put也是從7開始的。改為公平模式試試:
image_1dfviuu5revv1l4019ff1o971n6l74.png-233.5kB
這次可以找到所有生產者的日誌,公平模式也就是所有等待的執行緒FIFO次序來訪問佇列:
image_1dfvj1gm410g8crvh661fpuuft7h.png-56.1kB

延遲佇列

這裡給出一個延遲佇列的例子,我們往佇列提交10次延遲訊息,每次提交2條一樣的訊息,訊息的絕對延遲時間從1到10秒。

@Slf4j
public class DelayQueueTest {

    @Test
    public void test() throws InterruptedException {
        DelayQueue<Message> delayQueue = new DelayQueue<>();
        IntStream.rangeClosed(1, 10).forEach(i -> {
            for (int __ = 0; __ < 2; __++)
                delayQueue.add(new Message(i * 1000));
        });

        Executors.newFixedThreadPool(1).submit(() -> {
            while (true) {
                Message message = delayQueue.take();
                log.debug("Got:{}", message);
            }
        });

        TimeUnit.SECONDS.sleep(20);
    }


    @ToString
    class Message implements Delayed {

        private final long delay;
        private final long expire;

        public Message(long delay) {
            this.delay = delay;
            expire = System.currentTimeMillis() + delay;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            //log.debug("getDelay called : {}", unit);
            return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(Delayed o) {
            return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
        }
    }
}

輸出如下:

17:14:43.957 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=1000, expire=1563354883947)
17:14:44.007 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=1000, expire=1563354883947)
17:14:44.953 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=2000, expire=1563354884949)
17:14:44.953 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=2000, expire=1563354884949)
17:14:45.954 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=3000, expire=1563354885949)
17:14:45.954 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=3000, expire=1563354885949)
17:14:46.956 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=4000, expire=1563354886949)
17:14:46.956 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=4000, expire=1563354886949)
17:14:47.953 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=5000, expire=1563354887949)
17:14:47.953 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=5000, expire=1563354887949)
17:14:48.953 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=6000, expire=1563354888949)
17:14:48.953 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=6000, expire=1563354888949)
17:14:49.954 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=7000, expire=1563354889949)
17:14:49.954 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=7000, expire=1563354889949)
17:14:50.954 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=8000, expire=1563354890949)
17:14:50.955 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=8000, expire=1563354890949)
17:14:51.953 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=9000, expire=1563354891949)
17:14:51.953 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=9000, expire=1563354891949)
17:14:52.953 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=10000, expire=1563354892949)
17:14:52.953 [pool-1-thread-1] DEBUG me.josephzhu.javaconcurrenttest.concurrent.queues.DelayQueueTest - Got:DelayQueueTest.Message(delay=10000, expire=1563354892949)

可以看到每過1秒輸出2條日誌,符合預期。

一個真實的佇列誤用的血案

之前生產上遇到過一個OOM的問題,排查下來是佇列使用不當,這裡我們就來看下這個問題,程式碼邏輯是:

  • 我們有一個10個執行緒的執行緒池
  • 我們使用了LinkedTransferQueue阻塞佇列
  • 我們通過執行緒池非同步向這個佇列提交4000個任務
  • 我們通過執行緒池非同步從這個佇列獲取4000個任務

比較特殊的是,使用了transfer()方法,開發的小夥伴可能覺得LinkedTransferQueue比較酷炫,所以選擇了這個佇列,並且認為transfer()可以直接把任務交給消費者效能較高,所以使用了這個方法。

image_1dfvkan7url27sg3ig3of15j19u.png-140.7kB

程式碼如下:

@Slf4j
public class BlockingQueueMisuse {

    LinkedTransferQueue<String> linkedTransferQueue = new LinkedTransferQueue<>();

    @Test
    public void test() throws InterruptedException {
        int taskCount = 4000;
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("misuse");
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        //ExecutorService threadPool = Executors.newCachedThreadPool();
        IntStream.rangeClosed(1, taskCount).forEach(i -> threadPool.submit(() -> {
            try {
                linkedTransferQueue.transfer("message" + i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }));
        IntStream.rangeClosed(1, taskCount).forEach(i -> threadPool.submit(() -> {
            try {
                log.debug("Got:{}", linkedTransferQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }));
        threadPool.shutdown();
        threadPool.awaitTermination(1, TimeUnit.HOURS);
        stopWatch.stop();
        log.info(stopWatch.prettyPrint());
    }
}

執行程式後發現沒有任何輸出,其實這是因為只有10個執行緒,生產者需要存的元素數量是4000大大超過了10,所有執行緒都在等待:
image_1dfvknr245v01709123e94vmurar.png-261.9kB

於是,他沒多想把執行緒池修改為了newCachedThreadPool,程式可以正常執行了,看看執行結果:

image_1dfvktfut1uj0h8gke2g2s1pg5b8.png-89.9kB
這個程式碼是很嚇人的,執行過程中開啟了幾千個執行緒。我們想一下原因,其實newCachedThreadPool使用的是SynchronousQueue,在沒有可用執行緒的情況下就會新建執行緒,而這個特性遇上了transfer()的特性,就會導致執行緒池建立幾千個執行緒。

即使我們把程式碼修改為使用LinkedBlockingQueue,配合newCachedThreadPool也會建立幾十個執行緒(如果元素數量足夠多,幾百個幾千個也有可能)。因為一旦阻塞,newCachedThreadPool就會毫不猶豫建立新執行緒。

對於生產者消費者這種任務,還是建議直接使用執行緒來實現,生產者消費者的阻塞不相互干擾,而且執行緒池也是使用佇列來管理任務的,用了執行緒池相當於兩次佇列,沒有必要。

回顧總結

我們來看一下這次實驗涉及到的一些阻塞佇列:

  • ArrayBlockingQueue :一個由陣列結構組成的有界阻塞佇列。
  • LinkedBlockingQueue :一個由連結串列結構組成的有界阻塞佇列。
  • PriorityBlockingQueue :一個支援優先順序排序的無界阻塞佇列。
  • DelayQueue:一個使用優先順序佇列PriorityQueue實現的無界阻塞佇列。
  • SynchronousQueue:一個不儲存元素的阻塞佇列。
  • LinkedTransferQueue:一個由連結串列結構組成的無界阻塞佇列。

DelayQueue、SynchronousQueue和PriorityBlockingQueue是特種佇列,有特殊用途根據需要選擇。
LinkedTransferQueue也算是特種佇列,它可以實現類似背壓的效果,在特殊場景下使用。
ArrayBlockingQueue和LinkedBlockingQueue背後的資料結構不同,它們可能是我們最常用的佇列了,區別如下:

  • ArrayBlockingQueue有公平特性,開啟公平特性會降低吞吐,1000000次操作結果如下,前面一個是關閉公平,後面一個是開啟公平
    image_1dfvmkm9v1hmjto41e6p1ovs492bl.png-54.7kB
  • ArrayBlockingQueue會預分配儲存,但是這也意味著會一下子佔用大塊記憶體,LinkedBlockingQueue不是這樣的
  • 如果需要無界的話只能選擇LinkedBlockingQueue(當然LinkedBlockingQueue也可以有界)

非阻塞佇列ConcurrentLinkedQueue比較特殊,首先它不是阻塞佇列,其次它不使用鎖,而是使用CAS,在超高併發的場景下,顯然它可以到達更好的效能。

這裡利用之前的程式碼最後做了一次對比測試,這裡我們沒有測試併發存取模式,因為消費者不知道何時消費完畢,在消費不到資料的時候進行死迴圈意義不大:

image_1dfvo3359d6f14ah1qu21hu41bojc2.png-47.3kB

所以在特殊的場景下,比如生產者生產好了資料扔到佇列中,有N多個消費者需要併發消費這個時或許可以發揮ConcurrentLinkedQueue的威力(但是,之前也說過了,它的size()比較坑爹),常年處於空的佇列不太適合,這個時候使用阻塞佇列更合適。

好吧,看來90%的時候還是用ArrayBlockingQueue和LinkedBlockingQueue太平,有界用前者,需要無界用後者,但是認真考慮下,你真的需要無界嗎。通過我們的測試可以發現這些佇列在高併發下都有著百萬以上的QPS效能,一般而言用哪個都不會出現瓶頸,反而是我們更應該注意因為阻塞導致的執行緒數量增多和佇列的容量佔用的記憶體。

本文中,我們還花式使用了各種方式來測試佇列:

  • 普通執行緒池
  • ForkJoin
  • 獨立執行緒

這裡想說的是,對於生產消費這樣的任務最好還是使用阻塞佇列配置獨立的消費執行緒,生產者可以直接是業務執行緒,而不是去使用執行緒池,沒有這個必要。

同樣,程式碼見我的Github,歡迎clone後自己把玩,歡迎點贊。

歡迎關注我的微信公眾號:隨緣主人的園子

image_1dfvp8d55spm14t7erkr3mdbscf.png-45kB

相關文章