单例设计模式

简介: 单例设计模式

1.单例设计模式


设计模式:是解决一类问题最行之有效的方法。是一种思想,是规律的总结。Java中有23种设计模式。单例模式是设计模式中最简单的形式之一。

单例设计模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。


单例模式的目的是解决一个类在内存中只存在一个对象,使其对象成为系统中的唯一实例。


使用场景:

工作中经常需要在应用程序中保持一个唯一的实例,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。eg:IO处理,数据库操作等。



单例设计模式提供主要解决方式:饿汉式、懒汉式、静态内部类的单例方式、枚举实现单例等

Note:一般开发时,使用饿汉式,因为安全,效率高。懒汉式会出现线程安全等问题(当多对象同时加载时……)


保证唯一性的思想及步骤:

为了避免其他程序建立该类对象,先禁止其他程序建立该类对象,即将构造函数私有化,即建立一个私有的构造方法。

为了其他程序访问到该类对象,须在本类中创建一个该类私有对象,即创建一个私有并静态的本类对象。

为了方便其他程序访问到该类对象,可对外提供一个公共访问方式,即建立一个公有并静态的本类方法。


单例模式的优点:


单例模式(Singleton)会控制其实例对象的数量,从而确保访问对象的唯一性。


1)实例控制:单例模式防止其它对象对自己的实例化,确保所有的对象都访问一个实例。


2)伸缩性:因为由类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。


单例模式的缺点:


1)系统开销。虽然这个系统开销看起来很小,但是每次引用这个类实例的时候都要进行实例是否存在的检查。这个问题可以通过静态实例来解决。


2)开发混淆。当使用一个单例模式的对象的时候(特别是定义在类库中的),开发人员必须要记住不能使用new关键字来实例化对象。因为开发者看不到在类库中的源代码,所以当他们发现不能实例化一个类的时候会很惊讶。


3)对象生命周期。单例模式没有提出对象的销毁。在提供内存管理的开发语言(比如,基于.NetFramework的语言)中,只有单例模式对象自己才能将对象实例销毁,因为只有它拥有对实例的引用。在各种开发语言中,比如C++,其它类可以销毁对象实例,但是这么做将导致单例类内部的指针指向不明。


使用单例模式的注意事项:


1)使用Singleton模式有一个必要条件:在一个系统要求一个类只有一个实例时才应当使用单例模式。反之,如果一个类可以有几个实例共存,就不要使用单例模式。


2)不要使用单例模式存取全局变量。这违背了单例模式的用意,最好放到对应类的静态成员中。


3)不要将数据库连接做成单例,因为一个系统可能会与数据库有多个连接,并且在有连接池的情况下,应当尽可能及时释放连接。Singleton模式由于使用静态成员存储类实例,所以可能会造成资源无法及时释放,带来问题。


单例模式的核心原理:


将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取过程中必须保证线程安全、防止反序列化导致重新生成实例对象等问题。


Note:具体实现方式取决于项目本身,结合项目实际情况决定使用哪种方式。


根据项目实际业务情况选择使用哪种单例模式:


饿汉

标准饿汉 (安全防护方面 枚举单例更优于标准饿汉)

线程安全,高效,不可以懒加载

枚举单例

线程安全,高效,不可以懒加载(天然避免反射与反序列化)


懒汉 (效率方面 静态内部类更优于标准懒汉)

标准懒汉

线程安全,低效,可以懒加载

双重检测(不推荐,有bug)

线程安全,低效,可以懒加载

静态内部类

线程安全,低效,可以懒加载


2.饿汉式和懒汉式


2.1 饿汉式:先初始化对象(当类加载的时候,就创建对象)


/* 饿汉式:当类加载的时候就创建对象 */
class Single
{ 
  //描述事物
  private int num;
  public  void setNum(int num)
  {
    this.num=num;
  }
  public  int getNum()
  {
    return num;
  }
  /*
  对事物描述时,正常该怎么描述就怎么描述;当需要将该事物的对象保证唯一时,就将以下三步饿汉式代码加上即可
  */
  private  Single(){}  //将构造函数私有化。
  private static Single s = new Single(); /* 在类中创建一个本类对象。*/
  public static  Single getInstance() /*提供一个公有静态方法可以使其他类获取到该对象。*/
  {
    return s;
  } 
}
class SingleDemo 
{
  public static void main(String[] args) 
  {
    Single s1 = Single.getInstance();
    Single s2 = Single.getInstance();
    s1.setNum(23);
    System.out.println(s2.getNum());
  }
}

输出结果:

image.png

2.2 懒汉式:对象是 方法被调用时,才初始化,也叫做对象的延时加载。

//懒汉式:
//Single类进内存,对象还没有存在,只有调用了getInstance方法时,才建立对象。
class Single
{
  //描述事物
  private int num;
  public  void setNum(int num)
  {
    this.num=num;
  }
  public  int getNum()
  {
    return num;
  }
  /*
  对事物描述时,正常该怎么描述就怎么描述;当需要将该事物的对象保证唯一时,就将以下三步懒汉式代码加上即可
  */
  private static Single s = null;
  private Single(){}
  public static Single getInstance()
  {     
    if(s==null)
      s = new Single();
    return s;
  }
}
//记录原则:定义单例,建议使用饿汉式。
class SingleDemo 
{
  public static void main(String[] args) 
  {
    Single s1 = Single.getInstance();
    /*
    当加载Single类的时候,对象还不存在,s为null;
    当调用getInstance()方法时,对象才建立,s才被赋值。
    */
    Single s2 = Single.getInstance();
    s1.setNum(23);
    System.out.println(s2.getNum());
  }
}

输出结果:

image.png

Note:


此种实现方式只适用于单线程环境,因为在多线程的环境下有可能得到Single类的多个实例,线程不安全。假如同时有两个线程去判断s==null,并且得到的结果为真,那么两线程都会创建Single实例,就违背了单例模式“唯一实例”的初衷。

3.饿汉式和懒汉式的区别


1)饿汉式是类一加载进内存就创建好了对象;

 懒汉式则是类才加载进内存的时候,对象还没有存在,只有调用了getInstance()方法时,对象才开始创建。

2)懒汉式是延迟加载,如果多个线程同时操作懒汉式时就有可能出现线程安全问题,解决线程安全问题。可以加同步来解决。但是加了同步之后,每一次都要比较锁,效率就变慢了,所以可以加双重判断来提高程序效率。

Note:开发常用饿汉式,因为饿汉式简单安全。懒汉式多线程的时候容易发生问题。


4.解决懒汉式的线程安全问题


解决懒汉式的线程安全问题可以使用双重判断(加锁)


Note:


下面的实现方式线程是安全的,首先我们创建了一个静态只读的进程辅助对象,synchronized是确保当一个线程位于代码的临界区时,另一个线程不能进入临界区(同步操作)。如果其他线程试图进入锁定的代码,则它将一直等待,直到该对象被释放。从而确保在多线程下不会创建多个对象实例。这种实现方式要进行同步操作,需要在同步操作之前,添加判断该实例是否为null以降低通过操作的次数,避免影响系统性能的瓶颈和增加了额外的开销。这是经典的DCL(Double-Checked Locking)方法。


代码如下:

class Single
{
  //描述事物
  private int num;
  public  void setNum(int num)
  {
    this.num=num;
  }
  public  int getNum()
  {
    return num;
  }
  /*
  对事物描述时,正常该怎么描述就怎么描述;当需要将该事物的对象保证唯一时,就将以下三步懒汉式代码加上即可
  */
  private static Single s = null;
  private Single(){}
  public static Single getInstance()
  {   
    //此处进行双重判断、加锁即可解决懒汉式容易出现的问题
    if(s==null)
    {
      synchronized(Single.class)
      {       
        if(s==null)
          s = new Single();
      }
    }
    return s;
  }
}
class SingleDemo 
{
  public static void main(String[] args) 
  {
    Single s1 = Single.getInstance();
    /*
    当加载Single类的时候,对象还不存在,s为null;
    当调用getInstance()方法时,对象才建立,s才被赋值。
    */
    Single s2 = Single.getInstance();
    s1.setNum(23);
    System.out.println(s2.getNum());
  }
}

输出结果:

image.png


但是,很可惜,它也是存在问题的。主要在于 s = new Single();  这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:


1)给 s 分配内存

2)调用Single 的构造函数来初始化成员变量

3)将s 对象指向分配的内存空间(执行完这一步, s 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 s 已经是非 null 了(但却没有初始化),所以线程二会直接返回 s,然后使用,然后顺理成章地报错。


解决办法:只需要将 s 变量声明成 volatile 就可以了。


(注:volatile的用法和作用,日后会作单独写博客加以说明)


代码如下:

class Single  
{  
    //描述事物  
    private int num;  
    public  void setNum(int num)  
    {  
        this.num=num;  
    }  
    public  int getNum()  
    {  
        return num;  
    }  
    /* 
    对事物描述时,正常该怎么描述就怎么描述;当需要将该事物的对象保证唯一时,就将以下三步懒汉式代码加上即可 
    */  
    private static volatile Single s; //声明成volatile
    private Single(){}  
    public static Single getInstance()  
    {         
        if(s==null)  
        {  
            synchronized(Single.class)  
            {                 
                if(s==null)  
                    s = new Single();  
            }  
        }  
        return s;  
    }  
}  

部分人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 s对象的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。即在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。


eg:取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从“先行发生原则”的角度理解,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(此处的“后面”是时间上的先后顺序)。


Note:需特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。


5.单例模式的另一种实现方式:静态内部类


DCL方法虽然在一定程度上解决了资源消耗、多余的同步、线程安全等问题,但是,它在某些情况下还会出现失效的问题。这种问题被称为双重检查锁定(DCL)失效。建议使用静态内部类的方式实现单例模式。


以下只提供主要实现代码:

public class Singleton{
    /*
    对事物描述时,正常该怎么描述就怎么描述;当需要将该事物的对象保证唯一时,就将以下三步饿汉式代码加上即可
    */
    private Singleton(){} //将构造函数私有化。
    /*提供一个公有静态方法可以使其他类获取到该对象。*/
    public static Singleton getInstance(){
        return SingletonHolder.myInstance;
    }
    /* 静态内部类形式:在类中创建一个私有并静态的本类对象。*/
    private static class SingletonHolder{
        private static final Singleton myInstance=new Singleton();
    }
}

分析:


当第一次加载Singleton类时并不会初始化myInstance,只有在第一次调用Singleton的getInstance()方法时才会导致myInstance被初始化。因此,第一次调用getInstance()方法会导致虚拟机加载SingletonHolder内部类时,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化。推荐使用这种单例模式的实现方式。


6.上述单例的实现存在反序列化问题


在上述的几种单例模式实现中,在一种情况下会出现重新创建对象的情况,那就是反序列化。


通过反序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。


(在上述几个示例中)杜绝单例对象在反序列化时重新生成对象,必须加入以下方法:

 private Object readResolve() throws ObjectStreamException {
        return myInstance;
    }

7.单例模式的另一种实现方式:枚举实现单例


首先简单了解下枚举,再回到枚举实现单例模式:


1)枚举是一种特殊的类,其中的每一个元素都是该类的一个实例对象。


枚举可以定义构造函数、抽象方法、成员变量和普通方法。


每一个枚举元素,都是一个对象。


如果枚举只有一个成员时,就可以作为一个单例的实现方式。


Note:枚举元素必须位于枚举体中的最开始部分,枚举元素列表后要有分号,与其他成员分隔用逗号。枚举元素名称全部字母必须大写。


枚举类的构造方法必须是私有的。


枚举一般格式:

public enum 类名{
    //枚举成员列表eg:RED,GREEN;
    //私有构造
    //其他成员
}

2)使用枚举实现单例:

使用枚举实现单例的最大优点: 如果枚举只有一个成员时,就可以作为一种单例的实现方式。最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。

以下只提供主要实现代码:

public enum SingletonEnum{
    INSTANCE;
    //下面方法并不是单例的一部分
    public void doSomething(){
        System.out.print("do sthing");
    }
}


目录
相关文章
|
7月前
|
设计模式
单例设计模式步骤
单例设计模式步骤
35 1
|
7月前
|
设计模式 安全 测试技术
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
150 0
|
7月前
|
设计模式 安全 Java
最简单的设计模式是单例?
单例模式可以说是Java中最简单的设计模式,但同时也是技术面试中频率极高的面试题。因为它不仅涉及到设计模式,还包括了关于线程安全、内存模型、类加载等机制。所以说它是最简单的吗?
82 3
最简单的设计模式是单例?
|
7月前
|
设计模式 安全 Java
【设计模式】2、设计模式分类和单例设计模式
【设计模式】2、设计模式分类和单例设计模式
60 0
|
7月前
|
设计模式 消息中间件 安全
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(二)
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(二)
63 1
|
7月前
|
设计模式 Java
26、Java 简单实现单例设计模式(饿汉式和懒汉式)
26、Java 简单实现单例设计模式(饿汉式和懒汉式)
59 2
|
7月前
|
设计模式 安全 Java
在Java中即指单例设计模式
在Java中即指单例设计模式
41 0
|
26天前
|
设计模式 前端开发 JavaScript
JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式
本文深入探讨了JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式,结合电商网站案例,展示了设计模式如何提升代码的可维护性、扩展性和可读性,强调了其在前端开发中的重要性。
29 2
|
3月前
|
设计模式 存储 安全
设计模式——设计模式介绍和单例设计模式
饿汉式(静态常量)、饿汉式(静态代码块)、懒汉式(线程不安全)、懒汉式(线程安全,同步方法)、懒汉式(线程不安全,同步代码块)、双重检查(推荐,线程安全、懒加载)、静态内部类(推荐)、枚举(推荐)
设计模式——设计模式介绍和单例设计模式
|
7月前
|
设计模式 安全 Java
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式