透過妹子看本質:爬蟲小問題,併發大學問-2併發症要治,還要有Future
上一篇我們用jsoup解決了爬蟲解析的問題,但卻留下了下載圖片很慢,效率低下的問題。根據日誌的觀察,可以看到圖片都是一張一張的下,這種速度怎麼能跟的上我閱遍天下美女的雄心,於是,多執行緒下圖必須要上場了。
springboot配置多執行緒其實很簡單,首先是要配好執行緒池,這個需要一個config類來配置:
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,多執行緒就自動生效了。具體執行緒池的具體引數程式碼上都註釋了,就不詳述了。
配置完成後,我們自然要對下載圖片的controller下手了,馬上配置下載圖片的類為非同步類,開始多執行緒下載。
downImages()方法是我工具類DownloadUtils的一個static 方法,我把@Async註解加上去,表示這是一個非同步方法,會多執行緒執行。我然後在controller裡面呼叫,想著電腦飛快的下載妹子圖了。但一執行,我錯了,圖片仍然在一張一張按順序緩慢的下載:
開始下載 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註解無效
既然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傳遞了多執行緒的內容。但多執行緒是個很複雜的事,超時怎麼辦,執行緒的監控怎麼處理,都需要進一步研究。但我畢竟是個下妹子圖的,對我要求不能太高,這一章就講到這,那些高階內容等我在看妹子圖的間隙再寫吧。