一,為什么要使用async異步線程池?
1,在生產環境中,有一些需要延時處理的業務場景:
例如:發送電子郵件,
給手機發短信驗證碼
大數據量的查詢統計
遠程抓取數據等
這些場景占用時間較長,而用戶又沒有必須立刻得到返回數據的需求,
我們如果讓用戶占用到服務器的連接長時間等待也沒有必要,
這時異步處理是優先選擇。
2,使用線程池的好處?
第一,提高資源利用率:可以重復利用已經創建了的線程
第二,提高響應速度:如果有線程處於等待分配任務狀態時,則任務到來時無需創建線程就能被執行
第三,具有可管理性:線程池能減少創建和銷毀線程帶來的系統開銷
說明:劉宏締的架構森林是一個專注架構的博客,地址:https://www.cnblogs.com/architectforest
對應的源碼可以訪問這里獲取: https://github.com/liuhongdi/
說明:作者:劉宏締 郵箱: 371125307@qq.com
二,演示項目的相關信息
1,項目地址:
https://github.com/liuhongdi/asyncmail
2,項目的說明:
regmail:演示異步發送一封注冊成功的郵件
sleep:同步執行sleep1秒
asyncsleep:演示異步執行十個各sleep1秒的線程
3,項目的結構,如圖:
三,演示項目的配置文件說明:
1,pom.xml
<!--mail begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <!--mail end--> <!--thymeleaf begin--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!--thymeleaf end-->
引入mail和thymeleaf兩個依賴,其中thymeleaf是作為html格式郵件內容的模板
2,application.properties
spring.mail.host=smtp.163.com spring.mail.username=demouser@163.com spring.mail.password=demopassword spring.mail.default-encoding=UTF-8 spring.mail.protocol=smtps spring.mail.port=465 spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true
說明:此處注意:如果是在阿里雲ecs上運行,
不要使用25的smtp端口,要使用帶ssl的smtps,端口是465
另外:這里配置的password,是郵箱的授權碼,不是登錄密碼,
有疑問可以參見這一篇:
https://www.cnblogs.com/architectforest/p/12924395.html
四,java代碼說明:
1,AsyncConfig.java
//線程池的配置 @Configuration @EnableAsync public class AsyncConfig { @Bean(name = "taskExecutor") public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 設置核心線程數,它是可以同時被執行的線程數量 executor.setCorePoolSize(5); // 設置最大線程數,緩沖隊列滿了之后會申請超過核心線程數的線程 executor.setMaxPoolSize(10); // 設置緩沖隊列容量, executor.setQueueCapacity(20); // 設置線程生存時間(秒),當超過了核心線程出之外的線程在生存時間到達之后會被銷毀 executor.setKeepAliveSeconds(60); // 設置線程名稱前綴 executor.setThreadNamePrefix("threadpool-"); // 設置拒絕策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 等待所有任務結束后再關閉線程池 executor.setWaitForTasksToCompleteOnShutdown(true); //初始化 executor.initialize(); return executor; } }
說明:線程池的配置文件,
這個有個問題:核心線程數設置為多少為宜?
設N為cpu的核心數量,則
如果是CPU密集型應用,則線程池大小設置為N+1
如果是IO密集型應用,則線程池大小設置為2N+1
這個是常用的一個設置參考,具體是否適用自己的業務還要在生產環境中觀察
但可以確認的是:線程數不是越多越好,因為所在機器上的cpu等硬件並沒有變化,
異步也只是提高吞吐量,並不能加快任務的執行
2,MailUtil.java
@Component public class MailUtil { @Resource private JavaMailSender javaMailSender; @Resource TemplateEngine templateEngine; //發送html內容的郵件,使用thymeleaf渲染頁面 public void sendHtmlMail(String from, String[] to, String[] cc, String[] bcc, String subject, String templateName, HashMap<String,String> content) throws MessagingException { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); helper.setSubject(subject); helper.setFrom(from); helper.setTo(to); //抄送,收到郵件用戶可以看到其他收件人 if (cc != null && cc.length > 0) { helper.setCc(cc); } //密送 收到郵件用戶看不到其他收件人 if (bcc != null && bcc.length > 0) { helper.setBcc(bcc); } helper.setSentDate(new Date()); //生成郵件模板上的內容 Context context = new Context(); if (content != null && content.size() > 0) { for (String key : content.keySet()) { context.setVariable(key, content.get(key)); } } String process = templateEngine.process(templateName, context); helper.setText(process,true); javaMailSender.send(mimeMessage); } }
說明:功能是發送html內容的郵件,所以用到了templateEngine
3,郵件內容:regmail.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>您好,注冊成功! 以下是您在本網站的注冊信息:</h1> <table border="1"> <tr> <td>用戶名</td> <td th:text="${username}">${username}</td> </tr> <tr> <td>昵稱</td> <td th:text="${nickname}">${nickname}</td> </tr> <tr> <td>ID</td> <td th:text="${id}">${id}</td> </tr> </table> <div style="color: #ff1a0e">本站網址:http://springio.com</div> </body> </html>
4,MailServiceImpl.java
@Service public class MailServiceImpl implements MailService { @Resource private MailUtil mailUtil; //異步發送html格式的郵件 @Async @Override public void sendHtmlMail() { String from = "demouser@163.com"; String[] to = {"371125307@qq.com"}; String subject = "恭喜您成功注冊老劉代碼庫網站"; HashMap<String,String> content= new HashMap<String,String>(); content.put("username","laoliu"); content.put("nickname","老劉"); content.put("id","0000001"); String templateName= "mail/regmail.html"; try { mailUtil.sendHtmlMail(from, to, null, null, subject, templateName, content); } catch (MessagingException e) { e.printStackTrace(); System.out.println("郵件發送出錯"); } } }
功能:生成郵件的內容,注意這里指定的模板文件和模板上的變量
5,SlowServiceImpl.java
@Service public class SlowServiceImpl implements SlowService { private static Logger log= LoggerFactory.getLogger(SlowServiceImpl.class); //sleep 1秒 @Override public void sleepawhile(){ long startTime = System.currentTimeMillis(); log.info("function sleep begin"); try { Thread.sleep(1000); //延時1秒 } catch(InterruptedException e) { e.printStackTrace(); } log.info("function sleep end"); } //sleep 1秒,異步執行,並返回一個統計用的字串 @Async @Override public Future<String> asyncsleepawhile(int i){ log.info("async function sleep begin"); String start=TimeUtil.getMilliTimeNow(); try { Thread.sleep(1000); //延時1秒 } catch(InterruptedException e) { e.printStackTrace(); } log.info("async function sleep end"); String end=TimeUtil.getMilliTimeNow(); return new AsyncResult<>(String.format("第{%s}個異步調用asyncsleepawhile方法:開始時間:%s,結束時間:%s", i,start,end)); } }
僅供演示用,兩個方法:一個同步sleep,一個異步sleep
6,HomeController.java
@RequestMapping("/home") @Controller public class HomeController { private static Logger log= LoggerFactory.getLogger(HomeController.class); @Resource private MailService mailService; @Resource private SlowService slowService; //異步發送一封注冊成功的郵件 @GetMapping("/regmail") @ResponseBody public String regMail(ModelMap modelMap) { mailService.sendHtmlMail(); return "mail sended"; } //同步sleep1秒 @GetMapping("/sleep") @ResponseBody public String sleep() { System.out.println(TimeUtil.getMilliTimeNow()+" controller begin"); slowService.sleepawhile(); System.out.println(TimeUtil.getMilliTimeNow()+" controller end"); return "mail sended"; } //異步執行sleep1秒10次 @GetMapping("/asyncsleep") @ResponseBody public Map<String, Object> asyncsleep() throws ExecutionException, InterruptedException { long start = System.currentTimeMillis(); Map<String, Object> map = new HashMap<>(); List<Future<String>> futures = new ArrayList<>(); for (int i = 0; i < 10; i++) { Future<String> future = slowService.asyncsleepawhile(i); futures.add(future); } List<String> response = new ArrayList<>(); for (Future future : futures) { String string = (String) future.get(); response.add(string); } map.put("data", response); map.put("消耗時間", String.format("任務執行成功,耗時{%s}毫秒", System.currentTimeMillis() - start)); return map; } }
五,效果測試:
1,發送注冊郵件:
訪問:
http://127.0.0.1:8080/home/regmail
收到郵件:
2,測試異步線程池:
訪問:
http://127.0.0.1:8080/home/asyncsleep
查看輸出:
{"data":["第{0}個異步調用asyncsleepawhile方法:開始時間:2020-07-27 17:10:10.937,結束時間:2020-07-27 17:10:11.941",
"第{1}個異步調用asyncsleepawhile方法:開始時間:2020-07-27 17:10:10.937,結束時間:2020-07-27 17:10:11.945",
"第{2}個異步調用asyncsleepawhile方法:開始時間:2020-07-27 17:10:10.939,結束時間:2020-07-27 17:10:11.940",
"第{3}個異步調用asyncsleepawhile方法:開始時間:2020-07-27 17:10:10.938,結束時間:2020-07-27 17:10:11.940",
"第{4}個異步調用asyncsleepawhile方法:開始時間:2020-07-27 17:10:10.939,結束時間:2020-07-27 17:10:11.941",
"第{5}個異步調用asyncsleepawhile方法:開始時間:2020-07-27 17:10:11.943,結束時間:2020-07-27 17:10:12.944",
"第{6}個異步調用asyncsleepawhile方法:開始時間:2020-07-27 17:10:11.944,結束時間:2020-07-27 17:10:12.944",
"第{7}個異步調用asyncsleepawhile方法:開始時間:2020-07-27 17:10:11.944,結束時間:2020-07-27 17:10:12.945",
"第{8}個異步調用asyncsleepawhile方法:開始時間:2020-07-27 17:10:11.945,結束時間:2020-07-27 17:10:12.946",
"第{9}個異步調用asyncsleepawhile方法:開始時間:2020-07-27 17:10:11.946,結束時間:2020-07-27 17:10:12.946"],
"消耗時間":"任務執行成功,耗時{2023}毫秒"}
這里大家注意思考一下,每個線程sleep了一秒,
我們啟用了10個線程,為什么用的時間是2秒?
原因在於我們對線程池中核心線程數量的設置:可以並發執行5個線程,
所以10個線程共執行了兩次,每次5個,每次都是1秒,兩次用時2秒
六,查看spring boot版本:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE)