部落格

  • 理解MySQL數據庫事務-隔離性

    理解MySQL數據庫事務-隔離性

    Transaction事務是指一個邏輯單元,執行一系列操作的SQL語句。

    事務中一組的SQL語句,要麼全部執行,要麼全部回退。在Oracle數據庫中有個名字,叫做transaction ID

    在關係型數據庫中,事務必須ACID的特性。

    • 原子性,事務中的操作,要不全部執行,要不都不執行
    • 一致性,事務完成前後,數據的必須保持一致。
    • 隔離性,多個用戶併發訪問數據庫時,每一個用戶開啟的事務,相互隔離,不被其他事務的操作所干擾。
    • 持久性,事務一旦commit,它對數據庫的改變是持久性的。

    目前重點討論隔離性。數據庫一共有四個隔離級別

    • 未提交讀(RU,Read Uncommitted)。它能讀到一個事物的中間狀態,不符合業務中安全性的保證,違背 了ACID特性,存在臟讀的問題,基本不會用到,可以忽略

    • 提交讀(RC,Read Committed)。顧名思義,事務提交之後,那麼我們可以看到。這是一種最普遍的適用的事務級別。我們生產環境常用的使用級別。

    • 可重複讀(RR,Repeatable Read)。是目前被使用得最多的一種級別。其特點是有GAP鎖,目前還是默認級別,這個級別下會經常發生死鎖,低併發等問題。

    • 可串行化,這種實現方式,其實已經是不是多版本了,而是單版本的狀態,因為它所有的實現都是通過鎖來實現的。

    因此目前數據庫主流常用的是RCRR隔離級別。

    隔離性的實現方式,我們通常用Read View表示一個事務的可見性。

    RC級別,事務可見性比較高,它可以看到已提交的事務的所有修改。因此在提交讀(RC,Read Committed)隔離級別下,每一次select語句,都會獲取一次Read View,得到數據庫最新的事務提交狀態。因此對於數據庫,併發性能也最好。

    RR級別,則不是。它為了避免幻讀和不可重複讀。保證在一個事務內前後數據讀取的一致。其可見性視圖Read View只有在自己當前事務提交之後,才會更新。

    那如何保證數據的一致性?其核心是通過redo logundo log來保證的。

    而在數據庫中,為了實現這種高併發訪問,就需要對數據庫進行多版本控制,通過事務的可見性來保證事務看到自己想看到的那個數據版本(或者是最新的Read View亦或者是老的Read View)。這種技術叫做MVCC

    多版本是如何實現的?通過undo日誌來保證。每一次數據庫的修改,undo日誌會存儲之前的修改記錄值。如果事務未提交,會回滾至老版本的數據。其MVCC的核心原理,以後詳談

    舉例論證:

    ##  開啟事務
    MariaDB [scott]> begin;                   
    Query OK, 0 rows affected (0.000 sec)
    
    ##查看當前的數據
    MariaDB [scott]>  select * from dept;
    +--------+------------+----------+
    | deptno | dname      | loc      |
    +--------+------------+----------+
    |     10 | ACCOUNTING | beijing  |
    |     20 | RESEARCH   | DALLAS   |
    |     30 | SALES      | CHICAGO  |
    |     40 | OPERATIONS | beijing  |
    |     50 | security   | beijing  |
    |     60 | security   | nanchang |
    +--------+------------+----------+
    6 rows in set (0.001 sec)
    
    ##更新數據
    MariaDB [scott]> update dept set loc ='beijing' where deptno = 20;
    Query OK, 1 row affected (0.001 sec)
    
    ## 其行記錄| 20 | RESEARCH | DALLAS |已經被放置在undo日誌中,目前最新的記錄被改為'beijing':
    MariaDB [scott]> select * from dept;
    +--------+------------+----------+
    | deptno | dname      | loc      |
    +--------+------------+----------+
    |     10 | ACCOUNTING | beijing  |
    |     20 | RESEARCH   | beijing  |
    |     30 | SALES      | CHICAGO  |
    |     40 | OPERATIONS | beijing  |
    |     50 | security   | beijing  |
    |     60 | security   | nanchang |
    +--------+------------+----------+
    
    ##事務不提交,回滾。數據回滾至老版本的數據。
    MariaDB [scott]> rollback;
    Query OK, 0 rows affected (0.004 sec)
    
    MariaDB [scott]> select * from dept;
    +--------+------------+----------+
    | deptno | dname      | loc      |
    +--------+------------+----------+
    |     10 | ACCOUNTING | beijing  |
    |     20 | RESEARCH   | DALLAS   |
    |     30 | SALES      | CHICAGO  |
    |     40 | OPERATIONS | beijing  |
    |     50 | security   | beijing  |
    |     60 | security   | nanchang |
    +--------+------------+----------+
    6 rows in set (0.000 sec)

    因為MVCC,讓數據庫有了很強的併發能力。隨着數據庫併發事務處理能力大大增強,從而提高了數據庫系統的事務吞吐量,可以支持更多的用戶併發訪問。但併發訪問,會出現帶來一系列問題。如下:

    數據庫併發帶來的問題 概述解釋
    臟讀(Dirty Reads) 當一個事務A正在訪問數據,並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,另外一個事務B也訪問這同一個數據,如不控制,事務B會讀取這些”臟”數據,並可能做進一步的處理。這種現象被稱為”臟讀”(Dirty Reads)
    不可重複讀(Non-Repeatable Reads) 指在一個事務A內,多次讀同一數據。在這個事務還沒有結束時,另外一個事務B也訪問該同一數據。那麼,在事務A的兩次讀數據之間,由於第二個事務B的修改,那麼第一個事務兩次讀到的的數據可能是不一樣的 。出現了”不可重複讀”(Non-Repeatable Reads)的現象
    幻讀(Phantom Reads) 指在一個事務A內,按相同的查詢條件重新檢索以前檢索過的數據,同時發現有其他事務插入了數據,其插入的數據滿足事務A的查詢條件。因此查詢出了新的數據,這種現象就稱為”幻讀”(Phantom Reads)

    隔離級別和上述現象之間的聯繫。

    隔離級別有:未提交讀(RU,Read Uncommitted),提交讀(RC,Read Committed),可重複讀(RR,Repeatable Read),可串行化(Serializable)

    隔離級別 臟讀 不可重複讀 幻讀
    未提交讀(RU,Read Uncommitted) 可能 可能 可能
    提交讀(RC,Read Committed) 不可能 可能 可能
    可重複讀(RR,Repeatable Read) 不可能 不可能 可能
    (間隙鎖解決)
    可串行化(Serializable) 不可能 不可能 不可能

    實驗環節

    舉例在隔離級別RRRC下,說明“不可重複讀”問題。

    MySQL的默認級別是Repeatable Read,如下:

    MariaDB [(none)]> select @@global.tx_isolation;
    +-----------------------+
    | @@global.tx_isolation |
    +-----------------------+
    | REPEATABLE-READ       |
    +-----------------------+
    1 row in set (0.000 sec)

    這裏修改當前會話級別為Read Committed

    MariaDB [scott]> set session transaction isolation level read committed;
    Query OK, 0 rows affected (0.001 sec)
    
    MariaDB [scott]> select @@tx_isolation;
    +----------------+
    | @@tx_isolation |
    +----------------+
    | READ-COMMITTED |
    +----------------+
    1 row in set (0.000 sec)

    在隔離級別已提交讀(RC,Read Committed)下,出現了不可重複讀的現象。在事務A中可以讀取事務B中的數據。

    在隔離級別可重複讀(RR,Repeatable Read),不會出現不可重複讀現象,舉例如下:

    舉例說明“幻讀”的現象。

    行鎖可以防止不同事務版本的數據在修改(update)提交時造成數據衝突的問題。但是插入數據如何避免呢?

    在RC隔離級別下,其他事務的插入數據,會出現幻讀(Phantom Reads)的現象。

    而在RR隔離級別下,會通過Gap鎖,鎖住其他事務的insert操作,避免”幻讀”的發生。

    因此,在MySQL事務中,鎖的實現方式與隔離級別有關,如上述實驗所示。在RR隔離級別下,MySQL為了解決幻讀的問題,已犧牲并行度為代價,通過Gap鎖來防止數據的寫入。這種鎖,并行度差,衝突多。容易引發死鎖。

    目前流行的Row模式可以避免很多衝突和死鎖問題,因此建議數據庫使用ROW+RC(Read Committed)模式隔離級別,很大程度上提高數據庫的讀寫并行度,提高數據庫的性能。

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

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

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

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

  • 應屆畢業生工作7個月小結

    應屆畢業生工作7個月小結

    前言: 不知不覺已經工作了快 7 個月了,去年這個時候還躋身在考研的大軍中,不禁有些感慨… 結合這 7 個月發生的一些事情,簡單做一下總結吧…

    為獲得更好的閱讀體驗,請訪問原文地址:

    一、那時候剛入職

    不同於其他同學忙於畢設的 4 月,提早安排趁寒假已經完成畢設的我,已經開始撲在了「找工作」這件事上,有了去年「秋招」打下的基礎,複習起來快了很多,沒過多久就開始投簡歷面試了,面試也總體比較順利,剛面沒幾家就迅速和一家自己看好的初創公司簽下了。

    公司使用的技術棧區別於自己熟悉的 Java/ MySQL 這一套,而是主要使用的 Rails/ MongoDB,所以剛入職的一段時間,基本上都是在自己熟悉技術棧,也趁着閑暇的時間,把自己入門時候的一些學習心得寫成了文章發表:

    • MongoDB【快速入門】:
    • Java轉Ruby【快速入門】:

    對於職場小白來說,所謂「職場」還是顯得有些陌生,剛來的時候,雖然跟周圍的同事都稀鬆平常地打了一圈兒招呼,坐下之後,隨着他們又埋頭噼里啪啦敲打鍵盤工作的深入,又頓覺周圍一片陌生,還挺奇妙的,在第一周完的周報裏面我寫道:

    剛來公司有些迷茫,只是看着CheckList對照着熟悉一些技術,也不了解自己應該要熟悉到哪種程度,就希望自己能再主動些,不管是技術問題還是其他問題多請教,然後儘快跟其他成員熟悉起來。

    剛開始上手的時候也有好多問題不懂,我都習慣性的選擇自己研究一陣兒,因為自己有寫博客的一些經歷,被問過好多一搜索 or 自己一嘗試就能解決的問題,所以比較克制,但是後來「入職 1v1」溝通的時候被說到有問題別自己死磕,半個小時沒解決盡量就找一下旁邊的同事。摁?我一下子就把我的「主動性」發揮了出來。

    不過好記性也不如爛筆頭,找了一些工具記錄把這些「問題的答案」都記錄了下來,方便之後再查找,當時對於 Git 都不是很熟悉,也記錄了很多常用的命令在裏面,還有一些問題的反饋,甚至知道了月會要自我介紹,也打了一遍草稿記錄在了這裏:(那段時間真的問了好多問題,周報里也手動感謝了坐我旁邊的兩位大佬..)

    入職兩周的時候,雖然已經開始上手做一些簡單的埋點工作,但自己對於 Ruby 還是不是特別了解和熟悉,趁着某一個雙休,抓着一本《Effetive-Ruby》啃了两天,也把自己的學習輸出了一下:

    • 《Effective-Ruby》讀書筆記:

    二、逐漸能夠上手

    就這樣一邊熟悉,一邊開始接一些小需求,我記得我寫下的第一個 BUG,就報出了 6K 條記錄.. 慌慌張張在修復之後我不禁感嘆:「不要太相信用戶的任何數據」。(包括 equal 反寫也是之後在錯誤之中學習到的..)

    剛上手沒有一段時間,就接到了一個新項目的需求,跟着一位大佬開發一個新功能,大佬負責搭建基礎代碼和設計,我負責完成其餘的功能代碼,沒敢一絲懈怠,下班回家之後也對照着別人寫的代碼敲敲敲,時間和完成度上倒是沒有一絲耽擱,只是現在回過頭一想,當時沒有什麼單元測試的概念和意識,就自己在本地 Post-Man 測試完就完,所幸比較簡單 + 自己測試得比較仔細,到現在也沒有出現過什麼問題。

    工作對我這樣的小白另一個好處就是:「見識和增加技術的廣度」。公司所使用技術棧不論是廣度還是深度,都是自己在大學本科的學習中不可企及的程度,Jekins?Docker?K8S?跳板機?一下子冒出來好多新鮮陌生的名詞,懷着好奇心也嘗試了解了一些:

    • 了解【Docker】從這裏開始:
    • 「消息隊列」看過來!:
    • Kafka【入門】就這一篇!:

    也隨着公司的逐漸壯大,各模塊的耦合也越發嚴重,各條業務線之間的協作溝通成本越來越大,逐漸開始提出「微服務」這樣的概念,具體怎麼樣理解就不作討論了,總之就是期望通過梳理/ 重構/ 拆服務的方式來解決「協作」問題,所以期間也開始了解學習一些這方面的東西:

    • 什麼是微服務?:
    • 《重構:改善既有代碼的設計》讀書筆記:

    甚至期間還做了一些「微服務」的調研,我們選用什麼樣的姿勢和技術棧更加合適,所以也輸出了一些關於「Spring Cloud」的東西,但是最終駁回的原因是待我們整個容器化之後 k8s 平台自帶了這麼一套東西,業務同學只需要關心業務代碼就行了,也就沒有繼續深入了:

    • 你想了解的「Spring Cloud」都在這裏:

    然後我們在拆解的過程中,也借鑒到一些「DDD」的思想,也嘗試進行了一波學習:

    • 【吐血推薦】領域驅動設計學習輸出:

    總之,這一段時間我一邊通過各種小需求,接觸和了解了公司的系統的大半,一邊學習和了解着各種不同的技術,增加了技術上的廣度。

    三、開始負責一些項目

    為了加速服務化的推進工作和驗證「DDD」的一些東西,部門老大把一個邊界足夠清晰,也足夠小的一個模塊單獨交給我,期望我快速上線,不過最終交付已經逾期快大半個月了.. 雖然從最終的結果來看,順利交付完成了拆解任務並從 MongoDB 數據庫轉變成了 MySQL.. 但期間也踩過好些坑,當然也學習到一些東西..

    例如我真實地意識到「完美」這個詞的理想化。就拿設計 API 來說吧.. 自己就基於 RESTful 風格設計了好幾版.. 左想右想都覺得差一些,有一些接口覺得怎麼設計都不優雅.. 後來糾結一陣子也就放棄了.. 再例如寫代碼這件事情吧,好的代碼整潔的代碼是一次一次迭代和重構中出來的,如果一開始就想着寫出「完美」的代碼,那麼最終的結果可能就是寫不出來代碼。

    另外一個小插曲是,在做數據遷移的時候,我差點把線上服務器搞掛了.. 我在測試環境驗證了一把之後,就直接在線上進行操作了,因為當時對於數據庫的操作管控還沒有那麼嚴格,加上自己對於線上環境的複雜程度認識不足,我就起了 50 個線程,去分批量地讀取 MongoDB 的數據遷移到 MySQL,造成了線上庫的性能報警,就趕緊停了.. 緊接着就被一群大佬抓進了一個會議室做事件的復盤..

    說實話,我緊張壞了,第一次經歷這樣的算是「事故」的情況吧,差一點線上就被我搞掛啦,一時間不知所措… 讓人感到溫暖的是部門老大隨即丟來的消息:

    那天還有一些相關的同事都陪我寫復盤郵件到了晚上 10:30,現在想來都十分感謝他們。後來回到家我還打電話給我媽,我說我在工作中犯錯了,我做了xxxx這些動作,你覺得我做的怎麼樣呢,老媽的回復也讓人安心,只是現在想來,一些後續的動作可以做得更好的…

    因為「埋點」這件事涉及到系統的方方面面,我也藉此了解了很多不同的模塊,也是拜這一點所賜吧,後來我被派到各種各樣的支援任務中,同樣也因為對不同模塊都還不算陌生,都還算完成得不錯吧…

    時間一晃,在公司就四個月過去了,也在這個過程中從各個大佬那兒都學到了一些東西,在 8 月底發的周報裏面我寫下了以下的總結:

    之後也跟着大佬碰了一些公司的核心模塊,期間也沒有停止在工作中不斷地做學習輸出:

    • Git 原理入門解析:
    • Java計時新姿勢√:
    • Java8流操作-基本使用&性能測試:
    • 《代碼整潔之道》讀書筆記:
    • React 入門學習:
    • 談一談依賴倒置原則:

    四、回顧做的不好的部分

    • 對代碼還沒有保持足夠的敬畏之心。

    特別是一開始上手的時候,有時候甚至是在線上環境搞測試,後來越來越注重 codereview 和單元測試好了很多。

    • 溝通還不夠深入/ 到位

    有一次是臨時接到一個需求,因為「通用語言」沒有達成一致,導致最終交付的結果不符合產品的期望,最終我們所有相關人員在一起開了一個會,統一了「通用語言」,造成了額外的工作和負擔,拿到需求就應該確認好相關事宜的,越底層越細節越好,這方面的能力我仍然欠缺,但我已經持續在注意當中。

    另一次也是因為這一點,我需要幫助 A 系統擁有某一項功能,之前 A 系統已經介入了 B 系統完成了部分功能,我因為沒有進一步地確認 B 系統的現狀,就去接入了有完整功能的 C 系統,但其實 B 系統已經在上一周和開發 C 系統和 A 系統的同學對接好了,並完成了相關功能的接入,少了這一部分的溝通,就造成了不少額外的工作量.. 所以「溝通」還是非常重要的,也只能說持續進步吧…

    • 缺少一些主動性

    當我頭上掛着一些事情的時候,還是能夠保持着效率的,只是當我做完了,就時常缺乏一些主動地思考了,通常都是被動地去詢問同小組的同事有什麼事情是需要幫忙的.. 雖然也积極地參与到自己感興趣的那些技術評審之類的事情之中,但似乎效果都不佳.. 也沒有什麼實際好的輸出..

    • 接了一些私活兒黑活兒(沒有充分考慮團隊之間的配合)

    因為「埋點」會接觸各個平台的童鞋,並且時常變化和有一些新的需求,有時候直接繞過了一些環節,直接找上我了,我心想直接自己弄弄改改就可以了,也就沒多想… 但是現在想來,這樣跨團隊的事情,不能越過「頂頭上司」私自進行,一方面經常我的 BOSS 不知道我接了活兒,另一方面這樣的私自對接就會造成一些信息的流失,對於團隊之內還是團隊之間都會造成影響…

    五、回顧做得好的部分

    • 養成了閱讀的習慣

    公司買書是免費的,也有自己的圖書館,同事也不乏喜歡閱讀學習的,所以跟着跟着就養成了閱讀的習慣,期間也學習到了一些方法論的東西,貼一下入職以來讀過的那些書吧:(技術類的就沒有囊括了)

    其實每天閱讀的時間也不長,想我大學總共捧起的那麼些課外書,不禁有些唏噓…

    • 早睡早起 + 晨間日記

    早睡早起,從步入職場以來,就發現這樣的習慣會帶來一些額外的價值,例如一些閱讀我會放在早上,後來還加入了「晨間日記」,用來「回顧前一天的事情」和提前部署「今天的任務」,這不禁讓我多了一份清醒,也讓現在不怎麼鍛煉的我每一天精力更加好一些:(目前正在從印象筆記往 Notion 逐步遷移的過程中)

    • 學習撰寫 Commit Message && 遵守一些 Git 規範

    起初使用 Git 十分不規範,後來向大佬那兒學習到了如何標準地提交 Commit,包括 Commit Message 應該怎麼寫,我覺得這是一個很好的習慣,每一個 Commit 都有上下文,並且還帶上了 JIRA 號,任務也很好跟蹤,雖然公司並沒有大範圍地盛行起來,但我覺得這樣好習慣應該堅持下來:

    • 任務進度及時反饋給相關人員

    自己比較注意這一點,因為不這樣做會讓別人感受不怎麼好.. 光是自己心裏清楚是不行的.. 要保持信息的通暢才行,及時反饋是很重要的一步..

    • 自己先 review 一遍代碼

    犯過一些白痴錯誤之後,就有些擔心,逐步養成了自己先 review 一遍代碼的習慣..

    六、小結 && 展望

    總的來說,看着自己這樣一步一步成長過來,沒有很懈怠,自己就算比較滿意了,在工作中學習了很多東西,不管是技術上的硬技能,還是溝通中的軟技能,也認識到了很多厲害的大佬和有趣的小夥伴們..

    感恩在路上相遇,有幸共同行走過一段已然算是幸運,突然翻看起自己的朋友圈有一句話說得好:「成長從來都不是告別過去,成長是更加堅定的看向未來!」

    期待一路同行的大家,都能夠 Be Better!

    按照慣例黏一個尾巴:

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

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

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

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

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

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

  • ThreadLocal深度解析和應用示例

    ThreadLocal深度解析和應用示例

    開篇明意

      ThreadLocal是JDK包提供的線程本地變量,如果創建了ThreadLocal<T>變量,那麼訪問這個變量的每個線程都會有這個變量的一個副本,在實際多線程操作的時候,操作的是自己本地內存中的變量,從而規避了線程安全問題。

      ThreadLocal很容易讓人望文生義,想當然地認為是一個“本地線程”。其實,ThreadLocal並不是一個Thread,而是Thread的一個局部變量,也許把它命名ThreadLocalVariable更容易讓人理解一些。

      來看看官方的定義:這個類提供線程局部變量。這些變量與正常的變量不同,每個線程訪問一個(通過它的get或set方法)都有它自己的、獨立初始化的變量副本。ThreadLocal實例通常是類中的私有靜態字段,希望將狀態與線程關聯(例如,用戶ID或事務ID)。

    源碼解析

      1.核心方法之   set(T t)

     1     /**
     2      * Sets the current thread's copy of this thread-local variable
     3      * to the specified value.  Most subclasses will have no need to
     4      * override this method, relying solely on the {@link #initialValue}
     5      * method to set the values of thread-locals.
     6      *
     7      * @param value the value to be stored in the current thread's copy of
     8      *        this thread-local.
     9      */
    10     public void set(T value) {
    11         Thread t = Thread.currentThread();
    12         ThreadLocalMap map = getMap(t);
    13         if (map != null)
    14             map.set(this, value);
    15         else
    16             createMap(t, value);
    17     }

    解析:

      當調用ThreadLocal的set(T t)的時候,代碼首先會獲取當前線程的 ThreadLocalMap(ThreadLocal中的靜態內部類,同時也作為Thread的成員變量存在,後面會進一步了解ThreadLocalMap),如果ThreadLocalMap存在,將ThreadLocal作為map的key,要保存的值作為value來put進map中(如果map不存在就先創建map,然後再進行put);

      2.核心方法值 get()

    /**
         * Returns the value in the current thread's copy of this
         * thread-local variable.  If the variable has no value for the
         * current thread, it is first initialized to the value returned
         * by an invocation of the {@link #initialValue} method.
         *
         * @return the current thread's value of this thread-local
         */
        public T get() {
           Thread t = Thread.currentThread();
           ThreadLocalMap map = getMap(t);        //此處和set方法一致,也是通過當前線程獲取對應的成員變量ThreadLocalMap,map中存放的是Entry(ThreadLocalMap的內部類(繼承了弱引用))
        
    if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);
          if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
          }
        }
        return setInitialValue();
    }

    解析:

      剛才把對象放到set到map中,現在根據key將其取出來,值得注意的是這裏的map裏面存的可不是鍵值對,而是繼承了WeakReference<ThreadLocal<?>> 的Entry對象,關於ThreadLocalMap.Entry類,後面會有更加詳盡的講述。

    核心方法之  remove()

        /**
         * Removes the current thread's value for this thread-local
         * variable.  If this thread-local variable is subsequently
         * {@linkplain #get read} by the current thread, its value will be
         * reinitialized by invoking its {@link #initialValue} method,
         * unless its value is {@linkplain #set set} by the current thread
         * in the interim.  This may result in multiple invocations of the
         * {@code initialValue} method in the current thread.
         *
         * @since 1.5
         */
         public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }

    解析:

      通過getMap方法獲取Thread中的成員變量ThreadLocalMap,在map中移除對應的ThreadLocal,由於ThreadLocal(key)是一種弱引用,弱引用中key為空,gc會回收變量value,看一下核心的m.remove(this);方法

            /**
             * Remove the entry for key.
             */
            private void remove(ThreadLocal<?> key) {
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1); //定義Entry在數組中的標號 for (Entry e = tab[i];              //通過循環的方式remove掉Thread中所有的Entry
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {   
                    if (e.get() == key) {
                        e.clear();
                        expungeStaleEntry(i);
                        return;
                    }
                }
            } 

     

     

     

    靈魂提問

      問:threadlocal是做什麼用的,用在哪些場景當中?  

        結合官方對ThreadLocal類的定義,threadLocal主要滿足某些變量或者示例是線程隔離的,但是在相同線程的多個類或者方法中都能使用的到,並且當線程結束時該變量也應該銷毀。通俗點講:ThreadLocal保證每個線程有自己的數據副本,當線程結束后可  以獨立回收。由於ThreadLocal的特性,同一線程在某地方進行設置,在隨後的任意地方都可以獲取到。從而可以用來保存線程上下文信息。常用的比如每個請求怎麼把一串後續關聯起來,就可以用ThreadLocal進行set,在後續的任意需要記錄日誌的方法裏面進行get獲取到請求id,從而把整個請求串起來。     使用場景有很多,比如:

    • 基於用戶請求線程的數據隔離(每次請求都綁定userId,userId的值存在於ThreadLoca中)
    • 跟蹤一個請求,從接收請求,處理到返回的整個流程,有沒有好的辦法   思考:微服務中的鏈路追蹤是否利用了ThreadLocal特性
    • 數據庫的讀寫分離
    • 還有比如Spring的事務管理,用ThreadLocal存儲Connection,從而各個DAO可以獲取同一Connection,可以進行事務回滾,提交等操作。

        
    問:如果我啟動另外一個線程。那麼在主線程設置的Threadlocal值能被子線程拿到嗎?     原始的ThreadLocal是不具有繼承(或者說傳遞)特性的     
    問:那該如何解決ThreadLocal無法傳遞的問題呢?     用ThreadLocal的子類 InheritableThreadLocal,InheritableThreadLocal是具有傳遞性的

      /**
      * 重寫Threadlocal類中的getMap方法,在原Threadlocal中是返回
      * t.theadLocals,而在這麼卻是返回了inheritableThreadLocals,因為
      * Thread類中也有一個要保存父子傳遞的變量
      */ ThreadLocalMap getMap(Thread t) {
    return t.inheritableThreadLocals; }
        /**  * 同理,在創建ThreadLocalMap的時候不是給t.threadlocal賦值  *而是給inheritableThreadLocals變量賦值  *  */
        void createMap(Thread t, T firstValue) {
            t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
        }

    解析:因為InheritableThreadLocal重寫了ThreadLocal中的getMap 和createMap方法,這兩個方法維護的是Thread中的另外一個成員變量  inheritableThreadLocals,線程在創建的時候回複製inheritableThreadLocals中的值 ;

    /* ThreadLocal values pertaining to this thread. This map is maintained
         * by the ThreadLocal class. */
      //Thread類中維護的成員變量,ThreadLocal會維護該變量
    ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */
    //Thread中維護的成員變量 ,
    InheritableThreadLocal 中維護該變量
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;


     

    //Thread init方法中的關鍵代碼,簡單來說是將父類中inheritableThreadLocals中的值拷貝到當前線程的inheritableThreadLocals中(淺拷貝,拷貝的是value的地址引用)
     if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

    總結

    • ThreadLocal類封裝了getMap()、Set()、Get()、Remove()4個核心方法。
    • 通過getMap()獲取每個子線程Thread持有自己的ThreadLocalMap實例, 因此它們是不存在併發競爭的。可以理解為每個線程有自己的變量副本。
    • ThreadLocalMap中Entry[]數組存儲數據,初始化長度16,後續每次都是1.5倍擴容。主線程中定義了幾個ThreadLocal變量,Entry[]才有幾個key。
    • Entry的key是對ThreadLocal的弱引用,當拋棄掉ThreadLocal對象時,垃圾收集器會忽略這個key的引用而清理掉ThreadLocal對象, 防止了內存泄漏。

        tips:上面四個總結來源於其他技術博客,個人認為總結的比較合理所以直接摘抄過來了

    拓展:

      ThreadLocal在線程池中使用容易發生的問題: 內存泄漏,先看下圖

      

      每個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal實例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal實例置為null以後,沒有任何強引用指向threadlocal實例,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連接過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收.

      所以得出一個結論就是只要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設為null和線程結束這段時間不會被回收的,就發生了我們認為的內存泄露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是線程對象不被回收的情況,這就發生了真正意義上的內存泄露。比如使用線程池的時候,線程結束是不會銷毀的,會再次使用的。就可能出現內存泄露。  

      PS.Java為了最小化減少內存泄露的可能性和影響,在ThreadLocal的get,set的時候都會清除線程Map里所有key為null的value。所以最怕的情況就是,threadLocal對象設null了,開始發生“內存泄露”,然後使用線程池,這個線程結束,線程放回線程池中不銷毀,這個線程一直不被使用,或者分配使用了又不再調用get,set方法,那麼這個期間就會發生真正的內存泄露。 

     

    1. JVM利用設置ThreadLocalMap的Key為弱引用,來避免內存泄露。
    2. JVM利用調用remove、get、set方法的時候,回收弱引用。
    3. 當ThreadLocal存儲很多Key為null的Entry的時候,而不再去調用remove、get、set方法,那麼將導致內存泄漏。
    4. 當使用static ThreadLocal的時候,延長ThreadLocal的生命周期,那也可能導致內存泄漏。因為,static變量在類未加載的時候,它就已經加載,當線程結束的時候,static變量不一定會回收。那麼,比起普通成員變量使用的時候才加載,static的生命周期加長將更容易導致內存泄漏危機。

     

      參考鏈接:

     

    在線程池中使用ThreadLocal

    通過上面的分析可以知道InheritableThreadLocal是通過Thread()的inint方法實現父子之間的傳遞的,但是線程池是統一創建線程並實現復用的,這樣就好導致下面的問題發生:

    •   線程不會銷毀,ThreadLocal也不會被銷毀,這樣會導致ThreadLoca會隨着Thread的復用而復用
    •   子線程無法通過InheritableThreadLocal實現傳遞性(因為沒有單獨的調用Thread的Init方法進行map的複製),子線程中get到的是null或者是其他線程復用的錯亂值(疑問點還沒搞清楚原因,後續補充::在異步線程中會出現null的情況,同步線程不會出現)     

        ps:線程池中的線程是什麼時候創建的?

     

      解決方案:

        下面兩個鏈接有詳細的說明,我就不重複寫了,後續我會將本文進一般優化並添加一些例子來幫助說明,歡迎收藏,關於本文有不同的意見歡迎評論指正……

        

        

     

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

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

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

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

  • 【併發編程】Java中的原子操作

    【併發編程】Java中的原子操作

    什麼是原子操作

    原子操作是指一個或者多個不可再分割的操作。這些操作的執行順序不能被打亂,這些步驟也不可以被切割而只執行其中的一部分(不可中斷性)。舉個列子:

    //就是一個原子操作
    int i = 1;
    
    //非原子操作,i++是一個多步操作,而且是可以被中斷的。
    //i++可以被分割成3步,第一步讀取i的值,第二步計算i+1;第三部將最終值賦值給i
    i++;

    Java中的原子操作

    在Java中,我們可以通過同步鎖或者CAS操作來實現原子操作。

    CAS操作

    CAS是Compare and swap的簡稱,這個操作是硬件級別的操作,在硬件層面保證了操作的原子性。CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什麼都不做。Java中的sun.misc.Unsafe類提供了compareAndSwapIntcompareAndSwapLong等幾個方法實現CAS。

    另外,在jdk的atomic包下面提供了很多基於CAS實現的原子操作類,見下圖:

    下面我們就使用其中的AtomicInteger來看看怎麼使用這些原子操作類。

    package com.csx.demo.spring.boot.concurrent.atomic;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class AtomicIntegerDemo {
    
        private static int THREAD_COUNT = 100;
    
        public static void main(String[] args) throws InterruptedException {
    
    
            NormalCounter normalCounter = new NormalCounter("normalCounter",0);
            SafeCounter safeCounter = new SafeCounter("safeCounter",0);
            List<Thread> threadList = new ArrayList<>();
    
            for (int i = 0; i < THREAD_COUNT ; i++) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int j = 0; j < 10000; j++) {
                            normalCounter.add(1);
                            safeCounter.add(1);
                        }
                    }
                });
                threadList.add(thread);
            }
    
            for (Thread thread : threadList) {
                thread.start();
            }
            for (Thread thread : threadList) {
                thread.join();
            }
            System.out.println("normalCounter:"+normalCounter.getCount());
            System.out.println("safeCounter:"+safeCounter.getCount());
        }
    
    
        public static class NormalCounter{
            private String name;
            private Integer count;
    
            public NormalCounter(String name, Integer count) {
                this.name = name;
                this.count = count;
            }
    
            public void add(int delta){
                this.count = count+delta;
            }
    
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
    
            public Integer getCount() {
                return count;
            }
    
            public void setCount(Integer count) {
                this.count = count;
            }
        }
    
        public static class SafeCounter{
            private String name;
            private AtomicInteger count;
    
            public SafeCounter(String name, Integer count) {
                this.name = name;
                this.count = new AtomicInteger(count);
            }
    
            public void add(int delta){
                count.addAndGet(delta);
            }
    
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
    
            public int getCount() {
                return count.get();
            }
    
            public void setCount(Integer count) {
                this.count.set(count);
            }
        }
    
    }

    上面的代碼中,我們分別創建了一個普通的計數器和一個原子操作的計數器(使用AtomicInteger進行計數)。然後創建了100個線程,每個線程進行10000次計數。理論上線程執行完之後,計數器的值都是1000000,但是結果如下:

    normalCounter:496527
    safeCounter:1000000

    每次執行,普通計數器的值都是不一樣的,而使用AtomicInteger進行計數的計數器都是1000000。

    CAS操作存在的問題

    • ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

    從Java1.5開始JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置為給定的更新值。

    • 循環時間長開銷大:自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

    • 只能保證一個共享變量的原子操作:當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象里來進行CAS操作。

    使用鎖來保證原子操作

    還是以上面的列子為列,普通的計數器我們只需要在計數方法上加鎖就行了:

    public synchronized void  add(int delta){
      this.count = count+delta;
    }

    執行結果如下:

    normalCounter:1000000
    safeCounter:1000000

    兩個計數器都能拿到正確的結果

    CPU是怎麼實現原子操作的

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

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

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

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

  • 關於GC(中):Java垃圾回收相關基礎知識

    關於GC(中):Java垃圾回收相關基礎知識

    Java內存模型

    (圖源: )

    區域名 英文名 訪問權限 作用 備註
    程序計數器 Program Counter Register 線程隔離 標記待取的下一條執行的指令 執行Native方法時為空; JVM規範中唯一不會發生OutOfMemoryError的區域
    虛擬機棧 VM Stack 線程隔離 每個Java方法執行時創建,用於存儲局部變量表,操作棧,動態鏈接,方法出口等信息 方法執行的內存模型
    本地方法棧 Native Method Stack 線程隔離 Native方法執行時使用 JVM規範沒有強制規定,如Hotspot將VM和Native兩個方法棧合二為一
    Java堆 Java Heap 線程共享 存放對象實例 更好的回收內存 vs 更快的分配內存
    方法區 Method Area 線程共享 存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據 JVM規範不強制要求做垃圾收集
    運行時常量池 Runtime Constant Pool 線程共享 方法區的一部分
    直接內存 Direct Memory 堆外內存,通過堆的DirectByteBuffer訪問 不是運行時數據區的一部分,但也可能OutOfMemoryError

    對象的創建——new的時候發生了什麼

    討論僅限於普通Java對象,不包括數組和Class對象。

    1. 常量池查找類的常量引用,如果沒有先做類加載
    2. 分配內存,視堆內存是否是規整(由垃圾回收器是否具有壓縮功能而定)而使用“指針碰撞”或“空閑列表”模式
    3. 內存空間初始化為零值,可能提前在線程創建時分配TLAB時做初始化
    4. 設置必要信息,如對象是哪個類的示例、元信息、GC分代年齡等
    5. 調用<init>方法

    垃圾回收器總結

    垃圾回收,針對的都是堆。

    分代

    • 新生代:適合使用複製算法, 以下三個區一般佔比為8:1:1
      • Eden 新對象誕生區
      • From Survivor 上一次GC的倖存者(見“GC種類-minor GC”)
      • To Survivor 本次待存放倖存者的區域
    • 老年代:存活時間較久的,大小較大的對象,因此使用標記-整理或標記-清除算法比較合適
    • 永久代:存放類信息和元數據等不太可能回收的信息。Java8中被元空間(Metaspace)代替,不再使用堆,而是物理內存。

    分代的原因

    • 不同代的對象生命周期不同,可以針對性地使用不同的垃圾回收算法
    • 不同代可以分開進行回收

    回收算法

    名稱 工作原理 優點 缺點
    標記-清除 對可回收對對象做一輪標記,標記完成后統一回收被標記的對象 易於理解,內存利用率高 效率問題;內存碎片;分配大對象但無空間時提前GC
    複製 內存均分兩塊,只使用其中一塊。回收時將這一塊存活對象全部複製到另一塊 效率高 可用空間減少; 空間不夠時需老年代分配擔保
    標記-整理 對可回收對對象做一輪標記,標記完成后將存活對象統一左移,清理掉邊界外內存 內存利用率高 效率問題

    標記-X算法適用於老年代,複製算法適用於新生代。

    GC種類

    • Minor GC,只回收新生代,將Eden和From Survivor區的存活對象複製到To Survivor
    • Major GC,清理老年代。但因為伴隨着新生代的對象生命周期升級到老年代,一般也可認為伴隨着FullGC。
    • FullGC,整個堆的回收
    • Mixed GC,G1特有,可能會發生多次回收,可以參考

    垃圾回收器小結

    垃圾回收器名稱 特性 目前工作分代 回收算法 可否與Serial配合 可否與ParNew配合 可否與ParallelScavenge配合 可否與SerialOld配合 可否與ParallelOld配合 可否與CMS配合 可否與G1配合
    Serial 單線程 新生代 複製 Y N Y N/A
    ParNew 多線程 新生代 複製 N N Y N/A
    ParallelScavenge 多線程, 更關注吞吐量可調節 新生代 複製 N N Y N/A
    SerialOld 單線程 老年代 標記-整理 Y Y N N/A
    ParallelOld 多線程 老年代 標記-整理 N N Y N/A
    CMS 多線程,併發收集,低停頓。但無法處理浮動垃圾,標記-清除會產生內存碎片較多 老年代 標記-清除 Y Y N Y N/A
    G1 并行併發收集,追求可預測但回收時間,整體內存模型有所變化 新生代/老年代 整體是標記-整理,局部(兩Region)複製 N N N N N N

    在本系列的上一篇文章中,減少FullGC的方式是使用G1代替CMS,計劃在下一篇文章中對比CMS和G1的區別。

    理解GC日誌

    只舉比較簡單的例子,具體各項的格式視情況分析,不同回收器也會有差異。

    2019-11-22T10:28:32.177+0800: 60188.392: [GC (Allocation Failure) 2019-11-22T10:28:32.178+0800: 60188.392: [ParNew: 1750382K->2520K(1922432K), 0.0312604 secs] 1945718K->198045K(4019584K), 0.0315892 secs] [Times: user=0.09 sys=0.01, real=0.03 secs]

    開始時間-(方括號[)-發生區域(ParNew,命名和GC回收器有關)-回收前大小-回收后大小-(方括號])-GC前堆已使用容量-GC后堆已使用容量大小-回收時間-使用時間詳情(用戶態時間-內核時間-牆上時鐘時間)

    注意這裏沒有包括“2019-11-22T10:28:32.177+0800: 60188.392: [GC (Allocation Failure)”這部分的分析。

    可借鑒的編程模式

    對象分配的併發控制

    對象創建是很頻繁的,在線程共享的堆中會遇到併發的問題。兩種解決辦法:

    1. 同步鎖定:CAS+失敗重試,確保原子性
    2. 堆中預先給每個線程劃分一小塊內存區域——本地線程分配緩衝(TLAB),TLAB使用完並分配新的TLAB時才做同步鎖定。可看作1的優化。

    CAS: Conmpare And Swap,用於實現多線程同步的原子指令。 將內存位置的內容與給定值進行比較,只有在相同的情況下,將該內存位置的內容修改為新的給定值。關於CAS可以參考:

    對象訪問的定位方式

    前提條件:通過上本地變量表的reference訪問中的對象及它在方法區的對象類型數據(類信息)
    主流的兩種方式,這兩種方式各有優點,可以看出方式2是方式1的優化,但並不是全面超越方式1,無法完全取代。
    這裏可以看到要權衡垃圾回收和訪問速度兩方面。

    方式1: 直接指針訪問實例數據

    reference直接存放對象實例地址,只需要一次訪問即可,執行效率較高。

    方式2: 使用句柄池

    reference中地址穩定,對象被移動時只需要改句柄池的地址。相對的,訪問實例需要兩次指針定位。

    參考資料

    1. 周志明.著《深入理解JAVA虛擬機》

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

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

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

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

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

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

  • 三個月(敏捷)項目收穫

    三個月(敏捷)項目收穫

     

     

    項目背景

    客戶已有運行多年的官網老站(PC端),想在今年對老站進行一次UI全面更新、功能全部平移的升級,對接新的運營後端,然後建立官網小程序端且與官網PC端進行聯動,使得品牌自有渠道能夠更加全面化。

     

    挑戰

    • 時間緊。五月份進行Inception Workshop,確定項目交付範圍與架構方案。官網六月初開始開發,小程序八月份開始開發,整個項目九月中旬必須上線。
    • 系統集成和數據遷移。系統需要對接客戶的CRM,對接3個服務商,需要對老官網歷史數據(訂單、會員等)進行遷移。
    • 多團隊溝通。小程序設計稿由第三方提供,因此多出了溝通、確認的時間,以及把控第三方交付的時間,以避免交付進度的影響。

     

    迭代計劃

    Inception Workshop一結束,差不多就開始整理整個項目涉及的故事和技術卡,按照兩周一迭代進行迭代計劃安排並與客戶確認,每個迭代第一周周三安排跟客戶showcase上一周的預定的交付結果,得到反饋並安排進行改進。官網項目比較順利,改造自定義了一下SSR框架就能開始進行開發,並且因為歷史原因,還能享受到上一個項目遺留的一些福利,當然也少不了一些坑。

    小程序的時間比較緊,相當於整個複製了一遍官網的功能,主要是前端任務,後端可以復用官網後端,因此一開始就給團隊同學同步到整個項目的情況,讓大家有一個大概的心理準備。然後就是與官網類似的處理,整個交付內容進行迭代排期並與客戶確認,前期盡量能多做一些,避免後期怎麼努力都無法完成的囧鏡。

     

    項目進行時

    整個項目的過程中,PM會根據迭代完成情況靈活的找外援加入項目進行支援,以免交付延期。每日的站會(Standup Meeting)更新,讓團隊能對當前進度有一個大概的了解以及同步一些突發信息。定期的回顧會議(Retrospective Meeting)能暴露團隊內部問題,將風險扼殺於苗頭,鼓勵能為團隊帶來正向幫助的行為,及時停止不好的做法。

    迭代會議(IPM)能讓團隊對下一個迭代具體要做的事情有一個詳細的了解,進行大致的估點,以便check開發進度情況。技術人員定期的CodeReview成為一個大家交流的時段,發現風險,指出問題,互相提高,還可以幫助新人快速的融入團隊。

    根據團隊內部人員情況,可以定期進行一對一溝通,了解個人訴求或是給與近況反饋都是一個不錯的渠道。TL應考慮團隊內部人員提升自己的訴求,在一些安排上給與傾斜和鼓勵,發現問題也需要提前制止。

     

    不足之處

    • 後期對卡牆(Jira)的管理鬆懈。導致有些問題反覆修改,且丟失context
    • 項目對運營後台有一些的定製化配置,沒有提前準備運營需要了解的後台操作資料和培訓,導致後期花費大量精力幫助運營進行後台配置與更新
    • 人員(QA)變動頻繁。公司處於高速發展階段,項目經歷了4個QA,因此有些context可能丟失,測試不到位,導致項目上線出了一些低級問題。比如上線后發現部分瀏覽器有支付兼容問題
    • 甲乙方定位太角色化,不能站在專業角度評估客戶需求(項目做完感覺都一樣,客戶是爸爸)
    • 與第三方合作交付產物管控不到位,導致第三方設計稿的延遲影響到我們的交付計劃
    • 與客戶溝通的需求,後面有一些沒有進行郵件確認,導致交付驗收階段因一些需求上的問題產生不愉快(這個完全沒必要的)
    • 對第三方系統的了解不充分和集成系統的需求整理不清晰導致後續一系列的開發、測試都不到位,以致上線出了不可控的問題

     

    項目總結

    • 提前評估項目的風險點,且在項目進行過程中持續維護,後期安排足夠的時間進行調研與分析
    • 與第三方合作一定要有自己的規劃,並且將定好的規劃提前與第三方確認時間,然後派人提前專門細緻的了解第三方需求詳細點,確定好具體的業務場景,再來規劃己方與第三方的具體集成的點。此外,在進行的過程中,還應注意定時檢查合作進度,管控風險
    • 與客戶溝通的所有需求都要進行郵件的二次確認,一個是能夠對所有需求來源有所記錄,另一個是能避免後面的不必要的消耗
    • 開發管理不能鬆懈,盡量做到所有的改動都能有卡,能夠進行追溯
    • 對後期交付時所需要的資料提前準備,對需要進行培訓的人員提前約好時間進行溝通培訓

    在前端這塊的管理上,做的還不夠。前期經常codereview,然後效果都還不錯,讓我有了一些錯覺就是當前團隊趨於穩定,中後期即便是加班比較多,大家氣氛這塊我覺得都還好。不過在項目後期的時候,有些疏於管理,然後大家有些人也被分配到了其他項目,和同事們的交流不夠,沒有及時的顧及到一些個人情緒,這塊是可以加強的。

    作為一個Lead,不論同事是否還在一個項目都應該及時的去了解近況,給與自己力所能及的幫助,這樣才能產生向心力,以幫助一些比較迷茫的同學找到方向,看見燈塔。

    整個項目時間不長,得失還是挺多的,不論是管理還是技術上,都會有一些心得。然後項目的ROI還不錯,得到公司領導的肯定,最後客戶那邊的反饋也還不錯,算是對大家努力的一種認可。

    ps: 及時總結,靜心沉澱;如風少年,砥礪前行。

    如想了解更多,請移步

    歡迎關注我的公眾號 “和F君一起xx”

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

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

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

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

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

  • 附009.Kubernetes永久存儲之GlusterFS獨立部署

    附009.Kubernetes永久存儲之GlusterFS獨立部署

    一 前期準備

    1.1 基礎知識


    Heketi提供了一個RESTful管理界面,可以用來管理GlusterFS卷的生命周期。Heketi會動態在集群內選擇bricks構建所需的volumes,從而確保數據的副本會分散到集群不同的故障域內。同時Heketi還支持任意數量的ClusterFS集群。

    提示:本實驗基於glusterfs和Kubernetes分開部署,heketi管理glusterfs,Kubernetes使用heketi提供的API,從而實現glusterfs的永久存儲,,而非Kubernetes部署glusterfs。

    1.2 架構示意






    提示:本實驗Heketi僅管理單zone的glusterfs集群。

    1.3 相關規劃

































    主機 IP 磁盤 備註
    servera 172.24.8.41 sdb glusterfs節點
    serverb 172.24.8.42 sdb glusterfs節點
    serverc 172.24.8.43 sdb glusterfs節點
    heketi 172.24.8.44 Heketi主機










































    servera serverb serverc
    PV sdb1 sdb1 sdb1
    VG vg0 vg0 vg0
    LV datalv datalv datalv
    bricks目錄 /bricks/data /bricks/data /bricks/data

    1.4 其他準備


    所有節點NTP配置;

    所有節點添加相應主機名解析:

    172.24.8.41 servera

    172.24.8.42 serverb

    172.24.8.43 serverc

    172.24.8.44 heketi

    注意:若非必要,建議關閉防火牆和SELinux。

    二 規劃相應存儲卷

    2.1 劃分LVM

      1 [root@servera ~]# fdisk /dev/sdb				#創建lvm的sdb1,過程略
      2 [root@servera ~]# pvcreate /dev/sdb1			#使用/dev/vdb1創建PV
      3 [root@servera ~]# vgcreate vg0 /dev/sdb1			#創建vg
      4 [root@servera ~]# lvcreate -L 15G -T vg0/thinpool		#創建支持thin的lv池
      5 [root@servera ~]# lvcreate -V 10G -T vg0/thinpool -n datalv	#創建相應brick的lv
      6 [root@servera ~]# vgdisplay					#驗證確認vg信息
      7 [root@servera ~]# pvdisplay					#驗證確認pv信息
      8 [root@servera ~]# lvdisplay					#驗證確認lv信息



    提示:serverb、serverc類似操作,根據規劃需求創建完所有基於LVM的brick。

    三 安裝glusterfs

    3.1 安裝相應RPM源

      1 [root@servera ~]# yum -y install centos-release-gluster


    提示:serverb、serverc、client類似操作,安裝相應glusterfs源;

    安裝相應源之後,會在/etc/yum.repos.d/目錄多出文件CentOS-Storage-common.repo,內容如下:

      1 # CentOS-Storage.repo
      2 #
      3 # Please see http://wiki.centos.org/SpecialInterestGroup/Storage for more
      4 # information
      5 
      6 [centos-storage-debuginfo]
      7 name=CentOS-$releasever - Storage SIG - debuginfo
      8 baseurl=http://debuginfo.centos.org/$contentdir/$releasever/storage/$basearch/
      9 gpgcheck=1
     10 enabled=0
     11 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-Storage


    3.2 安裝glusterfs

      1 [root@servera ~]# yum -y install glusterfs-server


    提示:serverb、serverc類似操作,安裝glusterfs服務端。

    3.3 啟動glusterfs

      1 [root@servera ~]# systemctl start glusterd
      2 [root@servera ~]# systemctl enable glusterd



    提示:serverb、serverc類似操作,所有節點啟動glusterfs服務端;

    安裝完glusterfs之後建議exit退出終端重新登錄,從而可以補全glusterfs相關命令。

    3.4 添加信任池

      1 [root@servera ~]# gluster peer probe serverb
      2 peer probe: success.
      3 [root@servera ~]# gluster peer probe serverc
      4 peer probe: success.
      5 [root@servera ~]# gluster peer status		#查看信任池狀態
      6 [root@servera ~]# gluster pool list			#查看信任池列表




    提示:加信任池的操作,只需要在servera、serverb、serverc所有集群節點主機中的任意一台上面執行添加其他三個節點的操作即可。

    提示:若未關閉防火牆,在添加信任池之前必須放通防火牆相應規則,操作如下:

      1 [root@servera ~]# firewall­cmd ­­permanent ­­add­service=glusterfs
      2 [root@servera ~]# firewall­cmd ­­permanent ­­add­service=nfs
      3 [root@servera ~]# firewall­cmd ­­permanent ­­add­service=rpc­bind
      4 [root@servera ~]# firewall­cmd ­­permanent ­­add­service=mountd
      5 [root@servera ~]# firewall­cmd ­­permanent ­­add­port=5666/tcp
      6 [root@servera ~]# firewall­cmd ­­reload


    四 部署Heketi

    4.1 安裝heketi服務

      1 [root@heketi ~]# yum -y install centos-release-gluster
      2 [root@heketi ~]# yum -y install heketi heketi-client


    4.2 配置heketi

      1 [root@heketi ~]# vi /etc/heketi/heketi.json
      2 {
      3   "_port_comment": "Heketi Server Port Number",
      4   "port": "8080",					#默認端口
      5 
      6   "_use_auth": "Enable JWT authorization. Please enable for deployment",
      7   "use_auth": true,					#基於安全考慮開啟認證
      8 
      9   "_jwt": "Private keys for access",
     10   "jwt": {
     11     "_admin": "Admin has access to all APIs",
     12     "admin": {
     13       "key": "admin123"					#管理員密碼
     14     },
     15     "_user": "User only has access to /volumes endpoint",
     16     "user": {
     17       "key": "xianghy"					#普通用戶
     18     }
     19   },
     20 
     21   "_glusterfs_comment": "GlusterFS Configuration",
     22   "glusterfs": {
     23     "_executor_comment": [
     24       "Execute plugin. Possible choices: mock, ssh",
     25       "mock: This setting is used for testing and development.",	#用於測試
     26       "      It will not send commands to any node.",
     27       "ssh:  This setting will notify Heketi to ssh to the nodes.",	#ssh方式
     28       "      It will need the values in sshexec to be configured.",
     29       "kubernetes: Communicate with GlusterFS containers over",		#在GlusterFS由kubernetes創建時採用
     30       "            Kubernetes exec api."
     31     ],
     32     "executor": "ssh",
     33 
     34     "_sshexec_comment": "SSH username and private key file information",
     35     "sshexec": {
     36       "keyfile": "/etc/heketi/heketi_key",
     37       "user": "root",
     38       "port": "22",
     39       "fstab": "/etc/fstab"
     40     },
     41 ……
     42 ……
     43     "loglevel" : "warning"
     44   }
     45 }


    4.3 配置免秘鑰

      1 [root@heketi ~]# ssh-keygen -t rsa -q -f /etc/heketi/heketi_key -N ""
      2 [root@heketi ~]# chown heketi:heketi /etc/heketi/heketi_key
      3 [root@heketi ~]# ssh-copy-id -i /etc/heketi/heketi_key.pub root@servera
      4 [root@heketi ~]# ssh-copy-id -i /etc/heketi/heketi_key.pub root@serverb
      5 [root@heketi ~]# ssh-copy-id -i /etc/heketi/heketi_key.pub root@serverc


    4.4 啟動heketi

      1 [root@heketi ~]# systemctl enable heketi.service
      2 [root@heketi ~]# systemctl start heketi.service
      3 [root@heketi ~]# systemctl status heketi.service
      4 [root@heketi ~]# curl http://localhost:8080/hello		#測試訪問
      5 Hello from Heketi


    4.5 配置Heketi拓撲


           拓撲信息用於讓Heketi確認可以使用的存儲節點、磁盤和集群,必須自行確定節點的故障域。故障域是賦予一組節點的整數值,這組節點共享相同的交換機、電源或其他任何會導致它們同時失效的組件。必須確認哪些節點構成一個集群,Heketi使用這些信息來確保跨故障域中創建副本,從而提供數據冗餘能力,Heketi支持多個Gluster存儲集群。

    配置Heketi拓撲注意以下幾點:

    • 可以通過topology.json文件定義組建的GlusterFS集群;
    • topology指定了層級關係:clusters –> nodes –> node/devices –> hostnames/zone;
    • node/hostnames字段的manage建議填寫主機ip,指管理通道,注意當heketi服務器不能通過hostname訪問GlusterFS節點時不能填寫hostname;
    • node/hostnames字段的storage建議填寫主機ip,指存儲數據通道,與manage可以不一樣,生產環境管理網絡和存儲網絡建議分離;
    • node/zone字段指定了node所處的故障域,heketi通過跨故障域創建副本,提高數據高可用性質,如可以通過rack的不同區分zone值,創建跨機架的故障域;
    • devices字段指定GlusterFS各節點的盤符(可以是多塊盤),必須是未創建文件系統的裸設備。

      1 [root@heketi ~]# vi /etc/heketi/topology.json
      2 {
      3     "clusters": [
      4         {
      5             "nodes": [
      6                 {
      7                     "node": {
      8                         "hostnames": {
      9                             "manage": [
     10                                 "172.24.8.41"
     11                             ],
     12                             "storage": [
     13                                 "172.24.8.41"
     14                             ]
     15                         },
     16                         "zone": 1
     17                     },
     18                     "devices": [
     19                         "/dev/mapper/vg0-datalv"
     20                     ]
     21                 },
     22                 {
     23                     "node": {
     24                         "hostnames": {
     25                             "manage": [
     26                                 "172.24.8.42"
     27                             ],
     28                             "storage": [
     29                                 "172.24.8.42"
     30                             ]
     31                         },
     32                         "zone": 1
     33                     },
     34                     "devices": [
     35                         "/dev/mapper/vg0-datalv"
     36                     ]
     37                 },
     38                 {
     39                     "node": {
     40                         "hostnames": {
     41                             "manage": [
     42                                 "172.24.8.43"
     43                             ],
     44                             "storage": [
     45                                 "172.24.8.43"
     46                             ]
     47                         },
     48                         "zone": 1
     49                     },
     50                     "devices": [
     51                         "/dev/mapper/vg0-datalv"
     52                     ]
     53                 }
     54             ]
     55         }
     56     ]
     57 }
     58 
     59 [root@heketi ~]# echo "export HEKETI_CLI_SERVER=http://heketi:8080" >> /etc/profile.d/heketi.sh
     60 [root@heketi ~]# echo "alias heketi-cli='heketi-cli --user admin --secret admin123'" >> .bashrc
     61 [root@heketi ~]# source /etc/profile.d/heketi.sh
     62 [root@heketi ~]# source .bashrc
     63 [root@heketi ~]# echo $HEKETI_CLI_SERVER
     64 http://heketi:8080
     65 [root@heketi ~]# heketi-cli --server $HEKETI_CLI_SERVER --user admin --secret admin123 topology load --json=/etc/heketi/topology.json




    4.6 集群管理

      1 [root@heketi ~]# heketi-cli cluster list					#集群列表
      2 [root@heketi ~]# heketi-cli cluster info aa83b0045fafa362bfc7a8bfee0c24ad	#集群詳細信息
      3 Cluster id: aa83b0045fafa362bfc7a8bfee0c24ad
      4 Nodes:
      5 189ee41572ebf0bf1e297de2302cfb39
      6 46429de5666fc4c6cc570da4b100465d
      7 be0209387384299db34aaf8377c3964c
      8 Volumes:
      9 
     10 Block: true
     11 
     12 File: true
     13 [root@heketi ~]# heketi-cli topology info aa83b0045fafa362bfc7a8bfee0c24ad	#查看拓撲信息




      1 [root@heketi ~]# heketi-cli node list						#卷信息
      2 Id:189ee41572ebf0bf1e297de2302cfb39     Cluster:aa83b0045fafa362bfc7a8bfee0c24ad
      3 Id:46429de5666fc4c6cc570da4b100465d     Cluster:aa83b0045fafa362bfc7a8bfee0c24ad
      4 Id:be0209387384299db34aaf8377c3964c     Cluster:aa83b0045fafa362bfc7a8bfee0c24ad
      5 [root@heketi ~]# heketi-cli node info 189ee41572ebf0bf1e297de2302cfb39		#節點信息
      6 [root@heketi ~]# heketi-cli volume create --size=2 --replica=2			#默認為3副本的replica模式



      1 [root@heketi ~]# heketi-cli volume list						#卷信息
      2 [root@heketi ~]# heketi-cli volume info 7da55685ebeeaaca60708cd797a5e391
      3 [root@servera ~]# gluster volume info						#通過glusterfs節點查看


    4.7 測試驗證

      1 [root@heketi ~]# yum -y install centos-release-gluster
      2 [root@heketi ~]# yum -y install glusterfs-fuse					#安裝glusterfs-fuse
      3 [root@heketi ~]# mount -t glusterfs 172.24.8.41:vol_7da55685ebeeaaca60708cd797a5e391 /mnt



      1 [root@heketi ~]# umount /mnt
      2 [root@heketi ~]# heketi-cli volume delete 7da55685ebeeaaca60708cd797a5e391	#驗證完畢刪除



    參考:https://www.jianshu.com/p/1069ddaaea78

    https://www.cnblogs.com/panwenbin-logs/p/10231859.html

    五 Kubernetes動態掛載glusterfs

    5.1 StorageClass動態存儲


    kubernetes共享存儲provider模式:

    靜態模式(Static):集群管理員手工創建PV,在定義PV時設置後端存儲的特性;

    動態模式(Dynamic):集群管理員不需要手工創建PV,而是通過StorageClass的設置對後端存儲進行描述,標記為某種”類型(Class)”;此時要求PVC對存儲的類型進行說明,系統將自動完成PV的創建及與PVC的綁定;PVC可以聲明Class為””,說明PVC禁止使用動態模式。

    基於StorageClass的動態存儲供應整體過程如下圖所示:


    1. 集群管理員預先創建存儲類(StorageClass);
    2. 用戶創建使用存儲類的持久化存儲聲明(PVC:PersistentVolumeClaim);
    3. 存儲持久化聲明通知系統,它需要一個持久化存儲(PV: PersistentVolume);
    4. 系統讀取存儲類的信息;
    5. 系統基於存儲類的信息,在後台自動創建PVC需要的PV;
    6. 用戶創建一個使用PVC的Pod;
    7. Pod中的應用通過PVC進行數據的持久化;
    8. 而PVC使用PV進行數據的最終持久化處理。


    提示:關於Kubernetes的部署參考《附003.Kubeadm部署Kubernetes》。

    5.2 定義StorageClass


    關鍵字說明:

    • provisioner:表示存儲分配器,需要根據後端存儲的不同而變更;
    • reclaimPolicy: 默認即”Delete”,刪除pvc后,相應的pv及後端的volume,brick(lvm)等一起刪除;設置為”Retain”時則保留數據,若需刪除則需要手工處理;
    • resturl:heketi API服務提供的url;
    • restauthenabled:可選參數,默認值為”false”,heketi服務開啟認證時必須設置為”true”;
    • restuser:可選參數,開啟認證時設置相應用戶名;
    • secretNamespace:可選參數,開啟認證時可以設置為使用持久化存儲的namespace;
    • secretName:可選參數,開啟認證時,需要將heketi服務的認證密碼保存在secret資源中;
    • clusterid:可選參數,指定集群id,也可以是1個clusterid列表,格式為”id1,id2”;
    • volumetype:可選參數,設置卷類型及其參數,如果未分配卷類型,則有分配器決定卷類型;如”volumetype: replicate:3”表示3副本的replicate卷,”volumetype: disperse:4:2”表示disperse卷,其中‘4’是數據,’2’是冗餘校驗,”volumetype: none”表示distribute卷


    提示:關於glusterfs各種不同類型的卷見《004.RHGS-創建volume》。

      1 [root@k8smaster01 ~]# kubectl create ns heketi		#創建命名空間
      2 [root@k8smaster01 ~]# echo -n "admin123" | base64		#將密碼轉換為64位編碼
      3 YWRtaW4xMjM=
      4 [root@k8smaster01 ~]# mkdir -p heketi
      5 [root@k8smaster01 ~]# cd heketi/
      6 [root@k8smaster01 ~]# vi heketi-secret.yaml			#創建用於保存密碼的secret
      7 apiVersion: v1
      8 kind: Secret
      9 metadata:
     10   name: heketi-secret
     11   namespace: heketi
     12 data:
     13   # base64 encoded password. E.g.: echo -n "mypassword" | base64
     14   key: YWRtaW4xMjM=
     15 type: kubernetes.io/glusterfs
     16 [root@k8smaster01 heketi]# kubectl create -f heketi-secret.yaml	#創建heketi
     17 [root@k8smaster01 heketi]# kubectl get secrets -n heketi
     18 NAME                  TYPE                                  DATA   AGE
     19 default-token-5sn5d   kubernetes.io/service-account-token   3      43s
     20 heketi-secret         kubernetes.io/glusterfs               1      5s
     21 [root@kubenode1 heketi]# vim gluster-heketi-storageclass.yaml	#正式創建StorageClass
     22 apiVersion: storage.k8s.io/v1
     23 kind: StorageClass
     24 metadata:
     25   name: gluster-heketi-storageclass
     26 parameters:
     27   resturl: "http://172.24.8.44:8080"
     28   clusterid: "aa83b0045fafa362bfc7a8bfee0c24ad"
     29   restauthenabled: "true"					#若heketi開啟認證此處也必須開啟auth認證
     30   restuser: "admin"
     31   secretName: "heketi-secret"				#name/namespace與secret資源中定義一致
     32   secretNamespace: "heketi"
     33   volumetype: "replicate:3"
     34 provisioner: kubernetes.io/glusterfs
     35 reclaimPolicy: Delete
     36 [root@k8smaster01 heketi]# kubectl create -f gluster-heketi-storageclass.yaml



    注意:storageclass資源創建后不可變更,如修改只能刪除后重建。

      1 [root@k8smaster01 heketi]# kubectl get storageclasses		#查看確認
      2 NAME                          PROVISIONER               AGE
      3 gluster-heketi-storageclass   kubernetes.io/glusterfs   85s
      4 [root@k8smaster01 heketi]# kubectl describe storageclasses gluster-heketi-storageclass




    5.3 定義PVC

      1 [root@k8smaster01 heketi]# cat gluster-heketi-pvc.yaml
      2 apiVersion: v1
      3 metadata:
      4   name: gluster-heketi-pvc
      5   annotations:
      6     volume.beta.kubernetes.io/storage-class: gluster-heketi-storageclass
      7 spec:
      8   accessModes:
      9   - ReadWriteOnce
     10   resources:
     11     requests:
     12       storage: 1Gi



    注意:accessModes可有如下簡寫:

    • ReadWriteOnce:簡寫RWO,讀寫權限,且只能被單個node掛載;
    • ReadOnlyMany:簡寫ROX,只讀權限,允許被多個node掛載;
    • ReadWriteMany:簡寫RWX,讀寫權限,允許被多個node掛載。

      1 [root@k8smaster01 heketi]# kubectl create -f gluster-heketi-pvc.yaml
      2 [root@k8smaster01 heketi]# kubectl get pvc
      3 [root@k8smaster01 heketi]# kubectl describe pvc gluster-heketi-pvc
      4 [root@k8smaster01 heketi]# kubectl get pv
      5 [root@k8smaster01 heketi]# kubectl describe pv pvc-5f7420ef-082d-11ea-badf-000c29fa7a79




      1 [root@k8smaster01 heketi]# kubectl describe endpoints glusterfs-dynamic-5f7420ef-082d-11ea-badf-000c29fa7a79




    提示:由上可知:PVC狀態為Bound,Capacity為1G。查看PV詳細信息,除容量,引用storageclass信息,狀態,回收策略等外,同時可知GlusterFS的Endpoint與path。EndpointsName為固定格式:glusterfs-dynamic-PV_NAME,且endpoints資源中指定了掛載存儲時的具體地址。

    5.4 確認查看


    通過5.3所創建的信息:

    • volume與brick已經創建;
    • 主掛載點(通信)在172.24.8.41節點,其餘兩個節點備選;
    • 三副本的情況下,所有節點都會創建brick。

      1 [root@heketi ~]# heketi-cli topology info			#heketi主機查看
      2 [root@serverb ~]# lsblk						#glusterfs節點查看
      3 [root@serverb ~]# df -hT					#glusterfs節點查看
      4 [root@servera ~]# gluster volume list				#glusterfs節點查看
      5 [root@servera ~]# gluster volume info vol_e4c948687239df9833748d081ddb6fd5




    5.5 Pod掛載測試

      1 [root@xxx ~]# yum -y install centos-release-gluster
      2 [root@xxx ~]# yum -y install glusterfs-fuse					#安裝glusterfs-fuse



    提示:所有需要使用glusterfs volume的Kubernetes節點都必須安裝glusterfs-fuse以便於正常掛載,同時版本需要和glusterfs節點一致。

      1 [root@k8smaster01 heketi]# vi gluster-heketi-pod.yaml
      2 kind: Pod
      3 apiVersion: v1
      4 metadata:
      5   name: gluster-heketi-pod
      6 spec:
      7   containers:
      8   - name: gluster-heketi-container
      9     image: busybox
     10     command:
     11     - sleep
     12     - "3600"
     13     volumeMounts:
     14     - name: gluster-heketi-volume			#必須和volumes中name一致
     15       mountPath: "/pv-data"
     16       readOnly: false
     17   volumes:
     18   - name: gluster-heketi-volume
     19     persistentVolumeClaim:
     20       claimName: gluster-heketi-pvc			#必須和5.3創建的PVC中的name一致
     21 [root@k8smaster01 heketi]# kubectl create -f gluster-heketi-pod.yaml -n heketi		#創建Pod


    5.6 確認驗證

      1 [root@k8smaster01 heketi]# kubectl get pod -n heketi | grep gluster
      2 gluster-heketi-pod          1/1     Running   0          2m43s
      3 [root@k8smaster01 heketi]# kubectl exec -it gluster-heketi-pod /bin/sh		#進入Pod寫入測試文件
      4 / # cd /pv-data/
      5 /pv-data # echo "This is a file!" >> a.txt
      6 /pv-data # echo "This is b file!" >> b.txt
      7 /pv-data # ls
      8 a.txt  b.txt
      9 [root@servera ~]# df -hT					#在glusterfs節點查看Kubernetes節點的測試文件
     10 [root@servera ~]# cd /var/lib/heketi/mounts/vg_47c90d90e03de79696f90bd94cfccdde/brick_721243c3e0cf8a2372f05d5085a4338c/brick/
     11 [root@servera brick]# ls
     12 [root@servera brick]# cat a.txt
     13 [root@servera brick]# cat b.txt




    5.7 刪除資源

      1 [root@k8smaster01 heketi]# kubectl delete -f gluster-heketi-pod.yaml
      2 [root@k8smaster01 heketi]# kubectl delete -f gluster-heketi-pvc.yaml
      3 [root@k8smaster01 heketi]# kubectl get pvc
      4 [root@k8smaster01 heketi]# kubectl get pv
      5 [root@servera ~]# gluster volume list
      6 No volumes present in cluster



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

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

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

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

  • 正則表達式 第五篇:C# 正則表達式

    正則表達式 第五篇:C# 正則表達式

    本文整理C#正則表達式的元字符,正則表達式是由字符構成的表達式,每個字符代表一個規則,表達式中的字符分為兩種類型:普通字符和元字符。普通字符是指字面含義不變的字符,按照完全匹配的方式匹配文本,而元字符具有特殊的含義,代表一類字符。

    把文本看作是字符流,每個字符放在一個位置上,例如,正則表達式 “Room\d\d\d”,前面四個字符Room是普通字符,後面的字符\是轉義字符,和後面的字符d組成一個元字符\d,表示該位置上有任意一個数字。

    用正則表達式的語言來描述是:正則表達式 “Room\d\d\d”共捕獲7個字符,表示“以Room開頭、以三個数字結尾”的一類字符串,我們把這一類字符串稱作一個模式(Pattern),也稱作是一個正則。

    一,轉義字符

    轉義字符是\,把普通字符轉義為具有特殊含義的元字符,常用的轉義字符有:

    • \t:水平製表符
    • \v:垂直製表符
    • \r:回車
    • \n:換行
    • \\:表示字符 \,也就說,把轉義字符 \ 轉義為普通的字符 \
    • \”:表示字符 “,在C#中,雙引號用於定義字符串,字符串包含的雙引號用 \” 來表示

    二,字符類

    在進行正則匹配時,把輸入文本看作是有順序的字符流,字符類元字符匹配的對象是字符,並會捕獲字符。所謂捕獲字符是指,一個元字符捕獲的字符,不會被其他元字符匹配,後續的元字符只能從剩下的文本中重新匹配。

    常用的字符類元字符:

    • [ char_group]:匹配字符組中的任意一個字符
    • [^char_group]:匹配除字符組之外的任意一個字符
    • [first-last]:匹配從first到last的字符範圍中的任意一個字符,字符範圍包括first和last。
    • .   :通配符,匹配除\n之外的任意一個字符
    • \w:匹配任意一個單詞(word)字符,單詞字符通常是指A-Z、a-z和0-9
    • \W:匹配任意一個非單詞字符,是指除A-Z、a-z和0-9之外的字符
    • \s:匹配任意一個空白字符
    • \S:匹配任意一個非空白字符
    • \d:匹配任意一個数字字符
    • \D:匹配任意一個非数字字符

    注意,轉義字符也屬於字符類元字符,在進行正則匹配時,也會捕獲字符。

    三,定位符

    定位符匹配(或捕獲)的對象是位置,它根據字符的位置來判斷模式匹配是否成功,定位符不會捕獲字符,是零寬的(寬度為0),常用的定位符有:

    • ^:默認情況下,匹配字符串的開始位置;在多行模式下,匹配每行的開始位置;
    • $:默認情況下,匹配字符串的結束位置,或 字符串結尾的\n之前的位置;在多行模式下,匹配每行結束之前的位置,或者每行結尾的\n之前的位置。
    • \A:匹配字符串的開始位置;
    • \Z:匹配字符串的結束位置,或 字符串結尾的\n之前的位置;
    • \z:匹配字符串的結束位置;
    • \G:匹配上一個匹配結束的位置;
    • \b:匹配一個單詞的開始或結束的位置;
    • \B:匹配一個單詞的中間位置;

     

    四,量詞、貪婪和懶惰

    量詞是指限定前面的一個正則出現的次數,量詞分為兩種模式:貪婪模式和懶惰模式,貪婪模式是指匹配盡可能多的字符,而懶惰模式是指匹配盡可能少的字符。默認情況下,量詞處於貪婪模式,在量詞的後面加上?來啟用懶惰模式。

    • *:出現0次或多次
    • +:出現1次或多次
    • ?:出現0次或1次
    • {n}:出現n次
    • {n,}:出現至少n次
    • {n,m}:出現n到m次

    注意,出現多次是指前面的元字符出現多次,例如,\d{2} 等價於 \d\d,只是出現兩個数字,並不要求兩個数字是相同的。要表示相同的兩個数字,必須使用分組來實現。

    五,分組和捕獲字符

    ()  括號不僅確定表達式的範圍,還創建分組,()內的表達式就是一個分組,引用分組表示兩個分組匹配的文本是完全相同的。定義一個分組的基本語法:

    (pattern)

    該類型的分組會捕獲字符,所謂捕獲字符是指:一個元字符捕獲的字符,不會被其他元字符匹配,後續的元字符只能從剩下的文本中重新匹配。

    1,分組編號和命名

    默認情況下,每個分組自動分配一個組號,規則是:從左向右,按分組左括號的出現順序進行編號,第一個分組的組號為1,第二個為2,以此類推。也可以為分組指定名稱,該分組稱作命名分組,命名分組也會被自動編號,編號從1開始,逐個加1,為分組指定名稱的語法是:

    (?< name > pattern)

    通常來說,分組分為命名分組和編號分組,引用分組的方式有:

    • 通過分組名稱來引用分組:\k<name>
    • 通過分組編號來引用分組:\number

    注意,分組只能後向引用,也就是說,從正則表達式文本的左邊開始,分組必須先定義,然後才能在定義之後面引用。

    在正則表達式里引用分組的語法為“\number”,比如“\1”代表與分組1 匹配的子串,“\2”代表與分組2 匹配的字串,以此類推。

    例如,對於 “<(.*?)>.*?</\1>” 可以匹配 <h2>valid</h2>,在引用分組時,分組對應的文本是完全相同的。

    2,分組構造器

    分組構造方法如下:

    • (pattern):捕獲匹配的子表達式,併為分組分配一個組號
    • (?< name > pattern):把匹配的子表達式捕獲到命名的分組中
    • (?:pattern):非捕獲的分組,並未分組分配一個組號
    • (?> pattern):貪婪分組

    3,貪婪分組

    貪婪分組也稱作非回溯分組,該分組禁用了回溯,正則表達式引擎將盡可能多地匹配輸入文本中的字符。如果無法進行進一步的匹配,則不會回溯嘗試進行其他模式匹配。

    (?> pattern )

    4,二選一

    | 的意思是或,匹配兩者中的任意一個,注意,|把左右兩邊的表達式分為兩部分。

    pattern1 | pattern2

    六,零寬斷言

    零寬是指寬度為0,匹配的是位置,所以匹配的子串不會出現在匹配結果中,而斷言是指判斷的結果,只有斷言為真,才算匹配成功。

    對於定位符,可以匹配一句話的開始、結束(^ $)或者匹配一個單詞的開始、結束(\b),這些元字符只匹配一個位置,指定這個位置滿足一定的條件,而不是匹配某些字符,因此,它們被成為 零寬斷言。所謂零寬,指的是它們不與任何字符相匹配,而匹配一個位置;所謂斷言,指的是一個判斷,正則表達式中只有當斷言為真時才會繼續進行匹配。零寬斷言可以精確的匹配一個位置,而不僅僅是簡單的指定句子或者單詞。

    正則表達式把文本看作從左向右的字符流,向右叫做後向(Look behind),向左叫做前向(Look ahead)。對於正則表達式,只有當匹配到指定的模式(Pattern)時,斷言為True,叫做肯定式,把不匹配模式為True,叫做否定式。

    按照匹配的方向和匹配的定性,把零寬斷言分為四種類型:

    • (?= pattern):前向、肯定斷言
    • (?! pattern):前向、否定斷言
    • (?<= pattern):後向、肯定斷言
    • (?<! pattern):後向、否定斷言

     1,前向肯定斷言

    前向肯定斷言定義一個模式必須存在於文本的末尾(或右側),但是該模式匹配的子串不會出現在匹配的結果中,前向斷言通常出現在正則表達式的右側,表示文本的右側必須滿足特定的模式:

     (?= subexpression )

    使用前向肯定斷言可以定一個模糊匹配,後綴必須包含特定的字符:

    \b\w+(?=\sis\b)

    對正則表達式進行分析:

    • \b:表示單詞的邊界
    •  \w+:表示單詞至少出現一次
    • (?=\sis\b):前向肯定斷言,\s 表示一個空白字符, is 是普通字符,完全匹配,\b 是單詞的邊界。

    從分析中,可以得出,匹配該正則表達式的文本中必須包含 is 單詞,is是一個單獨的單詞,不是某一個單詞的一個部分。舉個例子

    Sunday is a weekend day 匹配該正則,匹配的值是Sunday,而The island has beautiful birds 不匹配該正則。

    2,後向肯定斷言

    後向肯定斷言定義一個模式必須存在於文本的開始(或左側),但是該模式匹配的子串不會出現在匹配的結果中,後向斷言通常出現在正則表達式的左側,表示文本的左側必須滿足特定的模式:

    (?<= subexpression )

    使用後向肯定斷言可以定一個模糊匹配,前綴必須包含特定的字符:

    (?<=\b20)\d{2}\b

    對正則表達式進行分析:

    • (?<=\b20):後向斷言,\b表示單詞的開始,20是普通字符
    • \d{2}:表示兩個数字,数字不要求相同
    • \b:單詞的邊界

    該正則表達式匹配的文本具備的模式是:文本以20開頭、以兩個数字結尾。

    七,用正則從格式化的文本中扣值

    有如下的JSON格式的文本,從文本中扣出字段(CustomerId、CustomerName、CustomerIdSource和CustomerType)的值:

    {"CustomerDetails":"[{\"CustomerId\":\"57512f19\",\"CustomerName\":\"cust xyz\",\"CustomerIdSource\":\"AadTenantId\",\"CustomerType\":\"Enterprise\"}]"}

    注意,該文本轉換為C#中的字符時,需要對雙引號和轉義字符進行轉義。由於這四個字段提取規則相同,可以寫一個通用的模式來提取:

    public static string GetNestedItem(string txt, string key)
    {
        string pat = string.Format("(?<=\\\\\"{0}\\\\\":\\\\\").*?(?=\\\\\")", key);
        return Regex.Match(txt, pat, RegexOptions.IgnoreCase).Value;
    }

    正則表達式得解析:

    • (?<=\\\\\”{0}\\\\\”:\\\\\”):後向斷言,用於匹配字段名、雙引號和冒號
    • .*?:懶惰模式,匹配盡可能少的文本
    • (?=\\\\\”):前向斷言,用於匹配字段值得雙引號

     

     

    參考文檔:

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

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

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

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

  • 【搞定 Java 併發面試】面試最常問的 Java 併發基礎常見面試題總結!

    【搞定 Java 併發面試】面試最常問的 Java 併發基礎常見面試題總結!

    本文為 SnailClimb 的原創,目前已經收錄自我開源的 中(61.5 k Star!【Java學習+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識。歡迎 Star!)。

    另外推薦一篇原創:

    Java 併發基礎常見面試題總結

    1. 什麼是線程和進程?

    1.1. 何為進程?

    進程是程序的一次執行過程,是系統運行程序的基本單位,因此進程是動態的。系統運行一個程序即是一個進程從創建,運行到消亡的過程。

    在 Java 中,當我們啟動 main 函數時其實就是啟動了一個 JVM 的進程,而 main 函數所在的線程就是這個進程中的一個線程,也稱主線程。

    如下圖所示,在 windows 中通過查看任務管理器的方式,我們就可以清楚看到 window 當前運行的進程(.exe 文件的運行)。

    1.2. 何為線程?

    線程與進程相似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享進程的方法區資源,但每個線程有自己的程序計數器虛擬機棧本地方法棧,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因為如此,線程也被稱為輕量級進程。

    Java 程序天生就是多線程程序,我們可以通過 JMX 來看一下一個普通的 Java 程序有哪些線程,代碼如下。

    public class MultiThread {
        public static void main(String[] args) {
            // 獲取 Java 線程管理 MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
            // 不需要獲取同步的 monitor 和 synchronizer 信息,僅獲取線程和線程堆棧信息
            ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
            // 遍歷線程信息,僅打印線程 ID 和線程名稱信息
            for (ThreadInfo threadInfo : threadInfos) {
                System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
            }
        }
    }

    上述程序輸出如下(輸出內容可能不同,不用太糾結下面每個線程的作用,只用知道 main 線程執行 main 方法即可):

    [5] Attach Listener //添加事件
    [4] Signal Dispatcher // 分發處理給 JVM 信號的線程
    [3] Finalizer //調用對象 finalize 方法的線程
    [2] Reference Handler //清除 reference 線程
    [1] main //main 線程,程序入口

    從上面的輸出內容可以看出:一個 Java 程序的運行是 main 線程和多個其他線程同時運行

    2. 請簡要描述線程與進程的關係,區別及優缺點?

    從 JVM 角度說進程和線程之間的關係

    2.1. 圖解進程和線程的關係

    下圖是 Java 內存區域,通過下圖我們從 JVM 的角度來說一下線程和進程之間的關係。如果你對 Java 內存區域 (運行時數據區) 這部分知識不太了解的話可以閱讀一下這篇文章:

    從上圖可以看出:一個進程中可以有多個線程,多個線程共享進程的方法區 (JDK1.8 之後的元空間)資源,但是每個線程有自己的程序計數器虛擬機棧本地方法棧

    總結: 線程 是 進程 劃分成的更小的運行單位。線程和進程最大的不同在於基本上各進程是獨立的,而各線程則不一定,因為同一進程中的線程極有可能會相互影響。線程執行開銷小,但不利於資源的管理和保護;而進程正相反

    下面是該知識點的擴展內容!

    下面來思考這樣一個問題:為什麼程序計數器虛擬機棧本地方法棧是線程私有的呢?為什麼堆和方法區是線程共享的呢?

    2.2. 程序計數器為什麼是私有的?

    程序計數器主要有下面兩個作用:

    1. 字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
    2. 在多線程的情況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。

    需要注意的是,如果執行的是 native 方法,那麼程序計數器記錄的是 undefined 地址,只有執行的是 Java 代碼時程序計數器記錄的才是下一條指令的地址。

    所以,程序計數器私有主要是為了線程切換后能恢復到正確的執行位置

    2.3. 虛擬機棧和本地方法棧為什麼是私有的?

    • 虛擬機棧: 每個 Java 方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、常量池引用等信息。從方法調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。
    • 本地方法棧: 和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧為虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。

    所以,為了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。

    2.4. 一句話簡單了解堆和方法區

    堆和方法區是所有線程共享的資源,其中堆是進程中最大的一塊內存,主要用於存放新創建的對象 (所有對象都在這裏分配內存),方法區主要用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。

    3. 說說併發與并行的區別?

    • 併發: 同一時間段,多個任務都在執行 (單位時間內不一定同時執行);
    • 并行: 單位時間內,多個任務同時執行。

    4. 為什麼要使用多線程呢?

    先從總體上來說:

    • 從計算機底層來說: 線程可以比作是輕量級的進程,是程序執行的最小單位,線程間的切換和調度的成本遠遠小於進程。另外,多核 CPU 時代意味着多個線程可以同時運行,這減少了線程上下文切換的開銷。
    • 從當代互聯網發展趨勢來說: 現在的系統動不動就要求百萬級甚至千萬級的併發量,而多線程併發編程正是開發高併發系統的基礎,利用好多線程機制可以大大提高系統整體的併發能力以及性能。

    再深入到計算機底層來探討:

    • 單核時代: 在單核時代多線程主要是為了提高 CPU 和 IO 設備的綜合利用率。舉個例子:當只有一個線程的時候會導致 CPU 計算時,IO 設備空閑;進行 IO 操作時,CPU 空閑。我們可以簡單地說這兩者的利用率目前都是 50%左右。但是當有兩個線程的時候就不一樣了,當一個線程執行 CPU 計算時,另外一個線程可以進行 IO 操作,這樣兩個的利用率就可以在理想情況下達到 100%了。
    • 多核時代: 多核時代多線程主要是為了提高 CPU 利用率。舉個例子:假如我們要計算一個複雜的任務,我們只用一個線程的話,CPU 只會一個 CPU 核心被利用到,而創建多個線程就可以讓多個 CPU 核心被利用到,這樣就提高了 CPU 的利用率。

    5. 使用多線程可能帶來什麼問題?

    併發編程的目的就是為了能提高程序的執行效率提高程序運行速度,但是併發編程並不總是能提高程序運行速度的,而且併發編程可能會遇到很多問題,比如:內存泄漏、上下文切換、死鎖還有受限於硬件和軟件的資源閑置問題。

    6. 說說線程的生命周期和狀態?

    Java 線程在運行的生命周期中的指定時刻只可能處於下面 6 種不同狀態的其中一個狀態(圖源《Java 併發編程藝術》4.1.4 節)。

    線程在生命周期中並不是固定處於某一個狀態而是隨着代碼的執行在不同狀態之間切換。Java 線程狀態變遷如下圖所示(圖源《Java 併發編程藝術》4.1.4 節):

    由上圖可以看出:線程創建之後它將處於 NEW(新建) 狀態,調用 start() 方法后開始運行,線程這時候處於 READY(可運行) 狀態。可運行狀態的線程獲得了 CPU 時間片(timeslice)后就處於 RUNNING(運行) 狀態。

    操作系統隱藏 Java 虛擬機(JVM)中的 RUNNABLE 和 RUNNING 狀態,它只能看到 RUNNABLE 狀態(圖源::),所以 Java 系統一般將這兩個狀態統稱為 RUNNABLE(運行中) 狀態 。

    當線程執行 wait()方法之後,線程進入 WAITING(等待) 狀態。進入等待狀態的線程需要依靠其他線程的通知才能夠返回到運行狀態,而 TIME_WAITING(超時等待) 狀態相當於在等待狀態的基礎上增加了超時限制,比如通過 sleep(long millis)方法或 wait(long millis)方法可以將 Java 線程置於 TIMED WAITING 狀態。當超時時間到達后 Java 線程將會返回到 RUNNABLE 狀態。當線程調用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到 BLOCKED(阻塞) 狀態。線程在執行 Runnable 的run()方法之後將會進入到 TERMINATED(終止) 狀態。

    7. 什麼是上下文切換?

    多線程編程中一般線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執行,CPU 採取的策略是為每個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會重新處於就緒狀態讓給其他線程使用,這個過程就屬於一次上下文切換。

    概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時,可以再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換

    上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味着消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。

    Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。

    8. 什麼是線程死鎖?如何避免死鎖?

    8.1. 認識線程死鎖

    多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於線程被無限期地阻塞,因此程序不可能正常終止。

    如下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個線程就會互相等待而進入死鎖狀態。

    下面通過一個例子來說明線程死鎖,代碼模擬了上圖的死鎖的情況 (代碼來源於《併發編程之美》):

    public class DeadLockDemo {
        private static Object resource1 = new Object();//資源 1
        private static Object resource2 = new Object();//資源 2
    
        public static void main(String[] args) {
            new Thread(() -> {
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource2");
                    synchronized (resource2) {
                        System.out.println(Thread.currentThread() + "get resource2");
                    }
                }
            }, "線程 1").start();
    
            new Thread(() -> {
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource1");
                    synchronized (resource1) {
                        System.out.println(Thread.currentThread() + "get resource1");
                    }
                }
            }, "線程 2").start();
        }
    }

    Output

    Thread[線程 1,5,main]get resource1
    Thread[線程 2,5,main]get resource2
    Thread[線程 1,5,main]waiting get resource2
    Thread[線程 2,5,main]waiting get resource1

    線程 A 通過 synchronized (resource1) 獲得 resource1 的監視器鎖,然後通過Thread.sleep(1000);讓線程 A 休眠 1s 為的是讓線程 B 得到執行然後獲取到 resource2 的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,然後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子符合產生死鎖的四個必要條件。

    學過操作系統的朋友都知道產生死鎖必須具備以下四個條件:

    1. 互斥條件:該資源任意一個時刻只由一個線程佔用。
    2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
    3. 不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強行剝奪,只有自己使用完畢后才釋放資源。
    4. 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。

    8.2. 如何避免線程死鎖?

    我們只要破壞產生死鎖的四個條件中的其中一個就可以了。

    破壞互斥條件

    這個條件我們沒有辦法破壞,因為我們用鎖本來就是想讓他們互斥的(臨界資源需要互斥訪問)。

    破壞請求與保持條件

    一次性申請所有的資源。

    破壞不剝奪條件

    佔用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源。

    破壞循環等待條件

    靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。

    我們對線程 2 的代碼修改成下面這樣就不會產生死鎖了。

            new Thread(() -> {
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource2");
                    synchronized (resource2) {
                        System.out.println(Thread.currentThread() + "get resource2");
                    }
                }
            }, "線程 2").start();

    Output

    Thread[線程 1,5,main]get resource1
    Thread[線程 1,5,main]waiting get resource2
    Thread[線程 1,5,main]get resource2
    Thread[線程 2,5,main]get resource1
    Thread[線程 2,5,main]waiting get resource2
    Thread[線程 2,5,main]get resource2
    
    Process finished with exit code 0

    我們分析一下上面的代碼為什麼避免了死鎖的發生?

    線程 1 首先獲得到 resource1 的監視器鎖,這時候線程 2 就獲取不到了。然後線程 1 再去獲取 resource2 的監視器鎖,可以獲取到。然後線程 1 釋放了對 resource1、resource2 的監視器鎖的佔用,線程 2 獲取到就可以執行了。這樣就破壞了破壞循環等待條件,因此避免了死鎖。

    9. 說說 sleep() 方法和 wait() 方法區別和共同點?

    • 兩者最主要的區別在於:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖
    • 兩者都可以暫停線程的執行。
    • Wait 通常被用於線程間交互/通信,sleep 通常被用於暫停執行。
    • wait() 方法被調用后,線程不會自動蘇醒,需要別的線程調用同一個對象上的 notify() 或者 notifyAll() 方法。sleep() 方法執行完成后,線程會自動蘇醒。或者可以使用wait(long timeout)超時后線程會自動蘇醒。

    10. 為什麼我們調用 start() 方法時會執行 run() 方法,為什麼我們不能直接調用 run() 方法?

    這是另一個非常經典的 java 多線程面試問題,而且在面試中會經常被問到。很簡單,但是很多人都會答不上來!

    new 一個 Thread,線程進入了新建狀態;調用 start() 方法,會啟動一個線程並使線程進入了就緒狀態,當分配到時間片后就可以開始運行了。 start() 會執行線程的相應準備工作,然後自動執行 run() 方法的內容,這是真正的多線程工作。 而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,所以這並不是多線程工作。

    總結: 調用 start 方法方可啟動線程並使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,還是在主線程里執行。

    開源項目推薦

    作者的其他開源項目推薦:

    1. :【Java學習+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識。
    2. : 適合新手入門以及有經驗的開發人員查閱的 Spring Boot 教程(業餘時間維護中,歡迎一起維護)。
    3. : 我覺得技術人員應該有的一些好習慣!
    4. :從零入門 !Spring Security With JWT(含權限驗證)後端部分代碼。

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

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

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

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

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

  • [NLP] Adaptive Softmax

    [NLP] Adaptive Softmax

    1. Overview

    Adaptive softmax算法在鏈接1中的論文中提出,該算法目的是為了提高softmax函數的運算效率,適用於一些具有非常大詞彙量的神經網絡。

    在NLP的大部分任務中,都會用到softmax,但是對於詞彙量非常大的任務,每次進行完全的softmax會有非常大的計算量,很耗時(每次預測一個token都需要O(|V|)的時間複雜度)。

    所以paper中提出adaptive softmax來提升softmax的運算效率。

    1) 該算法的提出利用到了單詞分佈不均衡的特點(unbalanced word distribution)來形成clusters, 這樣在計算softmax時可以避免對詞彙量大小的線性依賴關係,降低計算時間;

    2) 另外通過結合現代架構和矩陣乘積操作的特點,通過使其更適合GPU單元的方式進一步加速計算。

    2. Introduction

    2.1 一般降低softmax計算複雜度的兩種方式

    1) 考慮原始分佈:近似原始概率分佈或近似原始概率分佈的子集

    2) 構造近似模型,但是產生準確的概率分佈。比如:hierarchical softmax.

    (上述兩個方法可以大致區分為:一是準確的模型產生近似概率分佈,二是用近似模型產生準確的概率分佈)

    2.2 本文貢獻點

    paper中主要採用的上述(2)的方式,主要借鑒的是hierarchical softmax和一些變型。與以往工作的不同在於,本paper結合了GPU的特點進行了計算加速。主要

    1. 定義了一種可以生成 近似層次模型 的策略,該策略同時考慮了矩陣乘積運算的計算時間。這個計算時間並非與矩陣的維數是簡單線性關係。

    2. 在近來的GPU上對該模型進行了實證分析。在所提出的優化算法中也包含了對實際計算時間模型的定義。

    3. 與一般的softmax相比,有2× 到 10× 的加速。這等價於在同等算力限制下提高了準確度。另外一點非常重要,該paper所提出的這種高效計算的方法與一些通過并行提高效率的方法相比,在給定一定量的training data下,對準確度並沒有損失。

    3. Adaptive Softmax 

    3.1 矩陣乘積運算的計算時間模型

    hidden states: (B × d); Word representation: (d × k); 這兩個矩陣的乘積運算時間: g(k, B)。其中B是batch size, d: hidden layer 的size, k vectors的數量。

    (1) 固定B,d, 探究k值大小與g(k)的關係:

    在K40、 M40兩種GPU model中進行實驗,發現k在大約為$k_0 \approx 50$的範圍之內,g(k)是常數級別的,在$k > k_0$后,呈線性依賴關係。其計算模型為:

    同樣的,關於B的計算時間的函數也呈現這樣的關係。這可以表示,矩陣相乘時,當其中一個維度很小時,矩陣乘法是低效的。

    如何理解呢?比如對於 $k_1 = 2^2$, $k_2 = 2^4$,它們均在同樣的常量時間內完成運算,那麼顯然對於$k1$量級有算力被浪費了。

    那麼這也說明,一些words的層次結構,其每個節點只具有少量的子節點(比如huffman 編碼的tree),是次優的。

    (2) 探究batch size B值大小與g(B)的關係:

    同樣的,當探究計算用時與batch size $B$,的關係時,發現在矩陣乘法運算中,其中一個維度很小時計算並不高效。因此會導致:

    (i) 在層次結構中,因為每個節點只有少數一些孩子節點 (比如Huffman 樹),其計算效率是次優的;

    (ii) 對於按照詞頻劃分clusters后,那些只包含稀有詞的cluster,被選擇的概率為p, 並且其batch size也縮減為 $p B$,也會出現以上所述的低效的矩陣乘積運算問題。

    (3) 本文的計算模型

    所以paper中綜合考慮了k與B,提出了下面的計算模型:

    對於Adaptive Softmax, 其核心思想是根據詞頻大小,將不同詞頻區間的詞分為不同的clusters,按照詞頻高的優先訪問的原則,來對每個cluster整體,進而對cluster中的每個詞進行訪問,進行softmax操作。

    那麼paper中首先以2個clusters為例,對該模型進行講解,之後推廣到多個clusters的一般情況。

    3.2 Two-Clusters Case

    根據Zipf定律,在Penn TreeBank中 20%的詞,可以覆蓋一般文檔中87%的出現過的詞。

    直觀地,可以將字典$V$中的詞分為$V_h$、$V_t$兩個部分。其中$V_h$表示高頻詞集合(head),$V_t$表示低頻詞集合(tail)。一般地,$|V_h| << |V_t|$,$P(V_h) >> P(V_t)$。

    (1) Clusters 的組織

    直觀地可以想到對於這兩個clusters有兩種組織方式:(i) 兩個clusters都是2層的樹結構,(ii) 將head部分用一個短列表保存,對tail部分用二層樹結構。何種方式更優可以就其計算效率和準確率進行比較:

    在準確率方面,(i) 較之 (ii) 一般有 5 ~ 10 %的下降。原因如下:

    對於屬於cluster $c$的每個詞$w$的概率計算為: 

    若採用(i):  $P(c | h) * P(w | c, h)$

    若採用(ii): 對於高頻詞head部分可以直接計算獲得 $P(w | h)$,其更為簡單直接。

    因此,除非(i)(ii)的計算時間有非常大的差別,否則選擇(ii)的組織方式。

    (2) 對計算時間的縮短

     

    圖2. Two clusters示意圖

    如圖2所示,$k_h = |V_h|, k_t = k – k_h, p_t = 1 – p_h$

    (i) 對於第一層:對於batch size 為B的輸入,在head中對$k_h$個高頻詞以及1個單位的二層cluster的向量(加陰影部分),共$k_h + 1$個向量,做softmax,

    這樣占文檔中$p_h$的詞可以基本確定下來,在head 的 short list 部分找到對應的單詞;

    而如果與陰影部分進行softmax值更大,表明待預測的詞,在第二層的低頻詞部分;

    第一層的計算開銷為:$g(k_h + 1, B)$

    (ii) 在short list 中確定了 $p_h × B$ 的內容后,還剩下 $p_t B$的輸入需要在tail中繼續進行softmax來確定所預測的詞。

    第二層中需要對$k_t$個向量做softmax;

    第二層的計算開銷為:$g(k_t, pB)$

    綜上,總的計算開銷為:$$C = g(k_h + 1, B) + g(k_t, p_t B)$$

    相比於最初的softmax,分母中需要對詞典中的每個詞的向量逐一進行計算;採用adaptive softmax可以使得該過程分為兩部分進行,每部分只需對該部分所包含的詞的向量進行計算即可,降低了計算用時。

    論文中的Figure 2显示了,對於$k_h$的合理劃分,最高可以實現相較於完全softmax的5x加速。

    (3) 不同cluster的分類器能力不同

    由於每個cluster基本是獨立計算的,它們並不要求有相同的能力(capacity)。

    一般可以為高頻詞的cluster予以更高的capacity,而可以適當降低詞頻詞的cluster的capacity。因為低頻詞在文檔中很少出現,因此對其capacity的適當降低並不會非常影響總體的性能。

    在本文中,採取了為不同的clusters共享隱層,通過加映射矩陣的方式降低分類器的輸入大小。一般的,tail部分的映射矩陣將其大小從$d$維縮小到$d_t = d / 4$維。

    3.3 一般性情況

    在3.2中以分為2個cluster為例,介紹了adaptive softmax如何組織並計算的,這裏將其拓展為一般情況,即可以分為多個clusters時的處理方式。

    假設將整個詞典分為一個head部分和J個詞頻詞cluster部分,那麼有:

    $$V = V_h \cup V_1 … V_j, V_i \cap V_j = \phi$$

    其中$V_h$在第一層,其餘均在第二層,如圖Figure 3所示。

    每個cluster中的詞向量的數目為$k_i = |V_i|$,單詞$w$屬於某個cluster的概率為:$p_i = \sum_{w \in V_i} p(w)$.

    那麼,

    計算head部分的時間開銷為:$C_h = g(J + k_h, B)$

    計算每個二層cluster的時間開銷為:$\forall_i, C_i = g(k_i, p_i B)$

    那麼總的時間開銷為:$C = g(J + k_h, B) + \sum_i g(k_i, p_i B) \tag8$

     

    回顧公式(7):

    $$g(k,B) = max(c + \lambda k_0 B_0, c + \lambda k B) \tag7$$

    這裡有兩部分,一個是常數部分$c + \lambda k_0 B_0$,一個是線性變換部分$c + \lambda k B$。

    通過3.1可知,在常數部分GPU的算力並未得到充分利用,因此應盡量避免落入該部分。那麼需要滿足:

    $kB \geq k_0B_0$,這樣在求max時,可以利用以上的第二部分。

    將(8)式代入(7)的第二部分得:

     

    那麼接下來的目標就是根據(10) 以最小化時間開銷C. 

    在(10)中,$J ,B$是固定的,那麼可以重點關注$\sum_i p_ik_i$與$k_h$。

    (1) $\sum_i p_ik_i$

    假設$p_{i + j} = p_i + p_j$, 那麼 $p_jk_j = (p_{i + j} – p_i) k_j$

    $p_ik_i + p_jk_j = p_i(k_i – k_j) + p_{i + j}k_j \tag{11}$  

    假設$k_i > k_j, p_{i + j}$, $k_j$均固定,那麼(11)中只有$p_i$可變,可以通過減小$p_i$的方式使(11)式減小。=> 也即$k_i > k_j$ 且 $p_i$盡量小,即包含越多詞的cluster,其概率越小。

    (2) $k_h$

    減小$k_h$可以使(10)式減小。 => 即為可以使高頻詞所在的cluster包含更少的詞。

    綜上,給定了cluster的個數J,與batch size B,可以通過給大的cluster分配以更小的概率降低時間開銷C.

    另外paper中講到可以用動態規劃的方法,在給定了J后,對$k_i$的大小進行劃分。

    cluster的劃分個數$J$。paper中實驗了採用不同的J所導致的不同計算時間。如圖Figure 4所示。雖然在$10 ~ 15$區間內效果最好,但$ > 5$之後效果沒有非常明顯的提升。所以文中建議採用2~5個clusters。

    實驗主要在text8, europarl, one billion word三個數據集上做的,通過比較ppl發現,adaptive softmax仍能保持低的ppl,並且相比於原模型有2x到10x的提速。

      

    參考鏈接:

    1. Efficient softmax approximation for GPUs: 

    Some of us get dipped in flat, some in satin, some in gloss. But every once in a while you find someone who’s iridescent, and when you do, nothing will ever compare. ~ Flipped

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

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

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

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