法下週解除封鎖令 巴黎交通主幹道規劃給自行車

摘錄自2020年5月5日自由時報報導

法國為遏止武漢肺炎(新型冠狀病毒疾病,COVID-19)疫情擴散,自3月17日起實施全國封鎖,期間2度延長封鎖禁令至5月11日;面對下週即將解除的封鎖令,巴黎市長伊達戈(Anne Hidalgo)將把最繁忙的主要交通幹道規劃給自行車,以減少民眾對大眾運輸工具的依賴,進而避免群聚感染。

伊達戈今(5日)指出,城市解封後共將保留50公里原先的汽車道給自行車使用,另外將有30條街道將被設置為行人專用道,她強調,「特別是在學校周圍,以避免人群聚集」。

法國政府也宣布了一項2000萬歐元(約新台幣6.5億元)的自行車計畫,用以刺激民眾在封鎖解除後對自行車的使用度,其中包括每人50歐元(約新台幣1620元)的自行車維修或調整補貼。

生活環境
國際新聞
法國
檢疫封鎖
解除
自行車道

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

真不是隨便選的,原來車漆顏色的選擇有那麼多門道

而且合適的車漆能讓我們的愛車有着更好的外觀效果。

筆者總結:

所以說車漆的選擇是有一定門道,這是我們在購車前就應該了解的,畢竟這關乎到我們用車養車的各個方面。而且合適的車漆能讓我們的愛車有着更好的外觀效果。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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

30萬價格卻有50萬級的大氣場豪華中大型轎車

內飾方面也保持了旗艦車型應有的氣場,棕色為主的色調,大量實木材質飾板提升了不少檔次感,沉穩而大方,電動吸合門這麼高逼格的配置40。90萬的輝昂居然配備了,要知道同價位的BBA都沒有的東西,檔次感一下子就上去了。

在外打拚多年的老陳買了輛車子,過年帶着媳婦回到村子。

村民都投來了羡慕的眼光,鄰居家小黃問他:“陳哥賺了不少錢吧,都換了五六十萬的車子了”。

老陳心裏偷着樂:“嘿嘿,這豪華中大型轎車裸車才20幾萬呢,氣場就是強大,”

一說起豪華中大型轎車,大家都犹如耳濡目染,基本是被德系車如奧迪A6L、奔馳E級、寶馬5系等車型所包攬,但是如果價格去到30萬出頭,就只能是買到乞丐版車型了,那還不如買一些擁有強大氣場而且有着很高行車品質的車型,而且性價比也比較高,一起來看一下吧!

雷克薩斯-ES

指導價:29.80-49.80萬

說起雷克薩斯品牌總是給人一種溫文爾雅的感覺,前臉誇張的紡錘形設計進氣格柵,搭配外圈鍍鉻飾條,極具視覺衝擊感,提升了不少氣場,流暢的車身線條,立體感十足的尾燈,使得整輛車的氣質都提升了。

不同配置間的車型內飾材質也是略有不同,但是做工和品質還是一如既往的上乘,即使是最低配車型,也配備了胎壓監測、無鑰匙啟動/進入、上坡輔助、電動天窗、倒車影像、自動頭燈等配置,非常實用。

座椅採用了打孔皮革材料,坐上去感覺很厚實,與身體十分貼合,舒適性好,動力方面提供了2.0L最大功率167馬力或者2.5L最大功率184馬力的發動機,匹配6擋手自一體變速器,輕鬆好開才是重點,輸出和換擋都非常平順。

上汽大眾-輝昂

指導價:34.90-65.90萬

輝昂是上汽大眾打造的首款中大型轎車,與奧迪A6L出自MLB同一平台,足以吸引人的眼球,在大眾透視套娃式的外觀設計中,輝昂還是有這獨特的氣質的,寬大的前臉線條,雙邊四齣的排氣管裝飾罩,氣場還是挺嚇唬人的。

內飾方面也保持了旗艦車型應有的氣場,棕色為主的色調,大量實木材質飾板提升了不少檔次感,沉穩而大方,電動吸合門這麼高逼格的配置40.90萬的輝昂居然配備了,要知道同價位的BBA都沒有的東西,檔次感一下子就上去了。

輝昂的軸距達到了3009mm,想怎麼坐就怎麼坐,蹺二郎腿什麼的不在話下,寬厚的座椅設計人體工程學很到位,乘坐舒適性良好,動力提供了2.0T或者3.0T V6發動機的選擇,搭配7擋雙離合變速器,開起來很輕鬆就能上手駕馭,整車調校偏舒適,底盤是一如既往的沉穩。

英菲尼迪(進口)-Q70

指導價:39.98-64.98萬

作為英菲尼迪家族的旗艦豪華轎車,Q70L有着略帶攻擊性的外觀設計,菱形進氣格柵變得更加年輕了,犀利的全LED大燈組被大面積的鍍鉻飾條包裹,豪華氛圍濃厚,而車尾部的造型非常的飽滿、健碩,整體風格更加運動化。

環抱式的內飾設計給人很熟悉的感覺,真皮包裹的中控台手感很好,大量木紋飾板的點綴,加上中控上的石英鐘,豪華感非常強,除了最低配車型外,全系標配BOSE音響,還有電動吸合門也是全系標配的,這配置實在夠強大的。

座椅寬大厚實,對身體的各部位支撐到位,乘坐感受很出色,後排空間絕對是Q70L的一大亮點,3050mm的軸距競爭力很強,動力提供了V6布局的2.5L或者3.5L自然吸氣發動機,全系標配駕駛模式切換,動力輸出很線性,發動機聲音在高轉速是令人興奮的,但是不會給人很激烈駕駛的慾望。

總結:30萬左右的價格,選擇這些非主流的中大型豪華轎車,卻有着50萬級別車該有的氣場,而且配置上比寶馬奔馳奧迪那些主流品牌車型更為豐富,可以作為購車的一個新選擇。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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

論冠道具備何種洪荒之力,從SUV戰場突圍

在通常情況下,車輛處於前輪驅動行駛狀態,這套系統會實時監控發動機扭矩以及各車輪轉速等信息參數。當需要後輪的驅動力時,电子系統就會將動力分配給後輪,得到一定的脫困能力和爬坡能力,比我們平常熟悉的適時四驅系統智能上不少。

熟悉數碼產品的你們,應該都會知道有句話叫做“索尼大法好”,這讓索尼上升到了宗師級的地位,這滿滿的都是情懷。而在汽車界,“本田大法好”也是我們經常聊到的話題,本田的“黑科技”使不少朋友們都成為了本田粉,這也是滿滿的情懷之說。

繼小型化10AT變速箱之後,前不久,本田又要在變速箱領域中想要大顯身手,據海外媒體報道,本田已向日本專利局為其全新變速箱提出申請,這台變速箱有11個擋位並且包括了3個離合器。

這再次證明了本田在研製和調校變速箱有着絕對的實力,而全新上市的冠道則採用了來自德國ZF的9AT變速箱,相信大家都不陌生,它之前在路虎攬勝極光、Jeep自由光上都有搭載。

按照本田的設計和理論來說,更多的擋位和離合器會讓換擋響應效率更高,跳擋更加平順,而且還能有效減少扭矩損失,意味着能夠達到更好的燃油經濟性。

毫無疑問,搭載着9AT變速箱的冠道駕駛起來平順之餘,燃油經濟性同樣突出。根據官方的說法,這台9速自動變速箱中從6擋開始即為超速擋,也就是輸入轉速低於輸出轉速,更多的超速擋意味着在寬泛的車速區間能以更經濟的轉速行駛,這也是省油的原因之一。

告訴大家一個小秘密,冠道作為大塊頭喝93號(京92號)汽油,油箱容積為57L,加滿一箱油才不到四百塊,能夠省不少用車成本。

思域TYpE-R在本田粉心目中的地位是非常高的,紅頭髮動機對我們這一代人來說意味着本田的“最強動力”,這台發動機征戰了無數次紐博格林北環賽道,7分50秒這個数字在本田粉心中一直揮之不去。

採用2.0T發動機的冠道,其發動機就是源自思域TYpE-R的紅頭髮動機而打造的,作為一台中型SUV,採用了性能車的發動機也是實屬罕見。

最大功率 200 kW(272ps)/6500rpm,最大扭矩 370N m/2250-4500rpm,單純從數據上看,或許你以為這就是一台小鋼炮。

冠道在同級別車型中擁有着最強動力,比起2.0T漢蘭達最大功率162kW(220ps)強上不是一星半點,8秒內能時速破百,看到這裏你服氣嗎?你要想想這大塊頭擁有着1.8噸左右的車重…

●VTEC渦輪增壓技術,有着更高更徹底的燃燒效率;

●帶電動廢氣門的高功率渦輪增壓以及雙進排氣VTC,告別渦輪遲滯;

●全系標配發動機節能自動啟停系統,進一步實現燃油經濟性.

對於一台中型SUV來說,車輛的脫困能力自然要求不低,所以作為一台中型SUV的冠道,在四驅系統方面一點也不馬虎,採用的四驅系統為全路況的Real-Time AWD智能四驅,那麼該如何理解呢?

在通常情況下,車輛處於前輪驅動行駛狀態,這套系統會實時監控發動機扭矩以及各車輪轉速等信息參數。當需要後輪的驅動力時,电子系統就會將動力分配給後輪,得到一定的脫困能力和爬坡能力,比我們平常熟悉的適時四驅系統智能上不少。

▲IDM多路況駕駛適應系統

擁有一套完善優秀的四驅系統還不夠,講求越野或是舒適還是得靠底盤,冠道的IDM多路況駕駛適應系統既能滿足城市駕駛也能應對越野路況,總能給人一種最合適的駕駛感受,SpORT OR COMFORT?這是你的選擇。

或許很多人都質疑冠道為什麼沒有7座版本,但從另外一個角度來想,其實這才是明智的選擇。

相比雞肋的第三排,還不如更加寬敞的第二排來得實在,老實說,七座SUV的第三排座椅的利用率是真的低,換作是誰都不願意去第三排座椅坐,不是頂頭就是雙腳放着難受,總之第三排座椅的乘坐體驗是不太好。

就特別心疼老人家坐第三排座椅,為了讓年輕人或小孩子坐前排,通常都是強顏歡笑說不難受,可是作為兒子來說,心裏真的不好受,家裡人多還是選擇7座的MpV更好。

如果你是一名公務人員,經常接待客戶的話,冠道的超寬敞空間能給你的客戶帶來輕鬆的乘坐體驗,加上雙層靜音玻璃、12個音響環繞、後排獨立空調出風口、電動遮陽簾以及超大的全景天窗,這些處處都能給你重要的客戶或長輩帶來與眾不同的體驗。

當然,冠道有着大容量滑道式對開手扶箱,想一想从里面拿出一份資料給客戶看的情景,會心一笑。

冠道採用了本田CONCEpT D概念車的設計理念,什麼?你不知道CONCEpT D長啥樣?下面放圖希望大家都能HOLD住…

從CONCEpT D的設計理念中看,近來本田上市的車型都有着其中一些設計元素,從外貌上更新換代,也能猜到了本田往後推出車型的外觀設計方向。

近兩年,在前臉的設計上大家都喜歡將大燈總成和中網格柵連接在一起,這樣更顯得前衛一些。冠道基於CONCEpT-D的原型打造,我們不難看出有不少共同之處,最令人喜歡的莫過於就是全系LED的燈光,處處彰顯着高端大氣的形象。

足以可以用“迷人”兩個字來形容冠道的鷹翼式全LED前大燈,另外還搭配了ACL主動轉向照明系統,根據車輛的轉向,調節燈光動態,減少了行駛中的“盲區”,增加轉彎時的安全。

這樣的外觀設計,你覺得能支撐起冠道之名嗎?

冠道帶着洪荒之力,

從SUV戰場中突圍,

你期待嗎?

總結:

本田一向以緊湊型轎車和SUV打天下,如今冠道的推出,不斷完善本田SUV家族的矩陣,雖說冠道的起步定價高,但未來將會推出1.5T發動機版本的冠道,價格會更加親民一些,而一款優質的SUV車型擺在你眼前,不好好珍惜,又更待何時呢?本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※回頭車貨運收費標準

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

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

年輕就要往前跑!2016奇瑞 “強音酷跑節”收官站引爆廣州

如今年輕的新生代正在成為車市主流消費群體,此次奇瑞與中國移動咪咕善跑的合作正是把握住了市場消費趨勢,率先在業內嘗試汽車與跑步領域的跨界,成功打造出汽車行業的跨界營銷典範。自今年10月15日以來,“強音酷跑節”相繼跑遍了合肥、蘇州、大連、青島、西安、成都、長沙等國內各大城市。

轉眼又到了周一,上周末廣州天氣好得不像話。天藍得像洗過一樣。就在12月3日,奇瑞汽車咪咕善跑“強音酷跑節”全國系列活動終極之戰—廣州站,震撼來襲!

為慶祝“強音酷跑節”完美收官,舞台上不僅彙集了星光熠熠的2016“中國新歌聲”新科冠軍蔣敦豪、2015“中國好聲音”總冠軍李琦和人氣歌手張瑋,還有專為冠軍打造的金色瑞虎7“冠軍版”在萬眾矚目中正式亮相,並由奇瑞官方贈予2016“中國新歌聲”冠軍蔣敦豪。這款全球唯一的特別定製版瑞虎7車身噴塗了閃耀的黃金車身顏色,內飾交織了金色縫線,彰顯出“冠軍版”的優雅與高貴,現場圈粉無數。此前, 瑞虎7曾作為2016浙江衛視“中國新歌聲”官方指定用車,見證蔣敦豪披荊斬棘、邁向冠軍的不凡之路,也正是在2016“中國新歌聲”冠軍誕生之夜,蔣敦豪對瑞虎7一見傾心。

奇瑞汽車華南大區總經理翟小兵為《中國新歌聲》冠軍蔣敦豪頒發榮譽車主證書

瑞虎7定位於“未來派超動感SUV”,是奇瑞戰略2.0時代為年輕消費群體量身打造的一款全新旗艦SUV。自9月20日上市以來,以澎湃的動力、超凡卓越的性能以及無與倫比的前瞻設計,樹立新一代中國品牌SUV的巔峰高度,更取得了首月訂單突破2萬的傲人成績。用戶口碑在汽車之家、易車等主流門戶網站高居同級榜首,成為時下年輕一族購買高品質SUV的首選。

2016《中國新歌聲》冠軍蔣敦豪

當冠軍遇上中國品牌的冠軍車型,兩個冠軍的光芒交相輝映。2016年,瑞虎7與“中國新歌聲”強強攜手,共同演繹“活耀不凡”的品牌精神,這也是中國汽車品牌第一次和現象級的原創綜藝節目合作,體現出瑞虎7的實力和視野。《中國新歌聲》作為今年夏天最受年輕人歡迎的綜藝節目,4.21的高收視率、超52億的網絡播放以及高互動社交聲量的頂級Ip號召力也是吸引瑞虎7冠名《中國新歌聲》的原因之一。兩者的目標受眾都是當今社會新生代年輕人,真實、勇氣、自信,用獨特魅力傳遞积極進取的正能量。值得一提的是,“活耀不凡”還是瑞虎7與“蔣敦豪們”的共同特徵:不甘平庸、執着追求、不斷挑戰自我的夢想激情。瑞虎7“冠軍版”正以一種獨特的精神致敬不凡,為時代唱響最美強音。

瑞虎7冠軍版亮相

強音酷跑,8城20萬公里跑遍全國

晚上19:00,在廣州海心沙亞運公園,由艾瑞澤5、瑞虎7一路閃耀領跑,在五彩的電光氛圍中,5000多名年輕人踩着勁爆的電音節拍,釋放內心的熱愛和激情,縱享奔跑之樂!5公里的熒光炫跑不僅有高顏值的美女跑團,還有動感熱辣的舞蹈嗨翻全場。由蔣敦豪、李琦及張瑋等歌手獻上活力四射的“好聲音”,讓現場秒變最熱狂歡派對。

李琦動感獻唱

當運動不止是運動,它的意義將變得更加深遠!作為各自領域的領頭羊,此次“強音酷跑節”由奇瑞汽車與中國移動咪咕善跑強強聯手,針對各自年輕目標用戶群體,融入汽車、跑步、音樂等生活潮流元素,堪稱珠聯璧合。如今年輕的新生代正在成為車市主流消費群體,此次奇瑞與中國移動咪咕善跑的合作正是把握住了市場消費趨勢,率先在業內嘗試汽車與跑步領域的跨界,成功打造出汽車行業的跨界營銷典範。

自今年10月15日以來,“強音酷跑節”相繼跑遍了合肥、蘇州、大連、青島、西安、成都、長沙等國內各大城市。所到之處,掀起了一陣陣青春風暴。歷時50天,8座城市,里程超過20萬公里,吸引了全國線上線下73萬參与人次,455家媒體報道,累計活動曝光更高達3.8億次,一系列令人欣喜的數據反映出此次營銷跨界的成功。

張瑋high歌引爆全場

通過“強音酷跑節”,奇瑞在85后、90后群體中的知名度和好感度逐步提升,也以實際行動帶動更多年輕人加入到跑步的行列,“青春領跑”理念深入人心。奇瑞汽車營銷公司副總經理范星表示:“希望通過強音酷跑節,把在音樂和跑步過程中體會到的正能量,傳遞給更多的城市年輕人,讓更多人在跑步中得到健康、快樂和友誼。同時也希望大家看到,奇瑞還很年輕,正在向著陽光努力奔跑,也期望年輕人與我們一道奔跑向前,勇敢追逐自己的夢想。”

營銷“年輕化” 奇瑞2.0向上突破

四年前,奇瑞開始了戰略2.0階段新一代產品的開發,致力於更滿足以追求品質生活的年輕消費群體的需求。2016年伊始,奇瑞以“Fun 精彩無限”為品牌核心底蘊,將品牌年輕化提升至企業戰略層面。

隨着年輕化戰略的推進與深化,以“年輕化”為切入點,奇瑞通過年輕人喜愛的娛樂化溝通平台及跨界營銷,建立起與年輕人溝通的橋樑。也讓更多年輕消費者近距離感受奇瑞2.0產品的品質,傳遞出奇瑞的品牌特質,進一步提升奇瑞品牌在年輕人群中的影響力。奇瑞“強音酷跑節”就是以音樂和運動為載體,抓住了年輕人最時尚的生活方式。艾瑞澤5、瑞虎7作為活動車型,讓更多年輕人看到奇瑞2.0產品的青春與動感,大大促進了產品銷量的提升。

廣州站的落幕為奇瑞“強音酷跑節”畫上了一個圓滿的句號,在一系列創新營銷的助推下,艾瑞澤5和瑞虎7領銜熱銷。上市以來,艾瑞澤5連續7個月銷量破萬,更以累計253天銷量突破十萬輛的成績刷新了中國品牌增速最快的新車記錄。而瑞虎7上市首月訂單即突破2萬輛,一度一車難求。相信通過一系列的強勢營銷和強大的產品力,奇瑞未來會有更突出的市場表現,推動奇瑞品牌的再次飛躍,引領中國品牌再向上。

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

※回頭車貨運收費標準

20-30多萬這個價格區間,可以買到最好的是什麼車?

0升自然吸氣發動機的動力水平。而5系入門車型的2。0T發動機型號為N20B20,沒錯就是寶馬那台著名的2。0T發動機了,寶馬的操控之王328i也用的N20B20,但是寶馬給520LI車型裝上的是一台低功率版本,最大功率僅僅184馬力,最大扭矩270牛米,這樣的動力水平只能說讓人失望。

如果你問我預算20-30萬注重生活品質與享受有什麼車型推薦,那麼我一定會推薦凱迪拉克XTS。

XTS定位於中大型轎車,和奧迪A6L/寶馬5系/奔馳E級同屬一個級別,但是XTS的定價卻是比這些車型足足低了近十萬,那麼定價低了這麼多的前提下,在品質上和這些車型相比如何呢?今天小編就拿寶馬5系的入門車型與凱迪拉克XTS的入門車型做個對比。

凱迪拉克XTS 2016款 28T 技術型

指導價:34.99萬(下文簡稱XTS)

寶馬5系 2017款 520Li 典雅型

指導價:43.56萬(下文簡稱5系)

凱迪拉克XTS足足比寶馬5系便宜了8.57萬。

外觀

XTS勻稱/5系運動化

XTS的外觀採用凱迪拉克獨特的鑽石切割設計,整車的線條十分有力,直線條的運用恰到好處,使得XTS顯得十分修長有氣派。XTS的長度為5131mm,比寶馬5系長了76mm,乘坐艙最大化的設計理念使得XTS的乘坐艙十分寬敞。而值得一提的是XTS的行李箱空間也達到了537升,這在中大型車中也是十分大的。

5系的外觀採用運動化設計,短前懸長車頭的造型十分有運動感,不過過長的車頭侵佔了不少的乘坐艙空間,使得5系雖然長度達到5055mm,但是車內乘坐空間差強人意。

內飾

5系用料差/XTS奢華

和外觀一樣,XTS的內飾設計上更多採用平直線條,使得整車更顯穩重與莊嚴,更加有豪華車的派頭,而在內飾用料上XTS也是不惜成本,XTS的內飾大量使用材質細膩的真皮包裹,而木紋材質、啞光鋁合金、鋼琴烤漆面板等十分顯檔次的材料的使用也烘託了車內的豪華氛圍,觸控面板/大尺寸液晶屏的使用也讓車內科技感十足。

寶馬5系的內飾設計造型使用老一代的寶馬家族風格,這也和這一代寶馬5系車型偏老有關,2010年面世的現款5系已經走過了6個年頭了,設計上已經有些跟不上時代了,而在用料上寶馬5系也是飽受詬病,大量硬塑料的使用使得車內檔次感十分差,基本上和20萬的中級車無異。

2.0T動力

XTS動力更強勁

兩款入門車型都用了2.0T的動力系統,2.0T也是現在的主流的動力系統,那麼兩款車的2.0T發動機有什麼差異呢?XTS的2.0T發動機型號為LTG,最大功率269馬力,最大扭矩400牛米,這已經相當於一台4.0升自然吸氣發動機的動力水平。

而5系入門車型的2.0T發動機型號為N20B20,沒錯就是寶馬那台著名的2.0T發動機了,寶馬的操控之王328i也用的N20B20,但是寶馬給520LI車型裝上的是一台低功率版本,最大功率僅僅184馬力,最大扭矩270牛米,這樣的動力水平只能說讓人失望。

音響

XTS標配BOSE音響

為什麼音響要單獨說呢?因為音響對於一台豪車是十分重要的,愜意的旅途中沒有好的音樂相伴,對不少豪車買家來說都是難熬的。凱迪拉克的運動性能成就聞名世界,但是另一個不為人知的就是凱迪拉克在音響方面的造詣,XTS的音響在汽車開發之初便傾力設計,能夠滿足對音樂最嚴苛的需求。XTS全系標配BOSE音響,音質無可挑剔,小編聽過之後都迷上了。

5系使用的普通的6喇叭音響,咳咳,就不多說了。

配置

XTS配置更加實用

在配置上兩車可以說是打成平手,雖然5系的配置更多,但是XTS的配置更加實用,全景天窗、膝部氣囊、R18輪轂、BOSE音響、定位互動服務等都更加的貼近用戶需要,而且考慮到他們之間8.57萬的差價,XTS顯然更加划算。

說了這麼多,20多萬也買不到XTS呀?

錯!凱迪拉克即將退出的XTS猴年限量版預售價26.99萬。雖然這款車型還未上市,但是據稱這款車型將保持凱迪拉克一如既往的高配置水平,5131*1852*1501mm的大尺寸,強勁的2.0T動力以及奢華的內飾,預售價26.99萬的XTS猴年限量版的競爭力在這個價位幾乎是無敵的存在。26.99萬買一款純正美系豪華中大型轎車,還有什麼好猶豫的呢?

XTS猴年限量版的推出降低了購買XTS的門檻,使得更多人可以加入到XTS大家庭來,可以和現有的熱愛生活、注重生活品質與生活質量、事業有成工作高效的XTS車主一起相處,共同體會生活的真諦。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

※回頭車貨運收費標準

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

這些配備了省油利器的自主SUV僅7萬起

99萬作為一款小型SUV,森雅R7擁有着圓潤飽滿的外觀,小巧時尚的設計很討人喜歡,前臉不規則的中網樣式搭配着造型別緻的大燈,增添了幾分硬朗的氣息。內飾的設計很有層次感,黑銀色搭配拉絲面板,給人很運動的感覺,9英寸的中控大屏是一大亮點,包含了手機互聯和導航等功能,自動防炫目后視鏡出現在尊貴型車型上,檔次感瞬間提升了不少。

隨着科技的發展越來越迅速,汽車技術也在不斷的進步,自動變速箱的出現解決了我們黃金左腳的命運,使駕駛者在擁堵的城市中輕鬆地駕駛車輛,那麼隨着燃油價格的不斷提升,人們有沒有想出比較省油的汽車技術呢?

答案是肯定的,那就是發動機自動啟停系統,在車輛臨時停車等紅燈的時候,會自動熄火,待汽車需要重新啟動時,又能快速啟動發動機,大大的減小了油耗和廢氣的排放,綜合下來此項技術可以節約車子一年5%-15%的燃油哦,來看一下哪些自主品牌SUV都有配備這項技術的吧!

奇瑞汽車-瑞虎7

指導價:9.79-15.39萬

說瑞虎7是奇瑞目前最好的SUV一點也不為過,時尚精緻的外觀,凌厲的腰線和車身比例非常的協調,三叉戟式的大燈和造型獨特的進氣格柵使其看上去辨識度很高。

內飾無論是做工還是用料都給人留下深刻的印象,大量帶縫線的皮質材料和軟質搪塑工藝材料,豪華感十足,簡潔的中控大屏、自動頭燈(LED光源)、座椅加熱、無鑰匙進入/啟動等配置十分齊全。

2650mm的軸距雖在同級別對手中並不佔優,但是實際的乘坐感受還是表現很出色的,座椅的包裹性好,肩部支撐很到位,動力方面提供1.5T+6擋手動/雙離合變速器,或者2.0L+CVT變速箱的組合,懸架方面則採用了常規的前麥弗遜后多連桿式獨立懸架。

一汽吉林-森雅R7

指導價:6.89-9.99萬

作為一款小型SUV,森雅R7擁有着圓潤飽滿的外觀,小巧時尚的設計很討人喜歡,前臉不規則的中網樣式搭配着造型別緻的大燈,增添了幾分硬朗的氣息。

內飾的設計很有層次感,黑銀色搭配拉絲面板,給人很運動的感覺,9英寸的中控大屏是一大亮點,包含了手機互聯和導航等功能,自動防炫目后視鏡出現在尊貴型車型上,檔次感瞬間提升了不少。

森雅R7的軸距為2600mm,在這個價位車型中比較有優勢,無論是前後排的頭部空間還是腿部空間都相當寬敞;動力方面全系搭載1.6L自然吸氣發動機,最大功率116馬力,匹配5擋手動或者6擋手自一體變速器,全系標配發動機啟停功能,油耗表現更出色。

長安汽車-長安CS15

指導價:5.79-7.79萬

長安CS15的外觀充滿了個性化的設計元素,稜角分明的造型和豐富的線條相互搭配,看上去顯得更為硬朗,中網的造型也是獨樹一幟,側面較高的腰線設計,使得其車門肌肉感十足,整車是偏向運動的設計路線。

內飾為飛翼式的家族設計風格,紅色縫線的三幅式方向盤、炮筒式的儀錶盤有着濃厚的運動味道,製作工藝堪比合資車,胎壓監測、無鑰匙進入/啟動、上坡輔助、倒車影像等配置一應俱全。

雖然CS15是一款小型SUV,軸距也只有2510mm,但是內部空間完全超出你的想象,乘坐感受相當舒適,大大小小的儲物格達到39處之多,便利性很強,全系採用1.5L+5擋手動/5擋雙離合的動力組合,8萬塊買自動擋性價比是相當高的。

總結:瑞虎7的價格相對來說有些高,但畢竟是跨級別的,做工水平整體很高,堪比合資車,森雅R7的表現中規中矩,全系標配發動機啟停非常厚道,長安CS15的性價比最高,麻雀雖小五臟俱全,適合年輕人的第一台車。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

溫故知新-多線程-深入剖析AQS

目錄

  • 摘要
  • AbstractQueuedSynchronizer實現一把鎖
  • ReentrantLock
    • ReentrantLock的特點
    • Synchronized的基礎用法
    • ReentrantLock與AQS的關聯
    • AQS架構圖
    • acquire獲取鎖
      • tryAcquire
      • hasQueuedPredecessors
      • acquireQueued
      • setHead
      • shouldParkAfterFailedAcquire
      • parkAndCheckInterrupt
      • cancelAcquire
    • unlock解鎖
      • release
      • tryRelease
      • unparkSuccessor
    • 中斷恢復
  • 其它
  • 參考
  • 你的鼓勵也是我創作的動力
  • Posted by 微博@Yangsc_o
  • 原創文章,版權聲明:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0

摘要

本文通過ReentrantLock來窺探AbstractQueuedSynchronizer(AQS)的實現原理,在看此文之前。你需要了解一下park、unpark的功能,請移步至上一篇《深入剖析park、unpark》;

AbstractQueuedSynchronizer實現一把鎖

根據AbstractQueuedSynchronizer的官方文檔,如果想實現一把鎖的,需要繼承AbstractQueuedSynchronizer,並需要重寫tryAcquire、tryRelease、可選擇重寫isHeldExclusively提供locked state、因為支持序列化,所以需要重寫readObject以便反序列化時恢復原始值、newCondition提供條件;官方提供的java代碼如下(官方文檔見參考連接);

public class MyLock implements Lock, java.io.Serializable {
    private static class Sync extends AbstractQueuedSynchronizer {
      
        // Acquires the lock if state is zero
        @Override
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // Releases the lock by setting state to zero
        @Override
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // Provides a Condition
        Condition newCondition() {
            return new ConditionObject();
        }

        // Deserializes properly
        private void readObject(ObjectInputStream s)
                throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
      
       // Reports whether in locked state
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }

    /**
     * The sync object does all the hard work. We just forward to it.
     */
    private final Sync sync = new Sync();

    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }


    private static volatile Integer value = 0;

    public static void main(String[] args) {

        MyLock myLock = new MyLock();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                myLock.lock();
                value ++;
                myLock.unlock();
            }).start();
        }
        System.out.println(value);
    }
}

上面是一個不可重入的鎖,它實現了一個鎖基礎功能,目的是為了跟ReentrantLock的實現做對比;

ReentrantLock

ReentrantLock的特點

ReentrantLock意思為可重入鎖,指的是一個線程能夠對一個臨界資源重複加鎖。ReentrantLock跟常用的Synchronized進行比較;

Synchronized的基礎用法

Synchronized的分析可以參考《深入剖析synchronized關鍵詞》,ReentrantLock可以創建公平鎖、也可以創建非公平鎖,接下來看一下ReentrantLock的簡單用法,非公平鎖實現比較簡單,今天重點是公平鎖;

public class ReentrantLockTest {

    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock(true);
        reentrantLock.lock();
        try {
            log.info("lock");
        } catch (Exception e) {
            log.error(e);
        } finally {
            reentrantLock.unlock();
            log.info("unlock");
        }
    }
}

ReentrantLock與AQS的關聯

先看一下加鎖方法lock

  • 非公平鎖lock方法

    compareAndSetState很好理解,通過CAS加鎖,如果加鎖失敗調用acquire;

/**
 * Performs lock.  Try immediate barge, backing up to normal
 * acquire on failure.
 */
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
  • 公平鎖lock方法
final void lock() {
    acquire(1);
}
  • AQS框架的處理流程

​ 線程繼續等待,仍然保留獲取鎖的可能,獲取鎖流程仍在繼續,分析實現原理

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

總結:公平鎖的上鎖是必須判斷自己是不是需要排隊;而非公平鎖是直接進行CAS修改計數器看能不能加鎖成功;如果加鎖不成功則乖乖排隊(調用acquire);所以不管公平還是不公平;只要進到了AQS隊列當中那麼他就會排隊;

AQS架構圖

美團畫的AQS的架構圖,很詳細,當有自定義同步器接入時,只需重寫第一層所需要的部分方法即可,不需要關注底層具體的實現流程。當自定義同步器進行加鎖或者解鎖操作時,先經過第一層的API進入AQS內部方法,然後經過第二層進行鎖的獲取,接着對於獲取鎖失敗的流程,進入第三層和第四層的等待隊列處理,而這些處理方式均依賴於第五層的基礎數據提供層。

AQS核心思想是,如果被請求的共享資源空閑,那麼就將當前請求資源的線程設置為有效的工作線程,將共享資源設置為鎖定狀態;如果共享資源被佔用,就需要一定的阻塞等待喚醒機制來保證鎖分配。這個機制主要用的是CLH隊列的變體實現的,將暫時獲取不到鎖的線程加入到隊列中。

CLH:Craig、Landin and Hagersten隊列,是單向鏈表,AQS中的隊列是CLH變體的虛擬雙向隊列(FIFO),AQS是通過將每條請求共享資源的線程封裝成一個節點來實現鎖的分配。

  • 非公平鎖的加鎖流程
  • 公平鎖的加鎖流程
  • 解鎖公平鎖和非公平鎖邏輯一致

加鎖:

  • 通過ReentrantLock的加鎖方法Lock進行加鎖操作。
  • 會調用到內部類Sync的Lock方法,由於Sync#lock是抽象方法,根據ReentrantLock初始化選擇的公平鎖和非公平鎖,執行相關內部類的Lock方法,本質上都會執行AQS的Acquire方法。
  • AQS的Acquire方法會執行tryAcquire方法,但是由於tryAcquire需要自定義同步器實現,因此執行了ReentrantLock中的tryAcquire方法,由於ReentrantLock是通過公平鎖和非公平鎖內部類實現的tryAcquire方法,因此會根據鎖類型不同,執行不同的tryAcquire。
  • tryAcquire是獲取鎖邏輯,獲取失敗后,會執行框架AQS的後續邏輯,跟ReentrantLock自定義同步器無關。
  • 流程:Lock -> acquire -> tryAcquire( or nonfairTryAcquire)

解鎖:

  • 通過ReentrantLock的解鎖方法Unlock進行解鎖。
  • Unlock會調用內部類Sync的Release方法,該方法繼承於AQS。
  • Release中會調用tryRelease方法,tryRelease需要自定義同步器實現,tryRelease只在ReentrantLock中的Sync實現,因此可以看出,釋放鎖的過程,並不區分是否為公平鎖。
  • 釋放成功后,所有處理由AQS框架完成,與自定義同步器無關。
  • 流程:unlock -> release -> tryRelease

acquire獲取鎖

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
        selfInterrupt();
    }
}

tryAcquire

acquire方法首先會調tryAcquire方法,需要注意的是tryAcquire的結果做取反;根據前面分析,tryAcquire會調用子類的實現,ReentrantLock有兩個內部類,FairSync,NonfairSync,都繼承自Sync,Sync繼承AbstractQueuedSynchronizer;

實現方式差別在是否有hasQueuedPredecessors() 的判斷條件

  • 公平鎖實現
/**
 * Fair version of tryAcquire.  Don't grant access unless
 * recursive call or no waiters or is first.
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 獲取lock對象的上鎖狀態,如果鎖是自由狀態則=0,如果被上鎖則為1,大於1表示重入  
    int c = getState();
    if (c == 0) {
      	// hasQueuedPredecessors,判斷自己是否需要排隊
        // 下面我會單獨介紹,如果不需要排隊則進行cas嘗試加鎖
        // 如果加鎖成功則把當前線程設置為擁有鎖的線程
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
  	// 如果C不等於0,但是當前線程等於擁有鎖的線程則表示這是一次重入,那麼直接把狀態+1表示重入次數+1
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

非公平鎖

/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

hasQueuedPredecessors

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
  • Node

先來看下AQS中最基本的數據結構——Node,Node即為上面CLH變體隊列中的節點。

static final class Node {
    static final Node SHARED = new Node(); // 表示線程以共享的模式等待鎖
    static final Node EXCLUSIVE = null; // 表示線程正在以獨佔的方式等待鎖
    static final int CANCELLED =  1; // 表示線程獲取鎖的請求已經取消了
    static final int SIGNAL    = -1; // 表示線程已經準備好了,就等資源釋放了
    static final int CONDITION = -2; // 表示節點在等待隊列中,節點線程等待喚醒
    static final int PROPAGATE = -3; // 當前線程處在SHARED情況下,該字段才會使用
    volatile int waitStatus; // 當前節點在隊列中的狀態
    volatile Node prev; // 前驅指針
    volatile Node next; // 後繼指針
    volatile Thread thread; // 表示處於該節點的線程
    Node nextWaiter; // 指向下一個處於CONDITION狀態的節點
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    // 返回前驅節點,沒有的話拋出npe
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    Node() {    // Used to establish initial head or SHARED marker
    }
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

再看hasQueuedPredecessors,整個方法如果最後返回false,則去加鎖,如果返回true則不加鎖,因為這個方法被取反操作;hasQueuedPredecessors是公平鎖加鎖時判斷等待隊列中是否存在有效節點的方法。如果返回False,說明當前線程可以爭取共享資源;如果返回True,說明隊列中存在有效節點,當前線程必須加入到等待隊列中。

  • h != t && ((s = h.next) == null || s.thread != Thread.currentThread());

雙向鏈表中,第一個節點為虛節點,其實並不存儲任何信息,只是佔位。真正的第一個有數據的節點,是在第二個節點開始的。

  • 當h != t時: 如果(s = h.next) == null,等待隊列正在有線程進行初始化,但只是進行到了Tail指向Head,沒有將Head指向Tail,此時隊列中有元素,需要返回True(這塊具體見下邊代碼分析)。
  • 如果(s = h.next) != null,說明此時隊列中至少有一個有效節點。
  • 如果此時s.thread == Thread.currentThread(),說明等待隊列的第一個有效節點中的線程與當前線程相同,那麼當前線程是可以獲取資源的;
  • 如果s.thread != Thread.currentThread(),說明等待隊列的第一個有效節點線程與當前線程不同,當前線程必須加入進等待隊列。

如果這上面沒有看懂,沒有關係,先來分析一下構建整個隊列的過程;

  • addWaiter(Node.EXCLUSIVE)
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // tail為對尾,賦值給pred
    Node pred = tail;
    // 判斷pred是否為空,其實就是判斷對尾是否有節點,其實只要隊列被初始化了對尾肯定不為空
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
  • enq
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

用一張圖來分析一下,整個隊列構建過程;

  • (1)通過Node(Thread thread, Node mode) 方法構建一個node節點(node2),此時的nextWaiter為空,線程不為空,是當前線程;

  • (2)如果隊尾為空,則說明隊列未建立,調用enq構建第一個虛擬節點(node1),通過compareAndSetHead方法構建一個頭節點,需要注意的是該頭節點thread是null,後續很多都是用線程是否為null來判讀是否為第一個虛擬節點;

  • (3)將node1 cas設置為head

  • (4)將頭節點賦值為tail = head

  • (5)進入下一次for循環時,會走到else分支,會將傳入的node的指向頭部節點的next,此時node2的prev指向node1(tail)

  • (6)將node2 cas設置為tail;

  • (7)將node2指向node1的next;

    經過上面的步驟,就構建了一個長度為2的隊列;

添加第二個隊列時,走的是這段代碼,流程就簡單多了,代碼如下

if (pred != null) {
    node.prev = pred;
    if (compareAndSetTail(pred, node)) {
        pred.next = node;
        return node;
    }
}

再看一下h != t && ((s = h.next) == null || s.thread != Thread.currentThread());因為整個構建過程並不是原子操作,所以這個條件判斷,現在再是不是就看明白了?

  • 當h != t時(3)步驟已經完成: 如果(s = h.next) == null 此時步驟(4)未完成,等待隊列正在有線程進行初始化,但只是進行到了Tail指向Head,沒有將Head指向Tail,此時隊列中有元素,需要返回True
  • 如果(s = h.next) != null,說明此時隊列中至少有一個有效節點。
  • 如果此時s.thread == Thread.currentThread(),說明等待隊列的第一個有效節點中的線程與當前線程相同,那麼當前線程是可以獲取資源的;
  • 如果s.thread != Thread.currentThread(),說明等待隊列的第一個有效節點線程與當前線程不同,當前線程必須加入進等待隊列。

acquireQueued

addWaiter方法其實就是把對應的線程以Node的數據結構形式加入到雙端隊列里,返回的是一個包含該線程的Node。而這個Node會作為參數,進入到acquireQueued方法中。acquireQueued方法可以對排隊中的線程進行“獲鎖”操作。總的來說,一個線程獲取鎖失敗了,被放入等待隊列,acquireQueued會把放入隊列中的線程不斷去獲取鎖,直到獲取成功或者不再需要獲取(中斷)。

下面通過代碼從“何時出隊列?”和“如何出隊列?”兩個方向來分析一下acquireQueued源碼:

final boolean acquireQueued(final Node node, int arg) {
    // 標記是否成功拿到資源
    boolean failed = true;
    try {
        // 標記等待過程中是否中斷過
        boolean interrupted = false;
        for (;;) {
            // 獲取當前節點的前驅節點,有兩種情況;1、上一個節點為頭部;2上一個節點不為頭部
            final Node p = node.predecessor();
            // 如果p是頭結點,說明當前節點在真實數據隊列的首部,就嘗試獲取鎖(頭結點是虛節點)
            // 因為第一次tryAcquire判斷是否需要排隊,如果需要排隊,那麼我就入隊,此處再重試一次
            if (p == head && tryAcquire(arg)) {
                // 獲取鎖成功,頭指針移動到當前node
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 說明p為頭節點且當前沒有獲取到鎖(可能是非公平鎖被搶佔了)或者是p不為頭結點,這個時候就要判斷當前node是否要被阻塞(被阻塞條件:前驅節點的waitStatus為-1),防止無限循環浪費資源。具體兩個方法下面細細分析
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
       // 成功拿到資源,準備釋放
        if (failed)
            cancelAcquire(node);
    }
}

setHead

設置當前節點為頭節點,並且將node.thread為空(剛才提到判斷是否為頭部虛擬節點的條件就是node.thread == null。waitStatus狀態併為修改,等下我們再分析;

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

shouldParkAfterFailedAcquire

接下來看shouldParkAfterFailedAcquire代碼,需要注意的是,每一個新創建Node的節點是被下一個排隊的node設置為等待狀態為SIGNAL, 這裏比較難以理解為什麼需要去改變上一個節點的park狀態?

每個node都有一個狀態,默認為0,表示無狀態,-1表示在park;當時不能自己把自己改成-1狀態?因為你得確定你自己park了才是能改為-1;所以只能先park;在改狀態;但是問題你自己都park了;完全釋放CPU資源了,故而沒有辦法執行任何代碼了,所以只能別人來改;故而可以看到每次都是自己的后一個節點把自己改成-1狀態;

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 獲取前驅節點的狀態 
    int ws = pred.waitStatus;
    // 說明頭結點處於喚醒狀態
    if (ws == Node.SIGNAL)
        return true;
    // static final int CANCELLED =  1; // 表示線程獲取鎖的請求已經取消了
    // static final int SIGNAL    = -1; // 表示線程已經準備好了,就等資源釋放了
    // static final int CONDITION = -2; // 表示節點在等待隊列中,節點線程等待喚醒
    // static final int PROPAGATE = -3; // 當前線程處在SHARED情況下,該字段才會使用
    if (ws > 0) {
        do {
            // 把取消節點從隊列中剔除
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 設置前任節點等待狀態為SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt

調用LockSupport.park掛起當前線程,自己已經park,無法再修改狀態了!

private final boolean parkAndCheckInterrupt() {
    // 調⽤用park()使線程進⼊入waiting狀態
    LockSupport.park(this);
    // 如果被喚醒,查看⾃自⼰己是不不是被中斷的,這⾥里里先清除⼀下標記位
    return Thread.interrupted(); 
}

shouldParkAfterFailedAcquire的整個流程還是比較清晰的,如果不清楚,可以參考美團畫的流程圖;

cancelAcquire

通過上面的分析,當failed為true時,也就意味着park結束,線程被喚醒了,for循環已經跳出,開始執行cancelAcquire,通過cancelAcquire方法,將Node的狀態標記為CANCELLED;代碼如下:

private void cancelAcquire(Node node) {
    // 將無效節點過濾
    if (node == null)
        return;
    // 設置該節點不關聯任何線程,也就是虛節點(上面已經提到,node.thread = null是判讀是否是頭節點的條件)
    node.thread = null;
    Node pred = node.prev;
    // 通過前驅節點,處理waitStatus > 0的node
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    // 把當前node的狀態設置為CANCELLED,當下一個node排隊結束時,自己就會被上一行代碼處理掉;
    Node predNext = pred.next;
    node.waitStatus = Node.CANCELLED;
    // 如果當前節點是尾節點,將從后往前的第一個非取消狀態的節點設置為尾節點,更新失敗的話,則進入else,如果更新成功,將tail的後繼節點設置為null
    if (node == tail && compareAndSetTail(node, pred)) {
        // 把自己設置為null
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
        // 如果當前節點不是head的後繼節點
        // 1:判斷當前節點前驅節點的是否為SIGNAL
        // 2:如果不是,則把前驅節點設置為SINGAL看是否成功
        // 如果1和2中有一個為true,再判斷當前節點的線程是否為null
        // 如果上述條件都滿足,把當前節點的前驅節點的後繼指針指向當前節點的後繼節點 
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            // 如果當前節點是head的後繼節點,或者上述條件不滿足,那就喚醒當前節點的後繼節點
            unparkSuccessor(node);
        }
        node.next = node; // help GC
    }
}

當前的流程:

  • 獲取當前節點的前驅節點,如果前驅節點的狀態是CANCELLED,那就一直往前遍歷,找到第一個waitStatus <= 0的節點,將找到的Pred節點和當前Node關聯,將當前Node設置為CANCELLED。

  • 根據當前節點的位置,考慮以下三種情況:

    (1) 當前節點是尾節點。

    (2) 當前節點是Head的後繼節點。

    (3) 當前節點不是Head的後繼節點,也不是尾節點。

(1)當前節點時尾節點

(2)當前節點是Head的後繼節點。

這張圖描述的是這段代碼:unparkSuccessor

Node s = node.next;
if (s == null || s.waitStatus > 0) {
    s = null;
    for (Node t = tail; t != null && t != node; t = t.prev)
        if (t.waitStatus <= 0)
            s = t;
}

(3)當前節點不是Head的後繼節點,也不是尾節點。

這張圖描述的是這段代碼跟(2)一樣;

通過上面的圖,你會發現所有的變化都是對Next指針進行了操作,而沒有對Prev指針進行操作,原因是執行cancelAcquire的時候,當前節點的前置節點可能已經從隊列中出去了(已經執行過Try代碼塊中的shouldParkAfterFailedAcquire方法了),也就是下圖中代碼1和代碼2直接的間隙就會出現這種情況,此時修改Prev指針,有可能會導致Prev指向另一個已經移除隊列的Node,因此這塊變化Prev指針不安全。

unlock解鎖

解鎖時並不區分公平和不公平,因為ReentrantLock實現了鎖的可重入,可以進一步的看一下時如何處理的,上代碼:

public void unlock() {
    sync.release(1);
}

release

public final boolean release(int arg) {
    // 自定義的tryRelease如果返回true,說明該鎖沒有被任何線程持有
    if (tryRelease(arg)) {
        // 獲取頭結點
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 頭結點不為空並且頭結點的waitStatus不是初始化節點情況,解除線程掛起狀態
            unparkSuccessor(h);
        return true;
    }
    return false;
}

這裏的判斷條件為什麼是h != null && h.waitStatus != 0

  1. h == null Head還沒初始化。初始情況下,head == null,第一個節點入隊,Head會被初始化一個虛擬節點。如果還沒來得及入隊,就會出現head == null 的情況。
  2. h != null && waitStatus == 0 表明後繼節點對應的線程仍在運行中,不需要喚醒。
  3. h != null && waitStatus < 0 表明後繼節點可能被阻塞了,需要喚醒,(還記得一個node是在shouldParkAfterFailedAcquire方法中被設置為SIGNAL = -1的吧?不記得翻看一下上面吧)

tryRelease

protected final boolean tryRelease(int releases) {
    // 減少可重入次數,setState(c);
    int c = getState() - releases;
    // 當前線程不是持有鎖的線程,拋出異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果持有線程全部釋放,將當前獨佔鎖所有線程設置為null,並更新state
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

unparkSuccessor

這個方法在cancelAcquire其實也用到了,簡單分析一下

// 如果當前節點是head的後繼節點,或者上述條件不滿足,就喚醒當前節點的後繼節點unparkSuccessor(node);

private void unparkSuccessor(Node node) {
    // 獲取結點waitStatus,CAS設置狀態state=0
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 獲取當前節點的下一個節點
    Node s = node.next;
    // 如果下個節點是null或者下個節點被cancelled,就找到隊列最開始的非cancelled的節點
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 就從尾部節點開始找,到隊首,找到隊列第一個waitStatus<0的節點。
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 如果當前節點的下個節點不為空,而且狀態<=0,就把當前節點unpark
    if (s != null)
        LockSupport.unpark(s.thread);
}

為什麼要從后往前找第一個非Cancelled的節點呢?

原因1:addWaiter方法並非原子,構建鏈表結構時如下圖中 1、2間隙執行unparkSuccessor,此時鏈表是不完整的,沒辦法從前往後找了;

原因2:還有一點原因,在產生CANCELLED狀態節點的時候,先斷開的是Next指針,Prev指針並未斷開,因此也是必須要從后往前遍歷才能夠遍歷完全部的Node;

中斷恢復

喚醒后,會執行return Thread.interrupted();,這個函數返回的是當前執行線程的中斷狀態,並清除。

private final boolean parkAndCheckInterrupt() {
	LockSupport.park(this);
	return Thread.interrupted();
}

acquireQueued代碼,當parkAndCheckInterrupt返回True或者False的時候,interrupted的值不同,但都會執行下次循環。如果這個時候獲取鎖成功,就會把當前interrupted返回。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true;
	try {
		boolean interrupted = false;
		for (;;) {
			final Node p = node.predecessor();
			if (p == head && tryAcquire(arg)) {
				setHead(node);
				p.next = null; // help GC
				failed = false;
				return interrupted;
			}
			if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
				interrupted = true;
			}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

如果acquireQueued為True,就會執行selfInterrupt方法。

該方法其實是為了中斷線程。但為什麼獲取了鎖以後還要中斷線程呢?這部分屬於Java提供的協作式中斷知識內容,感興趣同學可以查閱一下。這裏簡單介紹一下:

  1. 當中斷線程被喚醒時,並不知道被喚醒的原因,可能是當前線程在等待中被中斷,也可能是釋放了鎖以後被喚醒。因此我們通過Thread.interrupted()方法檢查中斷標記(該方法返回了當前線程的中斷狀態,並將當前線程的中斷標識設置為False),並記錄下來,如果發現該線程被中斷過,就再中斷一次。
  2. 線程在等待資源的過程中被喚醒,喚醒后還是會不斷地去嘗試獲取鎖,直到搶到鎖為止。也就是說,在整個流程中,並不響應中斷,只是記錄中斷記錄。最後搶到鎖返回了,那麼如果被中斷過的話,就需要補充一次中斷。

這裏的處理方式主要是運用線程池中基本運作單元Worder中的runWorker,通過Thread.interrupted()進行額外的判斷處理,可以看下ThreadPoolExecutor源碼的判斷條件;

其它

AQS在JUC中有⽐比較⼴廣泛的使⽤用,以下是主要使⽤用的地⽅方:

  • ReentrantLock:使⽤用AQS保存鎖重複持有的次數。當⼀一個線程獲取鎖時, ReentrantLock記錄當
    前獲得鎖的線程標識,⽤用於檢測是否重複獲取,以及錯誤線程試圖解鎖操作時異常情況的處理理。
  • Semaphore:使⽤用AQS同步狀態來保存信號量量的當前計數。 tryRelease會增加計數,
    acquireShared會減少計數。
  • CountDownLatch:使⽤用AQS同步狀態來表示計數。計數為0時,所有的Acquire操作
    (CountDownLatch的await⽅方法)才可以通過。
  • ReentrantReadWriteLock:使⽤用AQS同步狀態中的16位保存寫鎖持有的次數,剩下的16位⽤用於保
    存讀鎖的持有次數。
  • ThreadPoolExecutor: Worker利利⽤用AQS同步狀態實現對獨佔線程變量量的設置(tryAcquire和
    tryRelease)。

至此,通過ReentrantLock分析AQS的實現原理一家完畢,需要說明的是,此文深度參考了美團分析的ReentrantLock,是參考鏈接的第三個,有興趣可以對比差異,感謝!

參考

JDK API 文檔

Java的LockSupport.park()實現分析

[從ReentrantLock的實現看AQS的原理及應用

[Thread的中斷機制(interrupt)

你的鼓勵也是我創作的動力

打賞地址

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

【Spring註解驅動開發】自定義TypeFilter指定@ComponentScan註解的過濾規則

寫在前面

Spring的強大之處不僅僅是提供了IOC容器,能夠通過過濾規則指定排除和只包含哪些組件,它還能夠通過自定義TypeFilter來指定過濾規則。如果Spring內置的過濾規則不能夠滿足我們的需求,那麼我們就可以通過自定義TypeFilter來實現我們自己的過濾規則。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

FilterType中常用的規則

在使用@ComponentScan註解實現包掃描時,我們可以使用@Filter指定過濾規則,在@Filter中,通過type指定過濾的類型。而@Filter註解的type屬性是一個FilterType枚舉,如下所示。

package org.springframework.context.annotation;

public enum FilterType {
	ANNOTATION,
	ASSIGNABLE_TYPE,
	ASPECTJ,
	REGEX,
	CUSTOM
}

每個枚舉值的含義如下所示。

(1)ANNOTATION:按照註解進行過濾。

例如,使用@ComponentScan註解進行包掃描時,按照註解只包含標註了@Controller註解的組件,如下所示。

@ComponentScan(value = "io.mykit.spring", includeFilters = {
    @Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
}, useDefaultFilters = false)

(2)ASSIGNABLE_TYPE:按照給定的類型進行過濾。

例如,使用@ComponentScan註解進行包掃描時,按照給定的類型只包含PersonService類(接口)或其子類(實現類或子接口)的組件,如下所示。

@ComponentScan(value = "io.mykit.spring", includeFilters = {
    @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {PersonService.class})
}, useDefaultFilters = false)

此時,只要是PersonService類型的組件,都會被加載到容器中。也就是說:當PersonService是一個Java類時,Person類及其子類都會被加載到Spring容器中;當PersonService是一個接口時,其子接口或實現類都會被加載到Spring容器中。

(3)ASPECTJ:按照ASPECTJ表達式進行過濾

例如,使用@ComponentScan註解進行包掃描時,按照ASPECTJ表達式進行過濾,如下所示。

@ComponentScan(value = "io.mykit.spring", includeFilters = {
    @Filter(type = FilterType.ASPECTJ, classes = {AspectJTypeFilter.class})
}, useDefaultFilters = false)

(4)REGEX:按照正則表達式進行過濾

例如,使用@ComponentScan註解進行包掃描時,按照正則表達式進行過濾,如下所示。

@ComponentScan(value = "io.mykit.spring", includeFilters = {
    @Filter(type = FilterType.REGEX, classes = {RegexPatternTypeFilter.class})
}, useDefaultFilters = false)

(5)CUSTOM:按照自定義規則進行過濾。

如果實現自定義規則進行過濾時,自定義規則的類必須是org.springframework.core.type.filter.TypeFilter接口的實現類。

例如,按照自定義規則進行過濾,首先,我們需要創建一個org.springframework.core.type.filter.TypeFilter接口的實現類MyTypeFilter,如下所示。

public class MyTypeFilter implements TypeFilter {
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        return false;
    }
}

當我們實現TypeFilter接口時,需要實現TypeFilter接口中的match()方法,match()方法的返回值為boolean類型。當返回true時,表示符合規則,會包含在Spring容器中;當返回false時,表示不符合規則,不會包含在Spring容器中。另外,在match()方法中存在兩個參數,分別為MetadataReader類型的參數和MetadataReaderFactory類型的參數,含義分別如下所示。

  • metadataReader:讀取到的當前正在掃描的類的信息。
  • metadataReaderFactory:可以獲取到其他任務類的信息。

接下來,使用@ComponentScan註解進行如下配置。

@ComponentScan(value = "io.mykit.spring", includeFilters = {
    @Filter(type = FilterType.CUSTOM, classes = {MyTypeFilter.class})
}, useDefaultFilters = false)

在FilterType枚舉中,ANNOTATION和ASSIGNABLE_TYPE是比較常用的,ASPECTJ和REGEX不太常用,如果FilterType枚舉中的類型無法滿足我們的需求時,我們也可以通過實現org.springframework.core.type.filter.TypeFilter接口來自定義過濾規則,此時,將@Filter中的type屬性設置為FilterType.CUSTOM,classes屬性設置為自定義規則的類對應的Class對象。

實現自定義過濾規則

在項目的io.mykit.spring.plugins.register.filter包下新建MyTypeFilter,並實現org.springframework.core.type.filter.TypeFilter接口。此時,我們先在MyTypeFilter類中打印出當前正在掃描的類名,如下所示。

package io.mykit.spring.plugins.register.filter;

import org.springframework.core.io.Resource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;

import java.io.IOException;

/**
 * @author binghe
 * @version 1.0.0
 * @description 自定義過濾規則
 */
public class MyTypeFilter implements TypeFilter {
    /**
     * metadataReader:讀取到的當前正在掃描的類的信息
     * metadataReaderFactory:可以獲取到其他任務類的信息
     */
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        //獲取當前類註解的信息
        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
        //獲取當前正在掃描的類的信息
        ClassMetadata classMetadata = metadataReader.getClassMetadata();
        //獲取當前類的資源信息,例如:類的路徑等信息
        Resource resource = metadataReader.getResource();
        //獲取當前正在掃描的類名
        String className = classMetadata.getClassName();
        //打印當前正在掃描的類名
        System.out.println("-----> " + className);
        return false;
    }
}

接下來,我們在PersonConfig類中配置自定義過濾規則,如下所示。

@Configuration
@ComponentScans(value = {
        @ComponentScan(value = "io.mykit.spring", includeFilters = {
                @Filter(type = FilterType.CUSTOM, classes = {MyTypeFilter.class})
        }, useDefaultFilters = false)
})
public class PersonConfig {

    @Bean("person")
    public Person person01(){
        return new Person("binghe001", 18);
    }
}

接下來,我們運行SpringBeanTest類中的testComponentScanByAnnotation()方法進行測試,輸出的結果信息如下所示。

-----> io.mykit.spring.test.SpringBeanTest
-----> io.mykit.spring.bean.Person
-----> io.mykit.spring.plugins.register.controller.PersonController
-----> io.mykit.spring.plugins.register.dao.PersonDao
-----> io.mykit.spring.plugins.register.filter.MyTypeFilter
-----> io.mykit.spring.plugins.register.service.PersonService
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig
person

可以看到,已經輸出了當前正在掃描的類的名稱,同時,除了Spring內置的bean名稱外,只輸出了personConfig和person,沒有輸出使用@Repository、@Service、@Controller註解標註的組件名稱。這是因為當前PersonConfig上標註的@ComponentScan註解是使用自定義的規則,而在MyTypeFilter自定義規則的實現類中,直接返回了false值,將所有的bean都排除了。

我們可以在MyTypeFilter類中簡單的實現一個規則,例如,當前掃描的類名稱中包含有字符串Person,就返回true,否則返回false。此時,MyTypeFilter類中match()方法的實現如下所示。

    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        //獲取當前類註解的信息
        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
        //獲取當前正在掃描的類的信息
        ClassMetadata classMetadata = metadataReader.getClassMetadata();
        //獲取當前類的資源信息,例如:類的路徑等信息
        Resource resource = metadataReader.getResource();
        //獲取當前正在掃描的類名
        String className = classMetadata.getClassName();
        //打印當前正在掃描的類名
        System.out.println("-----> " + className);
        return className.contains("Person");
    }

此時,在io.mykit.spring包下的所有類都會通過MyTypeFilter類的match()方法,來驗證類名是否包含Person,如果包含則返回true,否則返回false。

我們再次運行SpringBeanTest類中的testComponentScanByAnnotation()方法進行測試,輸出的結果信息如下所示。

-----> io.mykit.spring.test.SpringBeanTest
-----> io.mykit.spring.bean.Person
-----> io.mykit.spring.plugins.register.controller.PersonController
-----> io.mykit.spring.plugins.register.dao.PersonDao
-----> io.mykit.spring.plugins.register.filter.MyTypeFilter
-----> io.mykit.spring.plugins.register.service.PersonService
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig
person
personController
personDao
personService

此時,結果信息中輸出了使用@Repository、@Service、@Controller註解標註的組件名稱,分別為:personDao、personService和personController。

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

STM32串口打印的那些知識

常規打印方法

在STM32的應用中,我們常常對printf進行重定向的方式來把打印信息printf到我們的串口助手。在MDK環境中,我們常常使用MicroLIB+fputc的方式實現串口打印功能,即:

要實現fputc函數的原因是:printf函數依賴於fputc函數,重新實現fputc內部從串口發送數據即可間接地實現printf打印輸出數據到串口。

不知道大家有沒有看過正點原子裸機串口相關的例程,他們的串口例程里不使用MicroLIB,而是使用標準庫+fputc的方式。相關代碼如:

#if 1
#pragma import(__use_no_semihosting)
//標準庫需要的支持函數
struct __FILE
{
    int handle;
};

FILE __stdout;
/**
 * @brief	定義_sys_exit()以避免使用半主機模式
 * @param	void
 * @return  void
 */
void _sys_exit(int x)
{
    x = x;
}

int fputc(int ch, FILE *f)
{
    while((USART1->ISR & 0X40) == 0); //循環發送,直到發送完畢

    USART1->TDR = (u8) ch;
    return ch;
}
#endif

關於這兩種方法的一些說明可以查看Mculover666兄的重定向printf函數到串口輸出的多種方法這篇文章。這篇文章中不僅包含上面的兩種方法,而且也包含着在GCC中使用標準庫重定向printf的方法。

自己實現一個打印函數

以上的幾種方法基本上是改造C庫的printf函數來實現串口打印的功能。其實我們也可以自己實現一個串口打印的功能。

printf本身就是一個變參函數,其原型為:

int printf (const char *__format, ...);

所以,我們要重新封裝的一個串口打印函數自然也應該是一個變參函數。具體實現如下:

1、基於STM32的HAL庫

#define TX_BUF_LEN  256     /* 發送緩衝區容量,根據需要進行調整 */
uint8_t TxBuf[TX_BUF_LEN];  /* 發送緩衝區                       */
void MyPrintf(const char *__format, ...)
{
  va_list ap;
  va_start(ap, __format);
  
  /* 清空發送緩衝區 */
  memset(TxBuf, 0x0, TX_BUF_LEN);
  
  /* 填充發送緩衝區 */
  vsnprintf((char*)TxBuf, TX_BUF_LEN, (const char *)__format, ap);
  va_end(ap);
  int len = strlen((const char*)TxBuf);
  
  /* 往串口發送數據 */
  HAL_UART_Transmit(&huart1, (uint8_t*)&TxBuf, len, 0xFFFF);
}

因為我們使用printf函數基本不使用其返回值,所以這裏直接用void類型了。自定義變參函數需要用到va_start、va_end等宏,需要包含頭文件stdarg.h。關於變參函數的一些學習可以查看網上的一些博文,如:

https://www.cnblogs.com/wulei0630/p/9444062.html

這裏我們使用的是STM32的HAL庫,其給我們提供HAL_UART_Transmit接口可以直接把整個發送緩衝區的內容給一次性發出去。

2、基於STM32標準庫

若是基於STM32的標準庫,就需要一字節一字節的循環發送出去,具體代碼如:

#define TX_BUF_LEN  256     /* 發送緩衝區容量,根據需要進行調整 */
uint8_t TxBuf[TX_BUF_LEN];  /* 發送緩衝區                       */
void MyPrintf(const char *__format, ...)
{
  va_list ap;
  va_start(ap, __format);
    
  /* 清空發送緩衝區 */
  memset(TxBuf, 0x0, TX_BUF_LEN);
    
  /* 填充發送緩衝區 */
  vsnprintf((char*)TxBuf, TX_BUF_LEN, (const char *)__format, ap);
  va_end(ap);
  int len = strlen((const char*)TxBuf);
  
  /* 往串口發送數據 */
  for (int i = 0; i < len; i++)
  {
	while(USART_GetFlagStatus(USART1, USART_FLAG_TC)==RESET);    
	USART_SendData(USART1, TxBuf[i]);
  }
}

測試結果:

我們也可以使用我們的MyPrintf函數按照上一篇文章:======的方式封裝一個宏打印函數:

以上就是我們自定義方式實現的一種串口打印函數。

但是,我想說:對於串口打印的使用,我們沒必要自己創建一個打印函數。看到這,是不是有人想要打我了。。。。看了半天,你卻跟我說沒必要用。。。

哈哈,別急,我們不應用在串口打印調試方面,那可以用在其它方面呀。

(1)應用一:

比如最近我在實際應用中:我們的MCU跑的是我們老大自己寫的一個小的操作系統+我們公司自己開發的上位機。我們MCU端與上位機使用的是串口通訊,MCU往上位機發送的數據有兩種類型,一種是HEX格式數據,一種是字符串數據。

但是我們下位機的這兩種數據,在通過串口發送之前都得統一把數據封包交給那個系統通信任務,然後再由通信任務發出去。在這裏,就不能用printf了。老大也針對他的這個系統實現了一個deb_printf函數用於打印調試。

但是,那個函數既複雜又很雞肋,稍微複雜一點的數據就打印不出來了。因此我利用上面的思路給它新封裝了一個打印調試函數,很好用,完美地兼容了老大的那個系統。具體代碼就不分享了,大體代碼、思路如上。

(2)應用二:

我們在使用串口與ESP8266模塊通訊時,可利用類似這樣的方式封裝一個發送數據的函數,這個函數的使用可以像printf一樣簡單。可以以很簡單的方式把數據透傳至服務端,比如我以前的畢設中就有這麼應用:

以上就是本次的分享,如有錯誤,歡迎指出!謝謝

我的個人博客:https://www.lizhengnian.cn/

我的微信公眾號:嵌入式大雜燴

我的CSDN博客:https://blog.csdn.net/zhengnianli

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價