java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖

今天就跟大家聊聊有關(guān)java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

創(chuàng)新互聯(lián)是一家專注于網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)西信服務(wù)器托管的網(wǎng)絡(luò)公司,有著豐富的建站經(jīng)驗(yàn)和案例。

1、 悲觀鎖:總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)阻塞直到它拿到鎖。在Java語言中synchronized關(guān)鍵字的實(shí)現(xiàn)就悲觀鎖。

2、樂觀鎖:顧名思義,就是很樂觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒有去更新這個(gè)數(shù)據(jù),可以使用版本號等機(jī)制。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實(shí)現(xiàn)方式CAS( Conmpare And Swap 比較并交換)實(shí)現(xiàn)的。

從上面的描述我們可以看出2種鎖其實(shí)各有優(yōu)劣,不可認(rèn)為一種好于另一種,像樂觀鎖適用于寫比較少的情況下(多讀場景),即沖突真的很少發(fā)生的時(shí)候,這樣可以省去了鎖的開銷,加大了系統(tǒng)的整個(gè)吞吐量。但如果是多寫的情況,一般會(huì)經(jīng)常產(chǎn)生沖突,這就會(huì)導(dǎo)致上層應(yīng)用會(huì)不斷的進(jìn)行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。

    悲觀鎖其實(shí)沒什么好講,這里主要講解寫樂觀鎖。

    JAVA的樂觀鎖主要采用CAS算法即 compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的情況下實(shí)現(xiàn)多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實(shí)現(xiàn)變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三個(gè)操作數(shù)

  • 需要讀寫的內(nèi)存值 V

  • 進(jìn)行比較的值 A

  • 擬寫入的新值 B

當(dāng)且僅當(dāng) V 的值等于 A時(shí),CAS通過原子方式用新值B來更新V的值,否則不會(huì)執(zhí)行任何操作(比較和替換是一個(gè)原子操作)。一般情況下是一個(gè)自旋操作,即不斷的重試。正因?yàn)椴粩嘀卦囁匀绻L時(shí)間不成功,會(huì)給CPU帶來非常大的執(zhí)行開銷。

這樣說或許有些抽象,我們來看一個(gè)例子:

1.在內(nèi)存地址V當(dāng)中,存儲(chǔ)著值為10的變量。

java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖

2.此時(shí)線程1想要把變量的值增加1。對線程1來說,舊的預(yù)期值A(chǔ)=10,要修改的新值B=11。

java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖

3.在線程1要提交更新之前,另一個(gè)線程2搶先一步,把內(nèi)存地址V中的變量值率先更新成了11。

java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖

4.線程1開始提交更新,首先進(jìn)行A和地址V的實(shí)際值比較(Compare),發(fā)現(xiàn)A不等于V的實(shí)際值,提交失敗。

java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖

5.線程1重新獲取內(nèi)存地址V的當(dāng)前值,并重新計(jì)算想要修改的新值。此時(shí)對線程1來說,A=11,B=12。這個(gè)重新嘗試的過程被稱為自旋。

java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖

6.這一次比較幸運(yùn),沒有其他線程改變地址V的值。線程1進(jìn)行Compare,發(fā)現(xiàn)A和地址V的實(shí)際值是相等的。

java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖

7.線程1進(jìn)行SWAP,把地址V的值替換為B,也就是12。

java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖

在JAVA中是通過Unsafe類提供的一系列compareAndSawp*方法來實(shí)現(xiàn)

首先我們先研究下Unsafe,初始化Unsafe用到Unsafe.getUnsafe() ;

通過查看源碼我們發(fā)現(xiàn)這個(gè)類我們不能直接使用

@CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        //判斷調(diào)用類是否BootstrapClassLoader加載,Unsafe是系統(tǒng)Jar包按JAVA雙親委派模式,這個(gè)類是由BootstrapClassLoader加載的。而普通項(xiàng)目的類是由CustomClassLoader加載
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

我們繼續(xù)看源碼發(fā)現(xiàn)有3個(gè)CAS操作方法

var1: 要修改的對象 

var2: 對象中field的偏移量 

var4: 期望值(預(yù)期的原值) 

var5: 更新值 

返回值 true/false

這里可以看到這幾個(gè)方法都是native方法,低層都是調(diào)操作系統(tǒng)的方法,這里不深入研究

    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);

因?yàn)閁nsafe不方便調(diào)用(當(dāng)然我們可以通過反射勉強(qiáng)也可以用),所以我們只能拿AtomicInteger來研究下。我們new AtomicInteger()時(shí),會(huì)獲取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()這個(gè)方法的實(shí)現(xiàn)

/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

它最終調(diào)用的是unsafe的getAndAddInt方法,我們繼續(xù)往下跟蹤

    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。這么說可能比較清楚點(diǎn),例如:

AtomicInteger atomicInteger = new AtomicInteger(2);
        int a = atomicInteger.getAndIncrement() ;

則var5為2,var5+var4=2+1=3。我們從上上圖可以看出,這里用了個(gè)循環(huán)也就是說當(dāng)期望值不是2時(shí)會(huì)一直循環(huán)嘗試。相等就把x值賦值給offset位置的值,不相等,就取消賦值,方法返回false。這也是CAS的思想,及比較并交換。用于保證并發(fā)時(shí)的無鎖并發(fā)的安全性。

這里有同學(xué)可能會(huì)擔(dān)心死循環(huán)問題,其實(shí)不會(huì)的大家可以看下AtomicInteger那個(gè)類是設(shè)置成volatile類型,也就是內(nèi)存可見所有線程獲取到的值都是最新的。但是用循環(huán)卻會(huì)產(chǎn)生ABA的問題。 即如果在此之間,V被修改了兩次,但是最終值還是修改成了舊值V,這個(gè)時(shí)候,就不好判斷這個(gè)共享變量是否已經(jīng)被修改過。

java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖

為了防止這種不當(dāng)寫入導(dǎo)致的不確定問題,原子操作類提供了一個(gè)帶有時(shí)間戳的原子操作類。帶有時(shí)間戳的原子操作類AtomicStampedReference  CAS(V,E,N)當(dāng)帶有時(shí)間戳的原子操作類AtomicStampedReference對應(yīng)的數(shù)值被修改時(shí),除了更新數(shù)據(jù)本身外,還必須要更新時(shí)間戳。當(dāng)AtomicStampedReference設(shè)置對象值時(shí),對象值以及時(shí)間戳都必須滿足期望值,寫入才會(huì)成功。因此,即使對象值被反復(fù)讀寫,寫回原值,只要時(shí)間戳發(fā)生變化,就能防止不恰當(dāng)?shù)膶懭搿?/p>

以下是AtomicStampedReference類的compareAndSet方法

/**
* Params:
expectedReference – 當(dāng)前值
newReference – 修改后的值
expectedStamp – 當(dāng)前時(shí)間戳
newStamp – 修改后的時(shí)間戳
return true/false
*
*/ 
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp)

以下是我的測試?yán)?/p>

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: ">

看完上述內(nèi)容,你們對java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖有進(jìn)一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。

分享名稱:java中如何實(shí)現(xiàn)悲觀鎖與樂觀鎖
文章來源:http://bm7419.com/article32/jjcesc.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站排名企業(yè)網(wǎng)站制作、定制開發(fā)、軟件開發(fā)、網(wǎng)站改版網(wǎng)頁設(shè)計(jì)公司

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

成都app開發(fā)公司