單例:一個進程中只能存在惟一一個對象。java
/** * @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一致證實程序只有一個實例。餓漢單例在類初始化會提早建立對象。缺點:過早的建立對象須要提早消耗內存資源,咱們須要在使用單例對象時再去建立。下面咱們看看懶漢模式代碼。程序員
/** * @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 修飾單例代碼塊。面試
/** * @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
/** * @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結果一致說明進程中只有一個對象,看上去沒毛病!真是這樣嗎?接下來咱們對上面代碼再作一次深刻測試。設計模式
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步執行:安全
jvm爲了執行效率,可能將2,3重排,執行順序多是1->3->2,當多線程併發,就可能出現「aa」不等於5狀況,說明了指令若是發生重排,在多線程狀況下致使進程會有多個實例,就不符合單例的狀況了,正確的狀況是將實例變量用volatile修飾,它可以禁止指令重排,也就說new指令必須按照1->2->3順序執行,這就是volatile修飾對象變量必要性,詳細瞭解volatile特性,請看文章《volatile,synchronized可見性,有序性,原子性代碼證實(基礎硬核)》,裏面有大量實踐代碼!
多線程
/** * @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對象是單例。代碼簡單安全是咱們推薦使用方案。併發
/** * @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
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的狀況,大佬請留步!!