java安全編碼指南之:鎖的雙重檢測

flydean發表於2020-10-14

簡介

雙重檢測鎖定模式是一種設計模式,我們通過首次檢測鎖定條件而不是實際獲得鎖從而減少獲取鎖的開銷。

雙重檢查鎖定模式用法通常用於實現執行延遲初始化的單例工廠模式。延遲初始化推遲了成員欄位或成員欄位引用的物件的構造,直到實際需要才真正的建立。

但是我們需要非常小心的使用雙重檢測模式,以避免傳送錯誤。

單例模式的延遲載入

先看一個在單執行緒正常工作的單例模式:

public class Book {

    private static Book book;

    public static Book getBook(){
        if(book==null){
            book = new Book();
        }
        return book;
    }
}

上面的類中定義了一個getBook方法來返回一個新的book物件,返回物件之前,我們先判斷了book是否為空,如果不為空的話就new一個book物件。

初看起來,好像沒什麼問題,我們仔細考慮一下:

book=new Book()其實一個複雜的命令,並不是原子性操作。它大概可以分解為1.分配記憶體,2.例項化物件,3.將物件和記憶體地址建立關聯。

在多執行緒環境中,因為重排序的影響,我們可能的到意向不到的結果。

最簡單的辦法就是加上synchronized關鍵字:

public class Book {

    private static Book book;

    public synchronized static Book getBook(){
        if(book==null){
            book = new Book();
        }
        return book;
    }
}

double check模式

如果要使用double check模式該怎麼做呢?

public class BookDLC {
    private static BookDLC bookDLC;

    public static BookDLC getBookDLC(){
        if(bookDLC == null ){
            synchronized (BookDLC.class){
                if(bookDLC ==null){
                    bookDLC=new BookDLC();
                }
            }
        }
        return bookDLC;
    }
}

我們先判斷bookDLC是否為空,如果為空,說明需要例項化一個新的物件,這時候我們鎖住BookDLC.class,然後再進行一次為空判斷,如果這次不為空,則進行初始化。

那麼上的程式碼有沒有問題呢?

有,bookDLC雖然是一個static變數,但是因為CPU快取的原因,我們並不能夠保證當前執行緒被賦值之後的bookDLC,立馬對其他執行緒可見。

所以我們需要將bookDLC定義為volatile,如下所示:

public class BookDLC {
    private volatile static BookDLC bookDLC;

    public static BookDLC getBookDLC(){
        if(bookDLC == null ){
            synchronized (BookDLC.class){
                if(bookDLC ==null){
                    bookDLC=new BookDLC();
                }
            }
        }
        return bookDLC;
    }
}

靜態域的實現

public class BookStatic {
    private static BookStatic bookStatic= new BookStatic();

    public static BookStatic getBookStatic(){
        return bookStatic;
    }
}

JVM在類被載入之後和被執行緒使用之前,會進行靜態初始化,而在這個初始化階段將會獲得一個鎖,從而保證在靜態初始化階段記憶體寫入操作將對所有的執行緒可見。

上面的例子定義了static變數,在靜態初始化階段將會被例項化。這種方式叫做提前初始化。

下面我們再看一個延遲初始化佔位類的模式:


public class BookStaticLazy {

    private static class BookStaticHolder{
        private static BookStaticLazy bookStatic= new BookStaticLazy();
    }

    public static BookStaticLazy getBookStatic(){
        return BookStaticHolder.bookStatic;
    }
}

上面的類中,只有在呼叫getBookStatic方法的時候才會去初始化類。

ThreadLocal版本

我們知道ThreadLocal就是Thread的本地變數,它實際上是對Thread中的成員變數ThreadLocal.ThreadLocalMap的封裝。

所有的ThreadLocal中存放的資料實際上都儲存在當前執行緒的成員變數ThreadLocal.ThreadLocalMap中。

如果使用ThreadLocal,我們可以先判斷當前執行緒的ThreadLocal中有沒有,沒有的話再去建立。

如下所示:

public class BookThreadLocal {
    private static final ThreadLocal<BookThreadLocal> perThreadInstance =
            new ThreadLocal<>();
    private static BookThreadLocal bookThreadLocal;

    public static BookThreadLocal getBook(){
        if (perThreadInstance.get() == null) {
            createBook();
        }
        return bookThreadLocal;
    }

    private static synchronized void createBook(){
        if (bookThreadLocal == null) {
            bookThreadLocal = new BookThreadLocal();
        }
        perThreadInstance.set(bookThreadLocal);
    }
}

本文的程式碼:

learn-java-base-9-to-20/tree/master/security

本文已收錄於 http://www.flydean.com/java-security-code-line-double-check-lock/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章