在我们的应用系统中,经常会处理一些耗时任务,自然而然的会想到使用多线程。JDK给我们提供了非常方便的操作线程的API,JDK5之后更是新增了JUC包的支持,并发编程大师Doug Lea(JDK并发的作者)也是一直在为我们使用线程做着不懈的努力。

为什么还要使用Spring来实现多线程呢?这是句废话!实际有两个原因,第一使用Spring比使用JDK原生的并发API更简单。第二我们的应用环境一般都会集成Spring,我们的Bean也都交给Spring来进行管理,那么使用Spring来实现多线程更加简单,更加优雅。

在Spring3之后,Spring引入了对多线程的支持,如果你使用的版本在3.1以前,应该还是需要通过传统的方式来实现多线程的。从Spring3同时也是新增了Java的配置方式,而且Java配置方式也逐渐成为主流的Spring的配置方式,因此后面的例子都是以Java的配置进行演示。

在Spring中实现多线程,其实非常简单,只需要在配置类中添加@EnableAsync就可以使用多线程。在希望执行的并发方法中使用@Async就可以定义一个线程任务。通过spring给我们提供的ThreadPoolTaskExecutor就可以使用线程池。

快速入门

启用异步任务,线程池配置

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.concurrent.ThreadPoolExecutor;

/**
 * @Description spring多线程使用
 */
@Configuration
@EnableAsync  // 启用异步任务
public class ThreadConfig {

    @Bean("smsExecutor")
    public ThreadPoolTaskExecutor smsExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //设置线程的名称前缀
        executor.setThreadNamePrefix("短信通知线程");
        //线程池维护线程的最少数量
        executor.setCorePoolSize(5);
        //线程池维护线程的最大数量
        executor.setMaxPoolSize(10);
        //非核心线程数的存活时间
        executor.setKeepAliveSeconds(4);
        //阻塞队列LinkedBlockingQueue
        executor.setQueueCapacity(25);
        //对拒绝任务的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //初始化
        executor.initialize();
        return executor;
    }

    @Bean("mailExecutor")
    public ThreadPoolTaskExecutor mailExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //设置线程的名称前缀
        executor.setThreadNamePrefix("邮件通知线程");
        //线程池维护线程的最少数量
        executor.setCorePoolSize(5);
        //线程池维护线程的最大数量
        executor.setMaxPoolSize(10);
        //非核心线程数的存活时间
        executor.setKeepAliveSeconds(4);
        //阻塞队列LinkedBlockingQueue
        executor.setQueueCapacity(25);
        //对拒绝任务的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //初始化
        executor.initialize();
        return executor;
    }

}

在需要异步执行的方法上添加@Async注解,@Async注解可以指定线程池名称

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class MultiThreadProcessService {

    public static final Logger logger = LoggerFactory.getLogger(MultiThreadProcessService.class);

    /**
     * 短信发送服务
     * 默认处理流程耗时1000ms
     */
    @Async("smsExecutor")
    public void sendSms() {
        logger.debug("MultiThreadProcessService-sendSms" + Thread.currentThread() + "......start");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        logger.debug("MultiThreadProcessService-sendSms" + Thread.currentThread() + "......end");
    }

    /**
     * 邮件发送服务
     * 默认处理流程耗时1000ms
     */
    @Async("mailExecutor")
    public void sendMail() {
        logger.debug("MultiThreadProcessService-sendMail" + Thread.currentThread() + "......start");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        logger.debug("MultiThreadProcessService-sendMail" + Thread.currentThread() + "......end");
    }
}

测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, WebConfig.class})
@WebAppConfiguration
public class MultiThreadTest {

    @Autowired
    private MultiThreadProcessService multiThreadProcessService;

    @Test
    public void test() throws IOException {
        for (int i=0; i<20; i++){
            multiThreadProcessService.sendSms();
            multiThreadProcessService.sendMail();
        }
        System.in.read();
    }
}

下面关于线程池的配置还有一种方式,就是直接实现AsyncConfigurer接口,重写getAsyncExecutor方法即可,代码如下:

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.lang.reflect.Method;

@Configuration
@EnableAsync //开启异步任务支持
//配置类实现AsyncConfigurer接口并重写getAsyncExecutor方法
public class TaskExecutorConfig implements AsyncConfigurer {

    //返回一个ThreadPoolTaskExecutor,这就就获得了一个基于线程池的TaskExecutor
    @Override
    public ThreadPoolTaskExecutor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(5);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.initialize();
        return taskExecutor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new AsyncUncaughtExceptionHandler(){
            @Override
            public void handleUncaughtException(Throwable ex, Method method, Object... params) {
                System.out.println( "Exception Caught in Thread - "+ Thread.currentThread().getName());
                System.out.println( "Exception message - "+ ex.getMessage());
                System.out.println( "Method name - "+ method.getName());
                for(Object param : params) {
                    System.out.println( "Parameter value - "+ param);
                }
            }
        };
    }


}

 但是这种方式有一个局限性,就是只能有一个实现AsyncConfigurer接口的类,无法做到隔离不同业务(即不同的业务使用不同的线程池)。如果整个系统确实只需要一个线程池,可以这么做。

异步任务详解

任务的拒绝策略

JDK内置了四种任务的拒绝策略:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常(默认)。
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务。
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理,重试添加当前的任务,它会自动重复调用execute()方法 

当然我们也可以自定义我们的处理策略,实现java.util.concurrent.RejectedExecutionHandler接口。

/**
 * 自定义拒绝策略
 */
class SmsExecutorRejectedExecutionHandler implements RejectedExecutionHandler{
    //自定义拒绝策略,线程任务队列满了的情况下,任务等待入线程队列
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            executor.getQueue().put(r);
        } catch (InterruptedException e) {
            System.out.println("wait input queue error");
        }
    }
}

@Async注解

基于@Async标注的方法,称之为异步方法;这些方法将在执行的时候,将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作。

该注解标注在类上,就代表调用该类的所有方法均会自动异步执行;标注在方法上,该方法就会异步执行;当调用该方法时,Async的切面会先向异步线程池申请一个线程,然后使用该线程执行该方法内的业务逻辑。

@Async注解只有一个参数,可以指定异步任务所用的线程池bean名称,如@Async("taskExecutor")。

@Async使用的注意事项

(1) 异步化注解@Async标注的方法返回值只能是void或者Future<T>(AsyncResult)。

(2) @Async所修饰的方法不要定义为static类型,这样异步调用不会生效。

(3) @Async  必须不同类间调用,即异步方法和调用异步方法的方法不能再同一个类,原因是:Async底层是通过代理对注解扫描实现的。

(4) @Async所修饰的方法不能和@Transactional一起使用,因为会启用新的子线程来执行方法内的业务,主线程内的事务注解无法控制子线程的业务操作,原因就是事务存在线程隔离的原因,如果要加事务,请在方法内嵌套其他事务标注后的方法即可生效。

@Async返回值

1、无返回值

基于@Async无返回值调用,直接在使用类,使用方法(建议在使用方法)上,加上注解。若需要抛出异常,需手动new一个异常抛出。

/**
 * 带参数的异步调用
 * 对于返回值,异常会被AsyncUncaughtExceptionHandler处理掉,前提是实现了AsyncConfigurer接口并重写了异常捕获方法 
 * @param message
 */
@Async
public void sendMessage(String message) {
    logger.info("sendMessage >> message=" + message);
    throw new IllegalArgumentException(message);
}

2、有返回值 

/**
 * 异常调用返回Future
 * 对于返回值是Future,不会被AsyncUncaughtExceptionHandler处理,需要我们在进行捕获异常并处理
 * 或者在调用方法调用Future.get()方法时捕获异常并处理
 * @param message
 */
@Async
public Future<String> sendMessage(String message) {
    logger.info("sendMessage >> message=" + message);
    Future<String> future;
    try {
        Thread.sleep(1000);
        future = new AsyncResult("success:" + message);
        throw new IllegalArgumentException("a");
    }catch (InterruptedException e){
        future = new AsyncResult("error");
    }catch (IllegalArgumentException e){
        future = new AsyncResult("error-IllegalArgumentException");
    }
    return future;
}

什么情况下导致@Async失效

1、缺少@EnableAsync注解

2、异步方法不能是static,加了static就不走AOP了

3、异步方法(添加@Async注解)和调用者方法在同一个类中

原因:spring 在扫描bean的时候会扫描方法上是否包含@Async注解,如果包含,spring会为这个bean动态地生成一个子类(即代理类,proxy),代理类是继承原来那个bean的。此时,当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用时增加异步作用。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个bean,所以就没有增加异步作用,我们看到的现象就是@Async注解无效。

解决:将异步方法按照业务统一抽取到对应的bean中,当外部需要使用时将该bean注入,然后调用bean中的异步方法。