概念
单例模式也被称作单件模式(单体模式),主要作用是控制某个类型的实例在应用中是唯一的,还提供了一个全局唯一访问这个类实例的访问点getInstance方法。单例模式是对象的创建模式之一,此外还包括工厂模式。
单例模式的特点
- 该类只有一个实例
- 该类自行创建实例(该类内部创建自身的实例对象)
- 想整个系统公开实例接口(类构造方法私有化)
使用范围:
目前java里面实现的单例是一个ClassLoader及其子ClassLoader的访问,也就是说如果一个虚拟机里有多个ClassLoader,而这些ClassLoader都加载某个类的话,就算这个类是单例,也会产生多个实例。如果一个一个机器上有多个虚拟机,那么每个虚拟机里面至少有一个这个类的实例,对于整个机器来说就不是单例了。
使用意义:有些情况需要一个全局变量(如计数器),如果实例多的话,计数会冲突;还有一些配置文件,可能多个地方会用到,如果是不是单例模式,在每个用的地方都创建实例对象的话,系统中会同时存在多份相同的配置文件,这会浪费内存资源。
注意事项:这里讨论的单例不适用于集群环境。
建议单例模式的方法命名为getInstance()。该方法返回值是单例类的类型,方法可以有参数。
单例模式的几种类型
懒汉式单例
类加载的时候不创建实例,只在第一次请求实例的时候创建,并且只创建一次,但由于线程同步会降低访问速度。
/**
* 懒汉式单例模式示例
*/
class LazySingleton{
//私有静态对象,类加载的时候不做初始化
private static LazySingleton instance = null;
//私有构造方法,避免外部创建实例
private LazySingleton(){}
//静态工厂方法,返回此类的唯一实例,实例没有初始化时才初始化
synchronized public static LazySingleton getInstance(){
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
- 构造方法私有化:避免类外部通过构造方法创建多个实例
- 提供获取实例的方法:构造方法私有化后外部不能创建实例,此时,让类提供一个方法来返回类的实例
- 获取实例的方法是静态的:客户端需要调用这个方法就要的先得到类实例,可是这个方法就是为了返回类实例,避免武侠循环,在该方法添加static关键字,就可以通过类名直接访问
- 定义好存储实例的属性:如果直接 return new Singlenton() 返回实例,每次客户端调用时,都将产生一个新的实例,这样肯定会有多个实例。单例模式可以用一个属性来记录创建好的类实例,第一次创建时记录下来,以后就可以复用
- 把这个属性也定义成静态的:由于在一个静态方法里使用,这一这个属性被迫成为一个类变量
- 线程安全:降低访问速度,而且每次访问都需要判断一次。为了更好的实现,采用“双重检查加锁”(不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块后,再次检查实例是否存在。如果不存在,就在同步的情况下创建实例,这是第二重检查。这样的话整个过程只需要一次同步,从而减少了多次在同步情况下进行判断所浪费的时间)
- 使用“双重检查加锁”机制时,需要添加volatile关键字,使用在Java5以上版本
为什么要用volatile关键字?如果不用该关键字会出现什么问题?
代码分析:
- instance = new LazySingleton();可以分解为以下三行伪代码
memory = allocate(); // 1.分配对象的内存空间
ctorInstance(memory);//2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址 - 三行代码中2和3可能会重排序,也就是说编译器先执行3,再执行2,根据java编程规范 intra-thread semantics,重排序不会改变单线程内的执行结果,但是多线程并发就不能保证了,如果其他线程在初始化对象前访问该对象,那么得到的就是未初始化的对象
- 使用volatile关键字后,2与3 之间的重排序在多线程中将会被禁止
class LazySingleton{
private volatile static LazySingleton instance = null;
private LazySingleton(){}
public static LazySingleton getInstance(){
if (instance == null) {
synchronized (LazySingleton.class){
if(instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}
}
饿汉式单例
类被加载的时候唯一的实例已经被创建,不能实现延迟加载
/**
* 饿汉式单例模式示例
*/
class Singleton{
//私有静态变量存储创建好的实例
private static Singleton instance = new Singleton();
//private static final Singleton instance = new Singleton();可以定义为static final成员
//私有构造方法,避免外部创建实例
private Singleton(){}
//微客户端提供类实例
public static Singleton getInstance(){
return instance;
}
}
存储实例的属性是静态的,利用了static的特性
- static变量在类装载的时候进行初始化
- 多个实例的static变量会共享同一块内存区域
登记式单例
维护的是一组单例类的实例,将这些实例存放在一个map(登记簿),已登记的实例,从工厂直接返回,没登记的,则先登记再返回。
public class Singleton {
//登记簿,用来存放所有登记的实例
private static Map<String, Singleton> registry = new HashMap<>();
//在类加载的时候添加一个实例到登记簿
static {
Singleton x = new Singleton();
registry.put(x.getClass().getName(), x);
}
//受保护的默认构造方法
protected Singleton(){}
//静态工厂方法,放回指定等级对象的唯一实例
public static Singleton getInstance(String name){
if(name == null){
name = "Singleton";
}
if (registry.get(name) == null){
try {
registry.put(name, (Singleton) Class.forName(name).newInstance());
} catch (InstantiationException e){
e.printStackTrace();
} catch (ClassNotFoundException e){
e.printStackTrace();
} catch (IllegalAccessException e){
e.printStackTrace();
}
}
return registry.get(name);
}
}
另一种实现单例模式的方式
常见的两种实现方式都存在小小的缺陷,既能实现延迟加载,又能实现线程安全的方式 Lazy initialization holder class 模式,该模式综合使用了java的类级内部类和多线程默认同步锁的知识,巧妙的实现了延迟加载和线程安全。
- 静态初始化器方式:简单的实现了线程安全,可以由JVM保证线程安全性,但这种方式会在类装载的时候初始化对象,不管你需不需要,浪费一定的空间。
- 类级内部类:能够让类装载的时候不去初始化对象,在这个类级内部类里去创建对象实例,只要不适用内部类,就不会创建对象实例。
public class SingletonPattern {
/**
* 类级内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
* 没有绑定关系,而且只有被调用的时候才会被装载,从而实现了延迟健在
*/
private static class SingletonHolder{
//静态初始化器,由JVM保证线程安全
private static SingletonPattern instance = new SingletonPattern();
}
private SingletonPattern(){}
public static SingletonPattern getInstance(){
return SingletonHolder.instance;
}
}
当getInstance方法第一次被调用时,它第一次读取SingletonHolder.instance,使SingletonHolder类得到初始化,这个类被装载并初始化的时候,会初始化它的静态域,从而创建SingletionPattern实例,由于是静态域,只有在虚拟机在装载类的时候初始化一次,并由虚拟机来保证它的线程安全。
单例模式优缺点
- 内存中只有一个实例,减小了内存消耗,减少系统性能开销,例如读写配置
- 避免对资源的多重占用,例如文件的读写操作
- 可以在系统设置全局的访问点,优化共享资源的访问
- 单例模式没有接口,扩展困难,如果要扩展就要修改代码
- 不利于测试,如果在并行开发环境中,单例模式没有完成,是没办法进行测试的,不能通过接口或者mock的方式虚拟对象
- 没有实现单一只能原则,把“要单例”和业务逻辑融合在一起
使用场景
- 要生成唯一序列号
- 整个项目中需要一个共享访问点或共享数据,如web页面的计数器
- 创建一个对象需要消耗的资源过多,如要访问I/O,访问数据库等资源
- 需要定义大量的静态常量和静态方法(如工具类)