爬蟲中 阻塞隊列和 線程池 造成的類死鎖問題

很早就想嘗試一下爬蟲,相關的博文已經很多,這里記下幾個困擾了我挺久的問題。
既然說的是死鎖,我們來復習一下死鎖的四個條件:

  • 循環(huán)等待
  • 占有且請求(請求與持有)
  • 互斥(資源有限,每次只能被一個或一類線程使用)
  • 不可搶占(不可剝奪,無優(yōu)先級)
    四個條件中不可被破壞的是互斥條件,即多進程同時訪問會有數(shù)據(jù)的不一致性。

言歸正傳,首先在我的實現(xiàn)中自定義了線程池

    public ThreadPoolExecutor getFixedThreadPool(int corePoolSize,int maxPoolSize,int waitingQueuesize) {
        return new ThreadPoolExecutor(corePoolSize, maxPoolSize, 60L, TimeUnit.SECONDS, 
                new ArrayBlockingQueue<Runnable>(waitingQueuesize),new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable run, ThreadPoolExecutor executor) {
                        if(!executor.isTerminated())
                            try {
                                executor.getQueue().put(run);
                            } catch (Exception e) {
                                LOG.error("Inner Exception in workQueue, putting task  error",e);
                            }
                    }
                });
        
        //return Executors.newFixedThreadPool(2*nThread);
    }

這是一個類newFixedThreadPool的操作,自定義了等待隊列的大小,同時隊列滿時阻塞了入隊操作,避免Int.maxValue造成的溢出或是過多的任務堆積,兼具fixedThreadPool和cachedThreadPool的優(yōu)點,可這里阻塞的拒絕策略讓我在后續(xù)的實現(xiàn)中飽受折磨...

然后為了避免任務都被阻塞在線程池,我又額外開了一個阻塞隊列存儲爬出來的待爬URL,然后用一個監(jiān)控線程監(jiān)控這個隊列,爬蟲任務相當于生產者生產待爬URL,監(jiān)控線程相當于消費者消費產生的URL,而這個隊列就是倉庫了,完美的設計。。。

public class WatchTask implements Runnable{
        @Override
        public void run() {
            while(isCrawl) {
                try{
                    String url=urlQueue.poll(1000, TimeUnit.MILLISECONDS);
                    while(StringUtils.isNotBlank(url) && isCrawled(url)) url=urlQueue.poll(100, TimeUnit.MILLISECONDS);
                    if(StringUtils.isNotBlank(url)) executor.execute(new WorkTask(url));
                }catch(Exception e) {
                    LOG.error("Taking url from blocking queue error, urlQueue size:"+urlQueue.size(),e);
                }
                lastCur=System.currentTimeMillis();
                LOG.info("WatchTask running, urlQueue:"+urlQueue.size());
            }
        }
        
    }
    
    public class WorkTask implements Runnable{
        private String seedUrl=null;
        
        public WorkTask(String seedUrl) {
            this.seedUrl=seedUrl;
        }
        
        @Override
        public void run() {
            List<String> urls;
            try {
                urls=crawler.doCrawl(seedUrl);
                if(urls==null || urls.size()==0) return;
                for(String url:urls) {
                    if(StringUtils.isNotBlank(url) && !isCrawled(url)) {
                        urlQueue.put(url);
                    }
                }
            }catch(Exception e) {
                LOG.error("Puting url to blocking queue error, size:"+urlQueue.size(),e);
            }
        }
    }

在程序中URL隊列的大小要遠大于線程池等待隊列,明眼的朋友到這里應該看出我的操作問題在哪里了:


死鎖示意圖

于是,將額外的阻塞隊列和監(jiān)控任務去掉,工作線程改成這樣,頗有種自給自足的感覺:

    
    public class WorkTask implements Runnable{
        private String seedUrl=null;
        
        public WorkTask(String seedUrl) {
            this.seedUrl=seedUrl;
        }
        
        @Override
        public void run() {
            List<String> urls;
            try {
                urls=crawler.doCrawl(seedUrl);
                if(urls==null || urls.size()==0) return;
                for(String url:urls) {
                    if(StringUtils.isNotBlank(url) && !isCrawled(url)) {
                        executor.execute(new WorkTask(url));
                    }
                }
            }catch(Exception e) {
                LOG.error("Puting url to blocking queue error, size:"+urlQueue.size(),e);
            }
        }
    }

然而,實際運行中發(fā)現(xiàn)能爬取的數(shù)據(jù)條數(shù)在線程池最大線程數(shù)左右,往后程序就像掛掉一樣雖然在跑但什么輸出都沒有,肯定又是阻塞了!
經過一番分析,發(fā)現(xiàn)問題回到了線程池本身的等待隊列,圓圈代表線程池,黑點表示線程非空閑:


線程池死鎖示意圖

就這樣,又一個死鎖創(chuàng)造出來了,其原因歸根到底還是一個種子url能爬取出來的子URL太多了——幾百甚至幾千上萬個(沒錯我在爬某網(wǎng)用戶信息,子url是用戶的粉絲或其關注的人,因為一些需求不能進行部分舍棄),既然如此那就把等待隊列設至大一點,對子url太多的,全部舍棄,至于何為多大家自有判斷,我用子url數(shù)和等待隊列大小關系來決定,當?shù)却犃兄衭rl數(shù)量超過等待隊列容量的一半,或子url數(shù)量超過隊列數(shù)量一半退出:

    public class WorkTask implements Runnable{
        private String seedUrl=null;
        
        public WorkTask(String seedUrl) {
            this.seedUrl=seedUrl;
        }
        
        @Override
        public void run() {
            List<String> urls;
            try {
                urls=crawler.doCrawl(seedUrl);
                int size=tpe.getQueue().size();
                //無子url,或隊列中任務數(shù)量超過容量一半,或url數(shù)量超過隊列數(shù)量一半,避免崩掉故退出
                if(urls==null || urls.size()==0 || size>halfQueueSize || urls.size()>halfQueueSize) return;
                System.out.println("****************add to queue with size"+urls.size());
                for(String url:urls) {
                    if(StringUtils.isNotBlank(url) && !isCrawled(url)) {
                        executor.execute(new WorkTask(url));
                    }
                }
            }catch(Exception e) {
                LOG.error("Puting url to blocking queue error, size:"+urlQueue.size(),e);
            }
        }
    }

至此,終于把死鎖的問題解決了,但是爬蟲跑了一會ip就被封了,下一步是使用代理。

本文為本人解決實際問題的記錄,有任何高見歡迎留言。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 本文是我自己在秋招復習時的讀書筆記,整理的知識點,也是為了防止忘記,尊重勞動成果,轉載注明出處哦!如果你也喜歡,那...
    波波波先森閱讀 11,652評論 4 56
  • 為什么使用線程池 當我們在使用線程時,如果每次需要一個線程時都去創(chuàng)建一個線程,這樣實現(xiàn)起來很簡單,但是會有一個問題...
    閩越布衣閱讀 4,424評論 10 45
  • 1.ios高性能編程 (1).內層 最小的內層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結構(3).初始化時...
    歐辰_OSR閱讀 30,286評論 8 265
  • 尿毒癥透析 我們生活中常會聽說透析這個詞,那么具體什么是透析呢?所謂透析,是通過小分子經過半透膜擴散到水的原理,將...
    jw8868閱讀 364評論 0 0
  • 晴天入夜絜云翔,斷續(xù)輕風過粵江。 璀璨霓燈迷岸際,斑斕碧影映穹蒼。 扶搖廣宇千尋塔,漫卷浮波百尺廊。 彩舸穿梭人攢...
    海1619閱讀 276評論 0 10

友情鏈接更多精彩內容