上一篇咱們用jsoup解決了爬蟲解析的問題,但卻留下了下載圖片很慢,效率低下的問題。根據日誌的觀察,能夠看到圖片都是一張一張的下,這種速度怎麼能跟的上我閱遍天下美女的雄心,因而,多線程下圖必需要上場了。java
springboot配置多線程其實很簡單,首先是要配好線程池,這個須要一個config類來配置:git
import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @EnableAsync @Configuration class TaskPoolConfig { @Bean("taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //設置核心線程數 executor.setCorePoolSize(10); //設置最大線程數 executor.setMaxPoolSize(20); //線程池所使用的緩衝隊列 executor.setQueueCapacity(200); // 設置線程活躍時間(秒) executor.setKeepAliveSeconds(60); // 線程名稱前綴 executor.setThreadNamePrefix("taskExecutor-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 等待全部任務結束後再關閉線程池 executor.setWaitForTasksToCompleteOnShutdown(true); // 等待時間 (默認爲0,此時當即中止),並沒等待xx秒後強制中止 executor.setAwaitTerminationSeconds(60); return executor; } }
@Configuration註解表示這是一個配置類,效果等同於xml、properties、yml之類的配置文件,不過用類能夠更靈活一下。@EnableAsync則說明這個工程須要用多線程,不然即便後面掛了異步的註解,也是不生效的。另外不少教程要求@EnableAsync掛在SpringBootApplication類上面,其實不是必須的,在配置類加了@EnableAsync,多線程就自動生效了。具體線程池的具體參數代碼上都註釋了,就不詳述了。web
配置完成後,咱們天然要對下載圖片的controller下手了,立刻配置下載圖片的類爲異步類,開始多線程下載。spring
downImages()方法是我工具類DownloadUtils的一個static 方法,我把@Async註解加上去,表示這是一個異步方法,會多線程執行。我而後在controller裏面調用,想着電腦飛快的下載妹子圖了。但一運行,我錯了,圖片仍然在一張一張按順序緩慢的下載:springboot
開始下載 2019-06-12 12:11:56.854 INFO 13404 --- [nio-8080-exec-1] com.skyblue.crawel.utils.DownloadUtils : http://ac.meijiecao.net/ac/img/znb/meizitu/20190611_meizitu_07.jpg 2019-06-12 12:12:00.453 INFO 13404 --- [nio-8080-exec-1] com.skyblue.crawel.utils.DownloadUtils : http://ac.meijiecao.net/ac/img/znb/meizitu/20190611_meizitu_08.jpg 2019-06-12 12:12:06.620 INFO 13404 --- [nio-8080-exec-1] com.skyblue.crawel.utils.DownloadUtils : http://ac.meijiecao.net/ac/img/znb/meizitu/20190611_meizitu_01.jpg 2019-06-12 12:12:17.294 INFO 13404 --- [nio-8080-exec-1] com.skyblue.crawel.utils.DownloadUtils : http://ac.meijiecao.net/ac/img/znb/meizitu/20190611_meizitu_02.jpg
日誌中[nio-8080-exec-1]就是主線程的名稱,顯然多線程沒起做用,難道是個人線程池沒起做用,我有向上翻了翻項目啓動的日誌,赫然發現:多線程
2019-06-12 12:11:46.222 INFO 13404 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'taskExecutor'
在項目啓動完成前,線程池taskExecutor就已經初始化完成了,可見線程池是已經建好了,但卻沒用上。這是爲啥呢?併發
併發症第一條:static 方法使用@Async註解無效app
既然static 方法不能用,那麼我搞個非靜態方法調用調用static方法,而後我在這個非靜態方法上面使用@Async不就行了,我真是聰明機智。異步
@Async private void downloadMeizitu(String url) { ...... DownloadUtils.downImages(filePath, imgSrc, map); ...... }
我繼續憧憬着可以實現快速下圖,滿含但願的又開始了爬蟲行動。工具
一陣尷尬的沉默....
我又失敗了,日誌中仍然是主線程在慢悠悠的下載着圖片。
併發症第二條:異步方法和調用異步方法不能在同一個類裏面
好吧,既然這麼多規矩,我只好另外建了一個service類來申明異步方法,而後讓controller調用
@Component public class DownloadAsyncService { ...... @Async public void downloadImage(String filePath, String imgUrl, Map<String, String> requestPropMap) { DownloadUtils.downImages(filePath, imgUrl, requestPropMap); } ......
@RestController @RequestMapping("/crawler") public class CrawlerController { ...... String imgSrc = element.attr("src"); Map<String,String> map = new HashMap<String,String>(); map.put("Referer", url); map.put("User-Agent", "Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1"); downloadAsyncService.downloadImage(filePath, imgSrc,map);//多線程下圖 logger.info(imgSrc); ......
此次我懷着忐忑的心情開始了下載......
2019-06-12 14:33:12.838 INFO 7384 --- [taskExecutor-10] c.s.crawel.service.DownloadAsyncService : http://ac.meijiecao.net/ac/img/znb/meizitu/20190611_meizitu_05.jpg 2019-06-12 14:33:12.838 INFO 7384 --- [taskExecutor-10] c.s.crawel.service.DownloadAsyncService : E:/youtube/images/zhainanfuli/21483 2019-06-12 14:33:17.674 INFO 7384 --- [ taskExecutor-3] c.s.crawel.service.DownloadAsyncService : http://ac.meijiecao.net/ac/img/znb/meizitu/20190611_meizitu_01.jpg 2019-06-12 14:33:17.675 INFO 7384 --- [ taskExecutor-3] c.s.crawel.service.DownloadAsyncService : E:/youtube/images/zhainanfuli/21483 2019-06-12 14:33:18.892 INFO 7384 --- [ taskExecutor-2] c.s.crawel.service.DownloadAsyncService : http://ac.meijiecao.net/ac/img/znb/meizitu/20190611_meizitu_08.jpg 2019-06-12 14:33:18.892 INFO 7384 --- [ taskExecutor-2] c.s.crawel.service.DownloadAsyncService : E:/youtube/images/zhainanfuli/21483 2019-06-12 14:33:19.324 INFO 7384 --- [ taskExecutor-7] c.s.crawel.service.DownloadAsyncService : http://ac.meijiecao.net/ac/img/znb/meizitu/20190611_meizitu_10.jpg 2019-06-12 14:33:19.324 INFO 7384 --- [ taskExecutor-7] c.s.crawel.service.DownloadAsyncService : E:/youtube/images/zhainanfuli/21483 2019-06-12 14:33:21.263 INFO 7384 --- [ taskExecutor-9] c.s.crawel.service.DownloadAsyncService : http://ac.meijiecao.net/ac/img/znb/meizitu/20190611_meizitu_04.jpg 2019-06-12 14:33:21.263 INFO 7384 --- [ taskExecutor-9] c.s.crawel.service.DownloadAsyncService : E:/youtube/images/zhainanfuli/21483 2019-06-12 14:33:21.575 INFO 7384 --- [ taskExecutor-8] c.s.crawel.service.DownloadAsyncService : http://ac.meijiecao.net/ac/img/znb/meizitu/20190611_meizitu_09.jpg
看這個日誌就知道,[taskExecutor-x],10個線程已經撒開了手腳各自下圖去了,至此,多線程下圖的問題終於解決了。
但我不是一個容易知足的人不然幹嗎要下這麼多妹子圖,這種只求線程跑,不跟蹤線程結果的事不是我這種善始善終,負責人的人會幹出來的,我爲了證實個人人品看這種圖的人有什麼人品,我決定要跟蹤下線程的結果,何時結束,也方便之後線程結束時知道線程的運行時間,後續事件觸發啥的。因而,我又掏出了Future。
Future
是對於具體的Runnable
或者Callable
任務的執行結果進行取消、查詢是否完成、獲取結果的接口。必要時能夠經過get方法獲取執行結果,該方法會阻塞直到任務返回結果。說白了就是對多線程任務的監控和數據傳輸,具體做用有三點:
中斷任務咱們用不上,咱們須要判斷圖片啥時候完成,另外還須要知道圖片下完的時間是多少,讓咱們繼續擼代碼:
@Async public Future<DownloadFile> downloadImage(DownloadFile downloadFile,String filePath, String imgUrl, Map<String, String> requestPropMap) { logger.info("====="+filePath); DownloadUtils.downImages(filePath, imgUrl, requestPropMap); downloadFile.setEndDate(new Date()); return new AsyncResult<DownloadFile> (downloadFile); }
DownloadFile是我記錄下載信息的,先不用管。首先我把下載圖片的一步方法加上了返回值,Future類型,返回一個時間。而後在controller裏面取出futrue的結果,把結束時間打印到日誌裏面:
Future<Date> future = downloadAsyncService.downloadImage(filePath, imgSrc,map);//多線程下圖 try { logger.info(DateUtils.dateTimeDetail(future.get())); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ExecutionException e) { // TODO Auto-generated catch block e.printStackTrace(); }
我一運行,結果多線程又不靈了,一晚上回到解放前,下圖又開始單線程執行了。這是爲啥呢,我打開Future.get()的註釋,發現寫着:
Waits if necessary for the computation to complete, and thenretrieves its result.
原來取值的時候會等待計算結果,直到計算完成返回結果值,這樣的話多線程就等成了單線程了。
併發症第三條:FUTURE.GET()會阻塞多線程的運行,直到當前線程結果返回爲止。
那怎麼辦呢,正確的使用方法應該是這樣的,先建立一個對象存放線程須要保存的內容,就是上面出現過的DownloadFile:
public class DownloadFile { String fileName;//下載的文件名 Date beginDate;//開始下載時間 Date endDate;//結束下載時間 ......省略get,set /**下載的時長(單位是毫秒) * @return */ public long getDuration() { return (endDate.getTime()-beginDate.getTime()); }
獲取future值並展示出來
private void downloadMeizitu(String url) { ...... //用一個list存放future對象 List<Future<DownloadFile>> list = new ArrayList<Future<DownloadFile>>(); for (Element element : imgs) { ...... Future<DownloadFile> future = downloadAsyncService.downloadImage(downloadFile,filePath, imgSrc,map);//多線程下圖 list.add(future); } //循環讀取future的內容 for(Future<DownloadFile> future:list) { logger.info("size=================="+String.valueOf(list.size())); while(true) { if(future.isDone()) {//線程執行完畢 try { logger.info(future.get().getFileName()+"耗時"+future.get().getDuration()+"毫秒"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ExecutionException e) { // TODO Auto-generated catch block e.printStackTrace(); } break; } } } ......
這樣,執行完畢後就可以獲取到每張圖片的下載時間了:
2019-06-12 17:57:43.559 INFO 5476 --- [nio-8080-exec-1] c.skyblue.crawel.web.CrawlerController : E:/youtube/images/zhainanfuli/17261耗時1251毫秒 2019-06-12 17:57:43.568 INFO 5476 --- [ taskExecutor-2] c.s.crawel.service.DownloadAsyncService : *****http://ac.meijiecao.net/ac/img/znb/meizitu/20180123_meizitu_02.jpg 2019-06-12 17:57:43.569 INFO 5476 --- [nio-8080-exec-1] c.skyblue.crawel.web.CrawlerController : E:/youtube/images/zhainanfuli/17261耗時1253毫秒 2019-06-12 17:57:43.570 INFO 5476 --- [nio-8080-exec-1] c.skyblue.crawel.web.CrawlerController : E:/youtube/images/zhainanfuli/17261耗時1092毫秒 2019-06-12 17:57:43.571 INFO 5476 --- [nio-8080-exec-1] c.skyblue.crawel.web.CrawlerController : E:/youtube/images/zhainanfuli/17261耗時906毫秒 2019-06-12 17:57:43.571 INFO 5476 --- [nio-8080-exec-1] c.skyblue.crawel.web.CrawlerController : E:/youtube/images/zhainanfuli/17261耗時1078毫秒 2019-06-12 17:57:43.737 INFO 5476 --- [ taskExecutor-6] c.s.crawel.service.DownloadAsyncService : *****http://ac.meijiecao.net/ac/img/znb/meizitu/20180123_meizitu_06.jpg 2019-06-12 17:57:43.738 INFO 5476 --- [nio-8080-exec-1] c.skyblue.crawel.web.CrawlerController : E:/youtube/images/zhainanfuli/17261耗時1421毫秒 2019-06-12 17:57:43.872 INFO 5476 --- [ taskExecutor-7] c.s.crawel.service.DownloadAsyncService : *****http://ac.meijiecao.net/ac/img/znb/meizitu/20180123_meizitu_07.jpg 2019-06-12 17:57:43.873 INFO 5476 --- [nio-8080-exec-1] c.skyblue.crawel.web.CrawlerController : E:/youtube/images/zhainanfuli/17261耗時1556毫秒 2019-06-12 17:57:43.873 INFO 5476 --- [nio-8080-exec-1] c.skyblue.crawel.web.CrawlerController : E:/youtube/images/zhainanfuli/17261耗時914毫秒 2019-06-12 17:57:44.055 INFO 5476 --- [ taskExecutor-9] c.s.crawel.service.DownloadAsyncService : *****http://ac.meijiecao.net/ac/img/znb/meizitu/20180123_meizitu_09.jpg 2019-06-12 17:57:44.055 INFO 5476 --- [nio-8080-exec-1] c.skyblue.crawel.web.CrawlerController : E:/youtube/images/zhainanfuli/17261耗時1739毫秒
因爲多線程的緣由,下載完成的耗時日誌內容和下載的內容混雜在了一塊兒,這也是多線程正在執行的一個體現。
至此,咱們不只用多線程下了圖片,並且還用future傳遞了多線程的內容。但多線程是個很複雜的事,超時怎麼辦,線程的監控怎麼處理,都須要進一步研究。但我畢竟是個下妹子圖的,對我要求不能過高,這一章就講到這,那些高級內容等我在看妹子圖的間隙再寫吧。