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


概念:

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

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

樂觀鎖:一段執行邏輯加上樂觀鎖,不同線程同時執行時,可以同時進入執行,在最后更新數據的時候要檢查這些數據是否被其他線程修改了(版本和執行初是否相同),沒有修改則進行更新,否則放棄本次操作.

從解釋上可以看出,悲觀鎖具有很強的獨占性,也是最安全的.而樂觀鎖很開放,效率高,安全性比悲觀鎖低,因為在樂觀鎖檢查數據版本一致性時也可能被其他線程修改數據.

從下面的例子中可以看出來這里說的安全差別.

樂觀鎖例子:

/**
 * 樂觀鎖
 * 
 * 場景:有一個對象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();
    }

}

 

測試結果:

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=2


從結果看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 

 

http://www.cnblogs.com/qinggege/p/5284750.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM