程序中的樂觀鎖與悲觀鎖,以及動手實現樂觀鎖

概念:

這裏拋開數據庫來談樂觀鎖和悲觀鎖,扯上數據庫總會以爲和Java離得很遠.java

悲觀鎖:一段執行邏輯加上悲觀鎖,不一樣線程同時執行時,只能有一個線程執行,其餘的線程在入口處等待,直到鎖被釋放.數據庫

樂觀鎖:一段執行邏輯加上樂觀鎖,不一樣線程同時執行時,能夠同時進入執行,在最後更新數據的時候要檢查這些數據是否被其餘線程修改了(版本和執行初是否相同),沒有修改則進行更新,不然放棄本次操做.安全

從解釋上能夠看出,悲觀鎖具備很強的獨佔性,也是最安全的.而樂觀鎖很開放,效率高,安全性比悲觀鎖低,由於在樂觀鎖檢查數據版本一致性時也可能被其餘線程修改數據.從下面的例子中能夠看出來這裏說的安全差異.多線程

 

樂觀鎖例子:

package note.com;

/**
 * 樂觀鎖
 * 
 * 場景:有一個對象value,須要被兩個線程調用,因爲是共享數據,存在髒數據的問題
 * 悲觀鎖能夠利用synchronized實現,這裏不提.
 * 如今用樂觀鎖來解決這個髒數據問題
 * 
 * @author lxz
 *
 */
public class OptimisticLock {
    public static int value = 0; // 多線程同時調用的操做對象

    /**
     * A線程要執行的方法
     */
    public static void invoke(int Avalue, String i)
            throws InterruptedException {
        Thread.sleep(1000L);//延長執行時間
        if (Avalue != value) {//判斷value版本
            System.out.println(Avalue + ":" + value + "A版本不一致,不執行");
            value--;
        } else {
            Avalue++;//對數據操做
            value = Avalue;;//對數據操做
            System.out.println(i + ":" + value);
        }
    }

    /**
     * B線程要執行的方法
     */
    public static void invoke2(int Bvalue, String i)
            throws InterruptedException {
        Thread.sleep(1000L);//延長執行時間
        if (Bvalue != value) {//判斷value版本
            System.out.println(Bvalue + ":" + value + "B版本不一致,不執行");
        } else {
            System.out.println("B:利用value運算,value="+Bvalue);
        }
    }

    /**
     * 測試,期待結果:B線程執行的時候value數據老是當前最新的
     */
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {//A線程
            public void run() {
                try {
                    for (int i = 0; i < 3; i++) {
                        int Avalue = OptimisticLock.value;//A獲取的value
                        OptimisticLock.invoke(Avalue, "A");
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {//B線程
            public void run() {
                try {
                    for (int i = 0; i < 3; i++) {
                        int Bvalue = OptimisticLock.value;//B獲取的value
                        OptimisticLock.invoke2(Bvalue, "B");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

 

測試結果:eclipse

A:1
0:1B版本不一致,不執行
B:利用value運算,value=1
A:2
B:利用value運算,value=2
A:3

函數

從結果中看出,B線程在執行的時候最後發現本身的value和執行前不一致,說明被A修改了,那麼放棄了本次執行.性能

 

多運行幾回發現了下面的結果:測試

A:1
B:利用value運算,value=0
A:2
1:2B版本不一致,不執行
A:3
B:利用value運算,value=2spa


從結果看A修改了value值,B卻沒有檢查出來,利用錯誤的value值進行了操做. 爲何會這樣呢? 線程

這裏就回到前面說的樂觀鎖是有必定的不安全性的,B在檢查版本的時候A尚未修改,在B檢查完版本後更新數據前(例子中的輸出語句),A更改了value值,這時B執行更新數據(例子中的輸出語句)就發生了與現存value不一致的現象.

 

針對這個問題,我以爲樂觀鎖要解決這個問題還須要在檢查版本與更新數據這個操做的時候可以使用悲觀鎖,好比加上synchronized,讓它在最後一步保證數據的一致性.這樣既保證多線程都能同時執行,犧牲最後一點的性能去保證數據的一致.

 

補充

感謝評論中提出的cas方式解決樂觀鎖最後的安全問題,之前不知道cas(比較-交換)這個在java中的存在,找了找資料才發現java的concurrent包確實使用的cas實現樂觀鎖的數據同步問題.

下面是我對這兩種方式的一點見解:

有兩種方式來保證樂觀鎖最後同步數據保證它原子性的方法

1,CAS方式:Java非公開API類Unsafe實現的CAS(比較-交換),由C++編寫的調用硬件操做內存,保證這個操做的原子性,concurrent包下不少樂觀鎖實現使用到這個類,但這個類不做爲公開API使用,隨時可能會被更改.我在本地測試了一下,確實不可以直接調用,源碼中Unsafe是私有構造函數,只能經過getUnsafe方法獲取單例,首先去掉eclipse的檢查(非API的調用限制)限制之後,執行發現報 java.lang.SecurityException異常,源碼中getUnsafe方法中執行訪問檢查,看來java不容許應用程序獲取Unsafe類. 值得一提的是反射是能夠獲得這個類對象的.
2,加鎖方式:利用Java提供的現有API來實現最後數據同步的原子性(用悲觀鎖).看似樂觀鎖最後仍是用了悲觀鎖來保證安全,效率沒有提升.實際上針對於大多數只執行不一樣步數據的狀況,效率比悲觀加鎖整個方法要高.特別注意:針對一個對象的數據同步,悲觀鎖對這個對象加鎖和樂觀鎖效率差很少,若是是多個須要同步數據的對象,樂觀鎖就比較方便.

 

擴展:利用反射得到Unsafe對象

第一步:去掉eclipse受限制的API檢查:

將Windows->Preferences->Java-Complicer->Errors/Warnings->Deprecated and restricted API,中的Forbidden references(access rules)設置爲Warning,Unsafe能夠編譯經過。

第二步:利用反射跳過安全檢查獲取Unsafe對象:

    Class<Unsafe> s1  = (Class<Unsafe>) Class.forName("sun.misc.Unsafe");
    Field u1 = s1.getDeclaredField("theUnsafe");//得到Unsafe的theUnsafe屬性
    u1.setAccessible(true);//得到private屬性的可訪問權限
    Unsafe unsafe1 = (Unsafe) u1.get(null);//得到Class中屬性對應的值
    System.out.println(unsafe1.addressSize());//測試獲取的Unsafe對象
    //或者
    Field u = Unsafe.class.getDeclaredField("theUnsafe");
    u.setAccessible(true);
    Unsafe unsafe = (Unsafe) u.get(null);  
    System.out.println(unsafe.addressSize());//測試獲取的Unsafe對象

關於Unsafe的使用方法給個參考地址,平時用不到,我沒有去深刻看.

地址:Java Magic. Part 4: sun.misc.Unsafe

相關文章
相關標籤/搜索