今天讲讲最后一个创建型的设计模式:单例模式

 

单例模式:
保证全局有且仅有一个对象,不会发生全局存在第二个不同的对象。

 

实现:
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。

 

目的:

防止系统频繁的创建对象和销毁对象

 

优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点:

没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

 

单例模式UML图:

 

 单例模式的4种实现方式

1.懒汉式

代码:

public class SingletonClass {

    private static SingletonClass singletonClass;//1

    private SingletonClass() {//2
    }

    private static SingletonClass getSingletonClass() {//3
        if (singletonClass == null)
            singletonClass = new SingletonClass();
        return singletonClass;
    }
}

1.声明一个空对象
2.将构造方法设置为私有方法,只能在类中自己调用构造方法
3.声明一个公共方法获取对象
如果该对象是空的,则创建一个新的对象
否则直接返回已经生成好的类对象

 

为什么要叫懒汉模式呢?

因为我们在还没调用getSingletonClass()方法的时候都,不实例化该对象。
其实类似的实现(懒加载)出现在很多地方,比如:java对类的加载,调用ArrayList,HashMap等等的构造方法的时候,其实都没有先实例化数据结构,
而是在add,put方法调用的时候才创建该数据结构

 

仔细观察,上面的示例是线程不安全的,当两个线程同时执行getSingletonClass()会得到同时产生新的对象。不符合单例模式的设计。

加强版懒汉模式(线程安全)

public class SingletonClass {

    private static SingletonClass singletonClass;

    private SingletonClass() {
    }

    private synchronized static SingletonClass getSingletonClass() {
        if (singletonClass == null)
            singletonClass = new SingletonClass();
        return singletonClass;
    }
}

通过synchronized 来保证在同一时刻有且只有一个线程能使用该方法。

但是也存在线程间的阻塞,上下文切换开销较大的问题。

补充知识:
synchronized修饰方法时:
修饰static方法:static的方法是属于类方法。那么static获取到的锁,是属于类的锁
修饰非static方法:获取到的锁是属于调用该方法的对象锁

为什么要使用static,

我认为是因为单例的实例是属于类的属性(存储在方法区(元空间)),而不是属于对象的属性。

是不需要实例化该对象就能获得该对象。(好像没有表达清楚..)

 

2.饿汉式

 1 public class Singleton1 {
 2     private static Singleton1 instance = new Singleton1();
 3 
 4     private Singleton1() {
 5     }
 6 
 7     public static Singleton1 getInstance() {
 8         return instance;
 9     }
10 }

为什么要叫饿汉模式呢?

因为他没有等到调用对象方法,就已经把对象初始化了。

代码其实与懒汉模式相差不大,只是将单例的初始化放到了类的加载阶段。

 

这样实现是线程安全的,因为在类的加载阶段就已经实例化了。

通过前面对jvm的学习,我们知道类的加载只会执行一次(双亲委派模型(一个类在jvm中只会被一个类加载器加载一次))

当然缺点就是没有对实例进行懒加载。

 

饿汉模式是单例模式使用最高频率的实现方法

 

3.双检锁/双重校验锁

代码

 1 public class Singleton {  
 2     private volatile static Singleton singleton;  
 3     private Singleton (){}  
 4     public static Singleton getSingleton() {  
 5     if (singleton == null) {  
 6         synchronized (Singleton.class) {  
 7         if (singleton == null) {  
 8             singleton = new Singleton();  
 9         }  
10         }  
11     }  
12     return singleton;  
13     }  
14 }

这是对懒汉模式的加强版,将线程的阻塞放到了校验第二层中,大大提升了程序的执行效率。

首先使用volatile 修饰对象实例,保证可见性。

第一重校验就是校验该对象是否为空,如果不为空则返回单例。

否则进行第二重校验,使用sychronized来锁住该类,判断是否需要加载该对象。

通过两重校验+volatile 单例(线程间可见),大大减少了阻塞的线程数目,提高线程效率。同时他也保留了懒汉模式的懒加载单例的优点。

这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

 

4.登记式/静态内部类

代码

1 public class Singleton {  
2     private static class SingletonHolder {  
3     private static final Singleton INSTANCE = new Singleton();  
4     }  
5     private Singleton (){}  
6     public static final Singleton getInstance() {  
7     return SingletonHolder.INSTANCE;  
8     }  
9 }

我还没研究清楚是咋回事..

 

这种模式是饿汉模式的升级版,是线程安全的。同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟饿汉方式不同的是:汉方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。

能达到双检锁方式一样的功效,实现更简单。

对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。双检锁方式可在实例域需要延迟初始化时使用。

因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。

 

想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。

这个时候,这种方式相比饿汉方式就显得很合理。

 

优点:线程安全,效率高,懒加载

 

 

 

总结:

经验之谈:一般情况下,不建议使用懒汉方式,建议使用饿汉方式。

只有在要明确实现 lazy loading 效果时,才会使用登记方式。

如果有其他特殊的需求,可以考虑使用双检锁方式。