天天看點

java安全編碼指南之:鎖的雙重檢測簡介單例模式的延遲加載double check模式靜态域的實作ThreadLocal版本

簡介

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

雙重檢查鎖定模式用法通常用于實作執行延遲初始化的單例工廠模式。延遲初始化推遲了成員字段或成員字段引用的對象的構造,直到實際需要才真正的建立。

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

單例模式的延遲加載

先看一個在單線程正常工作的單例模式:

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/

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

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