欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

简说设计模式——单例模式

程序员文章站 2022-04-06 12:33:49
一、什么是单例模式 单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。UML结构图如下: 其中,Singleton类定义了一个getInstance操作,允许客户端访问它的唯一实例,getInstance是一个静态方法,主要负责创建自己的唯一实例。而对象的声明是p ......

一、什么是单例模式

      单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。UML结构图如下:

  简说设计模式——单例模式

       其中,Singleton类定义了一个getInstance操作,允许客户端访问它的唯一实例,getInstance是一个静态方法,主要负责创建自己的唯一实例。而对象的声明是private的,其他类无法访问到,只能通过getInstance()方法访问其唯一实例。

 1 public class Singleton {
 2 
 3     private static Singleton instance;
 4     
 5     //限制产生多个对象
 6     private Singleton() {
 7     }
 8     
 9     //通过方法获取实例对象
10     public static Singleton getInstance() {
11         if(instance == null) {
12             instance = new Singleton();
13         }
14         
15         return instance;
16     }
17     
18 }

       上述代码就是一个单例模式,首先声明了静态私有的一个对象,并通过getInstance方法返回该对象。如果该对象已经存在,则直接返回该对象,若不存在,则实例化后返回该对象。下面看一段代码:

public class Client {
    
    public static void main(String[] args) {
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        
        if(instance1 == instance2) {
            System.out.println("两个对象是相同的实例");
        }
    }
    
}
       我们运行上述程序,控制台输出了”两个对象是相同的实例“,这说明singleton1和singleton2是相同的实例,也即一个类仅有一个实例,符合单例模式的定义。

二、单例模式的应用

    1. 何时使用

  • 当我们想控制实力数目,节省系统资源时,可以使用单例模式。

    2. 优点

  • 内存中只有一个实例,减少了内存开支,尤其一个对象需要频繁地创建销毁,而此时性能又无法优化时,单例模式的优势就非常明显。
  • 避免对资源的多重占用(比如写文件操作,只有一个实例时,避免对同一个资源文件同时写操作),简单来说就是对唯一实例的受控访问。

    3. 缺点

  • 没有接口,不能继承,与单一职责冲突。

    4. 使用场景

  • 要求生成唯一序列号的环境。
  • 在整个项目中有一个共享访问点或共享数据(如web页面上的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来即可)。
  • 创建一个对象需要消耗的资源过多时(如访问I/O和数据库等资源)。

    5. 应用实例

  • 一个党只有一个主席/一个国家只有一个国王/一个皇朝只有一个皇帝。
  • 计划生育。
  • 多个进程或线程同时操作一个文件的现象。

三、高并发下的单例模式

       需要注意的是,在高并发情况下,要注意单例模式线程同步的问题。单例模式有几种不同的实现方式,如上方的代码就需要考虑线程同步。

    1. 懒汉式

       所谓懒汉式单例,就是通过在上述代码中增加synchronized关键字来实现。

public class Singleton {

    private volatile static Singleton instance;
    private static Object syncRoot = new Object();
    
    private Singleton() {
    }

    public static Singleton getInstance() {
        //双重锁定
        if(instance == null) {
            synchronized (syncRoot) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        
        return instance;
    }
    
}

       这里使用了双重锁定(Double-Check Locking),这样可以不用让线程每次都加锁,而只是在实例未被创建的时候再枷锁处理,同时也能保证多线程的安全。

      而这里判断了两次instance实例是否存在的原因是,当instance为null时,并且同时有两个线程调用getInstance()方法时,它们都可以通过第一重instance==null的判断,然后由于lock机制,这两个线程则只有一个进入,另一个在外排队等候,必须要其中一个进入并出来后,另一个才能进入,而此时如果没有了第二重排序,则第一个线程创建了实例,而第二个线程还是可以继续再创建新的实例,就没有达到单例的目的。

       这里还需要注意一个问题,第三行中加入了volatile关键字,这里如果不加volatile可能会出现一个错误,即当代码读取到第11行的判断语句时,如果instance不为null时,instance引用的对象有可能还没有完成初始化,线程将访问到一个还未初始化的对象。究其原因是因为代码第14行,创建了一个对象,此代码可分解为三行伪代码,即分配对象的内存空间、初始化对象、设置instance指向刚分配的内存地址,分别记为1、2、3,在2和3之间,可能会被重排序,重排序后初始化就变为了最后一步。因此,线程A的intra-thread semantics(所有线程在执行Java程序时必须遵守intra-thread semantics,它保证重排序不会改变单线程内的程序执行结果)没有改变,但A2和A3的重排序将导致线程B判断出instance不为空,线程B接下来将访问instance引用的对象,此时,线程B将会访问到一个还未初始化的对象。而使用volatile就可以实现线程安全的延迟初始化,本质时通过禁止2和3之间的重排序,来保证线程安全的延迟初始化。

    2. 饿汉式

       饿汉式单例就不会出现产生多个单例的情况,但它是在自己被加载时就将自己实例化,所以要提前占用系统资源。

public class Singleton {
    
    private static final Singleton instance = new Singleton();
    
    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
    
    //类中其他方法,尽量使static
    public static void dosomething() {
    }
    
}

    3. 静态内部类

       这种方式与饿汉式一样,同样利用了类加载来保证只创建一个instance实例,因此不存在线程安全的问题,不一样的是,它是在内部类里面去创建对象实例。这样只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式延迟加载。

public class Singleton {

    //静态内部类
    private static class SingletonHolder {
        public static Singleton instance = new Singleton();
    }
    
    private Singleton() {
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
    
}

    4. 枚举

       上面实现单例的方式都需要额外的工作来实现序列化,而且可以使用反射强行调用私有构造器。

       而枚举很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。

public enum Singleton {

    instance;
    
    public static void dosomething() {
    }
    
}

       枚举的客户端写法如下:

public class Client {
    
    public static void main(String[] args) {    
        //枚举
        Singleton instance1 = Singleton.instance;
        Singleton instance2 = Singleton.instance;
        
        if(instance1 == instance2) {
            System.out.println("两个对象是相同的实例");
        }
    }
    
}

四、单例模式的实现

       下面举一个完整的例子,就以一个皇朝只有一个皇帝为例,假设当今是唐朝小李的天下,来看看怎么用单例模式实现。UML图如下:

简说设计模式——单例模式

    1. 皇帝类(Emperor类)

public class Emperor {
    
    private static final Emperor EMPEROR = new Emperor();
    
    private Emperor() {
    }
    
    public static Emperor getEmperor() {
        return EMPEROR;
    }
    
    public static void say() {
        System.out.println("朕乃当今圣上小李");
    }

}

       这里使用的是饿汉式单例,这样我们在加载类的时候就对对象进行了实例化操作,后续只需调用getEmperor()方法即可。

    2. 客户端

public class Client {

    public static void main(String[] args) {
        //臣子朝拜
        for(int day=0; day<3 ;day++) {
            Emperor emperor = Emperor.getEmperor();
            emperor.say();
        }
    }
    
}

       客户端部分写了一个每日早朝的情况,臣子每日都要朝拜皇帝,今天朝拜的皇帝应该和昨天、前天的一样,运行结果如下:

简说设计模式——单例模式

       运行结果表示,连续三天上朝的皇帝都是小李,这就是一个简单的单例模式。

   

       源码地址:https://gitee.com/adamjiangwh/GoF