搶入特斯拉自駕電動車供應鏈 英特磊營收看漲

英特磊 8 日公布 6 月營收 7,693 萬元,月增 8.18% 而年增 15.2%,第 2 季營收 2.24 億元,季增 8.21% 年增 46.84%,月、季營收同步創高,公司並將在 9 月洽談車用防撞系統新單,可望打入 Tesla 發展自駕(ADAS)電動車需求,再創營運高峰。    據了解,英特磊收購 Soitec 後,取得全球最大車用防撞 IC 廠 UMS 供應認證,UMS 打入多數歐系高級車,包括 BMW、Audi、賓士等,同時間,UMS 也是與 Tesla 計畫合作自駕車系統 Mobileye 的主要客戶,在此緊密的車用供應鏈關係中,英特磊持續獲得熱賣車款點火。   英特磊看好第 3 季旺季需求,將獲得新布局的車用防撞雷達、Skyworks 物聯網帶動,因此公司先在 5 月進行歲修調整機台,將對全年營收衝刺動能有關鍵性助益。 ]

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

【其他文章推薦】

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

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

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

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

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

Elon Musk:Model X 會讓特斯拉銷量倍增

美國豪華電動車製造商特斯拉 (Tesla) 執行長 Elon Musk 仍舊對公司業務信心滿滿,據彭博社報導,Musk 7 日表示,特斯拉預定本季問世的電動運動休旅車「Model X」有望讓公司的電動車銷售量倍增。   特斯拉希望讓 2015 年的電動車銷售量達到 55,000 台,該公司目前只販售 Model S 車款,而其上半年的交車量達到 21,552 台、全年度目標達成度約有 40%。   特斯拉曾於 7 月 2 日宣布,旗下高檔電動車 Model S 第二季交車超出預期,第二季交車量來到 11,507 輛,高於原先預估的 10,000 至 11,000 輛。

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

【其他文章推薦】

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

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

支撐馬蜂窩「雙11」營銷大戰背後的技術架構

(馬蜂窩技術原創內容,公眾號 ID: mfwtech)

引言

消費者的狂歡節「雙 11」剛剛過去。在電商競爭環境日益激烈的今天,為了抓住流量紅利,雙 11 打響的已經不僅僅是「促銷戰」,也是「營銷戰」,這對平台的技術支撐能力提出新的要求。

從 2014 年的「318 大促」,到正在進行的 「馬蜂窩雙 11 全球旅行蜂搶節」,馬蜂窩旅遊電商業務的大促已經走過 5 年時間,僅僅是雙 11、暑期、十一黃金周、年終這些關鍵節點的 S 級促銷就張羅了 50 多場,每年上線活動達幾百個。

圖:馬蜂窩11.11全球旅行蜂搶節

 

在這個過程中,馬蜂窩營銷平台也在經歷着優化和改進,不斷探索靈活高效的營銷活動運營開發方式,更好地支撐業務營銷活動的模式創新和投放需求,努力實現平台商家與馬蜂窩旅遊消費者的高效匹配,目前已經形成了一套較為完整的技術體系。

本文將以馬蜂窩營銷活動後台的技術實踐為重點,分享馬蜂窩營銷平台的架構設計思路,希望能讓遇到類似問題的同學有所收穫。

 

一、馬蜂窩營銷平台架構

1. 營銷中心體系

談到大促,大家可能首先會想到的海量數據、高併發、高流量等關鍵詞。其實除了這些,營銷活動數量多、周期短、功能複雜多變等,都是營銷活動運營開發需要應對的挑戰。並且由於我們的很多活動會涉及到一些獎勵或權益的下發,對於安全性的要求也很高。

針對以上問題,馬蜂窩營銷系統的技術架構要具備的核心能力包括:

  • 打造靈活、高效的活動開發模式

  • 提供高可靠、高可用的活動運營支撐

  • 保證營銷活動業務的安全運行

因此,我們本着「平台化、組件化、模塊化」的方法,將營銷體系的架構設計如下:

 

馬蜂窩整體營銷體系分為 B 端和 C 端兩部分。B 端主要面向商家,幫助商家在馬蜂窩招商平台進行大促活動的提報以及商品選取;C 端主要是面向馬蜂窩用戶,平台用戶可以在業務營銷頁面完成活動商品的購買、秒殺、大促紅包贏取等具體的營銷活動參与互動。

2. C 端營銷平台

C 端營銷平台的系統架構主要分為主要分為營銷應用層、中間層、投放平台、搭建平台四個部分。

  • 活動開發平台:營銷平台最核心的部分,也是本文重點。包括前端頁面搭建層「魔方」、業務邏輯層「蜂玩樂園」、獎勵規則控制層「獎池」三部分

  • 投放平台:是指營銷活動頁的投放,包括投放策略、運營策略和機制等

  • 中間件:負責併發處理、分佈式緩存和容災限流等等

  • 營銷應用:包括馬蜂窩大促營銷、業務營銷、新人禮包等

下面,我們重點介紹營銷搭建平台的核心部分——活動開發平台,是如何實現高效、靈活的營銷活動開發模式的。

 

二、活動開發平台的實現

2.1 靈活高效的開發模式

通過上圖可以看到,由 MySQL、ElasticSearch、Redis 組成的數據池在底層為活動開發平台提供支撐。其中 MySQL 為最主要的存儲方案,用於會場搭建配置數據、蜂玩樂園的用戶運行數據、獎池配置數據等的存放。ElasticSearch 是搜索引擎,支持活動頁面商家活動報名與篩選過程。Redis 有 2 種用途:1)活動任務併發鎖;2)獎池的獎品數據存放;3)限流和削峰。

之前我們提到,活動開發的挑戰包括數量多、周期短、功能複雜多變。為了降低開發同學的工作量,提升研發效率,我們將前端和後端組件進行了整合,並封裝成功能模塊對提供服務,形成了目前的魔方、蜂玩樂園、獎池三個子系統,使整體結構更加清晰。每個部分解決的問題和主要功能模塊示意如下:

2.1.1 系統分層

魔方

「魔方」系統希望通過組件、工具的方式完成營銷頁面的搭建,實現統一維護和復用,從而減少前端團隊在活動開發中承載的重複性開發工作。目前為止我們已經在「魔方」中開發了 80 多個組件模塊,例如秒殺模塊、貨架模塊、店鋪模塊、導航模塊、領券模塊、遊戲互動模塊等。

現在,小型活動完全可以不用開發人員支持,只需要業務同學操作即可搭建促銷會場上線活動,提升了活動運營效率,也大大解放了前端開發人員。關於「魔方」更多的細節我們會在後續文章單獨介紹,本文不過多展開。

蜂玩樂園

(1) 邏輯功能抽象

營銷活動的核心是創新和吸引力。每次活動開始前,運營同學都會在創意策劃上絞盡腦汁,盡可能創造出與眾不同的新玩法。這些新穎有趣的遊戲玩法,可以在微信,App 等渠道引起用戶的好奇心和興趣,為賣場拉新,進而創造更多的交易。

隨着「花樣」的不斷翻新,活動開發的複雜度也在增加,有時甚至讓技術同學應接不暇,也促使我們探索更加高效的開發方式。

我們開始思考在複雜多變的活動玩法下,是否潛藏着一些不變的模式和規則?通過對不同業務活動模式的分析和抽象,我們將活動的流程和用戶的行為進行了一個有趣的類比:

  • 首先,開發活動就創建了一個「樂園」

  • 我們會根據不同的「規則」去設計每一個「活動」,激發潛在「參与者」的興趣,或建立他們希望贏得獎勵的期待。

  • 進入活動后,我們會驗證參与者「身份」,和需要滿足這次活動的「條件」,來確定他是否可以開始。

  • 活動開始時,參与者參与一次活動需要發生的行為,就是在完成「任務」

  • 完成「任務」后,為參与者發放相應的「權益」或「獎勵」。

這個類比模型在歷屆促銷活動中進行了推演,結果显示基本是通用的,但完成任務可能伴隨獎勵服務,也可能沒有,由具體業務需求決定。舉個例子,在一場紅包裂變的營銷活動中有一個需求是下紅包雨,用戶可以點擊掉下來的紅包領取相應的紅包獎勵。那麼「領取」這個動作就可以視為活動中的一個任務;另一個需求是每當用戶成功邀請一位好友后就可以在任務中心領取一個邀請紅包獎勵,那麼我們可以把在任務中心領取邀請紅包也看成一個任務。

這兩個任務有一個共同的特點就是觸發后都有紅包獎勵,只是在第二個場景中的任務,本質上是用戶發起了一個請求。

經過進一步的梳理、規整,我們抽象出了「參与者」、「活動」、「任務」、「獎品」等業務邏輯功能。

(2) 技術實現

蜂玩樂園將每一個業務邏輯功能收歸到一個唯一的入口和統一的體系中,形成獨立的功能組件模塊,如數據請求模塊、自定義數據配置模塊、驗證器模塊 、執行器模塊、獎勵服務模塊等。每個活動的任務開發都可以選擇模塊配置,模塊配置信息以 yaml. 的格式進行統一管理,這樣的配置具有靈活性、擴展性和可復用性。

在使用的時候解析配置數據,並向組件註冊中心註冊該任務所需要的組件模塊,再按照定義好的順序執行即可。流程如下圖所示:

為大家介紹幾個關鍵模塊的實現。

  • 數據請求模塊

數據請求模塊定義了客戶端與服務端約定好的請求參數規則:

request:
       -
        field:  deviceId
        rule: required  #必填項校驗
        method: post
        message:  deviceId參數錯誤
       -
        field:  sex
        rule: in:[0,1] #範圍校驗
        method: post
        message: 性別範圍錯誤
       -
        field:  phone
        rule:   regex:/^1[3456789]\d{9}$/ #正則校驗
        method: post
        message: 手機號格式錯誤

(i) field – 傳入參數的 key 
(ii) rule – 校驗該參數的規則,目前我們已經實現了一些常用的規則:
(iii) required – 必傳參數

  • in:驗證所傳參數必須在指定範圍內

  • regex:正則表達式校驗 

  • min,max:自定義規則最小和最大長度

  • integer:必須是数字

  • method:定義 GET、POST 請求方式,

(iv) message – 規則驗證失敗返回的錯誤信息。這一層會讀取配置模塊中的請求參數模塊配置內容,將內容解析出來,按照所配置的字段規則做響應的校驗,如校驗通過繼續向下執行,沒有通過則直接返回規則提示。

  • 參數配置模塊

參數配置模塊定義了該任務執行中所需配置的所有靜態數據配置項。營銷活動的特點是多樣性、創新性,所以很難去窮舉各種場景建立一個有針對性的配置中心,因此這裏就為每一個任務單獨開闢了一個沒有結構化的小空間,可根據具體場景的特定需求為任務自由配置,使程序代碼里基本不用再寫各種不合理的硬編碼。

params:
    stockRedPacket:
     amount: 1
     stock: 3
     stockKey:  limit_key
     stockField: limit_key_90
     timeWindow:
       beginTime: "2019-11-06 00:00:00"
       endTime: "2019-11-10 23:59:

以一個用戶開啟紅包的配置信息為例:

(i) stockRedPacket 配置了活動設定的固定庫存與固定金額紅包的業務邏輯

  • amount 金額

  • stock 庫存

  • stockKey、stockField 用來加鎖的字段

(ii) timeWindow 定義了該任務的活動開始和結束時間

 

  • 驗證器模塊

驗證器模塊的功能主要是是對業務或者規則的校驗。它定義了該任務要執行的業務驗證規則,特點是具有單一性、普適性,能提供一種適用於大多數場景的方法。這些驗證規則可以拆解得足夠細,越細則越靈活,得以在不同任務中靈活組裝使用。

validator:
   - MCommon_Validator_TimeWindowValidator
   - MCommon_Validator_AuthValidator
   - MCommon_Validator_LockValidator

  • 這裏使用了活動時間驗證 TimeWindowValidator,不在活動時間內則返回錯誤提示

  • 登陸驗證 AuthValidator,參加活動必須要登錄,前端通過判斷錯誤狀態碼統一跳轉到登陸頁面

  • 併發鎖 LockValidator,避免一個用戶同樣的操作多次提交

  • 取出所有的驗證器,然後通過反射依次按照順序調用,如果其中一個驗證器失敗,則直接返回錯誤信息,終止向下執行。

 

  • 執行器模塊

執行器模塊定義了該任務要執行的一些業務邏輯,比如要執行一段寫日誌的邏輯,要執行一個異步調用的邏輯等,都可以使用此模塊定義一個執行器去執行。

command: MSign_Command
afterCommand: MSign_Command_After

執行器又分為前置 command 和後置 afterComman:

 

  • 如果需要執行獎勵模塊,則先執行前置 command,再執行獎勵邏輯,最後執行後置 afterCommand,最終返回結果

  • 如果沒有獎勵,則先執行前置 command,接着執行後置 afterCommand

 

  • 獎勵服務模塊

獎勵服務模塊決定該任務是否需要執行獎勵發放,如果配置了獎勵,任務在執行時會根據獎勵的配置規則下發獎勵。在我們的實際場景中,主要涉及到的獎勵類型包括獎勵機會、紅包、抽獎、優惠券等:

  • 獎勵機會:有 2 種規則,分別是按固定頻次給用戶初始化機會數,和獎勵增量機會數。

  • 發送紅包:設定固定紅包和隨機紅包,隨機紅包按需求設置發放的概率與用戶群。

  • 抽獎:對接獎池系統,下文詳細介紹。

  • 優惠券:與馬蜂窩優惠中心直接打通,只需要配置優惠券 SN 和渠道號,即可把優惠券發送到用戶卡券。

 

獎池

在營銷活動中,許多場景都涉及用戶抽獎或獎品發放。營銷技術平台因此對獎品發放的整個生命周期進行了抽象和封裝,創建了「獎池」。

(1) 主要功能

獎池的主要功能點包括:

  • 創建獎品池:為每次活動創建一個或多個獎品池

  • 設置獎品:在單一獎品池中均勻放置獎品

  • 用戶抽獎:用戶在單一獎池中抽獎,支持按概率抽獎,支持獎品的發放和領取

  • 中獎統計:包括獎品已發放數量,已領取數量,剩餘數量

如下圖所示,只需創建好獎池,配置好獎品信息,把對應的獎池 ID 填寫到任務,即可實現抽獎功能:

(2) 方案設計

獎池早期的設計非常簡單,獎品實體僅定義「余量」的概念,利用關係型數據庫中單行記錄存儲一次活動的獎品、總量、發放量、余量等數據。在用戶流量較小且均勻的情況下,發放過程平穩正常。每次進行獎品發放時,在單行記錄上進行 update 操作,扣減余量並增加發放量即可。

然而隨着公司業務的發展,一次營銷活動帶來的效果讓我們不得不立刻改進獎池方案。這次營銷活動帶來的流量遠超預期,但獎品數量的配置卻一如往常。當活動開啟后,獎品消耗很快,並在一段時間后被提前抽光。為了不影響用戶體驗,營銷運營同學不得不持續向獎池中補充獎品。

經歷這次問題開發同學發現,獎池提前抽光的原因在於設計中忽略了時間分佈的因素,使獎品抽光的速度只與訪問量相關。因此,大家開始思考如何讓獎品固定且均勻分佈在活動周期內。

通過學習與比較,最終選擇了業界比較通用的方案,使用 Redis 的有序集合(Sorted Set)創建獎池和設置獎品,從而使獎品在活動時間段內均勻分佈,防止提前抽光的情況出現。

(3) 實現算法

 

1. 時間戳:根據獎品的數量和活動時長,為每 1 份獎品設置一個出獎時間戳,這份獎品僅能在這一時間點及之後被抽出。這一步使出獎時間戳盡量均勻分佈在活動時間範圍內。

2. 創建獎品池:為每一組獎品設置一個獎池,在 Redis 創建一個 zset 數據結構,將其中的每 1 份獎品作為 1 個成員(Member),將時間戳作為分值(score)。
3. 放置獎品:使用ZADD 獎池 出獎時間戳 1 份獎品 語法,在 Redis 中布置一個獎品。

4. 抽獎:使用 Sorted Set 的排序方法,每次排序后查看排名第一的獎品,比較當前時間戳與獎品時間戳的大小。如果當前時間晚於或等於出獎時間,則使用 ZREM 指令出獎,否則不出。

示意圖如下:

2.1.2  體系統一

為了讓開發同學只專註於任務的設計開發,我們抽象出「賬戶」的概念,每個任務產生的數據資源會存儲在所在的「賬戶」體系下,使其支撐多個類似的活動。這種設計的好處在於:

(1)同一用戶在參与不同的活動時得到的獎勵都是相互獨立的,不會出現混淆的情況。

(2)之前每次活動都需要單獨創建數據表,活動下線后表不能復用。時間長了造成系統佔用許多無用的數據表。而把數據庫表以抽象的任務形態創建,不針對具體的某一業務類型,就可以使數據表實現復用。這樣我們只專註任務的設計開發,不用再關心數據表的設計。

在營銷大促的活動中,我們也接入了風控中心、併發鎖和限流服務,以保障整個活動的安全和穩定。

2.2 可用性和可靠性

秒殺模塊是大促流量的最高峰。結合業務實際,我們針對這種場景也做了限流和削峰處理。

限流採用的方案是限制時間窗內最大請求數據,用戶再搶會員權益時,第一步會讀取限流配置 key 和 value,判斷單位時間內是否超過限制的最大請求數 value,如果超過則返回信息提示結束請求;如果沒有超過閾值,則進入下一步操作。目前的限流系統只是在應用層面的實現,為了更好地支撐業務發展,後續我們也會接入網關服務,通過 Sentinel 和 Hystrix 做限流熔斷,避免流量進入應用層,提高服務的高可用性。

削峰部分結合實例說明。

以秒殺金卡會員的場景為例,我們會先用 RabbitMQ 承接瞬時的流量洪峰,再平滑地消費所有請求,並提前把庫存數量對應的 Token 寫入 Redis 緩存(這裏我們針對性的對不同的用戶引入了 Token 機制,以保證我們的秒殺商品在時效和庫存上得以保障)。用戶在秒殺時會順序地從 Redis 中 rPop 對應的 Token,避免庫存超賣的情況;用戶拿到 Token 之後再去收銀台下單開通金卡會員,就可以避免流量同一時刻去下單。

隨着業務和技術的發展,系統的不確定性以及流量的預估都更為困難。我們也在不斷學習業界的先進經驗,來更好地提升系統的可用性和可靠性。目前我們正在調研基於 Noah 的「自適應」限流技術並积極推進,以期針對不同的服務器性能以及當前的流量情況進行針對性的限流控制,相信我們會在後續的優化中會做得更好。

2.3 風險控制

目前是接入公司統一的風控中心。在營銷活動需求確定好后,我們會向風控服務中心提供需要風控的業務場景邏輯。風控中心根據業務配置不同策略,給出不同的場景 key。我們只需要在營銷活動任務中的自定義參數配置模塊配置好風控場景 key,就可在獎勵服務模塊自動調用風控接口去校驗用戶,如果識別出是風險用戶則會被攔截,終止活動參与。

可用性和可靠性、風險控制的實現流程如下圖所示:

 

三、近期規劃

1. 完善監控體系

目前對於活動運行中的數據監控,主要依賴數據組的統計與輸出。線上活動的運行情況並不能通過「蜂玩樂園」與「獎池」系統實時並綜合表現出來。

未來會補齊運行時的活動監控功能,通過活動、任務、獎品的運行時數據指標,指導運營同學第一時間調整活動參數,取得最佳運營效果。

2. 服務化改造

營銷基礎平台依舊搭建在單體架構上,部分功能的邊界與職責並不完全清晰。接下來營銷技術平台會進行技術架構的升級與改造,從單體架構轉向微服務架構,使服務能力與開發能效同步提升。

小結

隨着營銷的逐年發展,活動的趣味性和複雜度會一起上升,這需要我們不斷更新對營銷活動的認識。在這過程中也要反覆嘗試新的抽象和重構,通過不斷改進現有系統,支持更多和更好玩的營銷活動,讓馬蜂窩用戶玩兒得更省心,玩兒得更省錢。

本文作者:馬蜂窩電商研發營銷中心團隊劉遠勻、任浩、唐溶波。

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

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

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

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

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

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

go中的數據結構-字典map

1. map的使用

  golang中的map是一種數據類型,將鍵與值綁定到一起,底層是用哈希表實現的,可以快速的通過鍵找到對應的值。

  類型表示:map[keyType][valueType] key一定要是可比較的類型(可以理解為支持==的操作),value可以是任意類型。

  初始化:map只能使用make來初始化,聲明的時候默認為一個為nil的map,此時進行取值,返回的是對應類型的零值(不存在也是返回零值)。添加元素無任何意義,還會導致運行時錯誤。向未初始化的map賦值引起 panic: assign to entry in nil map。

 1 package main
 2 
 3 import (  
 4     "fmt"
 5 )
 6 
 7 // bool 的零值是false
 8 var m map[int]bool 
 9 a, ok := m[1]
10 fmt.Println(a, ok) // false  false
11 
12 // int 的零值是0
13 var m map[int]int 
14 a, ok := m[1]
15 fmt.Println(a, ok) // 0  false
16 
17 
18 func main() {  
19     var agemap[string]int
20     if age== nil {
21         fmt.Println("map is nil.")
22         age= make(map[string]int)
23     }
24 }

  清空map:對於一個有一定數據的集合 exp,清空的辦法就是再次初始化: exp = make(map[string]int),如果後期不再使用該map,則可以直接:exp= nil 即可,但是如果還需要重複使用,則必須進行make初始化,否則無法為nil的map添加任何內容。

  屬性:與切片一樣,map 是引用類型。當一個 map 賦值給一個新的變量,它們都指向同一個內部數據結構。因此改變其中一個也會反映到另一個。作為形參或返回參數的時候,傳遞的是地址的拷貝,擴容時也不會改變這個地址。

 1 func main() {
 2     exp := map[string]int{
 3         "steve": 20,
 4         "jamie": 80,
 5     }
 6     fmt.Println("Ori exp", age)
 7     newexp:= exp
 8     newexp["steve"] = 18
 9     fmt.Println("exp changed", exp)
10 }
11 
12 //Ori age map[steve:20 jamie:80]
13 //age changed map[steve:18 jamie:80]

  遍歷map:map本身是無序的,在遍歷的時候並不會按照你傳入的順序,進行傳出。

 1 //正常遍歷:
 2 for k, v := range exp { 
 3     fmt.Println(k, v)
 4 }
 5 
 6 //有序遍歷
 7 import "sort"
 8 var keys []string
 9 // 把key單獨抽取出來,放在數組中
10 for k, _ := range exp {
11     keys = append(keys, k)
12 }
13 // 進行數組的排序
14 sort.Strings(keys)
15 // 遍曆數組就是有序的了
16 for _, k := range keys {
17     fmt.Println(k, m[k])
18 }

2. map的結構

   Go中的map在可以在 $GOROOT/src/runtime/map.go找到它的實現。哈希表的數據結構中一些關鍵的域如下所示:

 1 type hmap struct {
 2     count        int  //元素個數
 3     flags        uint8   
 4     B            uint8 //擴容常量
 5     noverflow    uint16 //溢出 bucket 個數
 6     hash0        uint32 //hash 種子
 7     buckets      unsafe.Pointer //bucket 數組指針
 8     oldbuckets   unsafe.Pointer //擴容時舊的buckets 數組指針
 9     nevacuate    uintptr  //擴容搬遷進度
10     extra        *mapextra //記錄溢出相關
11 }
12 
13 type bmap struct {
14     tophash        [bucketCnt]uint8  
15     // Followed by bucketCnt keys 
16     //and then bucketan Cnt values  
17     // Followed by overflow pointer.
18 } 

  說明:每個map的底層都是hmap結構體,它是由若干個描述hmap結構體的元素、數組指針、extra等組成,buckets數組指針指向由若干個bucket組成的數組,其每個bucket里存放的是key-value數據(通常是8個)和overflow字段(指向下一個bmap),每個key插入時會根據hash算法歸到同一個bucket中,當一個bucket中的元素超過8個的時候,hmap會使用extra中的overflow來擴展存儲key。

  圖中len 就是當前map的元素個數,也就是len()返回的值。也是結構體中hmap.count的值。bucket array是指數組指針,指向bucket數組。hash seed 哈希種子。overflow指向下一個bucket。

map的底層主要是由三個結構構成:

  1. hmap — map的最外層的數據結構,包括了map的各種基礎信息、如大小、bucket,一個大的結構體。
  2. mapextra — 記錄map的額外信息,hmap結構體里的extra指針指向的結構,例如overflow bucket
  3. bmap — 代表bucket,每一個bucket最多放8個kv,最後由一個overflow字段指向下一個bmap,注意key、value、overflow字段都不显示定義,而是通過maptype計算偏移獲取的。

  mapextra的結構如下

 1 // mapextra holds fields that are not present on all maps.
 2 type mapextra struct {
 3     // If both key and value do not contain pointers and are inline, then we mark bucket
 4     // type as containing no pointers. This avoids scanning such maps.
 5     // However, bmap.overflow is a pointer. In order to keep overflow buckets
 6     // alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
 7     // overflow and oldoverflow are only used if key and value do not contain pointers.
 8     // overflow contains overflow buckets for hmap.buckets.
 9     // oldoverflow contains overflow buckets for hmap.oldbuckets.
10     // The indirection allows to store a pointer to the slice in hiter.
11     overflow    *[]*bmap
12     oldoverflow *[]*bmap
13 
14     // nextOverflow holds a pointer to a free overflow bucket.
15     nextOverflow *bmap
16 }

  其中hmap.extra.nextOverflow指向的是預分配的overflow bucket,預分配的用完了那麼值就變成nil。

  bmap的詳細結構如下

  在map中出現哈希衝突時,首先
以bmap為最小粒度掛載,一個bmap累積8個kv之後,就會申請一個新的bmap(overflow bucket)掛在這個bmap的後面形成鏈表,優先用預分配的overflow bucket,如果預分配的用完了,那麼就malloc一個掛上去。這樣減少對象數量,減輕管理內存的負擔,利於gc。
注意golang的map不會shrink,內存只會越用越多,overflow bucket中的key全刪了也不會釋放。

  bmap中所有key存在一塊,所有value存在一塊,這樣做方便內存對齊。當key大於128字節時,bucket的key字段存儲的會是指針,指向key的實際內容;value也是一樣。

  hash值的高8位存儲在bucket中的tophash字段。每個桶最多放8個kv對,所以tophash類型是數組[8]uint8。把高八位存儲起來,這樣不用完整比較key就能過濾掉不符合的key,加快查詢速度。實際上當hash值的高八位小於常量minTopHash時,會加上minTopHash,區間[0, minTophash)的值用於特殊標記。查找key時,計算hash值,用hash值的高八位在tophash中查找,有tophash相等的,再去比較key值是否相同。

 1 type typeAlg struct {
 2     // function for hashing objects of this type
 3     // (ptr to object, seed) -> hash
 4     hash func(unsafe.Pointer, uintptr) uintptr
 5     // function for comparing objects of this type
 6     // (ptr to object A, ptr to object B) -> ==?
 7     equal func(unsafe.Pointer, unsafe.Pointer) bool
 8 
 9 // tophash calculates the tophash value for hash.
10 func tophash(hash uintptr) uint8 {
11     top := uint8(hash >> (sys.PtrSize*8 - 8))
12     if top < minTopHash {
13         top += minTopHash
14     }
15     return top
16 }

  golang為每個類型定義了類型描述器_type,並實現了hashable類型的_type.alg.hash和_type.alg.equal,以支持map的范型,定義了這類key用什麼hash函數、bucket的大小、怎麼比較之類的,通過這個變量來實現范型。

3. map的基本操作

3.1 map的創建

 1 //makemap為make(map [k] v,hint)實現Go map創建。
 2 //如果編譯器已確定映射或第一個存儲桶,可以在堆棧上創建,hmap或bucket可以為非nil。
 3 //如果h!= nil,則可以直接在h中創建map。
 4 //如果h.buckets!= nil,則指向的存儲桶可以用作第一個存儲桶。
 5 func makemap(t *maptype, hint int, h *hmap) *hmap {
 6     if hint < 0 || hint > int(maxSliceCap(t.bucket.size)) {
 7         hint = 0
 8     }
 9 
10     // 初始化Hmap
11     if h == nil {
12         h = new(hmap)
13     }
14     h.hash0 = fastrand()
15 
16     // 查找將保存請求的元素數的size參數
17     B := uint8(0)
18     for overLoadFactor(hint, B) {
19         B++
20     }
21     h.B = B
22 
23     // 分配初始哈希表
24     // if B == 0, 稍後會延遲分配buckets字段(在mapassign中)
25     //如果提示很大,則將內存清零可能需要一段時間。
26     if h.B != 0 {
27         var nextOverflow *bmap
28         h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
29         if nextOverflow != nil {
30             h.extra = new(mapextra)
31             h.extra.nextOverflow = nextOverflow
32         }
33     }
34 
35     return h
36 }

  hint是一個啟發值,啟發初建map時創建多少個bucket,如果hint是0那麼就先不分配bucket,lazy分配。大概流程就是初始化hmap結構體、設置一下hash seed、bucket數量、實際申請bucket、申請mapextra結構體之類的。   申請buckets的過程:

 1 // makeBucketArray初始化地圖存儲區的後備數組。
 2 // 1 << b是要分配的最小存儲桶數。
 3 // dirtyalloc之前應該為nil或bucket數組
 4 //由makeBucketArray使用相同的t和b參數分配。
 5 //如果dirtyalloc為零,則將分配一個新的支持數組,dirtyalloc將被清除並作為後備數組重用。
 6 func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
 7     base := bucketShift(b)
 8     nbuckets := base
 9     // 對於小b,溢出桶不太可能出現。
10     // 避免計算的開銷。
11     if b >= 4 {
12         //加上估計的溢出桶數
13         //插入元素的中位數
14         //與此值b一起使用。
15         nbuckets += bucketShift(b - 4)
16         sz := t.bucket.size * nbuckets
17         up := roundupsize(sz)
18         if up != sz {
19             nbuckets = up / t.bucket.size
20         }
21     }
22     if dirtyalloc == nil {
23         buckets = newarray(t.bucket, int(nbuckets))
24     } else {
25        // dirtyalloc先前是由上面的newarray(t.bucket,int(nbuckets)),但不能為空。
26         buckets = dirtyalloc
27         size := t.bucket.size * nbuckets
28         if t.bucket.kind&kindNoPointers == 0 {
29             memclrHasPointers(buckets, size)
30         } else {
31             memclrNoHeapPointers(buckets, size)
32         }
33     }
34 
35     if base != nbuckets {
36         //我們預先分配了一些溢出桶。
37         //為了將跟蹤這些溢出桶的開銷降至最低,我們使用的約定是,如果預分配的溢出存儲桶發生了溢出指針為零,則通過碰撞指針還有更多可用空間。
38         //對於最後一個溢出存儲區,我們需要一個安全的非nil指針;只是用bucket。
39         nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
40         last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
41         last.setoverflow(t, (*bmap)(buckets))
42     }
43     return buckets, nextOverflow
44 }

  默認創建2
b個bucket,如果
b大於等於4,那麼就預先額外創建一些overflow bucket。除了最後一個overflow bucket,其餘overflow bucket的overflow指針都是nil,最後一個overflow bucket的overflow指針指向bucket數組第一個元素,作為哨兵,說明到了到結尾了。

3.2 查詢操作

 1 // mapaccess1返回指向h [key]的指針。從不返回nil,而是 如果值類型為零,它將返回對零對象的引用,該鍵不在map中。
 2   //注意:返回的指針可能會使整個map保持活動狀態,因此請不要堅持很長時間。
 3   func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
 4       if raceenabled && h != nil {  //raceenabled是否啟用數據競爭檢測。
 5         callerpc := getcallerpc()
 6         pc := funcPC(mapaccess1)
 7         racereadpc(unsafe.Pointer(h), callerpc, pc)
 8         raceReadObjectPC(t.key, key, callerpc, pc)
 9     }
10     if msanenabled && h != nil {
11         msanread(key, t.key.size)
12     }
13     if h == nil || h.count == 0 {
14         return unsafe.Pointer(&zeroVal[0])
15     }    
16     // 併發訪問檢查
17     if h.flags&hashWriting != 0 {
18         throw("concurrent map read and map write")
19     }
20     
21     // 計算key的hash值
22     alg := t.key.alg
23     hash := alg.hash(key, uintptr(h.hash0)) // alg.hash
24 
25     // hash值對m取餘數得到對應的bucket
26     m := uintptr(1)<<h.B - 1
27     b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
28 
29     // 如果老的bucket還沒有遷移,則在老的bucket裏面找
30     if c := h.oldbuckets; c != nil {
31         if !h.sameSizeGrow() {
32             m >>= 1
33         }
34         oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
35         if !evacuated(oldb) {
36             b = oldb
37         }
38     }
39     
40     // 計算tophash,取高8位
41     top := uint8(hash >> (sys.PtrSize*8 - 8))
42     
43     for {
44         for i := uintptr(0); i < bucketCnt; i++ {
45             // 檢查top值,如高8位不一樣就找下一個
46             if b.tophash[i] != top {
47                 continue
48             }
49             
50             // 取key的地址
51             k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
52             
53             if alg.equal(key, k) { // alg.equal
54                 // 取value得地址
55                 v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
56             }
57         }
58        
59         // 如果當前bucket沒有找到,則找bucket鏈的下一個bucket
60         b = b.overflow(t)
61         if b == nil {
62             // 返回零值
63             return unsafe.Pointer(&zeroVal[0])
64         }
65     }
66 }
  1. 先定位出bucket,如果正在擴容,並且這個bucket還沒搬到新的hash表中,那麼就從老的hash表中查找。

  2. 在bucket中進行順序查找,使用高八位進行快速過濾,高八位相等,再比較key是否相等,找到就返回value。如果當前bucket找不到,就往下找overflow bucket,都沒有就返回零值。

  訪問的時候,並不進行擴容的數據搬遷。並且併發有寫操作時拋異常

  注意,t.bucketsize並不是bmap的size,而是bmap加上存儲key、value、overflow指針,所以查找bucket的時候時候用的不是bmap的szie。

3.3 更新/插入過程

 1 // 與mapaccess類似,但是如果map中不存在密鑰,則為該密鑰分配一個插槽
 2 func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
 3     ...
 4     //設置hashWriting調用alg.hash,因為alg.hash可能出現緊急情況后,在這種情況下,我們實際上並沒有進行寫操作.
 5     h.flags |= hashWriting
 6 
 7     if h.buckets == nil {
 8         h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
 9     }
10 
11 again:
12     bucket := hash & bucketMask(h.B)
13     if h.growing() {
14         growWork(t, h, bucket)
15     }
16     b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
17     top := tophash(hash)
18 
19     var inserti *uint8
20     var insertk unsafe.Pointer
21     var val unsafe.Pointer
22     for {
23         for i := uintptr(0); i < bucketCnt; i++ {
24             if b.tophash[i] != top {
25                 if b.tophash[i] == empty && inserti == nil {
26                     inserti = &b.tophash[i]
27                     insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
28                     val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
29                 }
30                 continue
31             }
32             k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
33             if t.indirectkey {
34                 k = *((*unsafe.Pointer)(k))
35             }
36             if !alg.equal(key, k) {
37                 continue
38             }
39             // 已經有一個 mapping for key. 更新它.
40             if t.needkeyupdate {
41                 typedmemmove(t.key, k, key)
42             }
43             val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
44             goto done
45         }
46         ovf := b.overflow(t)
47         if ovf == nil {
48             break
49         }
50         b = ovf
51     }
52     //// 如果已經達到了load factor的最大值,就繼續擴容。
53     //找不到鍵的映射。分配新單元格並添加條目。
54     //如果達到最大負載係數或溢出桶過多,並且我們還沒有處於成長的中間,就開始擴容。
55     if !h.growing() && (overLoadFactor(h.count+1, h.B) ||     
56         tooManyOverflowBuckets(h.noverflow, h.B)) {
57         hashGrow(t, h)
58         goto again // //擴大表格會使所有內容無效, so try again
59     }
60     if inserti == nil {
61         // 當前所有存儲桶已滿,請分配一個新的存儲桶
62         newb := h.newoverflow(t, b)
63         inserti = &newb.tophash[0]
64         insertk = add(unsafe.Pointer(newb), dataOffset)
65         val = add(insertk, bucketCnt*uintptr(t.keysize))
66     }
67 
68     // 在插入的位置,存儲鍵值
69     if t.indirectkey {
70         kmem := newobject(t.key)
71         *(*unsafe.Pointer)(insertk) = kmem
72         insertk = kmem
73     }
74     if t.indirectvalue {
75         vmem := newobject(t.elem)
76         *(*unsafe.Pointer)(val) = vmem
77     }
78     typedmemmove(t.key, insertk, key)
79     *inserti = top
80     h.count++
81 
82 done:
83     if h.flags&hashWriting == 0 {
84         throw("concurrent map writes")
85     }
86     h.flags &^= hashWriting
87     if t.indirectvalue {
88         val = *((*unsafe.Pointer)(val))
89     }
90     return val
91 }    
  • hash表如果正在擴容,並且這次要操作的bucket還沒搬到新hash表中,那麼先進行搬遷(擴容細節下面細說)。

  • 在buck中尋找key,同時記錄下第一個空位置,如果找不到,那麼就在空位置中插入數據;如果找到了,那麼就更新對應的value;

  • 找不到key就看下需不需要擴容,需要擴容並且沒有正在擴容,那麼就進行擴容,然後回到第一步。

  • 找不到key,不需要擴容,但是沒有空slot,那麼就分配一個overflow bucket掛在鏈表結尾,用新bucket的第一個slot放存放數據。

3.5 刪除的過程

 1 func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
 2     ...
 3     // Set hashWriting after calling alg.hash, since alg.hash may panic,
 4     // in which case we have not actually done a write (delete).
 5     h.flags |= hashWriting
 6 
 7     bucket := hash & bucketMask(h.B)
 8     if h.growing() {
 9         growWork(t, h, bucket)
10     }
11     b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
12     top := tophash(hash)
13 search:
14     for ; b != nil; b = b.overflow(t) {
15         for i := uintptr(0); i < bucketCnt; i++ {
16             if b.tophash[i] != top {
17                 continue
18             }
19             k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
20             k2 := k
21             if t.indirectkey {
22                 k2 = *((*unsafe.Pointer)(k2))
23             }
24             if !alg.equal(key, k2) {
25                 continue
26             }
27             // 如果其中有指針,則僅清除鍵。
28             if t.indirectkey {
29                 *(*unsafe.Pointer)(k) = nil
30             } else if t.key.kind&kindNoPointers == 0 {
31                 memclrHasPointers(k, t.key.size)
32             }
33             v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
34             if t.indirectvalue {
35                 *(*unsafe.Pointer)(v) = nil
36             } else if t.elem.kind&kindNoPointers == 0 {
37                 memclrHasPointers(v, t.elem.size)
38             } else {
39                 memclrNoHeapPointers(v, t.elem.size)
40             }
41         // 若找到把對應的tophash裏面的打上空的標記
42             b.tophash[i] = empty
43             h.count--
44             break search
45         }
46     }
47 
48     if h.flags&hashWriting == 0 {
49         throw("concurrent map writes")
50     }
51     h.flags &^= hashWriting
52 }    
  1. 如果正在擴容,並且操作的bucket還沒搬遷完,那麼搬遷bucket。

  2. 找出對應的key,如果key、value是包含指針的那麼會清理指針指向的內存,否則不會回收內存。

3.6 map的擴容

  通過上面的過程我們知道了,插入、刪除過程都會觸發擴容,判斷擴容的函數如下:

 1 // overLoadFactor 判斷放置在1 << B個存儲桶中的計數項目是否超過loadFactor。
 2 func overLoadFactor(count int, B uint8) bool {
 3     return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)  
 4     //return 元素個數>8 && count>bucket數量*6.5,其中loadFactorNum是常量13,loadFactorDen是常量2,所以是6.5,bucket數量不算overflow bucket.
 5 }
 6 
 7 // tooManyOverflowBuckets 判斷noverflow存儲桶對於1 << B存儲桶的map是否過多。
 8 // 請注意,大多數這些溢出桶必須稀疏使用。如果使用密集,則我們已經觸發了常規map擴容。
 9 func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
10     // 如果閾值太低,我們會做多餘的工作。如果閾值太高,則增大和縮小的映射可能會保留大量未使用的內存。
11     //“太多”意味着(大約)溢出桶與常規桶一樣多。有關更多詳細信息,請參見incrnoverflow。
12     if B > 15 {
13         B = 15
14     }
15     // 譯器在這裏看不到B <16;掩碼B生成較短的移位碼。
16     return noverflow >= uint16(1)<<(B&15)
17 }
18 
19 {
20     ....
21     // 如果我們達到最大負載率或溢流桶過多,並且我們還沒有處於成長的中間,就開始成長。
22     if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
23         hashGrow(t, h)
24         goto again // 擴大表格會使所有內容失效,so try again
25     }
26     //if (不是正在擴容 && (元素個數/bucket數超過某個值 || 太多overflow bucket)) {
27     進行擴容
28     //}
29     ....
30 }

  每次map進行更新或者新增的時候,會先通過以上函數判斷一下load factor。來決定是否擴容。如果需要擴容,那麼第一步需要做的,就是對hash表進行擴容:

 1 //僅對hash表進行擴容,這裏不進行搬遷
 2 func hashGrow(t *maptype, h *hmap) {
 3     // 如果達到負載係數,則增大尺寸。否則,溢出bucket過多,因此,保持相同數量的存儲桶並橫向“增長”。
 4     bigger := uint8(1)
 5     if !overLoadFactor(h.count+1, h.B) {
 6         bigger = 0
 7         h.flags |= sameSizeGrow
 8     }
 9     oldbuckets := h.buckets
10     newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
11 
12     flags := h.flags &^ (iterator | oldIterator)
13     if h.flags&iterator != 0 {
14         flags |= oldIterator
15     }
16     // 提交增長(atomic wrt gc)
17     h.B += bigger
18     h.flags = flags
19     h.oldbuckets = oldbuckets
20     h.buckets = newbuckets
21     h.nevacuate = 0
22     h.noverflow = 0
23 
24     if h.extra != nil && h.extra.overflow != nil {
25         // 將當前的溢出bucket提升到老一代。
26         if h.extra.oldoverflow != nil {
27             throw("oldoverflow is not nil")
28         }
29         h.extra.oldoverflow = h.extra.overflow
30         h.extra.overflow = nil
31     }
32     if nextOverflow != nil {
33         if h.extra == nil {
34             h.extra = new(mapextra)
35         }
36         h.extra.nextOverflow = nextOverflow
37     }
38 
39     //哈希表數據的實際複製是增量完成的,通過growWork()和evacuate()。
40 }

  如果之前為2^n ,那麼下一次擴容是2^(n+1),每次擴容都是之前的兩倍。擴容后需要重新計算每一項在hash中的位置,新表為老的兩倍,此時前文的oldbacket用上了,用來存同時存在的兩個新舊map,等數據遷移完畢就可以釋放oldbacket了。擴容的函數hashGrow其實僅僅是進行一些空間分配,字段的初始化,實際的搬遷操作是在growWork函數中:

1 func growWork(t *maptype, h *hmap, bucket uintptr) {
2     //確保我們遷移了了對應的oldbucket,到我們將要使用的存儲桶。
3     evacuate(t, h, bucket&h.oldbucketmask())
4 
5     // 疏散一箇舊桶以在生長上取得進展
6     if h.growing() {
7         evacuate(t, h, h.nevacuate)
8     }
9 }

  evacuate是進行具體搬遷某個bucket的函數,可以看出
growWork會搬遷兩個bucket,一個是入參bucket;另一個是h.nevacuate。這個nevacuate是一個順序累加的值。可以想想如果每次僅僅搬遷進行寫操作(賦值/刪除)的bucket,那麼有可能某些bucket就是一直沒有機會訪問到,那麼擴容就一直沒法完成,總是在擴容中的狀態,因此會額外進行一次順序遷移,理論上,有N個old bucket,最多N次寫操作,那麼必定會搬遷完。在advanceEvacuationMark中進行nevacuate的累加,遇到已經遷移的bucket會繼續累加,一次最多加1024。

  優點:均攤擴容時間,一定程度上縮短了擴容時間(和gc的引用計數法類似,都是均攤)overLoadFactor函數中有一個常量6.5(loadFactorNum/loadFactorDen)來進行影響擴容時機。這個值的來源是測試取中的結果。

4. map的併發安全性

  map的併發操作不是安全的。併發起兩個goroutine,分別對map進行數據的增加:

 1 func main() {
 2     test := map[int]int {1:1}
 3     go func() {
 4         i := 0
 5         for i < 10000 {
 6             test[1]=1
 7             i++
 8         }
 9     }()
10 
11     go func() {
12         i := 0
13         for i < 10000 {
14             test[1]=1
15             i++
16         }
17     }()
18 
19     time.Sleep(2*time.Second)
20     fmt.Println(test)
21 }
22 
23 //fatal error: concurrent map read and map write

  併發讀寫map結構的數據引起了錯誤。

  解決方案1:加鎖

 1 func main() {
 2     test := map[int]int {1:1}
 3     var s sync.RWMutex
 4     go func() {
 5         i := 0
 6         for i < 10000 {
 7             s.Lock()
 8             test[1]=1
 9             s.Unlock()
10             i++
11         }
12     }()
13 
14     go func() {
15         i := 0
16         for i < 10000 {
17             s.Lock()
18             test[1]=1
19             s.Unlock()
20             i++
21         }
22     }()
23 
24     time.Sleep(2*time.Second)
25     fmt.Println(test)
26 }

  特點:實現簡單粗暴,好理解。但是鎖的粒度為整個map,存在優化空間。適用場景:all。

  解決方案2:sync.Map

 1 func main() {
 2     test := sync.Map{}
 3     test.Store(1, 1)
 4     go func() {
 5         i := 0
 6         for i < 10000 {
 7             test.Store(1, 1)
 8             i++
 9         }
10     }()
11 
12     go func() {
13         i := 0
14         for i < 10000 {
15             test.Store(1, 1)
16             i++
17         }
18     }()
19 
20     time.Sleep(time.Second)
21     fmt.Println(test.Load(1))
22 }

  sync.Map的原理:sync.Map裡頭有兩個map一個是專門用於讀的read map,另一個是才是提供讀寫的dirty map;優先讀read map,若不存在則加鎖穿透讀dirty map,同時記錄一個未從read map讀到的計數,當計數到達一定值,就將read map用dirty map進行覆蓋。
特點:官方出品,通過空間換時間的方式,讀寫分離;不適用於大量寫的場景,會導致read map讀不到數據而進一步加鎖讀取,同時dirty map也會一直晉陞為read map,整體性能較差。適用場景:大量讀,少量寫。

  解決方案3:分段鎖

  這也是數據庫常用的方法,分段鎖每一個讀寫鎖保護一段區間。sync.Map其實也是相當於表級鎖,只不過多讀寫分了兩個map,本質還是一樣的。

  優化方向:將鎖的粒度盡可能降低來提高運行速度。思路:對一個大map進行hash,其內部是n個小map,根據key來來hash確定在具體的那個小map中,這樣加鎖的粒度就變成1/n了。

5. map的GC內存回收

  golang里的map是只增不減的一種數組結構,他只會在刪除的時候進行打標記說明該內存空間已經empty了,不會回收。

 1 var intMap map[int]int
 2 
 3 func main() {
 4     printMemStats("初始化")
 5 
 6     // 添加1w個map值
 7     intMap = make(map[int]int, 10000)
 8     for i := 0; i < 10000; i++ {
 9         intMap[i] = i
10     }
11 
12     // 手動進行gc操作
13     runtime.GC()
14     // 再次查看數據
15     printMemStats("增加map數據后")
16 
17     log.Println("刪除前數組長度:", len(intMap))
18     for i := 0; i < 10000; i++ {
19         delete(intMap, i)
20     }
21     log.Println("刪除后數組長度:", len(intMap))
22 
23     // 再次進行手動GC回收
24     runtime.GC()
25     printMemStats("刪除map數據后")
26 
27     // 設置為nil進行回收
28     intMap = nil
29     runtime.GC()
30     printMemStats("設置為nil后")
31 }
32 
33 func printMemStats(mag string) {
34     var m runtime.MemStats
35     runtime.ReadMemStats(&m)
36     log.Printf("%v:分配的內存 = %vKB, GC的次數 = %v\n", mag, m.Alloc/1024, m.NumGC)
37 }
38 
39 //初始化:分配的內存 = 65KB, GC的次數 = 0
40 //增加map數據后:分配的內存 = 381KB, GC的次數 = 1
41 //刪除前數組長度: 10000
42 //刪除后數組長度: 0
43 //刪除map數據后:分配的內存 = 381KB, GC的次數 = 2
44 //設置為nil后:分配的內存 = 68KB, GC的次數 = 3

  可以看到delete是不會真正的把map釋放的,所以要回收map還是需要設為nil

sync.Map的原理詳解: 

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

【其他文章推薦】

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

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

堆和棧的區別

堆與棧

關於堆和棧的問題,對於熟悉C++的同學來說可能理解的比較清楚,但是由於Java的一切對象都是在堆上,因此有時候可能反而會比較迷惑,比如:

為什麼要分堆和棧?

堆和棧的區別是什麼?

為什麼堆是線程共享的而棧不是?

很多懂一點Java的人甚至是懂一點編程的人都知道內存一般分為堆和棧,棧由系統進行關係,而堆由程序員自己管理,…balabala,基本任何一本語言基礎書都會提到這些特點,但是你有想過為什麼嗎?

為什麼要分堆和棧

之所以要區分堆和棧,是由於程序需要兩種不同特性的內存形似而確定的。在C++中,新建一個對象有兩種方式,靜態分配動態分配

靜態分配

一般來說,靜態分配用於初始化已知對象大小的時候,比如int a[10];如果我們能夠確定這個數組是10個,我們可以使用這種方式。這種方式所需要的內存在編譯期間即可確定,因此操作系統便可以預先確定所指定大小內存給變量,並且可以在變量生命周期結束后自動釋放。

動態分配

然而在某些場景下,可能需要根據某些情況來申請內存,比如int* a =new int[count];而變量count可能就來自於某個配置文件或者用戶手動輸入的值。這個時候操作系統就無法再預先確定內存大小,並且不執行到new int[count]這一行代碼的時候,是無法知道所要分配的內存大小,因此操作系統分出一塊內存,用來進行動態分配。並且規定,動態分配的內存需要由客戶端自行管理。

Java 中的堆

由於JVM規範中規定,JVM中的一切對象都存儲在堆上(內存逃逸除外)。因此在Java中並不存在對象的靜態分配,因此堆和棧的來源看似就非常理所當然。但是要明白,在操作系統中,堆和棧的出現的緣由。

堆和棧的區別

知道了為什麼要區分堆和棧,再來看看堆和棧的區別。

  • 堆是運行時確定內存大小,而棧在編譯時即可確定內存大小

    理由便是第一節中提到的,這是區分堆和棧的初衷

  • 堆內存由用戶管理(Java中由JVM管理),棧內存會被自動釋放

  • 棧實現方式採用數據結構中的棧實現,具有(LIFO)的順序特點,堆為一塊一塊的內存

  • 棧由於其實現方式,在分配速度上比堆快的多。分配一塊棧內存不過是簡單的移動一個指針

  • 在JVM中,棧不會被程序員直接使用,程序員操作的一般都是堆。

  • 棧為線程私有而堆為線程共享

雖然堆和棧有這麼多的區別,但是這些區別都是由於操作系統而決定的,在硬件上,他們本質都是RAM

為什麼堆是線程共享的而棧不是?

上面最後一點提到了棧為線程私有而堆為線程共享。這是為什麼呢???

其實很簡單,為了解決一個問題:線程間通信。

想要實現線程間通信,目前有兩種方法:

  • 消息傳遞
  • 共享內存

共享內存便是我們所說的將堆設置為線程間共享的,這樣我們能夠通過堆中的對象實現數據共享,這樣便使得其他線程能夠知道某個線程修改了某個數據。但是這樣帶來的問題可能就有線程安全問題等,但是這樣做的優勢便在於速度更快節約內存,Java,C#等使用這種方式

消息傳遞是每個線程都私有自己的數據空間,當需要線程通信的時候,便需要一個線程显示的給另外一個線程發送具體的消息,這樣做的能夠讓多線程更加安全,但是私有的線程空間和消息傳遞帶來的是需要給內個線程都拷貝相同的對象,變量等,並且可能會帶來效率問題。而Erlang和JoCaml便是使用消息傳遞式線程通信。

尊重勞動成功,轉載註明原創

參考文章:

如果覺得寫得不錯,歡迎關注微信公眾號:逸游Java ,每天不定時發布一些有關Java進階的文章,感謝關注

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

【其他文章推薦】

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

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

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

回顧2019年極端天氣事件 全球損失超過1000億美元

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

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

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

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

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

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

北海道天候異常沒暴風雪 罕見吹起沙塵暴

摘錄自2020年1月6日中央社東京報導

日本北海道東部的十勝地方部分地區6日突然吹起沙塵暴,造成兩條高速公路部分路段因視線不佳暫停通行。由於冬天並非當地沙塵暴季節,研判可能是因為這個冬天降雪不足所造成。

北海道往年這個時期都是大雪紛飛,十勝地方應該全部會被白雪覆蓋,但受到這個冬天降雪異常減少,讓許多未被白雪覆蓋的田地露出來,變成乾燥的沙子容易被風捲起的狀態。

上述暫停通行路段在下午2時過後就恢復車輛通行,但警方及東日本高速道路公司等還是呼籲,駕駛人行經這些路段仍要留意強風,小心駕駛。

日本放送協會(NHK)報導,十勝地方這個冬天降雪較往年來得少,強風捲起農地沙子造成視線不佳的「塵煙霧」現象,是首度在冬天出現,十勝地方帶廣市去年12月降雪量僅17公分,不到往年降雪量四成;今天積雪6公分,較往年的26公分同樣大幅減少。

 

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

【其他文章推薦】

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

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

澳洲野火災情跨年後持續擴大 下雨仍無緩解

整理:劉妙慈 (環境資訊中心實習編輯)

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

【其他文章推薦】

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

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

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

騰訊出資!中國版特斯拉 NextEV 先瞄準中國市場

騰訊和高瓴資本在內等多家中國網路巨頭和金融投資機構共同投資的新能源汽車廠商 NextEV,將成為特斯拉(Tesla)的競爭對手。據路透報導,NextEV 發言人劉吉利表示,已聘請福特前高管 Martin Leach 打造一家全球性汽車廠商,他們還招聘了有在特斯拉、寶馬(BMW)、大眾(VW)和其他主要汽車廠商工作經驗的專家。他強調,NextEV 將先瞄準中國市場,然後走向世界。   NextEV 投資方包括數家非汽車廠商的中國科技公司,且其開發電動車的努力得到中國政府的支持,中國政府最近修改相關法規,允許非汽車廠商投資電動汽車產業。目前已宣布或在考慮投資電動汽車產業的中國科技公司包括阿里巴巴、小米和樂視。   劉吉利表示,NextEV 推出的第一款產品將是一款電動超級跑車,預計該車表現將超過世界上所有採用內燃機的跑車。NextEV 並將於明年推出賽車車型,功率超過1000馬力,加速到時速 100 公里僅需 3 秒。之後 NextEV 還會推出一系列高性能車型。

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

【其他文章推薦】

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

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

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

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

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

搶佔市場份額!賓士推全新純電動車

日前,賓士稱將推出一款全新純電動車,續航里程在400-500km之間,競爭對手瞄準了特斯拉Model S。   據網通社報導,賓士2014年跟戴姆勒集團共同斥資64億美元研發低排放技術的專案,計畫未來幾年研發賓士新能源車,包括smartfortwo、A級、B級以及SLS AMG在內的多款電動車推出。   2017年起戴姆勒汽車集團斥資與福特、日產共同建設10個氫燃料補給站的舉措顯示,除了電動車外,氫燃料車也是賓士未來幾年重要產品策略。   此外,賓士還將隨主流發展插電混動版車型,據媒體消息稱截至2017年賓士將有10款插電混動版車型推出,包括S級和C級插電混動版。   除了賓士外,奧迪和寶馬均有新電動車計畫,奧迪全新Q6 e-tron Quattro概念車將於9月法蘭克福亮相,寶馬將於2018年推出一款純電動SUV競爭對手瞄準了特斯拉Model X,另外,寶馬一款基於5系打造的i5純電動也將為寶馬電動車細分市場贏得更多的份額。

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

【其他文章推薦】

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

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

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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