夏季電動機車市場正熱,Gogoro 掛牌數突破 20 萬

2019 年台灣電動機車市場百家爭鳴,也帶動市場熱度,越來越多消費者願意選擇電動機車。電動機車大廠 Gogoro 宣布旗下總掛牌數正式達 20 萬輛,寫下新的里程碑。

根據工研院產科國際所推估,台灣電動機車年銷量可以突破 15 萬輛,成長速度超過全球平均。2019 年夏季包括 Gogoro、光陽(Kymco)、山葉(YAMAHA)、中華汽車 eMOVING 和宏佳騰等品牌都推出新的電動機車,代表車廠也清楚了解市場對電動機車的需求。

受惠於暑假旺季提前發酵,加上受到新車效應與即將到來的開學季影響,Gogoro 從 2019 年 5 月以來已連續 4 個月單月掛牌數破萬,這也讓 Gogoro 總掛牌數正式達到 20 萬輛。在全新發表的 Gogoro S2 ABS 車款助攻之下,讓 Gogoro 在 8 月單月掛牌市占率達 16.35%,即使 8 月整體機車市場下滑 20%,仍然保持穩定成長。

Gogoro 資深行銷總監陳彥揚表示:「今年夏天對 Gogoro 來說深具意義,連續 4 個月掛牌破萬,帶動 Gogoro 總車主數超越 20 萬大關,除了非常感謝車主的熱情支持,業界的投入及政府政策的推動,也是電動機車產業發展向前邁進的關鍵要素,期盼能快速跟上全球化電動車趨勢的腳步。」

每年最具代表性的車主活動「快閃台北橋」將於 9 月 29 日舉辦,2018 年活動以 1,303 台機車創下「世界最大規模電動機車遊行」的金氏世界紀錄,Gogoro 也將擴大邀請所有電動機車車主,今年活動時集結台北橋。

8 月推出的「暢快騎 0 元起」購車方案廣受歡迎,Gogoro 為了慶祝車主數量突破 20 萬大關,宣布活動時間將延長至 9 月 30 日,購買 Gogoro 全車系即贈送 6 個月電池服務資費 299 元,無論選擇什麼方案每個月都可抵扣電池資費 299 元。即日起至 9 月 30 日購買 Gogoro 全車系並符合學生資格的車主,即可享有 12 期 0 利率分期優惠,同時再送「1 年期車碰車險」。

(合作媒體:。首圖來源:Gogoro)

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

【其他文章推薦】

※公開收購3c價格,不怕被賤賣!

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

面試官:你連RESTful都不知道我怎麼敢要你?

目錄

面試官:了解RESTful嗎?

我:聽說過。

面試官:那什麼是RESTful?

我:就是用起來很規範,挺好的

面試官:是RESTful挺好的,還是自我感覺挺好的

我:都挺好的。

面試官:… 把門關上。

我:…. 要幹嘛?先關上再說。

面試官:我說出去把門關上。

我:what ?,奪門而去

@

01 前言

回歸正題,看過很多RESTful相關的文章總結,參齊不齊,結合工作中的使用,非常有必要歸納一下關於RESTful架構方式了,RESTful只是一種架構方式的約束,給出一種約定的標準,完全嚴格遵守RESTful標準並不是很多,也沒有必要。但是在實際運用中,有RESTful標準可以參考,是十分有必要的。

實際上在工作中對api接口規範、命名規則、返回值、授權驗證等進行一定的約束,一般的項目api只要易測試、足夠安全、風格一致可讀性強、沒有歧義調用方便我覺得已經足夠了,接口是給開發人員看的,也不是給普通用戶去調用。

02 RESTful的來源

REST:Representational State Transfer(表象層狀態轉變),如果沒聽說過REST,你一定以為是rest這個單詞,剛開始我也是這樣認為的,後來發現是這三個單詞的縮寫,即使知道了這三個單詞理解起來仍然非常晦澀難懂。如何理解RESTful架構,最好的辦法就是深刻理解消化Representational State Transfer這三個單詞到底意味着什麼。

1.每一個URI代表一種資源;

2.客戶端和服務器之間,傳遞這種資源的某種表現層;

3.客戶端通過四個HTTP動詞(get、post、put、delete),對服務器端資源進行操作,實現”表現層狀態轉化”。

是由美國計算機科學家Roy Fielding(百度百科沒有介紹,真是尷尬了)。Adobe首席科學家、Http協議的首要作者之一、Apache項目聯合創始人。

03 RESTful6大原則

REST之父Roy Fielding在論文中闡述REST架構的6大原則。

1. C-S架構

數據的存儲在Server端,Client端只需使用就行。兩端徹底分離的好處使client端代碼的可移植性變強,Server端的拓展性變強。兩端單獨開發,互不干擾。

2. 無狀態

http請求本身就是無狀態的,基於C-S架構,客戶端的每一次請求帶有充分的信息能夠讓服務端識別。請求所需的一些信息都包含在URL的查詢參數、header、body,服務端能夠根據請求的各種參數,無需保存客戶端的狀態,將響應正確返回給客戶端。無狀態的特徵大大提高的服務端的健壯性和可拓展性。

當然這總無狀態性的約束也是有缺點的,客戶端的每一次請求都必須帶上相同重複的信息確定自己的身份和狀態(這也是必須的),造成傳輸數據的冗餘性,但這種確定對於性能和使用來說,幾乎是忽略不計的。

3.統一的接口

這個才是REST架構的核心,統一的接口對於RESTful服務非常重要。客戶端只需要關注實現接口就可以,接口的可讀性加強,使用人員方便調用。

4.一致的數據格式

服務端返回的數據格式要麼是XML,要麼是Json(獲取數據),或者直接返回狀態碼,有興趣的可以看看博客園的開放平台的操作數據的api,post、put、patch都是返回的一個狀態碼 。

自我描述的信息,每項數據應該是可以自我描述的,方便代碼去處理和解析其中的內容。比如通過HTTP返回的數據裏面有 [MIME type ]信息,我們從MIME type裏面可以知道數據的具體格式,是圖片,視頻還是JSON,客戶端通過body內容、查詢串參數、請求頭和URI(資源名稱)來傳送狀態。服務端通過body內容,響應碼和響應頭傳送狀態給客戶端。這項技術被稱為超媒體(或超文本鏈接)。

除了上述內容外,HATEOS也意味着,必要的時候鏈接也可被包含在返回的body(或頭部)中,以提供URI來檢索對象本身或關聯對象。下文將對此進行更詳細的闡述。

如請求一條微博信息,服務端響應信息應該包含這條微博相關的其他URL,客戶端可以進一步利用這些URL發起請求獲取感興趣的信息,再如分頁可以從第一頁的返回數據中獲取下一頁的URT也是基於這個原理。

4.系統分層

客戶端通常無法表明自己是直接還是間接與端服務器進行連接,分層時同樣要考慮安全策略。

5.可緩存

在萬維網上,客戶端可以緩存頁面的響應內容。因此響應都應隱式或顯式的定義為可緩存的,若不可緩存則要避免客戶端在多次請求後用舊數據或臟數據來響應。管理得當的緩存會部分地或完全地除去客戶端和服務端之間的交互,進一步改善性能和延展性。

6.按需編碼、可定製代碼(可選)

服務端可選擇臨時給客戶端下發一些功能代碼讓客戶端來執行,從而定製和擴展客戶端的某些功能。比如服務端可以返回一些 Javascript 代碼讓客戶端執行,去實現某些特定的功能。
提示:REST架構中的設計準則中,只有按需編碼為可選項。如果某個服務違反了其他任意一項準則,嚴格意思上不能稱之為RESTful風格。

03 RESTful的7個最佳實踐

1. 版本

如github開放平台
https://developer.github.com/v3/

就是將版本放在url,簡潔明了,這個只有用了才知道,一般的項目加版本v1,v2,v3?好吧,這個加版本估計只有大公司大項目才會去使用,說出來不怕尷尬,我真沒用過。有的會將版本號放在header裏面,但是不如url直接了當。

https://example.com/api/v1/

2.參數命名規範

query parameter可以採用駝峰命名法,也可以採用下劃線命名的方式,推薦採用下劃線命名的方式,據說後者比前者的識別度要高,可能是用的人多了吧,因人而異,因團隊規範而異吧。

https://example.com/api/users/today_login 獲取今天登陸的用戶 
https://example.com/api/users/today_login&sort=login_desc 獲取今天登陸的用戶、登陸時間降序排列

3.url命名規範

API 命名應該採用約定俗成的方式,保持簡潔明了。在RESTful架構中,每個url代表一種資源所以url中不能有動詞,只能有名詞,並且名詞中也應該使用複數。實現者應使用相應的Http動詞GET、POST、PUT、PATCH、DELETE、HEAD來操作這些資源即可

不規範的的url,冗餘沒有意義,形式不固定,不同的開發者還需要了解文檔才能調用。

https://example.com/api/getallUsers GET 獲取所有用戶 
https://example.com/api/getuser/1 GET 獲取標識為1用戶信息 
https://example.com/api/user/delete/1 GET/POST 刪除標識為1用戶信息 
https://example.com/api/updateUser/1 POST 更新標識為1用戶信息 
https://example.com/api/User/add POST 添加新的用戶

規範后的RESTful風格的url,形式固定,可讀性強,根據users名詞和http動詞就可以操作這些資源

https://example.com/api/users GET 獲取所有用戶信息 
https://example.com/api/users/1 GET 獲取標識為1用戶信息 
https://example.com/api/users/1 DELETE 刪除標識為1用戶信息 
https://example.com/api/users/1 Patch 更新標識為1用戶部分信息,包含在body中 
https://example.com/api/users POST 添加新的用戶

4. 統一返回數據格式

對於合法的請求應該統一返回數據格式,這裏演示的是json

  • code——包含一個整數類型的HTTP響應狀態碼。
  • status——包含文本:”success”,”fail”或”error”。HTTP狀態響應碼在500-599之間為”fail”,在400-499之間為”error”,其它均為”success”(例如:響應狀態碼為1XX、2XX和3XX)。這個根據實際情況其實是可要可不要的。
  • message——當狀態值為”fail”和”error”時有效,用於显示錯誤信息。參照國際化(il8n)標準,它可以包含信息號或者編碼,可以只包含其中一個,或者同時包含並用分隔符隔開。
  • data——包含響應的body。當狀態值為”fail”或”error”時,data僅包含錯誤原因或異常名稱、或者null也是可以的

返回成功的響應json格式

{
  "code": 200,
  "message": "success",
  "data": {
    "userName": "123456",
    "age": 16,
    "address": "beijing"
  }
}

返回失敗的響應json格式

{
  "code": 401,
  "message": "error  message",
  "data": null
}

下面這個ApiResult的泛型類是在項目中用到的,拓展性強,使用方便。返回值使用統一的 ApiResult 或 ApiResult
錯誤返回 使用 ApiResult.Error 進行返回; 成功返回,要求使用 ApiResult.Ok 進行返回

public class ApiResult: ApiResult
    {
        public new static ApiResult<T> Error(string message)
        {
            return new ApiResult<T>
            {
                Code = 1,
                Message = message,
            };
        }
        [JsonProperty("data")]
        public T Data { get; set; }
    }
    public class ApiResult
    {
        public static ApiResult Error(string message)
        {
            return new ApiResult
            {
                Code = 1,
                Message = message,
            };
        }

        public static ApiResult<T> Ok<T>(T data)
        {
            return new ApiResult<T>()
            {
                Code = 0,
                Message = "",
                Data = data
            };
        }
        /// <summary>
        /// 0 是 正常 1 是有錯誤
        /// </summary>
        [JsonProperty("code")]
        public int Code { get; set; }
        [JsonProperty("msg")]
        public string Message { get; set; }

        [JsonIgnore]
        public bool IsSuccess => Code == 0;
    }

5. http狀態碼

在之前開發的xamarin android博客園客戶端的時候,patch、delete、post操作時body響應裏面沒有任何信息,僅僅只有http status code。HTTP狀態碼本身就有足夠的含義,根據http status code就可以知道刪除、添加、修改等是否成功。(ps:有點linux設計的味道哦,沒有返回消息就是最好的消息,表示已經成功了)服務段向用戶返回這些狀態碼並不是一個強制性的約束。簡單點說你可以指定這些狀態,但是不是強制的。常用HTTP狀態碼對照表
HTTP狀態碼也是有規律的

  • 1**請求未成功
  • 2**請求成功、表示成功處理了請求的狀態代碼。
  • 3**請求被重定向、表示要完成請求,需要進一步操作。 通常,這些狀態代碼用來重定向。
  • 4** 請求錯誤這些狀態代碼錶示請求可能出錯,妨礙了服務器的處理。
  • 5**(服務器錯誤)這些狀態代碼錶示服務器在嘗試處理請求時發生內部錯誤。 這些錯誤可能是服務器本身的錯誤,而不是請求出錯。

    6. 合理使用query parameter

    在請求數據時,客戶端經常會對數據進行過濾和分頁等要求,而這些參數推薦採用HTTP Query Parameter的方式實現

比如設計一個最近登陸的所有用戶
https://example.com/api/users?recently_login_day=3
搜索用戶,並按照註冊時間降序
https://example.com/api/users?recently_login_day=3
搜索用戶,並按照註冊時間升序、活躍度降序
https://example.com/api/users?q=key&sort=create_title_asc,liveness_desc
關於分頁,看看博客園開放平台分頁獲取精華區博文列表
https://api.cnblogs.com/api/blogposts/@picked?pageIndex={pageIndex}&pageSize={pageSize} 
返回示例: 
[ 
{ 
“Id”: 1, 
“Title”: “sample string 2”, 
“Url”: “sample string 3”, 
“Description”: “sample string 4”, 
“Author”: “sample string 5”, 
“BlogApp”: “sample string 6”, 
“Avatar”: “sample string 7”, 
“PostDate”: “2017-06-25T20:13:38.892135+08:00”, 
“ViewCount”: 9, 
“CommentCount”: 10, 
“DiggCount”: 11 
}, 
{ 
“Id”: 1, 
“Title”: “sample string 2”, 
“Url”: “sample string 3”, 
“Description”: “sample string 4”, 
“Author”: “sample string 5”, 
“BlogApp”: “sample string 6”, 
“Avatar”: “sample string 7”, 
“PostDate”: “2017-06-25T20:13:38.892135+08:00”, 
“ViewCount”: 9, 
“CommentCount”: 10, 
“DiggCount”: 11 
} 
]

7. 多表、多參數連接查詢如何設計URL

這是一個比較頭痛的問題,在做單個實體的查詢比較容易和規範操作,但是在實際的API並不是這麼簡單而已,這其中常常會設計到多表連接、多條件篩選、排序等。
比如我想查詢一個獲取在6月份的訂單中大於500元的且用戶地址是北京,用戶年齡在22歲到40歲、購買金額降序排列的訂單列表

https://example.com/api/orders?order_month=6&order_amount_greater=500&address_city=北京&sort=order_amount_desc&age_min=22&age_max=40

從這個URL上看,參數眾多、調用起來還得一個一個仔細對着,而且API本身非常不容易維護,命名看起來不是很容易,不能太長,也不能太隨意。

在.net WebAPI總我們可以使用屬性路由,屬性路由就是講路由附加到特定的控制器或操作方法上裝飾Controll及其使用[Route]屬性定義路由的方法稱為屬性路由。

這種好處就是可以精準地控制URL,而不是基於約定的路由,簡直就是為這種多表查詢量身定製似的的。 從webapi 2開發,現在是RESTful API開發中最推薦的路由類型。
我們可以在Controll中標記Route

[Route(“api/orders/{address}/{month}”)] 

Action中的查詢參數就只有金額、排序、年齡。減少了查詢參數、API的可讀性和可維護行增強了。

https://example.com/api/orders/beijing/6?order_amount_greater=500&sort=order_amount_desc&age_min=22&age_max=40

這種屬性路由比如在博客園開放的API也有這方面的應用,如獲取個人博客隨筆列表

請求方式:GET 
請求地址:https://api.cnblogs.com/api/blogs/{blogApp}/posts?pageIndex={pageIndex} 
(ps:blogApp:博客名)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

【併發編程】摩爾定律失效“帶來”并行編程

本博客系列是學習併發編程過程中的記錄總結。由於文章比較多,寫的時間也比較散,所以我整理了個目錄貼(傳送門),方便查閱。

併發和并行

在真正開始聊本文的主題之前,我們先來回顧下兩個老生常談的概念:併發和并行。

  • 併發:是指多個線程任務在同一個CPU上快速地輪換執行,由於切換的速度非常快,給人的感覺就是這些線程任務是在同時進行的,但其實併發只是一種邏輯上的同時進行;
  • 并行:是指多個線程任務在不同CPU上同時進行,是真正意義上的同時執行。

下面貼上一張圖來解釋下這兩個概念:

上圖中的咖啡就可以看成是CPU,上面的只有一個咖啡機,相當於只有一個CPU。想喝咖啡的人只有等前面的人製作完咖啡才能製作自己的開發,也就是同一時間只能有一個人在製作咖啡,這是一種併發模式。下面的圖中有兩個咖啡機,相當於有兩個CPU,同一時刻可以有兩個人同時製作咖啡,是一種并行模式。

我們發現并行編程中,很重要的一個特點是系統具有多核CPU。要是系統是單核的,也就談不上什麼并行編程了。那麼是什麼原因導致了現代CPU架構都是多核架構?如果CPU架構都是單核的架構我們是不是就能不要研究什麼并行編程了?

“摩爾定律”失效

上面章節中留下了一個問題:為什麼現代CPU都是多核架構。為了回答這個問題,我們先來了解一個定律–摩爾定律。

1965年,英特爾聯合創始人戈登·摩爾提出以自己名字命名的「摩爾定律」,意指集成電路上可容納的元器件的數量每隔 18 至 24 個月就會增加一倍,性能也將提升一倍。

根據摩爾定律,CPU的性能每隔18到24個月就能增長一倍。但是從現在的情況來看,單核CPU的主頻已經逼近了極限,以現在的製造工藝,很難再繼續提升單核CPU的主頻。也就是說摩爾定律已經失效。

雖然摩爾定律失效了,但是科技的進度對CPU性能的需求沒有停止。這個也難不倒我們偉大的硬件工程師。一個CPU的性能提升有限,我將兩個CPU拼在一起性能不就提升一倍了么。於是多核CPU的架構就出現了。

提高CPU工作主頻主要受到生產工藝的限制。由於CPU是在半導體硅片上製造的,在硅片上的元件之間需要導線進行聯接,由於在高頻狀態下要求導線越細越短越好,這樣才能減小導線分佈電容等雜散干擾以保證CPU運算正確。因此製造工藝的限制,是CPU主頻發展的最大障礙之一。

多核架構引發并行編程

為了繼續保持性能的高速發展,硬件工程師破天荒地想出了將多個CPU內核塞進一個CPU里的奇妙想法。由此,并行計算就被非常自然地推廣開來,隨之而來的問題也層出不窮,程序員的黑暗時期也隨之到來。簡化的硬件設計方案必然帶來軟件設計的複雜性。換句話說,軟件工程師正在為硬件工程師無法完成的工作負責,他們將摩爾定律失效的責任推給了軟件開發者。

所以,如何讓多個CPU有效並且正確地工作也就成了一門技術,甚至是很大的學問。比如,多線程間如何保證線程安全,如何正確理解線程間的無序性、可見性,如何盡可能地設計并行程序,如何將串行程序改造為并行程序。而對并行計算的研究,也就是希望給這片黑暗帶來光明。

總結

世界就是這樣一個矛盾體,併發編程能讓我們充分地利用CPU資源,提升系統性能。但是同時也給我們帶來了很多問題,比如線程上下文切換對性能消耗的問題、共享變量的線程安全問題、線程死鎖問題和線程間通信等問題。研究并行編程就是研究怎麼在享受多線程編程給我們帶來便利的同時又能規避多線程帶來的坑。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

地理文本處理技術在高德的演進(上)

一、背景

地圖App的功能可以簡單概括為定位,搜索,導航三部分,分別解決在哪裡,去哪裡,和怎麼去的問題。高德地圖的搜索場景下,輸入的是,地理相關的檢索query,用戶位置,App圖面等信息,輸出的是,用戶想要的POI。如何能夠更加精準地找到用戶想要的POI,提高滿意度,是評價搜索效果的最關鍵指標。

一個搜索引擎通常可以拆分成query分析、召回、排序三個部分,query分析主要是嘗試理解query表達的含義,為召回和排序給予指導。

地圖搜索的query分析不僅包括通用搜索下的分詞,成分分析,同義詞,糾錯等通用NLP技術,還包括城市分析,wherewhat分析,路徑規劃分析等特定的意圖理解方式。

常見的一些地圖場景下的query意圖表達如下:

query分析是搜索引擎中策略密集的場景,通常會應用NLP領域的各種技術。地圖場景下的query分析,只需要處理地理相關的文本,多樣性不如網頁搜索,看起來會簡單一些。但是,地理文本通常比較短,並且用戶大部分的需求是唯一少量結果,要求精準度非常高,如何能夠做好地圖場景下的文本分析,並提升搜索結果的質量,是充滿挑戰的。

二、整體技術架構

搜索架構

類似於通用檢索的架構,地圖的檢索架構包括query分析,召回,排序三個主要部分。先驗的,用戶的輸入信息可以理解為多種意圖的表達,同時下發請求嘗試獲取檢索結果。后驗的,拿到每種意圖的檢索結果時,進行綜合判斷,選擇效果最好的那個。

query分析流程

具體的意圖理解可分為基礎query分析和應用query分析兩部分,基礎query分析主要是使用一些通用的NLP技術對query進行理解,包括分析,成分分析,省略,同義詞,糾錯等。應用query分析主要是針對地圖場景里的特定問題,包括分析用戶目標城市,是否是where+what表達,是否是從A到B的路徑規劃需求表達等。

整體技術演進

在地里文本處理上整體的技術演進經歷了規則為主,到逐步引入機器學習,到機器學習全面應用的過程。由於搜索模塊是一個高併發的線上服務,對於深度模型的引入有比較苛刻的條件,但隨着性能問題逐漸被解決,我們從各個子方向逐步引入深度學習的技術,進行新一輪的效果提升。

NLP領域技術在最近幾年取得了日新月異的發展,bert,XLNet等模型相繼霸榜,我們逐步統一化各個query分析子任務,使用統一的向量表示對進行用戶需求進行表達,同時進行seq2seq的多任務學習,在效果進一步提升的基礎上,也能夠保證系統不會過於臃腫。

本文就高德地圖搜索的地理文本處理,介紹相關的技術在過去幾年的演進。我們將選取一些點分上下兩篇進行介紹,上篇主要介紹搜索引擎中一些通用的query分析技術,包括糾錯,改寫和省略。下篇着重介紹地圖場景中特有query分析技術,包括城市分析,wherewhat分析,路徑規劃。

三、通用query分析技術演進

3.1 糾錯

在搜索引擎中,用戶輸入的檢索詞(query)經常會出現拼寫錯誤。如果直接對錯誤的query進行檢索,往往不會得到用戶想要的結果。因此不管是通用搜索引擎還是垂直搜索引擎,都會對用戶的query進行糾錯,最大概率獲得用戶想搜的query。

在目前的地圖搜索中,約有6%-10%的用戶請求會輸入錯誤,所以query糾錯在地圖搜索中是一個很重要的模塊,能夠極大的提升用戶搜索體驗。

在搜索引擎中,低頻和中長尾問題往往比較難解決,也是糾錯模塊面臨的主要問題。另外,地圖搜索和通用搜索,存在一個明顯的差異,地圖搜索query結構化比較突出,query中的片段往往包含一定的位置信息,如何利用好query中的結構化信息,更好地識別用戶意圖,是地圖糾錯獨有的挑戰。

常見錯誤分類

(1) 拼音相同或者相近,例如: 盤橋物流園-潘橋物流園
(2) 字形相近,例如: 河北冒黎-河北昌黎
(3) 多字或者漏字,例如: 泉州州頂街-泉州頂街

糾錯現狀

原始糾錯模塊包括多種召回方式,如:

拼音糾錯:主要解決短query的拼音糾錯問題,拼音完全相同或者模糊音作為糾錯候選。
拼寫糾錯:也叫形近字糾錯,通過遍歷替換形近字,用query熱度過濾,加入候選。
組合糾錯:通過翻譯模型進行糾錯替換,資源主要是通過query對齊挖掘的各種替換資源。

組合糾錯翻譯模型計算公式:

其中p(f)是語言模型,p(f|e)是替換模型。

問題1:召回方式存在缺陷。目前query糾錯模塊主要召回策略包括拼音召回、形近字召回,以及替換資源召回。對於低頻case,解決能力有限。

問題2:排序方式不合理。糾錯按照召回方式分為幾個獨立的模塊,分別完成相應的召回和排序,不合理。

技術改造

改造1:基於空間關係的實體糾錯
原始的糾錯主要是基於用戶session挖掘片段替換資源,所以對於低頻問題解決能力有限。但是長尾問題往往集中在低頻,所以低頻問題是當前的痛點。

地圖搜索與通用搜索引擎有個很大的區別在於,地圖搜索query比較結構化,例如北京市朝陽區阜榮街10號首開廣場。我們可以對query進行結構化切分(也就是地圖中成分分析的工作),得到這樣一種帶有類別的結構化描述,北京市【城市】朝陽區【區縣】阜榮街【道路】10號【門址後綴】首開廣場【通用實體】。

同時,我們擁有權威的地理知識數據,利用權威化的地理實體庫進行前綴樹+後綴樹的索引建庫,提取疑似糾錯的部分在索引庫中進行拉鏈召回,同時利用實體庫中的邏輯隸屬關係對糾錯結果進行過濾。實踐表明,這種方式對低頻的區劃或者實體的錯誤有着明顯的作用。

基於字根的字形相似度計算

上文提到的排序策略裏面通過字形的編輯距離作為排序的重要特徵,這裏我們開發了一個基於字根的字形相似度計算策略,對於編輯距離的計算更為細化和準確。漢字信息有漢字的字根拆分詞表和漢字的筆畫數。

將一個漢字拆分成多個字根,尋找兩個字的公共字根,根據公共字根筆畫數來計算連個字的相似度。

改造2:排序策略重構

原始的策略召回和排序策略耦合,導致不同的召回鏈路,存在顧此失彼的情況。為了能夠充分發揮各種召回方式的優勢,急需要對召回和排序進行解耦並進行全局排序優化。為此我們增加了排序模塊,將流程分為召回和排序兩階段。

模型選擇

對於這個排序問題,這裏我們參考業界的實踐,使用了基於pair-wise的gbrank進行模型訓練。

樣本建設

通過線上輸出結合人工review的方式構造樣本。

特徵建設
(1) 語義特徵。如統計語言模型。
(2) 熱度特徵。pv,點擊等。
(3) 基礎特徵。編輯距離,切詞和成分特徵,累積分佈特徵等。

這裏解決了糾錯模塊兩個痛點問題,一個是在地圖場景下的大部分低頻糾錯問題。另一個是重構了模塊流程,將召回和排序解耦,充分發揮各個召回鏈路的作用,召回方式更新后只需要重訓排序模型即可,使得模塊更加合理,為後面的深度模型升級打下良好的基礎。後面在這個框架下,我們通過深度模型進行seq2seq的糾錯召回,取得了進一步的收益。

3.2 改寫

糾錯作為query變換的一種方式的召回策略存在諸多限制,對於一些非典型的query變換表達,存在策略的空白。比如query=永城市新農合辦,目標POI是永城市新農合服務大廳。用戶的低頻query,往往得不到較好搜索效果,但其實用戶描述的語義與主poi的高頻query是相似的。

這裏我們提出一種query改寫的思路,可以將低頻query改寫成語義相似的高頻query,以更好地滿足用戶需求多樣性的表達。

這是一個從無到有的實現。用戶表達的query是多樣的,使用規則表達顯然是難以窮盡的,直觀的思路是通過向量的方式召回,但是向量召回的方式很可能出現泛化過多,不適應地圖場景的檢索的問題,這些都是要在實踐過程中需要考慮的問題。

方案

整體看,方案包括召回,排序,過濾,三個階段。

召回階段

我們調研了句子向量表示的幾種方法,選擇了算法簡單,效果和性能可以和CNN,RNN媲美的SIF(Smooth Inverse Frequency)。向量召回可以使用開源的Faiss向量搜索引擎,這裏我們使用了阿里內部的性能更好的的向量檢索引擎。

排序階段
樣本構建
原query與高頻query候選集合,計算語義相似度,選取語義相似度的TOPK,人工標註的訓練樣本。

特徵建設

1.基礎文本特徵
2.編輯距離
3.組合特徵

模型選擇

使用xgboost進行分數回歸

過濾階段
通過向量召回的query過度泛化非常嚴重,為了能夠在地圖場景下進行應用,增加了對齊模型。使用了兩種統計對齊模型giza和fastalign,實驗證明二者效果幾乎一致,但fastalign在性能上好於giza,所以選擇fastalign。

通過對齊概率和非對齊概率,對召回的結果進行進一步過濾,得到精度比較高的結果。

query改寫填補了原始query分析模塊中一些低頻表達無法滿足的空白,區別於同義詞或者糾錯的顯式query變換表達,句子的向量表示是相似query的一種隱式的表達,有其相應的優勢。

向量表示和召回也是深度學習模型逐步開始應用的嘗試。同義詞,改寫,糾錯,作為地圖中query變換主要的三種方式,以往在地圖模塊里比較分散,各司其職,也會有互相重疊的部分。在後續的迭代升級中,我們引入了統一的query變換模型進行改造,在取得收益的同時,也擺脫掉了過去很多規則以及模型耦合造成的歷史包袱。

3.2 省略

在地圖搜索場景里,有很多query包含無效詞,如果用全部query嘗試去召回很可能不能召回有效結果。如廈門市搜”湖裡區縣后高新技術園新捷創運營中心11樓1101室 縣后brt站”。這就需要一種檢索意圖,在不明顯轉義下,使用核心term進行召回目標poi候選集合,當搜索結果無果或者召回較差時起到補充召回的作用。

在省略判斷的過程中存在先驗后驗平衡的問題。省略意圖是一個先驗的判斷,但是期望的結果是能夠進行POI有效召回,和POI的召回字段的現狀密切相關。如何能夠在策略設計的過程中保持先驗的一致性,同時能夠在後驗POI中拿到相對好的效果,是做好省略模塊比較困難的地方。

原始的省略模塊主要是基於規則進行的,規則依賴的主要特徵是上游的成分分析特徵。由於基於規則擬合,模型效果存在比較大的優化空間。另外,由於強依賴成分分析,模型的魯棒性並不好。

技術改造

省略模塊的改造主要完成了規則到crf模型的升級,其中也離線應用了深度學習模型輔助樣本生成。

模型選擇

識別出來query哪些部分是核心哪些部分是可以省略的,是一個序列標註問題。在淺層模型的選型中,顯而易見地,我們使用了crf模型。

特徵建設

term特徵。使用了賦權特徵,詞性,先驗詞典特徵等。
成分特徵。仍然使用成分分析的特徵。
統計特徵。統計片段的左右邊界熵,城市分佈熵等,通過分箱進行離散化。

樣本建設

項目一期我們使用了使用線上策略粗標,外包細標的方式,構造了萬級的樣本供crf模型訓練。

但是省略query的多樣性很高,使用萬級的樣本是不夠的,在線上模型無法快速應用深度模型的情況下,我們使用了boostraping的方式,藉助深度模型的泛化能力,離線構造了大量樣本。

使用了這種方式,樣本從萬級很容易擴充到百萬級,我們仍然使用crf模型進行訓練和線上應用。

在省略模塊,我們完成了規則到機器學習的升級,引入了成分以外的其他特徵,提升了模型的魯棒性。同時並且利用離線深度學習的方式進行樣本構造的循環,提升了樣本的多樣性,使得模型能夠更加接近crf的天花板。

在後續深度模型的建模中,我們逐步擺脫了對成分分析特徵的依賴,對query到命中poi核心直接進行建模,構建大量樣本,取得了進一步的收益。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

對js中局部變量、全局變量和閉包的理解

對js中局部變量、全局變量和閉包的理解

局部變量

對於局部變量,js給出的定義是這樣的:在 JavaScript函數內部聲明的變量(使用 var)是局部變量,所以只能在函數內部訪問它。(該變量的作用域是局部的)。可以在不同的函數中使用名稱相同的局部變量,因為只有聲明過該變量的函數才能識別出該變量。只要函數運行完畢,本地變量就會被刪除

我們先來逐步理解:

  • 只能在函數內部訪問

    function test() {
        var a = 0;
        return a;
    }
    
    console.log(a);
    //結果:a is not defined

    上面的代碼聲明了一個test()函數,在函數內部聲明了一個局部變量a,當我們嘗試在函數外訪問局部變量a時,出來的結果是a is not defined

    我們再來看下面這個例子:

    function test() {
        var a = 0;
        return a;
    }
    
    console.log(test());
    //結果:0

    以上兩個例子很好的闡述了局部變量只能在函數內部訪問,當調用函數時,函數域自動執行其中的代碼,局部變量自然也被調用。

  • 只要函數運行完畢,本地變量就會被刪除

    function b() {
        var y = 0;
        z = ++y;
        console.log("這是局部變量y:",z)
        return z;
    }
    
    console.log(b(),b(),b());
    //結果:這是局部變量y: 1
    //這是局部變量y: 1
    //這是局部變量y: 1
    //1 1 1

    從上面代碼我們可以看出,我們執行了3次函數調用,得到的結果都是1,可能有人會說,這很簡單啊,每次出來的結果都是1,那是因為每次執行函數,函數內都會將局部變量y初始化為0。沒錯,的確是這樣,但是如果不初始化變量,則得到的返回值是NaN,所以初始化是必要的。所以,無論用什麼辦法,在函數內部用一個局部變量去做累加,是不可能實現的。但是,我們可以通過全局變量和閉包來實現累加。

全局變量

在js中,這樣定義全局變量, 在函數外聲明的變量是全局變量,網頁上的所有腳本和函數都能訪問它。 全局變量會在頁面關閉后被刪除

  • 我們再來看一個例子

    var a = 0;
    
    function b() {
        ++a;
        console.log("這是全局變量a",a);
        return a;
    }
    console.log("這是未改變的全局變量a:",a,"這是函數b():",b(),b(),b(),"這是改變后的全局變量a:",a);
    //結果:這是全局變量a 1
    //這是全局變量a 2
    //這是全局變量a 3
    //這是未改變的全局變量a: 0 這是函數b(): 1 2 3 這是改變后的全局變量a: 3

    上面代碼定義了一個全局變量a,和一個b()函數,通過函數內部對a執行自加加,實現了累加目的,通過三次調用函數,得到的結果a為3。

閉包

什麼是閉包呢?閉包的定義是這樣的,閉包是一種保護私有變量的機制,在函數執行時形成私有的作用域,保護裏面的私有變量不受外界干擾。直觀的說就是形成一個不銷毀的棧環境。

我對閉包的理解是這樣的,閉包就是一個內嵌函數引用頂層函數的變量,而頂層函數是一個立即執行函數(自調用函數),因為它會自動調用,所以局部變量不會被刪除,但是這會增加內存消耗。

  • 來看一個例子

    function a() {
        var b = 0;
        return function() {
            return ++b;
        }
    }
    
    var closure = a();
    console.log("這是閉包:",closure(),closure(),closure());
    //結果:這是閉包: 1 2 3

    我們看到,由於閉包的特殊機制,使得局部變量在函數執行完之後不會被銷毀,由此得到的最後結果為3 ,而不是1。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Redis 4.0鮮為人知的功能將加速您的應用程序

來源:Redislabs

作者:Kyle Davis

翻譯:Kevin (公眾號:中間件小哥)

Redis 4.0給Redis生態帶來了一個驚人的功能:Modules(模塊)。Modules是Redis的一大轉變,它是Redis內部自定義數據類型和全速計算的開放環境。但是,儘管對該版本的大多數關注都集中在Modules上,但新版本還引入了一個非常重要的命令,它就是遊戲規則的改變者:UNLINK。

您可以使用redis-cli連接redis-server執行info命令,去查看當前redis版本中是否可以使用UNLINK命令。info響應將告訴您有關服務器的所有信息。在第一部分(#Server)中,返回結果有一行值為redis_version。如果該值大於4.0,則可以使用UNLINK命令。並非所有Redis提供商都保持最新版本,因此最好在更改代碼之前檢查redis版本。

讓我們回顧一下Redis的關鍵架構功能之一:“單線程”。Redis在大多數情況下是一個單線程應用程序。它一次只做一件事,這樣可以把這些事做的更快。多線程有點複雜,並且引入了鎖和其他可能降低應用程序速度的問題。儘管Redis(最高4.0版)通過多線程方式執行了少量操作,但它通常在啟動另一個命令之前先要完成一個命令。

相比於快速讀寫,您可能會覺得使用DEL命令去刪除一個鍵值不需要考慮太多,但是在很多情況下,刪除數據同樣很重要。與Redis中的大多數命令一樣,DEL命令在單個線程中運行,如果您獲取一個幾千字節的鍵值,花費不到一毫秒的時間,這是您所感知不到的。然而,當您獲取的鍵值大小是兆字節、100兆字節或者500兆字節會發生什麼呢?哈希、排序、列表等數據結構會隨着時間的推移而添加更多的數據進去,這樣會生成一個數GB大小的數據集。然後用DEL命令去刪除大Key時會發生什麼呢?由於Redis是單線程操作的,處理這種請求時整個服務都處於等待中,需要等待該命令執行完成才能執行其它操作。同時,我們考慮更複雜的一種場景,這些鍵中保存的數據可能已經包含數以千萬個微小請求,因此應用程序或操作員可能無法真正了解刪除這些數據需要花費多長時間。

理智會告訴我們不要在擁有100萬元素的排序集上運行如下這樣的命令:

> ZRANGE some-zset 0 -1

但是,在上面的some-zset集合中執行DEL命令將花費和上面一樣的時間-中間沒有傳輸開銷,但是它會一直去分配內存,而且您會一直卡死在CPU繁忙中。在使用UNLINK之前,您可能會結合SCAN命令採用非原子性的方法進行一些少量刪除,去避免這種持續分配內存的噩夢。上面無論使用哪種方式,都是讓人無法接受的。

您可能已經猜到了,就是使用UNLINK命令來替換DEL!從語法上講,UNLINK與DEL相同,但UNLINK提供了更為理想的解決方案。首先,它將鍵值從整個鍵值空間中刪除。然後,在另一個線程中,它開始回收內存。從多線程的角度來看,這是一種安全的操作,因為它(在主線程中)從鍵空間中刪除了該項,從而使Redis其它命令無法訪問。

如果你有一個快速增長的鍵值-不管鍵值的大小如何,UNLINK都是O(1)操作(每個鍵;在主線程中)。使用DEL刪除一個大值可能需要幾百毫秒或更長時間,而UNLINK將在不到一毫秒的時間內完成(包括網絡往返)。當然,您的服務器仍將需要花一些時間在另一個線程中重新分配該值的內存(其中的工作是O(N),其中N是已刪除值的分配數),但是主線程的性能不會被另一個線程中正在進行的操作嚴重影響到。

因此,您是否應該用UNLINK命令替換代碼中的所有DEL命令?當然,在少數情況下,DEL正是您所需要的。這裏我可以想到兩點:

1、   在MULTI / EXEC或pipeline中,在添加和刪除大值時DEL命令是一種理想選擇。在這種情況下,UNLINK不會立即釋放空間,並且在處理繁忙的情況下(如果內存已滿),您可能會遇到麻煩。

2、   在更緊急的情況下,在無快速響應驅逐數據下您可以寫入數據。

在沒有極端內存限制的理想環境中,很難想到不使用UNLINK的情況。UNLINK將提供更一致的行為,總體上具有更好的性能,並且代碼更改非常小(如果可以在客戶端中重命名命令,則無需更改)。如果UNLINK適合您的應用程序,請就此將您的DEL更改為UNLINK,然後查看它的性能提高。

 

更多優質中間件技術資訊/原創/翻譯文章/資料/乾貨,請關注“中間件小哥”公眾號!

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

如何使用偽類選擇器

偽類選擇器介紹

  • 偽類選擇器就是用來給超級鏈接設置不同的狀態樣式。
  • 超級鏈接分為4種狀態如:正常狀態、訪問過後狀態、鼠標放上狀態、激活狀態。

偽類選擇器說明表

選擇器 描述
:link 向未被訪問的超級鏈接添加樣式,正常狀態。
:visited 向已經被訪問的超級鏈接添加樣式,訪問過後狀態。
:hover 當鼠標懸浮在超級鏈接上方時,向超級鏈接添加樣式,鼠標放上狀態。
:active 鼠標放在超級鏈接上並且點擊的一瞬間,向超級鏈接添加樣式,激活狀態。

偽類選擇器實踐

  • 讓我們進入偽類選擇器實踐,實踐內容將超級鏈接4種狀態進行演示,演示效果如:將向未被訪問的超級鏈接文本顏色設置為紅色、已經被訪問的超級鏈接文本顏色設置為綠色、當鼠標懸浮在超級鏈接上文本顏色設置為紫色、用鼠標點擊超級鏈接的一瞬間文本顏色設置為藍色

  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>偽類選擇器</title>
    <style>
        a:link{
            color:red;
        }
        a:visited{
            color: lime;
        }
        a:hover{
            color: purple;
        }
        a:active{
            color: blue;
        }
    </style>
</head>
  
<body>
    <a href="https://www.cnblogs.com/lq000122/">微笑是最初的信仰</a>
</body>
</html>
  • 正常狀態結果圖

  • 鼠標放上狀態結果圖

  • 激活狀態結果圖

  • 訪問過後狀態

總結

  • 超級鏈接的不同狀態他其實是由順序,也就是說偽類選擇器設置其實是順序的,如果按照偽類選擇器的順序,那麼設置的樣式就不會被渲染。
  • 順序:linkvisitedhoveractive

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

高併發編程學習(1)——併發基礎

為更良好的閱讀體驗,請訪問原文:

一、前言

當我們使用計算機時,可以同時做許多事情,例如一邊打遊戲一邊聽音樂。這是因為操作系統支持併發任務,從而使得這些工作得以同時進行。

  • 那麼提出一個問題:如果我們要實現一個程序能一邊聽音樂一邊玩遊戲怎麼實現呢?
public class Tester {

    public static void main(String[] args) {
        System.out.println("開始....");
        playGame();
        playMusic();
        System.out.println("結束....");
    }

    private static void playGame() {
        for (int i = 0; i < 50; i++) {
            System.out.println("玩遊戲" + i);
        }
    }

    private static void playMusic() {
        for (int i = 0; i < 50; i++) {
            System.out.println("播放音樂" + i);
        }
    }
}

我們使用了循環來模擬過程,因為播放音樂和打遊戲都是連續的,但是結果卻不盡人意,因為函數體總是要執行完之後才能返回。那麼到底怎麼解決這個問題?

并行與併發

并行性和併發性是既相似又有區別的兩個概念。

并行性是指兩個或多個事件在同一時刻發生。而併發性是指兩個或多個事件在同一時間間隔內發生。

在多道程序環境下,併發性是指在一段時間內宏觀上有多個程序在同時運行,但在單處理機環境下(一個處理器),每一時刻卻僅能有一道程序執行,故微觀上這些程序只能是分時地交替執行。例如,在 1 秒鐘時間內,0 – 15 ms 程序 A 運行;15 – 30 ms 程序 B 運行;30 – 45 ms 程序 C 運行;45 – 60 ms 程序 D 運行,因此可以說,在 1 秒鐘時間間隔內,宏觀上有四道程序在同時運行,但微觀上,程序 A、B、C、D 是分時地交替執行的。

如果在計算機系統中有多個處理機,這些可以併發執行的程序就可以被分配到多個處理機上,實現併發執行,即利用每個處理機愛處理一個可併發執行的程序。這樣,多個程序便可以同時執行。以此就能提高系統中的資源利用率,增加系統的吞吐量。

進程和線程

進程是指一個內存中運行的應用程序。一個應用程序可以同時啟動多個進程,那麼上面的問題就有了解決的思路:我們啟動兩個進程,一個用來打遊戲,一個用來播放音樂。這當然是一種解決方案,但是想象一下,如果一個應用程序需要執行的任務非常多,例如 LOL 遊戲吧,光是需要播放的音樂就有非常多,人物本身的語音,技能的音效,遊戲的背景音樂,塔攻擊的聲音等等等,還不用說遊戲本身,就光播放音樂就需要創建許多許多的進程,而進程本身是一種非常消耗資源的東西,這樣的設計顯然是不合理的。更何況大多數的操作系統都不需要一個進程訪問其他進程的內存空間,也就是說,進程之間的通信很不方便,此時我們就得引入“線程”這門技術,來解決這個問題。

線程是指進程中的一個執行任務(控制單元),一個進程可以同時併發運行多個線程。我們可以打開任務管理器,觀察到幾乎所有的進程都擁有着許多的「線程」(在 WINDOWS 中線程是默認隱藏的,需要在「查看」裏面點擊「選擇列」,有一個線程數的勾選項,找到並勾選就可以了)。

進程和線程的區別

進程:有獨立的內存空間,進程中的數據存放空間(堆空間和棧空間)是獨立的,至少有一個線程。

線程:堆空間是共享的,棧空間是獨立的,線程消耗的資源也比進程小,相互之間可以影響的,又稱為輕型進程或進程元。

因為一個進程中的多個線程是併發運行的,那麼從微觀角度上考慮也是有先後順序的,那麼哪個線程執行完全取決於 CPU 調度器(JVM 來調度),程序員是控制不了的。我們可以把多線程併發性看作是多個線程在瞬間搶 CPU 資源,誰搶到資源誰就運行,這也造就了多線程的隨機性。下面我們將看到更生動的例子。

Java 程序的進程(Java 的一個程序運行在系統中)里至少包含主線程和垃圾回收線程(後台線程),你可以簡單的這樣認為,但實際上有四個線程(了解就好):

  • [1] main——main 線程,用戶程序入口
  • [2] Reference Handler——清除 Reference 的線程
  • [3] Finalizer——調用對象 finalize 方法的線程
  • [4] Signal Dispatcher——分發處理髮送給 JVM 信號的線程

多線程和單線程的區別和聯繫?

  1. 單核 CPU 中,將 CPU 分為很小的時間片,在每一時刻只能有一個線程在執行,是一種微觀上輪流佔用 CPU 的機制。

  2. 多線程會存在線程上下文切換,會導致程序執行速度變慢,即採用一個擁有兩個線程的進程執行所需要的時間比一個線程的進程執行兩次所需要的時間要多一些。

結論:即採用多線程不會提高程序的執行速度,反而會降低速度,但是對於用戶來說,可以減少用戶的響應時間。

多線程的優勢

儘管面臨很多挑戰,多線程有一些優點仍然使得它一直被使用,而這些優點我們應該了解。

優勢一:資源利用率更好

想象一下,一個應用程序需要從本地文件系統中讀取和處理文件的情景。比方說,從磁盤讀取一個文件需要 5 秒,處理一個文件需要 2 秒。處理兩個文件則需要:

1| 5秒讀取文件A
2| 2秒處理文件A
3| 5秒讀取文件B
4| 2秒處理文件B
5| ---------------------
6| 總共需要14秒

從磁盤中讀取文件的時候,大部分的 CPU 時間用於等待磁盤去讀取數據。在這段時間里,CPU 非常的空閑。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用 CPU 資源。看下面的順序:

1| 5秒讀取文件A
2| 5秒讀取文件B + 2秒處理文件A
3| 2秒處理文件B
4| ---------------------
5| 總共需要12秒

CPU 等待第一個文件被讀取完。然後開始讀取第二個文件。當第二文件在被讀取的時候,CPU 會去處理第一個文件。記住,在等待磁盤讀取文件的時候,CPU 大部分時間是空閑的。

總的說來,CPU 能夠在等待 IO 的時候做一些其他的事情。這個不一定就是磁盤 IO。它也可以是網絡的 IO,或者用戶輸入。通常情況下,網絡和磁盤的 IO 比 CPU 和內存的 IO 慢的多。

優勢二:程序設計在某些情況下更簡單

在單線程應用程序中,如果你想編寫程序手動處理上面所提到的讀取和處理的順序,你必須記錄每個文件讀取和處理的狀態。相反,你可以啟動兩個線程,每個線程處理一個文件的讀取和操作。線程會在等待磁盤讀取文件的過程中被阻塞。在等待的時候,其他的線程能夠使用 CPU 去處理已經讀取完的文件。其結果就是,磁盤總是在繁忙地讀取不同的文件到內存中。這會帶來磁盤和 CPU 利用率的提升。而且每個線程只需要記錄一個文件,因此這種方式也很容易編程實現。

優勢三:程序響應更快

有時我們會編寫一些較為複雜的代碼(這裏的複雜不是說複雜的算法,而是複雜的業務邏輯),例如,一筆訂單的創建,它包括插入訂單數據、生成訂單趕快找、發送郵件通知賣家和記錄貨品銷售數量等。用戶從單擊“訂購”按鈕開始,就要等待這些操作全部完成才能看到訂購成功的結果。但是這麼多業務操作,如何能夠讓其更快地完成呢?

在上面的場景中,可以使用多線程技術,即將數據一致性不強的操作派發給其他線程處理(也可以使用消息隊列),如生成訂單快照、發送郵件等。這樣做的好處是響應用戶請求的線程能夠盡可能快地處理完成,縮短了響應時間,提升了用戶體驗。

其他優勢

多線程還有一些優勢也顯而易見:

  • 進程之前不能共享內存,而線程之間共享內存(堆內存)則很簡單。
  • 系統創建進程時需要為該進程重新分配系統資源,創建線程則代價小很多,因此實現多任務併發時,多線程效率更高.
  • Java 語言本身內置多線程功能的支持,而不是單純地作為底層系統的調度方式,從而簡化了多線程編程.

上下文切換

即使是單核處理器也支持多線程執行代碼,CPU 通過給每個線程分配 CPU 時間片來實現這個機制。時間片是 CPU 分配給各個線程的時間,因為時間片非常短,所以 CPU 通過不停地切換線程執行,讓我們感覺多個線程是同時執行的,時間片一般是幾十毫秒(ms)。

CPU 通過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。但是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務的時候,可以再加載這個任務的狀態。所以任務從保存到再加載的過程就是一次上下文切換。

這就像我們同時讀兩本書,當我們在讀一本英文的技術書時,發現某個單詞不認識,於是打開中英文字典,但是在放下英文技術書之前,大腦必須先記住這本書獨到了多少頁的多少行,等查完單詞之後,能夠繼續讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多線程的執行速度。

二、創建線程的兩種方式

繼承 Thread 類

public class Tester {

    // 播放音樂的線程類
    static class PlayMusicThread extends Thread {

        // 播放時間,用循環來模擬播放的過程
        private int playTime = 50;

        public void run() {
            for (int i = 0; i < playTime; i++) {
                System.out.println("播放音樂" + i);
            }
        }
    }

    // 方式1:繼承 Thread 類
    public static void main(String[] args) {
        // 主線程:運行遊戲
        for (int i = 0; i < 50; i++) {
            System.out.println("打遊戲" + i);
            if (i == 10) {
                // 創建播放音樂線程
                PlayMusicThread musicThread = new PlayMusicThread();
                musicThread.start();
            }
        }
    }
}

運行結果發現打遊戲和播放音樂交替出現,說明已經成功了。

實現 Runnable 接口

public class Tester {

    // 播放音樂的線程類
    static class PlayMusicThread implements Runnable {

        // 播放時間,用循環來模擬播放的過程
        private int playTime = 50;

        public void run() {
            for (int i = 0; i < playTime; i++) {
                System.out.println("播放音樂" + i);
            }
        }
    }

    // 方式2:實現 Runnable 方法
    public static void main(String[] args) {
        // 主線程:運行遊戲
        for (int i = 0; i < 50; i++) {
            System.out.println("打遊戲" + i);
            if (i == 10) {
                // 創建播放音樂線程
                Thread musicThread = new Thread(new PlayMusicThread());
                musicThread.start();
            }
        }
    }
}

也能完成效果。

以上就是傳統的兩種創建線程的方式,事實上還有第三種,我們後邊再講。

多線程一定快嗎?

先來一段代碼,通過并行和串行來分別執行累加操作,分析:下面的代碼併發執行一定比串行執行快嗎?

import org.springframework.util.StopWatch;

// 比較并行和串行執行累加操作的速度
public class Tester {

    // 執行次數
    private static final long COUNT = 100000000;
    private static final StopWatch TIMER = new StopWatch();

    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
        // 打印比較測試結果
        System.out.println(TIMER.prettyPrint());
    }

    private static void serial() {
        TIMER.start("串行執行" + COUNT + "條數據");

        int a = 0;
        for (long i = 0; i < COUNT; i++) {
            a += 5;
        }
        // 串行執行
        int b = 0;
        for (long i = 0; i < COUNT; i++) {
            b--;
        }

        TIMER.stop();
    }

    private static void concurrency() throws InterruptedException {
        TIMER.start("并行執行" + COUNT + "條數據");

        // 通過匿名內部類來創建線程
        Thread thread = new Thread(() -> {
            int a = 0;
            for (long i = 0; i < COUNT; i++) {
                a += 5;
            }
        });
        thread.start();

        // 并行執行
        int b = 0;
        for (long i = 0; i < COUNT; i++) {
            b--;
        }
        // 等待線程結束
        thread.join();
        TIMER.stop();
    }
}

大家可以自己測試一下,每一台機器 CPU 不同測試結果可能也會不同,之前在 WINDOWS 本兒上測試的時候,多線程的優勢從 1 千萬數據的時候才開始體現出來,但是現在換了 MAC,1 億條數據時間也差不多,到 10 億的時候明顯串行就比并行快了… 總之,為什麼併發執行的速度會比串行慢呢?就是因為線程有創建和上下文切換的開銷。

繼承 Thread 類還是實現 Runnable 接口?

想象一個這樣的例子:給出一共 50 個蘋果,讓三個同學一起來吃,並且給蘋果編上號碼,讓他們吃的時候順便要說出蘋果的編號:

運行結果可以看到,使用繼承方式實現,每一個線程都吃了 50 個蘋果。這樣的結果顯而易見:是因為顯式地創建了三個不同的 Person 對象,而每個對象在堆空間中有獨立的區域來保存定義好的 50 個蘋果。

而使用實現方式則滿足要求,這是因為三個線程共享了同一個 Apple 對象,而對象中的 num 數量是一定的。

所以可以簡單總結出繼承方式和實現方式的區別:

繼承方式:

  1. Java 中類是單繼承的,如果繼承了 Thread 了,該類就不能再有其他的直接父類了;
  2. 從操作上分析,繼承方式更簡單,獲取線程名字也簡單..(操作上,更簡單)
  3. 從多線程共享同一個資源上分析,繼承方式不能做到…

實現方式:

  1. Java 中類可以實現多個接口,此時該類還可以繼承其他類,並且還可以實現其他接口(設計上,更優雅)..
  2. 從操作上分析,實現方式稍微複雜點,獲取線程名字也比較複雜,需要使用 Thread.currentThread() 來獲取當前線程的引用..
  3. 從多線程共享同一個資源上分析,實現方式可以做到..

在這裏,三個同學完成搶蘋果的例子,使用實現方式才是更合理的方式。

對於這兩種方式哪種好並沒有一個確定的答案,它們都能滿足要求。就我個人意見,我更傾向於實現 Runnable 接口這種方法。因為線程池可以有效的管理實現了 Runnable 接口的線程,如果線程池滿了,新的線程就會排隊等候執行,直到線程池空閑出來為止。而如果線程是通過實現 Thread 子類實現的,這將會複雜一些。

有時我們要同時融合實現 Runnable 接口和 Thread 子類兩種方式。例如,實現了 Thread 子類的實例可以執行多個實現了 Runnable 接口的線程。一個典型的應用就是線程池。

常見錯誤:調用 run() 方法而非 start() 方法

創建並運行一個線程所犯的常見錯誤是調用線程的 run() 方法而非 start() 方法,如下所示:

1| Thread newThread = new Thread(MyRunnable());
2| newThread.run();  //should be start();

起初你並不會感覺到有什麼不妥,因為 run() 方法的確如你所願的被調用了。但是,事實上,run() 方法並非是由剛創建的新線程所執行的,而是被創建新線程的當前線程所執行了。也就是被執行上面兩行代碼的線程所執行的。想要讓創建的新線程執行 run() 方法,必須調用新線程的 start() 方法。

三、線程的安全問題

吃蘋果遊戲的不安全問題

我們來考慮一下上面吃蘋果的例子,會有什麼問題?

儘管,Java 並不保證線程的順序執行,具有隨機性,但吃蘋果比賽的案例運行多次也並沒有發現什麼太大的問題。這並不是因為程序沒有問題,而只是問題出現的不夠明顯,為了讓問題更加明顯,我們使用 Thread.sleep() 方法(經常用來模擬網絡延遲)來讓線程休息 10 ms,讓其他線程去搶資源。(注意:在程序中並不是使用 Thread.sleep(10)之後,程序才出現問題,而是使用之後,問題更明顯.)

為什麼會出現這樣的錯誤呢?

先來分析第一種錯誤:為什麼會吃重複的蘋果呢?就拿 B 和 C 都吃了編號為 47 的蘋果為例吧:

  • A 線程拿到了編號為 48 的蘋果,打印輸出然後讓 num 減 1,睡眠 10 ms,此時 num 為 47。
  • 這時 B 和 C 同時都拿到了編號為 47 的蘋果,打印輸出,在其中一個線程作出了減一操作的時候,A 線程從睡眠中醒過來,拿到了編號為 46 的蘋果,然後輸出。在這期間並沒有任何操作不允許 B 和 C 線程不能拿到同一個編號的蘋果,之前沒有明顯的錯誤僅僅可能只是因為運行速度太快了。

再來分析第二種錯誤:照理來說只應該存在 1-50 編號的蘋果,可是 0 和-1 是怎麼出現的呢?

  • 當 num = 1 的時候,A,B,C 三個線程同時進入了 try 語句進行睡眠。
  • C 線程先醒過來,輸出了編號為 1 的蘋果,然後讓 num 減一,當 C 線程醒過來的時候發現 num 為 0 了。
  • A 線程醒過來一看,0 都沒有了,只有 -1 了。

歸根結底是因為沒有任何操作來限制線程來獲取相同的資源並對他們進行操作,這就造成了線程安全性問題。

如果我們把打印和減一的操作分成兩個步驟,會更加明顯:

ABC 三個線程同時打印了 50 的蘋果,然後同時做出減一操作。

像這樣的原子操作,是不允許分步驟進行的,必須保證同步進行,不然可能會引發不可設想的後果。

要解決上述多線程併發訪問一個資源的安全性問題,就需要引入線程同步的概念。

線程同步

多個執行線程共享一個資源的情景,是最常見的併發編程情景之一。為了解決訪問共享資源錯誤或數據不一致的問題,人們引入了臨界區的概念:用以訪問共享資源的代碼塊,這個代碼塊在同一時間內只允許一個線程執行。

為了幫助編程人員實現這個臨界區,Java(以及大多數編程語言)提供了同步機制,當一個線程試圖訪問一個臨界區時,它將使用一種同步機制來查看是不是已經有其他線程進入臨界區。如果沒有其他線程進入臨界區,他就可以進入臨界區。如果已經有線程進入了臨界區,它就被同步機制掛起,直到進入的線程離開這個臨界區。如果在等待進入臨界區的線程不止一個,JVM 會選擇其中的一個,其餘的將繼續等待。

synchronized 關鍵字

如果一個對象已用 synchronized 關鍵字聲明,那麼只有一個執行線程被允許訪問它。使用 synchronized 的好處顯而易見:保證了多線程併發訪問時的同步操作,避免線程的安全性問題。但是壞處是:使用 synchronized 的方法/代碼塊的性能比不用要低一些。所以好的做法是:盡量減小 synchronized 的作用域。

我們還是先來解決吃蘋果的問題,考慮一下 synchronized 關鍵字應該加在哪裡呢?

發現如果還再把 synchronized 關鍵字加在 if 裏面的話,0 和 -1 又會出來了。這其實是因為當 ABC 同是進入到 if 語句中,等待臨界區釋放的時,拿到 1 編號的線程已經又把 num 減一操作了,而此時最後一個等待臨界區的進程拿到的就會是 -1 了。

同步鎖 Lock

Lock 機制提供了比 synchronized 代碼塊和 synchronized 方法更廣泛的鎖定操作,同步代碼塊/ 同步方法具有的功能 Lock 都有,除此之外更強大,更體現面向對象。在併發包的類族中,Lock 是 JUC 包的頂層接口,它的實現邏輯並未用到 synchronized,而是利用了 volatile 的可見性。

使用 Lock 最典型的代碼如下:

class X {

    private final ReentrantLock lock = new ReentrantLock();

    public void m() {
        lock.lock();
        try {
            // ..... method body
        } finally {
            lock.unlock();
        }
    }
}

線程安全問題

線程安全問題只在多線程環境下才會出現,單線程串行執行不存在此類問題。保證高併發場景下的線程安全,可以從以下四個維度考量:

維度一:數據單線程可見

單線程總是安全的。通過限制數據僅在單線程內可見,可以避免數據被其他線程篡改。最典型的就是線程局部變量,它存儲在獨立虛擬機棧幀的局部變量表中,與其他線程毫無瓜葛。TreadLocal 就是採用這種方式來實現線程安全的。

維度二:只讀對象

只讀對象總是安全的。它的特性是允許複製、拒絕寫入。最典型的只讀對象有 String、Integer 等。一個對象想要拒絕任何寫入,必須要滿足以下條件:

  • 使用 final 關鍵字修飾類,避免被繼承;
  • 使用 private final 關鍵字避免屬性被中途修改;
  • 沒有任何更新方法;
  • 返回值不能為可變對象。

維度三:線程安全類

某些線程安全類的內部有非常明確的線程安全機制。比如 StringBuffer 就是一個線程安全類,它採用 synchronized 關鍵字來修飾相關方法。

維度四:同步與鎖機制

如果想要對某個對象進行併發更新操作,但又不屬於上述三類,需要開發工程師在代碼中實現安全的同步機制。雖然這個機制支持的併發場景很有價值,但非常複雜且容易出現問題。

處理線程安全的核心理念

要麼只讀,要麼加鎖。

合理利用好 JDK 提供的併發包,往往能化腐朽為神奇。Java 併發包(java.util.concurrent,JUC)中大多數類註釋都寫有:@author Doug Lea。如果說 Java 是一本史書,那麼 Doug Lea 絕對是開疆拓土的偉大人物。Doug Lea 在當大學老師時,專攻併發編程和併發數據結構設計,主導設計了 JUC 併發包,提高了 Java 併發編程的易用性,大大推進了 Java 的商用進程。

參考資料

  • 《Java 零基礎入門教程》 –
  • 《Java 併發編程的藝術》
  • 《Java 7 併發編程實戰手冊》
  • 《碼出高效 Java 開發手冊》 – 楊冠寶(孤盡) 高海慧(鳴莎)著

按照慣例黏一個尾巴:

歡迎轉載,轉載請註明出處!
獨立域名博客:wmyskxz.com
簡書 ID:
github:
歡迎關注公眾微信號:wmyskxz
分享自己的學習 & 學習資料 & 生活
想要交流的朋友也可以加 qq 群:3382693

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

串燒 JavaCAS相關知識

JMM與問題引入

為啥先說JMM,因為CAS的實現類中維護的變量都被volatile修飾, 這個volatile 是遵循JMM規範(不是百分百遵循,下文會說)實現的保證多線程併發訪問某個變量實現線程安全的手段

一連串的知識點慢慢縷

首先說什麼是JMM, JMM就是大家所說的java的內存模型, 它是人們在邏輯上做出的劃分, 或者可以將JMM當成是一種規範, 有哪些規範呢? 如下

  1. 可見性: 某一個線程對內存中的變量做出改動后,要求其他的線程在第一事件內馬上馬得到通知,在CAS的實現中, 可見性其實是通過不斷的while循環讀取而得到的通知, 而不是被動的得到通知
  2. 原子性: 線程在執行某個操作的時,要麼一起成功,要麼就一起失敗
  3. 有序性: 為了提高性能, 編譯器處理器會進行指令的重排序, 源碼-> 編譯器優化重排 -> 處理器優化重排 -> 內存系統重排 -> 最終執行的命令

JVM運行的實體是線程, 每一個線程在創建之後JVM都會為其創建一個工作空間, 這個工作空間是每一個線程之間的私有空間, 並且任何兩條線程之間的都不能直接訪問到對方的工作空間, 線程之間的通信,必須通過共享空間來中轉完成

JMM規定所有的變量全部存在主內存中,主內存是一塊共享空間,那麼如果某個線程相對主內存中共享變量做出修改怎麼辦呢? 像下面這樣:

  1. 將共享變量的副本拷貝到工作空間中
  2. 對變量進行賦值修改
  3. 將工作空間中的變量寫回到內存中

JMM還規定如下:

  • 任何線程在解鎖前必須將工作空間的共享變量立即刷新進內存中
  • 線程在加鎖前必須讀取主內存中的值更新到自己的工作空間中
  • 加鎖和解鎖是同一把鎖

問題引入

這時候如果多個線程併發按照上面的三步走去訪問主內存中的共享變量的話就會出現線程安全性的問題, 比如說 現在主內存中的共享變量是c=1, 有AB兩個線程去併發訪問這個c變量, 都想進行c++, 現在A將c拷貝到自己的工作空間進行c++, 於是c=2 , 於此同時線程B也進行c++, c在B的工作空間中=2, AB線程將結果寫回工作空間最終的結果就是2, 而不是我們預期的3

相信怎麼解決大家都知道, 就是使用JUC,中的原子類就能規避這個問題

而原子類的底層實現使用的就是CAS技術

什麼是CAS

CAS(compare and swap) 顧名思義: 比較和交換,在JUC中原子類的底層使用的都是CAS無鎖實現線程安全,是一門很炫的技術

如下面兩行代碼, 先比較再交換, 即: 如果從主內存中讀取到的值為4就將它更新為2019

        AtomicInteger atomicInteger = new AtomicInteger(4);
        atomicInteger.compareAndSet(4,2019);

跟進AtomicInteger的源碼如下, 底層維護着一個int 類型的 變量, (當然是因為我選擇的原來類是AtomicInteger類型), 並且這個int類型的值被 volatile 修飾

    private volatile int value;

    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

什麼是volatile

volatile是JVM提供的輕量的同步機制, 為什麼是輕量界別呢? , 剛才在上面說了JMM規範中提到了三條特性, 而JVM提供的volatile僅僅滿足上面的規範中的 2/3, 如下:

  1. 保證可見性
  2. 不保證原子性
  3. 禁止指令重排序

單獨的volatile是不能滿足原子性的,即如下代碼在多線程併發訪問的情況下依然會出現線程安全性問題

private volatile int value;
 
public void add(){
  value++;   
}

那麼JUC的原子類是如何實現的 可以滿足原子性呢? 於是就不得不說本片博文的主角, CAS

CAS源碼跟進

我們跟進AtomicInteger中的先遞增再獲取的方法 incrementAndGet()

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

通過代碼我們看到調用了Unsafe類來實現

什麼是Unsafe類?

進入Unsafe類,可以看到他裏面存在大量的 native方法,這些native方法全部是空方法,

這個unsafe類其實相當於一個後門,他是java去訪問調用系統上 C C++ 函數類庫的方法 如下圖

繼續跟進這個方法incrementAndGet() 於是我們就來到了我們的主角方法, 關於這個方法倒是不難理解,主要是搞清楚方法中的var12345到底代表什麼就行, 如下代碼+註釋

var1: 上一個方法傳遞進來的: this,即當前對象
var2: 上一個方法傳遞進來的valueOffset, 就是內存地址偏移量
      通過這個內存地址偏移量我能精確的找到要操作的變量在內存中的地址
      
var4: 上一個方法傳遞進來的1, 就是每次增長的值
var5: 通過this和內存地址偏移量讀取出來的當前內存中的目標值
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

注意它用的是while循環, 相對if(flag){} 這種寫法會多一次判斷, 整體的思路就是 在進行修改之前先進行一次比較,如果讀取到的當前值和預期值是相同的,就自增,否則的話就繼續輪詢修改

小總結

通過上面的過程, 其實就能總結出CAS的底層實現原理

  • volatile
  • 自旋鎖
  • unsafe類

補充: CAS通過Native方法的底層實現,本質上是操作系統層面上的CPU的併發原語,JVM會直接實現出彙編層面的指令,依賴於硬件去實現, 此外, 對於CPU的原語來說, 有兩條特性1,必定連續, 2.不被中斷

CAS的優缺點

優點:

它的底層我們看到了通過do-while 實現的自旋鎖來實現, 就省去了在多個線程之間進行切換所帶來的額外的上下文切換的開銷

缺點:

  1. 通過while循環不斷的嘗試獲取, 省去了上下文切換的開銷,但是佔用cpu的資源
  2. CAS只能保證一個共享變量的原子性, 如果存在多個共享變量的話不得不加鎖實現
  3. 存在ABA問題

ABA問題

什麼是ABA問題

我們這樣玩, 還是AB兩個線程, 給AtomicInteger賦初始值0

A線程中的代碼如下:

        Thread.sleep(3000);
        atomicInteger.compareAndSet(0,2019);

B線程中的代碼如下:

        atomicInteger.compareAndSet(0,1);
        atomicInteger.compareAndSet(1,0);

AB線程同時啟動, 雖然最終的結果A線程能成果的將值修改成2019,,但是它不能感知到在他睡眠過程中B線程對數據進行過改變, 換句話說就是A線程被B線程欺騙了

ABA問題的解決— AtomicStampedRefernce.java

帶時間戳的原子引用, 實現的機制就是通過 原子引用+版本號來完成, 每次對指定值的修改相應的版本號會加1, 實例如下

        // 0表示初始化, 1表示初始版本號
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(0, 1);
        reference.getStamp(); // 獲取版本號
        reference.attemptStamp(1,2); // 期待是1, 如果是1就更新為2

原子引用

JUC中我們可以找到像AtomicInteger這樣已經定義好了實現類, 但是JUC沒有給我們提供類似這樣 AtomicUser或者 AtomicProduct 這樣自定義類型的原子引用類型啊, 不過java仍然是提供了後門就是 原子引用類型

使用實例:

        User user  = getUserById(1);
        AtomicReference<User> userAtomicReference = new AtomicReference<User>();
        user.setUsername("張三");
        userAtomicReference.compareAndSet(user,user);

歡迎關注我, 會繼續更新筆記

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

SpringSecurity動態加載用戶角色權限實現登錄及鑒權

很多人覺得Spring Security實現登錄驗證很難,我最開始學習的時候也這樣覺得。因為我好久都沒看懂我該怎麼樣將自己寫的用於接收用戶名密碼的Controller與Spring Security結合使用,這是一個先入為主的誤區。後來我搞懂了:根本不用你自己去寫Controller。你只需要告訴Spring Security用戶信息、角色信息、權限信息、登錄頁是什麼?登陸成功頁是什麼?或者其他有關登錄的一切信息。具體的登錄驗證邏輯它來幫你實現。

一、動態數據登錄驗證的基礎知識

在本號之前的文章中,已經介紹了Spring Security的formLogin登錄認證模式,RBAC的權限控制管理模型,並且針對Spring Security的登錄認證邏輯源碼進行了解析等等。我們所有的用戶、角色、權限信息都是在配置文件裏面寫死的,然而在實際的業務系統中,這些信息通常是存放在RBAC權限模型的數據庫表中的。下面我們來回顧一下其中的核心概念:

  • RBAC的權限模型可以從用戶獲取為用戶分配的一個或多個角色,從用戶的角色又可以獲取該角色的多種權限。通過關聯查詢可以獲取某個用戶的角色信息和權限信息。
  • 在源碼解析的文章中,我們知道如果我們不希望用戶、角色、權限信息寫死在配置裏面。我們應該實現UserDetails與UserDetailsService接口,從而從數據庫或者其他的存儲上動態的加載這些信息。

以上是對一些核心的基礎知識的總結,如果您對這些知識還不是很清晰,建議您先往下讀本文。如果看完本文仍然理解困難,建議您翻看本號之前的文章。

二、UserDetails與UserDetailsService接口

  • UserDetailsService接口有一個方法叫做loadUserByUsername,我們實現動態加載用戶、角色、權限信息就是通過實現該方法。函數見名知義:通過用戶名加載用戶。該方法的返回值就是UserDetails。
  • UserDetails就是用戶信息,即:用戶名、密碼、該用戶所具有的權限。

下面我們來看一下UserDetails接口都有哪些方法。

public interface UserDetails extends Serializable {
    //獲取用戶的權限集合
    Collection<? extends GrantedAuthority> getAuthorities();

    //獲取密碼
    String getPassword();

    //獲取用戶名
    String getUsername();

    //賬號是否沒過期
    boolean isAccountNonExpired();

    //賬號是否沒被鎖定
    boolean isAccountNonLocked();

    //密碼是否沒過期
    boolean isCredentialsNonExpired();

    //賬戶是否可用
    boolean isEnabled();
}

現在,我們明白了,只要我們把這些信息提供給Spring Security,Spring Security就知道怎麼做登錄驗證了,根本不需要我們自己寫Controller實現登錄驗證邏輯。

三、實現UserDetails 接口

public class SysUser implements UserDetails{
    
    String password();  //密碼
    String username();  //用戶名
    boolean accountNonExpired;   //是否沒過期
    boolean accountNonLocked;   //是否沒被鎖定
    boolean credentialsNonExpired;  //是否沒過期
    boolean enabled;  //賬號是否可用
    Collection<? extends GrantedAuthority> authorities;  //用戶的權限集合

    //省略構造方法
    //省略set方法
    //省略get方法(即接口UserDetails的方法)
}

我們就是寫了一個適應於UserDetails的java POJO類,所謂的 UserDetails接口實現就是一些get方法。get方法由Spring Security調用,我們通過set方法或構造函數為 Spring Security提供UserDetails數據。

四、實現UserDetailsService接口

@Component
public class MyUserDetailsService implements UserDetailsService{

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            
       //這裏從數據庫sys_user表裡面查詢實體類對象。loadUser方法可使用Mybatis或JDBC或JPA自行實現。
       SysUser sysUser =  loadUser(username);   

        // 判斷用戶是否存在 
       if(user == null)  {  throw  new  UsernameNotFoundException("用戶名不存在");  }

       //從數據庫該用戶所有的角色信息,所有的權限標誌
       //遍歷所有的ROLE角色及所有的Authority權限(菜單、按鈕)。
       //用逗號分隔他們的唯一標誌,具體過程自行實現。
       sysUser.setAuthorities(
               AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_AMIN , system:user:delete"));
        
        //sysUser.setAccountNonLocked(true或false);
        return sysUser;
    }
}
  • 通常數據庫表sys_user字段要和SysUser屬性一一對應,比如username、password、enabled。但是比如accountNonLocked字段用於登錄多次錯誤鎖定,但我們一般不會在表裡存是否鎖定,而是存一個鎖定時間字段。通過鎖定時間是否大於當前時間判斷賬號是否鎖定,所以實現過程中可以靈活做判斷並用好set方法,不必拘泥於一一對應的形式。
  • 角色是一種特殊的權限,在Spring Security我們可以使用hasRole(角色標識)表達式判斷用戶是否具有某個角色,決定他是否可以做某個操作;通過hasAuthority(權限標識)表達式判斷是否具有某個操作權限。

五、最後說明

至此,我們將系統裏面的所有的用戶、角色、權限信息都通過UserDetailsService和UserDetails告知了Spring Security。但是多數朋友可能仍然不知道該怎樣實現登錄的功能,其實剩下的事情很簡單了:

  • 寫一個登錄界面,寫一個登錄表單,表單使用post方法提交到默認的/login路徑
  • 表單的用戶名、密碼字段名稱默認是username、password。
  • 寫一個登錄成功之後的跳轉頁面,比如index.html

然後把這些信息通過配置方式告知Spring Security ,以上的配置信息名稱都可以靈活修改。如果您不知道如何配置請參考本號之前的文章《formLogin登錄認證模式》。

期待您的關注

  • 向您推薦博主的系列文檔:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!