分類
發燒車訊

加州拚減排、擬給予電動車30億美元購車退稅補助

華爾街日報15日報導,加州下議院已通過規模達30億美元的電動車購車退稅折扣法案,後續還得過上議院與州長Jerry Brown這兩關。負責起草這項法案的舊金山民主黨籍議員Phil Ting表示,加州若想落實氣候變遷目標(2025年讓150萬輛零排放車輛上路)、勢必得想辦法給電動車產業打強心針才行。根據加州空氣資源局的統計,加州已有超過25萬輛零排放汽車(包括電動車)上路、佔美國約半數的比重。根據加州新車經銷商協會發表的報告,2017年第1季電動車僅佔加州整體汽車銷售比重的2.7%。

Edmunds(汽車銷售追蹤網站)汽車業分析師Jessica Caldwell指出,加州若推新補貼方案、對銷售應該有所幫助,特別是Chevrolet Bolt、特斯拉Model 3(見圖)擁有更好的續航力。加州自2010年起給予每台全電動車2,500美元的折抵稅額、但自今年夏季起只有所得水準符合標準的消費者才能取得補助。

Thomson Reuters 7月16日引述法國週報《Le Journal du Dimanche(JDD)》報導,法國總統馬克宏(Emmanuel Macron)在受訪時表示,他對於美國總統川普(Donald Trump)改變退出巴黎氣候協議的決定抱持希望。馬克宏說,川普在兩人碰面時表示他會試著在未來數個月內找到解決方案。

Cobalt 27 Capital Corp(TSX-V:KBLT)6月23日在加拿大創業板初次公開發行(IPO)、一舉募得2.0億加幣(相當於1.507億美元)。瑞銀(UBS)表示,鋰電池供應鏈中的原物料將受電動車滲透率快速增長的衝擊、但當中僅有鈷面臨儲備有限的問題。

通用汽車(General Motors)Chevrolet Bolt電動車續航力達238英里(383公里)、建議零售價37,495美元起(註:最多可取得7,500美元的聯邦折抵稅額、扣除後入手價相當於29,995美元)。

美聯社報導,IHS Markit汽車業分析師Stephanie Brinley指出,Bolt續航力遠高於美國平均來回通勤距離(40英里),但有時人們回家後可能忘了或沒有足夠時間進行充電,這是電動車主得多加費心的地方。

電動車大廠特斯拉(Tesla)14日上漲1.35%、收327.78美元;週線上漲4.65%、3週以來首度收高。

通用汽車14日上漲1.37%、收36.35美元,創3月16日以來收盤新高。

Cobalt 27 IPO價格為9.0加幣;7月14日大漲7.00%、以10.70加幣坐收,創IPO以來收盤新高。

(本文內容由授權使用。圖片出處:public domain CC0)

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

【其他文章推薦】

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

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

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

分類
發燒車訊

上海新能源汽車展8月23舉行 新能源物流車運營模式受關注

資料顯示,目前中國物流車的保有量在2000萬量左右,而新能源物流車在整個物流市場僅占2%。隨著建設“美麗中國”的宏願及“十三五規劃”的國策推進,物流車輛的新能源化勢在必行。目前,中國物流行業發展迅速,除了中國郵政、順豐及四通一達等快遞企業;天貓、京東等電商自建物流之外,供應鏈企業、飲料食品、餐飲配送、食蔬鮮農、工廠貨運等行業無不存在物流。

 

千億市場規模放量

 

市場普遍認為,僅考慮替代市場,城市物流車市場空間可達250萬輛。2017年以來,物流車的訂單也是不斷。3月,欣旺達與陝西通家簽訂了20000輛電動物流車的動力電池系統訂單。5月,九龍汽車與廣通汽車簽訂《採購合同》,採購金額達26.55億元。6月,中植汽車與浙江軍盛控股有限公司、城市合夥人創客(南京)股份有限公司簽訂5年10萬輛純電動物流車的超大生產協議。7月,萬馬股份發佈最新投資者關係活動記錄表披露,公司圍繞“運力”重點佈局電動物流車、充電樁、貨源和快充網四個方面。上市公司頻獲訂單和加速佈局的背後無一不在說明,新能源物流車行業的發展將呈井噴態勢。

 

市場放量巨大但運營卻遇瓶頸

 

雖然市場放量巨大,政府剛需迫切,但是新能源物流車與傳統物流車同樣存在著購車成本高、運營模式單一、盈利難等問題。同時新能源物流車基本為純電動,充電難也成為了新能源物流車的推廣中亟待解決的問題。

目前,新能源物流車運營的模式大致分為四種:1.新能源物流車輛中長期租賃模式;2.新能源物流車輛分時租賃模式;廠商自有物流車定向租賃模式;4.新能源專用車輛定制模式。但是無論哪種模式來看,新能源物流車目前普遍面臨著充維保障體系建設週期長、投資大、需要土地、電網等需政府部門協調的諸多問題。

 

現有困難如何突破?

 

業內人士認為,大規模、體系化的運營,才能真正取得客戶的深度信任,讓客戶切實感受到物流車電動化時代的到來。如果純電動物流車運營仍然沿襲傳統物流車的老模式,必將會面臨諸多的發展瓶頸。只有改革新能源物流車的商業模式與運營模式,才能從根本上解決充電難、購車成本高、維保成本高、運營效率低等一系列問題。

據瞭解,由充電設施線上網、廣東省充電設施協會、廣東省新能源汽車產業協會和振威展覽股份共同主辦,中國土木工程學會城市公共交通學會協辦的2017上海國際新能源汽車產業博覽會將於8月23-25日在上海新國際博覽中心舉行。同期還將舉辦純電動物流車運營商大會、新能源汽車充換電技術高峰論壇及共用汽車大會。

其中,純電動物流車運營商大會邀請了八匹馬、一微新能源、駒馬物流、雲杉智慧、地上鐵、傳化智聯、士繼達新能源、綠道新能源等國內知名新能源物流車運營商,以及來自政府主管部門、專家學者、科研院校、資本方等重量級嘉賓參會交流,多維度深層次探討新能源物流車運營模式,旨在推動新能源物流車產業的快速發展。

此外,比亞迪、申龍客車、珠海銀隆、上汽集團、上饒客車、中植新能源、中通、江淮、吉利、眾泰、知豆、南京金龍、成功汽車、新吉奧集團、瑞馳新能源、福汽新龍馬等新能源汽車企業,以及精進電動、英威騰、東風電機、力神、沃特瑪、國軒高科、地上鐵、特來電、科陸、巴斯巴、萬馬專纜、奧美格、瑞可達等核心三電及零部件知名企業將亮相本次展會,展出最新款產品和前沿技術。

 

參觀預登記,請點擊:

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

【其他文章推薦】

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

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

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

分類
發燒車訊

推薦算法之用矩陣分解做協調過濾——LFM模型

隱語義模型(Latent factor model,以下簡稱LFM),是基於矩陣分解的推薦算法,在其基本算法上引入L2正則的FunkSVD算法在推薦系統領域更是廣泛使用,在Spark上也有其實現。本文將對 LFM原理進行詳細闡述,給出其基本算法原理。此外,還將介紹使得隱語義模型聲名大噪的算法FunkSVD和在其基礎上改進較為成功的BiasSVD。最後,對LFM進行一個較為全面的總結。

1. 矩陣分解應用於推薦算法要解決的問題

在推薦系統中,我們經常可能面臨的場景是:現有大量用戶和物品,以及少部分用戶對少部分物品的評分,我們需要使用現有的用戶對少部分物品的評分去推測用戶對物品集中其他物品的可能的評分,從而將預測中評分高的物品推薦給用戶。例如下面的用戶物品評分表:

用戶\物品 物品 1 物品 2 物品 3 物品 4 物品 5
用戶 1 3 2
用戶 2 1 2 6
用戶 3 3 4 6
用戶 4 1 2 5
用戶 5 4 2 3

對於每個用戶,我們希望較準確的預測出其對未評分物品的評分。將m個用戶和n個物品的評分看做一個矩陣M,從而將矩陣分解應用到該場景,即可解決這一問題。而本文,將關注於矩陣分解用於到推薦的方法之一,即LFM算法的解決方案。

2. LFM

LFM算法的核心思想是通過隱含特徵(Latent factor)聯繫用戶和物品,該算法最早在文本挖掘領域中被提出用於找到文本的隱含語義,相關名詞還有LDATopic Model等。

2.1 如何表示用戶的偏好和物品(item)屬性?

在被問到這個問題時,針對MovieLens(電影評分)數據集,你可能會說用戶是否喜歡動作片,物品所屬電影類型去回答。但用戶對其他類型的電影的偏好程度呢?物品在其它類型所佔的權重又是多少呢?就算針對現有的電影類型去表徵用戶偏好和物品,那麼能否能夠完全的表示出用戶的偏好和物品屬性呢?答案是不能,比如用戶喜歡看成龍的電影這個偏好沒法表示出來,電影由誰導演,主演是誰沒法表示。但你要問我用哪些屬性去表徵呢?這個誰也無法給出一個很好的答案,粒度很難控制。

2.2 LFM來救場

隱語義模型較好的解決了該問題,它從數據出發,通過基於用戶行為統計的自動聚類,可指定出表徵用戶偏好和物品的向量維度,最終得到用戶的偏好向量以及物品的表徵向量。LFM通過以下公式計算用戶u對物品i的偏好:
\[ preference(u, i)=p^T_u q_i=\sum_f^F{p_{u,k}q_{i,k}} \]
這個公式,\(p_{u,k}\)度量了用戶u的偏好和第f個隱類的關係,\(q_{i,k}\)度量了物品i和第f個隱類的關係。

那麼現在,我們期望用戶的評分矩陣M這樣分解:
\[ M_{m*n}=P^T_{m*k}Q_{k*n} \]
那麼,我們如何將矩陣分解呢?這裏採用了線性回歸的思想,即盡可能的讓用戶的評分和我們預測的評分的殘差盡可能小,也就是說,可以用均方差作為損失函數來尋找最終的PQ。考慮所有的用戶和樣本的組合,我們期望的最小化損失函數為:
\[ \sum_{i,j}{(m_{ij}-p_i^Tq_j)^2} \]
只要我們能夠最小化上面的式子,並求出極值所對應的\(p_i\)\(q_j\),則我們最終可以得到矩陣PQ,那麼對於任意矩陣M任意一個空白評分的位置,我們就可以通過\(p^T_i q_j\)計算預測評分。

2.3 FunkSVD用於推薦

上面是隱語義模型LFM的基本原理,但在實際業務中,為防止過擬合,我們常常會加入一個L2的正則化項,這也就誕生了我們的FunkSVD算法。其優化目標函數\(J(p,q)\)定義為:
\[ \underbrace{argmin}_{p_i,q_j}\sum_{i,j}{(m_{ij}-p^T_iq_j)^2+\lambda({\Arrowvert{p_i}\Arrowvert}^2_2+{\Arrowvert{q_i}\Arrowvert}^2_2)} \]
其中λ為正則化係數,需要調參。對於這個優化問題,我們一般通過梯度下降法來進行優化得到結果。

將上式分別對\(p_i\)\(q_j\)求導我們得到:
\[ \frac{\partial{J}}{\partial{p_i}}=-2(m_{ij}-p^T_iq_j)q_j+2\lambda{p_i} \]

\[ \frac{\partial{J}}{\partial{q_j}}=-2(m_{ij}-p^T_iq_j)p_i+2\lambda{q_j} \]

則梯度下降中迭代公式為:
\[ p_i = p_i +\alpha((m_{ij}-p^T_iq_j)q_j-\lambda{p_i}) \]

\[ q_j = q_j+\alpha((m_{ij}-p^T_iq_j)p_i-\lambda{q_j}) \]

通過迭代我們最終可以得到PQ,進而用於推薦。

為讀者進一步理解,筆者實現了基於MovieLens數據集實現了該方法。代碼詳見github:

2.4 BiasSVD用於推薦

BiasSVDFunkSVD較為成功的改進版算法。BiasSVD假設評分系統包括三部分的偏置因素:一些和用戶物品無關的評分因素。用戶有一些和物品無關的評分因素,稱為用戶偏置項。而物品也有一些和用戶無關的評分因素,稱為物品偏置項。這很好理解,對於樂觀的用戶來說,它的評分行為普遍偏高,而對批判性用戶來說,他的評分記錄普遍偏低,即使他們對同一物品的評分相同,但是他們對該物品的喜好程度卻並不一樣。同理,對物品來說,以電影為例,受大眾歡迎的電影得到的評分普遍偏高,而一些爛片的評分普遍偏低,這些因素都是獨立於用戶或產品的因素,而和用戶對產品的的喜好無關。

假設評分系統平均分為μ,第i個用戶的用戶偏置項為\(b_i\),而第j個物品的物品偏置項為\(b_j\),則加入了偏置項以後的優化目標函數\(J(p_i,q_j)\)是這樣的:
\[ \underbrace{argmin}_{p_i,q_j}\sum_{i,j}{(m_{ij}-p^T_iq_j-u-b_i-b_j)^2+\lambda({\Arrowvert{p_i}\Arrowvert}^2_2+{\Arrowvert{q_i}\Arrowvert}^2_2+{\Arrowvert{b_i}\Arrowvert}^2_2+{\Arrowvert{b_j}\Arrowvert}^2_2)} \]
這個優化目標也可以採用梯度下降法求解。和FunkSVD不同的是,此時我們多了兩個偏執項\(b_i\)\(b_j\)\(p_i\)\(q_j\)的迭代公式和FunkSVD類似,只是每一步的梯度導數稍有不同而已。\(b_i\)\(b_j\)一般可以初始設置為0,然後參与迭代。迭代公式為:
\[ p_i = p_i +\alpha((m_{ij}-p^T_iq_j-u-b_i-b_j)q_j-\lambda{p_i}) \]

\[ q_j = q_j+\alpha((m_{ij}-p^T_iq_j-u-b_i-b_j)p_i-\lambda{q_j}) \]

\[ b_i=b_i+\alpha(m_{ij}-p^T_iq_j-u-b_i-b_j-\lambda{b_i}) \]

\[ b_j=b_j+\alpha(m_{ij}-p^T_iq_j-u-b_i-b_j-\lambda{b_j}) \]

通過迭代我們最終可以得到PQ,進而用於推薦。BiasSVD增加了一些額外因素的考慮,因此在某些場景會比FunkSVD表現好。

為讀者進一步理解,筆者實現了基於MovieLens數據集實現了該方法。代碼詳見github

小結

LFM 是一種基於機器學習的方法,具有比較好的理論基礎,通過優化一個設定的指標建立最優的模型。它實質上是矩陣分解應用到推薦的方法,其中FunkSVD更是將矩陣分解用於推薦方法推到了新的高度,在實際應用中使用非常廣泛。當然矩陣分解方法也在不停的進步,目前矩陣分解推薦算法中分解機方法(factorization machine, FM)已成為一個趨勢。

對於矩陣分解用於推薦方法本身來說,它容易編程實現,實現複雜度低,預測效果也好,同時還能保持擴展性。這些都是其寶貴的優點。但是LFM 無法給出很好的推薦解釋,它計算出的隱類雖然在語義上確實代表了一類興趣和物品,卻很難用自然語言描述並生成解釋展現給用戶。

LFM 在建模過程中,假設有 M 個用戶、 N 個物品、 K 條用戶對物品的行為記錄,如果是 F 個隱類,那麼它離線計算的空間複雜度是 \(O(F*(M+N))\) ,迭代 S次則時間複雜度為 \(O(K * F * S)\)。當 M(用戶數量)和 N(物品數量)很大時LFM相對於ItemCFUserCF可以很好地節省離線計算的內存,在時間複雜度由於LFM會多次迭代上所以和ItemCFUserCF沒有質的差別。

同時,遺憾的是,LFM 無法進行在線實時推薦,即當用戶有了新的行為後,他的推薦列表不會發生變化。而從 LFM的預測公式可以看到, LFM 在給用戶生成推薦列表時,需要計算用戶對所有物品的興趣權重,然後排名,返回權重最大的 N 個物品。那麼,在物品數很多時,這一過程的時間複雜度非常高,可達 \(O(M*N*F)\) 。因此, LFM 不太適合用於物品數非常龐大的系統,如果要用,我們也需要一個比較快的算法給用戶先計算一個比較小的候選列表,然後再用LFM重新排名。另一方面,LFM 在生成一個用戶推薦列表時速度太慢,因此不能在線實時計算,而需要離線將所有用戶的推薦結果事先計算好存儲在數據庫中。

參考:

  • 推薦系統實戰—項亮

(歡迎轉載,轉載請註明出處。歡迎溝通交流: losstie@outlook.com)

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

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

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

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

分類
發燒車訊

同步鎖基本原理與實現

  為充分利用機器性能,人們發明了多線程。但同時帶來了線程安全問題,於是人們又發明了同步鎖。

  這個問題自然人人知道,但你真的了解同步鎖嗎?還是說你會用其中的上鎖與解鎖功能?

  今天我們就一起來深入看同步鎖的原理和實現吧!

 

一、同步鎖的職責

  同步鎖的職責可以說就一個,限制資源的使用(線程安全從屬)。

  它一般至少會包含兩個功能: 1. 給資源加鎖; 2. 給資源解鎖;另外,它一般還有 等待/通知 即 wait/notify 的功能;

  同步鎖的應用場景:多個線程同時操作一個事務必須保證正確性;一個資源只能同時由一線程訪問操作;一個資源最多只能接入k的併發訪問;保證訪問的順序性;

  同步鎖的實現方式:操作系統調度實現;應用自行實現;CAS自旋;

  同步鎖的幾個問題:

    為什麼它能保證線程安全?

    鎖等待耗CPU嗎?

    使用鎖后性能下降嚴重的原因是啥?

 

二、同步鎖的實現一:lock/unlock

  其實對於應用層來說,非常多就是 lock/unlock , 這也是鎖的核心。

  AQS 是java中很多鎖實現的基礎,因為它屏蔽了很多繁雜而底層的阻塞操作,為上層抽象出易用的接口。

  我們就以AQS作為跳板,先來看一下上鎖的過程。為不至於陷入具體鎖的業務邏輯中,我們先以最簡單的 CountDownLatch 看看。

    // 先看看 CountDownLatch 的基礎數據結構,可以說是不能再簡單了,就繼承了 AQS,然後簡單覆寫了幾個必要方法。
    // java.util.concurrent.CountDownLatch.Sync
    /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            // 只有一種情況會獲取鎖成功,即 state == 0 的時候
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                // 原始的鎖數量是在初始化時指定的不可變的,每次釋放一個鎖標識
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    // 只有一情況會釋放鎖成功,即本次釋放后 state == 0
                    return nextc == 0;
            }
        }
    }
    private final Sync sync;

 

重點1,我們看看上鎖過程,即 await() 的調用。

    public void await() throws InterruptedException {
        // 調用 AQS 的接口,由AQS實現了鎖的骨架邏輯
        sync.acquireSharedInterruptibly(1);
    }
    
    // java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireSharedInterruptibly
    /**
     * Acquires in shared mode, aborting if interrupted.  Implemented
     * by first checking interrupt status, then invoking at least once
     * {@link #tryAcquireShared}, returning on success.  Otherwise the
     * thread is queued, possibly repeatedly blocking and unblocking,
     * invoking {@link #tryAcquireShared} until success or the thread
     * is interrupted.
     * @param arg the acquire argument.
     * This value is conveyed to {@link #tryAcquireShared} but is
     * otherwise uninterpreted and can represent anything
     * you like.
     * @throws InterruptedException if the current thread is interrupted
     */
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        // 首先嘗試獲取鎖,如果成功就不用阻塞了
        // 而從上面的邏輯我們看到,獲取鎖相當之簡單,所以,獲取鎖本身並沒有太多的性能消耗喲
        // 如果獲取鎖失敗,則會進行稍後嘗試,這應該是複雜而精巧的
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    
    /**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        // 首先將當前線程添加排隊隊尾,此處會保證線程安全,稍後我們可以看到
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                // 獲取其上一節點,如果上一節點是頭節點,就代表當前線程可以再次嘗試獲取鎖了
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                // 先檢測是否需要阻塞,然後再進行阻塞等待,阻塞由 LockSupport 底層支持
                // 如果阻塞后,將不會主動喚醒,只會由 unlock 時,主動被通知
                // 因此,此處即是獲取鎖的最終等待點
                // 操作系統將不會再次調度到本線程,直到獲取到鎖
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    // 如此線程安全地添加當前線程到隊尾? CAS 保證
    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    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;
                }
            }
        }
    }
    
    // 檢測是否需要進行阻塞
    /**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
             // 只有前置節點是 SIGNAL 狀態的節點,才需要進行 阻塞等待,當然前置節點會在下一次循環中被設置好
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    
    // park 阻塞實現
    /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        // 將當前 AQS 實例作為鎖對象 blocker, 進行操作系統調用阻塞, 所以所有等待鎖的線程將會在同一個鎖前提下執行
        LockSupport.park(this);
        return Thread.interrupted();
    }

  如上,上鎖過程是比較簡單明了的。加入一隊列,然後由操作系統將線程調出。(那麼操作系統是如何把線程調出的呢?有興趣自行研究)

 

重點2. 解鎖過程,即 countDown() 調用

    public void countDown() {
        // 同樣直接調用 AQS 的接口,由AQS實現了鎖的釋放骨架邏輯
        sync.releaseShared(1);
    }
    // java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared
    /**
     * Releases in shared mode.  Implemented by unblocking one or more
     * threads if {@link #tryReleaseShared} returns true.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryReleaseShared} but is otherwise uninterpreted
     *        and can represent anything you like.
     * @return the value returned from {@link #tryReleaseShared}
     */
    public final boolean releaseShared(int arg) {
        // 調用業務實現的釋放邏輯,如果成功,再執行底層的釋放,如隊列移除,線程通知等等
        // 在 CountDownLatch 的實現中,只有 state == 0 時才會成功,所以它只會執行一次底層釋放
        // 這也是我們認為 CountDownLatch 能夠做到多線程同時執行的效果的原因之一
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    
    /**
     * Release action for shared mode -- signals successor and ensures
     * propagation. (Note: For exclusive mode, release just amounts
     * to calling unparkSuccessor of head if it needs signal.)
     */
    private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        for (;;) {
            Node h = head;
            // 隊列不為空才進行釋放
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                // 看過上面的 lock 邏輯,我們知道只要在阻塞狀態,一定是 Node.SIGNAL 
                if (ws == Node.SIGNAL) {
                    // 狀態改變成功,才進行後續的喚醒邏輯
                    // 因為先改變狀態成功,才算是線程安全的,再進行喚醒,否則進入下一次循環再檢查
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 將頭節點的下一節點喚醒,如有必要
                    unparkSuccessor(h);
                }
                // 這裏的 propagates, 是要傳播啥呢??
                // 為什麼只喚醒了一個線程,其他線程也可以動了?
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }
    /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        // 喚醒下一個節點
        // 但如果下一節點已經取消等待了,那麼就找下一個沒最近的沒被取消的線程進行喚醒
        // 喚醒只是針對一個線程的喲
        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;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

 

重要3. 線程解鎖的傳播性?

  因為從上一節的講解中,我們看到,當用戶調用 countDown 時,僅僅是讓操作系統喚醒了 head 的下一個節點線程或者最近未取消的節點。那麼,從哪裡來的所有線程都獲取了鎖從而運行呢?

  其實是在 獲取鎖的過程中,還有一點我們未看清:

    // java.util.concurrent.locks.AbstractQueuedSynchronizer#doAcquireShared
    /**
     * Acquires in shared uninterruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    // 當countDown被調用后,head節點被喚醒,執行
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        // 獲取到鎖后,設置node為下一個頭節點,並把喚醒狀態傳播下去,而這裏面肯定會做一些喚醒其他線程的操作,請看下文
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    /**
     * Sets head of queue, and checks if successor may be waiting
     * in shared mode, if so propagating if either propagate > 0 or
     * PROPAGATE status was set.
     *
     * @param node the node
     * @param propagate the return value from a tryAcquireShared
     */
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            // 如果有必要,則做一次喚醒下一線程的操作
            // 在 countDown() 不會觸發此操作,所以這裏只是一個內部調用傳播
            Node s = node.next;
            if (s == null || s.isShared())
                // 此處鎖釋放邏輯如上,總之,又是另一次的喚醒觸發
                doReleaseShared();
        }
    }

  到此,我們明白了它是怎麼做到一個鎖釋放,所有線程可通行的。也從根本上回答了我們猜想,所有線程同時併發運行。然而並沒有,它只是通過喚醒傳播性來依次喚醒各個等待線程的。從絕對時間性上來講,都是有先後關係的。以後可別再淺顯說是同時執行了喲。

 

三、 鎖的切換:wait/notify

  上面看出,針對一個lock/unlock 的過程還是很簡單的,由操作系統負責大頭,實現代碼也並不多。

  但是針對稍微有點要求的場景,就會進行條件式的操作。比如:持有某個鎖運行一段代碼,但是,運行時發現某條件不滿足,需要進行等待而不能直接結束,直到條件成立。即所謂的 wait 操作。

  乍一看,wait/notify 與 lock/unlock 很像,其實不然。區分主要是 lock/unlock 是針對整個代碼段的,而 wait/notify 則是針對某個條件的,即獲取了鎖不代表條件成立了,但是條件成立了一定要在鎖的前提下才能進行安全操作。

  那麼,是否 wait/notify 也一樣的實現簡單呢?比如java的最基礎類 Object 類就提供了 wait/notify 功能。

  我們既然想一探究竟,還是以併發包下的實現作為基礎吧,畢竟 java 才是我們的強項。

  本次,咱們以  ArrayBlockingQueue#put/take 作為基礎看下這種場景的使用先。

  ArrayBlockingQueue 的put/take 特性就是,put當隊列滿時,一直阻塞,直到有可用位置才繼續運行下一步。而take當隊列為空時一樣阻塞,直到隊列里有數據才運行下一步。這種場景使用鎖主不好搞了,因為這是一個條件判斷。put/take 如下:

    // java.util.concurrent.ArrayBlockingQueue#put
    /**
     * Inserts the specified element at the tail of this queue, waiting
     * for space to become available if the queue is full.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            // 當隊列滿時,一直等待
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
    
    // java.util.concurrent.ArrayBlockingQueue#take
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            // 當隊列為空時一直等待
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

  看起來相當簡單,完全符合人類思維。只是,這裏使用的兩個變量進行控制流程 notFull,notEmpty. 這兩個變量是如何進行關聯的呢?

  在這之前,我們還需要補充下上面的例子,即 notFull.await(), notEmpty.await(); 被阻塞了,何時才能運行呢?如上代碼在各自的入隊和出隊完成之後進行通知就可以了。

    // 與 put 對應,入隊完成后,隊列自然就不為空了,通知下 notEmpty 就好了
    /**
     * Inserts element at current put position, advances, and signals.
     * Call only when holding lock.
     */
    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        // 我已放入一個元素,不為空了
        notEmpty.signal();
    }
    // 與 take 對應,出隊完成后,自然就不可能是滿的了,至少一個空餘空間。
    /**
     * Extracts element at current take position, advances, and signals.
     * Call only when holding lock.
     */
    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        // 我已移除一個元素,肯定沒有滿了,你們繼續放入吧
        notFull.signal();
        return x;
    }

  是不是超級好理解。是的。不過,我們不是想看 ArrayBlockingQueue 是如何實現的,我們是要論清 wait/notify 是如何實現的。因為畢竟,他們不是一個鎖那麼簡單。

    // 三個鎖的關係,即 notEmpty, notFull 都是 ReentrantLock 的條件鎖,相當於是其子集吧
    /** Main lock guarding all access */
    final ReentrantLock lock;

    /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;
    
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
    // lock.newCondition() 是什麼鬼?它是 AQS 中實現的 ConditionObject
    // java.util.concurrent.locks.ReentrantLock#newCondition
    public Condition newCondition() {
        return sync.newCondition();
    }
        // java.util.concurrent.locks.ReentrantLock.Sync#newCondition
        final ConditionObject newCondition() {
            // AQS 中定義
            return new ConditionObject();
        }

  接下來,我們要帶着幾個疑問來看這個 Condition 的對象:

    1. 它的 wait/notify 是如何實現的?
    2. 它是如何與互相進行聯繫的?
    3. 為什麼 wait/notify 必須要在外面的lock獲取之後才能執行?
    4. 它與Object的wait/notify 有什麼相同和不同點?

  能夠回答了上面的問題,基本上對其原理與實現也就理解得差不多了。

 

重點1. wait/notify 是如何實現的?

  我們從上面可以看到,它是通過調用 await()/signal() 實現的,到底做事如何,且看下面。

        // java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#await()
        /**
         * Implements interruptible condition wait.
         * <ol>
         * <li> If current thread is interrupted, throw InterruptedException.
         * <li> Save lock state returned by {@link #getState}.
         * <li> Invoke {@link #release} with saved state as argument,
         *      throwing IllegalMonitorStateException if it fails.
         * <li> Block until signalled or interrupted.
         * <li> Reacquire by invoking specialized version of
         *      {@link #acquire} with saved state as argument.
         * <li> If interrupted while blocked in step 4, throw InterruptedException.
         * </ol>
         */
        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // 添加當前線程到 等待線程隊列中,有 lastWaiter/firstWaiter 維護
            Node node = addConditionWaiter();
            // 釋放當前lock中持有的鎖,詳情且看下文
            int savedState = fullyRelease(node);
            // 從以下開始,將不再保證線程安全性,因為當前的鎖已經釋放,其他線程將會重新競爭鎖使用
            int interruptMode = 0;
            // 循環判定,如果當前節點不在 sync 同步隊列中,那麼就反覆阻塞自己
            // 所以判斷是否在 同步隊列上,是很重要的
            while (!isOnSyncQueue(node)) {
                // 沒有在同步隊列,阻塞
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 當條件被滿足后,需要重新競爭鎖,詳情看下文
            // 競爭到鎖后,原樣返回到 wait 的原點,繼續執行業務邏輯
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            // 下面是異常處理,忽略
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
    /**
     * Invokes release with current state value; returns saved state.
     * Cancels node and throws exception on failure.
     * @param node the condition node for this wait
     * @return previous sync state
     */
    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            // 預期的,都是釋放鎖成功,如果失敗,說明當前線程並並未獲取到鎖,引發異常
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }
    /**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    public final boolean release(int arg) {
        // tryRelease 由客戶端自定義實現
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    // 如何判定當前線程是否在同步隊列中或者可以進行同步隊列?
    /**
     * Returns true if a node, always one that was initially placed on
     * a condition queue, is now waiting to reacquire on sync queue.
     * @param node the node
     * @return true if is reacquiring
     */
    final boolean isOnSyncQueue(Node node) {
        // 如果上一節點還沒有被移除,當前節點就不能被加入到同步隊列
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        // 如果當前節點的下游節點已經存在,則它自身必定已經被移到同步隊列中
        if (node.next != null) // If has successor, it must be on queue
            return true;
        /*
         * node.prev can be non-null, but not yet on queue because
         * the CAS to place it on queue can fail. So we have to
         * traverse from tail to make sure it actually made it.  It
         * will always be near the tail in calls to this method, and
         * unless the CAS failed (which is unlikely), it will be
         * there, so we hardly ever traverse much.
         */
         // 最終直接從同步隊列中查找,如果找到,則自身已經在同步隊列中
        return findNodeFromTail(node);
    }

    /**
     * Returns true if node is on sync queue by searching backwards from tail.
     * Called only when needed by isOnSyncQueue.
     * @return true if present
     */
    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }
    
    // 當條件被滿足后,需要重新競爭鎖,以保證外部的鎖語義,因為之前自己已經將鎖主動釋放
    // 這個鎖與 lock/unlock 時的一毛一樣,沒啥可講的
    // java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued
    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    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);
        }
    }

  總結一下 wait 的邏輯:

    1. 前提:自身已獲取到外部鎖;
    2. 將當前線程添加到 ConditionQueue 等待隊列中;
    3. 釋放已獲取到的鎖;
    4. 反覆檢查進入等待,直到當前節點被移動到同步隊列中;
    5. 條件滿足被喚醒,重新競爭外部鎖,成功則返回,否則繼續阻塞;(外部鎖是同一個,這也是要求兩個對象必須存在依賴關係的原因)
    6. wait前線程持有鎖,wait后線程持有鎖,沒有一點外部鎖變化;

 

重點2. 釐清了 wait, 接下來,我們看 signal() 通知喚醒的實現:

        // java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#signal
        /**
         * Moves the longest-waiting thread, if one exists, from the
         * wait queue for this condition to the wait queue for the
         * owning lock.
         *
         * @throws IllegalMonitorStateException if {@link #isHeldExclusively}
         *         returns {@code false}
         */
        public final void signal() {
            // 只有獲取鎖的實例,才可以進行signal,否則你拿什麼去保證線程安全呢
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            // 通知 firstWaiter 
            if (first != null)
                doSignal(first);
        }
        
        /**
         * Removes and transfers nodes until hit non-cancelled one or
         * null. Split out from signal in part to encourage compilers
         * to inline the case of no waiters.
         * @param first (non-null) the first node on condition queue
         */
        private void doSignal(Node first) {
            // 最多只轉移一個 節點
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
    // 將一個節點從 等待隊列 移動到 同步隊列中,即可參与下一輪競爭
    // 只有確實移動成功才會返回 true
    // 說明:當前線程是持有鎖的線程
    // java.util.concurrent.locks.AbstractQueuedSynchronizer#transferForSignal
    /**
     * Transfers a node from a condition queue onto sync queue.
     * Returns true if successful.
     * @param node the node
     * @return true if successfully transferred (else the node was
     * cancelled before signal)
     */
    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        // 同步隊列由 head/tail 指針維護
        Node p = enq(node);
        int ws = p.waitStatus;
        // 注意,此處正常情況下並不會喚醒等待線程,僅是將隊列轉移。 
        // 因為當前線程的鎖保護區域並未完成,完成后自然會喚醒其他等待線程
        // 否則將會存在當前線程任務還未執行完成,卻被其他線程搶了先去,那接下來的任務當如何??
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

  總結一下,notify 的功能原理如下:

    1. 前提:自身已獲取到外部鎖;
    2. 轉移下一個等待隊列的節點到同步隊列中;
    3. 如果遇到下一節點被取消情況,順延到再下一節點直到為空,至多轉移一個節點;
    4. 正常情況下不做線程的喚醒操作;

  所以,實現 wait/notify, 最關鍵的就是維護兩個隊列,等待隊列與同步隊列,而且都要求是在有外部鎖保證的情況下執行。

  到此,我們也能回答一個問題:為什麼wait/notify一定要在鎖模式下才能運行?

  因為wait是等待條件成立,此時必定存在競爭需要做保護,而它自身又必須釋放鎖以使外部條件可成立,且後續需要做恢復動作;而notify之後可能還有後續工作必須保障安全,notify只是鎖的一個子集。。。

 

四、通知所有線程的實現:notifyAll

  有時條件成立后,可以允許所有線程通行,這時就可以進行 notifyAll, 那麼如果達到通知所有的目的呢?是一起通知還是??

  以下是 AQS 中的實現:

        // java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#signalAll
        public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }
        /**
         * Removes and transfers all nodes.
         * @param first (non-null) the first node on condition queue
         */
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

  可以看到,它是通過遍歷所有節點,依次轉移等待隊列到同步隊列(通知)的,原本就沒有人能同時干幾件事的!

  本文從java實現的角度去解析同步鎖的原理與實現,但並不局限於java。道理總是相通的,只是像操作系統這樣的大佬,能幹的活更純粹:比如讓cpu根本不用調度一個線程。

 

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

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

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

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

分類
發燒車訊

實現 Redis 協議解析器

本文是 《用 Golang 實現一個 Redis》系列文章第二篇,本文將分別介紹 以及 的實現,若您對協議有所了解可以直接閱讀協議解析器部分。

Redis 通信協議

Redis 自 2.0 版本起使用了統一的協議 RESP (REdis Serialization Protocol),該協議易於實現,計算機可以高效的進行解析且易於被人類讀懂。

RESP 是一個二進制安全的文本協議,工作於 TCP 協議上。客戶端和服務器發送的命令或數據一律以 \r\n (CRLF)結尾。

RESP 定義了5種格式:

  • 簡單字符串(Simple String): 服務器用來返回簡單的結果,比如”OK”。非二進制安全,且不允許換行。
  • 錯誤信息(Error): 服務器用來返回簡單的結果,比如”ERR Invalid Synatx”。非二進制安全,且不允許換行。
  • 整數(Integer): llenscard等命令的返回值, 64位有符號整數
  • 字符串(Bulk String): 二進制安全字符串, get 等命令的返回值
  • 數組(Array, 舊版文檔中稱 Multi Bulk Strings): Bulk String 數組,客戶端發送指令以及lrange等命令響應的格式

RESP 通過第一個字符來表示格式:

  • 簡單字符串:以”+” 開始, 如:”+OK\r\n”
  • 錯誤:以”-” 開始,如:”-ERR Invalid Synatx\r\n”
  • 整數:以”:”開始,如:”:1\r\n”
  • 字符串:以 $ 開始
  • 數組:以 * 開始

Bulk String有兩行,第一行為 $+正文長度,第二行為實際內容。如:

$3\r\nSET\r\n

Bulk String 是二進制安全的可以包含任意字節,就是說可以在 Bulk String 內部包含 “\r\n” 字符(行尾的CRLF被隱藏):

$4
a\r\nb

$-1 表示 nil, 比如使用 get 命令查詢一個不存在的key時,響應即為$-1

Array 格式第一行為 “*”+數組長度,其後是相應數量的 Bulk String。如, ["foo", "bar"]的報文:

*2
$3
foo
$3
bar

客戶端也使用 Array 格式向服務端發送指令。命令本身將作為第一個參數,如 SET key value指令的RESP報文:

*3
$3
SET
$3
key
$5
value

將換行符打印出來:

*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

協議解析器

我們在 一文中已經介紹過TCP服務器的實現,協議解析器將實現其 Handler 接口充當應用層服務器。

協議解析器將接收 Socket 傳來的數據,並將其數據還原為 [][]byte 格式,如 "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\value\r\n" 將被還原為 ['SET', 'key', 'value']

本文完整代碼:

來自客戶端的請求均為數組格式,它在第一行中標記報文的總行數並使用CRLF作為分行符。

bufio 標準庫可以將從 reader 讀到的數據緩存到 buffer 中,直至遇到分隔符或讀取完畢后返回,所以我們使用 reader.ReadBytes('\n') 來保證每次讀取到完整的一行。

需要注意的是RESP是二進制安全的協議,它允許在正文中使用CRLF字符。舉例來說 Redis 可以正確接收並執行SET "a\r\nb" 1指令, 這條指令的正確報文是這樣的:

*3  
$3
SET
$4
a\r\nb 
$7
myvalue

ReadBytes 讀取到第五行 “a\r\nb\r\n”時會將其誤認為兩行:

*3  
$3
SET
$4
a  // 錯誤的分行
b // 錯誤的分行
$7
myvalue

因此當讀取到第四行$4后, 不應該繼續使用 ReadBytes('\n') 讀取下一行, 應使用 io.ReadFull(reader, msg) 方法來讀取指定長度的內容。

msg = make([]byte, 4 + 2) // 正文長度4 + 換行符長度2
_, err = io.ReadFull(reader, msg)

定義 Client 結構體作為客戶端抽象:

type Client struct {
    /* 與客戶端的 Tcp 連接 */
    conn   net.Conn

    /* 
     * 帶有 timeout 功能的 WaitGroup, 用於優雅關閉
     * 當響應被完整發送前保持 waiting 狀態, 阻止鏈接被關閉
     */
    waitingReply wait.Wait

    /* 標記客戶端是否正在發送指令 */ 
    sending atomic.AtomicBool
    
    /* 客戶端正在發送的參數數量, 即 Array 第一行指定的數組長度 */
    expectedArgsCount uint32
    
    /* 已經接收的參數數量, 即 len(args)*/ 
    receivedCount uint32
    
    /*
     * 已經接收到的命令參數,每個參數由一個 []byte 表示
     */
    args [][]byte
}

定義解析器:

type Handler struct {

    /* 
     * 記錄活躍的客戶端鏈接 
     * 類型為 *Client -> placeholder 
     */
    activeConn sync.Map 

    /* 數據庫引擎,執行指令並返回結果 */
    db db.DB

    /* 關閉狀態標誌位,關閉過程中時拒絕新建連接和新請求 */
    closing atomic.AtomicBool 
}

接下來可以編寫主要部分了:

func (h *Handler)Handle(ctx context.Context, conn net.Conn) {
    if h.closing.Get() {
        // 關閉過程中不接受新連接
        _ = conn.Close()
    }

    /* 初始化客戶端狀態 */
    client := &Client {
        conn:   conn,
    }
    h.activeConn.Store(client, 1)

    reader := bufio.NewReader(conn)
    var fixedLen int64 = 0 // 將要讀取的 BulkString 的正文長度
    var err error
    var msg []byte
    for {
        /* 讀取下一行數據 */ 
        if fixedLen == 0 { // 正常模式下使用 CRLF 區分數據行
            msg, err = reader.ReadBytes('\n')
            // 判斷是否以 \r\n 結尾
            if len(msg) == 0 || msg[len(msg) - 2] != '\r' {
                errReply := &reply.ProtocolErrReply{Msg:"invalid multibulk length"}
                _, _ =  client.conn.Write(errReply.ToBytes())
            }
        } else { // 當讀取到 BulkString 第二行時,根據給出的長度進行讀取
            msg = make([]byte, fixedLen + 2)
            _, err = io.ReadFull(reader, msg)
            // 判斷是否以 \r\n 結尾
            if len(msg) == 0 || 
              msg[len(msg) - 2] != '\r' ||  
              msg[len(msg) - 1] != '\n'{
                errReply := &reply.ProtocolErrReply{Msg:"invalid multibulk length"}
                _, _ =  client.conn.Write(errReply.ToBytes())
            }
            // Bulk String 讀取完畢,重新使用正常模式
            fixedLen = 0 
        }
        // 處理 IO 異常
        if err != nil {
            if err == io.EOF || err == io.ErrUnexpectedEOF {
                logger.Info("connection close")
            } else {
                logger.Warn(err)
            }
            _ = client.Close()
            h.activeConn.Delete(client)
            return // io error, disconnect with client
        }

        /* 解析收到的數據 */
        if !client.sending.Get() { 
            // sending == false 表明收到了一條新指令
            if msg[0] == '*' {
                // 讀取第一行獲取參數個數
                expectedLine, err := strconv.ParseUint(string(msg[1:len(msg)-2]), 10, 32)
                if err != nil {
                    _, _ = client.conn.Write(UnknownErrReplyBytes)
                    continue
                }
                // 初始化客戶端狀態
                client.waitingReply.Add(1) // 有指令未處理完成,阻止服務器關閉
                client.sending.Set(true) // 正在接收指令中
                // 初始化計數器和緩衝區 
                client.expectedArgsCount = uint32(expectedLine) 
                client.receivedCount = 0
                client.args = make([][]byte, expectedLine)
            } else {
                // TODO: text protocol
            }
        } else {
            // 收到了指令的剩餘部分(非首行)
            line := msg[0:len(msg)-2] // 移除換行符
            if line[0] == '$' { 
                // BulkString 的首行,讀取String長度
                fixedLen, err = strconv.ParseInt(string(line[1:]), 10, 64)
                if err != nil {
                    errReply := &reply.ProtocolErrReply{Msg:err.Error()}
                    _, _ = client.conn.Write(errReply.ToBytes())
                }
                if fixedLen <= 0 {
                    errReply := &reply.ProtocolErrReply{Msg:"invalid multibulk length"}
                    _, _ = client.conn.Write(errReply.ToBytes())
                }
            } else { 
                // 收到參數
                client.args[client.receivedCount] = line
                client.receivedCount++
            }


            // 一條命令發送完畢
            if client.receivedCount == client.expectedArgsCount {
                client.sending.Set(false)

                // 執行命令並響應
                result := h.db.Exec(client.args)
                if result != nil {
                    _, _ = conn.Write(result.ToBytes())
                } else {
                    _, _ = conn.Write(UnknownErrReplyBytes)
                }

                // 重置客戶端狀態,等待下一條指令
                client.expectedArgsCount = 0
                client.receivedCount = 0
                client.args = nil
                client.waitingReply.Done()
            }
        }
    }
}

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

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

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

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

分類
發燒車訊

【目標檢測實戰】目標檢測實戰之一–手把手教你LMDB格式數據集製作!

文章目錄

1 目標檢測簡介
2 lmdb數據製作
    2.1 VOC數據製作
    2.2 lmdb文件生成

lmdb格式的數據是在使用caffe進行目標檢測或分類時,使用的一種數據格式。這裏我主要以目標檢測為例講解lmdb格式數據的製作。

1 目標檢測簡介

【1】目標檢測主要有兩個任務:

  1. 判斷圖像中對象的類別
  2. 類別的位置

【2】目標檢測需要的數據:

  1. 訓練所需的圖像數據,可以是jpg、png等圖片格式
  2. 圖像數據對應的類別信息和類別框的位置信息。

2 lmdb數據製作

caffe一般使用lmdb格式的數據,在製作數據之前,我們需要對數據進行標註,可以使用labelImg對圖像進行標註(),這裏就不多贅述數據標註的問題。總之,我們得到了圖像的標註Annotations數據。lmdb數據製作,首先需要將annotations數據和圖像數據製作為VOC格式,然後將其生成LMDB文件即可。下邊是詳細的步驟:

2.1 VOC數據製作

這裏我以caffe環境的Mobilenet+YOLOv3模型的代碼為例(),進行lmdb數據製作,並且也假設你已經對其配置編譯成功(如沒成功,可以參考博文進行配置),所以我們的根目錄為:caffe-Mobilenet-YOLO-master,下邊為詳細步驟:

【1】VOC格式目錄建立:

VOC格式目錄主要包含為:

其中,Annotations里存儲的是xml標註信息,JPEGImages存儲的是圖片,ImageSets則是訓練和測試的txt列表等信息,下邊我們就要安裝如上的目錄進行建立我們自己的數據目錄。

創建Annotations、JPEGImages、ImageSets/Main等文件,命令如下(也可直接界面操作哈):

注:建議新手按照我的名稱,對於後續文件修改容易!!!

cd ~/   # 進入home目錄
cd Documents/  # 進入Documents目錄
cd caffe-Mobilenet-YOLO-master/  # 進入我們的根目錄
cd data         # 進入data目錄內
mkdir VOCdevkit   # 創建存儲我們自己的數據的文件夾
cd VOCdevkit
mkdir MyDataSet  # 創建存儲voc的目錄
cd MyDataSet   
# 創建VOC格式目錄
mkdir Annotations
mkdir JPEGImages
mkdir ImageSets
cd ImageSets
mkdir Main

好啦,我們的文件夾就建立好了,如下圖所示:

【2】將所有xml文件考入至Annotations文件夾內
【3】將所有圖片考入至JPEGImages文件夾內
【4】劃分訓練接、驗證集合測試集,如下為Python代碼,需要修改的地方註釋已標明:

import os  
import random 
# 標註文件的路徑,需要你自己修改
xmlfilepath=r'/home/Documents/caffe-Mobilenet-YOLO-master/data/VOCdevkit/MyDataSet/Annotations/'      
# 這裡是存儲數據的本目錄,需要改為你自己的目錄              
saveBasePath=r"/home/Documents/caffe-Mobilenet-YOLO-master/data/VOCdevkit/"                        
trainval_percent=0.8            # 表示訓練集和驗證集所佔比例,你需要自己修改,也可選擇不修改
train_percent=0.8               # 表示訓練集所佔訓練集驗證集的比例,你需要自己修改,也可選擇不修改       
total_xml = os.listdir(xmlfilepath)
num=len(total_xml)    
list=range(num)    
tv=int(num*trainval_percent)    
tr=int(tv*train_percent)    
trainval= random.sample(list,tv)    
train=random.sample(trainval,tr)    
  
print("train and val size",tv)  
print("traub suze",tr)  
ftrainval = open(os.path.join(saveBasePath,'MyDataSet/ImageSets/Main/trainval.txt'), 'w')    
ftest = open(os.path.join(saveBasePath,'MyDataSet/ImageSets/Main/test.txt'), 'w')    
ftrain = open(os.path.join(saveBasePath,'MyDataSet/ImageSets/Main/train.txt'), 'w')    
fval = open(os.path.join(saveBasePath,'MyDataSet/ImageSets/Main/val.txt'), 'w')    
  
for i  in list:    
    name=total_xml[i][:-4]+'\n'    
    if i in trainval:    
        ftrainval.write(name)    
        if i in train:    
            ftrain.write(name)    
        else:    
            fval.write(name)    
    else:    
        ftest.write(name)    
    
ftrainval.close()    
ftrain.close()    
fval.close()    
ftest .close() 

上述代碼修改之後,在根目錄caffe-Mobilenet-YOLO-master執行上述代碼即可,
在data/VOCdevkit/MyDataSet/ImageSets下生成trainval.txt、test.txt、train.txt、val.txt等所需的txt文件,如下圖所示:

這些TXT文件會包含圖片的名字,不帶路徑,如下圖所示:

2.2 lmdb文件生成

【1】執行如下命令,將生成lmdb所需的腳本複製至data/VOCdevkit/MyDataSet文件夾內:

cp data/VOC0712/create_* data/MyDataSet/                # 把create_list.sh和create_data.sh複製到MyDataSet目錄                  
cp data/VOC0712/labelmap_voc.prototxt data/MyDataSet/   # 把labelmap_voc.prototxt複製到MyDataSet目錄 

【2】修改create_list.sh文件:

1 第3行修改目錄路徑,截止到VOCdevkit即可

2 第13行修改為for name in MyDataSet(VOCdevkit下自己建立的文件夾名字)

3 第15-18行註釋掉

4 第41行get_image_size修改為自己的路徑(注意,這裡是build caffe_mobilenet_yolo之後才會形成的):

#!/bin/bash
# 如果嚴格安裝我上述的步驟,就可以不用修改路徑位置。
# 需要修改的位置也使用註釋進行了標註和解釋

# 這裏需要更改,你數據的根目錄位置,需要修改的地方!!!!
root_dir="/home/Documents/Caffe_Mobilenet_YOLO/data/VOCdevkit/"   
sub_dir=ImageSets/Main
bash_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for dataset in trainval test
do
  dst_file=$bash_dir/$dataset.txt
  if [ -f $dst_file ]
  then
    rm -f $dst_file
  fi
  for name in MyDataSet  # 如果你建立的不是MyDataSet,這裏需要修改為你自己的名字
  do
    # 這裏需要修改,註釋掉即可
    #if [[ $dataset == "test" && $name == "VOC2012" ]]
    #then
    #  continue
    #fi
    echo "Create list for $name $dataset..."
    dataset_file=$root_dir/$name/$sub_dir/$dataset.txt

    img_file=$bash_dir/$dataset"_img.txt"
    cp $dataset_file $img_file
    sed -i "s/^/$name\/JPEGImages\//g" $img_file
    sed -i "s/$/.jpg/g" $img_file

    label_file=$bash_dir/$dataset"_label.txt"
    cp $dataset_file $label_file
    sed -i "s/^/$name\/Annotations\//g" $label_file
    sed -i "s/$/.xml/g" $label_file

    paste -d' ' $img_file $label_file >> $dst_file

    rm -f $label_file
    rm -f $img_file
  done

  # Generate image name and size infomation.
  if [ $dataset == "test" ]
  then
    home/Documents/Caffe_Mobilenet_YOLO/caffe-MobileNet-YOLO-master/build/tools/get_image_size $root_dir $dst_file $bash_dir/$dataset"_name_size.txt"

【3】creat_data.sh修改:

1 第2行修改為自己的路徑:root_dir=”/home/Documents/caffe-MobileNet-YOLO-master/”

2 第7行修改為:data_root_dir=”/home/Documents/caffe-MobileNet-YOLO-master/data/VOVdevkit/

3 第8行修改為:dataset_name=”MyDataSet”

4 第9行修改為:mapfile=”\(root_dir/data/VOCdevkit/\)dataset_name/labelmap_voc.prototxt”

5 第26行修改為\(root_dir/data/VOCdevkit/\)dataset_name/$subset.txt

cur_dir=$(cd $( dirname ${BASH_SOURCE[0]} ) && pwd )
# 修改為自己的路徑
root_dir="/home/Documents/Caffe_Mobilenet_YOLO/caffe-MobileNet-YOLO-master/"

cd $root_dir

redo=1
# 這裏需要修改為自己的路徑
data_root_dir="/home/Documents/Caffe_Mobilenet_YOLO/caffe-MobileNet-YOLO-master/data/VOCdevkit/"
dataset_name="MyDataSet"  # 修改為自己的名字
mapfile="$root_dir/data/VOCdevkit/$dataset_name/labelmap_voc.prototxt"  # 修改為自己的路徑
anno_type="detection"
db="lmdb"
min_dim=0
max_dim=0
width=0
height=0

extra_cmd="--encode-type=jpg --encoded"
if [ $redo ]
then
  extra_cmd="$extra_cmd --redo"
fi
for subset in test trainval
# subset.txt路徑需要修改
do
  python $root_dir/scripts/create_annoset.py --anno-type=$anno_type \
  --label-map-file=$mapfile --min-dim=$min_dim --max-dim=$max_dim --resize-width=$width \
  --resize-height=$height --check-label $extra_cmd $data_root_dir $root_dir/data/VOCdevkit/$dataset_name/$subset.txt \
  $data_root_dir/$dataset_name/$db/$dataset_name"_"$subset"_"$db examples/$dataset_name

【3】修改labelmap_voc.prototxt文件:

除了第一個背景標籤部分不要修改,其他改成自己的標籤就行,多的刪掉,少了添加進入就行

【4】最後在caffe-MobileNet-YOLO-master/examples文件夾內新建一個MyDataSet文件夾(空的)

【5】運行create_list.sh腳本: ./data/VOCdevkit/MyDataSet/create_list.sh,運行完后,會在自己建的VOCdevkit/MyDataSet/目錄內生成trainval.txt, test.txt, test_name_size.txt。

【6】運行create_data.sh腳本: ./data/VOCdevkit/MyDataSet/create_data.sh

運行此命令時,提示:bash:./data/VOCdevkit/MyDataSet/create_list.sh:Permission denied,沒有權限,需要執行如下命令賦予執行命令:

chmod u+x data/VOCdevkit/MyDataSet/create_data.sh

出現了錯誤:ValueError: need more than 2 values to unpack,

需要將create_annoset.py中第88行的seg去掉,因為我們的Annotations只有兩個值,img_file和anno。

運行完后,會在會在自己建的VOCdevkit/MyDataSet目錄內生成lmdb文件夾:

lmdb對應訓練集和測試集的lmdb格式的文件夾:

***
好啦,今天的教程就到這裏,如有疑問,可關注公眾號【計算機視覺聯盟】私信我或留言交流!!

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

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

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

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

分類
發燒車訊

豐田:車輛全面由電池驅動前、須經歷2-3次技術突破

 

CNBC 9月5日報導,豐田汽車公司會長內山田竹志(Takeshi Uchiyamada)在接受專訪時表示,基於當前電池技術的侷限、他懷疑消費者會立即投向電動車的懷抱。內山田表示,豐田不是排斥電動車,但為了要提供足夠的續航力、電動車目前需要安裝許多電池並得花相當長的時間去充電,而且電池壽命也是一大問題。

內山田認為車輛全面由電池驅動之前、還須經歷2到3次的技術性突破才行。不過,他也坦承,隨著中國、美國等地鼓勵電動車發展的法令生效,汽車製造商若不推出電動車的話可能就會被淘汰出局,因此豐田已著手研發較好的電池技術。

吉利汽車控股旗下Volvo日前宣布,2019年起旗下所有新車將會是純電動或油電混合驅動。

根據DNV GL首度發布的「能源轉型展望」報告,受電動車滲透率持續上揚的影響,石油供應將在2020到2028年期間轉趨持平、隨後大幅下降,2034年將遭天然氣超越。這份報告預估電動車、內燃引擎車將在2022年達到「成本平價」,預估到2033年全球半數輕型新車銷售量都將是電動車。

特斯拉(Tesla)平價電動車「Model 3」不含獎勵計畫的售價為35,000美元起、電池續航力為345公里。

Thomson Reuters報導,嘉能可(Glencore)董事長Tony Hayward 於5月受訪時表示,電動車的快速進步意味著石油需求可能會在2040年以前觸頂,深海鑽油、加拿大油砂等高成本原油生產商恐將先被淘汰出局,擁有生產成本優勢的石油輸出國組織(OPEC)相對較不受衝擊。Hayward曾任英國石油公司(BP Plc)執行長。

嘉能可執行長Ivan Glasenberg指出,如果電動車在2035年拿下90到95%的市占率,全球年度銅需求量可望較目前的2,300萬噸呈現倍增。德國總理梅克爾(Angela Merkel)5月22日指出,鋰電池技術已經進步到可以讓電動車擁有1千公里的續航力、遠高於目前的200-300公里,德國必須大舉投資以確保產業繼續保有優勢。

戴姆勒(Daimler AG)董事長Deiter Zetsche 5月22日表示,預估到2022年旗下將有超過10款的純電動轎車系列。戴姆勒旗下全資子公司ACCUMOTIVE當日在德國卡門茨(Kamenz)為第二座電池工廠舉行奠基儀式、邀請梅克爾出席。這座工廠耗資5億歐元、預計在2018年年中正式營運。

(本文內容由授權使用。圖片出處:public domian CC0)

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

【其他文章推薦】

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

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

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

分類
發燒車訊

再擴大充電網路,特斯拉宣布將開始在市區設置充電站

 

除了擴建超級充電站、推出家用充電器安裝選擇,特斯拉(Tesla)近日又宣布將在美國一些大型城市內設置市區充電站,來提供給駕駛人更方便的充電選擇。

根據特斯拉透露,市區內的小型充電站將會先在芝加哥、波士頓推出,市區充電站僅能夠提供約72kW 的電力,大約是以往超級充電站120kW 功率的一半,這意味著充電時間會稍微變長,大約會需要45-50 分鐘來為汽車充電。

但在市區充電站中,電動車將不會受到超級充電站內那種「充電分流」的影響,即使另一輛車使用相鄰的充電器,雙方的充電功率也不會因為分流而降低,讓駕駛在高密度人口的城市內也能夠享受快速、實惠的充電。

 

Major increases in the Supercharger and Tesla urban charger network happening over the next several months

— Elon Musk (@elonmusk)

 

除此之外,為了讓駕駛人在汽車充電時不會無聊,特斯拉計畫將這些充電站設置在超級市場、購物中心和市中心的景點附近,讓駕駛能夠利用時間去採買或逛街。市區充電站的收費也將和超級充電站一致,價格遠比汽油的花費便宜的多。

特斯拉希望透過這些市區充電站的設置,能在家庭充電器或目的地級充電站之外,提供給駕駛一個新的選擇,並慢慢達成特斯拉遠大計劃的一部分——建立一個完整的充電網路,讓特斯拉駕駛能夠開車到達世界各地。

「我們會持續擴大充電網路,讓特斯拉駕駛無論身在何處,都能隨時找到可靠的方法取得電力。」

(合作媒體:。圖片出處:)

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

分類
發燒車訊

谷歌上網氣球公司組建機構 推進技術全面商用

  騰訊科技訊,谷歌公司的一家兄弟企業,正在研發利用高空氣球幫助偏僻地區的民眾上網。據外媒最新消息,這家公司最近成立了一個由電信內部人士組成的新的諮詢委員會,以幫助將其價格低廉的“互聯網氣球”變成一種商業服務。

  據國外媒體報道,谷歌母公司 Alphabet 在 2013 年推出了高空氣球上網計劃(Loon 計劃),該項目將創建一個將互聯網傳輸到偏遠地區的氣球網絡。經過幾年的發展,Loon 公司於去年夏天宣布,它相信這項技術已經證明了自己的能力,並表示將開始把這項技術轉變為一項對外推出的業務。

  要做到這一點,這家公司需要與世界各地的移動運營商合作,因此該公司已經聘請了三名有電信運營商經驗的人擔任顧問。

  Loon 首席執行官阿拉斯泰爾·韋斯特加斯(Alastair Westgarth)在發表於社交媒體 Medium 上的一篇文章中指出:“隨着 Loon 向商業企業過渡,公司希望與世界各地的移動運營商合作,我們正在通過一個新的諮詢委員會為我們的隊伍增添一些專業知識,該委員會彙集了在該行業擁有數十年經驗的頂級移動通信創新者。”

  事實上,這三位專家在電信領域帶來了相當多的經驗。克雷格·麥考因創辦麥克考蜂窩公司而聞名,該公司是一家開拓性的無線公司,最終被 AT??T收購。伊恩·斯莫爾目前是 Evernote 的首席執行官,曾擔任西班牙電信公司 Telefonica 的首席數據官。瑪妮·瓦爾登(MarniWalden)是 Verizon 的前執行副總裁兼全球媒體和新業務(Global Media and New Business)總裁。

  這三位專家只是 Loon 公司新機構的“創始成員”,所以如果有更多的人加入這個行列,外界無需感到震驚。鑒於 Loon 的重點是為偏遠或服務不足的地區提供互聯網接入,該公司可能需要擴大規模。目前的董事會成員包括來自美國和歐洲供應商的人員。該公司可能需要特別熟悉發展中國家和其他寬帶稀缺或不存在地區的顧問。

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

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

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

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

分類
發燒車訊

美國司法部:黑客侵入SEC數據庫 利用內幕信息獲利數百萬美元

美國證券交易委員會(SEC)主席傑伊·克萊頓(Jay Clayton)在華盛頓特區的公開會議上發表講話

  1 月 16 日消息,據外媒報道,美國聯邦檢察官周二公布了一項與國際股票內幕交易有關的指控,該案嫌疑人涉嫌侵入美國證券交易委員會(SEC)的 EDGAR 公司備案系統。

  據稱,在這起案件中,來自美國的詐騙犯在美國、俄羅斯和烏克蘭凈賺了 410 萬美元。通過利用 157 家公司的財報文件,這些嫌疑人得以在重大非公開信息公布前進行內幕交易獲利。這些文件大多是“測試文件”,由企業將其上傳到 SEC 網站上。

  周二,美國新澤西州聯邦檢察官克雷格·卡貝托(Craig Carpenito)與 SEC、聯邦調查局(FBI)和負責調查金融犯罪的美國特勤局(U.S. Secret Service)共同宣布了上述指控。

  這起國際股票內幕交易案涉及 9 人,作案時間從 2016 年 5 月持續到當年 10 月份。卡貝托在周二的新聞發布會上說,此案涉及數以千計有價值的私人商業文件。他說:“在侵入 EDGAR 系統后,黑客竊取了這些報告的草稿,隨後這些信息才向公眾公開。”

  這些文件包括季度財報、併購計劃等敏感新聞,犯罪分子可以在文件公開前查看,從而影響到個別公司的股價。被指控的黑客利用這些報告執行內幕交易,並將其出售給其他非法交易商。據卡貝托稱,一名內部交易員一天賺了 27 萬美元。

  卡貝托說,黑客使用通過电子郵件發送給 SEC 員工的惡意軟件。在 SEC 的電腦上安裝了該軟件后,他們將能夠從 EDGAR 系統收集到的信息發送到立陶宛的服務器上。在那裡,他們要麼利用這些信息,要麼將數據分發給其他罪犯。

  SEC 執法部門聯席負責人斯蒂芬妮·阿瓦基安(Stephanie Avakian)說,這些罪犯還竊取了發給三家通訊社的事先新聞稿,不過她沒有透露這些通訊社的名字。她還說,黑客利用多個經紀人賬戶來獲取非法收益。

  美國司法部指控兩名烏克蘭人入侵數據庫,他們是奧列克桑德爾·恭弘=恭弘=恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘 恭弘 恭弘雷門科(Oleksandr Ieremenko)和阿提姆·拉琴科(Artem Radchenko)。另外 7 個個人和實體也因非法信息交易在民事訴訟中被 SEC 起訴。

  與此同時,這一事件也引發了人們對 SEC 合併審計跟蹤數據庫 CAT 的擔憂。CAT 被用來記錄在美國的每筆交易和訂單,無論是股票還是期權,其目標是提供足夠的數據進行分析,以發現市場操縱和其他惡意行為。

  紐約證券交易所已要求 SEC 考慮限制 CAT 收集的數據數量,這些數據包括每日 580 億筆交易的數據,以及進行交易的個人詳細信息,包括他們的社會保障號碼和出生日期。

  2017 年 9 月,SEC 主席傑伊·克萊頓(Jay Clayton)在長篇聲明中宣布,EDGAR 數據庫遭到黑客攻擊。歐盟委員會表示,該數據庫在 2016 年被入侵,但直到 2017 年 8 月才被發現。

  克萊頓當時稱:“網絡安全對我們的市場運作至關重要,風險巨大,而且在很多情況下都是系統性的入侵。我們還必須認識到,包括 SEC 在內的公共和私營部門將會受到入侵。”

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

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

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

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