JAVA併發編程中關於鎖的小結

        最近在學習java的併發編程時,遇到了很多鎖的概念,有很多其實都是同一個鎖的多種叫法而已,或者是某種鎖的一個功能。爲了更好的梳理這塊知識,這裏做一個小結,將鎖的概念進行區分。

        先說目前我們所遇到的鎖的名詞,大致有如下這些:公平鎖/非公平鎖、可重入鎖(遞歸鎖)、獨享鎖/共享鎖、互斥鎖/讀寫鎖、樂觀鎖/悲觀鎖、自旋鎖、分段鎖、分佈式鎖。

公平鎖/非公平鎖

        這個概念主要是指,當多個進程同時搶佔一個資源時,由於該資源已經被佔用,那麼這些線程會進入等待隊列。此時,如果是公平鎖,那麼所有的進程會按照訪問時間依次進入隊列,當資源被釋放後,從隊列中出隊一個線程進行處理(由於隊列是先進先出原則,所以保證了公平性);如果是非公平鎖,那麼會有兩種情況進行處理:1、每個線程都有各自的優先級,每當資源被釋放後,會根據線程優先級進行出隊,優先級高的線程先獲得資源搶佔權。2、每個線程在訪問資源發現被佔用,進入等待隊列前,會再進行一次搶佔動作,如果此時能搶到鎖,那麼就繼續執行,搶不到,就進入等待隊列(AQS非公平鎖原理)。

        非公平鎖的優點是,高併發下的吞吐量比公平鎖要高(畢竟少了入隊出隊操作),但是極端情況下會導致某些線程長時間搶不到資源而導致超時。

可重入鎖(遞歸鎖)

        這個概念是指,如果一個線程在獲得鎖的情況下,內部調用邏輯有同樣的鎖,那麼它依然可以獲得該鎖,而不需要等待。如下,funcA方法本身有鎖,調用的funcB方法也有鎖,但是他們是在同一個線程邏輯內的鎖,可以認爲是一個鎖,這樣在調用funcB方法時,可以直接獲取該鎖而不用等待,避免死鎖的發生。

synchronized void funcA() throws Exception{
    Thread.sleep(1000);
    funcB();
}

synchronized void funcB() throws Exception{
    Thread.sleep(1000);
}

        可以這麼理解,你有一把家門鑰匙,當你開門進入後,將門反鎖(即上鎖,避免其他人進入),屋裏的其他房間都可以對你敞開,你要做的操作僅僅是:進入主臥--開門--離開主臥--關門,進入廚房--開門--離開廚房--關門。這個操作在Java中,就是用一個計數器來控制,當你重複獲得一個鎖時,該鎖的計數器會加一,釋放時減一,直到計數器爲0,才標誌着該資源被完全釋放。

獨享鎖/共享鎖

互斥鎖/讀寫鎖

        這兩個概念一起說吧。獨享的意思,就是互斥,這個很好理解,就是一個資源每次只能被一個線程佔用,佔用期間其他線程不能搶佔,只能等待。共享鎖是指一個資源同時可以被多個線程佔用。java中的SynchronizedReentrantLock都是獨享鎖。不過Lock的另一個實現ReadWriteLock(讀寫鎖)就有共享鎖的實現。因爲讀操作並沒有對數據進行更改,加不加鎖其實不影響,而寫操作就需要加鎖避免髒數據的產生。爲了應對這種場景,就產生了讀寫鎖,提高併發效率。

樂觀鎖/悲觀鎖

自旋鎖

        樂觀和悲觀,指的就是對待加鎖的態度。悲觀鎖認爲,不加鎖的併發操作一定會出問題,當一個線程在操作數據時,認爲這個數據一定被其他線程修改了,必須加鎖保證數據一致性(哪怕當前就它一個線程);樂觀鎖認爲,當一個線程在操作數據時,認爲這個數據一定沒有被其他線程修改,不需要加鎖(也就是所謂的無鎖機制)。

        悲觀鎖很好理解,只要加上鎖就好。

        樂觀鎖因爲認爲不需要加鎖,但是爲了保證數據一致性,才用了CAS機制,即compare and swap(set),比較後再交換(賦值)。舉兩個例子來說明:

        1、在sql執行更新語句時,悲觀鎖會在這個方法只上加Synchronized關鍵字保證每次只有一個線程能更新。而樂觀鎖不需要加鎖,他只需要在表中增加一個字段:version,每次修改之前先查詢該值(比如10),然後執行update語句爲

update table_name set name='zhangsan',version=version+1 where id=1 and version=10

        只有當version=10的時候,這條語句纔會被執行,如果此時有另一個線程執行了這條語句,version會變成11,那麼該線程會執行失敗,有兩種選擇進行處理:A--退出,該情況可能的場景之一是用戶修改姓名,連續點了兩次提交,既然別的線程已經改了,那該線程直接結束就好;B--進入死循環,重新獲取新的version,再去更新,直到更新成功爲止,因爲這裏不停的循環等待,所以這種情況就被稱爲自旋鎖。該情況可能的場景就是記錄訪問量、庫存等等(當然,redis記錄更好)

        2、Atomicinteger原子類的核心機制,採用了java的unsafe類,直接對內存進行操作,這裏有三個概念,初始值、期望值和比較值。比如要自增,從1增加到2,那麼初始值就是1,期望值就是2,比較值是從主存中拿回來的這個變量當前的值(涉及了JAVA內存模型,這裏不細說)。如果比較值是1,說明當前可以自增,然後增加後,將2刷回主存;如果比較值大於1,說明已經有其他線程將該變量進行了自增,那麼它就會在循環中開始自旋:將比較值賦給初始值,再進行一次CAS操作。

分段鎖

        這是一種加鎖思想,並不是具體的一種鎖,是解決高併發下全局鎖可能造成的性能損失而出現的,在ConcurrentHashMap中有很好的體現。在進行put操作時,並不會對整個hashmap進行加鎖,會先根據hashcode算出將要插入的點,然後針對該點進行加鎖,這樣就保證了多個線程可以同時進行put操作了。

分佈式鎖

        分佈式鎖主要是針對分佈式環境下,多臺主機同時操作同一個資源,因爲不在同一個jvm環境下,無法通過java自身進行控制,所以就需要藉助第三方工具來實現。主要有兩種方案:

        1、藉助redis的setnx方法,該方法在插入一個值後成功後會返回1,失敗返回0。因爲redis本身是線程安全的,所以在操作時通過自旋的方式判斷返回值

        2、藉助zookeeper的臨時節點。zk規定,同一個節點下,不能存在同名的節點,臨時節點則會在斷開連接的時候自動刪除。所以當一個節點被創建後,對應的線程會獲得鎖,其他線程獲取鎖失敗,此時不需要自旋,會執行線程等待狀態。zk在節點刪除時,會主動發出時間通知,其他線程在收到通知後,再進行一次申請鎖操作,獲得鎖就會解除等待狀態,否則繼續等待。

小結

        1、java中有兩種鎖,Synchronized關鍵字和Lock鎖,其中Lock鎖是一個接口,其下有多種實現,最著名的就是ReentrantLockReadWriteLock,上面都有提到。

        2、ReentrantLock的字面意思,其實就是可重入鎖的意思,它藉助了AQS機制,進行了加鎖。

        3、Synchronized是通過jvm來實現的非公平鎖,所以無法改爲公平鎖。而ReentrantLock是通過代碼的方式實現了加鎖,所以可以進行公平和非公平的選擇,默認情況下是非公平鎖

        4、自旋鎖(樂觀鎖)因爲實質上並沒有加鎖,所以它的併發效率會很高,但是因爲死循環的存在,會造成CPU的資源佔用。所以在使用鎖機制時,要針對不同的情況採用不同的鎖。

        5、在使用自旋鎖時,要考慮實際情況,不能一味的死循環等待。其實自旋鎖也可以理解爲一個重試機制,當超出一定次數後,可以考慮退出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章