巴西大沼澤火災 美洲豹主要棲息地逾7成燒毀

摘錄自2020年9月16日中央社報導

位於巴西中西部馬托格羅索州大沼澤的水遇州立公園是全世界美洲豹最集中的地方,如今卻因森林火災蔓延,嚴重威脅美洲豹和其他物種的生存。

英國廣播公司巴西新聞網(BBC News Brasil)今(15日)報導,位於馬托格羅索州(Mato Grosso)占地10.8公頃的水遇州立公園(Parque Estadual Encontro dasÁguas),目前已有超過7成範圍被大火燒毀。

如同水遇州立公園,整個大沼澤(Pantanal)地區的火勢也迅速蔓延,迄今已燒毀逾230萬公頃林地,範圍相當於這整個生物群系在巴西15%的面積。

根據巴西國家太空署(Inpe),今年1月至9月初,大沼澤共記錄1.21萬處火災,創下1999年以來同期最高紀錄。

美洲豹已被國際自然保育聯盟(IUCN)和巴西環境部視為幾乎瀕危物種。根據研究,全球90%的美洲虎集中在南美洲。

巴西生物學家波柳薩(Ricardo Boulhosa)指出,這些火災是人為的,無法控制,所以變成森林大火,危害美洲豹的生存。

非政府組織Panthera研究員托爾塔托(Fernando Tortato)強調水遇州立公園的重要性,因為那裡一直是受保護的生態走廊,確保美洲豹的優質棲息地,園內有許多美洲豹可以觀察,有利於發展生態旅遊。

生物多樣性
國際新聞
巴西
大沼澤
美洲豹
動物棲地
森林野火

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

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

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

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

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

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

飼料費太貴養不起! 無尾熊遭大阪天王寺動物園「裁員」

摘錄自2020年9月15日自由時報報導

根據日本《讀賣新聞》報導,為了讓動物園經營合理化,大阪天王寺動物園園長牧慎一郎持續對園內動物進行「裁員」,其中包括超人氣的無尾熊。天王寺動物園2014年在園內曾有3隻無尾熊,但飼料費一年高達6400萬日圓(約新台幣1800萬元),占園區整體動物飼料費4至5成,沉重的負擔讓園方經營日漸困難。

為了減省開支,園內最後一隻無尾熊「Ark」於2019年10月免費借給國外的動物園。

牧慎一郎認為,在有限的預算與土地下,動物也必須進行「集中與選擇」。而園方原本計畫,園內飼養的212種動物中,要「裁員」22種,但因高齡動物死亡、動物打架致死等意外,至2019年底時,園內只剩下184種動物。

生物多樣性
國際新聞
日本
無尾熊
動物園
動物福利

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

第一次購車,想花10來萬買豪車、用小錢買大SUV!?

58%/1。40%,而SUV可以說是市場上的寵兒,在17年全年共銷售了1007。77萬輛,同比增長了15。3%。SUV總給到我們高大威猛的感覺,而且憑藉著高底盤所帶來的高通過性以及廂式結構的高實用性,在同樣價格下,即便SUV車型的定位要低於轎車,但是在氣場上依然會比轎車更強。

人總會有第一次,而你第一次購車到底碰到過怎麼樣的經歷呢?今天咱們不妨來討論一下。

簡單來說,第一次大家都沒有太大經驗,所以在選車方面也變得比較迷茫,於是也會存在各種各樣的誤區。說起人生第一次購車,其實也是分為數人群。

第一種就是剛畢業不久出社會打滾的大學生,這部分消費者購車大多數會有家人的經濟支持(也有少部分精英通過自己的努力去買下一台心儀之車);另一種是有購車計劃但奈何購車資金遲遲不到位的消費者。

值得一提的是,無論你是哪一種情況,在選車時所碰到的情況無非只有幾種:

眼高手低這一個四字詞語也可以很好地形容我們的購車觀念,關於預算這方面的問題,也可以說是90%的消費者都躺槍的一個地方,就像有不少人一直有顆買思域的心,但卻忽視了自己只有買飛度的預算一樣。

也正因為這一種情況,你會發現像有很多豪華品牌都開始自降身份,推出極為“親民”的車型搶佔合資市場,像奧迪的A3、寶馬的1系也是一個非常成功的例子,雖然網上的吐槽聲不斷,但是買的人依然絡繹不絕,畢竟它們是豪華品牌、關鍵還賣得便宜呀!

另一個方面,預算不夠又想買更高級的車型要怎麼辦?做貸款是大多數車主的選擇,在絕大多數情況下,4S店更希望你選擇貸款購車,而且在貸款方案上也給出比較大的優惠,譬如超低首付、0利息等等的貸款政策(當然,羊毛出在羊身上,貸了多少錢,該還的還是要還的)。

花了大部分積蓄去買一台車,每一位消費者都不希望買到一台問題不斷的故障車,所以才會出現有很多消費者在選車時會出現偏向於選擇合資品牌甚至對某個品牌有極高忠誠度的情況,歸根到底的原因是這些品牌自從進入中國市場以來,通過旗下車型的極佳表現早早在消費者心中打下了“質量高”的良好口碑,讓消費者信賴。

這也是為什麼近幾年來國產品牌有着質一般的飛躍,但是很多消費者依然偏向於考慮合資,關鍵也就在於“口碑”之上(畢竟在很多消費者心中,國產品牌剛起步時口碑真心不怎麼樣),像這樣的固有思想需要用一段很長的時間去改變。

也是因為這個,所以在用樣的預算之下,有很多消費者便會選擇去購買一輛SUV車型,根據中汽協發布的數據显示,2017年我國乘用車產銷量分別為2480.67萬/2471.83萬,同比增長了1.58%/1.40%,而SUV可以說是市場上的寵兒,在17年全年共銷售了1007.77萬輛,同比增長了15.3%。

SUV總給到我們高大威猛的感覺,而且憑藉著高底盤所帶來的高通過性以及廂式結構的高實用性,在同樣價格下,即便SUV車型的定位要低於轎車,但是在氣場上依然會比轎車更強。

關於選車這一方面,其實也是一個比較尷尬的情況,譬如是靠家中的資金購車的消費者,給錢的不是你,最終你可能買到一台你家人喜愛的車型;又或者你看上了思域,可是家中的老婆大人卻看上了A3,相信…無論怎麼選,最終的選擇權估計不會在你手上。

簡單來說就是將更多的注意力放在等待新款車型上市,對於絕大部分新款車型而言,剛上市並沒有太多的優惠(基本是沒有的),這時候購車你又嫌優惠少,再等幾個月又有新款車型上市,究竟要到什麼時候你才會下訂決心買車呢?

普遍情況下,車型上市時間越久,現金優惠力度越大,而且車型價格越高優惠幅度越大。像合資的B級車或者更高級的豪華品牌,動不動就會有數萬的優惠。

簡單來說,第一次購車,大家都會像一個懵懂的少年,不知所措。你還記得當初你在首次購車的時候碰到過什麼樣的煩惱嗎?不妨在下方評論區分享出來!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

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

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

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

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

不要小看這些LOGO,分分鐘差個幾百萬!

而其標誌性的立標設計,當你坐進奔馳座艙抬頭看見發動機艙矗立的“三叉星”,心底一種莫名的自豪感,以及一種底氣油然而生,這也是E級立標的銷量遠超大標的重要因素。除此外,將安全帶融入到LOGO的沃爾沃也是一個很有特色的設計。

LOGO,在很大程度上代表着一個品牌、一件產品的形象、價值,乃至地位。一個好的LOGO,不僅能讓人過目不忘,並且能為消費者灌入一個品牌歸屬感,以及品牌忠誠度。設計出一個優秀的LOGO,十分考驗設計師的功力,以及決策層對企業和品牌文化的定調,二者缺一不可。

今天,我們就來聊聊市面上常見的汽車品牌,它們各自間由LOGO而讓人聯想到的一些有趣的故事,一起來看看:

諸多汽車品牌LOGO中,叫獸個人最欣賞奔馳的LOGO。“三叉星”簡潔有力,並象徵著奔馳向海陸空三方位發展的野心。而其標誌性的立標設計,當你坐進奔馳座艙抬頭看見發動機艙矗立的“三叉星”,心底一種莫名的自豪感,以及一種底氣油然而生,這也是E級立標的銷量遠超大標的重要因素。

除此外,將安全帶融入到LOGO的沃爾沃也是一個很有特色的設計。沃爾沃從LOGO開始,就在向世人显示自身對安全的不懈追求。

拋開品牌檔次問題,不知道各位小夥伴比較喜歡哪家品牌的LOGO呢?歡迎在評論區留言。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

15年、20多萬公里還在跑,這是別克曾經最牛的車型!

Samuel拿到駕照的那一天,他從父親手中接過了君威的方向盤。在這十多年裡,君威給Samuel留下了不少深刻的回憶。他說這台重達1。6噸的中型轎車在遇上起伏較大的路面時,車身就像一艘船般搖晃,舒適性卻是一流。他還吐槽這2。

筆者有一位叫Samuel的朋友,是一位年輕的醫生,最近他跟筆者說,他要買新車了。由於沒有找到額外的固定車位,他決定“淘汰”掉已經服役了十多年的老君威。與即將開上新車的喜悅相比,Samuel更舍不得離開這台陪伴了他多年的老君威。

那天,Smauel說:

不如你幫我的老君威拍一組照片吧

做個留念。

筆者答應了。

拍攝的地點選在了廣州大學城,在這一片江邊的林蔭小道上,我們拍攝了一組老車配落恭弘=叶 恭弘的照片。

夕陽無限好,只是近黃昏

歲月讓這台老君威的面容憔悴,身上的小毛病也越來越多

它真的不能再服務Samuel一家了

在拍攝間隙,筆者和Samuel聊起了這台老君威的故事。

Samuel表示,這台別克君威GL 2.5是他家裡第一台車。

作為別克在中國生產的第一代別克君威,同時也是別克Regal系列的第四代車型,國內消費者心中的“美系車”印象在很大程度上就是基於這款車型產生的。

雖然與別克新世紀(Reagl剛剛進入中國時被命名為新世紀)相比,君威的外觀經過修改,但也依然保留了非常原汁原味的美式設計風格。

和大部分80后、90后青年一樣

家中的第一台車往往也是自己感受最深刻的一台車。

Samuel回想起2005年家中決定買車時,市面上的合資中型轎車選擇並不多,他說當時他的父親將選擇範圍確定在了別克君威、本田雅閣和現代索納塔之間。經過一輪深思熟慮,別克君威成為了他父親的心頭好。Samuel拿到駕照后,這台君威也就同時成為了他的座駕,直至今日。

Samuel拿到駕照的那一天,他從父親手中接過了君威的方向盤。

在這十多年裡,君威給Samuel留下了不少深刻的回憶;他說這台重達1.6噸的中型轎車在遇上起伏較大的路面時,車身就像一艘船般搖晃,舒適性卻是一流。他還吐槽這2.5L自然吸氣V6發動機竟只能輸出152馬力,油耗高卻沒有足夠的動力輸出;而這就是老君威給他留下的“美式豪華轎車”的印象。不過,Samuel卻不覺得這些是缺陷,他說早前曾試駕過現款別克君越,無論是設計還是駕控表現都非常優秀,但是卻沒有了老君威這種突出的美式印象,個性稍有缺失。

十多年的服役,中央扶手已經破損。

在Samuel看來,老君威的座椅舒適性即便是在今天看來也是一流的。

2.5L自然吸氣V6發動機只能輸出152馬力,搭配4擋自動變速箱,對於超過1.5噸的整車重量來說,動力表現不盡人意,這也是Samuel對老君威唯一的“抱怨”。

Samuel跟筆者說,正如電影《桃姐》里所講述的故事一樣,一位給家裡服務了多年的保姆,當有一天她年紀大了,必須離開的時候,就犹如家人分離,那種不舍的感覺讓人難以描述;他正在努力尋找一個能夠長期停放這台老君威的地方,就像電影里劉德華把桃姐送到了護老院,他要將這台滿載回憶的車保存起來,希望他能夠早日找到這個地方吧。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

全新日系SUV能幹掉途觀?這6款將入華的SUV搶先看!

凱迪拉克XT4(家族特徵明顯的大哥——XT5)為了增加在緊湊型SUV市場的布局,凱迪拉克將在此次紐約車展發布全新XT4車型,新車預計會在年內(第三季度)投產。據悉XT4將延續凱迪拉克家族式的鑽石切割語言,LED大燈組加上淚眼式LED日行燈,參考現款XT5的樣子,我們似乎已經能腦補出XT4的模樣,不過定位更低尺寸更小的XT4肯定會更加時尚。

轉眼已將是春回大地的人間四月天,人們都爭相外出踏青擁抱大自然的時候,卻有不少狂熱的汽車愛好者將注意力都集中在了地球的另一邊。只因為紐約東海岸即將上演的車迷狂歡——紐約國際車展將在這個三月底正式開幕,也迫不及待地篩選出了會在本次車展亮相的多款重磅SUV,來給大夥做個預熱盤點!

豐田新一代RAV4

(外媒此前發布的假想圖)

2017年豐田RAV4憑藉著40多萬輛的銷量斬獲美國SUV市場銷冠的殊榮,相比之下在國內市場的表現就只能以一般般來形容了。在即將開幕的紐約車展上我們將迎來第五代豐田RAV4的正式發布,從海外媒體放出的假想圖及預告信息來看,新車外觀將會有明顯的變化,基於豐田全新TNGA架構打造的它有着更為大氣厚重的外形,相比現款顏值會有不少的提升。

全新RAV4同時將增加碰撞預警、行人檢測、車道保持等諸多安全配置以提升競爭力,動力上它將搭載兩款全新的2.0L、2.5L自然吸氣發動機,傳動系統分別匹配CVT、8AT變速箱,未來和全新凱美瑞一樣還有2.5L混動版車型推出。

斯巴魯全新森林人

(現款森林人)

全新一代斯巴魯森林人將和XV、翼豹共享斯巴魯SGp全球模塊化平台(Subaru Global platform),從現有的預告信息來看,新一代森林人在外觀上變化並不大,僅通過細節設計的改進以迎合當今消費者的審美。不過斯巴魯最新的EyeSight駕駛輔助系統倒是有望出現在這款新車上,而動力方面除了主打2.0L/2.0T/2.5L水平對置發動機以外,基於新平台的全新森林人未來還有可能推出插電式混動、純電動等不同動力配置車型。

凱迪拉克XT4

(家族特徵明顯的大哥——XT5)

為了增加在緊湊型SUV市場的布局,凱迪拉克將在此次紐約車展發布全新XT4車型,新車預計會在年內(第三季度)投產。據悉XT4將延續凱迪拉克家族式的鑽石切割語言,LED大燈組加上淚眼式LED日行燈,參考現款XT5的樣子,我們似乎已經能腦補出XT4的模樣,不過定位更低尺寸更小的XT4肯定會更加時尚。新車未來除了搭載盲點監控、變道輔助等安全配置,還將配備Super Cruise自動駕駛輔助系統,而2.0T+9AT的動力總成也絲毫不輸同級車型。

謳歌RDX量產版

全新謳歌RDX原型車已經在前不久的北美車展上率先亮相,新車採用鑽石形狀前格柵、個性的LED大燈組等家族設計語言,而從即將在紐約車展上正式發布的RDX量產版預告圖上看,全新RDX將首次推出“A-Spec”版本車型(A-Spec旨在不改變原車動力、舒適性及安全性的前提下提高車輛性能,以排氣系統的升級改裝著稱)。新車未來除了將採用全新的底盤及2.0T+10AT的動力總成,還將配備謳歌最新的SH-AWD全時四驅系統,坐等它年內的國產了!

大眾Atlas五座版

作為上汽大眾途昂在美國的姊妹車型,大眾Atlas車型將在7座版本的基礎上,在本屆紐約車展中正式發布一款5座版本車型。外觀上新車延續了大眾家族化設計風格,尺寸上會比現款Atlas有所減小,但車內乘坐空間與後備廂空間勢必會有明顯提升,屆時新車動力上將繼續搭載與現款Atlas一樣的2.0T+8AT的動力總成。

林肯Aviator

(使用全新家族設計的MKC)

根據林肯官方發布的預告圖來看,曾於2002年推出、2005年停產的大型SUV——Aviator(飛行員)即將復活,新車據悉與福特新一代探險者同平台打造,定位在現款MKX與全新領航員之間,未來或將代替林肯旗下即將停產的MKT進軍國內大型SUV市場,完善林肯在國內的SUV產品序列。而目前林肯已經在國內註冊了“飛行員”商標,新車未來極有可能會成為林肯在華的首款國產車型。

從重磅SUV的陣容來看,豐田RAV4、全新凱迪拉克XT4這類更貼近普通消費者生活的緊湊型SUV依舊是重中之重,除了緊湊型這一級別,謳歌、林肯這樣的豪華品牌更是爭相往更高級別的中大型及大型SUV領域布局產品,並且國產的消息也增加了不少自身話題性與熱度。而隨着新能源潮流襲來,未來混動SUV也不再是稀有物種,想知道更多紐約車展的看點熱點,敬請關注後續的報道~本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

3000萬人都喜歡開的10萬級神車,竟然還有5種不同選擇!

19萬R-line系列主要是基於280TSI車型對外觀和內飾進行運動風格的升級,比如前包圍的設計、輪轂造型都更運動化,和更年輕化。但是整體造型也不及高爾夫GTI系列的激進,就如同一件純白色的襯衫印了一些很酷的標識在上面,整體很平淡自然,細節又很個性特別。

至今,高爾夫已經歷經7.5代,累積產量已突破3000萬輛,是世界上銷量最大的汽車品牌,在累積千萬車主的同時,高爾夫也收穫不少粉絲,上至達官貴族、下至平頭百姓,其中下面這些名人都曾是高爾夫的忠實粉絲。

雖然我們今天未能拿到這些“名人”車主的用車口碑與大家分享,而且我們現在也買不到他們當年的車型了,所以我們今天的話題是針對我們國內現在能買得到最新的高爾夫,看看那些比較有代表的車主口碑都是怎麼說的?

現在我們在國內能買的高爾夫車型其實也不少,包括最近在中國上市的純電版e-Golf,但是由於車型太新,我們還未能收集到車主的口碑,所以今天我們聊的車型還是以大家熟悉的為主。

2018款 高爾夫·嘉旅

指導價:13.19-19.79萬

如何理解高爾夫·嘉旅的定位?其實它跟高爾夫的關係就像iphone 7和iphone 7 plus的關係,現款嘉旅無論是軸距還是空間上都比高爾夫要更長和更大,而且配置上也有一些先天的優勢,比如全景天窗。不過外觀造型明顯就比高爾夫要臃腫,沒有高爾夫給人的那種協調感,也沒有高爾夫那種獨有的氣質,更強調的是實用性和舒適性。

內飾的設計語言當然也和高爾夫保持一致,不過在風格上也有明顯的差異,嘉旅的中控會台使用了大面積的飾板(根據不同車型,飾板紋路不同),這樣更能營造出居家的氛圍。

車主口碑:買之前與高爾夫7對比過,感覺嘉旅要更運動,像是一輛小型SUV,空間很大,特別是後備箱空間比高爾夫大很多。最滿意的是嘉旅配備全景天窗。(2018款 230TIS自動豪華版)

2018款 高爾夫

指導價:12.19-18.29萬

現款7.5代高爾夫的外觀比起7代其實沒有太大變化,畢竟只是中期改款,前大燈組採用雙段式LED日間行車燈,中央格柵的鍍鉻飾條與日間行車燈相連,整體而言就是更精緻了。

內飾設計與7代車型也沒有太大變化,主要是2018款280TSI 自動旗艦型可選裝12.3英寸全液晶儀錶以及9.2英寸多媒體显示屏幕(注:只有這款車型可選擇)。特別是全液晶儀錶,真的太帥了,可是不用問也知道,在4S選裝這個配置,怎麼也得1W+吧。

車主口碑:最滿意的當然是動力了,還能體驗到推背感,甚至說有點點刺激。。。前排空間是很充足的,後排就不知道了,很少用。內飾做工用料也很滿意,質感跟奧迪的車差不多,非常棒。(2018款280TSI 自動旗艦型)

2018款 高爾夫 R-Line

指導價:15.79-17.19萬

R-line系列主要是基於280TSI車型對外觀和內飾進行運動風格的升級,比如前包圍的設計、輪轂造型都更運動化,和更年輕化。但是整體造型也不及高爾夫GTI系列的激進,就如同一件純白色的襯衫印了一些很酷的標識在上面,整體很平淡自然,細節又很個性特別。

內飾主要特點在於方向盤、座椅靠背等配有“R-line”的標識,但可惜的是R-line系列車型不能選配全液晶儀錶和9.2英寸多媒體显示屏幕。

車主口碑:最滿意的肯定是外觀,全LED大燈,流水尾燈,以及R-line的運動包圍,17寸的輪轂,外觀可以說是完美的。動力也很滿意,150馬力的1.4T發動機會讓你經常想暴力駕駛它,超車也很簡單,操控起來很靈活。最不滿意可能是隔音了。(2018款 280TSI 自動R-Line型)

2018款 高爾夫 GTI

指導價:23.99萬

現款高爾夫GTI中網格柵的紅色裝飾線非常搶眼,與前大燈內部的紅色裝飾線相互呼應,在搭配專屬的五輻戰斧式輪圈,可謂殺氣十足啊。

而內飾方面,最吸引人的當然是Clark格子布座椅,這可是一項極具情懷的設計,多少人為之着迷。還有就是標配了全液晶儀錶和8英寸多媒體显示屏幕,9.2英寸的仍然要選配。

車主口碑:外觀非常激進,全車LED燈源、運動包圍、雙出排氣、18寸戰斧輪轂,實在太帥。經典的格子面料回歸讓這一代GTI更有情話。但是,動力才是最讓人興奮的,220匹根本用不完。(2018款 2.0TSI GTI)

2017款 高爾夫 R

指導價:40.78萬

雖然2018款高爾夫R早已在海外上市了,可是目前為止,官方還沒正式引進中國,在國內只能買到2017款或更老的車型。至於什麼時候引進2018款,我們不得而知。但是,2017款的高爾夫R仍然是高爾夫車迷的信仰。

外觀方面其實比現款的GTI要低調得多,可是說是真正意義上“扮豬吃老虎”的車型。不過內飾同樣也很具情懷,比如發藍光的儀錶等。

車主口碑:最滿意的只有性能,其它方面的表現都不是這個價位的水平,只有踩下油門,聽到源源不斷的轟鳴才覺得這車很值。(2017款 2.0TSI R)

總結

看了這麼多款高爾夫之後,你是否有選到你心動的哪款?或者說你已經是高爾夫的車主,歡迎在下面的評論區分析你的用車感受哦。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

設計模式系列之工廠模式三兄弟(Factory Pattern)

說明:設計模式系列文章是讀劉偉所著《設計模式的藝術之道(軟件開發人員內功修鍊之道)》一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的博客https://blog.csdn.net/LoveLion/article/details/17517213

工廠模式是最常用的一類創建型設計模式,通常我們所說的工廠模式是指工廠方法模式,它也是使用頻率最高的工廠模式。簡單工廠模式是工廠方法模式的“小弟”,它不屬於GoF23種設計模式,但在軟件開發中應用也較為頻繁,通常將它作為學習其他工廠模式的入門。此外,工廠方法模式還有一位“大哥”——抽象工廠模式。這三種工廠模式各具特色,難度也逐個加大,在軟件開發中它們都得到了廣泛的應用,成為面向對象軟件中常用的創建對象的工具。

簡單工廠模式

簡單工廠模式並不屬於GoF 23個經典設計模式,但通常將它作為學習其他工廠模式的基礎,它的設計思想很簡單。

模式定義

簡單工廠模式(Simple Factory Pattern):定義一個工廠類,它可以根據參數的不同返回不同類的實例,被創建的實例通常都具有共同的父類。因為在簡單工廠模式中用於創建實例的方法是靜態(static)方法,因此簡單工廠模式又被稱為靜態工廠方法(Static Factory Method)模式,它屬於類創建型模式。

簡單工廠模式的要點在於:當你需要什麼,只需要傳入一個正確的參數,就可以獲取你所需要的對象,而無須知道其創建細節。

模式結構圖

簡單工廠模式結構圖如下所示:

模式偽代碼

在使用簡單工廠模式時,首先需要對產品類進行重構,不能設計一個包羅萬象的產品類,而需根據實際情況設計一個產品層次結構,將所有產品類公共的代碼移至抽象產品類,並在抽象產品類中聲明一些抽象方法,以供不同的具體產品類來實現,典型的抽象產品類代碼如下所示:

public abstract class Product {
    // 所有產品的公共屬性

    // 所有產品類的公共業務方法
    public void methodSame() {
        //公共方法的實現
    }

    // 聲明抽象業務方法
    public abstract void methodDiff();
}

具體產品類中實現了抽象產品類中聲明的抽象業務方法。

public class ConcreteProduct extends Product {
    @Override
    public void methodDiff() {
        // 具體產品業務方法的實現
    }
}

簡單工廠模式的核心是工廠類,在沒有工廠類之前,客戶端一般會使用new關鍵字來直接創建產品對象,而在引入工廠類之後,客戶端可以通過工廠類來創建產品,在簡單工廠模式中,工廠類提供了一個靜態工廠方法供客戶端使用,根據所傳入的參數不同可以創建不同的產品對象,典型的工廠類代碼如下所示:

public class Factory {
    //靜態工廠方法
    public static Product getProduct(String arg) {
        Product product = null;
        if (arg.equalsIgnoreCase("A")) {
            product = new ConcreteProductA();
            //初始化設置product
        } else if (arg.equalsIgnoreCase("B")) {
            product = new ConcreteProductB();
            //初始化設置product
        }
        return product;
    }
}

客戶端代碼中,我們通過調用工廠類的工廠方法即可得到產品對象,典型代碼如下所示:

public class Client {
    public static void main(String[] args) {
        Product product;
        product = Factory.getProduct("A"); //通過工廠類創建產品對象
        product.methodSame();
        product.methodDiff();
    }
}

模式簡化

有時候,為了簡化簡單工廠模式,我們可以將抽象產品類工廠類合併,將靜態工廠方法移至抽象產品類中,如下圖所示。

客戶端可以通過產品父類的靜態工廠方法,根據參數的不同創建不同類型的產品子類對象,這種做法在JDK等類庫和框架中也廣泛存在。
比如:java.nio.charset.Charset

public abstract class Charset {

    /**
     * Returns a charset object for the named charset.
     */
    public static Charset forName(String charsetName) {
        java.nio.charset.Charset cs = lookup(charsetName);
        if (cs != null)
            return cs;
        throw new UnsupportedCharsetException(charsetName);
    }
}

模式小結

簡單工廠模式提供了專門的工廠類用於創建對象,將對象的創建和對象的使用分離開,它作為一種最簡單的工廠模式在軟件開發中得到了較為廣泛的應用。

使用場景:

  1. 工廠類負責創建的對象比較少,由於創建的對象較少,不會造成工廠方法中的業務邏輯太過複雜。
  2. 客戶端只知道傳入工廠類的參數,對於如何創建對象並不關心。

工廠方法模式

簡單工廠模式雖然簡單,但存在一個很嚴重的問題。當系統中需要引入新產品時,由於靜態工廠方法通過所傳入參數的不同來創建不同的產品,這必定要修改工廠類的源代碼,將違背“開閉原則”,如何實現增加新產品而不影響已有代碼?工廠方法模式應運而生。

模式定義

工廠方法模式中,我們不再提供一個統一的工廠類來創建所有的產品對象,而是針對不同的產品提供不同的工廠,系統提供一個與產品等級結構對應的工廠等級結構。工廠方法模式定義如下:

工廠方法模式(Factory Method Pattern):定義一個用於創建對象的接口,讓子類決定將哪一個類實例化。工廠方法模式讓一個類的實例化延遲到其子類。工廠方法模式又簡稱為工廠模式(Factory Pattern),又可稱作虛擬構造器模式(Virtual Constructor Pattern)或多態工廠模式(Polymorphic Factory Pattern)。工廠方法模式是一種類創建型模式。

模式結構圖

工廠方法模式提供一個抽象工廠接口來聲明抽象工廠方法,而由其子類來具體實現工廠方法,創建具體的產品對象。工廠方法模式結構如圖所示:

在工廠方法模式結構圖中包含如下幾個角色:

  • Product(抽象產品):它是定義產品的接口,是工廠方法模式所創建對象的超類型,也就是產品對象的公共父類。
  • ConcreteProduct(具體產品):它實現了抽象產品接口,某種類型的具體產品由專門的具體工廠創建,具體工廠和具體產品之間一一對應。
  • Factory(抽象工廠):在抽象工廠類中,聲明了工廠方法(Factory Method),用於返回一個產品。抽象工廠是工廠方法模式的核心,所有創建對象的工廠類都必須實現該接口。
  • ConcreteFactory(具體工廠):它是抽象工廠類的子類,實現了抽象工廠中定義的工廠方法,並可由客戶端調用,返回一個具體產品類的實例。

模式偽代碼

與簡單工廠模式相比,工廠方法模式最重要的區別是引入了抽象工廠角色,抽象工廠可以是接口,也可以是抽象類或者具體類,其典型代碼如下所示:

public interface Factory {
    Product factoryMethod();
}

在抽象工廠中聲明了工廠方法但並未實現工廠方法,具體產品對象的創建由其子類負責,客戶端針對抽象工廠編程,可在運行時再指定具體工廠類,具體工廠類實現了工廠方法,不同的具體工廠可以創建不同的具體產品,其典型代碼如下所示:

public class ConcreteFactory implements Factory {
    @Override
    public Product factoryMethod() {
        return new ConcreteProduct();
    }
}

在客戶端代碼中,只需關心工廠類即可,不同的具體工廠可以創建不同的產品,典型的客戶端類代碼片段如下所示:

public class Client {
    public static void main(String[] args) {
        // 確定是哪個工廠可得到產品
        Factory factory = new ConcreteFactory();
        // 獲取產品
        Product product = factory.factoryMethod();
    }
}

模式簡化

有時候,為了進一步簡化客戶端的使用,還可以對客戶端隱藏工廠方法,此時,在工廠類中將直接調用產品類的業務方法,客戶端無須調用工廠方法創建產品,直接通過工廠即可使用所創建的對象中的業務方法。

// 改為抽象類
public class AbstractFactory {
    // 在工廠類中直接調用產品類的業務方法
    public void productMethod() {
        Product product = this.createProduct();
        product.method();
    }

    public abstract Product createProduct();
}

通過將業務方法的調用移入工廠類,可以直接使用工廠對象來調用產品對象的業務方法,客戶端無須直接調用工廠方法,在客戶端並不關心Product細節的情況下使用這種設計方案會更加方便。

模式小結

工廠方法模式能夠讓工廠可以自主確定創建何種產品對象,而如何創建這個對象的細節則完全封裝在具體工廠內部,用戶只需要關心所需產品對應的工廠,無須關心創建細節,甚至無須知道具體產品類的類名。基於工廠角色產品角色的多態性設計是工廠方法模式的關鍵。

抽象工廠模式

工廠方法模式通過引入工廠等級結構,解決了簡單工廠模式中工廠類職責太重的問題,但由於工廠方法模式中的每個工廠只生產一類產品,可能會導致系統中存在大量的工廠類,勢必會增加系統的開銷。此時,我們可以考慮將一些相關的產品組成一個產品族,由同一個工廠來統一生產,這就是我們本文將要學習的抽象工廠模式的基本思想。

這裏我斗膽舉個例子來說明一下吧,如果不恰當歡迎指出。

眾所周知,國內知名的電器廠有海爾、海信(姑且就認為是2個),電器廠會生產電視機、電冰箱、空調(姑且就認為是3種產品)。

  • 使用工廠方法模式:工廠方法模式中每個工廠只生產一類產品,那麼就必須要有海爾電視機廠海爾電冰箱廠海爾空調廠海信電視機廠海信電冰箱廠海信空調廠
  • 使用抽象工廠模式:抽象工廠中每個工廠生產由多種產品組成的”產品族”,那麼就只需要有海爾工廠海信工廠就夠了,每個工廠可生產自家的電視機、電冰箱、空調。

由此看出使用抽象工廠模式極大地減少了系統中類的個數。

模式定義

抽象工廠模式為創建一組對象提供了一種解決方案。與工廠方法模式相比,抽象工廠模式中的具體工廠不只是創建一種產品,它負責創建一族產品。抽象工廠模式定義如下:

抽象工廠模式(Abstract Factory Pattern):提供一個創建一系列相關或相互依賴對象的接口,而無須指定它們具體的類。抽象工廠模式又稱為Kit模式,它是一種對象創建型模式。

模式結構圖

在抽象工廠模式中,每一個具體工廠都提供了多個工廠方法用於產生多種不同類型的產品,這些產品構成了一個產品族,抽象工廠模式結構如圖所示:

在抽象工廠模式結構圖中包含如下幾個角色:

  • AbstractFactory(抽象工廠):它聲明了一組用於創建一族產品的方法,每一個方法對應一種產品。
  • ConcreteFactory(具體工廠):它實現了在抽象工廠中聲明的創建產品的方法,生成一組具體產品,這些產品構成了一個產品族,每一個產品都位於某個產品等級結構中。
  • AbstractProduct(抽象產品):它為每種產品聲明接口,在抽象產品中聲明了產品所具有的業務方法。
  • ConcreteProduct(具體產品):它定義具體工廠生產的具體產品對象,實現抽象產品接口中聲明的業務方法。

模式偽代碼

在抽象工廠中聲明了多個工廠方法,用於創建不同類型的產品,抽象工廠可以是接口,也可以是抽象類或者具體類,其典型代碼如下所示:

public abstract class AbstractFactory {

    public abstract AbstractProductA createProductA();

    public abstract AbstractProductB createProductB();

    public abstract AbstractProductC createProductC();
}

具體工廠實現了抽象工廠,每一個具體的工廠方法可以返回一個特定的產品對象,而同一個具體工廠所創建的產品對象構成了一個產品族。對於每一個具體工廠類,其典型代碼如下所示:

public class ConcreteFactory1 extends AbstractFactory {
    @Override
    public AbstractProductA createProductA() {
        return new ConcreteProductA1();
    }

    @Override
    public AbstractProductB createProductB() {
        return new ConcreteProductB1();
    }

    @Override
    public AbstractProductC createProductC() {
        return new ConcreteProductC1();
    }
}

模式小結

如果一開始就學習抽象工廠模式估計很難理解為什麼這樣設計,按次序學習分析簡單工廠模式工廠方法模式抽象工廠模式基本就順理成章了。實際開發中,可能並不是照搬照套工廠模式三兄弟的偽代碼,大多會簡化其中的部分實現。本來學習設計模式就是重思想,學習如何用抽象類、接口、拆分、組合等將軟件解耦合,並增強系統可擴展性,這才是最關鍵的。

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

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

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

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

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

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

vue-toy: 200行代碼模擬Vue實現

vue-toy

200行左右代碼模擬vue實現,視圖渲染部分使用React來代替Snabbdom,歡迎Star。
項目地址:https://github.com/bplok20010/vue-toy

codesandbox示例

已實現的參數:

interface Options {
    el: HTMLElement | string;
	propsData?: Record<string, any>;
	props?: string[];
	name?: string;
	data?: () => Record<string, any>;
	methods?: Record<string, (e: Event) => void>;
	computed?: Record<string, () => any>;
	watch?: Record<string, (newValue: any, oldValue: any) => any>;
	render: (h: typeof React.createElement) => React.ReactNode;
	renderError?: (h: typeof React.createElement, error: Error) => React.ReactNode;
	mounted?: () => void;
	updated?: () => void;
	destroyed?: () => void;
	errorCaptured?: (e: Error, vm: React.ReactInstance) => void;
}

示例:

import Vue from "vue-toy";

const Hello = Vue.component({
	render(h){
		return h('span', null, 'vue-toy') ;
	}
})

new Vue({
  el: document.getElementById("root"),
  data() {
    return {
      msg: "hello vue toy"
    };
  },
  render(h) {
    return h("h1", null, this.msg, h(Hello));
  }
});

基本原理

官方原理圖:

實現基本步驟:

  1. 使用Observable創建觀察對象
  2. 定義好視圖既render函數
  3. 收集視圖依賴,並監聽依賴屬性
  4. 渲染視圖
  5. 重複3-4
// 創建觀察對象
// 觀察對象主要使用的是Object.defineProperty或Proxy來實現,
const data = observable({
    name: 'vue-toy',
});

// 渲染模版
const render = function(){
    return <h1>{data.name}</h1>
}

// 計算render的依賴屬性,
// 依賴屬性改變時,會重新計算computedFn,並執行監控函數watchFn,
// 屬性依賴計算使用棧及可以了。
// watch(computedFn, watchFn);
watch(render, function(newVNode, oldVNode){
    update(newVNode, mountNode);
});

//初始渲染
mount(render(), mountNode);

// 改變觀察對象屬性,如果render依賴了該屬性,則會重新渲染
data.name = 'hello vue toy';

視圖渲染部分(既render)使用的是vdom技術,vue使用Snabbdom庫,vue-toy使用的是react來進行渲染,所以在render函數里你可以直接使用React的JSX語法,不過別忘記import React from 'react',當然也可以使用preact inferno 等 vdom庫。

由於vue的template的最終也是解析並生成render函數,模版的解析可用htmleParser庫來生成AST,剩下就是解析指令並生產代碼,由於工作量大,這裏就不具體實現,直接使用jsx。

響應式實現

一個響應式示例代碼:

const data = Observable({
	name: "none",
});

const watcher =new Watch(
	data,
	function computed() {
		return "hello " + this.name;
	},
	function listener(newValue, oldValue) {
		console.log("changed:", newValue, oldValue);
	}
);
// changed vue-toy none
data.name = "vue-toy";

Observable實現

源碼
觀察對象創建這裏使用Proxy實現,示例:

function Observable(data) {
	return new Proxy(data, {
		get(target, key) {
			return target[key];
		},
		set(target, key, value) {
			target[key] = value;
			return true;
		},
	});
}

這就完成了一個對象的觀察,但以上示例代碼雖然能觀察對象,但無法實現對象屬性改動后通知觀察者,這時還缺少Watch對象來計算觀察函數的屬性依賴及Notify來實現屬性變更時的通知。

Watch實現

源碼

定義如下:

Watch(data, computedFn, watchFn);
  • data 為 computedFn 的 上下文 既 this 非必須
  • computedFn 為觀察函數並返回觀察的數據,Watch會計算出裏面的依賴屬性。
  • watchFn 當computedFn 返回內容發生改變時,watchFn會被調用,同時接收到新、舊值

大概實現如下:

// Watch.js
// 當前正在收集依賴的Watch
const CurrentWatchDep = {
    current: null,
};
class Watch {
    constructor(data, exp, fn) {
        this.deps = []; 
        this.watchFn = fn;
        this.exp =  () => {
                    return exp.call(data);
                };
        // 保存上一個依賴收集對象
        const lastWatchDep = CurrentWatchDep.current;
        // 設置當前依賴收集對象
        CurrentWatchDep.current = this;
        // 開始收集依賴,並獲取觀察函數返回的值
        this.last = this.exp();
        // 還原
        CurrentWatchDep.current = lastWatchDep;
    }
    clearDeps() {
        this.deps.forEach((cb) => cb());
        this.deps = [];
    }
    // 監聽依賴屬性的改動,並保存取消回調
    addDep(notify) {
        // 當依賴屬性改變時,重新觸發依賴計算
        this.deps.push(notify.sub(() => {
            this.check();
        }));
    }
    // 重新執行依賴計算
    check() {
        // 清空所有依賴,重新計算
        this.clearDeps();
        // 作用同構造函數
        const lastWatchDep = CurrentWatchDep.current;
        CurrentWatchDep.current = this;
        const newValue = this.exp();
        CurrentWatchDep.current = lastWatchDep;
        const oldValue = this.last;
        // 對比新舊值是否改變
        if (!shallowequal(oldValue, newValue)) {
            this.last = newValue;
            // 調用監聽函數
            this.watchFn(newValue, oldValue);
        }
    }
}

Notify實現

觀察對象發生改變后需要通知監聽者,所以還需要實現通知者Notify:

class Notify {
    constructor() {
        this.listeners = [];
    }
    sub(fn) {
        this.listeners.push(fn);
        return () => {
            const idx = this.listeners.indexOf(fn);
            if (idx === -1)
                return;
            this.listeners.splice(idx, 1);
        };
    }
    pub() {
        this.listeners.forEach((fn) => fn());
    }
}

調整Observable

前面的Observable太簡單了,無法完成屬性計算的需求,結合上面Watch Notify的來調整下Observable。

function Observable(data) {
	const protoListeners = Object.create(null);
	// 給觀察數據的所有屬性創建一個Notify
	each(data, (_, key) => {
		protoListeners[key] = new Notify();
	});
	return new Proxy(data, {
		get(target, key) {
			// 屬性依賴計算
			if (CurrentWatchDep.current) {
				const watcher = CurrentWatchDep.current;
				watcher.addDep(protoListener[key]);
			}
			return target[key];
		},
		set(target, key, value) {
			target[key] = value;
			if (protoListeners[key]) {
				// 通知所有監聽者
				protoListeners[key].pub();
			}
			return true;
		},
	});
}

好了,觀察者的創建和訂閱都完成了,開始模擬Vue。

模擬Vue

vue-toy 使用React來實現視圖的渲染,所以render函數里如果使用JSX則需要引入React

準備

既然已經實現了Observable和Watch,那我們就來實現基本原理的示例:

codesandbox示例

import Observable from "vue-toy/cjs/Observable";
import Watch from "vue-toy/cjs/Watch";

function mount(vnode) {
  console.log(vnode);
}

function update(vnode) {
  console.log(vnode);
}

const data = Observable({
  msg: "hello vue toy!",
  counter: 1
});

function render() {
  return `render: ${this.counter} | ${this.msg}`;
}

new Watch(data, render, update);

mount(render.call(data));

setInterval(() => data.counter++, 1000);
// 在控制台可看到每秒的輸出信息

這時將mount update的實現換成vdom就可以完成一個基本的渲染。

但這還不夠,我們需要抽象並封裝成組件來用。

Component

源碼

這裏的Component像是React的高階函數HOC,使用示例:

const Hello = Component({
	props: ["msg"],
	data() {
		return {
			counter: 1,
		};
	},
	render(h) {
		return h("h1", null, this.msg, this.counter);
	},
});

大概實現如下,options 參考文章開頭

function Component(options) {
	return class extends React.Component {
	    // 省略若干...
		constructor(props) {
			super(props);
			// 省略若干...
			// 創建觀察對象
			this.$data = Observable({ ...propsData, ...methods, ...data }, computed);
			// 省略若干...
			// 計算render依賴並監聽
			this.$watcher = new Watch(
				this.$data,
				() => {
					return options.render.call(this, React.createElement);
				},
				debounce((children) => { 
					this.$children = children;
					this.forceUpdate();
				})
			);
			this.$children = options.render.call(this, React.createElement);
		}
		shouldComponentUpdate(nextProps) {
			if (
				!shallowequal(
					pick(this.props, options.props || []),
					pick(nextProps, options.props || [])
				)
			) {
				this.updateProps(nextProps);
				this.$children = options.render.call(this, React.createElement);
				return true;
			}
			return false;
		}
        // 生命周期關聯
		componentDidMount() {
			options.mounted?.call(this);
		}

		componentWillUnmount() {
			this.$watcher.clearDeps();
			options.destroyed?.call(this);
		}

		componentDidUpdate() {
			options.updated?.call(this);
		}

		render() {
			return this.$children;
		}
	};
}

創建主函數 Vue

最後創建入口函數Vue,實現代碼如下:

export default function Vue(options) {
	const RootComponent = Component(options);
	let el;
	if (typeof el === "string") {
		el = document.querySelector(el);
	}

	const props = {
		...options.propsData,
		$el: el,
	};

	return ReactDOM.render(React.createElement(RootComponent, props), el);
}
Vue.component = Component;

好了,Vue的基本實現完成了。

感謝閱讀。

最後,歡迎Star:https://github.com/bplok20010/vue-toy

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

震驚!ConcurrentHashMap裏面也有死循環,作者留下的“彩蛋”了解一下?

JDK BUG

這篇文章,聊一下我最近才知道的一個關於 JDK 8 的 BUG 吧。

首先說一下我是怎麼發現這個 BUG 的呢?

大家都知道我對 Dubbo 有一定的關注,前段時間 Dubbo 2.7.7 發布后我看了它的更新點,就是下面這個網址: https://github.com/apache/dubbo/releases/tag/dubbo-2.7.7

其中有 Bugfixes 這一部分:

每一個我都去簡單的看了一下,其他的 Bugfixes 或多或少都和 Dubbo 框架有一定的關聯性。但是上面紅框框起來的部分完全就是 JDK 的 Bug 了。

所以可以單獨拎出來說。

這個 Bug 我也是看到了這個地方才知道的,但是研究的過程中我發現,這個怎麼說呢:我懷疑這根本就不是 Bug ,這就是 Doug Lea 老爺子在釣魚執法。

為什麼這樣的說呢,大家看完本文就知道了。

Bug 穩定復現

點擊 Dubbo 裏面的鏈接,我們可以看到具體的描述就是一個鏈接:

打開這個鏈接:

https://bugs.openjdk.java.net/browse/JDK-8062841

我們可以看到:這個 Bug 是位於大名鼎鼎的 concurrent 包裏面的 computeIfAbsent 方法。

這個 Bug 在 JDK 9 裏面被修復了,修復人是 Doug Lea。

而我們知道 ConcurrentHashMap 就是 Doug Lea 的大作,可以說是“誰污染誰治理”。

要了解這個 Bug 是怎麼回事,就必須先了解下面這個方法是幹啥的:

java.util.concurrent.ConcurrentHashMap#computeIfAbsent

從這個方法的第二個入參 mappingFunction 我們可以知道這是 JDK 8 之後提供的方法了。

該方法的含義是:當前 Map 中 key 對應的值不存在時,會調用 mappingFunction 函數,並且將該函數的執行結果(不為 null)作為該 key 的 value 返回。

比如下面這樣的:

初始化一個 ConcurrentHashMap ,然後第一次去獲取 key 為 why 的 value,沒有獲取到,直接返回 null。

接着調用 computeIfAbsent 方法,獲取到 null 后調用 getValue 方法,將該方法的返回值和當前的 key 關聯起來。

所以,第二次獲取的時候拿到了 “why技術”。

其實上面的代碼的 17 行的返回值就是 “why技術”,只是我為了代碼演示,再去調用了一次 map.get() 方法。

知道這個方法干什麼的,接下來就帶大家看看 Bug 是什麼。

我們直接用這個問題裏面給的測試用例,地址:

https://bugs.openjdk.java.net/secure/attachment/23985/Main.java

我只是在第 11 行和第 21 行加入了輸出語句:

正常的情況下,我們希望方法正常結束,然後 map 裏面是這樣的:{AaAa=42,BBBB=42}

但是你把這個代碼拿到本地去跑(需要 JDK 8 環境),你會發現,這個方法永遠不會結束。因為它在進行死循環。

這就是 Bug。

提問的藝術

知道 Bug 了,按理來說就應該開始分析源碼,了解為啥出現了會出現這個 Bug。

但是我想先插播一小節提問的藝術。因為這個 Bug 就是一個活生生的示例呀。

這個鏈接,我建議你打開看看,這裏面還有 Doug Lea 老爺子的親自解答:

https://bugs.openjdk.java.net/browse/JDK-8062841

首先我們看提出問題的這個人對於問題的描述(可以先不用細看,反正看着也是懵逼的):

通常情況下,被提問的人分為兩類人:

1.遇到過並知道這個問題的人,可以看的明白你在說什麼。

2.雖然沒有碰見過這個問題,但感覺是自己熟悉的領域,可能知道答案,但是看了你的問題描述,也不知道你在說什麼。

這個描述很長,我第一次看的時候很懵逼,很難理解他在說什麼。我就是屬於第二類人。

而且在大多數的問題中,第二類人比第一類人多很多。

但是當我了解到這個 Bug 的來龍去脈的時候,再看這個描述,其實寫的很清楚了,也很好理解。我就變成第一類人了。

但是變成第一類人是有前提的,前提就是我已經了解到了這個地方 Bug 了。可惜,現在是提問,而被提問的人,還對這個 Bug 不是特別了解。

即使,這個被提問的人是 Doug Lea。

可以看到,2014 年 11 月 04 日 Martin 提出這個問題后, Doug Lea 在不到一個小時內就進行了回復,我給大家翻譯一下,老爺子回復的啥:

首先,你說你發現了 ConcurrentHashMap 的問題,但是我沒有看到的測試用例。那麼我就猜測一下是不是有其他線程在計算值的時候被卡住了,但是從你的描述中我也看不到相應的點。

簡單來說就是:Talk is cheap. Show me the code.(屁話少說,放碼過來。)

於是另一個哥們 Pardeep 在一個月後提交了一個測試案例,就是我們前面看到的測試案例:

Pardeep 給 Martin 回復到下面這段話:

他開門見山的說:我注意這個 bug 很長時間了,然後我還有一個測試用例。

可以說這個測試案例的出現,才是真正的轉折點。

然後他提出了自己的看法,這段描述簡短有力的說出了問題的所在(後面我們會講到),然後他還提出了自己的意見。

不到一個小時,這個回到得到了 Doug Lea 的回復:

他說:小伙子的建議還是不錯的,但是現在還不是我們解決這個問題的時候。我們也許會通過代碼改進死鎖檢查機制,以幫助用戶 debug 他們的程序。但是目前而言,這種機制就算做出來,工作效率也是非常低下的,比如在當前的這個案例下。但是現在我們至少清楚的知道,是否要實現這種機制是不能確定的。

總之一句話:問題我知道了,但是目前我還沒想到好的解決方法。

但是,在 19 天以後,老爺子又回來處理這個問題了:

這次的回答可謂是峰迴路轉,他說:請忽略我之前的話。我們發現了一些可行的改進方法,這些改進可以處理更多的用戶錯誤,包括本報告中所提供的測試用例,即解決在 computeIfAbsent 中提供的函數中進行遞歸映射更新導致死鎖這樣的問題。我們會在 JDK 9 裏面解決這個問題。

所以,回顧這個 Bug 被提出的過程。

首先是 Martin 提出了這個問題,並進行了詳細的描述。可惜的是他的描述很專業,是站在你已經了解了這個 Bug 的立場上去描述的,讓人看的很懵逼。

所以 Doug Lea 看到后也表示這啥呀,沒搞懂。

然後是 Pardeep 跟進這個問題,轉折點在於他拋出的這個測試案例。而我相信,既然 Martin 能把這個問題描述的很清楚,他一定是有一個自己的測試案例的,但是他沒有展現出來。

所以,朋友們,測試案例的重要性不言而喻了。問問題的時候不要只是拋出異常,你至少給段對應的代碼,或者日誌,或者一次性描述清楚,寫在文檔裏面發出來也行呀。

Bug 的原因

導致這個 Bug 的原因也是一句話就能說清楚,前面的 Pardeep 老哥也說了:

問題在於我們在進行 computeIfAbsent 的時候,裏面還有一個 computeIfAbsent。而這兩個 computeIfAbsent 它們的 key 對應的 hashCode 是一樣的。

你說巧不巧。

當它們的 hashCode 是一樣的時候,說明它們要往同一個槽放東西。

而當第二個元素進來的時候,發現坑位已經被前一個元素佔領了,可能就是這樣的畫風:

接下來我們就解析一下 computeIfAbsent 方法的工作流程:

第一步是計算 key 對應的 hashCode 應該放到哪個槽裏面。

然後是進入1649 行的這個 for 循環,而這個 for 循環是一個死循環,它在循環體內部判斷各種情況,如果滿足條件則 break 循環。

首先,我們看一下 “AaAa” 和 “BBBB” 經過 spread 計算(右移 16 位高效計算)后的 h 值是什麼:

哇塞,好巧啊,從框起來的這兩部分可以看到,都是 2031775 呢。

說明他們要在同一個槽裏面搞事情。

先是 “AaAa” 進入 computeIfAbsent 方法:

在第一次循環的時候 initTable,沒啥說的。

第二次循環先是在 1653 行計算出數組的下標,並取出該下標的 node。發現這個 node 是空的。於是進入分支判斷:

在標號為 ① 的地方進行 cas 操作,先用 r(即 ReservationNode)進行一個佔位的操作。

在標號為 ② 的地方進行 mappingFunction.apply 的操作,計算 value 值。如果計算出來不為 null,則把 value 組裝成最終的 node。

在標號為 ③ 的東西把之前佔位的 ReservationNode 替換成標號為 ② 的地方組裝成的node 。

問題就出現標號為 ② 的地方。可以看到這裏去進行了 mappingFunction.apply 的操作,而這個操作在我們的案例下,會觸發另一次 computeIfAbsent 操作。

現在 “AaAa” 就等着這個 computeIfAbsent 操作的返回值,然後進行下一步操作,也就是進行標號為 ③ 的操作了。

接着 “BBBB” 就來了。

通過前面我們知道了 “BBBB” 的 hashCode 經過計算后也是和 “AaAa” 一樣。所以它也要想要去那個槽裏面搞事情。

可惜它來晚了一步。

帶大家看一下對應的代碼:

當 key 為 “BBBB” 的時候,算出來的 h 值也是 2031775。

它也會進入 1649 行的這個死循環。然後進行各種判斷。

接下來我要論證的是:

在本文的示例代碼中,當運行到 key 為 “BBBB” 的時候,進入 1649 行這個死循環后,就退不出來了。程序一直在裏面循環運行。

在標號為 ① 的地方,由於這個時候 tab 已經不為 null 了,所以不會進入這個分支。

在標號為 ② 的地方,由於之前 “AaAa” 已經扔了一個 ReservationNode 進去佔位置了,所以不等於 null。所以,也就不會進入這個分支。

怕你懵逼,給你配個圖,真是暖男作者石錘了:

接下來到標號為 ③ 的地方,裏面有一個 MOVED,這個 MOVED 是幹啥的呢?

表示當前的 ConcurrentHashMap 是否是在進行擴容。

很明顯,現在還沒有到該擴容的時候:

第 1678 行的 f 就是之前 “AaAa” 扔進去的 ReservationNode ,這個 Node 的 hash 是 -3,不等於MOVED(-1)。

所以,不會進入這個分支判斷。

接下來,能進的只有標號為 ④ 的地方了,所以我們只需要把這個地方攻破,就徹底了解這個 Bug 了。

走起:

通過前面的分析我們知道了,當前案例情況下,只會進入 1672 行這個分支。

而這個分支裏面,還有四個判斷。我們一個個的攻破:

標號為 ⑤ 的地方,tabAt 方法取出來的對象,就是之前 “AaAa” 放進去的佔位的 ReservationNode ,也就是這個 f 。所以可以進入這個分支判斷。

標號為 ⑥ 的地方,fh >=0 。而 fh 是當前 node 的 hash 值,大於 0 說明當前是按照鏈表存儲的數據。之前我們分析過了,當前的 hash 值是 -3。所以,不會進入這個分支。

標號為 ⑦ 的地方,判斷 f 節點是否是紅黑樹存儲。當然不是的。所以,不會進入這個分支。

標號為 ⑧ 的地方,binCount 代表的是該下標裏面,有幾個 node 節點。很明顯,現在一個都沒有。所以當前的 binCount 還是 0 。所以,不會進入這個分支。

完了。分析完了。

Bug 也就出來了,一次 for 循環結束后,沒有 break。苦就苦在這個 for 循環還是個死循環。

再來一個上帝視角,看看當 key 為 “BBBB” 的時候發生了什麼事情:

進入無限循環內:

①.經過 “AaAa” 之後,tab 就不為 null 了。

②.當前的槽中已經被 “AaAa” 先放了一個 ReservationNode 進行佔位了,所以不為 null。

③.當前的 map 並沒有進行擴容操作。

④.包含⑤、⑥、⑦、⑧。

⑤.tabAt 方法取出來的對象,就是之前 “AaAa” 放進去的佔位的 ReservationNode,所以滿足條件進入分支。

⑥.判斷當前是否是鏈表存儲,不滿足條件,跳過。

⑦.判斷當前是否是紅黑樹存儲,不滿足條件,跳過。

⑧.判斷當前下標裏面是否放了 node,不滿足條件(“AaAa” 只有個佔位的Node ,並沒有初始完成,所以還沒有放到該下標裏面),進入下一次循環。

然後它就在死循環裏面出不來了!

我相信現在大家對於這個 Bug 的來路了解清楚了。

如果你是在 idea 裏面跑這個測試用例,也可以這樣直觀的看一眼:

點擊這個照相機圖標:

從線程快照裏面其實也是可以看到端倪的,大家可以去分析分析。

有的觀點說的是由於線程安全的導致的死循環,經過分析我覺得這個觀點是不對的。

它存在死循環,不是由於線程安全導致的,純粹是自己進入了死循環。

或者說,這是一個“彩蛋”?

或者……自信點,就說這事 Bug ,能穩定復現的那種。

那麼我們如果是使用 JDK 8 怎麼避免踩到這個“彩蛋”呢?

看看 Dubbo 裏面是怎麼解決的:

先調用了 get 方法,如果返回為 null,則調用 putIfAbsent 方法,這樣就能實現和之前一樣的效果了。

如果你在項目中也有使用 computeIfAbsent 的地方,建議也這樣去修改。

說到 ConcurrentHashMap get 方法返回 null,我就想起了之前討論的一個面試題了:

答案都寫在這個文章裏面了,有興趣的可以了解一下《這道面試題我真不知道面試官想要的回答是什麼》

Bug 的解決 其實徹底理解了這個 Bug 之後,我們再來看一下 JDK 9 裏面的解決方案,看一下官方源碼對比:

http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/main/java/util/concurrent/ConcurrentHashMap.java?r1=1.258&r2=1.259&sortby=date&diff_format=f

就加了兩行代碼,判斷完是否是紅黑樹節點后,再判斷一下是否是 ReservationNode 節點,因為這個節點就是個佔位節點。如果是,則拋出異常。

就這麼簡單。沒有什麼神秘的。

所以,如果你在 JDK 9 裏面執行文本的測試用例,就會拋出 IllegalStateException。

這就是 Doug Lea 之前提到的解決方案:

了解了這個 Bug 的來龍去脈后,特別是看到解決方案后,我們就能輕描淡寫的說一句:

害,就這?沒聽說過!

另外,我看 JDK 9 修復的時候還不止修復了一個問題:

http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/6dd59c01f011/src/java.base/share/classes/java/util/concurrent/ConcurrentHashMap.java

你去翻一翻。發現,啊,全是知識點啊,學不動了。

釣魚執法

為什麼我在文章的一開始就說了這是 Doug Lea 在釣魚執法呢?

因為在最開始提問的藝術那一部分,我相信,Doug Lea 跑完那個測試案例之後,心裏也有點數了。

大概知道問題在哪了,而且從他的回答和他寫的文檔中我也有理由相信,他寫的這個方法的時候就知道可能會出問題。

而且,Pardeep 的回復中提到了文檔,那我們就去看看官方文檔對於該方法的描述是怎樣的:

https://docs.oracle.com/javase/8/docs/api/

文檔中說函數方法應該簡短,簡單。而且不能在更新的映射的時候更新映射。就是說不能套娃。

套娃,用程序說就是recursive(遞歸),按照文檔說如果存在遞歸,則會拋出 IllegalStateException 。

而提到遞歸,你想到了什麼?

我首先就想到了斐波拉契函數。我們用 computeIfAbsent 實現一個斐波拉契函數如下:

public class Test {

static Map<Integer, Integer> cache = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        System.out.println("f(" + 14 + ") =" + fibonacci(14));
    }

    static int fibonacci(int i) {
        if (i == 0)
            return i;
        if (i == 1)
            return 1;
        return cache.computeIfAbsent(i, (key) -> {
            System.out.println("Slow calculation of " + key);
            return fibonacci(i - 2) + fibonacci(i - 1);
        });
    }
}

這就是遞歸調用,我用 JDK 1.8 跑的時候並沒有拋出 IllegalStateException,只是程序假死了,原因和我們前面分析的是一樣一樣的。我理解這個地方是和文檔不符的。

所以,我懷疑是 Doug Lea 在這個地方釣魚執法。

CHM一定線程安全嗎?

既然都說到 currentHashMap(CHM)了,那我說一個相關的注意點吧。

首先 CHM 一定能保證線程安全嗎?

是的,CHM 本身一定是線程安全的。但是,如果你使用不當還是有可能會出現線程不安全的情況。

給大家看一點 Spring 中的源碼吧:

org.springframework.core.SimpleAliasRegistry

在這個類中,aliasMap 是 ConcurrentHashMap 類型的:

在 registerAlias 和 getAliases 方法中,都有對 aliasMap 進行操作的代碼,但是在操作之前都是用 synchronized 把 aliasMap 鎖住了。

為什麼?為什麼我們操作 ConcurrentHashMap 的時候還要加鎖呢?

這個是根據場景而定的,這個別名管理器,在這裏加鎖應該是為了避免多個線程操作 ConcurrentHashMap 。

雖然 ConcurrentHashMap 是線程安全的,但是假設如果一個線程 put,一個線程 get,在這個代碼的場景裏面是不允許的。

如果覺得不太好理解的話我舉一個 redis 的例子。

redis 的 get、set 方法都是線程安全的吧。但是你如果先 get 再 set,那麼在多線程的情況下還是會有問題的。

因為這兩個操作不是原子性的。所以 incr 就應運而生了。

我舉這個例子的是想說線程安全與否不是絕對的,要看場景。給你一個線程安全的容器,你使用不當還是會有線程安全的問題。

再比如,HashMap 一定是線程不安全的嗎?

說不能說的這麼死吧。它是一個線程不安全的容器。但是如果我的使用場景是只讀呢?

在這個只讀的場景下,它就是線程安全的。

總之,看場景。道理,就是這麼一個道理。

最後說兩句(求關注)

所以點個“贊”吧,周更很累的,不要白嫖我,需要一點正反饋。

才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言指出來,我對其加以修改。

感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

我是 why,一個被代碼耽誤的文學創作者,不是大佬,但是喜歡分享,是一個又暖又有料的四川好男人。

歡迎關注我的微信公眾號:why技術。在這裏我會分享一些java技術相關的知識,用匠心敲代碼,對每一行代碼負責。偶爾也會荒腔走板的聊一聊生活,寫一寫書評、影評。感謝你的關注,願你我共同進步。

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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