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

相关文章
相关标签/搜索