資安

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

簡介

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

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

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

單例模式的延遲加載

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

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/

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

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

Leave a Reply

Your email address will not be published. Required fields are marked *