多線程永動任務設計與實現

今天教大家擼一個 Java 的多線程永動任務,這個示例的原型是公司自研的多線程異步任務項目,我把裏面涉及到多線程的代碼抽離出來,然後進行一定的改造。

裏面涉及的知識點非常多,特別適合有一定工作經驗的同學學習,或者可以直接拿到項目中使用。

文章結構非常簡單:

1. 功能說明

做這個多線程異步任務,主要是因爲我們有很多永動的異步任務,什麼是永動呢?就是任務跑起來後,需要一直跑下去。

比如消息 Push 任務,因爲一直有消息過來,所以需要一直去消費 DB 中的未推送消息,就需要整一個 Push 的永動異步任務。

我們的需求其實不難,簡單總結一下:

  1. 能同時執行多個永動的異步任務
  2. 每個異步任務,支持開多個線程去消費這個任務的數據;
  3. 支持永動異步任務的優雅關閉,即關閉後,需要把所有的數據消費完畢後,再關閉。

完成上面的需求,需要注意幾個點:

  1. 每個永動任務,可以開一個線程去執行;
  2. 每個子任務,因爲需要支持併發,需要用線程池控制;
  3. 永動任務的關閉,需要通知子任務的併發線程,並支持永動任務和併發子任務的優雅關閉

2. 多線程任務示例

2.1 線程池

對於子任務,需要支持併發,如果每個併發都開一個線程,用完就關閉,對資源消耗太大,所以引入線程池:

public class TaskProcessUtil {
    // 每個任務,都有自己單獨的線程池
    private static Map<String, ExecutorService> executors = new ConcurrentHashMap<>();

    // 初始化一個線程池
    private static ExecutorService init(String poolName, int poolSize) {
        return new ThreadPoolExecutor(poolSize, poolSize,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(),
                new ThreadFactoryBuilder().setNameFormat("Pool-" + poolName).setDaemon(false).build(),
                new ThreadPoolExecutor.CallerRunsPolicy());
    }

    // 獲取線程池
    public static ExecutorService getOrInitExecutors(String poolName,int poolSize) {
        ExecutorService executorService = executors.get(poolName);
        if (null == executorService) {
            synchronized (TaskProcessUtil.class) {
                executorService = executors.get(poolName);
                if (null == executorService) {
                    executorService = init(poolName, poolSize);
                    executors.put(poolName, executorService);
                }
            }
        }
        return executorService;
    }

    // 回收線程資源
    public static void releaseExecutors(String poolName) {
        ExecutorService executorService = executors.remove(poolName);
        if (executorService != null) {
            executorService.shutdown();
        }
    }
}

這是一個線程池的工具類,這裏初始化線程池和回收線程資源很簡單,我們主要討論獲取線程池。

獲取線程池可能會存在併發情況,所以需要加一個 synchronized 鎖,然後鎖住後,需要對 executorService 進行二次判空校驗。

2.2 單個任務

爲了更好講解單個任務的實現方式,我們的任務主要就是把 Cat 的數據打印出來,Cat 定義如下:

@Data
@Service
public class Cat {
    private String catName;
    public Cat setCatName(String name) {
        this.catName = name;
        return this;
    }
}

單個任務主要包括以下功能:

  • 獲取永動任務數據:這裏一般都是掃描 DB,我直接就簡單用 queryData() 代替。
  • 多線程執行任務:需要把數據拆分成 4 份,然後分別由多線程併發執行,這裏可以通過線程池支持;
  • 永動任務優雅停機:當外面通知任務需要停機,需要執行完剩餘任務數據,並回收線程資源,退出任務;
  • 永動執行:如果未收到停機命令,任務需要一直執行下去。

直接看代碼:

public class ChildTask {

    private final int POOL_SIZE = 3; // 線程池大小
    private final int SPLIT_SIZE = 4; // 數據拆分大小
    private String taskName;

    // 接收jvm關閉信號,實現優雅停機
    protected volatile boolean terminal = false;

    public ChildTask(String taskName) {
        this.taskName = taskName;
    }

    // 程序執行入口
    public void doExecute() {
        int i = 0;
        while(true) {
            System.out.println(taskName + ":Cycle-" + i + "-Begin");
            // 獲取數據
            List<Cat> datas = queryData();
            // 處理數據
            taskExecute(datas);
            System.out.println(taskName + ":Cycle-" + i + "-End");
            if (terminal) {
                // 只有應用關閉,纔會走到這裏,用於實現優雅的下線
                break;
            }
            i++;
        }
        // 回收線程池資源
        TaskProcessUtil.releaseExecutors(taskName);
    }

    // 優雅停機
    public void terminal() {
        // 關機
        terminal = true;
        System.out.println(taskName + " shut down");
    }

    // 處理數據
    private void doProcessData(List<Cat> datas, CountDownLatch latch) {
        try {
            for (Cat cat : datas) {
                System.out.println(taskName + ":" + cat.toString() + ",ThreadName:" + Thread.currentThread().getName());
                Thread.sleep(1000L);
            }
        } catch (Exception e) {
            System.out.println(e.getStackTrace());
        } finally {
            if (latch != null) {
                latch.countDown();
            }
        }
    }

    // 處理單個任務數據
    private void taskExecute(List<Cat> sourceDatas) {
        if (CollectionUtils.isEmpty(sourceDatas)) {
            return;
        }
        // 將數據拆成4份
        List<List<Cat>> splitDatas = Lists.partition(sourceDatas, SPLIT_SIZE);
        final CountDownLatch latch = new CountDownLatch(splitDatas.size());

        // 併發處理拆分的數據,共用一個線程池
        for (final List<Cat> datas : splitDatas) {
            ExecutorService executorService = TaskProcessUtil.getOrInitExecutors(taskName, POOL_SIZE);
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    doProcessData(datas, latch);
                }
            });
        }

        try {
            latch.await();
        } catch (Exception e) {
            System.out.println(e.getStackTrace());
        }
    }

    // 獲取永動任務數據
    private List<Cat> queryData() {
        List<Cat> datas = new ArrayList<>();
        for (int i = 0; i < 5; i ++) {
            datas.add(new Cat().setCatName("羅小黑" + i));
        }
        return datas;
    }
}

簡單解釋一下:

  • queryData:用於獲取數據,實際應用中其實是需要把 queryData 定爲抽象方法,然後由各個任務實現自己的方法。
  • doProcessData:數據處理邏輯,實際應用中其實是需要把 doProcessData 定爲抽象方法,然後由各個任務實現自己的方法。
  • taskExecute:將數據拆分成 4 份,獲取該任務的線程池,並交給線程池併發執行,然後通過 latch.await() 阻塞。當這 4 份數據都執行成功後,阻塞結束,該方法才返回。
  • terminal:僅用於接受停機命令,這裏該變量定義爲 volatile,所以多線程內存可見;
  • doExecute:程序執行入口,封裝了每個任務執行的流程,當 terminal=true 時,先執行完任務數據,然後回收線程池,最後退出。

2.3 任務入口

直接上代碼:

public class LoopTask {
    private List<ChildTask> childTasks;
    public void initLoopTask() {
        childTasks = new ArrayList();
        childTasks.add(new ChildTask("childTask1"));
        childTasks.add(new ChildTask("childTask2"));
        for (final ChildTask childTask : childTasks) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    childTask.doExecute();
                }
            }).start();
        }
    }
    public void shutdownLoopTask() {
        if (!CollectionUtils.isEmpty(childTasks)) {
            for (ChildTask childTask : childTasks) {
                childTask.terminal();
            }
        }
    }
    public static void main(String args[]) throws Exception{
        LoopTask loopTask = new LoopTask();
        loopTask.initLoopTask();
        Thread.sleep(5000L);
        loopTask.shutdownLoopTask();
    }
}

每個任務都開一個單獨的 Thread,這裏我初始化了 2 個永動任務,分別爲 childTask1 和 childTask2,然後分別執行,後面 Sleep 了 5 秒後,再關閉任務,我們可以看看是否可以按照我們的預期優雅退出。

2.4 結果分析

執行結果如下:

childTask1:Cycle-0-Begin
childTask2:Cycle-0-Begin
childTask1:Cat(catName=羅小黑0),ThreadName:Pool-childTask1
childTask1:Cat(catName=羅小黑4),ThreadName:Pool-childTask1
childTask2:Cat(catName=羅小黑4),ThreadName:Pool-childTask2
childTask2:Cat(catName=羅小黑0),ThreadName:Pool-childTask2
childTask1:Cat(catName=羅小黑1),ThreadName:Pool-childTask1
childTask2:Cat(catName=羅小黑1),ThreadName:Pool-childTask2
childTask2:Cat(catName=羅小黑2),ThreadName:Pool-childTask2
childTask1:Cat(catName=羅小黑2),ThreadName:Pool-childTask1
childTask2:Cat(catName=羅小黑3),ThreadName:Pool-childTask2
childTask1:Cat(catName=羅小黑3),ThreadName:Pool-childTask1
childTask2:Cycle-0-End
childTask2:Cycle-1-Begin
childTask1:Cycle-0-End
childTask1:Cycle-1-Begin
childTask2:Cat(catName=羅小黑0),ThreadName:Pool-childTask2
childTask2:Cat(catName=羅小黑4),ThreadName:Pool-childTask2
childTask1:Cat(catName=羅小黑4),ThreadName:Pool-childTask1
childTask1:Cat(catName=羅小黑0),ThreadName:Pool-childTask1
childTask1 shut down
childTask2 shut down
childTask2:Cat(catName=羅小黑1),ThreadName:Pool-childTask2
childTask1:Cat(catName=羅小黑1),ThreadName:Pool-childTask1
childTask1:Cat(catName=羅小黑2),ThreadName:Pool-childTask1
childTask2:Cat(catName=羅小黑2),ThreadName:Pool-childTask2
childTask1:Cat(catName=羅小黑3),ThreadName:Pool-childTask1
childTask2:Cat(catName=羅小黑3),ThreadName:Pool-childTask2
childTask1:Cycle-1-End
childTask2:Cycle-1-End

輸出數據:

  • “Pool-childTask” 是線程池名稱;
  • “childTask” 是任務名稱;
  • “Cat(catName=羅小黑)” 是執行的結果;
  • “childTask shut down” 是關閉標記;
  • “childTask:Cycle-X-Begin” 和“childTask:Cycle-X-End” 是每一輪循環的開始和結束標記。

我們分析一下執行結果:

  • childTask1 和 childTask2 分別執行,在第一輪循環中都正常輸出了 5 條羅小黑數據;
  • 第二輪執行過程中,我啓動了關閉指令,這次第二輪執行沒有直接停止,而是先執行完任務中的數據,再執行退出,所以完全符合我們的優雅退出結論。

2.5 源碼地址

GitHub 地址:

https://github.com/lml200701158/java-study/tree/master/src/main/java/com/java/parallel/pool/ofc
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章