文章目录
一、单例模式定义
保证一个类只能有一个实例,并提供一个访问这个唯一实例的全局访问点。
二、单例模式的结构和说明
Singleton:负责创建Singleton自己的唯一实例,并提供一个getInstance方法,供外部来访问这个唯一实例。
三、懒汉式和饿汉式的实现
单例模式有两种典型的创建方式,一种叫懒汉式,另一种叫饿汉式。
1、懒汉式
懒汉式的特点是延迟加载,你不用我就不创建,等到第一次调用的时候,才去创建实例对象。
public class Singleton {
//4:定义一个变量来存储创建好的类实例
//5:因为这个变量要在静态方法中使用,所以需要加上static修饰
private static Singleton instance = null;
//1:私有化构造方法,在内部控制创建实例的数目,防止在外部创建实例
private Singleton(){
}
//2:定义一个方法来为客户端提供类实例
//3:这个方法需要定义成类方法,也就是要加static
public static Singleton getInstance(){
//6:判断存储实例的变量是否有值
if(instance == null){
//6.1:如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
instance = new Singleton();
}
//6.2:如果有值,那就直接使用
return instance;
}
}
2、饿汉式
饿汉式的特点是饥不择食,在加载类的时候就会创建类的实例。
public class Singleton {
//4:定义一个静态变量来存储创建好的类实例
//直接在这里创建类实例,只会在类加载的时候创建一次
private static Singleton instance = new Singleton();
//1:私有化构造方法,好在内部控制创建实例的数目
private Singleton(){
}
//2:定义一个方法来为客户端提供类实例
//3:这个方法需要定义成类方法,也就是要加static
public static Singleton getInstance(){
//5:直接使用已经创建好的实例
return instance;
}
}
四、懒汉式和饿汉式的优缺点
1、时间和空间方面
- 懒汉式是典型的时间换空间**,每次获取实例时都会去判断是否需要创建实例,浪费判断时间。而如果一直没有人使用的话,就不会去创建实例,节约内存空间。
- 饿汉式是典型的空间换时间**,当类加载时就会创建实例,不管用不用,先创建出来,再以后调用时,就不需要去判断了,节省了运行时间。
2、线程安全方面
- 不加同步的懒汉式是线程不安全的,可能会出现并发问题。
- 饿汉式是线程安全的,因为虚拟机保证只会加载一次,在加载类的时候不会发生并发问题。
五、双重检查加锁方式的实现
为了解决懒汉式的线程安全问题,我们可以在获取实例方法上加上synchronized,如下:
public static synchronized Singleton getInstance(){...}
但是这样会降低整个访问的速度。那么,怎么才能既实现线程安全,又能让性能不受到很大的影响呢?我们可以利用 “双重检查加锁” 的方式来实现。
public class Singleton {
/**
* 对保存实例的变量添加volatile的修饰
*/
private volatile static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
//先检查实例是否存在,如果不存在才进入下面的同步块
if(instance == null){
//同步块,线程安全的创建实例
synchronized(Singleton.class){
//再次检查实例是否存在,如果不存在才真的创建实例
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
“双重检查加锁” 的方式会用到关键字volatile
,这里volatile的作用是防止在创建单例对象时JVM的 指令重排序 。 JVM 为了提高程序的运行效率,会对代码按照 JVM 编译器认为最优的顺序执行,从而可能打乱代码的执行顺序,这也就是我们所说的 指令重排序 。
我们先来看看我们期望的构建对象的操作指令:
- 指令1:分配一块内存 M;
- 指令2:在内存 M 上初始化 Singleton 对象;
- 指令3:然后将 M 的地址赋值给 instance 变量。
如果不加volatile
,JVM 编译器上可能不是这样,可能会被优化成如下指令:
- 指令1:分配一块内存 M;
- 指令2:将 M 的地址赋值给 instance 变量;
- 指令3:在内存 M 上初始化 Singleton 对象。
这个指令重排的优化,就可能会导致线程安全问题。假设线程1刚执行完指令2,此时instance已经不是null了,但是还没有执行指令3对instance对象进行初始化。这时候又来一个线程2,线程2看到instance不是null,直接返回instance,并调用instance的方法或者成员变量,这时将可能触发空指针异常。
六、类级内部类方式的实现
前面几种方式,都存在小小的缺陷。那么有没有什么方式,既能实现延迟加载,又能实现线程安全呢?类级内部类的方式,就同时实现了延迟加载和线程安全。
public class Singleton {
/**
* 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系,
* 而且只有被调用到才会装载,从而实现了延迟加载
*/
private static class SingletonHolder{
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static Singleton instance = new Singleton();
}
/**
* 私有化构造方法
*/
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
当getInstance()方法第一次被调用的时候,SingletonHolder类得到初始化,当SingletonHolder类被加载并初始化时,会初始化它的静态域,从而创建Singleton的实例。由于是静态的域,因此只会在虚拟机加载类的时候初始化一次,并由虚拟机来保证它的线程安全。
七、枚举方式的实现 (最佳方式)
前面几种方式都有共同的缺点,从而导致多实例的出现。
- 每次反序列化一个序列化的对象时都会创建一个新的实例。
- 可以使用反射强行调用私有构造器。
而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
单元素的枚举类型已经成为实现单例的最佳方式。
/**
* 使用枚举来实现单例模式的示例
*/
public enum Singleton {
/**
* 定义一个枚举的元素,它就代表了Singleton的一个实例
*/
INSTANCE;
/**
* 示意方法,单例可以有自己的操作
*/
public void singletonOperation(){
//功能处理
}
}
使用枚举的方式来实现单例会更加简洁,而且无偿的提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。
八、单例模式的应用场景
- 网站的计数器,一般是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都采用单例模式实现,这是由于共享的日志文件一直处于打开状态,只能有一个实例去操作,否则内容不好追加。
- Web应用的配置文件的读取,一般也采用单例模式,这个是由于配置文件是共享的资源。
- 数据库连接池的设计一般也是采用单例模式。
- 多线程的线程池的设计一般也是采用单例模式。
- 在Spring中创建的Bean实例默认都是单例模式存在的。
- 在Spring MVC框架中,每个控制器对象也是单例。