蘇格蘭「全面禁止塑膠棉花棒」 環保人士:前所未有的勝利

摘錄自2019年10月12日自由時報報導

綜合外媒報導,英國民眾每年估計使用50億支塑膠吸管、3億支塑膠攪拌棒及20億枝塑膠棉花棒,其中,塑膠棉花棒多數被沖進馬桶,藉下水道流入海洋,對海洋生態及野生動物造成難以估計的危害;英國原訂從2020年4月起全面禁止販售塑膠吸管、塑膠攪拌棒及塑膠棉花棒,而蘇格蘭議會率先在9月通過2018年提案的「全面禁止塑膠棉花棒生產與銷售」法案,目前此禁令已正式生效,成為英國宣布2020年4月起全面「禁塑」後,率先「全面禁止塑膠棉花棒」生產與銷售的區域。

過去25年間,蘇格蘭環保人士總共在當地海灘上「撿起」超過15萬根塑膠棉花棒,他們稱這是「海洋與野生動植物的勝利」,也期待蘇格蘭政府能採取更多措施,制止這場「塑膠潮」,也希望其他國家能跟進。

當地海洋保護協會的蓋梅爾(Catherine Gemmel)表示,這項禁令生效對「海洋和野生動植物」而言,是「前所未有」的勝利,期待蘇格蘭政府能繼續採取其他行動。

 

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

環團包圍《BBC》總部 要求以「二戰狀態」報導氣候變遷

摘錄自2019年10月11日自由時報報導

關注氣候變遷議題的環保團體「反抗滅絕」(Extinction Rebellion)11包圍英國《BBC》倫敦總部的消息,呼籲該公共媒體將氣候變遷視為如「二戰」般的緊急狀態報導,並向社會大眾說實話。

環團發言人麥卡錫(Donnachadh McCarthy)表示:「我們聚集於此是因為《BBC》拒絕宣布『氣候緊急狀態』,且他們不斷將『高碳排放』的生活正常化、合理化,像他們的節目《頂級跑車秀》(Top Gear)就是一例。」

《BBC》發言人則表示她目前無法評論總部的安全問題,她拒絕為《BBC》的報導力度進行回覆。

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

平整化底盤式樣GC8 強大的540ps馬力

這輛前期型的Impreza GC8鯊魚,是在英國的速霸陸改裝車群組中,獲得相當高評價的ZEN‏ Performerzen自家工廠賽車,沒有誇張的寬車體,也沒有將懸吊形式變更,在UK單圈計時賽事中依舊取得冠軍寶座,是一輛單純破紀錄所打造的賽車。

這輛車是由ZEN‏ Performerzen所打造,專門為了在UK單圈計時賽中破紀錄使用的純賽車,當初打造時的考量,就是僅有純粹的快而已。

心臟部分使用上EJ25的中缸作為基礎,使用上EJ25後期的鎢鋼曲軸,搭配上WISECO鍛造活塞與H斷面連桿,將排氣量設定在少見的2.3升化。黑豆(上半座)部分更換上了ZEN自家製的高角度凸輪軸與汽門套件,徹底強化高轉速區域的動力輸出。

中缸的部分採用EJ25的規格,ZEN打造時選擇了EJ25的鎢鋼曲軸,搭配WISECO鍛造活塞與H斷面連桿,將排氣量設定在相當少見的2300cc規格,並且搭配自家製的高角度凸輪軸與強化汽門系列產品,徹底強化高轉速時的性能出力。

透過自製的芭蕉將蓋瑞特的GT40R渦輪本體移動至水箱架後方,這具渦輪機的大小約是在HKS GT3540與HKS T04Z之間的大小,AUTRONIC SM4全取代賽車用可程式電腦的貫穿下,現狀已經可以輸出540ps的最大馬力。

在自製的排氣芭蕉最上的渦輪機部分,安裝上了蓋瑞特的GT40R渦輪機,出風量約是在HKS GT3540與HKS T04Z之間的大小,AUTRONIC SM4全取代賽車用可程式電腦的貫穿下,現狀已經可以輸出540ps的最大馬力。

避震器為ZEN自家在賽道磨練出的EXE-TC RS04 外掛氣瓶多way版本,由於GC8的車體重量並不重的關係,制動系統選擇上對向四活塞的STOPTECH卡鉗,搭配並不大的330mm浮動碟盤,輪胎則是大家相當熟悉的Toyo R888。

斯巴達式密密麻麻的防滾季充斥的車室。儀錶已經被多功能的AMI液晶儀錶取代,將所有車輛資訊整合其中,讓駕駛可以更容易了解目前車況,並且為了降低反光的影響,ZEN也順勢將中控台施以麂皮包覆,變速箱則是並未選擇重量較高的六速系列,而是少了許多機構的PPG五速序列式變速箱,在齒輪強度、換檔速度與重量三元素中取得完美平衡,由於是單圈式樣,EXEDY碳纖維離合器需要預熱的特性自然也不影響。

進氣岐管用上EJ20早期的產品,因為與引擎本體的口徑與位置上的差異,因此訂做這個專用的轉接墊片,透過加工再讓岐管內管徑更大,這也是很多歐洲跑拉力賽事鯊魚常見的改法。

冷卻系統是水與酒精採50比50的混合比例,透過岐管上向燃燒室內噴射,降低燃燒室的溫度,大幅防止爆震的產生,也就是常見的水噴射系統。

雖然外觀上的空力套件並沒有太多著墨,但底盤的部分則是做了相當完整的設定,從車頭下方開始一路到車尾全部都施以包覆,完成了平整化底盤的設定,當然其中要將零件塞到裡面是下了不少苦功。

很多愛GC的車主來說對於這種98前的中控台設計是情有獨鍾。為了預防反光的問題,ZEN包上了麂皮材質解決這情況,方向盤前方的AMI儀錶則是取代了原廠設定,並且將大量的車輛資訊整合在裡面。

密密麻麻的防滾架與車體施以焊接強化,並且與車身施以同色的塗裝烤漆。

看起來就像一台原廠的GC8鯊魚,但動力與底盤的提昇都已經是滿配了,雖然細節上沒有相當的精緻,但確實是擁有相當的水準。

新聞來源:https://www.carnews.com/article/info/628b596e-dbac-11ea-ad31-42010af00004/

【其他文章推薦】

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案

中德合作綱要簽署 電動車受惠

10月10日,中國國務院總理李克強在柏林同德國總理安格拉•默克爾共同主持第三輪中德政府磋商。雙方決定發表《中德合作行動綱要:共塑創新》(下文簡稱《綱要》)。《綱要》共包含86項條文,其中8項提及汽車工業,新能源汽車標準制定與應用推廣成為重中之重。   雙方同意,要繼續加大政府對電動汽車研發、市場開發、基礎設施建設等領域的扶持。雙方商定,給予企業平等享受電動汽車國家扶持和優惠的待遇,並在國家規章、標準制訂中加強協調。雙方將繼續深化中德在電動汽車領域的標準合作。雙方應在充電基建領域就擴建策略和經營模式等議題加強對話。   《綱要》提及,中德兩國應深化電動汽車示範專案和試點城市的交流與合作。在已建立的城市合作框架下,鼓勵中德其他具備條件的城市積極參與。雙方將在電動汽車戰略平臺框架下,探討共建充電基礎設施和電動汽車與智慧電網互通示範項目。它還提出由中國國家發展改革委和德國聯邦環境、自然保護、建築和核安全部牽頭,雙方將繼續深化在電動汽車電池回收利用領域的合作。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

電動機車 E-bike 上路 試營運前 30 分鐘免費

  除了有腳踏車 U-bike 可租借,現在連電動機車都能租借得到了。台灣城市動力公司開發自動化公共 E-bike 租借系統,試營運期間提供前 30 分鐘免費優惠,民眾前往租借還可參加多項大獎的抽獎活動。   台灣城市動力公司建構的E-bike租借系統,上周率先在新北市板橋區縣民大道與漢生東路交口的車站停車廣場供國人自由租借,民眾透過悠遊卡註冊後就可選車、取車、還車、付費,方便性與租借U-bike無異。開放試租首日在一個站點就吸引數百民眾註冊借車,該公司相信未來E-bike站點持續擴增後,勢必讓政府推動的機車電動化進度呈跳躍式進展。   台灣城市動力公司總經理洪國修表示,環保減碳是政府重要的施政項目,為提高民眾騎乘電動二輪車之意願,環保署除提供購買電動車 2 萬 4 千元的補助,台灣城市動力公司也配合這項政策,從便民方向建置全球首創的電池交換站系統(BES),民眾在 30 秒即可完成電池更換,免除電池充電過久或保固維護的不便。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案

其他動力電池車望塵莫及 鋁氣電池續航達 1,600 公里

不讓美國特斯拉電動車專美於前,美鋁公司(Alcoa)宣稱已完成由以色列飛能(Phinergy)研發成功的鋁氣電池(Al–air batteries),並且達成航距超過 1,600 公里的測試。這項研發讓相繼投入電動車領域的德國寶馬、日本豐田等相形失色。   美鋁和以色列飛能最近在美國蒙特利爾維倫紐夫賽車場,測試了以鋁空氣電池為能源的車輛,驚人的是續航里程竟上升到 994 英里。在金屬空氣電池中,鋁空氣電池就是其中之一,如今這 1,600 千公里的續航力已遠超過包括特斯拉 Model S 在內等現有各類動力電池車。其他插電式混合動力車、增程式電動車(續航在 500 公里左右)或者豐天、現代所謂的氫燃料電池車(續航超過 500-600 公里)等,也同樣望塵莫及。   飛能公司說,這一組電池重僅約 100 公斤,裝有 50 塊電池板,平均 1 塊鋁空氣電池板就可驅動車輛行駛約 32 公里。但鋁空氣電池的放電過程會導致陽極腐蝕產生氫,導致陽極材料過度消耗,並增加電池內部損耗,使其商業化進程放緩。   然而,鋁空氣電池維護非常方便。按現有技術方案,鋁空氣電池是作為鋰電池的補充電源,鋰電池能量耗盡後,鋁空氣電池才接手,所以使用者無需進行充電,每 1-2 個月注入自來水維持其化學反應,每年也只需讓技術人員進行一次保養即可,電池壽命甚至可達 20 至 30 年。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案

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

東元強攻菲電動車市場 擬明年第二季投產

東元海外新布局持續有所斬獲,公司年初獲菲律賓馬尼拉最大車隊千台電動吉普巴士訂單,首批 50 輛將於年底前陸續出貨。東元表示,菲律賓當地擁有銀行的前三大機車經銷商今年正式向公司提出合作,並期盼公司能在當地設廠就近供應,而內部評估對方所提之方案可行性很高,預期最快 2015 年第二季就會在當地投產,未來該廠將同時生產特種電動三輪車、電動吉普巴士等。   東元集團看好電動車市場大有可為,但無意與美國 Tesla、德國 BMW、日本豐田、本田及日產等國際大廠競逐純電動車市場商機,利用本身擅長的馬達及電控系統強項發展特種電動車,作為進軍全球電動車市場的試金石,目前拿下雲林西螺果菜市場 8 百輛電動搬運車,約 2 億元標案。   此外,東元在土耳其市場也有所進展;公司指出,預期土耳其市場 的 2014 年可帶進營收貢獻約 9 千萬元,2015 年布局效益將更進一步顯現,並以挑戰倍增為銷售目標;同時,土耳其亦可作為業務拓展的跳板,有助於進一步把旗下機電、馬達、電控與商用空調等產品,銷售至歐洲、中東與非洲等市場。  

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

日 TDK 開發出電動車無線充電技術

日本 TDK 開發出了用於電動車(EV)及插電式混合動力車(PHV)的非接觸供電系統。當電動車或插電式混合動力車停在使用了該系統的停車場時,無需使用有線電纜即可充電。若在公路上大規模鋪設該系統,甚至可能讓電動車等邊行駛邊充電,預計在 2018 年上路應用。   TDK 今年 4 月與美國麻州的創業企業 WiTricity 合作,獲得有關電動汽車無線供電的技術經驗。基於 WiTricity 的技術,並利用 TDK 擅長的磁線圈技術成功開發了無線供充電系統。系統由無線供電用送電線圈和受電線圈組成,線圈之間的距離即使超過 10 厘米也能送電。TDK 計劃從 2015 年上半年開始向汽車廠商等提供樣品。   此外,TDK 還針對電動車的行駛途中供電,開始試驗。在周長為 30 米的試驗場地路面下方,每隔 5 米鋪設 6 個送電線圈,讓試製車在上方行駛。據稱,目前已能以 5 公里時速行駛 120 公里。  

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

小班同學學習經歷分享(一)遊戲程序員成長札記

作者信息

昵稱:目及遠方

課程設計 HumanFramework:

https://github.com/cyclons/HumanFramewo

正文

大四畢業,心血來潮,閑余之際,撰文留念。

萌芽

遊戲程序員,把這個分成兩塊的話就是,遊戲,程序。

這兩个中,只有一個,遊戲,在我很小的時候就開始接觸,在那個視电子遊戲如电子海洛因,父母抵制到要送孩子去網癮治療所的年代,三年級的我就已經在玩ps2了,周圍的同學還在玩紅警qq大亂斗冒險島飛車的時候,我已經玩高達戰神龍珠古墓麗影,最終導致和周圍同學沒有共同話題。

隨後,按順序入手了nds,psp,xbox360,ps4,可以說從小到大,遊戲沒有停過。

如果說這條職業路上我有什麼超前之處,就是玩遊戲玩得多。

起步

那什麼時候開始想做遊戲了呢?要等到大學。

整個大一的我沉浸在社團幹事與學習中,完全沒有接觸過遊戲編程,學了個譚浩強的c語言,寒假里把下學期的高數學了一遍,然後發現課堂上除了能裝個逼好像也沒什麼特別大用。

直到下半學期的一天夜裡,我在床上思考,以後究竟干什麼比較好,突然一個念頭冒出來,要不去做遊戲吧!這個想法一冒出來,我猛地從床上坐了起來,彷彿一股能量貫穿了全身一般,於是我下定決心買一台電腦,開始我的程序之路。

暑假的時候,搜了好久,終於找到一個感覺靠譜的教程,那就是SIKI,現在已經是遊戲網課的巨頭了,然而當時僅僅只是一個維持了一個小小的公眾號而已,唯一收費的只有一個A計劃,終身費只要400塊。當時選擇的原因有很多,其中最主要的就是大量的免費課程,一個個案例都是自己想要實現的遊戲,總感覺每實現一個,就離成為遊戲製作人更近一步(當時沒有遊戲職業概念)。

成長

剛開始學的階段,可以說我就是個沙雕,半吊子中的半吊子,打字速度20字母/s,不懂語法,大小寫不分,對着視頻敲代碼。(彎路)

後來發現儘管照着視頻做出來了,但仍然不熟練,於是逼迫自己,看視頻不寫代碼,寫代碼不看視頻,偶爾實在不知道怎麼做了,再回去看視頻里怎麼做,一個視頻要看兩三遍。(稍微正了一些)

為了提高自己的打字速度,把手機的鍵盤從9鍵變成26鍵。(有一定作用但不是正道,推薦https://www.keybr.com/)

那時候我還心血來潮,趕VR潮流,在學校里搞起了VR工作室還有VR社團,但剛開始不做技術,是賣硬件的,賣手機紙盒子VR給學生。後來發現潮流過了,硬件沒前途,隨後就想把工作室往技術上轉,當時心裏的想法是邊能學Unity又能搞起一番事業(too young,too simple)。

大二的寒假里,我馬不停蹄,不斷學習SIKI公眾號上的項目案例,每做完一個就信心爆棚,彷彿自己已經是個優秀的遊戲程序員了。之後還在社團里教大家如何做一個AR應用,Unity小遊戲等等。

在大二的暑假里,我還認識了一個朋友,一起做汽車VR噴漆,想通過這個賺錢,但最終失敗了。同時,我發現專業偏硬件,於是我轉專業到計算機學院,開始了第二個大二生活。

旅途

後來的日子里,我依然是一有時間就跟着教程學,但彷彿到了一定的瓶頸,感覺做遊戲不就是調調api,用用插件,什麼遊戲都能做出來啊,恰巧當時看到心動在搞獨立營活動,我就立即報名了,這也成為了我第一次的gamejam。

gamejam的感覺呢,怎麼說呢,就好像回家了一樣,裏面各個都是人才,說話又好聽,超喜歡這裏面的~。在活動里,能夠充分體會到周圍人對遊戲的熱愛,精妙的遊戲設計,驚艷的美術,牛逼的程序老哥,主辦方給我們學生還特別優待,給我們免費訂了两天的五星級酒店,還包早餐,可惜都沒怎麼好好享受到,两天可能就住了8個小時不到吧,但整個活動充滿樂趣,給我的第一次gamejam留下了非常棒的印象。

之後便開始積极參加各種jam活動,線上線下到現在快應該有10場,每一次都很有收穫,無論是認識了新的朋友,還是看到了非常驚艷的遊戲,每次都是一場充電之旅。

里程碑

改變我職業生涯的是一次比賽。

還記得隔壁工作室的老哥問我一句有個AR的比賽來不來參加,我說來。那次比賽是一次hackthon,恰巧有一個單項獎由網易贊助,而且專門設置的AR題材。對我來說,我不了解hackthon,所以就把它當作是一場gamejam,看着周圍一圈985的同學們,壓力山大。

那次我們做了一個AR版本的胡鬧廚房,現在回想起來那代碼寫得就是一坨屎,但遊戲運行非常順暢,沒有bug,從可玩性來看還是挺不錯的,但和以前看的優秀作品比差距還是太遠。聽到主辦方在選出十個演講隊伍中沒有我們的名字時,我們已經收拾好行李,開始回校了。然鵝,這時候主辦方說不要急着走,網易的獎還沒開,我們一路就火急火燎的趕了回去。

由於來得太晚,演講已經開始了一大半,我們幾個人就站在入口的地方聽演講,看着別人的項目,什麼機器學習,區塊鏈,智能小車,各個高大上的不行,彷彿改變未來的技術一樣,而且沒有一個是做遊戲的,我這時候意識到,是不是走錯場了?

等其他獎開完了,才等到網易的負責人上台,大概是這麼說的,“我們在兩支隊伍里徘徊,所以一直沒能下一個定論,但最終我們在完整度上考慮,最終決定把一等獎給9號隊。”

當時整個人都已經懵逼了,周圍隊友興奮的握着我的手,這時候感到一切的努力都是值得的。

獲獎是次要的,最主要的是一等獎附贈一個網易終面機會,作為項目主程,我成功通過了,拿到了實習offer。

這次事件是里程碑,告訴我在這條路上繼續走下去是值得的。

泥潭

網易實習生活非常豐富,由於是實習生還是在一個偏創新的部門,我和周圍的小夥伴們一起做了非常多好玩有趣的AR遊戲,回來的我也是信心爆棚。

我繼續不斷學習,做項目。但做着做着發現,項目都能跑,但是最終的成品要想改功能,牽一發而動全身,最後改着改着就變成了一坨屎,而那些神乎其神的插件,自己始終停留在會用不會做的階段。

那時候的我非常的慌張,加群,逛知乎,看教程。最後我找到了一本遊戲設計模式,看完之後才知道,原來代碼能這麼寫,好方便啊,這之後代碼又上升了一個階段。

轉眼又一年經過,大三末的我又開始找實習。我本以為我那項目滿滿,經歷豐富的簡歷,一投一個准,做個offer收割機不是問題,然而事實就是,我就是a piece of shit。

算法,數據結構,計算機組成原理,是面試的重中之重,而這裏面每一個都是我的弱點,筆試都通不過。做了幾套面試題之後,我意識到,自己的基礎太弱了。

我開始瘋狂看面經,牛客網,leetcode,uwa也看。最終的出來一個結論,原來我就是個小白。

人貴有自知,知道自己多弱是件好事,至少知道自己要補哪些。這時候就非常感謝恭弘=叶 恭弘大的遊戲程序員學習路線,在書籍的指導下我決定從0開始,從primer cpp開始,從頭重新練,隨着一個個的知識點梳理過去,自己的知識漏洞逐漸補全。

一邊惡補一邊找工作,此時的我就是任人宰割的羔羊,哪家公司要我就去哪裡,大不了過半年,我又是一條好漢。

沒想到,本以為已經涼涼了的騰訊來了電話,那就索性面下去吧,沒想到一路面到了底,拿到了實習offer。。

升華

這次的實習和之前就完全不是一個感覺,正規的大項目,專業的導師,完善的框架,專業的團隊。據說實習留用率低,感覺壓力山大,一邊做着業務,一邊把手邊該看的基礎書在看。

這次依然運氣可以,上岸了。

回校之後,我開始繼續看基礎部分,但發現學習的面越來越廣,尤其是遊戲這塊更是複雜,因此,我逐漸放緩,雖然我的目標是做遊戲,但具體最終是做哪個職位的研究依然不夠清晰,甚至中途還打起了轉行做策劃的念頭。

我設立了第一個目標,搞一個框架。為什麼是這個目標?原因大致如下:

  • 目前我做了很多遊戲,都是小項目,做大了,代碼就變成一坨屎,攪都攪不動。
  • 框架可以讓項目變得有結構,是職業必經之路。
  • 想要做大項目,一定要有框架

我搜索了很多現有的框架,首先就是學着用,其中就包括strangeIoC,還有MVC等。不得不說,StrangeIoC是新手勸退框架,那一堆東西理念對初級程序員來說就是一頭霧水,明明三行就能實現的東西,為什麼要8個類幾百行實現。

偶然發現了一個QFramework,github千星項目,還有文檔,於是我就開始搞QFramework。

又是一個機會,發現QFramework的作者涼大準備搞事,做一個小班,專心帶學生,12月分期,學生還帶優惠,我轉念一想,當年SIKI還是個小公眾號,現在A計劃永久能賣大幾千,這個車一定要上。

交錢上車后,跟着涼大學,一天兩篇,框架搭建和shader都有涉及。有一說一,雖然是日更的,但是我一般三四天一看,甚至一周一看,剛開始比較勤勞,看得多,有一段時間看着比較累,就斷了一大片。

這裏非常感謝涼大時不時會來私聊,問問學習情況,有沒有遇到什麼困難。我當然也心知肚明,聊完就去補文章了。

正是在這樣的一步步過坎之後,自己的框架意識也逐步建立,共有問題也逐步顯現,C#上欠缺的部分通過中毒篇專欄有了很大的彌補,更重要的是,在未來的路途上有了專業的指導,少走了非常多的彎路,這點真的非常重要。

不知不覺間,一年就過去了,我也幸運的交上了一份畢業設計,學習過程中幾次差點放棄,但看看文章之後覺得這個知識有必要掌握,就一直續到了現在。

本來這篇文章是涼大讓我談談這個框架學習之路,扯了太多自己的東西,這裏就詳細聊聊框架的學習心得:

  • 課程內容里,最核心的還是框架學習,是主菜,至於shader部分,其實只是一個補充,比如業務里要用到相關知識,和專業技美或圖形程序不同,屬於副菜。
  • 文章講框架部分寫得非常好,由淺入深,講解細緻,且代碼詳盡,每一個學習單元都可以做一個小實現。
  • 記得涼大在學習開始的時候提到,小班文章的目的是讓大家看文章就行,不用做筆記,不用真的動手,看就完事了,但這一套在我這邊的嘗試下不太行的通,簡單的或者熟悉的內容可以一遍過,但一些需要反覆理解的如IoC部分,只看文章會覺得迷迷糊糊的。古人有句話,“紙上得來終覺淺,絕知此事要躬行”,對於難理解的內容,一定要下手嘗試,即使簡單的內容,親自做過之後都會有不一樣的感覺,有時間一定要多練習,偷懶只會虧待自己。
  • 明白最終自己想要的是什麼。由於課程涉及方方面面,學員的學習程度也層次不齊,有時候碰到的內容不感興趣,不是自己主要目標的組成部分,那便可以選擇性的不太需要花精力去學習,但這類情況較少,如果平時較為空閑,建議都看,有益無害。
  • 課程不太適合小白,適合有一定項目經驗的同學。
  • 剛開始會充滿激情,但中間會由於各種事情被打斷,最好養成一個看文章的固定時間段,比如996晚上摸魚的時候看,或者在周末挑一個時間。
  • 學習框架的過程為:使用框架to看懂框架to能寫框架,各自為階段,越過會導致許多障礙,比如沒有用過消息中心很難寫出一個消息中心
  • 不用害怕學不會,只要方向正確,沒有完成不了的

涼大在小班上非常負責,可以說關心到了每一個成員,內容質量也非常有保證,每每我有“棄坑”的想法時,涼大都會來“善意的提醒”。而我遇到什麼問題時,都能夠得到專業的回答。

大學的前幾年實際上走了不少彎路,如果能夠早期遇到專業的老師來指點的話能少走很多。如果入職一段時間了,職業提升遇到瓶頸,尤其還是從事Unity行業的話,那非常推薦來小班,這裏交流活躍,同行眾多,總有老哥給你指條明路。

在一年結束后,我也最終實現了我的目標,實現了自己的框架——HumanFramework,在大佬眼中應該就是個小不點的存在,但即使這個框架不會成為流行,這個過程也使我對軟件設計的理解更上了一層樓。

退潮

最近各種事情算是告一段落,畢業也好,小班也好,工作也好,自己的學生時代也結束了,即將開啟工作時代,由於之前的幾年坎坷奮鬥,加上自己的身體不算強健,現在留下了點胃病,這幾個月里都在養生,時不時看看業內新聞之類的,最近越來越對shader相關的內容感興趣了,之後的主要平台也會變為Unreal,想想也挺有趣的。

在這一年中,學到了很多,尤其是技術分享的重要性,自己也會寫一些文章分享出來,包括HumanFramework的製作過程分享,歡迎來知乎關注我

最終祝願所有讀者在學習的同時身體健康,身體是革命的本錢,有好的身體才有力氣追求更美好的生活。

轉載請註明地址:liangxiegame.com

更多內容

QFramework 地址:https://github.com/liangxiegame/QFramework
QQ 交流群:623597263
涼鞋的主頁:https://liangxiegame.com/zhuanlan
關注公眾號:liangxiegame 獲取第一時間更新通知及更多的免費內容。

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

多線程高併發編程(12) — 阻塞算法實現ArrayBlockingQueue源碼分析

一.前言

  前文探究了非阻塞算法的實現ConcurrentLinkedQueue安全隊列,也說明了阻塞算法實現的兩種方式,使用一把鎖(出隊和入隊同一把鎖ArrayBlockingQueue)和兩把鎖(出隊和入隊各一把鎖LinkedBlockingQueue)來實現,今天來探究下ArrayBlockingQueue。

  ArrayBlockingQueue是一個阻塞隊列,底層使用數組結構實現,按照先進先出(FIFO)的原則對元素進行排序。

  ArrayBlockingQueue是一個線程安全的集合,通過ReentrantLock鎖來實現,在併發情況下可以保證數據的一致性。

  此外,ArrayBlockingQueue的容量是有限的,數組的大小在初始化時就固定了,不會隨着隊列元素的增加而出現擴容的情況,也就是說ArrayBlockingQueue是一個“有界緩存區”。

  從下圖可以看出,ArrayBlockingQueue是使用一個數組存儲元素的,當向隊列插入元素時,首先會插入到數組下標索引為6的位置,再有新元素進來時插入到索引為7的位置,依次類推,如果滿了就不會再插入。

  當元素出隊時,先移除索引為2的元素3,與入隊一樣,依次類推,移除索引3、4、5…上的元素。這也形成了“先進先出”。

 

二.源碼解析

  1. 構造方法

    public class ArrayBlockingQueue<E> extends AbstractQueue<E>
            implements BlockingQueue<E>, java.io.Serializable {
    
        //隊列實現:數組
        final Object[] items;
    
        //當讀取元素時數組的下標(下一個被取出元素的索引)
        int takeIndex;
    
        //添加元素時數組的下標 (下一個被添加元素的索引)
        int putIndex;
    
        //隊列中元素個數:
        int count;
    
        //可重入鎖:
        final ReentrantLock lock;
    
        //入隊操作時是否讓線程等待
        private final Condition notEmpty;
    
        //出隊操作時是否讓線程等待
        private final Condition notFull;
    
        /**
         * 初始化隊列容量構造:由於公平鎖會降低隊列的性能,因而使用非公平鎖(默認)。
         */
        public ArrayBlockingQueue(int capacity) {
            this(capacity, false);
        }
    
        //帶初始容量大小和公平鎖隊列(公平鎖通過ReentrantLock實現):
        public ArrayBlockingQueue(int capacity, boolean fair) {
            if (capacity <= 0)
                throw new IllegalArgumentException();
            this.items = new Object[capacity];
            lock = new ReentrantLock(fair);
            notEmpty = lock.newCondition();
            notFull =  lock.newCondition();
        }
    }
    •  在多線程中,默認不保證線程公平的訪問隊列;

    •  在ArrayBlockingQueue中為了保證數據的安全,使用了ReentrantLock鎖。由於鎖的引入,導致了線程之間的競爭。當有一個線程獲取到鎖時,其餘線程處於等待狀態。當鎖被釋放時,所有等待線程為奪鎖而競爭;

    • 鎖有公平鎖和非公平鎖:

      •  公平鎖:等待的線程在獲取鎖而競爭時,按照等待的先後順序FIFO進行獲取操作;公平鎖可以應用在比如併發下的日誌輸出隊列中,保證了日誌輸出的順序完整性;
        •  優點:等待鎖的線程不會餓死,和非公平鎖相比,在獲得鎖和保證鎖分配的均衡性差異較小;
        • 缺點:使用公平鎖的程序在多線程訪問時表現為很低的吞吐量(即速度很慢),等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖的大;公平鎖不能保證線程調度的公平性,因此,使用公平鎖的眾多線程中的一員可能獲得多倍的成功機會,這種情況發生在其他活動線程沒有被處理並且目前並未持有鎖時【ReentrantLock源碼對公平鎖的定義】;
           Note however, that fairness of locks does not guarantee
           fairness of thread scheduling. Thus, one of many threads using a
           fair lock may obtain it multiple times in succession while other
           active threads are not progressing and not currently holding the
           lock.
          •  上面這句話有重入鎖的概念,一個線程可以在已經獲取鎖的情況下再次進入獲取到鎖,不需要競爭;同時,如果一個線程獲取到了鎖,然後釋放,在其他線程來獲取之前再次是可以獲取到鎖的。
            A: Request Lock -> Release Lock -> Request Lock Again (Succeeds) 
                                                   B: Request Lock (Denied)... 
            -----------------------   Time   --------------------------------->
      •  非公平鎖:在獲取鎖時,無論是先等待還是后等待的線程,均有可能獲取到鎖。即根據搶佔機制,是隨機獲取鎖的,和公平鎖不一樣的是先來的不一定能獲取到鎖,有可能一直拿不到鎖,這樣會造成“飢餓”現象;
        • 優點:非公平鎖性能高於公平鎖性能。首先,在恢復一個被掛起的線程與該線程真正運行之間存在着嚴重的延遲,而且,非公平鎖更能充分的利用CPU的時間片,盡量減少CPU空閑的狀態時間;即可以減少喚起線程的開銷,整體的吞吐效率高,因為線程有幾率不阻塞直接獲取到鎖,CPU不必喚醒其他所有線程;
        • 缺點:處於等待隊列中的線程可能會餓死或者等很久才會獲得鎖;
      • 產生“飢餓”的原因:
        • 高優先級吞噬所有低優先級的CPU時間片,優先級越高,就會獲得越高的CPU執行機會; —> 使用默認的優先級;
        • 線程被永久阻塞在一個等待進入同步塊synchronized的狀態(長時間執行) ,同時synchronized並不保障等待線程的順序(鎖釋放后,隨機競爭,由OS調度),這會存在一個可能是某個線程總是搶鎖搶不到導致一直等待狀態 —> 避免持有鎖的線程長時間執行、使用显示lock來代替synchronized;
          synchronized(obj) {
                  while (true) {
               // .... infinite loop
               }
        •  等待的線程永遠不被喚醒:如果多個線程處在wait方法執行上,而對其調用notify方法不會保證哪一個線程會獲得喚醒,喚醒是無序的,跟VM/OS調度有關,甚至底層是隨機選取一個或是隊列中的第一個,任何線程都有可能處於繼續等待的狀態,因此存在這樣一個風險,即一個等待線程從來得不到喚醒,因為其他等待線程總是能被獲得喚醒 —> 使用显示lock來代替synchronized;
      •  比如ReentrantLock:
        •  在公平鎖中,如果有另一個線程持有鎖或者有其他線程在等待隊列中等待這個鎖,那麼新發出的請求的線程將被放入到隊列中;
        • 非公平鎖中, 根據搶佔機制,擁有鎖的線程在釋放鎖資源的時候, 新發出請求的線程可以和等待隊列中的第一個線程競爭鎖資源, 新線程競爭失敗才放入隊列中,但是已經進入等待隊列的線程, 依然是按照先進先出的順序獲取鎖資源;
  2. 入隊:有阻塞式和非阻塞式

    1. 阻塞式:當隊列中的元素已滿時,則會將此線程停止,讓其處於等待狀態,直到隊列中有空餘位置產生

      public void put(E e) throws InterruptedException {
              checkNotNull(e);
              final ReentrantLock lock = this.lock;
              lock.lockInterruptibly();//獲取鎖
              try {
                  //隊列中元素 == 數組長度(隊列滿了),則線程等待
                  while (count == items.length)
                      notFull.await();
                  enqueue(e);//元素加入隊列
              } finally {
                  lock.unlock();//釋放鎖
              }
          }
      • lockInterruptibly:
        • 如果當前線程未被中斷,則獲取鎖。
        • 如果該鎖沒有被另一個線程保持,則獲取該鎖並立即返回,將鎖的保持計數設置為 1。
        • 如果當前線程已經保持此鎖,則將保持計數加 1,並且該方法立即返回。
        • 如果鎖被另一個線程保持,則出於線程調度目的,禁用當前線程,並且在發生以下兩種情況之一以前,該線程將一直處於休眠狀態:1)鎖由當前線程獲得;2)其他某個線程中斷當前線程
    2. 非阻塞式:當隊列中的元素已滿時,並不會阻塞此線程的操作,而是讓其返回又或者是拋出異常

      public boolean add(E e) {
              return super.add(e);// AbstractQueue.add
          }
          public boolean add(E e) {
              if (offer(e))//調用實現接口
                  return true;
              else
                  throw new IllegalStateException("Queue full");
          }
          public boolean offer(E e) {
              checkNotNull(e);//檢測是否有空指針異常
              final ReentrantLock lock = this.lock;//獲得鎖對象
              lock.lock();//加鎖
              try {
                  //如果隊列滿了,返回false
                  if (count == items.length)
                      return false;
                  else {
                      //元素加入隊列
                      enqueue(e);
                      return true;
                  }
              } finally {
                  lock.unlock();//釋放鎖
              }
          }
          private void enqueue(E x) {
              // assert lock.getHoldCount() == 1;
              // assert items[putIndex] == null;
              //獲得數組
              final Object[] items = this.items;
              //槽位填充元素
              items[putIndex] = x;
              //獲得下一個被添加元素的索引,如果值等於數組長度,表示到達尾部了,需要從頭開始填充
              if (++putIndex == items.length)
                  putIndex = 0;
              count++;//數量+1
              notEmpty.signal();//喚醒出隊上的等待線程,表示有元素可以消費了
          }
      • enqueue中++putIndex == items.length,putIndex=0:這是因為當前隊列執行元素出隊時總是從隊列頭部獲取,而添加元素的索引從隊列尾部獲取所以當隊列索引(從0開始)與數組長度相等時,下次我們就需要從數組頭部開始添加了
    3. 阻塞式和非阻塞式的結合:offer(E e, long timeout, TimeUnit unit),向隊列尾部添加元素,可以設置線程等待時間,如果超過指定時間隊列還是滿的,則返回false;

      public boolean offer(E e, long timeout, TimeUnit unit)
              throws InterruptedException {
      
              checkNotNull(e);//檢測是否為空
              long nanos = unit.toNanos(timeout);//轉換成超時時間閥值
              final ReentrantLock lock = this.lock;
              lock.lockInterruptibly();//加鎖
              try {
                  //隊列是否滿了的判斷
                  while (count == items.length) {
                      if (nanos <= 0)//等待超時結束返回false
                          return false;
                      nanos = notFull.awaitNanos(nanos);//隊列滿了,等待出隊有空位填充
                  }
                  enqueue(e);//加入隊列中
                  return true;
              } finally {
                  lock.unlock();//釋放鎖
              }
          }
  3. 出隊:同樣有阻塞式和非阻塞式

    1. 阻塞式:當隊列中的元素已空時,則會將此線程停止,讓其處於等待狀態,直到隊列中有元素插入

      public E take() throws InterruptedException {
              final ReentrantLock lock = this.lock;
              lock.lockInterruptibly();
              try {
                  //隊列為空,進行等待
                  while (count == 0)
                      notEmpty.await();
                  return dequeue();//返回出隊元素
              } finally {
                  lock.unlock();
              }
          }
    2. 非阻塞式:當隊列中的元素已滿時,並不會阻塞此線程的操作,而是讓其返回null或元素【裏面的迭代器比較複雜,留待下文探究】

      public E poll() {
              final ReentrantLock lock = this.lock;
              lock.lock();
              try {
                  //隊列為空,返回null,否則返回元素
                  return (count == 0) ? null : dequeue();
              } finally {
                  lock.unlock();
              }
          }
          private E dequeue() {
              // assert lock.getHoldCount() == 1;
              // assert items[takeIndex] != null;
              final Object[] items = this.items;//獲得隊列
              @SuppressWarnings("unchecked")
              E x = (E) items[takeIndex];//獲得出隊元素
              items[takeIndex] = null;//出隊槽位元素置為null
              //下一個被取出元素的索引+1,如果值等於長度,表示後面沒有元素了,需要從頭開始取出
              if (++takeIndex == items.length)
                  takeIndex = 0;
              count--;//數量-1
              if (itrs != null)//迭代器不為空
                  itrs.elementDequeued();//同時更新迭代器中的元素數據
              notFull.signal();//喚醒入隊線程
              return x;//返回出隊元素
          }
    3. 阻塞式和非阻塞式的結合:poll(long timeout, TimeUnit unit),出隊獲取元素,可以設置線程等待時間,如果超過指定時間隊列還是空的,則返回null;

      public E poll(long timeout, TimeUnit unit) throws InterruptedException {
              long nanos = unit.toNanos(timeout);//轉換成超時時間閥值
              final ReentrantLock lock = this.lock;
              lock.lockInterruptibly();//加鎖
              try {
                  while (count == 0) {//隊列空了,等待
                      if (nanos <= 0)//超時了返回null
                          return null;
                      nanos = notEmpty.awaitNanos(nanos);//等待入隊填充元素
                  }
                  return dequeue();//返回出隊元素
              } finally {
                  lock.unlock();//釋放鎖
              }
          }
  4. 移除元素remove:

    public boolean remove(Object o) {
            //要移除的元素為空返回false
            if (o == null) return false;
            //獲得隊列數組
            final Object[] items = this.items;
            final ReentrantLock lock = this.lock;
            lock.lock();//加鎖
            try {
                //隊列有元素
                if (count > 0) {
                    final int putIndex = this.putIndex;//獲得下一個被添加元素的索引
                    int i = takeIndex;//下一個被取出元素的索引
                    do {
                        if (o.equals(items[i])) {//從takeIndex下標開始,找到要被刪除的元素
                            removeAt(i);//移除
                            return true;
                        }
                        if (++i == items.length)//下一個被取出元素的索引+1並判斷是否等於隊列長度,如果是,表示需要從頭開始遍歷
                            i = 0;
                    } while (i != putIndex);//繼續查找,直到找到最後一個元素
                }
                return false;
            } finally {
                lock.unlock();//解鎖
            }
        }
    
      /**
       * 根據下標移除元素,那麼會分成兩種情況一個是移除的是隊首元素,一個是移除的是非隊首元素,移除隊首元素,就相當於出隊操作,
       * 移除非隊首元素那麼中間就有空位了,後面元素需要依次補上,然後如果是隊尾元素,那麼putIndex也就是插入操作的下標也就需要跟着移動。
       */
        void removeAt(final int removeIndex) {
            // assert lock.getHoldCount() == 1;
            // assert items[removeIndex] != null;
            // assert removeIndex >= 0 && removeIndex < items.length;
            final Object[] items = this.items;//獲得隊列
            if (removeIndex == takeIndex) {//移除的是隊首元素
                // removing front item; just advance
                items[takeIndex] = null;//隊首置為null
                if (++takeIndex == items.length)//下一個被取出元素的索引+1並判斷是否等於隊列長度
                    takeIndex = 0;
                count--;//數量-1
                if (itrs != null)//迭代器不為空
                    itrs.elementDequeued();//更新迭代器元素
            } else {//移除的不是隊首元素,而是中間元素
                // an "interior" remove
    
                // slide over all others up through putIndex.
                final int putIndex = this.putIndex;//下一個被添加元素的索引
                for (int i = removeIndex;;) {//對隊列進行遍歷,因為是隊列中間的值被移除了,所有後面的元素都要挨個遷移
                    int next = i + 1;//獲取移除元素的下一個坐標
                    if (next == items.length)//判斷是否等於隊列長度
                        next = 0;
                    if (next != putIndex) {//獲取移除元素的下一個坐標!=下一個被添加元素的索引,表示移除元素的索引後面有值
                        items[i] = items[next];//當前要移除的元素置為後面的元素,即對後面的元素往前遷移,覆蓋要移除的元素
                        i = next;//下一個遷移的索引
                    } else {//移除的元素是最後一個,後面沒有值了
                        items[i] = null;//移除元素,直接置為null
                        this.putIndex = i;//更新下一個被添加元素的索引
                        break;//結束
                    }
                }
                count--;//數量-1
                if (itrs != null)//迭代器不為空
                    itrs.removedAt(removeIndex);//更新迭代器元素
            }
            notFull.signal();//喚醒入隊線程,可以添加元素了
        }
  5. 清空元素clear:用於清空ArrayBlockingQueue,並且會釋放所有等待notFull條件的線程(存放元素的線程)

    public void clear() {
            final Object[] items = this.items;//獲得隊列
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                int k = count;//獲取元素數量
                if (k > 0) {//有元素,表示隊列不為空
                    final int putIndex = this.putIndex;//下一個被添加元素的索引
                    int i = takeIndex;//下一個被取出元素的索引
                    do {
                        items[i] = null;//對每個有元素的槽位置為null
                        if (++i == items.length)
                            i = 0;
                    } while (i != putIndex);//從有元素的第一個槽位開始遍歷,直到槽位元素為null
                    takeIndex = putIndex;//更新取出和添加的索引
                    count = 0;//數量更新為0
                    if (itrs != null)//迭代器不為空
                        itrs.queueIsEmpty();//更新迭代器為空
                    //若有等待notFull條件的線程,則逐一喚醒
                    for (; k > 0 && lock.hasWaiters(notFull); k--)
                        notFull.signal();//喚醒入隊線程,可以添加元素了
                }
            } finally {
                lock.unlock();
            }
        }
  6. offer(E e, long timeout, TimeUnit unit)和poll(long timeout, TimeUnit unit)裏面有awaitNanos,下面探討該功能實現:對當前線程或等待的入/出隊線程進行掛起,如果有入/出隊操作進行了喚醒出/入隊操作,則acquireQueued自旋獲取到鎖,然後出/入隊中的ReentrantLock是重入鎖,可以重入獲取到鎖進行出/入隊操作

        AbstractQueuedSynchronizer:
        //進行超時控制
        public final long awaitNanos(long nanosTimeout)
                throws InterruptedException {
            //如果當前線程中斷了拋出中斷異常
            if (Thread.interrupted())
                throw new InterruptedException();
            //當前線程加入到Condition隊列中
            Node node = addConditionWaiter();
            //鎖釋放是否成功:釋放當前線程的lock,從AQS的隊列中移出
            int savedState = fullyRelease(node);
            //到達等待時間點
            final long deadline = System.nanoTime() + nanosTimeout;
            //中斷標識
            int interruptMode = 0;
            //當前節點是否在同步隊列中,否表示不在,進入掛起判斷操作,如果已經在Sync隊列中,則退出循環
            //那什麼時候會把當前線程又加入到Sync隊列中呢?當然是調用signal方法的時候,因為這裏需要喚醒之前調用await方法的線程,喚醒之後進行下面的獲取鎖等操作
            while (!isOnSyncQueue(node)) {
                //如果超時了,將線程掛起,然後停止遍歷
                if (nanosTimeout <= 0L) {
                    transferAfterCancelledWait(node);
                    break;
                }
                //如果等待時間間隔超過了1000,繼續掛起
                if (nanosTimeout >= spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                //線程中斷了停止遍歷
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                //獲得剩餘的等待時間間隔
                nanosTimeout = deadline - System.nanoTime();
            }
            //結束掛起,acquireQueued自旋對當前線程的隊列出隊進行獲取鎖並返回線程是否中斷
            //如果線程被中斷,並且中斷的方式不是拋出異常,則設置中斷後續的處理方式設置為REINTERRUPT
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;//中斷標識更新為退出等待時重新中斷
            if (node.nextWaiter != null)//當前節點後面還有節點,多併發操作了
                unlinkCancelledWaiters();//從頭到尾遍歷Condition隊列,移除被cancel的節點
            //如果線程已經被中斷,則根據之前獲取的interruptMode的值來判斷是繼續中斷還是拋出異常
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return deadline - System.nanoTime();//返回剩餘等待時間
        }
  7. drainTo可以一次性獲取隊列中所有的元素,它減少了鎖定隊列的次數,使用得當在某些場景下對性能有不錯的提升

    //最多從此隊列中移除給定數量的可用元素,並將這些元素添加到給定collection中
        public int drainTo(Collection<? super E> c) {
            return drainTo(c, Integer.MAX_VALUE);
        }
        public int drainTo(Collection<? super E> c, int maxElements) {
            checkNotNull(c);//檢查是否為空
            if (c == this)//如果集合類型相同拋出參數異常
                throw new IllegalArgumentException();
            if (maxElements <= 0)//如果給定移除數量小於0,返回0,表示不做移除操作
                return 0;
            final Object[] items = this.items;//獲得隊列
            final ReentrantLock lock = this.lock;
            lock.lock();//加鎖
            try {
                int n = Math.min(maxElements, count);//獲得元素的最小數量
                int take = takeIndex;//下一個被取出元素的索引
                int i = 0;
                try {
                    while (i < n) {//遍歷移除和添加
                        @SuppressWarnings("unchecked")
                        E x = (E) items[take];//獲得移除元素
                        c.add(x);//元素添加到直到集合中
                        items[take] = null;//元素原先隊列位置置為null
                        if (++take == items.length)//如果取出索引到達尾部,從頭開始遍歷取出
                            take = 0;
                        i++;//移除的數量+1,如果達到了移除的最小數量,結束遍歷
                    }
                    return n;//返回一共移除並添加了多少個元素
                } finally {
                    // Restore invariants even if c.add() threw
                    if (i > 0) {//如果有移除操作
                        count -= i;//隊列元素數量-i
                        takeIndex = take;//重置下一個被取出元素的索引
                        if (itrs != null) {//迭代器不為空
                            if (count == 0)//隊列空了
                                itrs.queueIsEmpty();//迭代器清空
                            else if (i > take)//說明take中間變成0了,通知itr
                                itrs.takeIndexWrapped();
                        }
                        //喚醒在因為隊列滿而等待的入隊線程,最多喚醒i個,避免線程被喚醒了因為隊列又滿了而阻塞
                        for (; i > 0 && lock.hasWaiters(notFull); i--)
                            notFull.signal();
                    }
                }
            } finally {
                lock.unlock();
            }
        }

 

三.Logback 框架中異步日誌打印中ArrayBlockingQueue的使用

  1. 在高併發並且響應時間要求比較小的系統中同步打日誌已經滿足不了需求了,這是因為打日誌本身是需要同步寫磁盤的,會造成 響應時間 增加,如下圖同步日誌打印模型為:

  2. 異步模型是業務線程把要打印的日誌任務寫入一個隊列后直接返回,然後使用一個線程專門負責從隊列中獲取日誌任務寫入磁盤,其模型具體如下圖:

    • 如圖可知其實 logback 的異步日誌模型是一個多生產者單消費者模型,通過使用隊列把同步日誌打印轉換為了異步,業務線程調用異步 appender 只需要把日誌任務放入日誌隊列,日誌線程則負責使用同步的 appender 進行具體的日誌打印到磁盤;
  3. 接下來看看異步日誌打印具體實現,要把同步日誌打印改為異步需要修改 logback 的 xml 配置文件:

    <appender name="PROJECT" class="ch.qos.logback.core.FileAppender">
            <file>project.log</file>
            <encoding>UTF-8</encoding>
            <append>true</append>
    
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!-- daily rollover -->
                <fileNamePattern>project.log.%d{yyyy-MM-dd}</fileNamePattern>
                <!-- keep 7 days' worth of history -->
                <maxHistory>7</maxHistory>
            </rollingPolicy>
            <layout class="ch.qos.logback.classic.PatternLayout">
                <pattern>
                    <![CDATA[%n%-4r [%d{yyyy-MM-dd HH:mm:ss}] %X{productionMode} - %X{method} %X{requestURIWithQueryString} [ip=%X{remoteAddr}, ref=%X{referrer},
                    ua=%X{userAgent}, sid=%X{cookie.JSESSIONID}]%n  %-5level %logger{35} - %m%n]]>
                </pattern>
            </layout>
        </appender>
    
        <appender name="asyncProject" class="ch.qos.logback.classic.AsyncAppender">
            <discardingThreshold>0</discardingThreshold>
            <queueSize>1024</queueSize>
            <neverBlock>true</neverBlock>
            <appender-ref ref="PROJECT" />
        </appender>
         <logger name="PROJECT_LOGGER" additivity="false">
            <level value="WARN" />
            <appender-ref ref="asyncProject" />
        </logger>
  4. 從上面可知 AsyncAppender 是實現異步日誌的關鍵,下面探究它的原理:

    1. 如上圖可知 AsyncAppender 繼承自 AsyncAppenderBase,其中後者具體實現了異步日誌模型的主要功能,前者只是重寫了其中的一些方法。另外從類圖可知 logback 中的異步日誌隊列是一個阻塞隊列, 後面會知道其實是一個有界阻塞隊列 ArrayBlockingQueue, 其中 queueSize 是有界隊列的元素個數默認為 256;
    2. worker則是工作線程,也就是異步打印日誌的消費者線程,aai則是一個appender的裝飾器,裡邊存放的同步日誌的appender,其中appenderCount記錄aai裡邊附加的同步appender的個數(這個和配置文件相對應,一個異步的appender對應一個同步的appender),neverBlock用來指示當同步隊列已滿時是否阻塞打印日誌線程(如果配置neverBlock=true,當隊列滿了之後,後面阻塞的線程想要輸出的消息就直接被丟棄,從而線程不會阻塞),discardingThreshold是一個閾值,當日誌隊列裡邊的空閑元素個數小於該值時,新來的某些級別的日誌就會直接被丟棄。
  5.  接下來看下何時創建的日誌隊列以及何時啟動的消費線程,這需要看下 AsyncAppenderBase 的 start 方法,該方法是在解析完畢配置 AsyncAppenderBase 的 xml 的節點元素后被調用 :

    public void start() {
            if (isStarted())
                return;
            if (appenderCount == 0) {
                addError("No attached appenders found.");
                return;
            }
            if (queueSize < 1) {
                addError("Invalid queue size [" + queueSize + "]");
                return;
            }
            // 創建一個ArrayBlockingQueue阻塞隊列,queueSize默認為256,創建阻塞隊列的原因是:防止生產者過多,造成隊列中元素過多,產生OOM異常
            blockingQueue = new ArrayBlockingQueue<E>(queueSize);
            // 如果discardingThreshold未定義的話,默認為queueSize的1/5
            if (discardingThreshold == UNDEFINED)
                discardingThreshold = queueSize / 5;
            addInfo("Setting discardingThreshold to " + discardingThreshold);
            // 將工作線程設置為守護線程,即當jvm停止時,即使隊列中有未處理的元素,也不會在進行處理
            worker.setDaemon(true);
            // 為線程設置name便於調試
            worker.setName("AsyncAppender-Worker-" + getName());
            // make sure this instance is marked as "started" before staring the worker Thread
            // 啟動線程
            super.start();
            worker.start();
        }
    1. logback 使用的隊列是有界隊列 ArrayBlockingQueue,之所以使用有界隊列是考慮到內存溢出問題,在高併發下寫日誌的 qps 會很高如果設置為無界隊列隊列本身會佔用很大內存,很可能會造成 內存溢出。
    2. 這裏消費日誌隊列的 worker 線程被設置為了守護線程,意味着當主線程運行結束並且當前沒有用戶線程時候該 worker 線程會隨着 JVM 的退出而終止,而不管日誌隊列裏面是否還有日誌任務未被處理。另外這裏設置了線程的名稱是個很好的習慣,因為這在查找問題的時候很有幫助,根據線程名字就可以定位到是哪個線程。
  6. 既然是有界隊列那麼肯定需要考慮如果隊列滿了,該如何處置,是丟棄老的日誌任務,還是阻塞日誌打印線程直到隊列有空餘元素那?下面看append 方法:

    protected void append(E eventObject) {
            // 判斷隊列中的元素數量是否小於discardingThreshold,如果小於的話,並且日誌等級小於info的話,則直接丟棄這些日誌任務
            if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) {
                return;
            }
            preprocess(eventObject);
            // 日誌入隊
            put(eventObject);
        }
        private boolean isQueueBelowDiscardingThreshold() {
            return (blockingQueue.remainingCapacity() < discardingThreshold);
        }
    
       // 子類重寫的方法   判斷日誌等級
        protected boolean isDiscardable(ILoggingEvent event) {
            Level level = event.getLevel();
            return level.toInt() <= Level.INFO_INT;
        }    
    • 日誌入隊put:從下面可知如果 neverBlock 設置為 false(默認為 false)則會調用阻塞隊列的 put 方法,而 put 是阻塞的,也就是說如果當前隊列滿了,如果再企圖調用 put 方法向隊列放入一個元素則調用線程會被阻塞直到隊列有空餘空間。這裡有必要提下其中blockingQueue.put(eventObject)當日誌隊列滿了的時候 put 方法會調用 await() 方法阻塞當前線程,如果其它線程中斷了該線程,那麼該線程會拋出 InterruptedException 異常,那麼當前的日誌任務就會被丟棄了。如果 neverBlock 設置為了 true 則會調用阻塞隊列的 offer 方法,而該方法是非阻塞的,如果當前隊列滿了,則會直接返回,也就是丟棄當前日誌任務。
      private void put(E eventObject) {
              // 判斷是否阻塞(默認為false),則會調用阻塞隊列的put方法
              if (neverBlock) {
                  blockingQueue.offer(eventObject);
              } else {
                  putUninterruptibly(eventObject);
              }
      }
          // 可中斷的阻塞put方法
          private void putUninterruptibly(E eventObject) {
              boolean interrupted = false;
              try {
                  while (true) {
                      try {
                          blockingQueue.put(eventObject);
                          break;
                      } catch (InterruptedException e) {
                          interrupted = true;
                      }
                  }
              } finally {
                  if (interrupted) {
                      Thread.currentThread().interrupt();
                  }
              }
          }
  7. 最後看下 addAppender 方法,可以看出,一個異步的appender只能綁定一個同步appender,這個appender會被放入AppenderAttachableImpl的appenderList列表裡邊

    public void addAppender(Appender<E> newAppender) {
            if (appenderCount == 0) {
                appenderCount++;
                addInfo("Attaching appender named [" + newAppender.getName() + "] to AsyncAppender.");
                aai.addAppender(newAppender);
            } else {
                addWarn("One and only one appender may be attached to AsyncAppender.");
                addWarn("Ignoring additional appender named [" + newAppender.getName() + "]");
            }
    }
  8. 通過上面我們已經分析完了日誌生產線程放入日誌任務到日誌隊列的實現,下面一起來看下消費線程是如何從隊列裏面消費日誌任務並寫入磁盤的,由於消費線程是一個線程,那就從 worker 的 run 方法看起(消費者,將日誌寫入磁盤的線程方法):

    class Worker extends Thread {
    
            public void run() {
                AsyncAppenderBase<E> parent = AsyncAppenderBase.this;
                AppenderAttachableImpl<E> aai = parent.aai;
    
                // loop while the parent is started 一直循環知道線程被中斷
                while (parent.isStarted()) {
                    try {// 從阻塞隊列中獲取元素,交由給同步的appender將日誌打印到磁盤
                        E e = parent.blockingQueue.take();
                        aai.appendLoopOnAppenders(e);
                    } catch (InterruptedException ie) {
                        break;
                    }
                }
    
                addInfo("Worker thread will flush remaining events before exiting. ");
                //執行到這裏說明該線程被中斷,則把隊列裡邊的剩餘日誌任務刷新到磁盤
                for (E e : parent.blockingQueue) {
                    aai.appendLoopOnAppenders(e);
                    parent.blockingQueue.remove(e);
                }
    
                aai.detachAndStopAllAppenders();
            }
        }
    • try邏輯中從日誌隊列使用 take 方法獲取一個日誌任務,如果當前隊列為空則當前線程會阻塞到 take 方法直到隊列不為空才返回,獲取到日誌任務後會調用 AppenderAttachableImpl 的 aai.appendLoopOnAppenders 方法,該方法會循環調用通過 addAppender 注入的同步日誌 appener 具體實現日誌打印到磁盤的任務。

四.參考:

  1. 公平鎖的使用場景:https://stackoverflow.com/questions/26455578/when-to-use-fairness-mode-in-java-concurrency
  2. 公平鎖和非公平鎖的區別的提問:https://segmentfault.com/q/1010000006439146
  3. 公平鎖不能保證線程調度的公平性:https://stackoverflow.com/questions/60903107/understanding-fair-reentrantlock-in-java
  4. logback異步日誌打印中的ArrayBlockingQueue的使用:https://my.oschina.net/u/4410397/blog/3428573

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

【其他文章推薦】

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案