23种设计模式[1]:单例模式

前言:  听说懂设计模式的Developer工资会高很多?最近面试也被问到熟悉设计模式有哪些?于是便有此文。

语言背景:PHP、Java

 

定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

类型:创建类模式

类图:

类图知识点:

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 对象锁和类锁的区别

 

 

posted @ 2018-01-17 14:09  phpdragon  阅读(375)  评论(0编辑  收藏  举报