天天看點

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

個人部落格

這次我們來看鎖

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

鎖有什麼作用呢?

說了這麼多還是不清楚鎖到底有什麼用處?這一點就要深思我們為什麼要使用鎖,我們用手機鎖是為了保障我們的隐私安全,使用門鎖是為了保障我們的财産安全,準确的來說我們使用鎖就是為了安全。

那麼在生活中我們可以加鎖來保障自己的隐私和财産安全,那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中的異常,歡迎轉發關注
讓人頭大的各種鎖,從這裡讓你思緒清晰