使用執行緒執行框架的一次經歷

Chown發表於2016-10-16

場景

一個執行緒從某個地方接收訊息(資料),可以是其他主機或者訊息佇列,然後轉由另外的一個執行緒池來執行具體處理訊息的邏輯,並且訊息的處理速度小於接收訊息的速度。這種情景很常見,試想一下,你會怎麼設計和實現?

直觀想法

很顯然採用JUC的執行緒框架,可以迅速寫出程式碼。

訊息接收者:

public class Receiver {
    private static volatile boolean inited = false;
    private static volatile boolean shutdown = false;
    private static volatile int cnt = 0;

    private MessageHandler messageHandler;

    public void start(){
        Executors.newSingleThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {
                while(!shutdown){
                    init();
                    recv();
                }
            }
        });
    }

    /**
     * 模擬訊息接收
     */
    public void recv(){
            Message msg = new Message("Msg" + System.currentTimeMillis());
            System.out.println(String.format("接收到訊息(%d): %s", ++cnt, msg));
            messageHandler.handle(msg);
    }

    public void init(){
        if(!inited){
            messageHandler = new MessageHandler();
            inited = true;
        }
    }

    public static void main(String[] args) {
        new Receiver().start();
    }
}

訊息處理:

public class MessageHandler {

    private static final int THREAD_POOL_SIZE = 4;

    private ExecutorService service = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

    public void handle(Message msg) {
        try {
            service.execute(new Runnable() {
                @Override
                public void run() {
                    parseMsg(msg);
                }
            });
        } catch (Throwable e) {
            System.out.println("訊息處理異常" + e);
        }
    }

    /**
     * 比較耗時的訊息處理流程
     */
    public void parseMsg(Message message) {
        while (true) {
            try {
                System.out.println("解析訊息:" + message);
                Thread.sleep(5000);
                System.out.println("============================");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

效果:這種方案導致的現象是接收到的訊息會迅速堆積,我們從訊息佇列(或者其他地方)取出了大量訊息,但是處理執行緒的速度又跟不上,所以導致的問題是大量的Task會堆積線上程池底層維護的一個阻塞佇列中,這會極大的耗費儲存空間,影響系統的效能。

分析:當execute()一個任務的時候,如果有空閒的worker執行緒,那麼投入執行,否則看設定的最大執行緒個數,沒有達到執行緒個數限制就建立新執行緒,接新任務,否則就把任務緩衝到一個阻塞佇列中,問題就是這個佇列,預設的大小是沒有限制的,所以就會大量的堆積任務,必然耗費heap空間。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE); // capacity
    }

計數限制

面對上述問題,想到了要限制訊息接收的速度,自然就想到了各種執行緒同步的原語,不過在這裡最簡單的就是使用一個Volatile的計數器。

訊息接收者:

public class Receiver {
    private static volatile boolean inited = false;
    private static volatile boolean shutdown = false;
    private static volatile int cnt = 0;
    private MessageHandler messageHandler;
    public void start(){
        Executors.newSingleThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {
                while(!shutdown){
                    init();
                    recv();
                }
            }
        });
    }

    /**
     * 模擬訊息接收
     */
    public void recv(){
            Message msg = new Message("Msg" + System.currentTimeMillis());
            System.out.println(String.format("接收到訊息(%d): %s", ++cnt, msg));
            messageHandler.handle(msg);
    }

    public void init(){
        if(!inited){
            messageHandler = new MessageHandler();
            inited = true;
        }
    }

    public static void main(String[] args) {
        new Receiver().start();
    }
}

訊息處理:

public class MessageHandler {
    private static final int THREAD_POOL_SIZE = 1;
    private ExecutorService service = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

    public void handle(Message msg){
        try {
            service.execute(new Runnable() {

                @Override
                public void run() {
                    parseMsg(msg);
                }
            });
        } catch (Throwable e) {
            System.out.println("訊息處理異常" + e);
        }
    }

    /**
     * 比較耗時的訊息處理流程
     */
    public void parseMsg(Message message){
        try {
            Thread.sleep(10000);
            System.out.println("解析訊息:" + message);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            Receiver.limit --;
        }

    }
}

效果:通過控制訊息的個數來阻塞訊息的接收過程,就不會導致任務的堆積,系統的記憶體消耗會比較平緩,限制訊息的個數本質就和下面限制任務佇列大小一樣。

使用同步佇列 SynchronousQueue

SynchronousQueue 雖名為佇列,但是其實不會緩衝任務的物件,只是作為物件傳遞的控制點,如果有空閒執行緒或者沒有達到最大執行緒限制,就會交付給worker執行緒去執行,否則就會拒絕,我們需要自己實現對應的拒絕策略RejectedExecutionHandler,預設的是丟擲異常RejectedExecutionException。

訊息接收者同上。

訊息處理:

public class MessageHandler {
    private static final int THREAD_POOL_SIZE = 4;

    ThreadPoolExecutor service = new ThreadPoolExecutor(THREAD_POOL_SIZE, THREAD_POOL_SIZE, 0L, TimeUnit.MILLISECONDS,
            new SynchronousQueue<Runnable>(), new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            System.out.println("自定義拒絕策略");
            try {
                executor.getQueue().put(r);
                System.out.println("重新放任務回佇列");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    public void handle(Message msg) {
        try {
            System.out.println(service.getTaskCount());
            System.out.println(service.getQueue().size());
            System.out.println(service.getCompletedTaskCount());
            service.execute(new Runnable() {
                @Override
                public void run() {
                    parseMsg(msg);
                }
            });
        } catch (Throwable e) {
            System.out.println("訊息處理異常" + e);
        }
    }

    /**
     * 比較耗時的訊息處理流程
     */
    public void parseMsg(Message message) {
        while (true) {
            try {
                System.out.println("執行緒名:" + Thread.currentThread().getName());
                System.out.println("解析訊息:" + message);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

效果:能夠控制訊息的接收速度,但是我們需要在rejectedExecution中實現某種阻塞的操作,但是選擇在發生拒絕的時候把任務重新放回佇列,帶來的問題就是這個Task會發生飢餓現象。

使用大小限制的阻塞佇列

使用LinkedBlockingQueue作為執行緒框架底層的任務緩衝區,並且設定大小限制,思想上和上述方案一樣,都是有一個阻塞的點,但是通過最後的jvm monitor看到這裡的CPU消耗更少,記憶體使用有所降低,並且波動小(具體原因有待探索)。

訊息接收者同上。

訊息處理:

public class MessageHandler {
    private static final int THREAD_POOL_SIZE = 4;
    private static final int BLOCK_QUEUE_CAP = 500;
    ThreadPoolExecutor service = new ThreadPoolExecutor(THREAD_POOL_SIZE, THREAD_POOL_SIZE, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(BLOCK_QUEUE_CAP), new SimpleThreadFactory(), new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            System.out.println("自定義拒絕策略");
            try {
                executor.getQueue().put(r);
                System.out.println("重新放任務回佇列");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    public void handle(Message msg) {
        try {
            service.execute(new Runnable() {
                @Override
                public void run() {
                    parseMsg(msg);
                }
            });
        } catch (Throwable e) {
            System.out.println("訊息處理異常" + e);
        }
    }

    /**
     * 比較耗時的訊息處理流程
     */
    public void parseMsg(Message message) {
        try {
            Thread.sleep(5000);
            System.out.println("執行緒名:" + Thread.currentThread().getName());
            System.out.println("解析訊息:" + message);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class SimpleThreadFactory implements ThreadFactory {

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("Thread-" + System.currentTimeMillis());
            return thread;
        }
    }
}

總結

多執行緒是比較容易出問題的地方,特別當對方法不熟悉的時候

相關文章