讓人頭大的各種鎖,從這裡讓你思緒清晰

  • 2019 年 10 月 3 日
  • 筆記

個人部落格

個人部落格

這次我們來看鎖

說到了鎖我們經常會聯想到生活中的鎖,在我們日常中我們經常會接觸到鎖。比如我們的手機鎖,電腦鎖,再比如我們生活中的門鎖,這些都是鎖。

鎖有什麼作用呢?

說了這麼多還是不清楚鎖到底有什麼用處?這一點就要深思我們為什麼要使用鎖,我們用手機鎖是為了保障我們的隱私安全,使用門鎖是為了保障我們的財產安全,準確的來說我們使用鎖就是為了安全。
那麼在生活中我們可以加鎖來保障自己的隱私和財產安全,那Java中的鎖有什麼用處呢?

Java中的鎖

Java中的鎖準確的來說也是為了保證安全,不過不同的是Java中的鎖是為了保證並發所需要的。所以在Java中加鎖準確的來說是為了保證並發安全,同時也是為了解決記憶體中的一致性,原子性,有序性三種問題。在Java中提供了各式各樣的鎖,每種鎖都有其自身的特點和適用範圍。所以我們都要熟悉鎖的區別和原理才能正確的使用。

樂觀鎖和悲觀鎖

悲觀鎖

樂觀鎖和悲觀鎖的話在之前我剛剛開始寫的時候就寫過相關的文章,在這裡就重新介紹一下吧。
悲觀鎖如其名它是悲觀的,它覺得每次訪問數據都可能被其他人(執行緒)修改,所以在訪問資源的時候就會對資源進行加鎖,用這種方式來保證資源在訪問的時候不會被其他執行緒修改。這樣的話其他執行緒想要獲取資源的話就只能阻塞,等到當前執行緒釋放鎖後在獲取。在Java中悲觀鎖的實現有synchronized關鍵字Lock的實現類都是悲觀鎖。我們來看一下悲觀鎖到底是怎麼執行的。

執行緒A搶佔到資源後執行緒B就陷入了阻塞中,然後就等待執行緒A釋放資源。

當執行緒A釋放完資源後執行緒B就去獲取鎖開始操作資源˛悲觀鎖保證了資源同時只能一個執行緒進行操作。

樂觀鎖

與悲觀鎖相反,樂觀鎖並不會覺得訪問數據的時候會有人修改(所以它是樂觀的),所以在訪問資源的時候並不會上鎖,但是在提交的時候回去判斷一下是否有人修改了當前數據,在資料庫中我們可以使用version版本號去實現。在Java中我們是使用CSA來實現。我們看一下樂觀鎖的執行過程

CAS

CAS(Compare And Swap)演算法是一種無鎖演算法,是Java提供的非阻塞原子性操作。在不使用鎖的情況下實現多執行緒下的同步。在並發包中(java.util.concurrent)原子性類都是使用CAS來實現樂觀鎖的。CAS通過硬體保證了比較更新的原子性,在JDK中Unsafe提供了一系列的compareAndSwap*方法,這裡就不深究Unsafe這個類了。
CAS操作過程就是將記憶體中的將要被修改的數據與預期的值進行比較,如果這兩個值相等就修改值為新值,否則就不做操作也就是說CAS需要三個操作值:

  • 預期值的 A
  • 記憶體中的V
  • 將要修改的B

    簡單的來說CAS就是一個死循環,在循環中判斷預期的值和記憶體中的值是否相等,如果相等的話就執行修改,如果如果不相等的話就繼續循環,直到執行成功後退出。

    CAS的問題

    CAS雖然很牛逼但是它也存在一些問題比如ABA問題,舉個例子,現在有記憶體中有一個共享變數X的值為A,這個時候出現一個變數想要去修改變數X的值,首先會獲取X的值這個時候獲取的是A,然後使用CAS操作把X變數修改成B。這樣看起來是沒有問題,那如果在執行緒1獲取變數X之後,執行CAS之前出現一個執行緒2把X的值修改成B然後CAS操作執行又修改成了了A,雖然最後執行的結果共享變數的值為A但是此A已經不是執行緒1獲取的A了。
    這就是經典的ABA問題。產生ABA問題是因為變數的狀態值發生了環形轉換,A可以到B,B可以到A,如果A到B,B到C就不會發生這種問題。

    解決辦法:在JDK1.5後加入了AtomicStampedReference方法給每個變數加入了一個時間戳來避免ABA問題。
    同時CAS還有循環開銷大的問題,因為會一直循環直到預期和記憶體相等修改成功。同時還有隻能保證一個共享變數的原子性的問題不過在JDK1.5之後加入了AtomicReference類來保證引用對象之間的原子性。

    使用悲觀鎖和樂觀鎖

可以使用synchronized關鍵字來實現悲觀鎖,樂觀鎖可以使用並法包下提供的原子類。

公平鎖和非公平鎖

上面說了悲觀鎖和樂觀鎖,現在來看公平鎖和非公平鎖。在鎖中也是有公平和不公平滴,公平鎖如其名講究的是一個公平,所以多個執行緒同時申請申請鎖的話,執行緒會放入一個隊列中,在隊列中第一個進入隊列的執行緒才能獲取鎖資源,講究的是先到先得。就比如我們在學校食堂打飯的時候,那個時候記得我同學一放學就趕快去食堂排隊這樣的話才能儘快的打上飯,而且在排隊的過程中並不會有人吃不到飯,這個時候食堂阿姨是公平的每個人排隊的話都能吃到飯,執行緒也是如此。非公平鎖可以這樣理解,我那個同學去食堂排隊打飯了但是有人卻插隊,食堂阿姨卻不公平直接給插隊的人打飯卻不給他打,你說氣不氣是不是很不公平,劃重點非公平鎖先到不一定先得。不過公平鎖也是有缺點的,當一個執行緒獲取資源後在隊列中的其他的執行緒就只能在阻塞,CPU的所以公平鎖比非公平鎖的效率要低很多。因為CPU喚醒阻塞執行緒的開銷比非公平鎖大。我們來看一個一個例子:

在Java中ReentrantLock提供了公平鎖和非公平鎖的實現。看一下ReentrantLock怎麼實現公平鎖和非公平鎖

使用公平鎖和非公平鎖

ReentrantLock默認就是非公平的鎖,我們來看一下公平鎖的例子:

看一下輸出結果:

我們可以看到公平鎖的輸出結果是按照順序來的,先到先得。
在看一下非公平鎖的例子:

輸出結果:

我們可以看到如果使用非公平鎖的話最後輸出的結果是完全沒有順序的,先到不一定先得。
所以在使用公平鎖的時候執行緒1獲取到鎖之後執行緒2在請求鎖的話就會掛起等待執行緒1釋放鎖,然後執行緒2才能獲取鎖。如果再有一個執行緒3想要請求鎖的話,這時候如果使用的是非公平鎖,那麼執行緒2和執行緒3中兩個有一個會獲取到鎖,公平鎖的情況下執行緒3隻能先掛起,等待執行緒2獲取鎖資源釋放後在獲取。

什麼時候使用公平鎖和非公平鎖

在需要公平資源的場景下使用公平鎖,如果不需要特殊的公平對待的話盡量使用非公平鎖,因為公平鎖會帶來性能的開銷。

獨佔鎖和共享鎖

看到獨佔和共享會聯想到什麼,對的獨佔鎖就是每次只有一個執行緒能霸佔這個鎖資源,而其他執行緒就只能等待當前獲取鎖資源的執行緒釋放鎖才能再次獲取鎖,剛剛上面的ReentrantLock就是獨佔鎖,那這樣看來獨佔鎖不也就是悲觀鎖嗎?因為悲觀鎖搶佔資源後就只能等待釋放其他執行緒才能再次獲取到鎖資源。其實準確的說獨佔鎖也是悲觀鎖。
在談共享鎖,共享鎖其實也是樂觀鎖它放寬了鎖的策略允許多個執行緒同時獲取鎖。在並發包中ReadWriteLock就是一個典型的共享鎖。它允許一個資源可以被多個讀操作訪問,或者被一個 寫操作訪問,但兩者不能同時進行。

自旋鎖

什麼是自旋鎖,自旋鎖其實就是當一個執行緒獲取鎖的時候,這個鎖已經被其他人獲取到了那麼這個執行緒不會立馬掛起,反而在不放棄CPU使用權的情況下會嘗試再次獲取鎖資源,默認次數是10次,可以使用-XX: PreBlockSpinsh來設置次數。如果自旋鎖獲取鎖的時間太長,會造成後面的執行緒CPU資源耗盡釋放。並且自旋鎖是不公平的。

優點

自旋鎖不會使執行緒狀態發生切換,一直處於用戶態,即執行緒一直都是active的;不會使執行緒進入阻塞狀態,減少了不必要的上下文切換,執行速度快。

生活中有各種意想不到的狀況,Java中也有各種意想不到的異常,下次我們聊聊Java中的異常,歡迎轉發關注