​Java线程池详解​(+线程池多线程发邮件博客)

构造一个线程池为什么需要几个参数?如果避免线程池出现OOM?RunnableCallable的区别是什么?本文将对这些问题一一解答,同时还将给出使用线程池的常见场景和代码片段。

基础知识

Executors创建线程池

Java中创建线程池很简单,只需要调用Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads),但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。

Executors创建线程池便捷方法列表:

方法名功能
newFixedThreadPool(int nThreads)创建固定大小的线程池
newSingleThreadExecutor()创建只有一个线程的线程池
newCachedThreadPool()创建一个不限线程数上限的线程池,任何提交的任务都将立即执行

小程序使用这些快捷方法没什么问题,对于服务端需要长期运行的程序,创建线程池应该直接使用ThreadPoolExecutor的构造方法。没错,上述Executors方法创建的线程池就是ThreadPoolExecutor

线程池创建的7种方法:

线程池的创建方式也有很多种,每种线程池的创建方式都对应了不同的使用场景。总结来说线程池的创建可以分为两大类:

通过 Executors 创建

通过 ThreadPoolExecutor 创建

以上这两类创建线程池的方式有 7 种具体实现方法,这 7 种方法便是本文要说的创建线程池的七种方式。分别是:

方法含义
Executors.newFixedThreadPool()创建一个大小固定的线程池,可控制并发的线程数,超出的线程会在队列中等待
Executors.newCachedThreadPool()创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程
Executors.newSingleThreadExecutor()创建单个线程的线程池,可以保证先进先出的执行顺序
Executors.newScheduledThreadPool()创建一个可以执行延迟任务的线程池
Executors.newSingleThreadScheduledExecutor()创建一个单线程的可以执行延迟任务的线程池
Executors.newWorkStealingPool()创建一个抢占式执行的线程池
ThreadPoolExecutor()手动创建线程池,可自定义相关参数

Executors.newFixedThreadPool():创建一个固定大小的线程池,可控制并发的线程数。

public class FixedThreadPoolDemo {
 
    public static void main(String[] args) {
 
        // 创建 2 个线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        // 创建任务
        Runnable runnable = () -> System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
        // 线程池执行任务(一次添加 8 个任务)
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
        threadPool.execute(runnable);
    }
 
}

创建一个具有 2 个线程的线程池,执行 8 个任务,执行结果为:

Executors.newCachedThreadPool():创建一个可缓存的线程池,若线程数超过人物所需,那么多余的线程会被缓存一段时间后再回收,若线程数不够,则会新建线程。

public class CachedThreadPoolDemo {
 
    public static void main(String[] args) {
 
        // 创建线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        // 执行任务
        for (int i = 0; i < 5; i++) {
            threadPool.execute(() -> {
                System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
            });
        }
    }
 
}

创建了一个具有 5 个线程的线程池来执行相应的任务。

使用场景

CachedThreadPool 是根据短时间的任务量来决定创建的线程数量的,所以它适合短时间内有突发大量任务的处理场景。

Executors.newSingleThreadExecutor():创建只有单个线程的线程池,可以保证先进先出的顺序。

public class SingleThreadExecutorDemo {
 
    public static void main(String[] args) {
 
        // 创建线程池
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // 执行任务
        for (int i = 0; i < 10; i++) {
            int index = i;
            threadPool.execute(() -> {
                System.out.println(index + ": 任务被执行: " + Thread.currentThread().getName());
            });
        }
    }
 
}

如果在打印语句下再加一行睡眠的语句,就会看到每个一段时间输出任务被执行的过程~

try {
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
}

Executors.newScheduledThreadPool():创建一个可以执行延迟任务的线程池。

public class ScheduledThreadPoolDemo {
 
    public static void main(String[] args) {
        // 创建线程池
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
        // 添加定时执行任务(1s 后执行)
        System.out.println("添加任务,时间:" + new Date());
        threadPool.schedule(() -> {
            System.out.println("任务被执行,时间:" + new Date());
        }, 2, TimeUnit.SECONDS);
    }
 
}

创建一个延迟 2 秒执行任务的线程池。

Executors.newSingleThreadScheduledExecutor():创建一个单线程的可以执行延迟任务的线程池。这种线程池可以看做是 ScheduledThreadPool 的单线程版本。

public class SingleThreadScheduledExecutorDemo {
 
    public static void main(String[] args) {
        // 创建线程池
        ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
        // 添加定时执行任务(2s 后执行)
        System.out.println("添加任务,时间:" + new Date());
        threadPool.schedule(() -> {
            System.out.println("任务被执行,时间:" + new Date());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        }, 2, TimeUnit.SECONDS);
    }
 
}

Executors.newWorkStealingPool():创建一个抢占式执行的线程池,执行任务的顺序不确定。需要注意的是此方法是 JDK 1.8 版本新增的,所以 1.8 版本之前的程序中不能使用。

public class WorkStealingPoolDemo {
 
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService threadPool = Executors.newWorkStealingPool();
        // 执行任务
        for (int i = 0; i < 10; i++) {
            final int index = i;
            threadPool.execute(() -> {
                System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
            });
        }
        // 确保任务执行完成
        while (!threadPool.isTerminated()) {
        }
    }
 
}

需注意与 SingleThreadExecutor 单个线程的线程池的比较。

可以看到,任务的执行顺序并不是确定的,因为这是抢占式的线程池,哪个任务抢到,哪个任务先执行。

ThreadPoolExecutor():这是最原始,也是最推荐的手动创建线程池的方法。创建时支持自定义某些属性,比如核心线程数、最大线程数等。

public class ThreadPoolExecutorDemo {
 
    public static void main(String[] args) {
 
        // 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
        // 执行任务
        for (int i = 0; i < 10; i++) {
            final int index = i;
            threadPool.execute(() -> {
                System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
            });
        }
    }
 
}

创建一个具有 10 个核心线程、最大线程数为 10 的线程池。具体可设置的参数请参考:线程池七大参数_文丑颜不良啊的博客-CSDN博客

本文参考自:Java 中线程池的 7 种创建方式! - 掘金

ThreadPoolExecutor构造方法(常用)

Executors中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor的构造方法(定时任务使用的是ScheduledThreadPoolExecutor),该类构造方法参数列表如下:

// Java线程池的完整构造函数
public ThreadPoolExecutor(
  int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
  int maximumPoolSize, // 线程数的上限
  long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长,
                                     // 超过这个时间,多余的线程会被回收。
  BlockingQueue<Runnable> workQueue, // 任务的排队队列
  ThreadFactory threadFactory, // 新线程的产生方式
  RejectedExecutionHandler handler) // 拒绝策略

竟然有7个参数,很无奈,构造一个线程池确实需要这么多参数。这些参数中,比较容易引起问题的有corePoolSizemaximumPoolSizeworkQueue以及handler

  • corePoolSizemaximumPoolSize设置不当会影响效率,甚至耗尽线程;
  • workQueue设置不当容易导致OOM;
  • handler设置不当会导致提交任务时抛出异常。

正确的参数设置方式会在下文给出。

博客:

ThreadPoolExecutor参数图解_丶Veer的博客-CSDN博客_threadpoolexecutor 参数设置

线程池的工作顺序

If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.

线程执行过程:

任务被提交到线程池,会先判断当前线程数量是否小于corePoolSize,如果小于则创建线程来执行提交的任务,否则将任务放入workQueue队列,如果workQueue满了,则判断当前线程数量是否小于maximumPoolSize,如果小于则创建线程执行任务,否则就会调用handler,以表示线程池拒绝接收任务。

corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略

Runnable和Callable

可以向线程池提交的任务有两种:RunnableCallable,二者的区别如下:(Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。)

  1. 方法签名不同,void Runnable.run()V Callable.call() throws Exception
  2. 是否允许有返回值,Callable允许有返回值
  3. 是否允许抛出异常,Callable允许抛出异常。

ExecutorService接口定义了线程池的三种提交任务的方式:

提交方式是否关心返回结果
Future<T> submit(Callable<T> task)
void execute(Runnable command)
Future<?> submit(Runnable task)否,虽然返回Future,但是其get()方法总是返回null

如何正确使用线程池

避免使用无界队列

不要使用Executors.newXXXThreadPool()快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
				0, TimeUnit.SECONDS, 
				new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
				new ThreadPoolExecutor.DiscardPolicy());

明确拒绝任务时的行为

任务队列总有占满的时候,这时再submit()提交新的任务会怎么样呢?RejectedExecutionHandler接口为我们提供了控制方式,接口定义如下:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

线程池给我们提供了几种常见的拒绝策略:

undefined

拒绝策略拒绝行为
AbortPolicy抛出RejectedExecutionException
DiscardPolicy什么也不做,直接忽略
DiscardOldestPolicy丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置
CallerRunsPolicy直接由提交任务者执行这个任务

线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
				0, TimeUnit.SECONDS, 
				new ArrayBlockingQueue<>(512), 
				new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略

获取处理结果和异常

线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionExceptionsubmit()方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:

Future接口
Future是一个接口,代表了一个异步计算的结果。接口中的方法用来检查计算是否完成、
等待完成和得到计算的结果。
当计算完成后,只能通过get()方法得到结果,get方法会阻塞直到结果准备好了。
如果想取消,那么调用cancel()方法。其他方法用于确定任务是正常完成还是取消了。
一旦计算完成了,那么这个计算就不能被取消。

我们只在使用submit的时候,才使用get()方法,因为submit才会有返回值。不过get方法会阻塞直到结果准备好了,具体应用可以见:springboot使用线程池发送邮件demo(文章末尾博客)
ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<Object> future = executorService.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            throw new RuntimeException("exception in call~");
            // 该异常会在调用Future.get()时传递给调用者
        }
    });
	
try {
  Object result = future.get();

//Future接口
//Future是一个接口,代表了一个异步计算的结果。接口中的方法用来检查计算是否完成、
//等待完成和得到计算的结果。
//当计算完成后,只能通过get()方法得到结果,get方法会阻塞直到结果准备好了。
//如果想取消,那么调用cancel()方法。其他方法用于确定任务是正常完成还是取消了。
//一旦计算完成了,那么这个计算就不能被取消。

} catch (InterruptedException e) {
  // interrupt
} catch (ExecutionException e) {
  // exception in Callable.call()
  e.printStackTrace();
}

上述代码输出类似如下:

undefined

线程池的常用场景

正确构造线程池

int poolSize = Runtime.getRuntime().availableProcessors() * 2;
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(512);
RejectedExecutionHandler policy = new ThreadPoolExecutor.DiscardPolicy();
executorService = new ThreadPoolExecutor(poolSize, poolSize,
    0, TimeUnit.SECONDS,
            queue,
            policy);

获取单个结果

submit()向线程池提交任务后会返回一个Future,调用V Future.get()方法能够阻塞等待执行结果,V get(long timeout, TimeUnit unit)方法可以指定等待的超时时间。

获取多个结果

如果向线程池提交了多个任务,要获取这些任务的执行结果,可以依次调用Future.get()获得。但对于这种场景,我们更应该使用ExecutorCompletionService,该类的take()方法总是阻塞等待某一个任务完成,然后返回该任务的Future对象。向CompletionService批量提交任务后,只需调用相同次数的CompletionService.take()方法,就能获取所有任务的执行结果,获取顺序是任意的,取决于任务的完成顺序:

void solve(Executor executor, Collection<Callable<Result>> solvers)
   throws InterruptedException, ExecutionException {
   
   CompletionService<Result> ecs = new ExecutorCompletionService<Result>(executor);// 构造器
   
   for (Callable<Result> s : solvers)// 提交所有任务
       ecs.submit(s);
	   
   int n = solvers.size();
   for (int i = 0; i < n; ++i) {// 获取每一个完成的任务
       Result r = ecs.take().get();
       if (r != null)
           use(r);
   }
}

单个任务的超时时间

V Future.get(long timeout, TimeUnit unit)方法可以指定等待的超时时间,超时未完成会抛出TimeoutException

多个任务的超时时间

等待多个任务完成,并设置最大等待时间,可以通过CountDownLatch完成:

public void testLatch(ExecutorService executorService, List<Runnable> tasks) 
	throws InterruptedException{
      
    CountDownLatch latch = new CountDownLatch(tasks.size());
      for(Runnable r : tasks){
          executorService.submit(new Runnable() {
              @Override
              public void run() {
                  try{
                      r.run();
                  }finally {
                      latch.countDown();// countDown
                  }
              }
          });
      }
	  latch.await(10, TimeUnit.SECONDS); // 指定超时时间
  }

线程池和装修公司

以运营一家装修公司做个比喻。公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。

线程池就是程序中的“装修公司”,代劳各种脏活累活。上面的过程对应到线程池上:

// Java线程池的完整构造函数
public ThreadPoolExecutor(
  int corePoolSize, // 正式工数量
  int maximumPoolSize, // 工人数量上限,包括正式工和临时工
  long keepAliveTime, TimeUnit unit, // 临时工游手好闲的最长时间,超过这个时间将被解雇
  BlockingQueue<Runnable> workQueue, // 排期队列
  ThreadFactory threadFactory, // 招人渠道
  RejectedExecutionHandler handler) // 拒单方式

总结

Executors为我们提供了构造线程池的便捷方法,对于服务器程序我们应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造方法,避免无界队列可能导致的OOM以及线程个数限制不当导致的线程数耗尽等问题。ExecutorCompletionService提供了等待所有任务执行结束的有效方式,如果要设置等待的超时时间,则可以通过CountDownLatch完成。

参考

ThreadPoolExecutor API Doc

应用场景:线程池发邮件

注意:带图片的邮件可以直接 base64 照片就可以识别

Spring boot锦集(二):整合邮件发送的四种方法 | 纯文本的邮件、带有图片的邮件、带Html的邮件、带附件的邮件(很详细)_simplemailmessage_血煞长虹的博客-CSDN博客

需要批量向用户发送邮件,并且保存邮件发送状态到数据库。
因为需要批量发送邮件,而我们知道发送邮件其实是一个耗时操作,如果我们让接口等待发送邮件完成后再返回的话,该接口的效率就会非常慢啦~所以说,我们可使用线程池来批量发送邮件。

详细代码

整个代码基于spring boot实现,实现两种线程工作类,第一种是实现Runnable的线程,这种方式不可以拿到返回值,也不可以抛出异常,第二种是实现Callable的线程,这种方式可以拿到返回值,也可以抛出异常。

因为想分析一些其他的内容,所以代码写的略复杂~

详细代码github地址

线程池配置类
ExecutorConfig.java

package com.example.demo.config;

import java.util.concurrent.ThreadPoolExecutor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;


@Configuration
public class ExecutorConfig extends AsyncConfigurerSupport {

    /** 
     * 设置线程池大小 
     **/
    private int corePoolSize = 4;
    /** 
     * 设置线程池最大数量 
     **/
    private int maxPoolSize = 16;
    /** 
     * 线程池阻塞队列的容量
     **/
    private int queueCapacity = 10;

//    private String threadNamePrefix = "omsAsyncExecutor-";

    @Bean
    @Override
    public ThreadPoolTaskExecutor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
//        executor.setThreadNamePrefix(threadNamePrefix);

        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //一定要等线程执行完成的时候才去关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //最大等待时间60s
        executor.setAwaitTerminationSeconds(60);
        //项目启动的时候就初始化线程池,避免到调用的时候才初始化
        executor.initialize();
        return executor;
    }

}

邮件相关信息实体类
EmailModel.java

package com.example.demo;

import java.io.Serializable;

public class EmailModel implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1404185636399251685L;

	private String email;
	private String subject;
	private String content;
	private String attachFilePath;
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public String getSubject() {
		return subject;
	}
	public void setSubject(String subject) {
		this.subject = subject;
	}
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}
	public String getAttachFilePath() {
		return attachFilePath;
	}
	public void setAttachFilePath(String attachFilePath) {
		this.attachFilePath = attachFilePath;
	}
	
}

 实现Callable的线程工作类:EmailThreadPoolTask.Java

package com.example.demo.util;

import java.util.concurrent.Callable;

import org.slf4j.LoggerFactory;

import com.example.demo.service.MailService;

/**
 *
 * @author zhangyuxuan 2019年7月23日
 */

public class EmailThreadPoolTask implements Callable<Boolean> {

	private static final org.slf4j.Logger logger = LoggerFactory.getLogger(EmailNoticeThreadPoolTask.class);
	private MailService mailService;
	private String email;
	private String content;
	private String subject;
	private String filePath;

  /**
   * @param mailService
   * @param email
   * @param content
   * @param subject
   * @param part
   */
  public EmailThreadPoolTask(MailService mailService, String email, String subject, String content, String filePath) {
    this.mailService = mailService;
    this.email = email;
    this.content = content;
    this.subject = subject;
    this.filePath = filePath;
  }

  @Override
  public Boolean call() throws Exception {
	logger.info("call开始");
    boolean status = mailService.sendSimpleMail(email, subject, content);
    logger.info("call结束");
//    		mailService.sendAttachmentMail(email, subject, content, filePath);
    return status;
  }

}

实现Runnable的线程工作类:EmailNoticeThreadPoolTask.java

package com.example.demo.util;

import org.slf4j.LoggerFactory;

import com.example.demo.service.MailService;

/**
 *
 * @author zhangyuxuan 2019年7月23日
 */

public class EmailNoticeThreadPoolTask implements Runnable {

  private static final org.slf4j.Logger logger = LoggerFactory.getLogger(EmailNoticeThreadPoolTask.class);
  private MailService mailService;
  private String email;
  private String content;
  private String subject;
  private String filePath;

  /**
   * @param mailService
   * @param email
   * @param content
   * @param subject
   * @param part
   */
  public EmailNoticeThreadPoolTask(MailService mailService, String email, String subject,
      String content, String filePath) {
    this.mailService = mailService;
    this.email = email;
    this.content = content;
    this.subject = subject;
    this.filePath = filePath;
  }

  @Override
  public void run() {
	logger.info("run开始");
    mailService.sendSimpleMail(email, subject, content);
    logger.info("run结束");
//    mailService.sendAttachmentMail(email, subject, content, filePath);
  }

}

批量发送邮件的Service

package com.example.demo.service.impl;

import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import com.example.demo.EmailModel;
import com.example.demo.service.BatchMailService;
import com.example.demo.service.MailService;
import com.example.demo.util.EmailNoticeThreadPoolTask;
import com.example.demo.util.EmailThreadPoolTask;

@Service
public class BatchMailServiceImpl implements BatchMailService {

	@Autowired
	private ThreadPoolTaskExecutor executor;
	@Autowired
	private MailService mailService;
	
	private static final org.slf4j.Logger logger = LoggerFactory.getLogger(BatchMailServiceImpl.class);

	@Override
	public void batchSendReturnEmail(List<EmailModel> emails) {
		for (EmailModel emailModel : emails) {
			logger.info("向" + emailModel.getEmail() + "发送邮件开始");
			Future<Boolean> statusFuture = executor.submit(new EmailThreadPoolTask(mailService, emailModel.getEmail(), emailModel.getSubject(), emailModel.getContent(), emailModel.getAttachFilePath()));
			// 根据返回值来进行判断下一步操作,注意,future中的get方法是一个阻塞的方法,会一直等到future返回结果才会结束
			try {
				boolean status = statusFuture.get();
				// 根据结果可以进行存入数据库等操作   这边暂时只做输出操作
				logger.info("状态:" + status);
				logger.info("向" + emailModel.getEmail() + "发送邮件结束");
			} catch (InterruptedException | ExecutionException e) {
				e.printStackTrace();
			}
		}
	}

	@Override
	public void batchSendEmail(List<EmailModel> emails) {
		
		for (EmailModel emailModel : emails) {
			logger.info("向" + emailModel.getEmail() + "发送邮件开始");
			try {
				executor.execute(new EmailNoticeThreadPoolTask(mailService, emailModel.getEmail(), emailModel.getSubject(), emailModel.getContent(), emailModel.getAttachFilePath()));
				logger.info("向" + emailModel.getEmail() + "发送邮件结束");
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

}

发送邮件的Service (这里面很多发送邮件的方法其实没有在代码中调用)

package com.example.demo.service.impl;

import java.io.File;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

import com.example.demo.service.MailService;

@Service
public class MailServiceImpl implements MailService {

	private static final org.slf4j.Logger logger = LoggerFactory.getLogger(MailServiceImpl.class);
	@Value("${spring.mail.username}")
    //使用@Value注入application.properties中指定的用户名
    private String from;

    @Autowired
    //用于发送文件
    private JavaMailSender mailSender;
	public boolean sendSimpleMail(String to, String subject, String content) {
	    try {
	        SimpleMailMessage message = new SimpleMailMessage();
	        message.setTo(to);//收信人
	        message.setSubject(subject);//主题
	        message.setText(content);//内容
	        message.setFrom(from);//发信人
	        
	        mailSender.send(message);
	    } catch (Exception e) {
	    	e.printStackTrace();
	    	logger.error("发送HTML邮件失败");
	    	return false;
	    }
	    return true;
    }
	
public boolean sendHtmlMail(String to, String subject, String content) {
        
        logger.info("发送HTML邮件开始:{},{},{}", to, subject, content);
        //使用MimeMessage,MIME协议
        MimeMessage message = mailSender.createMimeMessage();
        
        MimeMessageHelper helper;
        //MimeMessageHelper帮助我们设置更丰富的内容
        try {
            helper = new MimeMessageHelper(message, true);
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content, true);//true代表支持html
            mailSender.send(message);
            logger.info("发送HTML邮件成功");
        } catch (MessagingException e) {
            logger.error("发送HTML邮件失败:", e);
            return false;
        }
        return true;
    }

public boolean sendAttachmentMail(String to, String subject, String content, String filePath) {
    
    logger.info("发送带附件邮件开始:{},{},{},{}", to, subject, content, filePath);
    MimeMessage message = mailSender.createMimeMessage();
    
    MimeMessageHelper helper;
    try {
        helper = new MimeMessageHelper(message, true);
        //true代表支持多组件,如附件,图片等
        helper.setFrom(from);
        helper.setTo(to);
        helper.setSubject(subject);
        helper.setText(content, true);
        FileSystemResource file = new FileSystemResource(new File(filePath));
        String fileName = file.getFilename();
        helper.addAttachment(fileName, file);//添加附件,可多次调用该方法添加多个附件  
        mailSender.send(message);
        logger.info("发送带附件邮件成功");
    } catch (MessagingException e) {
        logger.error("发送带附件邮件失败", e);
        return false;
    }
    return true;
}
public boolean sendInlineResourceMail(String to, String subject, String content, String rscPath, String rscId) {
        
        logger.info("发送带图片邮件开始:{},{},{},{},{}", to, subject, content, rscPath, rscId);
        MimeMessage message = mailSender.createMimeMessage();
        
        MimeMessageHelper helper;
        try {
            helper = new MimeMessageHelper(message, true);
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content, true);
            // 以绝对路径的方式读取文件
            FileSystemResource res = new FileSystemResource(new File(rscPath));
            helper.addInline(rscId, res);//重复使用添加多个图片
            mailSender.send(message);
            logger.info("发送带图片邮件成功");
        } catch (MessagingException e) {
            logger.error("发送带图片邮件失败", e);
            return false;
        }
        return true;
    }
public void sendHtmlImageMail(String to, String subject, String content, String rscPath) {
    
    logger.info("发送带图片邮件开始:{},{},{},{}", to, subject, content, rscPath);
    MimeMessage message = mailSender.createMimeMessage();
    
    MimeMessageHelper helper;
    try {
        helper = new MimeMessageHelper(message, true);
        helper.setFrom(from);
        helper.setTo(to);
        helper.setSubject(subject);
        helper.setText(content, true);
        // cid是固定写法
        helper.setText(  
                "<html><head></head><body><h1>hello!!spring image html mail</h1>"  
                        + "<img src=\"cid:aaa\"/></body></html>", true);  
        FileSystemResource img = new FileSystemResource(new File(rscPath));  
        
        helper.addInline("aaa", img);  

        mailSender.send(message);
        logger.info("发送带图片邮件成功");
    } catch (MessagingException e) {
        logger.error("发送带图片邮件失败", e);
    }
}
}

测试Controller

package com.example.demo.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.example.demo.EmailModel;
import com.example.demo.service.BatchMailService;
import com.example.demo.service.MailService;

@Controller
@RequestMapping(value="/mail")
public class MailController {

	@Autowired
	private MailService mailService;

	@Autowired
	private BatchMailService batchMailService;

	@RequestMapping(value="/simple")
	@ResponseBody
	public void sendMail() {
		mailService.sendSimpleMail("demo@163.com", "test", "我是一封测试邮件");
	}

	@RequestMapping(value="/attach", method = RequestMethod.POST)
	@ResponseBody
	public void sendAttachMail(List<EmailModel> emailModels) {
		
		mailService.sendSimpleMail("demo@163.com", "test", "我是一封测试邮件");
	}
	
	@RequestMapping(value="/batch", method = RequestMethod.POST)
	@ResponseBody
	public void batchSendMail(@RequestBody List<EmailModel> emails) {
		batchMailService.batchSendEmail(emails);
	}
	
	@RequestMapping(value="/batchReturn", method = RequestMethod.POST)
	@ResponseBody
	public void batchSendMailReturn(@RequestBody List<EmailModel> emails) {
		batchMailService.batchSendReturnEmail(emails);
	}
}

测试结果:

使用Callable方式的运行结果,我们可以看到它几乎是顺序执行的,似乎没有达到多线程的结果,实际上并不是因为我们没有使用多线程,而是因为我们代码中使用的获取状态的 future中的get方法是一个阻塞的方法,会一直等到future返回结果才会结束,因而堵塞了线程而已~
在这里插入图片描述
使用Runnable方式的运行结果,我们可以看到多线程的效果拉~
在这里插入图片描述

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值