這些低價高配的合資SUV,最低僅11萬不到!

67萬元在我們的印象中,法國人總喜歡在造型設計上大下筆墨,怎麼不實用怎麼來,所以大家都覺得很少會有一款法系車能與“性價比”沾邊。但是。從天逸C5 AIRCROSS開始情況似乎就有點不一樣了。這款緊湊型SUV不僅完美繼承了法系車飄逸的設計風格,並且在配置和價格上都算得上實在,這對於預算吃緊又想得到更多的消費者來說可是個好事。

不知大家有沒有覺得,15萬左右的購車預算是最尷尬的,說多不多,說少不少,尤其是在挑選SUV車型的時候,如今國產+合資一大堆選擇,看得眼花繚亂。

而隨着國產SUV的崛起,不少人也在鼓吹買國產車就是“低價高配”,這也使得很多對汽車不怎麼了解的消費者錯過了一些優秀的合資車型。那麼今天就要給大家介紹幾款極具性價比的合資品牌SUV,讓大家知道所謂的“低價高配”並不是國產車的專屬。

1、北京現代 全新ix35

官方指導價:11.58-18.88萬元

作為一款合資品牌車型,ix35的價格的確是非常給力了,最入門的車型在有些4S店僅11萬不到,實在是令人意想不到。而參考現代車一直以來的耐用性,ix35可以說就是目前市面上你能買到的價格最低,且總體表現最穩定的合資緊湊型SUV了。

從外形上看,新款車型在歷經換代后整體風格更加陽剛硬朗了,甚至還有點小硬漢的感覺。內飾也同樣如此,採用全新的設計和布局,而方正的造型也明顯與外觀呼應。

配置方面,全系標配ESp、上坡輔助、后駐車雷達等,中配及以上車型則相應增加了胎壓監測、陡坡緩降、無鑰匙進入/啟動、9.6英寸中控液晶显示屏(帶倒車雷達/影像)、自動大燈與電加熱/摺疊后視鏡等等。

考慮到其中配車型13.99萬元的售價,以合資品牌的標準看,這配置已經足夠厚道了。

動力方面,ix35全系均搭載了傳統的2.0L自然吸氣發動機,傳動上匹配6速手動或6速手自一體變速箱。雖然數據並不亮眼,但勝在性能成熟穩定,並且後期的使用故障率和維護成本都較低,所以從家用車SUV的角度來看,ix35無疑是一款非常省錢又省心的車型。

2、東風雪鐵龍 天逸C5 AIRCROSS

官方指導價:15.27-23.67萬元

在我們的印象中,法國人總喜歡在造型設計上大下筆墨,怎麼不實用怎麼來,所以大家都覺得很少會有一款法系車能與“性價比”沾邊。

但是!從天逸C5 AIRCROSS開始情況似乎就有點不一樣了。這款緊湊型SUV不僅完美繼承了法系車飄逸的設計風格,並且在配置和價格上都算得上實在,這對於預算吃緊又想得到更多的消費者來說可是個好事。

首先進入到車內,第一感覺就是讓你懷疑這根本就不是一輛售價僅15萬元起的SUV,因為無論是獨具一格的內飾設計,還是雙色皮革的搭配,都讓你感受到實實在在的質感提升。

配置上就更不用說了,除入門版車型外,其餘車型均配備了有多個安全氣囊、胎壓監測、全景天窗、無鑰匙進入/啟動、皮質多功能方向盤、全液晶儀錶盤、全車車窗一鍵升降、電加熱/摺疊后視鏡,自動大燈與自動空調等等,應有盡有!

所以對於一款法系車來說,天逸C5 AIRCROSS的確在“性價比”方面打了一場漂亮的翻身仗,也證明了美貌和實力是可以兼得的。

3、上汽斯柯達 柯珞克

預計售價:13.88萬起

斯柯達也是一個容易被人國內消費者忽略的品牌,所以最後推薦的就是近期即將上市的一款入門緊湊型SUV-柯珞克,而13.88萬的預售價格也十分值得期待。

在外形上,柯珞克採用了與柯迪亞克相同的家族式設計語言,中庸的造型適合絕大部分消費者。而在車身尺寸上,新車長寬高為4432/1841/1614mm,軸距為2688mm,也就是比繽智、昂科拉都要稍大一些。

動力上,新車搭載的是1.2T和1.4T發動機,匹配7速DSG雙離合變速箱,想想不到15萬就能買到這樣一款德系品質(大眾平台)SUV,是不是覺得自己賺了?所以野帝停產後,柯珞克或將成為大眾集團在15萬元左右SUV市場的主力車型。

好了!以上就是今天給大家推薦的三款15萬元左右、極具性價比的合資品牌SUV。大家如果還有其他想了解的購車話題,歡迎留言評論,盡量滿足大家需求。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

同一款2.0T發動機,價格卻差了10來20萬!買了就坑?

其實大家也可以反過來想一下,如果光是靠發動機可以來衡量一款車是不是好車的話,那麼那麼吉利博越一直都被各路大神吐槽油耗高、小問題多,但依然不愁賣,這又該如何解釋。如今在網上還有一種說法:豪華車型不弄7、8、9個擋,都是為了省成本,不夠高大上,多擋位變速箱理應是豪華車型的標配。

在你心目中,最坑爹的車型是哪一款?



套娃的?

加價提車的?

買發動機送車的?

可是最近在網上竟然看到有人說最坑爹的竟然是一些豪車,說它們的發動機、變速箱都是通過購買而來的,可以說是“高級拼裝車”。其中像路虎極光、捷豹XFL、林肯MKZ等好幾款車型紛紛躺槍,究竟是怎麼回事?

被說“坑爹”的原因很簡單,因為這些車型都搭載了福特全新蒙迪歐的2.0T發動機,同時售價也要比福特全新蒙迪歐更高,因此成了“史上最坑爹的汽車”。

按照這樣的說法,如今在汽車市場上“坑爹”的車型可多了。像寶馬在2006年之前,他們就一直為路虎攬勝和發現3車型提供V8發動機,難不成用了寶馬發動機的路虎就不是路虎?會影響車型檔次?

但如果你想說“他們都是豪華品牌,不至於掉檔次”的話,那現款奧迪Q5又跟大眾途觀L相同的EA888 2.0T發動機,但奧迪Q5的價格要比大眾途觀L高出許多,你又怎麼看呢?

先不說品牌檔次,光是在一些調校、用料等一些內在品質方面,奧迪Q5的表現就比大眾途觀L更為出色,開起來的質感也會更舒適,而且它的安全性能也是得到了市場的檢驗,這就是它的價值所在!

其實大家也可以反過來想一下,如果光是靠發動機可以來衡量一款車是不是好車的話,那麼那麼吉利博越一直都被各路大神吐槽油耗高、小問題多,但依然不愁賣,這又該如何解釋?

如今在網上還有一種說法:豪華車型不弄7、8、9個擋,都是為了省成本,不夠高大上,多擋位變速箱理應是豪華車型的標配?

擋位多與少這個問題,已經不是第一次跟大家科普了,在這裏先不說多擋位的變速箱省不省油,但是拿變速箱擋位來衡量一款車是否上檔次,這完全是一種“病態”。

先來說說路虎極光,不少網友都知道它搭載了9AT變速箱,擋位數夠多吧?可是之前就被曝光過它的變速箱在D擋位存在故障問題,這是一個軟件調校的問題。

同樣,Jeep自由光也搭載了9AT變速箱,可不少車主紛紛表示“沒看見過它的第9擋”,即使把時速跑到120km/h也仍然在第8擋以下,第9擋如同雞肋。很顯然,這同樣是一個匹配、調校的問題。

通過這兩個例子,想說:如果高擋位成為雞肋或者換擋不平順,擋位數量再多都是沒意義的。現款的寶馬X1同為豪華車型,而且現在只搭載了6AT變速箱,加上它空間大、操控好、上檔次,而且口碑還不錯,你還在意它的擋位數量嗎?

再說了,如今像眾泰T700、陸風X7、傳祺GS4等車型,它們雖然並不是什麼高檔車型但都搭載了多擋位變速箱,這很顯然並不是高擋車型的“專屬”,一味的追求是毫無意義的。

此外,有些消費者還會關注到平台的問題,認為一款豪車跟普通車型採用相同的平台打造會顯得“廉價、有失身份”,其實這也是一個誤區!

就拿本田飛度跟謳歌CDX作例子,雖然兩款車都是基於同一個平台打造,但謳歌CDX在用料、舒適性等方面的表現會更加出色,而且它擁有跟飛度一樣的大空間和良好的操控。

但是相反過來,本田飛度能有謳歌CDX那樣舒適的乘坐和駕駛質感嗎?因此,“同平台”的意義並不單純只是降低研發成本,也能對提升車型品質起到一定的作用。

還有奧迪Q7跟大眾途昂,儘管它們都採用了大眾MQB平台進行打造,但是論豪華感、行駛質感等方面,兩款車依然有較大的差距。也就是說“同平台”並不代表工藝、用料、調校都是一樣的,只是共用了部分技術罷了。

如今在汽車市場上,共用發動機、變速箱,甚至採用同一個研發平台的車型還有很多,雖然最大的意義還是在於降低研發成本,但最核心的部分還是在匹配調校、用料方面。

SO~這並不能成為“坑爹、廉價、不上檔次”的代名詞,真正的一款車“好不好”、“值不值”並不能由誰說了算,只有自己親身體驗過,感受過覺得OK,而且價格也能接受,那才是最好、最適合的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

10萬買啥車?這5大終極問答能解決99.1%的選車難題!

還有划不划算。其實這裏還需考慮兩個問題:一、貨幣的貶值二、投資的收益第一點很簡單就是你今年花10萬元買的車這10萬元三年後可能就不值10萬元了又比如你貸10萬元買車這10萬元三年後可能也不值10萬元了可是這個必須考慮到貨幣的貶值率而按現在趨勢來說,是一直在貶值的第二點也舉個例子就是如果你有20萬元預算購車可是你只給10萬,貸款10萬再把剩下的10萬去投資(炒個房地產、買個理財產品什麼的)而當你所投資后得到的收益對於你10萬貸款所需的利息那這個貸款就很划算了可是具體購車貸款利率多少。

人生第一次買車

都有哪些情景?

情景一:

打開微信,找個比較懂車的朋友,上來就問

情景二:

找個汽車App或小程序,勾選條件篩出自己的意向車型

情景三:

先逛幾家4S店,看看哪個銷售妹子腿更長?

好吧,其實情況有很多種,可是最有效的方法應該是:打開微信→點擊公眾號→添加公眾號→搜索“車買買”→點擊關注,一系列購車選車用車評車信息任你看(這裏希望老闆可以看到,嘻嘻嘻),好吧,廢話說多了,直接上乾貨。

如果有足夠預算,就選擇進口車吧

要多大有多大、要多長有多長

要多快有多快、要多帥有多帥

然而理想和現實還是有很大差距的

畢竟不是每個人都叫思聰

而且還有一個姓王的爸爸

更多人還是得用自己辛苦賺來的錢

買一輛物有所值或物超所值的車

選擇自主或合資比起進口,性價比會更高一點吧

每次聊到自主和合資品牌

評論區似乎都兩極分化

要不就是以愛國的名義支持國產

要不就是要求更高的品質支持合資

在此虎哥就不參与任何政治言論了

(希望大家能理解)

不過只針對產品本身

虎哥的看法是:都可以買,喜歡就好

由於現在國人的消費水平越來越高

就算買到不好或者不合適自己的車

用一段時間賣掉好像也不是很虧

誰都說不準哪輛車的質量一定就好或壞

國產車也有五菱宏光這樣的神車

合資品牌也有很多斷軸燒機油的案例

你可以根據自己的見解去選

沒有見解,那自主跟合資可以一起考慮

相信每個男人都有個跑車夢吧?

可是跑車對於在座大部分人來說都不現實吧

我認為絕大部分跑車都只是男人們的大玩具

對於有錢又帥又單身的你,買來耍耍還是可以的

當然,今天虎哥並不是推薦你去買跑車上下班

而是告訴你除了轎車和SUV還有很多選擇

比如近期越來越火的MpV車型

裝得比轎車多,用起來比SUV更舒適更方便

比如越野能力和裝載能力都很強的皮卡

關於買轎車還是SUV的問答在網上實在太多

其實對於城市用車來說無非也就那幾個區別

轎車:操控好、加速快、更省油等等

SUV:坐姿高、視野好、裝得多等等

如果當你了解兩者的區別後還不會選

那你就很有必要去看看其它車型了

二手車性價比高是毋庸置疑的

不過二手車最大的問題就是“水太深”

對於不懂行的人來說沒有必要去嘗試

可是如果你有足夠的積累和經驗

肯定能用很低的價錢買到二手好車

(而且不用給中間商賺差價哦)

所以人生第一次買車

更多的是建議買新車

新車不僅可以給你更多的保障

而且還能給你一種超爽的新鮮感

就跟你小時候收到新玩具的感覺一樣

貸款不是都要收利息嗎?還有划不划算?

其實這裏還需考慮兩個問題:

一、貨幣的貶值

二、投資的收益

第一點很簡單

就是你今年花10萬元買的車

這10萬元三年後可能就不值10萬元了

又比如你貸10萬元買車

這10萬元三年後可能也不值10萬元了

可是這個必須考慮到貨幣的貶值率

而按現在趨勢來說,是一直在貶值的

第二點也舉個例子

就是如果你有20萬元預算購車

可是你只給10萬,貸款10萬

再把剩下的10萬去投資

(炒個房地產、買個理財產品什麼的)

而當你所投資后得到的收益

對於你10萬貸款所需的利息

那這個貸款就很划算了

可是具體購車貸款利率多少?

不同主機廠或經銷商所提供的政策不同

具體還需你親自去計算過才知道

而且那些說0%利率的優本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

不到20萬的寶馬,車主們都說開起來很爽!

8百公里油耗(L):5。5/6。2/6。4車主百公里油耗(L):8。41/8。62驅動方式:前置前驅底盤懸挂:前麥弗遜/后多連桿式前排腿部空間(mm):890-1135前排高度(mm):940前排寬度(mm):1460後排腿部空間(mm):550-800後排高度(mm):900後排寬度(mm):1420實際體驗(體驗者178cm):前排頭部1拳/後排頭部4指/後排腿部1拳4指。

身為國內售價最接地氣的寶馬車型,寶馬1系三廂車型把定價下探到20萬元左右,它既能與奧迪A3展開競爭,也給寶馬的潛在用戶們提供了一個新選擇。

寶馬1系三廂車型採用了UKL前驅平台進行打造,並且搭載了B38系列1.5T發動機和分為高低功率調校的B48系列2.0T發動機,在傳動方面則採用來自愛信的6AT/8AT變速箱。

寶馬1系採用前驅的形式,雖然在駕駛感受上比以往的寶馬車型有所減弱,但它更易於控制,成本也得以下降。現在已經在使用這款車型的車主們對它有什麼評價?下面我們就來詳細看看。

長寬高:4456*1803*1448mm

軸距:2670mm

定位:緊湊型車

寶馬1系三廂的前臉采經典的“雙腎”造型中網,格柵造型的比較圓潤攻擊性不算十分突出。頭燈內帶有多邊形天使眼設計,並且大燈輪廓明顯向後延伸。稍有遺憾的是大燈沒有採用開眼角式設計,而側面輪廓緊湊而協調,短促的車尾造型提升了整車的運動感!

在內飾方面,它的布局分明清晰,中控台上部採用軟質材料覆蓋並且帶有縫線裝飾,中部配有高亮鋼琴烤漆面板,整體時尚感較好而且內飾的做工細緻、質感頗為優良!車門內襯也採用皮革和搪塑工藝材質包裹,整體質感良好。

發動機:1.5T/2.0T

最大馬力(pS):136/192/231

最大扭矩(Nm):220/280/350

變速箱:6AT/8AT

百公里加速(s):9.4/7.5/6.8

百公里油耗(L):5.5/6.2/6.4

車主百公里油耗(L):8.41/8.62

驅動方式:前置前驅

底盤懸挂:前麥弗遜/后多連桿式

前排腿部空間(mm):890-1135

前排高度(mm):940

前排寬度(mm):1460

後排腿部空間(mm):550-800

後排高度(mm):900

後排寬度(mm):1420

實際體驗(體驗者178cm):前排頭部1拳/後排頭部4指/後排腿部1拳4指。

感興趣的朋友可以點擊小程序查看詳細口碑,從口碑中可以看到,車主們對這款車型的操控感受,油耗控制能力和動力輸出比較滿意!而對於發動機NVH控制,一些配置的缺失有一些意見……

咱們發現寶馬1系的優惠還是比較大的,在廣州、武漢等地一般需要的搭配店內貸款、上保險、加裝飾等消費項目。

寶馬1系三廂車型在乘坐空間和內飾質感方面表現比較優良,是一款比較適合家用的車型,只是1.5T發動機採用直列3缸的設計結構,目前國內消費者對於這樣的發動機形式的接受度仍不算高。它的2.0T車型動力表現很強勁,只是售價門檻比較高。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

內在藏有硬實力!20萬起買這款SUV很超值!

發動機:1。8T/2。0T最大馬力(ps):180/186/220最大扭矩(Nm):300/320/350變速箱:7DCT驅動方式:前置前驅/前置四驅底盤懸挂:麥弗遜式獨立懸架/多連桿獨立懸挂動態表現應該算是車主們對柯迪亞克最滿意的一個方面吧。在此前的對比評測中,無論是加速、行駛平順性、油耗表現,還是剎車的成績,柯迪亞克都算得上是同級數一數二的水平。

曾經有不少想要買車的朋友說,20萬以下不買SUV,因為只有去到中型SUV的級別,才有SUV應有的空間和通過性。而現在市場上有許多合資中型SUV的空間真的讓人垂涎欲滴,比如日系的豐田漢蘭達和本田冠道,也有德系的大眾途觀L和美系的別克昂科威。選擇真的挺多。

然而在這麼多完美選擇之中,卻有人選擇了比較非主流的斯柯達-柯迪亞克,這車買得到底值不值?一起看看車主怎說吧!

長寬高:4698*1883*1676mm

軸距:2791mm

定位:中型SUV

雖然斯柯達在中國的品牌影響力比不上大眾或豐田這些一線品牌,可是在車型設計上也算不上遜色,車主們對整體外觀設計還是比較滿意的。前臉的“凹”字型中網是家族式設計比較顯著的特徵,上中貫穿着兩大燈,下進氣格柵很舒展,整體前臉比較霸氣;車身側面沒有太多設計亮點,不過整體還算協調,車尾設計也比較低調。

內飾風格很簡潔,很居家,大面積的木飾板看上去很有質感,儀錶盤採用的綠色點綴也是斯柯達比較標誌性的設計,個人感覺很具特色,呈現出比較“清新”的風格。柯迪亞克的外觀內飾設計沒有漢蘭達和冠道那麼具有吸引力,也可以體驗出車主們都比較喜歡低調點的風格。

發動機:1.8T/2.0T

最大馬力(ps):180/186/220

最大扭矩(Nm):300/320/350

變速箱:7DCT

驅動方式:前置前驅/前置四驅

底盤懸挂:麥弗遜式獨立懸架/多連桿獨立懸挂

動態表現應該算是車主們對柯迪亞克最滿意的一個方面吧。在此前的對比評測中,無論是加速、行駛平順性、油耗表現,還是剎車的成績,柯迪亞克都算得上是同級數一數二的水平。

1米8的試乘員坐在前排調整到舒適坐姿,頭部空間為4指,保持前排坐姿不變,同一位試乘員坐進後排,頭部空間為4指,腿部空間大於兩拳。在同級車型中,柯迪亞克的乘坐空間表現其實比較中庸,不過除了動力方面,車主們表示最為滿意的是其空間。不過一般的中型SUV,其實空間表現都能滿足大部分人的需求吧。

從車主口碑中,我們可以看出,動力和空間是車主們最滿意的方面,其外觀也是喜歡的人特別喜歡。而最不滿意的基本都是配置方面比較遜色。

如果從價位出發,柯迪亞克瞄準的競爭對手應該是昂科威和探界者,不過也比較難體現出競爭力,最主要還是在動力方面有一定的優勢。所以大多數選擇柯迪亞克的車主們都是奔着自己喜歡的外觀內飾,不錯的動力表現,以及相對於冠道和途觀L更低的售價。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

動力一樣還更大更便宜,這車銷量卻不到POLO的1/10

說實話,追求大大大空間的人就別買小型車啊。而我就是覺得空間不那麼重要,平日都是一個人用車居多的,後排很少用來載人的。所以後排空間小一點對我來說影響不大。一句話:一兩個人用車就挺好的。晶銳車主:其他地方,我都可以原諒pOLO,但是安全配置方面,真的讓我對pOLO下不了手啊。

大眾pOLO可以說是國內賣得最好的小型兩廂車

但是同胞兄弟晶銳的銷量則可以用慘淡來形容

簡直就是連pOLO的零頭都不如

明明技術平台、空間表現、性能等硬性指標都差別不大

那為什麼銷量卻是一個天堂一個地獄呢?

我們來看看車主們怎麼說吧!

晶銳車主:選擇晶銳之前我也考慮過pOLO(因為太多人買了,自然會考慮一下),但是pOLO的外觀令我覺得太乏味了。而相比之下,晶銳就顯得個性多了,雙色車身,車身線條很剛勁,還有繽紛車身色彩,看上去活力十足,非常適合年輕人。

pOLO車主:我想,購買大眾pOLO的車主絕大多數都是年輕人,而且很多都是過着平淡生活的年輕人。那麼pOLO就非常適合我這類人。pOLO整體看上去很飽滿,甚至看久了還會覺得挺精美的。反正,沒有人會覺得它有多突出,但也沒有人會將它歸入丑的那一類。

一句話:普通人總比個性主義者多。

晶銳車主:雖然晶銳的內飾用料一般、布局很常規,不過這些我都不在乎。價格就擺在這裏了,並不能奢求太多,用起來順手就好了。我更看中的是晶銳的內飾有拼色處理,不會像pOLO那黑黑的內飾那麼壓抑。

pOLO車主:我知道黑色顯小、黑色壓抑,但是黑色也百搭啊!我家的pOLO就是內飾全黑的;中控台是全黑的,但功能區劃分明確,實體按鍵操作便捷,平時開車盲操作也沒問題;座椅也是全黑的,用久了就知道很耐臟。反正,我覺得pOLO就是非常實在的存在。

一句話:反正都是簡潔實用為主。

晶銳車主:雖然軸距是一樣的,但是外觀尺寸,晶銳還是比pOLO大那麼一點。而且其實車內的頭部空間和腿部空間都是說得過去的。

pOLO車主:說話為什麼不能實在一點呢?說實話,追求大大大空間的人就別買小型車啊。而我就是覺得空間不那麼重要,平日都是一個人用車居多的,後排很少用來載人的。所以後排空間小一點對我來說影響不大。

一句話:一兩個人用車就挺好的。

晶銳車主:其他地方,我都可以原諒pOLO,但是安全配置方面,真的讓我對pOLO下不了手啊。

pOLO車主:在安全配置方面,pOLO可能做得不夠全面,但其他方面的配置pOLO給得也不少啊;要想比較豐富的配置可以選pOLO的頂配車型啊。

一句話:算了吧,其實兩款車的配置都算不上厚道啊。

晶銳車主:嗯,這方面可以說跟pOLO一模一樣的。靠譜!

pOLO車主:晶銳還挺喜歡用“與pOLO一樣的動力系統”來蹭熱度的。但別忘了還有pOLO GTI哦,那是1.4T發動機。還有,在調校、科技含量等方面,都是有些不一樣的啊。

一句話:兩款車也就都是日常夠用吧。

晶銳車主:晶銳的價格本身就比pOLO低一點點,優惠也有個7、8千。買下來還是挺划算的。

pOLO車主:pOLO的指導價是會比晶銳貴一點,但優惠基本是以萬為單位的呀。算下來,最後的成交價分分鐘比晶銳要低呢~

一句話:在不少地方,pOLO的價格算下來更便宜。

說到底啊,晶銳相比於pOLO來說是有它的個性所在的,而且指導價看上去是相對低一點點。但是!小編認為,晶銳的銷量會大不如pOLO的原因主要在於品牌效應的問題。兩者品牌定位的不同,晶銳的配置更低,而且用料上也更显示出廉價感,這樣子就進一步拉低了產品形象和銷量咯~

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

動手造輪子:實現一個簡單的 AOP 框架

動手造輪子:實現一個簡單的 AOP 框架

Intro

最近實現了一個 AOP 框架 — FluentAspects,API 基本穩定了,寫篇文章分享一下這個 AOP 框架的設計。

整體設計

概覽

IProxyTypeFactory

用來生成代理類型,默認提供了基於 Emit 動態代理的實現,基於接口設計,可以擴展為其他實現方式

接口定義如下:

public interface IProxyTypeFactory
{
    Type CreateProxyType(Type serviceType);

    Type CreateProxyType(Type serviceType, Type implementType);
}

IProxyFactory

用來生成代理實例,默認實現是基於 IProxyTypeFactory 生成代理類型之後創建實例

接口定義如下:

public interface IProxyFactory
{
    object CreateProxy(Type serviceType, object[] arguments);

    object CreateProxy(Type serviceType, Type implementType, params object[] arguments);

    object CreateProxyWithTarget(Type serviceType, object implement, object[] arguments);
}

IInvocation

執行上下文,默認實現就是方法執行的上下文,包含了代理方法信息、被代理的方法信息、方法參數,返回值以及用來自定義擴展的一個 Properties 屬性

public interface IInvocation
{
    MethodInfo ProxyMethod { get; }

    object ProxyTarget { get; }

    MethodInfo Method { get; }

    object Target { get; }

    object[] Arguments { get; }

    Type[] GenericArguments { get; }

    object ReturnValue { get; set; }

    Dictionary<string, object> Properties { get; }
}

IInterceptor

攔截器,用來定義公用的處理邏輯,方法攔截處理方法

接口定義如下:

public interface IInterceptor
{
    Task Invoke(IInvocation invocation, Func<Task> next);
}

invocation 是方法執行的上下文,next 代表後續的邏輯處理,類似於 asp.net core 里的 next ,如果不想執行方面的方法不執行 next 邏輯即可

IInterceptorResolver

用來根據當前的執行上下文獲取到要執行的攔截器,默認是基於 FluentAPI 的實現,但是如果你特別想用基於 Attribute 的也是可以的,默認提供了一個 AttributeInterceotorResovler,你也可以自定義一個適合自己的 InterceptorResolver

public interface IInterceptorResolver
{
    IReadOnlyList<IInterceptor> ResolveInterceptors(IInvocation invocation);
}

IInvocationEnricher

上面 IInvocation 的定義中有一個用於擴展的 Properties,這個 enricher 主要就是基於 Properties 來豐富執行上下文信息的,比如說記錄 TraceId 等請求鏈路追蹤數據,構建方法執行鏈路等

public interface IEnricher<in TContext>
{
    void Enrich(TContext context);
}
public interface IInvocationEnricher : IEnricher<IInvocation>
{
}

AspectDelegate

AspectDelegate 是用來將構建要執行的代理方法的方法體的,首先執行註冊的 InvocationEnricher,豐富上下文信息,然後根據執行上下文獲取要執行的攔截器,構建一個執行委託,生成委託使用了之前分享過的 PipelineBuilder 構建中間件模式的攔截器,執行攔截器邏輯

// apply enrichers
foreach (var enricher in FluentAspects.AspectOptions.Enrichers)
{
    try
    {
        enricher.Enrich(invocation);
    }
    catch (Exception ex)
    {
        InvokeHelper.OnInvokeException?.Invoke(ex);
    }
}

// get delegate
var builder = PipelineBuilder.CreateAsync(completeFunc);
foreach (var interceptor in interceptors)
{
    builder.Use(interceptor.Invoke);
}
return builder.Build();

更多信息可以參考源碼: https://github.com/WeihanLi/WeihanLi.Common/blob/dev/src/WeihanLi.Common/Aspect/AspectDelegate.cs

使用示例

推薦和依賴注入結合使用,主要分為以微軟的注入框架為例,有兩種使用方式,一種是手動註冊代理服務,一種是自動批量註冊代理服務,來看下面的實例就明白了

手動註冊代理服務

使用方式一,手動註冊代理服務:

為了方便使用,提供了一些 AddProxy 的擴展方法:

IServiceCollection services = new ServiceCollection();
services.AddFluentAspects(options =>
    {
        // 註冊攔截器配置
        options.NoInterceptProperty<IFly>(f => f.Name);

        options.InterceptAll()
            .With<LogInterceptor>()
            ;
        options.InterceptMethod<DbContext>(x => x.Name == nameof(DbContext.SaveChanges)
                                                || x.Name == nameof(DbContext.SaveChangesAsync))
            .With<DbContextSaveInterceptor>()
            ;
        options.InterceptMethod<IFly>(f => f.Fly())
            .With<LogInterceptor>();
        options.InterceptType<IFly>()
            .With<LogInterceptor>();

        // 註冊 InvocationEnricher
        options
            .WithProperty("TraceId", "121212")
            ;
    });
// 使用 Castle 生成代理
services.AddFluentAspects(options =>
    {
        // 註冊攔截器配置
        options.NoInterceptProperty<IFly>(f => f.Name);

        options.InterceptAll()
            .With<LogInterceptor>()
            ;
        options.InterceptMethod<DbContext>(x => x.Name == nameof(DbContext.SaveChanges)
                                                || x.Name == nameof(DbContext.SaveChangesAsync))
            .With<DbContextSaveInterceptor>()
            ;
        options.InterceptMethod<IFly>(f => f.Fly())
            .With<LogInterceptor>();
        options.InterceptType<IFly>()
            .With<LogInterceptor>();

        // 註冊 InvocationEnricher
        options
            .WithProperty("TraceId", "121212")
            ;
    }, builder => builder.UseCastle());

services.AddTransientProxy<IFly, MonkeyKing>();
services.AddSingletonProxy<IEventBus, EventBus>();
services.AddDbContext<TestDbContext>(options =>
{
    options.UseInMemoryDatabase("Test");
});
services.AddScopedProxy<TestDbContext>();

var serviceProvider = services.BuildServiceProvider();

批量自動註冊代理服務

使用方式二,批量自動註冊代理服務:

IServiceCollection services = new ServiceCollection();
services.AddTransient<IFly, MonkeyKing>();
services.AddSingleton<IEventBus, EventBus>();
services.AddDbContext<TestDbContext>(options =>
{
    options.UseInMemoryDatabase("Test");
});

var serviceProvider = services.BuildFluentAspectsProvider(options =>
            {
                options.InterceptAll()
                    .With<TestOutputInterceptor>(output);
            });

// 使用 Castle 來生成代理
var serviceProvider = services.BuildFluentAspectsProvider(options =>
            {
                options.InterceptAll()
                    .With<TestOutputInterceptor>(output);
            }, builder => builder.UseCastle());

// 忽略命名空間為 Microsoft/System 的服務類型
var serviceProvider = services.BuildFluentAspectsProvider(options =>
            {
                options.InterceptAll()
                    .With<TestOutputInterceptor>(output);
            }, builder => builder.UseCastle(), t=> t.Namespace != null && (t.Namespace.StartWith("Microsft") ||t.Namespace.StartWith("Microsft")));

More

上面的兩種方式個人比較推薦使用第一種方式,需要攔截什麼就註冊什麼代理服務,自動註冊可能會生成很多不必要的代理服務,個人還是比較喜歡按需註冊的方式,更為可控。

這個框架還不是很完善,有一些地方還是需要優化的,目前還是在我自己的類庫中,因為我的類庫里要支持 net45,所以有一些不好的設計改起來不太方便,打算遷移出來作為一個單獨的組件,直接基於 netstandard2.0/netstandard2.1, 甩掉 netfx 的包袱。

Reference

  • https://github.com/WeihanLi/WeihanLi.Common/blob/dev/src/WeihanLi.Common/Aspect
  • https://www.cnblogs.com/weihanli/p/12700006.html

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

多線程高併發編程(10) — ConcurrentHashMap源碼分析

  一.背景

  前文講了HashMap的源碼分析,從中可以看到下面的問題:

  • HashMap的put/remove方法不是線程安全的,如果在多線程併發環境下,使用synchronized進行加鎖,會導致效率低下;
  • 在遍歷迭代獲取時進行修改(put/remove)操作,會導致發生併發修改異常(ConcurrentModificationException);
  • 在JDK1.7之前,對HashMap進行put添加操作,會導致鏈表反轉,造成鏈表迴路,從而發生get死循環,(當然這個問題在JDK1.8被改進了按照原鏈表順序進行重排移動);
  • 如果多個線程同時檢測到元素個數超過 數組大小 * loadFactor,這樣就會發生多個線程同時對數組進行擴容,都在重新計算元素位置以及複製數據,但是最終只有一個線程擴容后的數組會賦給 table,也就是說其他線程的都會丟失,並且各自線程 put 的數據也丟失;

  基於上述問題,都可以使用ConcurrentHashMap進行解決,ConcurrentHashMap使用分段鎖技術解決了併發訪問效率,在遍歷迭代獲取時進行修改操作也不會發生併發修改異常等等問題。

  二.源碼解析

  1. 構造方法:

    //最大容量大小
        private static final int MAXIMUM_CAPACITY = 1 << 30;
        //默認容量大小
        private static final int DEFAULT_CAPACITY = 16;
        /**
         *控制標識符,用來控制table的初始化和擴容的操作,不同的值有不同的含義
         *  多線程之間,以volatile方式讀取sizeCtl屬性,來判斷ConcurrentHashMap當前所處的狀態。
         *  通過cas設置sizeCtl屬性,告知其他線程ConcurrentHashMap的狀態變更
         *未初始化:
         *  sizeCtl=0:表示沒有指定初始容量。
         *  sizeCtl>0:表示初始容量。
         *初始化中:
         *  sizeCtl=-1,標記作用,告知其他線程,正在初始化
         *正常狀態:
         *  sizeCtl=0.75n ,擴容閾值
         *擴容中:
         *  sizeCtl < 0 : 表示有其他線程正在執行擴容
         *  sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 :表示此時只有一個線程在執行擴容
         */
        private transient volatile int sizeCtl;
        //併發級別
        private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
        //創建一個新的空map,默認大小是16
        public ConcurrentHashMap() {
        }
        public ConcurrentHashMap(int initialCapacity) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException();
            //調整table的大小,tableSizeFor的實現查看前文HashMap源碼分析的構造方法模塊
            int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
            this.sizeCtl = cap;
        }
        public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
            this.sizeCtl = DEFAULT_CAPACITY;
            putAll(m);
        }
        public ConcurrentHashMap(int initialCapacity, float loadFactor) {
            this(initialCapacity, loadFactor, 1);
        }
        /**
         * concurrencyLevel:併發度,預估同時操作數據的線程數量
         * 表示能夠同時更新ConccurentHashMap且不產生鎖競爭的最大線程數。
         * 默認值為16,(即允許16個線程併發可能不會產生競爭)。
         */
        public ConcurrentHashMap(int initialCapacity,
                                 float loadFactor, int concurrencyLevel) {
            if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
                throw new IllegalArgumentException();
            //至少使用同樣多的桶容納同樣多的更新線程來操作元素
            if (initialCapacity < concurrencyLevel)   // Use at least as many bins
                initialCapacity = concurrencyLevel;   // as estimated threads
            long size = (long)(1.0 + (long)initialCapacity / loadFactor);
            int cap = (size >= (long)MAXIMUM_CAPACITY) ?
                MAXIMUM_CAPACITY : tableSizeFor((int)size);
            this.sizeCtl = cap;
        }
  2. put:

    public V put(K key, V value) {
            return putVal(key, value, false);
        }
        static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash普通節點哈希的可用位
        //把位數控制在int最大整數之內,h ^ (h >>> 16)的含義查看前文的put源碼解析
        static final int spread(int h) {
            return (h ^ (h >>> 16)) & HASH_BITS;
        }
        final V putVal(K key, V value, boolean onlyIfAbsent) {
            //key和value為空拋出異常
            if (key == null || value == null) throw new NullPointerException();
            //得到hash值
            int hash = spread(key.hashCode());
            int binCount = 0;
            //自旋對table進行遍歷
            for (Node<K,V>[] tab = table;;) {
                Node<K,V> f; int n, i, fh;
                //初始化table
                if (tab == null || (n = tab.length) == 0)
                    tab = initTable();
                //如果hash計算出的槽位元素為null,CAS將元素填充進當前槽位並結束遍歷
                else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                    if (casTabAt(tab, i, null,
                                 new Node<K,V>(hash, key, value, null)))
                        break;                   // no lock when adding to empty bin
                }
                //hash為-1,說明正在擴容,那麼就幫助其擴容。以加快速度
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else {
                    V oldVal = null;
                    synchronized (f) {// 同步 f 節點,防止增加鏈表的時候導致鏈表成環
                        if (tabAt(tab, i) == f) {// 如果對應的下標位置的節點沒有改變
                            if (fh >= 0) {//f節點的hash值大於0
                                binCount = 1;//鏈表初始長度
                                // 死循環,直到將值添加到鏈表尾部,並計算鏈表的長度
                                for (Node<K,V> e = f;; ++binCount) {
                                    K ek;
                                    //hash和key相同,值進行覆蓋
                                    if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                         (ek != null && key.equals(ek)))) {
                                        oldVal = e.val;
                                        if (!onlyIfAbsent)
                                            e.val = value;
                                        break;
                                    }
                                    Node<K,V> pred = e;
                                    //hash和key不同,添加到鏈表後面
                                    if ((e = e.next) == null) {
                                        pred.next = new Node<K,V>(hash, key,
                                                                  value, null);
                                        break;
                                    }
                                }
                            }
                            //是樹節點,添加到樹中
                            else if (f instanceof TreeBin) {
                                Node<K,V> p;
                                binCount = 2;
                                if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                               value)) != null) {
                                    oldVal = p.val;
                                    if (!onlyIfAbsent)
                                        p.val = value;
                                }
                            }
                        }
                    }
                     //如果節點添加到鏈表和樹中
                    if (binCount != 0) {
                        //鏈表長度大於等於8時,將鏈錶轉換成紅黑樹樹
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
            // 判斷是否需要擴容
            addCount(1L, binCount);
            return null;
        }
    1. initTable:初始化

      private final Node<K,V>[] initTable() {
              Node<K,V>[] tab; int sc;
              while ((tab = table) == null || tab.length == 0) {
                  //如果一個線程發現sizeCtl<0,意味着另外的線程執行CAS操作成功,當前線程只需要讓出cpu時間片,即保證只有一個線程初始化
                  //由於sizeCtl是volatile的,保證了順序性和可見性
                  if ((sc = sizeCtl) < 0)
                      Thread.yield(); // lost initialization race; just spin
                  else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//cas操作判斷並置為-1
                      try {
                          if ((tab = table) == null || tab.length == 0) {
                              int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//若沒有參數則默認容量為16
                              @SuppressWarnings("unchecked")
                              Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//創建數組
                              table = tab = nt;//數組賦值給當前ConcurrentHashMap
                              //計算下一次元素到達擴容的閥值,如果n為16的話,那麼這裏 sc = 12,其實就是 0.75 * n
                              sc = n - (n >>> 2);
                          }
                      } finally {
                          sizeCtl = sc;
                      }
                      break;
                  }
              }
              return tab;
          }
    2. tabAt:尋找指定數組在內存中i位置的數據

      static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
              /**getObjectVolatile:獲取obj對象中offset偏移地址對應的object型field的值,支持volatile load語義。
               * 數組的尋址計算方式:a[i]_address = base_address + i * data_type_size
               * base_address:起始地址;i:索引;data_type_size:數據類型長度大小
               */
              return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
          }
    3. helpTransfer:幫助擴容

      private static int RESIZE_STAMP_BITS = 16;
          /**
           * numberOfLeadingZeros()的具體算法邏輯請參考:https://www.jianshu.com/p/2c1be41f6e59
           * numberOfLeadingZeros(n)返回的是n的二進制標識的從高位開始到第一個非0的数字的之間0的個數,比如numberOfLeadingZeros(8)返回的就是28 ,因為0000 0000 0000 0000 0000 0000 0000 1000在1前面有28個0
           * RESIZE_STAMP_BITS 的值是16,1 << (RESIZE_STAMP_BITS - 1)就是將1左移位15位,0000 0000 0000 0000 1000 0000 0000 0000
           * 然後將兩個数字再按位或,將相當於 將移位后的 兩個數相加。
           * 比如:
           * 8的二進製表示是: 0000 0000 0000 0000 0000 0000 0000 1000 = 8
           * 7的二進製表示是: 0000 0000 0000 0000 0000 0000 0000 0111 = 7
           * 按位或的結果是:  0000 0000 0000 0000 0000 0000 0000 1111 = 15
           * 相當於 8 + 7 =15
           * 為什麼會出現這種效果呢?因為8是2的整數次冪,也就是說8的二進製表示只會在某個高位上是1,其餘地位都是0,所以在按位或的時候,低位表示的全是7的位值,所以出現了這種效果。
           */
          static final int resizeStamp(int n) {
              return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
          }
          final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
              Node<K,V>[] nextTab; int sc;
               //如果table不是空,且node節點是轉移類型,且node節點的nextTable(新 table)不是空,嘗試幫助擴容
              if (tab != null && (f instanceof ForwardingNode) &&
                  (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
                  //根據length得到一個標識符號
                  int rs = resizeStamp(tab.length);
                   //如果nextTab沒有被併發修改,且tab也沒有被併發修改,且sizeCtl<0(說明還在擴容)
                  while (nextTab == nextTable && table == tab &&
                         (sc = sizeCtl) < 0) {
                      /**
                       * 如果 sizeCtl 無符號右移16不等於rs( sc前16位如果不等於標識符,則標識符變化了)
                       * 或者 sizeCtl == rs + 1(擴容結束了,不再有線程進行擴容)(默認第一個線程設置 sc ==rs 左移 16 位 + 2,當第一個線程結束擴容了,就會將 sc 減1。這個時候,sc 就等於 rs + 1)
                       * 或者 sizeCtl == rs + 65535  (如果達到最大幫助線程的數量,即 65535)
                       * 或者轉移下標正在調整 (擴容結束)
                       * 結束循環,返回 table
                       * 【即如果還在擴容,判斷標識符是否變化,判斷擴容是否結束,判斷是否達到最大線程數,判斷擴容轉移下標是否在調整(擴容結束),如果滿足任意條件,結束循環。】
                       */
                      if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                          sc == rs + MAX_RESIZERS || transferIndex <= 0)
                          break;
                      // 如果以上都不是, 將 sizeCtl + 1, (表示增加了一個線程幫助其擴容)
                      if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                          transfer(tab, nextTab);//進行擴容和數據遷移
                          break;
                      }
                  }
                  return nextTab;//返回擴容后的數組
              }
              return table;//沒有擴容,返回原數組
          }
    4.  transfer:擴容和數據遷移,採用多線程擴容,整個擴容過程,通過cas設置sizeCtl、transferIndex等變量協調多個線程進行併發擴容;

      1.  transferIndex屬性:
        //擴容索引,表示已經分配給擴容線程的table數組索引位置。主要用來協調多個線程,併發安全地獲取遷移任務(hash桶)。
        private transient volatile int transferIndex;
        1. 在擴容之前,transferIndex 在數組的最右邊 。此時有一個線程發現已經到達擴容閾值,準備開始擴容。

        2. 擴容線程,在遷移數據之前,首先要將transferIndex左移(以cas的方式修改 transferIndex=transferIndex-stride(要遷移hash桶的個數)),獲取遷移任務。每個擴容線程都會通過for循環+CAS的方式設置transferIndex,因此可以確保多線程擴容的併發安全。( 換個角度,我們可以將待遷移的table數組,看成一個任務隊列,transferIndex看成任務隊列的頭指針。而擴容線程,就是這個隊列的消費者。擴容線程通過CAS設置transferIndex索引的過程,就是消費者從任務隊列中獲取任務的過程。 )
      2.  擴容過程:
        1.  容量已經達到擴容閾值,需要進行擴容操作,此時transferindex=tab.length=32
        2. 擴容線程A 以cas的方式修改transferindex=31-16=16 ,然後按照降序遷移table[31]–table[16]這個區間的hash桶
        3. 遷移hash桶時,會將桶內的鏈表或者紅黑樹,按照一定算法,拆分成2份,將其插入nextTable[i]和nextTable[i+n](n是table數組的長度)。 遷移完畢的hash桶,會被設置成ForwardingNode節點,以此告知訪問此桶的其他線程,此節點已經遷移完畢
        4. 此時線程2訪問到了ForwardingNode節點,如果線程2執行的put或remove等寫操作,那麼就會先幫其擴容。如果線程2執行的是get等讀方法,則會調用ForwardingNode的find方法,去nextTable裏面查找相關元素
        5. 線程2加入擴容操作
        6. 如果準備加入擴容的線程,發現以下情況,放棄擴容,直接返回。
          1. 發現transferIndex=0,即所有node均已分配

          2. 發現擴容線程已經達到最大擴容線程數

                                                    

      1.  源碼解析
        private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
            int n = tab.length, stride;
            //先判斷CPU核數,如果是多核,將數組長度/8,再/核數,得到stride,否則stride=數組長度,如果stride<16,則stride=16
            //這裏的目的是讓每個CPU處理的桶一樣多,避免出現轉移任務不均勻的現象,如果桶較少的話,默認一個CPU(一個線程)處理16個桶,即確保每次至少獲取16個桶(遷移任務)
            if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
                stride = MIN_TRANSFER_STRIDE; // subdivide range
            //未初始化進行初始化
            if (nextTab == null) {            // initiating
                try {
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];//擴容2倍
                    nextTab = nt;//更新
                } catch (Throwable ex) {      // try to cope with OOME
                    sizeCtl = Integer.MAX_VALUE;//擴容失敗,sizeCtl使用int最大值。
                    return;
                }
                nextTable = nextTab;//更新成員變量
                //transferIndex默認=table.length
                transferIndex = n;
            }
            int nextn = nextTab.length;//新tab的長度
            //創建一個fwd節點,用於佔位。當別的線程發現這個槽位中是fwd類型的節點,表示其他線程正在擴容,並且此節點已經擴容完畢,跳過這個節點。關聯了nextTab,可以通過ForwardingNode.find()訪問已經遷移到nextTab的數據。
            ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
            //首次推進為 true,如果等於true,說明需要再次推進一個下標(i--),反之,如果是false,那麼就不能推進下標,需要將當前的下標處理完畢才能繼續推進
            boolean advance = true;
            //完成狀態,如果是true,就結束此方法。
            boolean finishing = false; // to ensure sweep before committing nextTab
            //自旋,i表示當前線程可以處理的當前桶區間最大下標,bound表示當前線程可以處理的當前桶區間最小下標
            for (int i = 0, bound = 0;;) {
                Node<K,V> f; int fh;
                 //while:如果當前線程可以向後推進;這個循環就是控制i遞減。同時,每個線程都會進入這裏取得自己需要轉移的桶的區間
                //分析場景:table.length=32,此時執行到這個地方nextTab.length=64 A,B線程同時進行擴容。
                //A,B線程同時執行到while循環中cas這段代碼
                //A線程獲第一時間搶到資源,設置bound=nextBound=16,i = nextIndex - 1=31 A線程搬運table[31]~table[16]中間16個元素
                //B線程再次回到while起點,然後在次獲取到 bound = nextBound-0,i=nextIndex - 1=15,B線程搬運table[15]~table[0]中間16個元素
                //當transferIndex=0的時候,說明table裏面所有搬運任務都已經完成,無法在分配任務。
                while (advance) {
                    int nextIndex, nextBound;
                    // 對i減1,判斷是否大於等於bound(正常情況下,如果大於bound不成立,說明該線程上次領取的任務已經完成了。那麼,需要在下面繼續領取任務)
                    // 如果對i減1大於等於 bound,或者完成了,修改推進狀態為 false,不能推進了。任務成功后修改推進狀態為 true。
                    // 通常,第一次進入循環,i-- 這個判斷會無法通過,從而走下面的nextIndex = transferIndex(獲取最新的轉移下標)。其餘情況都是:如果可以推進,將i減1,然後修改成不可推進。如果i對應的桶處理成功了,改成可以推進。
                    if (--i >= bound || finishing)
                        advance = false;//這裏設置false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進
                   // 這裏的目的是:1. 當一個線程進入時,會選取最新的轉移下標。
                   //             2. 當一個線程處理完自己的區間時,如果還有剩餘區間的沒有別的線程處理,再次CAS獲取區間。
                    else if ((nextIndex = transferIndex) <= 0) {
                        // 如果小於等於0,說明沒有區間可以獲取了,i改成-1,推進狀態變成false,不再推進
                        // 這個-1會在下面的if塊里判斷,從而進入完成狀態判斷
                        i = -1;
                        advance = false;//這裏設置false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進
                    }
                    // CAS修改transferIndex,即 length - 區間值,留下剩餘的區間值供後面的線程使用
                    else if (U.compareAndSwapInt
                             (this, TRANSFERINDEX, nextIndex,
                              nextBound = (nextIndex > stride ?
                                           nextIndex - stride : 0))) {
                        bound = nextBound;//這個值就是當前線程可以處理的最小當前區間最小下標
                        i = nextIndex - 1;//初次對i賦值,這個就是當前線程可以處理的當前區間的最大下標
                        advance = false;// 這裏設置false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進,這樣導致漏掉某個桶。下面的 if(tabAt(tab, i) == f) 判斷會出現這樣的情況。
                    }
                }
                //i<0(不在 tab 下標內,按照上面的判斷,領取最後一段區間的線程結束)
                if (i < 0 || i >= n || i + n >= nextn) {
                    int sc;
                    if (finishing) {// 如果完成了擴容和數據遷移
                        nextTable = null;//刪除成員遍歷
                        table = nextTab;//更新table
                        sizeCtl = (n << 1) - (n >>> 1);//更新閥值
                        return;//結束transfer
                    }
                    //如果沒完成,嘗試將sc -1. 表示這個線程結束幫助擴容了,將 sc 的低 16 位減一。
                    if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                        //如果 sc - 2 不等於標識符左移 16 位。如果他們相等了,說明沒有線程在幫助他們擴容了。也就是說,擴容結束了。
                        /**
                         *第一個擴容的線程,執行transfer方法之前(helpTransfer方法中),會設置 sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2)
                         *後續幫其擴容的線程,執行transfer方法之前,會設置 sizeCtl = sizeCtl+1
                         *每一個退出transfer的方法的線程,退出之前,會設置 sizeCtl = sizeCtl-1
                         *那麼最後一個線程退出時:
                         *必然有sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2),即 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
                        */
                        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                            return;// 不相等,說明不到最後一個線程,直接退出transfer方法
                        finishing = advance = true;// 如果相等,擴容結束了,更新 finising 變量
                        i = n; // recheck before commit,最後退出的線程要重新check下是否全部遷移完畢
                    }
                }
                else if ((f = tabAt(tab, i)) == null) // 獲取老tab的i下標位置的變量,如果是 null,就使用 fwd 佔位。
                    advance = casTabAt(tab, i, null, fwd);// 如果成功寫入 fwd 佔位,再次推進一個下標
                else if ((fh = f.hash) == MOVED)// 如果不是 null 且 hash 值是 MOVED。
                    advance = true; // already processed,說明別的線程已經處理過了,再次推進一個下標
                else {// 到這裏,說明這個位置有實際值了,且不是佔位符。對這個節點上鎖。為什麼上鎖,防止 putVal 的時候向鏈表插入數據
                    synchronized (f) {
                        // 判斷 i 下標處的桶節點是否和 f 相同
                        if (tabAt(tab, i) == f) {
                            Node<K,V> ln, hn;// low, height 高位桶,低位桶
                            // 如果 f 的 hash 值大於 0 。TreeBin 的 hash 是 -2
                            if (fh >= 0) {
                                // 對老長度進行與運算(第一個操作數的的第n位於第二個操作數的第n位如果都是1,那麼結果的第n為也為1,否則為0)
                                // 由於 Map 的長度都是 2 的次方(000001000 這類的数字),那麼取於 length 只有 2 種結果,一種是 0,一種是1
                                //  如果是結果是0 ,Doug Lea 將其放在低位,反之放在高位,目的是將鏈表重新 hash,放到對應的位置上,讓新的取於算法能夠擊中他。
                                int runBit = fh & n;
                                Node<K,V> lastRun = f; // 尾節點,且和頭節點的 hash 值取於不相等
                                // 遍歷這個桶
                                for (Node<K,V> p = f.next; p != null; p = p.next) {
                                    // 取於桶中每個節點的 hash 值
                                    int b = p.hash & n;
                                    // 如果節點的 hash 值和首節點的 hash 值取於結果不同
                                    if (b != runBit) {
                                        runBit = b; // 更新 runBit,用於下面判斷 lastRun 該賦值給 ln 還是 hn。
                                        lastRun = p; // 這個 lastRun 保證後面的節點與自己的取於值相同,避免後面沒有必要的循環
                                    }
                                }
                                if (runBit == 0) {// 如果最後更新的 runBit 是 0 ,設置低位節點
                                    ln = lastRun;
                                    hn = null;
                                }
                                else {
                                    hn = lastRun; // 如果最後更新的 runBit 是 1, 設置高位節點
                                    ln = null;
                                }// 再次循環,生成兩個鏈表,lastRun 作為停止條件,這樣就是避免無謂的循環(lastRun 後面都是相同的取於結果)
                                for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                    int ph = p.hash; K pk = p.key; V pv = p.val;
                                    // 如果與運算結果是 0,那麼就還在低位
                                    if ((ph & n) == 0) // 如果是0 ,那麼創建低位節點
                                        ln = new Node<K,V>(ph, pk, pv, ln);
                                    else // 1 則創建高位
                                        hn = new Node<K,V>(ph, pk, pv, hn);
                                }
                                // 其實這裏類似 hashMap
                                // 設置低位鏈表放在新數組的 i
                                setTabAt(nextTab, i, ln);
                                // 設置高位鏈表,在原有長度上加 n
                                setTabAt(nextTab, i + n, hn);
                                // 將舊的鏈表設置成佔位符,表示處理過了
                                setTabAt(tab, i, fwd);
                                // 繼續向後推進
                                advance = true;
                            }// 如果是紅黑樹
                            else if (f instanceof TreeBin) {
                                TreeBin<K,V> t = (TreeBin<K,V>)f;
                                TreeNode<K,V> lo = null, loTail = null;
                                TreeNode<K,V> hi = null, hiTail = null;
                                int lc = 0, hc = 0;
                                // 遍歷
                                for (Node<K,V> e = t.first; e != null; e = e.next) {
                                    int h = e.hash;
                                    TreeNode<K,V> p = new TreeNode<K,V>
                                        (h, e.key, e.val, null, null);
                                    // 和鏈表相同的判斷,與運算 == 0 的放在低位
                                    if ((h & n) == 0) {
                                        if ((p.prev = loTail) == null)
                                            lo = p;
                                        else
                                            loTail.next = p;
                                        loTail = p;
                                        ++lc;
                                    } // 不是 0 的放在高位
                                    else {
                                        if ((p.prev = hiTail) == null)
                                            hi = p;
                                        else
                                            hiTail.next = p;
                                        hiTail = p;
                                        ++hc;
                                    }
                                }
                                // 如果樹的節點數小於等於 6,那麼轉成鏈表,反之,創建一個新的樹
                                ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                    (hc != 0) ? new TreeBin<K,V>(lo) : t;
                                hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                    (lc != 0) ? new TreeBin<K,V>(hi) : t;
                                // 低位樹
                                setTabAt(nextTab, i, ln);
                                // 高位數
                                setTabAt(nextTab, i + n, hn);
                                // 舊的設置成佔位符
                                setTabAt(tab, i, fwd);
                                // 繼續向後推進
                                advance = true;
                            }
                        }
                    }
                }
            }
        }
    1. addCount:計數

      // 從 putVal 傳入的參數是x=1,check=binCount默認是0,只有hash衝突了才會大於1,且他的大小是鏈表的長度(如果不是紅黑樹結構的話,紅黑樹=2)。
          private final void addCount(long x, int check) {
              CounterCell[] as; long b, s;
               //如果計數盒子不是空或者修改 baseCount 失敗
              if ((as = counterCells) != null ||
                  !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
                  CounterCell a; long v; int m;
                  boolean uncontended = true;
                   // 如果計數盒子是空(尚未出現併發)
                   // 如果隨機取餘一個數組位置為空 或者
                   // 修改這個槽位的變量失敗(出現併發了)
                   // 執行 fullAddCount 方法,在fullAddCount自旋直到CAS操作成功才結束退出
                  if (as == null || (m = as.length - 1) < 0 ||
                      (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                      !(uncontended =
                        U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                      fullAddCount(x, uncontended);
                      return;
                  }
                  if (check <= 1)
                      return;
                  s = sumCount();
              }
              // 檢查是否需要擴容,在 putVal 方法調用時,默認就是要檢查的(check默認是0,鏈表是鏈表長度,紅黑樹是2),如果是值覆蓋了,就忽略
              if (check >= 0) {
                  Node<K,V>[] tab, nt; int n, sc;
                  // 如果map.size() 大於 sizeCtl(達到擴容閾值需要擴容) 且
                  // table 不是空;且 table 的長度小於 1 << 30。(可以擴容)
                  while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                         (n = tab.length) < MAXIMUM_CAPACITY) {
                      // 根據 length 得到一個標識
                      int rs = resizeStamp(n);
                      if (sc < 0) {//表明此時有別的線程正在進行擴容
                          // 如果 sc 的低 16 位不等於 標識符(校驗異常 sizeCtl 變化了)
                          // 如果 sc == 標識符 + 1 (擴容結束了,不再有線程進行擴容)(默認第一個線程設置 sc ==rs 左移 16 位 + 2,當第一個線程結束擴容了,就會將 sc 減一。這個時候,sc 就等於 rs + 1)
                          // 如果 sc == 標識符 + 65535(幫助線程數已經達到最大)
                          // 如果 nextTable == null(結束擴容了)
                          // 如果 transferIndex <= 0 (轉移狀態變化了)
                          // 結束循環
                          if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                              sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                              transferIndex <= 0)
                              break;
                          // 不滿足前面5個條件時,嘗試參与此次擴容,把正在執行transfer任務的線程數加1,+2代表有1個,+1代表有0個,表示多了一個線程在幫助擴容,執行transfer
                          if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                              transfer(tab, nt);
                      }
                      //如果不在擴容,將 sc 更新:標識符左移 16 位 然後 + 2. 也就是變成一個負數。高 16 位是標識符,低 16 位初始是 2.
                      //試着讓自己成為第一個執行transfer任務的線程
                      else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                                   (rs << RESIZE_STAMP_SHIFT) + 2))
                          transfer(tab, null);
                      s = sumCount();// 重新計數,判斷是否需要開啟下一輪擴容
                  }
              }
          }
  1. get:

    public V get(Object key) {
            Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
            //得到hash
            int h = spread(key.hashCode());
            //table有值,且查找到的槽位有值(tabAt方法通過valatile讀)
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (e = tabAt(tab, (n - 1) & h)) != null) {
                //hash、key、value都相同返回當前查找到節點的值
                if ((eh = e.hash) == h) {
                    if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                        return e.val;
                }
                //遍歷特殊節點:紅黑樹、已經遷移的節點(ForwardingNode)等
                else if (eh < 0)
                    return (p = e.find(h, key)) != null ? p.val : null;
                //遍歷node鏈表(e.next也是valitle變量)
                while ((e = e.next) != null) {
                    if (e.hash == h &&
                        ((ek = e.key) == key || (ek != null && key.equals(ek))))
                        return e.val;
                }
            }
            return null;
        }
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }
  2. remove:

    public V remove(Object key) {
            return replaceNode(key, null, null);
        }
        //通過volatile設置第i個節點的值
        static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
            U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
        }
        final V replaceNode(Object key, V value, Object cv) {
            int hash = spread(key.hashCode());
            //自旋
            for (Node<K,V>[] tab = table;;) {
                Node<K,V> f; int n, i, fh;
                //數組或查找的槽位為空,結束自旋返回null
                if (tab == null || (n = tab.length) == 0 ||
                    (f = tabAt(tab, i = (n - 1) & hash)) == null)
                    break;
                //正在擴容,幫助擴容
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else {
                    V oldVal = null;//返回的舊值
                    boolean validated = false;//是否進行刪除鏈表或紅黑樹節點
                    synchronized (f) {//槽位加鎖
                        //getObjectVolatile獲取tab[i],如果此時tab[i]!=f,說明其他線程修改了tab[i]。回到for循環開始處,重新執行
                        if (tabAt(tab, i) == f) {//槽位節點沒有變化
                            if (fh >= 0) {//槽位節點是鏈表
                                validated = true;
                                //遍歷鏈表
                                for (Node<K,V> e = f, pred = null;;) {
                                    K ek;
                                    //hash、key、value相同
                                    if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                         (ek != null && key.equals(ek)))) {
                                        V ev = e.val;//臨時節點緩存當前節點值
                                        //值相同
                                        if (cv == null || cv == ev ||
                                            (ev != null && cv.equals(ev))) {
                                            oldVal = ev;//給舊值賦值
                                            if (value != null)//值覆蓋,replace()調用
                                                e.val = value;
                                            else if (pred != null)//有前節點,表示當前節點不是頭節點
                                                pred.next = e.next;//刪除當前節點
                                            else
                                                setTabAt(tab, i, e.next);//刪除頭節點,即更新當前槽位(數組槽位)節點為頭節點的下一節點
                                        }
                                        break;
                                    }
                                    //當前節點不是目標節點,繼續遍歷下一個節點
                                    pred = e;
                                    //到達鏈表尾部,依舊沒有找到,跳出循環
                                    if ((e = e.next) == null)
                                        break;
                                }
                            }
                            else if (f instanceof TreeBin) {//紅黑樹
                                validated = true;
                                TreeBin<K,V> t = (TreeBin<K,V>)f;
                                TreeNode<K,V> r, p;
                                //樹有節點且查找的節點不為null
                                if ((r = t.root) != null &&
                                    (p = r.findTreeNode(hash, key, null)) != null) {
                                    V pv = p.val;
                                    //值相同
                                    if (cv == null || cv == pv ||
                                        (pv != null && cv.equals(pv))) {
                                        oldVal = pv;//給舊值賦值
                                        if (value != null)//值覆蓋,replace()調用
                                            p.val = value;
                                        else if (t.removeTreeNode(p))//刪除節點成功
                                            setTabAt(tab, i, untreeify(t.first));//更新當前槽位(數組槽位)節點為樹的第一個節點
                                    }
                                }
                            }
                        }
                    }
                    if (validated) {
                        //如果刪除了節點,更新size
                        if (oldVal != null) {
                            if (value == null)
                                addCount(-1L, -1);//數量-1
                            return oldVal;
                        }
                        break;
                    }
                }
            }
            return null;
        }

  三.總結

  1.  put:使用cas插入,如果是鏈表或樹節點才會加鎖同步操作,提高了性能

    1. 不允許有key或value為null,否則拋出異常;
    2. 在第一次put時初始化table(initTable()),初始化有併發控制,通過sizeCtl變量判斷,sizeCtl<0表示已經有線程在初始化,當前線程就不在進行,否則sizeCtl置為-1(CAS)並創建數組;
    3. 當hash計算出的槽位節點為null時,使用CAS插入元素;
    4. 當hash為MOVED(-1)時,幫助擴容,但可能幫助不了,因為每個線程默認16個桶,如果只有16個桶,第二個線程無法幫助擴容;
    5. 如果hash衝突了,同步槽位節點,如果槽位是鏈表結構,進行鏈表操作,覆蓋舊值或插入到鏈表尾部;如果是樹結構,添加到樹中;
    6. 元素添加到鏈表或樹中,如果鏈表長度大於8,將鏈錶轉換為紅黑樹;
    7. 調用addCount(),對size+1,並判斷是否需要擴容addCount(),如果是值覆蓋操作就不需要調用該方法;
  2. initTable:初始化

    1. 數組table為null才進行初始化,否則直接返回舊數組;
    2. 如果當前sizeCtl小於0,表示有線程正在初始化,則當前線程禮讓CPU,保證只有一個線程正在初始化數組;
    3. 如果沒有線程在初始化,則當前線程CAS將sizeCtl置為-1並創建數組,然後重新計算閥值;
  3. helpTransfer:幫助擴容

    1. 當嘗試插入操作時,發現節點是forward類型,則會幫助擴容;
    2. 每次加入一個線程都會將sizeCtl的低16位+1,同時校驗高16位的標識符;
    3. 擴容最大的幫助線程是65535,這是低16位的最大值限制;
    4. 每個線程默認分配16個桶,如果桶的數量是16,那麼第二個線程無法幫助擴容,即桶被分配完其他線程無法進場擴容;
  4. transfer:擴容和數據遷移

    1. 根據CPU核數平均分配給每個CPU相同數量的桶,如果不夠16個,默認就是16個;
    2. 按照2倍容量進行擴容;
    3. 每個線程在處理完自己領取的區間后,還可以繼續領取,如果還有的話,通過transferIndex變量遞減16實現桶數量控制;
    4. 每次處理空桶的時候,會把當前桶標識為forward節點,告訴put的其他線程說“我正在擴容,快來幫忙”,但如果只有16個桶,只能有一個線程進行擴容;
    5. 如果有了佔位符MOVED,表示已經被處理過,跳過這個桶,繼續推進處理其他桶;
    6. 如果有真正的實際值,那麼就同步加鎖頭節點,防止putVal的併發;
    7. 同步塊里將鏈表拆分成兩份,根據 hash & length 得到是否是0,如果是0,放在新數組低位,反之放在length+i的高位。這是防止下次取值hash找不到正確的位置;
    8. 如果該桶類型是紅黑樹,也會拆分成2個,然後判斷拆分過的桶的大小是否小於等於6,如果是轉換成鏈表;
    9. 線程處理完如果沒有可選區間,且任務沒有完成,則會將整個表檢查一遍,防止遺漏;
  5. addCount:擴容判斷

    1. 當插入結束時,會對size+1,並判斷是否需要擴容的判斷;
    2. 優先使用計數盒子(如果不是空,說明併發了),如果計數盒子是空,使用baseCount變量+1;
    3. 如果修改baseCount失敗,使用計數盒子,如果還是修改失敗,在fullAddCount()中自旋直到CAS操作成功;
    4. 檢查是否需要擴容;
    5. 如果size大於等於sizeCtl且長度小於1<<30,可以擴容;
    6. 如果已經在擴容,幫助其擴容;
    7. 如果沒有在擴容,自行開啟擴容,更新sizeCtl變量為負數,賦值為標識符高16位+2;
  6. remove:刪除元素

    1. 自旋遍曆數量,如果數組或根據hash計算的槽位節點值為null,直接結束自旋返回null;
    2. 如果槽位節點正在擴容,幫助擴容;
    3. 如果槽位節點有值,同步加鎖;
    4. 如果該槽位節點還是沒有任何變化,判斷是鏈表結構類型節點還是樹結構類型節點,通過遍歷查找元素,找到刪除該節點或重新設置頭節點;
    5. 如果刪除了節點,更新size-1,如果有舊值則返回舊值,否則返回null;

  四.參考

  1. https://www.jianshu.com/p/2829fe36a8dd
  2. https://www.jianshu.com/p/487d00afe6ca
  3. https://juejin.im/post/5b001639f265da0b8f62d0f8#comment

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

【其他文章推薦】

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

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

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

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

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

讀懂操作系統之虛擬內存TLB與緩存(cache)關係篇(四)

前言

前面我們講到通過TLB緩存頁表加快地址翻譯,通過上一節緩存原理的講解為本節做鋪墊引入TLB和緩存的關係,同時我們來完整梳理下從CPU產生虛擬地址最終映射為物理地址獲取數據的整個過程是怎樣的,若有錯誤之處,還請批評指正。

TLB和緩存串行訪問(Serial TLB & Cache Access)

這裡會跳過前面對虛擬頁號、虛擬頁偏移量、TLB索引和標記等的詳細分析和計算,不清楚的童鞋請先查看前面文章再來看本文。假設我們有14位的虛擬地址、12位的物理地址,每頁大小為64字節,如下:

 

 

同時假設已完全清楚虛擬地址和物理地址劃分,接下來則是針對虛擬地址和物理地址進行位劃分,如下:

同時我們假設TLB是通過組相聯來進行映射,TLB中有16個條目,4路相聯,所以TLB索引(TI)和TLB標記(TT)在虛擬地址中虛擬頁號進行位劃分如下: 

 

我們假設緩存採取直接映射的機制,緩存大小為64字節,每塊大小為4個字節,說明緩存有16塊即4位,位偏移為2位,所以緩存索引(CI)和緩存標記(CT)在物理地址中進行位劃分如下:

現假設讀取虛擬地址(0x0255),那麼將其劃分為VPN(0x09),VPO(0x15),然後將VPN劃分為TT(0x02)和TI(1)如下:

接下來通過TT(0x02)和TI(1)去查找TLB,如下:

此時我們會發現TLB缺失,緊接着通過VPN(0x0916 = 2110)去頁表中查找得到PPN(0x1716 = 2310),如下: 

 

因其PPO = VPO(0x15),所以計算出物理地址為(23 * 64+21 = 149310 = 0x5D516

然後根據上述物理地址劃分為CT(0x17)、CI(5)、CO(1),如下:

最後通過上述CT(0x17)和CI(5)去查找緩存,此時緩存命中,然後將數據發送到CPU,如下:

從CPU到獲取數據整個的過程是這樣的:【1】CPU產生虛擬地址【2】TLB翻譯成物理地址【3】TLB命中,將物理地址發送到緩存【4】緩存命中返回數據。其中每一個過程涉及到的細節,比如TLB缺失、頁缺失等等前面已有詳細講解,殊途同歸,大致過程則是如下圖解

通過如上可看出此時TLB與Cache是串行訪問的關係,這是最簡單同時也是比較慢的方式,因為不得不等待TLB翻譯完成后才去檢查緩存中是否有數據,如此一來將對CPU處理速度產生重大影響,涉及到大量內存訪問時間。

TLB和緩存并行訪問(Parallel TLB & Cache Access)

當前處理器最普遍的設計是採取TLB和Cache并行的方式,有些也稱之為重疊訪問(Overlapping TLB & Cache Access),從而提高訪問速度,那麼并行訪問到底是如何做的呢?有沒有什麼使用限制呢?這裏我們以Intel Skylake(英特爾第六代微處理器架構)為例來說明,其虛擬地址和物理地址結構大致如下:

看到上述結構我們可以發現物理地址中的PPN和緩存標記(CT)位數相等以及其他,英特爾這樣設計就是為了讓TLB和Cache可以并行訪問。TLB和Cache并行訪問原理:虛擬地址(VA)中的高階位即(VPN)用來查找TLB,而低階位(VPO)用來查找緩存。通過TLB將VPN映射到PPN,此時PPN作為緩存標記(CT),而將VPO中的低階位作為緩存偏移量(CO),高階位作為緩存索引(CI)。有了緩存標記和緩存索引我們就可以查詢到數據,比如CPU產生虛擬地址(0x7916 = 00011110012),此時通過并行訪問則為如下圖解

我們結合上述圖解繼續進行分析將并行訪問分為三種情況,比如上緩存中的tag = 11,同時我們產生的PPN = tag = 11,說明緩存標記等於物理頁號,同時緩存命中,最終返回數據B5給CPU(其一)。假設產生的緩存標記不是11,那麼說明緩存標記不等於頁號或者緩存缺失,但此時TLB命中,那麼將通過TLB中的物理頁號直接訪問主存(其二)。否則做標準的虛擬地址翻譯(其三)。為便於大家理解,我們通過偽代碼形式來說明:

if (cache hit && cache tag = PPN)
  //返回數據到CPU
else if (cache miss || cache tag != PPN && TLB hit) 
  //通過TLB中的PPN訪問主存
else
  //標準地址翻譯

兩種緩存架構(Cache & TLB Access)

緩存索引(Cache index)用於查找數據在緩存中的索引位置,而緩存標記(Cache tag)則是驗證緩存中有哪些數據。從上述對并行訪問原理講解我們知道將虛擬地址中的虛擬偏移量可作為物理緩存索引,這裏我們稱之為虛擬索引,同時我們將VPN轉換為PPN,這種模式稱之為虛擬索引、物理標記緩存架構(Virtual-indexed Physically-tagged Caches),其實我們也可以將虛擬地址中的偏移量作為緩存標記,也就是說虛擬地址中的偏移量(VPO)既作為緩存索引也作為緩存標記,這種緩存架構成為虛擬索引、虛擬標記緩存架構(Virtual-indexed Virtually-tagged Caches),也叫虛擬地址緩存(Virtual Address Caches),接下來我們來分析這兩種緩存架構。

虛擬索引、虛擬標記緩存(Virtual-indexed Virtually-tagged Caches)

 

 

此種緩存架構讓緩存保存虛擬地址,但是現代處理器極少使用這種緩存設計,雖然很塊,但是處理起來很複雜, 比如進行上下文切換時需要刷新緩存(當然可以在地址空間添加ASID),但是即使這樣,由於頁面可以共享而造成處理頁面別名問題,用於直接映射緩存的解決方案,共享頁面的VA必須在緩存索引位中一致,確保訪問同一PA的所有VA將在直接映射的緩存(早期SPARC)中發生衝突,所以大多處理器採用第二種(VA-PA)緩存架構。

虛擬索引、物理標記緩存(Virtual-indexed Physically-tagged Caches)

 

并行TLB & Cache訪問採取的就是此種架構,此種架構要求緩存索引完全包含在虛擬地址中的虛擬偏移量中。緩存標記和PPN相等(當然第一種)當查詢緩存時也執行TLB訪問,它是當前處理器最常見的設計,我們知道緩存使用的是物理地址,而CPU產生的是虛擬地址,這也就意味着沒有TLB就無法完成緩存查找。前面我們了解到緩存數據存儲結構存在直接映射、組相聯、全相聯三種結構,在此種緩存架構中有使用限制,我們首先來看看直接映射。

 

并行訪問的本質在於緩存查詢數據無需等待TLB完成,二者可同時開始,所以當兩者訪問完成后需要進行比較,如果(cache size <=  page size)即(C = L + b) <= P才有效,因為對於緩存的所有輸入都無需進行任何翻譯。

通過組相聯增加了緩存的關聯性從而減少索引到緩存所需地址的位數,在訪問完成後進行比較,如果(cache size) / (associativity) ≤ page size即(C <= P + A)才有效。對於緩存和TLB都採用的組相聯從而減少缺失率,所以對於并行訪問中的緩存組相聯映射必須滿足(cache size) / (associativity) ≤ page size。那麼問題來了,如果一個緩存大小為64KB,採用2路相聯,頁大小>=4k,那麼可以進行并行訪問TLB & Cache嗎?很顯然不能,如下

緩存大小:64KB = 216     ————-》 C = 16

組相聯:2                        ————-》 A = 1

頁大小: 4KB = 212        ————–》P >= 12

那麼問題又來了,對於一個16位的虛擬地址,頁大小為64字節,緩存大小為256b,採用8路相聯的1級緩存且有16塊,那麼可以并行訪問TLB &Cache嗎?請輸出原因。

總結

本節我們詳細介紹了TLB &Cache二者的關係,採用并行訪問通過VPN查找TLB,VPO查詢緩存同時進行來提高訪問速度。下一節我們進入頁表數據結構的詳細講解,謝謝。 

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

防疫封鎖 馬達加斯加狐猴族群獲得喘息

摘錄自2020年9月24日公視報導

安達西貝自然保護區是馬達加斯加島生態觀光的熱門地點之一,過去五個月因為新冠病毒疫情造成的封鎖以及邊境管制,當地最具代表性的動物狐猴,在原始雨林裡享受了沒有人類嘈雜群聚,寧靜自得的生活。島內九月起放寬了封鎖政策,觀光人數開始緩步回升,但因為國際航班仍然停擺,多數是馬達加斯加人自己的島內旅遊。

3月起陷入封鎖寒冬的旅遊業者,樂見顧客回歸,但富裕的外國觀光客進不來,仍然入不敷出。經濟壓力也傷害了自然環境。森林嚮導發現,最近林木被濫伐破壞的情況比過去30年都要嚴重。

馬達加斯加的旅遊業佔全國經濟的7%,堪稱重要支柱,而雨林生態體系脆弱,全國上百種的狐猴,幾乎都已經因為盜獵、暖化與森林砍伐,名列國際自然保護聯盟的紅色瀕危名單,其中30種更已經是極度瀕危,離滅絕只差一步。

生物多樣性
國際新聞
馬達加斯加
防疫
狐猴
動物與大環境變遷

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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