单例设计模式

简介: 单例设计模式

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");
    }
}


目录
相关文章
|
2月前
|
设计模式 安全 测试技术
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
67 0
|
2月前
|
设计模式 缓存 安全
单例设计模式的优缺点
单例设计模式的优缺点
34 0
|
2月前
|
设计模式 XML 存储
关于 ABAP 单例设计模式的一个冷门知识点
关于 ABAP 单例设计模式的一个冷门知识点
23 0
|
2月前
|
设计模式 安全 Java
【设计模式】2、设计模式分类和单例设计模式
【设计模式】2、设计模式分类和单例设计模式
28 0
|
2月前
|
设计模式 Java
26、Java 简单实现单例设计模式(饿汉式和懒汉式)
26、Java 简单实现单例设计模式(饿汉式和懒汉式)
30 2
|
4月前
|
设计模式 消息中间件 安全
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(二)
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(二)
35 1
|
4月前
|
设计模式 安全 Java
最简单的设计模式是单例?
单例模式可以说是Java中最简单的设计模式,但同时也是技术面试中频率极高的面试题。因为它不仅涉及到设计模式,还包括了关于线程安全、内存模型、类加载等机制。所以说它是最简单的吗?
58 3
最简单的设计模式是单例?
|
2月前
|
设计模式 安全 Java
在Java中即指单例设计模式
在Java中即指单例设计模式
22 0
|
6月前
|
设计模式 存储
static应用之 单例设计模式(饿汉单例&懒汉单例)
本章我们来学习单例模式中的饿汉单例和懒汉单例,那么什么是单例模式呢?应用该模式的这个类永远只有一个实列,即一个类只能创建一个对象例如电脑上的任务管理器对象只需要一个就能解决问题,可以节省内存空间先定义一个类,把构造器私有如下图,先来看一下没有把构造器私有化的SingleInstance类,此时Test类中可以随意创建多个SingleInstance的实例化。 在SingleInstance类中用private修饰无参构造器,此时左边new方法报错了。我们在右边创建一个静态变量来存储对象,变量名为instan
27 0
|
16天前
|
设计模式 安全 Java
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式