1. 程式人生 > >透過妹子看本質:爬蟲小問題,併發大學問-2併發症要治,還要有Future

透過妹子看本質:爬蟲小問題,併發大學問-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方法獲取執行結果,該方法會阻塞直到任務返回結果。說白了就是對多執行緒任務的監控和資料傳輸,具體作用有三點:

  1. 判斷任務是否完成;
  2. 能夠中斷任務;
  3. 能夠獲取任務執行結果

中斷任務我們用不上,我們需要判斷圖片啥時候完成,另外還需要知道圖片下完的時間是多少,讓我們繼續擼程式碼:

@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傳遞了多執行緒的內容。但多執行緒是個很複雜的事,超時怎麼辦,執行緒的監控怎麼處理,都需要進一步研究。但我畢竟是個下妹子圖的,對我要求不能太高,這一章就講到這,那些高階內容等我在看妹子圖的間隙再寫吧。

原始碼地