您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關java中如何實現悲觀鎖與樂觀鎖,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內容,希望大家根據這篇文章可以有所收獲。
1、 悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。在Java語言中synchronized關鍵字的實現就悲觀鎖。
2、樂觀鎖:顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS( Conmpare And Swap 比較并交換)實現的。
從上面的描述我們可以看出2種鎖其實各有優劣,不可認為一種好于另一種,像樂觀鎖適用于寫比較少的情況下(多讀場景),即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的情況,一般會經常產生沖突,這就會導致上層應用會不斷的進行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。
悲觀鎖其實沒什么好講,這里主要講解寫樂觀鎖。
JAVA的樂觀鎖主要采用CAS算法即 compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的情況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實現變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三個操作數
需要讀寫的內存值 V
進行比較的值 A
擬寫入的新值 B
當且僅當 V 的值等于 A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試。正因為不斷重試所以如果長時間不成功,會給CPU帶來非常大的執行開銷。
這樣說或許有些抽象,我們來看一個例子:
1.在內存地址V當中,存儲著值為10的變量。
2.此時線程1想要把變量的值增加1。對線程1來說,舊的預期值A=10,要修改的新值B=11。
3.在線程1要提交更新之前,另一個線程2搶先一步,把內存地址V中的變量值率先更新成了11。
4.線程1開始提交更新,首先進行A和地址V的實際值比較(Compare),發現A不等于V的實際值,提交失敗。
5.線程1重新獲取內存地址V的當前值,并重新計算想要修改的新值。此時對線程1來說,A=11,B=12。這個重新嘗試的過程被稱為自旋。
6.這一次比較幸運,沒有其他線程改變地址V的值。線程1進行Compare,發現A和地址V的實際值是相等的。
7.線程1進行SWAP,把地址V的值替換為B,也就是12。
在JAVA中是通過Unsafe類提供的一系列compareAndSawp*方法來實現
首先我們先研究下Unsafe,初始化Unsafe用到Unsafe.getUnsafe() ;
通過查看源碼我們發現這個類我們不能直接使用
@CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); //判斷調用類是否BootstrapClassLoader加載,Unsafe是系統Jar包按JAVA雙親委派模式,這個類是由BootstrapClassLoader加載的。而普通項目的類是由CustomClassLoader加載 if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
我們繼續看源碼發現有3個CAS操作方法
var1: 要修改的對象
var2: 對象中field的偏移量
var4: 期望值(預期的原值)
var5: 更新值
返回值 true/false
這里可以看到這幾個方法都是native方法,低層都是調操作系統的方法,這里不深入研究
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
因為Unsafe不方便調用(當然我們可以通過反射勉強也可以用),所以我們只能拿AtomicInteger來研究下。我們new AtomicInteger()時,會獲取AtomicInteger對像Value字段的偏移量
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { // 通過Unsafe方法獲取value字段的偏移量(可以理解為C++的指針) valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; ... }
我們再來看下atomicInteger.getAndIncrement()這個方法的實現
/** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
它最終調用的是unsafe的getAndAddInt方法,我們繼續往下跟蹤
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
由上圖可知,var1是AtomicInteger對象,var2是AtomicInteger對象value字段的偏移量,var5是期望值expected,var5+var4=var5+1。這么說可能比較清楚點,例如:
AtomicInteger atomicInteger = new AtomicInteger(2); int a = atomicInteger.getAndIncrement() ;
則var5為2,var5+var4=2+1=3。我們從上上圖可以看出,這里用了個循環也就是說當期望值不是2時會一直循環嘗試。相等就把x值賦值給offset位置的值,不相等,就取消賦值,方法返回false。這也是CAS的思想,及比較并交換。用于保證并發時的無鎖并發的安全性。
這里有同學可能會擔心死循環問題,其實不會的大家可以看下AtomicInteger那個類是設置成volatile類型,也就是內存可見所有線程獲取到的值都是最新的。但是用循環卻會產生ABA的問題。 即如果在此之間,V被修改了兩次,但是最終值還是修改成了舊值V,這個時候,就不好判斷這個共享變量是否已經被修改過。
為了防止這種不當寫入導致的不確定問題,原子操作類提供了一個帶有時間戳的原子操作類。帶有時間戳的原子操作類AtomicStampedReference CAS(V,E,N)當帶有時間戳的原子操作類AtomicStampedReference對應的數值被修改時,除了更新數據本身外,還必須要更新時間戳。當AtomicStampedReference設置對象值時,對象值以及時間戳都必須滿足期望值,寫入才會成功。因此,即使對象值被反復讀寫,寫回原值,只要時間戳發生變化,就能防止不恰當的寫入。
以下是AtomicStampedReference類的compareAndSet方法
/** * Params: expectedReference – 當前值 newReference – 修改后的值 expectedStamp – 當前時間戳 newStamp – 修改后的時間戳 return true/false * */ public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
以下是我的測試例子
import java.util.concurrent.atomic.AtomicStampedReference; public class CASTest { public static void main(String[] args) { int initialStamp = 1; AtomicStampedReference<String> atomicStringReference = new AtomicStampedReference<String>( "value1", initialStamp); boolean exchanged1 = atomicStringReference.compareAndSet("value1", "value2", initialStamp, initialStamp+1); System.out.println("exchanged: ">
看完上述內容,你們對java中如何實現悲觀鎖與樂觀鎖有進一步的了解嗎?如果還想了解更多知識或者相關內容,請關注億速云行業資訊頻道,感謝大家的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。