【SpringBoot】多線程以及自定義拒絕策略(基於@Async)

一、使用@Async在SpringBoot項目中實現多線程

1. 多線程Configuration

啓動類:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@EnableAutoConfiguration
@ComponentScan("com.asiainfo.*")
public class Application extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }
}

**@EnableAutoConfiguration:**幫助SpringBoot應用將所有符合條件的@Configuration配置都加載到當前SpringBoot創建並使用的IoC容器。

多線程配置類:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;

@Configuration
@EnableAsync
public class ExecutorConfig {

    private static final Logger logger = LoggerFactory.getLogger(ExecutorConfig.class);

    private static Hashtable<String, List<FutureTask>> rejectTaskMap;

    @Value("${thread.CORE_POOL_SIZE}")
    private int corePoolSize;

    @Value("${thread.MAX_POOL_SIZE}")
    private int maxPoolSize;

    @Value("${thread.QUEUE_CAPACITY}")
    private int queueCapacity;

    @Bean
    public Executor asyncServiceExecutor() {
        logger.info("start asyncServiceExecutor");
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        //配置核心線程數
        executor.setCorePoolSize(corePoolSize);
        //配置最大線程數
        executor.setMaxPoolSize(maxPoolSize);

        //配置隊列最大長度
        executor.setQueueCapacity(queueCapacity);

        // rejection-policy:當pool已經達到max size的時候,如何處理新任務
        // CALLER_RUNS:不在新線程中執行任務,而是有調用者所在的線程來執行

        //自定義拒絕策略
        executor.setRejectedExecutionHandler(new MyRejectedPolicyHandler());
        rejectTaskMap = new Hashtable<>();
        //執行初始化
        executor.initialize();
        return executor;
    }

    public static Hashtable<String, List<FutureTask>> getRejectTaskMap() {
        return rejectTaskMap;
    }

}

@Configuration:
@Configuration用於定義配置類,可替換xml配置文件,被註解的類內部包含有一個或多個被@Bean註解的方法,這些方法將會被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext類進行掃描,並用於構建bean定義,初始化Spring容器。
注意:@Configuration註解的配置類有如下要求:

  • @Configuration不可以是final類型;
  • @Configuration不可以是匿名類;
  • 嵌套的configuration必須是靜態類;

@EnableAsync:
以異步執行,允許開啓多線程。

executor.setRejectedExecutionHandler(new MyRejectedPolicyHandler());
設置拒絕策略,當任務源源不斷的過來,而我們的系統又處理不過來的時候,我們要採取的策略是拒絕服務。RejectedExecutionHandler接口提供了拒絕任務處理的自定義方法的機會。在ThreadPoolExecutor中已經包含四種處理策略。

  • CallerRunsPolicy:線程調用運行該任務的 execute 本身。此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if
    (!e.isShutdown()) { r.run(); }}
    這個策略顯然不想放棄執行任務。但是由於池中已經沒有任何資源了,那麼就直接使用調用該execute的線程本身來執行。(開始我總不想丟棄任務的執行,但是對某些應用場景來講,很有可能造成當前線程也被阻塞。如果所有線程都是不能執行的,很可能導致程序沒法繼續跑了。需要視業務情景而定吧。)
  • AbortPolicy:處理程序遭到拒絕將拋出運行時 RejectedExecutionException public void
    rejectedExecution(Runnable r, ThreadPoolExecutor e) {throw new
    RejectedExecutionException();}
    這種策略直接拋出異常,丟棄任務。(jdk默認策略,隊列滿併線程滿時直接拒絕添加新任務,並拋出異常,所以說有時候放棄也是一種勇氣,爲了保證後續任務的正常進行,丟棄一些也是可以接收的,記得做好記錄)
  • DiscardPolicy:不能執行的任務將被刪除 public void rejectedExecution(Runnable r,
    ThreadPoolExecutor e) {} 這種策略和AbortPolicy幾乎一樣,也是丟棄任務,只不過他不拋出異常。
  • DiscardOldestPolicy:如果執行程序尚未關閉,則位於工作隊列頭部的任務將被刪除,然後重試執行程序(如果再次失敗,則重複此過程)
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if
    (!e.isShutdown()) {e.getQueue().poll();e.execute®; }}
    該策略就稍微複雜一些,在pool沒有關閉的前提下首先丟掉緩存在隊列中的最早的任務,然後重新嘗試運行該任務。這個策略需要適當小心。
    除上述四種策略外,還可以添加自定義拒絕策略, MyRejectedPolicyHandler就是一個自定義決絕策略,下文中會着重講一下該策略的實現方式。

設置線程池:

#多線程配置
thread:
  CORE_POOL_SIZE: 10
  MAX_POOL_SIZE: 100
  QUEUE_CAPACITY: 1000

@Value("${thread.CORE_POOL_SIZE}")
private int corePoolSize;
設置核心線程數量。

@Value("${thread.MAX_POOL_SIZE}")
private int maxPoolSize;
設置最大線程數量。

@Value("${thread.QUEUE_CAPACITY}")
private int queueCapacity;
設置緩衝隊列大小。

2. 使用Runner啓動項目
SpringBoot給我們提供了兩個接口來幫助我們實現容器啓動完成後立即執行。這兩個接口分別爲CommandLineRunner和ApplicationRunner。
定義一個類SimosApplicationRunner實現ApplicationRunner接口,然後Override這個ApplicationRunner接口的run方法即可。

Runner:

import com.asiainfo.processor_other.config.MyRejectedPolicy;
import com.asiainfo.processor_other.task.TestTask;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(1)
public class TestRunner implements ApplicationRunner {

    @Autowired
    TestRunner testRunner;

    @Autowired
    TestTask testTask;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        testRunner.test();
    }

    @MyRejectedPolicy("runTest")
    private void test() {
        for (int i = 0; i < 100; i++) {
            testTask.runTest(i);
        }
    }
}

Task:
使用 @Async註解,每調用一次TestTask的runTest方法都會開啓一個新的線程;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class TestTask {

    @Async("asyncServiceExecutor")
    public void runTest(int i) {
        System.out.printf("Test:" + i);
    }
}

二、自定義拒絕策略
自定義拒絕策略思路:
自定義拒絕策略流程
若線程池配置不合理,或者任務添加的速度大於處理的速度,會執行線程池拒絕策略,四個系統默認的拒絕策略,或者阻塞主進程,或者拋出異常,或者丟棄任務,在某些情況都不適用的情況下需要自定義拒絕策略經行容災。
改造的思路是自定義拒絕策略,將線程池拒絕的任務緩存到內存中,再在合適的時機重新放入線程池中處理,從而達到了線程池防阻塞、容災的目的(方案使用的內存對象爲線程安全對象,效率較低,只可作爲特定情況下的容災機制使用)。

改造點:

  • 修改默認緩存隊列大小(默認:2147483647),改爲1000
  • 初始化一個全局的,線程安全的MAP<String,List>,用來代替原有的線程池緩存隊列。其中,String 爲方法名稱, List 爲task(FutureTask)線程隊列。
  • 自定義拒絕策略。 當任務拒絕時,將拒絕的任務添加到Map中。
  • 自定義註解類,註解到添加任務到線程池的方法上。原有的方法需要重新抽取。
  • 通過AOP,Around方式截取註解的方法。判斷全局Map 中是否有對應此方法的線程隊列,如果有,則先執行在MAP緩存隊列中的task,返回null,不再往線程池中添加新的task,如果MAP緩存隊列中沒有此方法對應的task,則正常添加task到線程池中。

代碼實現:
1、ExecutorConfig
參考上文代碼,全局Map
2、MyRejectedPolicyHandler
自定義拒絕策略類

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @Description: 線程池自定義拒絕策略
 * @Date: 2018/12/26 11:25
 **/
public class MyRejectedPolicyHandler implements RejectedExecutionHandler {

    private static Logger logger = LoggerFactory.getLogger(MyRejectedPolicyHandler.class);
    
    /**
     * @Description:  將拒絕的線程放到全局變量中
     * @Date: 2018/12/26 11:25
     * @Param: [r, executor]
     * @Return: void
     **/
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        FutureTask task = (FutureTask)r;
        // 獲取調用的方法名稱(反射獲取私有屬性)
        try {
            Object callable = getFiled(task,"callable");
            Method userDeclaredMethod = (Method) getFiled(callable,"val$userDeclaredMethod");
            String methodName = userDeclaredMethod.getName();

            logger.info("Add task to Map, methodName: [ "  + methodName + " ]");

            //在內存中維護一個全局Map, 將策略拒絕的task放置到map中
            Hashtable<String, List<FutureTask>> rejectTaskMap = ExecutorConfig.getRejectTaskMap();
            if(!rejectTaskMap.containsKey(methodName)){
                List<FutureTask> taskList = new CopyOnWriteArrayList<>();
                rejectTaskMap.put(methodName, taskList);
            }
            List<FutureTask> taskList = rejectTaskMap.get(methodName);
            taskList.add(task);
            logger.info("MethodName : [ " + methodName + " ] taskList size : [" + taskList.size() + " ]" );
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }
    /**
     * @Description: 反射,通過循環父類獲取field值(含private)
     * @Date: 2018/12/26 15:28
     * @Param: [c, name]
     * @Return: java.lang.Object
     **/
    private static Object getFiled(Object c, String name) throws IllegalAccessException {
        while (c != null && !c.getClass().getName().toLowerCase().equals("java.lang.object")) {
            try {
                Field field = c.getClass().getDeclaredField(name);
                field.setAccessible(true);
                return field.get(c);
            } catch (NoSuchFieldException e) {
                c = c.getClass().getSuperclass();
            }
        }
        return null;
    }
}

3、MyRejectedPolicy
自定義拒絕策略註解類

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRejectedPolicy {
    String value() default "";
}

4、MyRejectedPolicyAspect

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;

/**
 * @Description: 判斷線程類任務是否有積留,處理
 * @Date: 2018/12/26 16:41
 **/
@Aspect
@Component
public class MyRejectedPolicyAspect {

    private static Logger logger = LoggerFactory.getLogger(MyRejectedPolicyAspect.class);

    @Resource
    private Executor asyncServiceExecutor;

    @Around("@annotation(MyRejectedPolicy)")
    public Object doAroundMethod(ProceedingJoinPoint pjd) throws Throwable {

        //取得 PermissionContext 註解屬性(值)信息
        MethodSignature methodSignature = (MethodSignature)pjd.getSignature();
        MyRejectedPolicy myRejectPolicy =  methodSignature.getMethod().getAnnotation(MyRejectedPolicy.class);
        String methodName = myRejectPolicy.value();

        // 判斷內存維護的列表中是否有此方法產生的task
        Hashtable<String, List<FutureTask>> rejectTaskMap = ExecutorConfig.getRejectTaskMap();
        if(rejectTaskMap.containsKey(methodName)){
            List<FutureTask> taskList = rejectTaskMap.get(methodName);
            // 如果有此方法對應的緩存task,不再往線程池中添加新的task,執行緩存中未執行的task,
            int taskListSize = taskList.size();
            if(taskListSize > 0){
                logger.info("[ " + methodName + " ] method blocked, list size: [" + taskListSize + "]");
                Iterator<FutureTask> it = taskList.iterator();
                while (it.hasNext()){
                    asyncServiceExecutor.execute(it.next());
                    it.remove();
                }
                // return 是爲了打斷後續執行, 不再往線程池中添加新的task
                return null;
            }
        }
        return pjd.proceed();
    }
}

5、使用方式
參考Runner啓動部分代碼。
註解
在分發線程的部分增加註解,註解值爲Map中的key。
Runner
run方法中調用註解方法需要注入自身對象,否則切面無法正常捕獲。

Task
Task代碼中的異步方法需要增加@Async註解,表明爲一個異步方法。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章