Java單例7種測試實踐

單例:一個進程中只能存在惟一一個對象。java


1.餓漢模式。 主動型太粗暴。


/**
 * @author :jiaolian
 * @date :Created in 2021-01-10 21:25
 * @description:餓漢單例測試
 * @modified By:
 * 公衆號:叫練
 */
public class HungerSignletonTest {
    //類初始化會建立單例對象
    private static HungerSignletonTest signleton = new HungerSignletonTest();

    private HungerSignletonTest(){};

    public static HungerSignletonTest getInstance() {
        return signleton;
    }

    public static void main(String[] args) {
        //三個線程測試單例,打印hashcode是否一致
        new Thread(()->{System.out.println(HungerSignletonTest.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(HungerSignletonTest.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(HungerSignletonTest.getInstance().hashCode()); }).start();
    }
}

    餓漢模式是主動建立對象,如上面程序代碼,JDK1.8環境中主線程啓動三個線程獲取HungerSignletonTest實例的hashcode是否爲同一個對象,測試結果以下圖所示,全部的hashcode一致證實程序只有一個實例。餓漢單例在類初始化會提早建立對象。缺點過早的建立對象須要提早消耗內存資源,咱們須要在使用單例對象時再去建立。下面咱們看看懶漢模式代碼。程序員

image.png


2.懶漢模式 線程不安全

/**
 * @author :jiaolian
 * @date :Created in 2021-01-10 21:39
 * @description:懶漢設計模式測試
 * @modified By:
 * 公衆號:叫練
 */
public class LazySignletonTest {
    private static LazySignletonTest signleton = null;
    private LazySignletonTest(){};

    public static LazySignletonTest getInstance() {
        if (signleton == null) {
            /*try {
                //建立對象睡2秒
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            signleton = new LazySignletonTest();
        }
        return signleton;
    }

    public static void main(String[] args) {
        //三個線程測試單例
        new Thread(()->{System.out.println(LazySignletonTest.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(LazySignletonTest.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(LazySignletonTest.getInstance().hashCode()); }).start();
    }
}

    懶漢模式是須要用到單例才調用getInstance()方法建立對象,看上去沒有什麼問題,若是放開上面註釋語句,在建立對象睡2秒,可能獲得的結果以下圖所示,三個線程獲得的hashcode的值並不同,說明signleton對象不是單例。在延遲的狀況下,全部線程都會進入if條件語句,因此會有以下狀況。缺點:非線程安全,咱們須要加一把鎖。咱們將getInstance()方法改造下,public static synchronized LazySignletonTest getInstance(),用synchronized 修飾下,運行程序,三個線程打印hashcode一致。測試一把,大功告成。還沒結束呢?你仔細看下synchronized 修飾的是方法,鎖力度會比較大,咱們只須要在建立實例對象時加鎖就能夠了,像咱們對追求代碼優化極致的程序員必需要「扣」到底。下面咱們再來看看用synchronized 修飾單例代碼塊。面試

image.png

3.懶漢加鎖模式  線程仍是不安全

/**
 * @author :jiaolian
 * @date :Created in 2021-01-10 21:39
 * @description:懶漢設計模式測試
 * @modified By:
 * 公衆號:叫練
 */
public class LazySignletonTest {
    private static LazySignletonTest signleton = null;
    private LazySignletonTest(){};

    public static LazySignletonTest getInstance() {
        if (signleton == null) {
            /*try {
                //建立對象睡2秒
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            synchronized (LazySignletonTest.class) {
                signleton = new LazySignletonTest();
            }
        }
        return signleton;
    }

    public static void main(String[] args) {
        //三個線程測試單例
        new Thread(()->{System.out.println(LazySignletonTest.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(LazySignletonTest.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(LazySignletonTest.getInstance().hashCode()); }).start();
    }
}

    單次檢測加鎖模式第一次判斷signleton不爲空就加鎖建立對象,看上去沒有什麼問題,若是放開上面代碼註釋,在建立對象睡2秒,可能獲得的結果以下圖所示,三個線程獲得的hashcode的值並不同,說明signleton對象不是單例,爲何會這樣呢?由於三個線程調用Thread.sleep(2000);會阻塞在建立對象前面,由於三個線程已經判斷了signleton等於空,因此都會建立一個新的實例!OK,既然這樣,咱們就能夠在synchronized 同步代碼塊再加一次判斷了,保證萬無一失!這是單例雙重檢測加鎖,很是經典的面試題!咱們把代碼修改爲雙重檢測加鎖機制,能萬無一失嗎?下面咱們看代碼!事實勝於雄辯!sql

image.png


4.雙重檢測加鎖 指令重排序

/**
 * @author :jiaolian
 * @date :Created in 2021-01-10 21:39
 * @description:懶漢設計模式測試
 * @modified By:
 * 公衆號:叫練
 */
public class LazySignletonTest {
    private static LazySignletonTest signleton = null;
    private LazySignletonTest(){};

    public static LazySignletonTest getInstance() {
        if (signleton == null) {
            synchronized (LazySignletonTest.class) {
                if (signleton == null) {
                    signleton = new LazySignletonTest();
                }
            }
        }
        return signleton;
    }

    public static void main(String[] args) {
        //三個線程測試單例
        new Thread(()->{System.out.println(LazySignletonTest.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(LazySignletonTest.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(LazySignletonTest.getInstance().hashCode()); }).start();
    }
}

    加鎖模式第一次判斷signleton不爲空就加鎖建立對象,通過屢次測試,hashcode結果一致說明進程中只有一個對象,看上去沒毛病!真是這樣嗎?接下來咱們對上面代碼再作一次深刻測試設計模式

image.png



5.雙重檢測加鎖 volatile必要性

import java.util.concurrent.CountDownLatch;

/**
 * @author :jiaolian
 * @date :Created in 2021-01-10 21:39
 * @description:沒有volatile修飾單例對象測試!
 * @modified By:1.堆分配空間 2.初始化構造函數 3.地址指向
 * 公衆號:叫練
 */
public class VolatileLockTest {
    private static VolatileLockTest signleton = null;
    public int aa;

    private VolatileLockTest(){
        aa = 5;
    };

    public static VolatileLockTest getInstance() {
        if (signleton == null) {
            synchronized (VolatileLockTest.class) {
                if (signleton == null) {
                    signleton = new VolatileLockTest();
                }
            }
        }
        return signleton;
    }

    public static void reset() {
        signleton = null;
    }

    public static void main(String[] args) throws InterruptedException {
        //循環三個線程測試單例
        while (true) {
            CountDownLatch start = new CountDownLatch(1);
            CountDownLatch end = new CountDownLatch(100);
            for (int i=0;i<100; i++) {
                Thread thread = new Thread(()->{
                    try {
                        //多線程同時等待
                        start.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //獲取單例,若是鎖aa等於0至關因而new 指令重排序了;
                    if (VolatileLockTest.getInstance().aa != 5) {
                        System.out.println("線程終止");
                        System.exit(0);
                    }
                    end.countDown();
                });
                thread.start();
            }
            start.countDown();
            end.await();
            reset();
        }
    }
}

    如上代碼所示:在主程序中死循環建立多線程併發生成單例對象,定義變量「aa」爲了測試new VolatileLockTest();對象是否發生重排,new指令通常在JVM中能夠分紅3步執行:安全

  1. 分配空間。堆上開闢空間。
  2. 執行構造函數賦值。調用VolatileLockTest私有構造函數。
  3. 將引用指向對象。將signleton指向新的對象。

jvm爲了執行效率,可能將2,3重排,執行順序多是1->3->2,當多線程併發,就可能出現「aa」不等於5狀況,說明了指令若是發生重排,在多線程狀況下致使進程會有多個實例,就不符合單例的狀況了,正確的狀況是將實例變量用volatile修飾,它可以禁止指令重排,也就說new指令必須按照1->2->3順序執行,這就是volatile修飾對象變量必要性,詳細瞭解volatile特性,請看文章《volatile,synchronized可見性,有序性,原子性代碼證實(基礎硬核)》,裏面有大量實踐代碼!
多線程


6.靜態內部類 被動型建立實例(推薦使用

/**
 * @author :jiaolian
 * @date :Created in 2021-01-11 15:49
 * @description:靜態內部類單例模式
 * @modified By:
 * 公衆號:叫練
 */
public class InnerClassSingleton {
    private InnerClassSingleton(){};

    public static InnerClassSingleton getInstance() {
        return InnerClass.innerClassSingleton;
    }

    private static class InnerClass {
        private static InnerClassSingleton innerClassSingleton = new InnerClassSingleton();
    }

    public static void main(String[] args) {
        //三個線程測試單例
        new Thread(()->{System.out.println(InnerClassSingleton.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(InnerClassSingleton.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(InnerClassSingleton.getInstance().hashCode()); }).start();
    }
}

    靜態內部類須要用到單例才調用getInstance()方法建立對象,通過屢次測試三個線程獲得的hashcode的值是同樣,證實signleton對象是單例。代碼簡單安全是咱們推薦使用方案。併發


7.靜態代碼塊

/**
 * @author :jiaolian
 * @date :Created in 2021-01-11 16:05
 * @description:靜態代碼塊初始單例
 * @modified By:
 * 公衆號:叫練
 */
public class StaticSingleton {

    private static StaticSingleton staticSingleton;

    //靜態代碼塊初始單例對象.
    static {
        staticSingleton = new StaticSingleton();
    }


    //private構造函數
    private StaticSingleton(){};

    //獲取單例靜態方法
    public static StaticSingleton getInstance() {
        return staticSingleton;
    }


    public static void main(String[] args) {
        //三個線程測試單例
        new Thread(()->{System.out.println(StaticSingleton.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(StaticSingleton.getInstance().hashCode()); }).start();
        new Thread(()->{System.out.println(StaticSingleton.getInstance().hashCode()); }).start();
    }
}

    如上代碼所示,靜態代碼塊會在類初始化調用,3個線程同時獲取單例對象,反覆測試hashcode值始終保持一致,證實了靜態代碼塊能夠實現單例。缺點:主動型建立對象,和「餓漢」單例有點相似。jvm



8.枚舉

import java.sql.Connection;

/**
 * @author :jiaolian
 * @date :Created in 2021-01-11 16:39
 * @description:枚舉單例
 * @modified By:
 * 公衆號:叫練
 */
enum DatabaseFactory {
    connectionFactory;
    private Connection connection;
    private DatabaseFactory(){
        System.out.println("初始化鏈接Connection");
        //初始化鏈接 省略TODO
    }
    public Connection getConnection() {
        return connection;
    }

    public static void main(String[] args) {
        //三個線程測試單例
        new Thread(()->{System.out.println(DatabaseFactory.connectionFactory.getConnection()); }).start();
        new Thread(()->{System.out.println(DatabaseFactory.connectionFactory.getConnection()); }).start();
        new Thread(()->{System.out.println(DatabaseFactory.connectionFactory.getConnection()); }).start();
    }
}

    如上代碼所示,和「餓漢」單例相似。初始化枚舉會默認加載構造方法ide


總結


    咱們說了7種單例用法,總結寫法:

  • 靜態私有變量
  • 私有構造方法
  • 公有靜態獲取單例方法

另外咱們比較推薦靜態內部類方式實現單例,緣由是簡單和高效,除此以外,咱們重點介紹了雙重檢測加鎖實現單例方式,詳細說明了裏面的坑,並解釋了來龍去脈。若是對你有幫助請點贊加關注哦。我是叫練【公衆號】,邊叫邊練。


遺留問題:在雙重檢測加鎖 volatile必要性中,通過大量測試,始終沒有測試出aa不等於5的狀況,大佬請留步!!

image.png

相關文章
相關標籤/搜索