23種設計模式[1]:單例模式

前言:  據說懂設計模式的Developer工資會高不少?最近面試也被問到熟悉設計模式有哪些?因而便有此文。php

語言背景:PHP、Javahtml

 

定義:確保一個類只有一個實例,並且自行實例化並向整個系統提供這個實例。mysql

類型:建立類模式程序員

類圖:面試

類圖知識點:sql

1.類圖分爲三部分,依次是類名、屬性、方法數據庫

2.以<<開頭和以>>結尾的爲註釋信息編程

3.修飾符+表明public,-表明private,#表明protected,什麼都沒有表明包可見。設計模式

4.帶下劃線的屬性或方法表明是靜態的。安全

5.對類圖中對象的關係不熟悉的朋友能夠參考文章:設計模式中類的關係

 

單例模式應該是23種設計模式中最簡單的一種模式了。

它有如下幾個要素(特色):

  • 私有的構造方法(只能有一個實例)。
  • 指向本身實例的私有靜態引用(必須自行建立這個實例)。
  • 以本身實例爲返回值的靜態的公有的方法(必須給其餘對象提供這一實例)。

單例模式根據實例化對象時機的不一樣分爲兩種:

一種是餓漢式單例,一種是懶漢式單例。

餓漢式單例在單例類被加載時候,就實例化一個對象交給本身的引用;

而懶漢式在調用取得實例方法的時候纔會實例化對象。

 

PHP版本代碼以下:

懶漢模式:

<?php

class Db {
    
    //靜態變量保存全局實例
    private static $_instance = null;
    
    //私有構造函數,防止外界實例化對象
    private function __construct() {
        
    }
    
    //私有克隆函數,防止外辦克隆對象
    private function __clone() {
        
    }
    
    //靜態方法,單例統一訪問入口
    public static function getInstance() {
        if (is_null(self::$_instance) || isset(self::$_instance)) {
            self::$_instance = new self ();
        }
        return self::$_instance;
    }
    
    private function getDbLink() {
        return new  mysqli ( "localhost" ,  "my_user" ,  "my_password" ,  "world" );
    } 
 }

 

餓漢模式:

<?php

class Db {
    
    //靜態變量保存全局實例
    private static $_instance = new self ();
    
    //私有構造函數,防止外界實例化對象
    private function __construct() {
        
    }
    
    //私有克隆函數,防止外辦克隆對象
    private function __clone() {
        
    }
    
    //靜態方法,單例統一訪問入口
    public static function getInstance() {
        return self::$_instance;
    }
    
    private function getDbLink() {
        return new  mysqli ( "localhost" ,  "my_user" ,  "my_password" ,  "world" );
    } 
 }

 

在講爲何要使用單例模式以前,咱們先回顧一下以往使用DB的方式。

在以往的PHP4舊版本項目開發中,沒使用單例模式前的狀況以下:

<?php

//初始化一個數據庫句柄
$db_link = mysql_connect('YOUR_DB_ADDRESS','YOUR_DB_USER','YOUR_DB_PASS') or die("Database error");
mysql_select_db('YOUR_DB', $db_link); 

$result = mysql_query("set names 'utf8'"); 

//執行一次db查詢
$query = "select * from YOUR_DB_TABLE"; 
$result = mysql_query($query); 

//關閉DB連接
mysql_close($db_link);

這種面向過程開發的代碼,DB句柄變量徹底暴露在外,有被修改的可能。

那麼咱們這樣寫:

db.php

<?php

$db = null;
function get_db_link(){
    global $db;
    
    //初始化一個數據庫句柄
    $db_link = mysql_connect('YOUR_DB_ADDRESS','YOUR_DB_USER','YOUR_DB_PASS') or die("Database error");
    mysql_select_db('YOUR_DB', $db_link); 
    $result = mysql_query("set names 'utf8'"); 
}

function close_db(){
    //關閉DB連接
    mysql_close($db_link);
}

index.php

<?php

require_once 'db.php';

get_db_link();

//執行一次db查詢
$query = "select * from YOUR_DB_TABLE"; 
$result = mysql_query($query); 

//關閉DB連接
close_db();

OK,這回沒有顯式的在上下文中看到DB相關的變量了吧? 但仍是有被修改的可能。如咱們的程序員小A ,他沒有查看db.php上下文的狀況下:

<?php

require_once 'db.php';

get_db_link();

//這裏,DB的連接句柄就被覆蓋了,下面的代碼都會致使報錯!
$db = '123456'; 

//執行一次db查詢
$query = "select * from YOUR_DB_TABLE"; 
$result = mysql_query($query); 

//關閉DB連接
close_db();

上面的程序例子,咱們瞭解到面向過程開發哪怕你用函數進行了包裝,仍是沒法杜絕被修改的可能。

自 PHP 5 起徹底重寫了對象模型以獲得更佳性能和更多特性。這是自 PHP 4 以來的最大變化。PHP 5 具備完整的對象模型。

因而從PHP5開始完整的支持OOP概念編程了。咱們用面向對象的方式再寫一版本。代碼以下:

<?php

class User {
    //...
    
    public function getOne($id){
        $dbh = new PDO($YOUR_DB_DSN, 'YOUR_DB_USER','YOUR_DB_PASS', null);
        $result = $dbh->query('SELECT * FROM user WHERE id=' . $id);
        
        $user = [];
        //......
        return $user;
    }
}

$users = [];
$user_obj = new User();
for($i=0;$i<100;$++){
    $users[] = $user_obj->getOne();
}
var_dump($users);

這樣寫代碼沒問題沒毛病,DB句柄也是局部變量,外部沒法訪問。

可是問題來了,上面代碼進行了100次PDO連數據庫,數據庫在本地狀況下,性能還好說。但一旦跨機房了呢?

應用程序端須要進行100次的創建DB連接,這帶來了沒必要要的通訊開銷。並且在併發狀況下,會有不少客戶端保持對DB的連接,但數據庫的連接資源是有限的。這種編程方式是不可取的。

因此咱們要使用單例模式,把DB連接保存在最少。這樣就在程序上下文中能減小沒必要要的創建DB連接請求。

 

爲何要使用單例模式?

一個主要應用場合就是應用程序與數據庫打交道的場景,在一個應用中會存在大量的數據庫操做,針對數據庫句柄鏈接數據庫的行爲,使用單例模式能夠避免大量的new操做。由於每一次new操做都會消耗系統和內存的資源。

因而咱們有了下面的單例模式:

Db.php:這是一個單例模式

<?php

class DB{

    //靜態變量保存全局實例
    private static $_db = null;
    
    //私有構造函數,防止外界實例化對象
    private function __construct() {}
    
    //私有克隆函數,防止外辦克隆對象
    private function __clone() {}
    
    //靜態方法,單例統一訪問入口
    public static function getInstance() {
        if (is_null (self::$_instance) || isset(self::$_instance)) {
            self::$_db = new PDO($YOUR_DB_DSN, 'YOUR_DB_USER','YOUR_DB_PASS', null);
        }
        
        return self::$_db;
    }

}

User.php

<?php

require_once  'Db.php';

class User {
    //...
    
    public function getOne($id){
        //$dbh = new PDO($YOUR_DB_DSN, 'YOUR_DB_USER','YOUR_DB_PASS', null);
        $result = Db::getInstance()->query('SELECT * FROM user WHERE id=' . $id);

        $user = [];
        //......
        return $user;
    }
}

$users = [];
$user_obj = new User();
for($i=0;$i<100;$++){
    $users[] = $user_obj->getOne();
}
var_dump($users);

如今的代碼,就不會進行創建100次DB連接請求了,整個程序上下文就只進行了一個耗時的DB連接。而這就是單例模式的優點。

 

單例模式的優勢:

  • 在內存(程序上下文)中只有一個對象,節省內存空間。
  • 避免頻繁的建立銷燬對象,能夠提升性能。
  • 避免對共享資源的多重佔用。
  • 能夠全局訪問。

適用場景:因爲單例模式的以上優勢,因此是編程中用的比較多的一種設計模式。

  • 須要頻繁實例化而後銷燬的對象。
  • 建立對象時耗時過多或者耗資源過多,但又常常用到的對象。
  • 有狀態的工具類對象。
  • 頻繁訪問數據庫或文件或其餘資源的對象。
  • 以及其餘沒用過的全部要求只有一個對象的場景。

單例模式注意事項:

  • 只能使用單例類提供的方法獲得單例對象,不要使用反射,不然將會實例化一個新對象。(php還須要確認)
  • 不要作斷開單例類對象與類中靜態引用的危險操做。
  • 多線程使用單例使用共享資源時,注意線程安全問題。(php中沒有該顧慮,Java中會有)

 

總結:

獲得一個規律就是,設計模式是解決OOP編程概念衍生出來的一種概念,不是爲了解決面向過程的。

換句百度百科說的: 設計模式(英語 design pattern)是對面向對象設計中反覆出現的問題的解決方案。

因此設計模式必然有各類設計原則,延伸閱讀 設計模式--六大原則與三種類型

 

=============================================完結撒花=============================================

 

擴展閱讀:

 

寫到這就完了嗎?不,咱們尚未考慮多線程狀況下的問題呢!

上面綠字說明,在多線程狀況下,單例模式是會有問題的。因而乎爲了解決多線程狀況下的單例使用共享資源的問題。

Java中有7中模式的單例寫法,並非茴的多種寫法那麼回事,而是每一種寫法都有各自的優點。No B B, Show me code !  如下是Java代碼:

 

第一種(懶漢,線程不安全):

public class Singleton {  

    //靜態變量保存全局實例
    private static Singleton instance;  

    //私有構造函數,防止外界實例化對象
    private Singleton (){}  

    //靜態方法,單例統一訪問入口
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

這種模式實現了懶加載,但在多線程狀況下,紅色部分代碼屢次執行,這就沒有達到採用單例模式的優勢: 在內存中只有一個對象,節省內存空間。避免頻繁的建立對象。

那如何避免屢次執行呢? 對,Java中提供了synchronized關鍵字,它就是一把鎖,能夠修飾在方法上,代碼塊上。代碼以下:

 

第二種(懶漢,線程安全)

public class Singleton {  
    
    private static Singleton instance;  
    
    private Singleton (){}  
    
    //得到當前類的類鎖
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

這種寫法可以在多線程中很好的工做,並且看起來它也具有很好的懶加載效果,可是遺憾的是,效率很低,99%狀況下不須要同步。

既然使用到了synchronized關鍵字來加鎖,從而實現線程安全。那麼咱們知道synchronized是能夠給代碼塊加鎖的,咱們調整一下上面的代碼:

 

第三種(懶漢,變種雙重校驗鎖線程安全)

public class Singleton {  
    
    //注意這裏使用volatile關鍵字的內存屏障,來達到禁止指令重排序優化,使得線程間變量修改可見。
    private volatile static Singleton singleton;  
    
    private Singleton (){}  
    
    public static Singleton getSingleton() {  
        if (singleton == null) {
            //得到當前對象的類鎖
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = new Singleton();  
                }
            }  
        } 
        return singleton;  
    }
}

這個是第二種方式的升級版,俗稱雙重檢查鎖定。注意紅色部分關鍵字volatile,延伸閱讀(雙重檢查鎖失效是由於對象的初始化並不是原子操做?   、如何正確地寫出單例模式  )。

在JDK1.5以後,雙重檢查鎖定纔可以正常達到單例效果。關於synchronized鎖的區別,請看:透徹理解 Java synchronized 對象鎖和類鎖的區別

 

第四種(餓漢,線程安全)

public class Singleton {  
    
    //靜態變量保存全局實例,該靜態實例在該類的初始化階段被實例化,Java中的靜態變量、靜態方法與靜態代碼塊詳解與初始化順序
    private static Singleton instance = new Singleton();  
    
    private Singleton (){}  
    
    public static Singleton getInstance() {  
        return instance;  
    }  
}

這種方式基於Java的 classloder 機制避免了多線程的同步問題,不過,instance在類裝載時就實例化了(Java中的靜態變量、靜態方法與靜態代碼塊詳解與初始化順序)。

雖然致使類裝載的緣由有不少種,在單例模式中大多數都是調用getInstance方法, 可是也不能肯定有其餘的方式(或者其餘的靜態方法)致使類裝載,這時候初始化instance顯然沒有達到懶加載的效果。

 

第五種(漢,變種,線程安全)

public class Singleton {  
    
    private Singleton instance = null;  
    
    static {  
        instance = new Singleton();  
    }  
    
    private Singleton (){}  
    
    public static Singleton getInstance() {  
        return this.instance;  
    }
}

表面上看起來代碼組織形式有差異,其實跟第三種方式差很少,都是在類初始化即實例化instance(Java中的靜態變量、靜態方法與靜態代碼塊詳解與初始化順序)。

 

(懶漢,靜態內部類,線程安全)

public class Singleton {  
 
    private Singleton (){}  
    
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }
    
    //內部靜態類
    //Java機制規定,內部類 LazyHolder只有在getInstance()方法第一次調用的時候纔會被加載(實現了延遲加載效果),
    //並且其加載過程是線程安全的(實現線程安全)
    private static class SingletonHolder {  

        private static final Singleton INSTANCE = new Singleton();  

    }
}

這種方式一樣利用了classloder的機制來保證初始化instance時只有一個線程,它跟第四種和第五種方式不一樣的是(很細微的差異):

第四種和第五種方式是隻要Singleton類被裝載了,那麼instance就會被實例化(沒有達到懶加載效果),而這種方式是Singleton類被裝載了,instance不必定被初始化。

由於SingletonHolder類沒有被主動使用,只有顯示經過調用getInstance方法時,纔會顯示裝載SingletonHolder類,從而實例化instance。

想象一下,若是實例化instance很消耗資源,我想讓他延遲加載,另一方面,我不但願在Singleton類加載時就實例化,由於我不能確保Singleton類還可能在其餘的地方被主動使用從而被加載,那麼這個時候實例化instance顯然是不合適的。

這個時候,這種方式相比第四和第五種方式就顯得很合理。

 

第七種(枚舉,線程安全)

class Resource{}

public enum Singleton {
    INSTANCE;
    
    private Resource instance;
    
    Singleton() {
        instance = new Resource();
    }
    
    //外部程序直接用 Singleton.INSTANCE.getInstance() 獲取單例實例;
    public Resource getInstance() {
        return instance;
    }
}

獲取資源的方式很簡單,只要 Singleton.INSTANCE.getInstance() 便可得到所要實例。擴展閱讀,Java 利用枚舉實現單例模式

枚舉模式對比第三種餓漢模式,相同之處就是沒有實現懶加載。不一樣之處就是一個提供的是靜態方法,一個是公有方法。單例的實例引用一個是私有靜態變量,一個是私有變量。

這種方式是Effective Java做者Josh Bloch 提倡的方式,書中說: 單元素的枚舉類型已經成爲實現Singleton的最佳方法。

 

 

總結

1.通常來講,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、枚舉。第一種方法不算正確的寫法(Java多線程狀況下),剩下都是線程安全的實現。

就平常Java編程而言,通常狀況下直接使用餓漢式就行了。

若是明確要求要懶加載(實例化單例對象比較消耗資源)應該傾向於使用靜態內部類方式,

若是涉及到反序列化建立對象時能夠試着使用枚舉的方式來實現單例。

 

2.經過Java單例模式的七種實現來講,對比與PHP編程,真是方式豐富不少不少。

這也可能就是Javaer鄙視PHPer的緣由之一吧。

能夠說這也是我爲什麼要把Java看成第二語言的緣由,PHP沒有介入到多線程或協程領域,少了不少豐富的應用層面的數據結構、同步鎖 和 一些編程理論知識!

 

3.學習編程的道路還任重而道遠啊!

 

如寫的很差,歡迎拍磚!

 

 

 

PS:

 

設計模式--六大原則與三種類型

單例模式的七種寫法

如何正確地寫出單例模式

Java 利用枚舉實現單例模式

Java中的靜態變量、靜態方法與靜態代碼塊詳解與初始化順序

雙重檢查鎖失效是由於對象的初始化並不是原子操做? 

透徹理解 Java synchronized 對象鎖和類鎖的區別

相關文章
相關標籤/搜索