透過妹子看本質:爬蟲小問題,併發大學問-2併發症要治,還要有Future

     上一篇咱們用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方法獲取執行結果,該方法會阻塞直到任務返回結果。說白了就是對多線程任務的監控和數據傳輸,具體做用有三點:

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

源碼地址

相關文章
相關標籤/搜索