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

一、什么是单例模式

  大家学操作系统的时候应该知道,当多个进程或线程同时操作一个文件时,只有一个能访问;java中类似的例子也有很多,比如多线程中我们最常用的锁,保证了多线程同时对一个方法或对象操作时只有一个能够访问。单例模式就是如此,我们给出它的定义。

  单例模式(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    

 

posted @ 2018-04-23 20:24 JAdam 阅读(...) 评论(...) 编辑 收藏