香港開闊原野鳥類數量跌7成 香港觀鳥會促政府多保護農地、近郊荒地

摘錄自2020年11月5日香港01報導

香港觀鳥會為了更了解香港的鳥類分布,與義工及浸會大學協力作出為期3年(2016至2019)的研究。研究中發現開闊原野雀鳥的分布量大跌7成,調查中的281種鳥類,發現繁殖季中有29種鳥類分布範圍縮減,而冬季則有31種,當中佔大多數的為開闊原野鳥類。包括一年四季棲息在香港的喜鵲分布範圍收窄高達6成,而喜歡在農地棲息的理氏鷚及山斑鳩,前者約下跌6成,後者下跌超過8成。反映開闊原野的環境遭到破壞。

香港觀鳥會指出,鳥類分布的改變能說明環境指標實況,現時的開闊原野如農地、低地草坡、近郊荒地等大多未有受被劃在郊野公園,亦不受法律保護。棲息在開闊原野的鳥類眾多,惟政府過往未有好好關注這些地方。反而以往較為側重保護一些森林和濕地,希望政府日後能多關注及保護農地及近郊荒地。

生物多樣性
國際新聞
香港
鳥類調查
鳥類保育

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

拜登勝選,環境翻盤——從野火談到頁岩油 美國大選氣候看點

環境資訊中心綜合外電;許祖菱、鄒敏惠 編譯;鄒敏惠 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

專家駐點「能源檢查站」 一年服務10萬人 省電30億度

環境資訊中心記者 陳文姿報導

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

南韓推動十年逐步禁塑計畫

摘錄自2018年9月5日公視新聞報導

南韓為了加強環保,制定減少塑膠生產量的「十年計畫」,提出未來十年,將逐步禁止一次性塑膠杯及塑膠吸管;此外,廚餘處理因秤重計費成效不錯,將逐漸取消以袋計費方式,估計可減少三成五的食物浪費。

南韓人每年大約用掉257億個一次性塑膠杯,以及100億根一次性塑膠吸管,為了不再製造更多塑膠產品以及減少資源浪費,南韓政府制定十年計劃,2027年之前,一次性使用的塑膠杯、塑膠吸管,將逐步禁用,此外,產品過度包裝也會被禁止;預計將南韓本土製造出的廢棄物,從目前的處理費每10億韓圜、相當於台幣274萬為單位的95.5噸,減少到76.4噸。

南韓環境部資源再生局副局長椿萬(音譯)表示,「我們首要任務就是在整個生產、消耗、管理,以及回收過程中減少浪費,並且將廢物重新配流到生產過程中。」

此外,為了減少食物的浪費,將強制規定所有超過一定規模的公寓大樓安裝有無線射頻辨識系統RFID的廚餘回收桶,計畫於2022年前達成,各餐廳於10年內強制安裝;持有RFID卡的民眾,必須先刷卡,蓋子才會打開,將廚餘倒進去後依重量繳費,也就是說,廚餘越重就越貴,一改過去無RFID回收桶而必須用回收袋固定計費的方法。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

俄出現「鯨魚監獄」 傳跟中國建水族館有關

摘錄自2018年11月16日大紀元報導

近日,一家位於俄羅斯遠東納霍德卡港附近的圈養著100多頭鯨魚的水上農場被媒體曝光,據知情者提供的無人機航拍視頻顯示,在海邊圈起的一片水域中,安放有十個水槽,在開放的七個水槽中,可以看到其中有很多鯨魚,大多數為珍稀的白鯨,超過90頭。俄羅斯動物保護人士指責這些圈養鯨魚的環境狹小惡劣,可以被稱為是「鯨魚監獄」。

俄羅斯動物保護組織「撒哈林生態監督」負責人德米德里•利西岑對此評論說:「近年來中國興建了大批的水族館,僅在最近兩年就有32家新的水族館開業。每個水族館都需要數頭鯨魚,所以中國方面有巨大的需求,而這些鯨魚都是為向中國出口而捕撈的。」

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

燒庫存惹議 Burberry停止銷毀並捨棄真皮草

摘錄自2018年9月6日中央社報導

英國時尚品牌Burberry表示,未來將不再燒毀價值數百萬英鎊計的庫存商品,也不再於產品中使用真皮草。

路透社報導,Burberry在上個財政年度報告中,承認燒毀價值高達2860萬英鎊(約新台幣11.7億元)商品後,表示會致力於讓公司在社會和環境議題更負責任。

Burberry還表示,將追隨Versace、Gucci和道德時尚品牌先驅Stella McCartney的腳步,從系列產品中去除真皮,如兔子,狐狸,貂、亞洲浣熊等的皮毛。Burberry新設計師里卡多提西(Riccardo Tisci)首發系列將不含任何皮草。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

福島災民立場不一的現況

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

幼獸淪名流自拍炫富工具後棄養

摘錄自2018年11月15日公視報導

法國警方19號在香榭大道攔下一輛名車藍寶堅尼,發現裡面有一隻不到兩個月大的獅子寶寶,動保團體表示有越來越多人,會買一隻幼獸自拍炫富,然後棄養,形成歪風。

動保團體「300萬之友」主席胡庭說:「問題就出在馬戲團,他們可以利用野生動物(表演),法國馬戲團大約有1500隻野生動物,可怕的是這些馬戲團有繁殖的權利。」

胡庭認為,買幼獸自拍並上傳這群媒體的歪風,是來自波斯灣阿拉伯國家,這些小動物長大就會被丟棄;他們在約旦撿到三、四十隻被丟棄的小獅子,但更令人擔心的是,現在法國似乎也開始流行,過去半年,他們陸續撿到四隻獅子幼獸。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

精美圖文講解Java AQS 共享式獲取同步狀態以及Semaphore的應用

| 好看請贊,養成習慣

  • 你有一個思想,我有一個思想,我們交換后,一個人就有兩個思想

  • If you can NOT explain it simply, you do NOT understand it well enough

現陸續將Demo代碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star

看到本期內容這麼少,是不是心動了呢?

前言

上一篇萬字長文 Java AQS隊列同步器以及ReentrantLock的應用 為我們讀 JUC 源碼以及其設計思想做了足夠多的鋪墊,接下來的內容我將重點說明差異化,如果有些童鞋不是能很好的理解文中的一些內容,強烈建議回看上一篇文章,搞懂基礎內容,接下來的閱讀真會輕鬆加愉快

AQS 中我們介紹了獨佔式獲取同步狀態的多種情形:

  • 獨佔式獲取鎖
  • 可響應中斷的獨佔式獲取鎖
  • 有超時限制的獨佔式獲取鎖

AQS 提供的模版方法裏面還差共享式獲取同步狀態沒有介紹,所以我們今天來揭開這個看似神秘的面紗

AQS 中的共享式獲取同步狀態

獨佔式是你中沒我,我中沒你的的一種互斥形式,共享式顯然就不是這樣了,所以他們的唯一區別就是:

同一時刻能否有多個線程同時獲取到同步狀態

簡單來說,就是這樣滴:

我們知道同步狀態 state 是維護在 AQS 中的,拋開可重入鎖的概念,我在上篇文章中也提到了,獨佔式和共享式控制同步狀態 state 的區別僅僅是這樣:

所以說想了解 AQS 的 xxxShared 的模版方法,只需要知道它是怎麼控制 state 的就好了

AQS共享式獲取同步狀態源碼分析

為了幫助大家更好的回憶內容,我將上一篇文章的兩個關鍵內容粘貼在此處,幫助大家快速回憶,關於共享式,大家只需要關注【騷紫色】就可以了

自定義同步器需要重寫的方法

AQS 提供的模版方法

故事就從這裏說起吧 (你會發現和獨佔式驚人的相似),關鍵代碼都加了註釋

    public final void acquireShared(int arg) {
      	// 同樣調用自定義同步器需要重寫的方法,非阻塞式的嘗試獲取同步狀態,如果結果小於零,則獲取同步狀態失敗
        if (tryAcquireShared(arg) < 0)
          	// 調用 AQS 提供的模版方法,進入等待隊列
            doAcquireShared(arg);
    }

進入 doAcquireShared 方法:

    private void doAcquireShared(int arg) {
      	// 創建共享節點「SHARED」,加到等待隊列中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
          	// 進入“自旋”,這裏並不是純粹意義上的死循環,在獨佔式已經說明過
            for (;;) {
              	// 同樣嘗試獲取當前節點的前驅節點
                final Node p = node.predecessor();
              	// 如果前驅節點為頭節點,嘗試再次獲取同步狀態
                if (p == head) {
                  	// 在此以非阻塞式獲取同步狀態
                    int r = tryAcquireShared(arg);
                  	// 如果返回結果大於等於零,才能跳出外層循環返回
                    if (r >= 0) {
                      	// 這裡是和獨佔式的區別
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

上面代碼第 18 行我們提到和獨佔式獲取同步狀態的區別,貼心的給大家一個更直觀的對比:

差別只在這裏,所以我們就來看看 setHeadAndPropagate(node, r) 到底幹了什麼,我之前說過 JDK 源碼中的方法命名絕大多數還是非常直觀的,該方法直譯過來就是 【設置頭並且傳播/繁衍】。獨佔式只是設置了頭,共享式除了設置頭還多了一個傳播,你的疑問應該已經來了:

啥是傳播,為什麼會有傳播這個設置呢?

想了解這個問題,你需要先知道非阻塞共享式獲取同步狀態返回值的含義:

這裏說的傳播其實說的是 propagate > 0 的情況,道理也很簡單,當前線程獲取同步狀態成功了,還有剩餘的同步狀態可用於其他線程獲取,那就要通知在等待隊列的線程,讓他們嘗試獲取剩餘的同步狀態

如果要讓等待隊列中的線程獲取到通知,需要線程調用 release 方法實現的。接下來,我們走近 setHeadAndPropagate 一探究竟,驗證一下

  // 入參,node: 當前節點
	// 入參,propagate:獲取同步狀態的結果值,即上面方法中的變量 r
	private void setHeadAndPropagate(Node node, int propagate) {
    		// 記錄舊的頭部節點,用於下面的check
        Node h = head; 
    		// 將當前節點設置為頭節點
        setHead(node);
        
    		// 通過 propagate 的值和 waitStatus 的值來判斷是否可以調用 doReleaseShared 方法
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
          	// 如果後繼節點為空或者後繼節點為共享類型,則進行喚醒後繼節點
    				// 這裏後繼節點為空意思是只剩下當前頭節點了,另外這裏的 s == null 也是判斷空指針的標準寫法
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

上面方法的大方向作用我們了解了,但是代碼中何時調用 doReleaseShared 的判斷邏輯還是挺讓人費解的,為什麼會有這麼一大堆的判斷,我們來逐個分析一下:

這裏的空判斷有點讓人頭大,我們先挑出來說明一下:

排除了其他判斷條件的干擾,接下來我們就專註分析 propagate 和 waitStatus 兩個判斷條件就可以了,這裏再將 waitStatus 的幾種狀態展示在這裏,幫助大家理解,【騷粉色】是我們一會要用到的:

propagate > 0

上面已經說過了,如果成立,直接短路後續判斷,然後根據 doReleaseShared 的判斷條件進行釋放

propagate > 0 不成立, h.waitStatus < 0 成立 (注意這裏的h是舊的頭節點)

什麼時候 h.waitStatus < 0 呢?拋開 CONDITION 的使用,只剩下 SIGNAL 和 PROPAGATE,想知道這個答案,需要提前看一下 doReleaseShared() 方法了:

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                  	// CAS 將頭節點的狀態設置為0                
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 設置成功后才能跳出循環喚醒頭節點的下一個節點
                  	unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         // 將頭節點狀態CAS設置成 PROPAGATE 狀態
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

doReleaseShared() 方法中可以看出:

  • 如果讓 h.waitStatus < 0 成立,只能將其設置成 PROPAGATE = -3 的情況,設置成功的前提是 h 頭節點 expected 的狀態是 0;

  • 如果 h.waitStatus = 0,是上述代碼第 8 行 CAS 設置成功,然後喚醒等待中的線程

所以猜測,當前線程執行到 h.waitStatus < 0 的判斷前,有另外一個線程剛好執行了 doReleaseShared() 方法,將 waitStatus 又設置成PROPAGATE = -3

這個理解有點繞,我們還是來畫個圖理解一下吧:

可能有同學還是不太能理解這麼寫的道理,我們一直說 propagate <> = 0 的情況,propagate = 0 代表的是當時/當時/當時 嘗試獲取同步狀態沒成功,但是之後可能又有共享狀態被釋放了,所以上面的邏輯是以防這種萬一,你懂的,嚴謹的併發就是要防止一切萬一,現在結合這個情景再來理解上面的判斷你是否豁然開朗了呢?

繼續向下看,

前序條件不成立,(h = head) == null || h.waitStatus < 0 注意這裏的h是新的頭節點)

有了上面鋪墊,這個就直接畫個圖就更好理解啦,其實就是沒有那麼巧有另外一個線程摻合了

相信到這裏你應該理解共享式獲取同步狀態的全部過程了吧,至於非阻塞共享式獲取同步狀態帶有超時時間獲取同步狀態,結合本文講的 setHeadAndPropagate 邏輯和獨佔式獲取同步狀態的實現過程過程來看,真是一毛一樣,這裏就不再累述了,趕緊打開你的 IDE 去驗證一下吧

我們分析了AQS 的模版方法,還一直沒說 tryAcquireShared(arg) 這個方法是如何被重寫的,想要了解這個,我們就來看一看共享式獲取同步狀態的經典應用 Semaphore

Semaphore 的應用及源碼分析

Semaphore 概念

Semaphore 中文多翻譯為 【信號量】,我還特意查了一下劍橋辭典的英文解釋:

其實就是信號標誌(two flags),比如紅綠燈,每個交通燈產生兩種不同行為

  • Flag1-紅燈:停車
  • Flag2-綠燈:行車

在 Semaphore 裏面,什麼時候是紅燈,什麼時候是綠燈,其實就是靠 tryAcquireShared(arg) 的結果來表示的

  • 獲取不到共享狀態,即為紅燈
  • 獲取到共享狀態,即為綠燈

所以我們走近 Semaphore ,來看看它到底是怎麼應用 AQS 的,又是怎樣重寫 tryAcquireShared(arg) 方法的

Semaphore 源碼分析

先看一下類結構

看到這裏你是否有點跌眼鏡,和 ReentrantLock 相似的可怕吧,如果你有些陌生,再次強烈建議你回看上一篇文章 Java AQS隊列同步器以及ReentrantLock的應用 ,這裏直接提速對比看公平和非公平兩種重寫的 tryAcquireShared(arg) 方法,沒有意外,公平與否,就是判斷是否有前驅節點

方法內部只是計算 state 的剩餘值,那 state 的初始值是多少怎麼設置呢?當然也就是構造方法了:

		public Semaphore(int permits) {
      	// 默認仍是非公平的同步器,至於為什麼默認是非公平的,在上一篇文章中也特意說明過
        sync = new NonfairSync(permits);
    }
    
    NonfairSync(int permits) {
    		super(permits);
    }

super 方法,就會將初始值給到 AQS 中的 state

也許你發現了,當我們把 permits 設置為1 的時候,不就是 ReentrantLock 的互斥鎖了嘛,說的一點也沒錯,我們用 Semaphore 也能實現基本互斥鎖的效果


static int count;
//初始化信號量
static final Semaphore s 
    = new Semaphore(1);
//用信號量保證互斥    
static void addOne() {
  s.acquire();
  try {
    count+=1;
  } finally {
    s.release();
  }
}

But(英文聽力中的重點),Semaphore 肯定不是為這種特例存在的,它是共享式獲取同步狀態的一種實現。如果使用信號量,我們通常會將 permits 設置成大於1的值,不知道你是否還記得我曾在 為什麼要使用線程池? 一文中說到的池化概念,在同一時刻,允許多個線程使用連接池,每個連接被釋放之前,不允許其他線程使用。所以說 Semaphore 可以允許多個線程訪問一個臨界區,最終很好的做到一個限流/限流/限流 的作用

雖然 Semaphore 能很好的提供限流作用,說實話,Semaphore 的限流作用比較單一,我在實際工作中使用 Semaphore 並不是很多,如果真的要用高性能限流器,Guava RateLimiter 是一個非常不錯的選擇,我們後面會做分析,有興趣的可以提前了解一下

關於 Semaphore 源碼,就這麼三下五除二的結束了

總結

不知你有沒有感覺到,我們的節奏明顯加快了,好多原來分散的點在被瘋狂的串聯起來,如果按照這個方式來閱讀 JUC 源碼,相信你也不會一頭扎進去迷失方向,然後沮喪的退出 JUC 吧,然後面試背誦答案,然後忘記,然後再背誦?

跟上節奏,關於共享式獲取同步狀態,Semaphore 只不過是非常經典的應用,ReadWriteLock 和 CountDownLatch 日常應用還是非常廣泛的,我們接下來就陸續聊聊它們吧

靈魂追問

  1. Semaphore 的 permits 設置成1 “等同於” 簡單的互斥鎖實現,那它和 ReentrantLock 的區別還是挺大的,都有哪些區別呢?
  2. 你在項目中是如何使用 Semaphore 的呢?

參考

  1. Java 併發實戰
  2. Java 併發編程的藝術
  3. https://blog.csdn.net/anlian523/article/details/106319294

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

HashSet擴容機制在時間和空間上的浪費,遠大於你的想象

一:背景

1. 講故事

自從這個純內存項目進了大客戶之後,搞得我現在對內存和CPU特別敏感,跑一點數據內存幾個G的上下,特別沒有安全感,總想用windbg抓幾個dump看看到底是哪一塊導致的,是我的代碼還是同事的代碼? 很多看過我博客的老朋友總是留言讓我出一套windbg的系列或者視頻,我也不會呀,沒辦法,人在江湖飄,遲早得挨上幾刀,逼着也得會幾個花架子,廢話不多說,這一篇就來看看 HashSet 是如何擴容的。

二:HashSet的擴容機制

1. 如何查看

了解如何擴容,最好的辦法就是翻看HashSet底層源碼,最粗暴的入口點就是 HashSet.Add 方法。

從圖中可以看到最後的初始化是用 Initialize 的,而且裏面有這麼一句神奇的代碼: int prime = HashHelpers.GetPrime(capacity);,從字面意思看是獲取一個質數,哈哈,有點意思,什麼叫質數? 簡單說就是只能被 1 和 自身 整除的數就叫做質數,那好奇心就來了,一起看看質數是怎麼算的吧! 再次截圖。

從圖中看,HashSet底層為了加速默認定義好了 72 個質數,最大的一個質數是 719w,換句話就是說當元素個數大於 719w 的時候,就只能使用 IsPrime 方法動態計算質數,如下代碼:


public static bool IsPrime(int candidate)
{
	if ((candidate & 1) != 0)
	{
		int num = (int)Math.Sqrt(candidate);
		for (int i = 3; i <= num; i += 2)
		{
			if (candidate % i == 0)
			{
				return false;
			}
		}
		return true;
	}
	return candidate == 2;
}

看完了整個流程,我想你應該明白了,當你第一次Add的時候,默認的空間佔用是 72 個預定義中最小的一個質數 3,看過我之前文章的朋友知道List的默認大小是4,後面就是簡單粗暴的 * 2 處理,如下代碼。


private void EnsureCapacity(int min)
{
	if (_items.Length < min)
	{
		int num = (_items.Length == 0) ? 4 : (_items.Length * 2);
	}
}

2. HashSet 二次擴容探究

當HashSet的個數達到3之後,很顯然要進行二次擴容,這一點不像List用一個 EnsureCapacity 方法搞定就可以了,然後細看一下怎麼擴容。


public static int ExpandPrime(int oldSize)
{
	int num = 2 * oldSize;
	if ((uint)num > 2146435069u && 2146435069 > oldSize)
	{
		return 2146435069;
	}
	return GetPrime(num);
}

從圖中可以看到,最後的擴容是在 ExpandPrime 方法中完成的,流程就是先 * 2, 再取最接近上限的一個質數,也就是 7 ,然後將 7 作為 HashSet 新的Size,如果你非要看演示,我就寫一小段代碼證明一下吧,如下圖:

2. 您嗅出風險了嗎?

<1> 時間上的風險

為了方便演示,我把 72 個預定義的最後幾個質數显示出來。


public static readonly int[] primes = new int[72]
{
	2009191,
	2411033,
	2893249,
	3471899,
	4166287,
	4999559,
	5999471,
	7199369
};

也就是說,當HashSet的元素個數為 2893249 的時候觸發擴容變成了 2893249 * 2 => 5786498 最接近的一個質數為:5999471,也就是 289w 暴增到了 599w,一下子就是 599w -289w = 310w 的空間虛占,這可是增加了兩倍多哦,嚇人不? 下面寫個代碼驗證下。


        static void Main(string[] args)
        {
            var hashSet = new HashSet<int>(Enumerable.Range(0, 2893249));

            hashSet.Add(int.MaxValue);

            Console.Read();
        }

0:000> !clrstack -l

000000B8F4DBE500 00007ffaf00132ae ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 16]
    LOCALS:
        0x000000B8F4DBE538 = 0x0000020e0b8fcc08
0:000> !DumpObj /d 0000020e0b8fcc08
Name:        System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
Size:        64(0x40) bytes
File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0-preview.5.20278.1\System.Collections.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffaf0096d10  4000017        8       System.Int32[]  0 instance 0000020e2025e9f8 _buckets
00007ffaf00f7ad0  4000018       10 ...ivate.CoreLib]][]  0 instance 0000020e2bea1020 _slots
00007ffaeffdf828  4000019       28         System.Int32  1 instance          2893250 _count
0:000> !DumpObj /d 0000020e2025e9f8
Name:        System.Int32[]
Size:        23997908(0x16e2dd4) bytes
Array:       Rank 1, Number of elements 5999471, Type Int32 (Print Array)
Fields:
None


而且最重要的是,這裡是一次性擴容的,而非像redis中實現的那樣漸進式擴容,時間開銷也是大家值得注意的。

<2> 空間上的風險

這個有什麼風險呢?可以看一下:289w 和 599w 兩個HashSet的佔用空間大小,這也是我最敏感的。


        static void Main(string[] args)
        {
            var hashSet1 = new HashSet<int>(Enumerable.Range(0, 2893249));

            var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
            hashSet2.Add(int.MaxValue);

            Console.Read();
        }

0:000> !clrstack -l
OS Thread Id: 0x4a44 (0)
000000B1B4FEE460 00007ffaf00032ea ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 18]
    LOCALS:
        0x000000B1B4FEE4B8 = 0x000001d13363cc08
        0x000000B1B4FEE4B0 = 0x000001d13363d648

0:000> !objsize 0x000001d13363cc08
sizeof(000001D13363CC08) = 46292104 (0x2c25c88) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])
0:000> !objsize 0x000001d13363d648
sizeof(000001D13363D648) = 95991656 (0x5b8b768) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])

可以看到, hashSet1的佔用: 46292104 / 1024 / 1024 = 44.1M, hashSet2 的佔用 : 95991656 / 1024 / 1024 = 91.5M,一下子就浪費了: 91.5 - 44.1 = 47.4M

如果你真以為僅僅浪費了 47.4M 的話,那你就大錯特錯了,不要忘了底層在擴容的時候,使用新的 size 覆蓋了老的 size,而這個 老的 size 集合在GC還沒有回收的時候會一直佔用堆上空間的,這個能聽得懂嗎? 如下圖:

要驗證的話可以用 windbg 去託管堆上抓一下 Slot[] m_slotsint[] m_buckets 兩個數組,我把代碼修改如下:


    static void Main(string[] args)
    {
        var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
        hashSet2.Add(int.MaxValue);
        Console.Read();
    }


0:011> !dumpheap -stat
00007ffaf84f7ad0        3    123455868 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

這裏就拿 Slot[] 說事,從上面代碼可以看到,託管堆上有三個 Slot[] 數組,這就有意思了,怎麼有三個哈,是不是有點懵逼,沒關係,我們將三個 Slot[] 的地址找出來,一個一個看。


0:011> !DumpHeap /d -mt 00007ffaf84f7ad0
         Address               MT     Size
0000016c91308048 00007ffaf84f7ad0 16743180     
0000016c928524b0 00007ffaf84f7ad0 34719012     
0000016ce9e61020 00007ffaf84f7ad0 71993676  

0:011> !gcroot 0000016c91308048
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016c928524b0
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016ce9e61020
Thread 2b0c:
    0000006AFAB7E5F0 00007FFAF84132AE ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 15]
        rbp-18: 0000006afab7e618
            ->  0000016C8000CC08 System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
            ->  0000016CE9E61020 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

從上面可以看到,我通過 gcroot 去找這三個地址的引用根,有兩個是沒有的,最後一個有的自然就是新的 599w 的size,對不對,接下來用 !do 打出這三個地址的值。


0:011> !do 0000016c91308048
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        16743180(0xff7b0c) bytes
Array:       Rank 1, Number of elements 1395263, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016c928524b0
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        34719012(0x211c524) bytes
Array:       Rank 1, Number of elements 2893249, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016ce9e61020
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        71993676(0x44a894c) bytes
Array:       Rank 1, Number of elements 5999471, Type VALUETYPE (Print Array)
Fields:
None

從上面的 Rank 1, Number of elements 信息中可以看到,原來託管堆不僅有擴容前的Size :2893249,還有更前一次的擴容Size: 1395263,所以按這種情況算: 託管堆上的總大小近似為: 23.7M + 47.4M + 91.5M = 162.6M,我去,不簡單把。。。 也就是說:託管堆上有 162.6 - 91.5 =71.1M 的未回收垃圾 剛才的 47.4M 的空間虛佔用,總浪費為:118.5M,但願我沒有算錯。。。

3. 有解決方案嗎?

在List中大家可以通過 Capacity 去控制List的Size,但是很遺憾,在 HashSet 中並沒有類似的解決方案,只有一個很笨拙的裁剪方法: TrimExcess,用於將當前Size擴展到最接近的 質數 值, 如下代碼所示:


public void TrimExcess()
{
	int prime = HashHelpers.GetPrime(m_count);
	Slot[] array = new Slot[prime];
	int[] array2 = new int[prime];
	int num = 0;
	for (int i = 0; i < m_lastIndex; i++)
	{
		if (m_slots[i].hashCode >= 0)
		{
			array[num] = m_slots[i];
			int num2 = array[num].hashCode % prime;
			array[num].next = array2[num2] - 1;
			array2[num2] = num + 1;
			num++;
		}
	}
}

應用到本案例就是將 289w 限制到 347w,仍然有 58w的空間佔用。 如下圖:

三: 總結

HashSet的時間和空間上虛占遠比你想象的大很多,而且實占也不小,因為底層用到了雙數組 m_slotsm_buckets,每個Slot還有三個元素: struct Slot { int hashCode;internal int next;internal T value; },所以了解完原理之後謹慎着用吧。

如您有更多問題與我互動,掃描下方進來吧~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案