回覆列表
  • 1 # Lans啊

    多執行緒技術是提高系統併發能力的重要技術,在應用多執行緒技術時需要注意很多問題,如執行緒退出問題、CPU及記憶體資源利用問題、執行緒安全問題等,本文主要講執行緒安全問題及如何使用“鎖”來解決執行緒安全問題。

    一、相關概念

    在瞭解鎖之前,首先闡述一下執行緒安全問題涉及到的相關概念:

    執行緒安全

    如果你的程式碼所在的程序中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次執行結果和單執行緒執行的結果是一樣的,而且其他變數的值也和預期的是一樣的,則是執行緒安全的。執行緒安全問題是由共享資源引起的,可以是一個全域性變數、一個檔案、一個數據庫表中的某條資料,當多個執行緒同時訪問這類資源的時候,就可能存線上程安全問題。

    臨界資源

    臨界資源是一次僅允許一個程序(執行緒)使用的共享資源,當其他程序(執行緒)訪問該共享資源時需要等待。

    臨界區

    臨界區是指一個訪問共享資源的程式碼段。

    執行緒同步

    為了解決執行緒安全問題,通常採用“序列化訪問臨界資源”的方案,或者叫“序列化訪問臨界資源”,即在同一時刻,保證只能有一個執行緒訪問臨界資源,也稱執行緒同步互斥訪問。

    鎖是實現執行緒同步的重要手段,它將包圍的程式碼語句塊標記為臨界區,這樣一次只有一個執行緒進入臨界區執行程式碼。

  • 2 # 豫見俊男

    我是一名擁有4年Java開發經驗的程式設計師,下面結合自己的實際專案經驗給大家講解下多執行緒程式設計中鎖是如何保證執行緒安全:

    一、JAVA 是一個多執行緒併發的語言,現在只要有點經驗的JAVA程式設計師,對於多執行緒、併發等詞彙相信並不陌生,但是對於具體的執行原理,很多也都沒深入,這裡我也分享一部分自己的經驗,主要對於執行緒安全以及鎖的一些機制原理,進行介紹。

    1.1 什麼是執行緒安全?

    這裡我借“JAVA 併發實踐”裡面的話:當多個執行緒訪問一個物件,如果不考慮這些執行緒在執行時環境下的排程和交替執行,也不需要額外的同步,或者呼叫方進行任何其他的協調操作,呼叫這個物件的行為都可以獲得正確的結果,那麼這個物件是執行緒安全的。

    我的理解:多執行緒訪問一個物件,任何情況下,都能保持正確行為,就是物件就是安全的。

    執行緒安全有強弱劃分,分為5類:

    a.不可變

    不可變的物件的,也就是被宣告成fianl的物件,只要被正確構建出來,在不發現this逃逸的情況下,其外部狀態永遠不會改變,永遠不會看到多個執行緒中處於不一致的狀態。也就是說所有物件的共享變數都宣告成final ,那麼就是安全的。

    b.絕對執行緒安全

    在某些情況下,我們希望我們的程式能在任何情況下都是安全的,比如加了final 型別的基本型別變數,這裡可以認為是的,但是這種不可變的變數沒有太大意義。而像StringBulider 類似的變數,即使加了final 型別,也不能認為是執行緒絕對安全,final 只能保證地址值不動。

    c.執行緒相對安全

    這裡的相對安全比如我們瞭解的vector,StirngBuffer 等執行緒安全的類,也許vector 類的所有操作我們都加上內部鎖,但是在使用過程中比如:宣告一個 vector 的變數,然後A,B 執行緒併發操作它。假設A執行緒在增加元素,B執行緒在遍歷獲取元素,那麼就會出現錯誤(元素個數不對),因此執行緒安全性更多的表現為對同一操作的正確執行安全,也是相對的安全。

    d.執行緒相容

    簡單的說就是,這個類本人不是執行緒安全的,但那時可以使用一些為外部手段,使其完成我們的執行緒安全。比如ArrayList,HashMap 本身不是執行緒安全的,但是如果你使用Collections.synchronizedList(Map)就可以達到安全效果,其實現原理很簡單,就是對List 或者Map 進行封轉,對其主要方法都加上內部鎖,相當於整合一個List(Map),全部重寫方法加上鎖,呼叫父類執行體。具體的這裡不深究。

    e.執行緒對立

    簡單的說無論我們是否採用了執行緒安全的機制(比如加鎖),或者其他同步措施,都不能保證多執行緒併發是安全的。比如Thread 的supend()和resume()方法,一個執行緒去中斷執行緒,另一個執行緒去恢復執行緒。那麼併發就容易產生死鎖,這裡兩個方法也就廢棄了。其他例子暫時不舉了。

    二、原子性操作

    當我們決定完成一個任務,通常情況下,在計算機中,看似很簡單的任務也是有多個不同的步驟共同完成。該步驟是由cpu 的 一些指令完成的。比如我們常見的 i ++ ;這是一個非原子性操作,因為它先.從記憶體取出i的值,然後再增1,最後再寫入記憶體中,經過三個步驟完成,如果在中間一個步驟被其他執行緒影響了,那麼就可能出現錯誤。

    舉個實際例子:我想完成過安檢的過程,我會先取下包,然後放在檢驗機上,我走過去,然後等待透過檢查,最後拿回來。但是實際過程發現有小偷在我將包放到檢測機上,還沒有進入檢查過程中,被拿走了,然後我走過去,發現我的包沒過來...這個悲劇的問題就發生了!

    那麼如何完成原子性操作呢?

    三、鎖

    3.1 互斥同步

    互斥同步是我們最基本的保障併發安全的一種手段,比如剛才的例子,假設我透過安檢這個過程,是不允許其他人接觸或者靠近的,有一道獨立的空間,也就是說我去透過檢查的的行為和小偷接近我,偷我包的行為是互斥的,那麼我的行為就很安全的完成了。

    互斥最簡單的手段是synchronized 關鍵字,synchronized 關鍵字在透過編譯之後,會在同步塊前後分別形成monitorentor 和 monitorexit 兩個位元組碼指令,這個兩個指令都需要一個reference 型別來指明要鎖定和解鎖的物件,如果synchnronized 明確指定了物件引數,那就是這個物件的reference ,如果沒有指明,那麼就根據synchronied 修飾的是例項方法還是類方法,然後取對應物件的例項或者Class物件那個作為鎖物件。

    3.2 synchronizd 工作原理

    Java 執行緒在執行到synchronied 的時候,會形成兩個位元組碼指令,這裡相當於是一個監視器(monitor),監控synchronized 保護的區域,監視器會設定幾種狀態用來區分請求執行緒:

    Contention List : 所有請求的執行緒將被首先放置到該競爭佇列

    Entry List: Contention List 的那些有資格成為候選人的執行緒會被移到Entry List

    Wait Set:那些呼叫wait 方法被阻塞的執行緒被放置到這裡

    OnDeck :任何時刻最多有一個執行緒正競爭鎖,該執行緒稱為OnDeck

    Owner :獲得所的執行緒叫Owner

    !Owner :釋放鎖的執行緒

    下面是狀態的轉換關係:

    我們知道,併發會引起競爭,那麼上圖更詳細的描述了整個過程,我這裡以我和小明和小強一起去上飛機為例子,假設所有通道唯一。

    1.我們一起打車來到飛機場,相當於進入了Contention List

    2.然後我們準備去買票櫃檯(Entry List),但是還沒到

    3.這是小明發現身份證沒帶,打電話叫他媽媽送過來,他就只能等待,進入(WaitSet).

    4.然後我和小明一起到櫃檯,如果櫃檯沒有人,那麼我們就去(Entry List) 買票。

    5.這時候到我和小強一起跑到櫃檯,但是誰先買,得看OnDesk 的,相當於選擇權在她手裡,這裡的競爭機制 是隨機的,也就是說OnDesk 看誰順眼,誰就能買。(當然大家現實都很文明排隊~.~)

    6.假設我得到的優先權,那麼我就是Owner,只有我買票成功了,才有資格說OK。因為OnDesk 必然會問還

    還有什麼需要幫助的嗎?這時候的決定權就在我手裡了,然後我會!Owner,然後OnDesk 會以同樣的方式 進行下一個人。

    7.如果小明的票拿到了(喚醒),那麼他也可以去櫃檯。

    8.當然即使Owner 的執行緒,也可能出現問題,比如買票過程中 - -發現沒錢了,等別人給我帶,也只能進入WaitSet 中了。

    3.2 重入鎖

    synchronized 內部鎖是互斥鎖,也就是說當A執行緒請求B執行緒所佔有的一個鎖時,只能等待(阻塞),直到B釋放它,如果B不釋放,那麼A就一直等待(阻塞)。也就是同一時間只能由同一執行緒進入synchronized 的保護塊,這能保證它的原子性操作。

    但是相同持有該鎖的執行緒可以再次進入該程式碼塊,它再次請求獲得鎖的時候,會成功。這裡的實現是當執行緒獲得鎖的時候,監視器(JVM) 會記錄鎖的佔有者,並且與鎖關聯的計數器 + 1,當計數器為 0的時候我們才認為該鎖沒有被佔用。

    Java程式碼

    class Parent{

    public synchronized void doSome(){}

    }

    class Child extends Parent{

    public synchronized void doSome(){

    // 如果沒有重入鎖 ,這裡會出現死鎖

    super.doSome();

    }

    }

    3.3 ReentrantLock

    這個在java.util.concurrent(J.U.C) 下的的顯示鎖,也具有重入鎖的特徵,與synchronizd 相比,Lock 鎖更加的靈活,因為內部鎖synchronized 在阻塞的時候,其他執行緒必須等待,如果出點問題,可能無限等待下去,而且內部鎖機制在狀態轉換過程中,需要對映到作業系統的原生執行緒上,這塊轉換比較耗時的,雖然JVM 也做了一些比如自旋鎖的最佳化,但是還是不夠。而Lock 鎖,是表現在API 層次的鎖,增加了額外的幾個功能:

    a. 等待可中斷:如果獲得鎖的執行緒,長時間不釋放鎖,正在等待的執行緒可以選擇放棄等待,改為初期其他事情,這樣不至於大家都等在那裡,浪費時間。

    b.公平鎖:當多個執行緒等待同一個鎖時,必須按照申請鎖的時間順序來一次獲得鎖,可以透過boolean 型別的建構函式使用公平鎖。當然此方法吞吐量稍微慢點,並且和執行緒優先順序一樣,僅僅是讓先申請的的獲得更大的大的機會,並不能完全保證它一定是公平的。

    c.繫結多個條件:ReentrantLock 物件可以同時繫結多個Condition 物件,而synchronized 中,鎖物件的wait() 和 notify() 或notifyAll() 方法可以實現一個隱含的條件,如果多餘一個條件關聯的時候,就不得不額外加個鎖,而ReentantLock 無需這麼做,只需要多次newCondition 方法即可。

    這裡簡單的理解是:synchronized 阻塞,相當於大家不認識,由工作人員(CPU)排程,自由競爭鎖,也不管競爭的人(執行緒)有啥意外情況。condition 相當於把大家都資訊都獲取了,比如A(執行緒) 獲得買票(獲得鎖),結果發現沒錢,他可以設定一個條件condition-A 等待,然後讓出位置,讓另外的人買。假設B(執行緒)買好票了,發現A有錢了,他可以透過condition-A 喚醒A,讓他繼續參與買票。相當於大家更和諧,不用一個卡死在前面,後面的人就一直等待,condition 可以多個條件切換工作。這裡是透過一個佇列進行的,至於具體的實現原理,可以參考:http://ifeve.com/understand-condition/

    小結:

    1.上面內容我是從深入理解JVM和併發實踐等地方copy的,加入了自己的一些理解。

    2.由於都是理論性的東西,因此先介紹一小部分,不至於大家看著很累,但是希望看的時候能融入自己的理解,不然都是天書,沒意思。

    3.如果發現不理解,或者我理解錯誤的,請指出,以免誤導他人嘛,非常感謝!

  • 3 # 自學JAVA

    要理解多執行緒種的鎖機制我們得先了解執行緒的五大狀態:

    建立狀態:當執行緒類編寫完畢,我們建立這個執行緒類的物件的時候,當前建立的執行緒就處於建立狀態。

    就緒狀態:當執行緒建立完畢,呼叫start()方法,該執行緒進入就緒狀態,等等cpu分配資源執行的時間片。

    執行狀態:當cpu分配給該執行緒時間片的時候,執行緒就可以執行現在的內容, 那麼執行緒記進入執行狀態。

    阻塞狀態:當執行緒在執行的時候,可能被休眠或者其他方式讓該執行緒讓出cpu的使用資源,那麼當前執行緒就進入阻塞狀態。當阻塞時間完畢,執行緒再次進入就緒狀態,等待cpu分配資源。

    死亡狀態:當執行緒該執行的所有內容執行完畢之後,執行緒就虎進入死亡狀態。

    多執行緒程式設計為什麼要加鎖

    瞭解了執行緒的五大狀態,那麼執行緒為什麼要加鎖其實一個搶票的例子就能理解了:

    搶票相信大家都能懂,是很多個人搶一張票,那麼這裡的每個人都是一個執行緒,也就是說多個執行緒要搶一個資源。如果不加鎖的話,舉個例子:網路遊戲相信大家都玩過,對於程式來說,每個一個遊戲角色都是一個執行緒。那麼當世界boss出來的時候,是所有人都在打這麼一個BOOS,但是遊戲的機制就是這一個boss爆出來的裝備只能被一個人拾取。如果有人已經在檢視這個boss爆出來的箱子的時候,其它人是不能檢視這個箱子的。但是如果這個boss爆出來的箱子沒有加鎖的話,那麼所有遊戲角色都可以同一時間開啟這個箱子,那麼也就是說所有人都可以拾取一遍裡面的裝備。這個時候鎖的重要性就體現出來了。

    什麼是多執行緒的鎖機制

    說白了就是給多個執行緒共享的要做的事情加一把鎖。每次進入這個事情操作的執行緒只能有一個,那麼這樣就會避免多個執行緒搶一個資源造成資料的不完整性。還是上面的比喻:加了鎖之後會避免很多人同一時間來訪問這個寶箱,並且當第一個檢視的人拿了裡面的其中一個裝備,那麼下一個人再次去檢視的時候是沒有了拿走的這件裝備。那麼也就是說檢視並拾取裝備這件事被加了鎖。一次只能有一個執行緒進入並操作,這個執行緒從加鎖的操作裡面退出了其它執行緒才能進入。下面有個圖就可以很好的解釋這個問題:

    三個顏色的球對應三個執行緒,中間的管道是所有執行緒都可以做的事情,那麼對中間管道加鎖之後,每次只能有一個球可以進去,並且這個求出來之後其它執行緒才能進入透過。

    用專業點的術語來解釋下:當我們給某個方法加鎖之後,每次只能有一個執行緒進入該方法,進入該方法的執行緒會得到一個鎖物件,如果這個執行緒不從加鎖的方法中出來,就不會釋放這個鎖資源,那麼其它執行緒得不到這個鎖資源是不能進入該方法的。只有當進入的執行緒執行完畢釋放這個鎖資源,其它執行緒才有可能得到鎖資源進入該方法如何使用多執行緒的鎖機制

    好,我們理解了Java種執行緒的鎖機制,在來看看如何使用執行緒中的鎖機制:

    使用鎖機制其實就是用到一個關鍵字synchronized

    synchronized修飾方法:同步方法

    訪問修飾符 synchronized 返回型別 方法名(引數列表){……}

    或者

    synchronized 訪問修飾符 返回型別 方法名(引數列表){……}

    synchronized修飾程式碼塊:同步程式碼塊

    public void run() {

    while (true) {

    synchronized (this) { //同步程式碼塊

    // 省略修改資料的程式碼......

    // 省略顯示資訊的程式碼......

    }

    }

    }

    同步程式碼塊一般情況下使用所有執行緒共同使用的物件,this是最好的。

    執行緒的概念比較簡單,如果想深入瞭解的可以看看這個圖

    總結:加鎖,會使執行緒進入的時候得到一個鎖資源,那麼這個鎖資源就是同步中最重要的實現執行緒安全的概念。

  • 中秋節和大豐收的關聯?
  • 分手之後後悔了,該怎麼辦?