一种常见的设计模式,在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。具有以下特点:
特点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
主要分为
饿汉式单例类、
懒汉式单例类、
Lazy initialization holder class。
优点:
- 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
- 有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
缺点:
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等
1、饿汉式单例类
饿汉式是典型的
空间换时间,当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。潜在的问题是线程不安全,实现较为容易。
代码如下:
/* 饿汉式单例类 */
public class EagerSingleton {
private static EagerSingleton instance = new EagerSingleton();
//私有的构造函数
private EagerSingleton() {}
//静态工厂方法
public static EagerSingleton getInstance() {
return instance;
}
public void show() {
System.out.println("饿汉式单例...");
}
}
public class SingletonDemo {
public static void main(String[] args) {
//获取对象
EagerSingleton eagerSingleton = EagerSingleton.getInstance();
eagerSingleton.show();
}
}
当类装载的时候就会创建类的实例,然后每次调用的时候,都可以直接使用,节省了运行时间。
2、懒汉式单例类
懒汉式单例类实现里对静态工厂方法使用了同步化,以处理多线程环境。懒汉式是时间换空间,每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。如果未使用则不会创建。懒汉式的实现是线程安全的,会降低速度。
代码如下:
/* 懒汉式单例 */
public class LazySingleton {
private static LazySingleton instance = null;
//私有的构造函数
private LazySingleton() {}
//静态的工厂方法
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
public void show() {
System.out.println("懒汉式单例...");
}
}
public class SingletonDemo {
public static void main(String[] args) {
//获取饿汉式单例对象
EagerSingleton eagerSingleton = EagerSingleton.getInstance();
eagerSingleton.show();
//获取懒汉式单例对象
LazySingleton lazySingleton = LazySingleton.getInstance();
lazySingleton.show();
}
}
上述方法虽然线程安全但是新性能会有一些问题,因此使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。
具体方法是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
采用关键字
volatile(
被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量)。
public class Singleton {
private volatile static Singleton instance = null;
/* 私有的构造函数 */
private Singleton() {}
public static Singleton getInstance() {
//先检查实例是否存在,如果不存在才进入下面的同步块
if(instance == null) {
//同步块,线程安全的创建实例
synchronized (Singleton.class) {
//检查实例是否存在,如果不存在才真正的创建实例
instance = new Singleton();
}
}
return instance;
}
}
3、Lazy initialization holder class模式
这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。这种方式采用 classloder 机制来保证初始化 instance 时只有一个线程,这种方式是 SingletonLazyInitHolder 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
public class SingletonLazyInitHolder {
private SingletonLazyInitHolder() {}
/**
* @description 类级的内部类,也就是静态的成员式内部类,
* 该内部类的实例与外部类的实例没有绑定关系,
* 而且只有被调用到时才会装载,从而实现了延迟加载
*/
private static class SingletonHolder {
/* 静态初始化器,由JVM来保证线程安全 */
private static SingletonLazyInitHolder instance = new SingletonLazyInitHolder();
}
public static SingletonLazyInitHolder getInstance() {
return SingletonHolder.instance;
}
}
这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。