设计模式之单例模式
文章目录
- 设计模式之单例模式
- 一. 简介
- 1. 什么是单例模式?
- 2. 单例模式的应用场景?
- 3. 单例模式的类型?
- 二. 单例模式的几种写法
- 1. 饿汉式
- 2. 懒汉式
- 3. 懒汉式(线程安全+性能优化)
- 4. 使用volatile防止指令重排
- 5. 登记式/静态内部类
- 6. 枚举
- 7. 粉碎懒汉式单例与饿汉式单例
一. 简介
1. 什么是单例模式?
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
2. 单例模式的应用场景?
- 网页中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 要求生产唯一序列号。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等
3. 单例模式的类型?
- 懒汉式:在真正需要使用对象时才去创建该单例类对象
- 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
二. 单例模式的几种写法
1. 饿汉式
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
// 饿汉式
class Singleton {
//1.私有化构造器函数
private Singleton() {}
//2.创建本类对象并指向本类引用
private final static Singleton instance = new Singleton();
//3.提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
return instance;
}
}
优点:
- 这种写法比较简单,就是在类装载的时候就完成了实例化。避免了线程同步问题。
- 在类加载的同时已经创建好一个静态对象,调用时反应速度快。
- 线程安全
缺点:
- 来类装载的时候就完成了实例化,没有达到Lazy Loading的效果。如果从始至终未使用过这个实例,则会造成实例的浪费。
2. 懒汉式
懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。,否则则先执行实例化操作。
public class Singleton {
//1.私有化构造器函数
private Singleton(){}
//2.先不创建对象
private static Singleton instance ;
//3.提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
//4.如果instance==null 的时候再去创建,否则直接返回
if (instance == null) {
instance = new Singleton();
}
return instance ;
}
}
缺点:
- 线程不安全,多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及 往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
3. 懒汉式(线程安全+性能优化)
要想懒汉式线程安全,最容易想到的方法就是加锁。
public class Singleton {
//1.私有化构造器函数
private Singleton() {}
//2.先不创建对象
private static Singleton instance;
//3.提供一个公有的静态方法,返回实例对象
public static synchronized Singleton getInstance() {
//4.如果instance==null 的时候再去创建,否则直接返回
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。
性能优化:
public class Singleton {
//1.私有化构造器函数
private Singleton() {}
//2.先不创建对象
private static Singleton instance;
//3.提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
//4.线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
if (instance == null) {
//5.线程A或线程B获得该锁进行初始化
synchronized (Singleton.class) {
if (instance == null) {
//6.其中一个线程进入该分支,另外一个线程则不会进入该分支
instance = new Singleton();
}
}
}
return instance;
}
}
上面这段代码已经近似完美了,但是还存在最后一个问题:指令重排
4. 使用volatile防止指令重排
指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能
创建一个对象,在JVM中会经过三步:
- 为singleton分配内存空间
- 初始化singleton对象
- 将singleton指向分配好的内存空间
在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。
使用volatile关键字可以防止指令重排序,其原理较为复杂,这篇博客不打算展开,可以这样理解:使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生NPE异常了。
volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。
最终的代码如下所示:
public class Singleton {
//1.私有化构造器函数
private Singleton() {}
//2.使用volatile关键字修饰的变量
private static volatile Singleton instance;
//3.提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
//4.线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
if (instance == null) {
//5.线程A或线程B获得该锁进行初始化
synchronized (Singleton.class) {
if (instance == null) {
//6.其中一个线程进入该分支,另外一个线程则不会进入该分支
instance = new Singleton();
}
}
}
return instance;
}
}
5. 登记式/静态内部类
这种方式能达到双检锁方式一样的功效,并且线程是安全的,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
public class Singleton {
// 1. 私有化构造函数
private Singleton (){}
// 2. 使用SingletonHolder类装载Singleton类
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
//3.提供一个公有的静态方法,返回装载Singleton的类
public static final Singleton getInstance() {
return SingletonHolder.instance;
}
}
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。
想象一下,如果实例化,instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第3 种方式就显得很合理。
6. 枚举
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch
提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
示例如下:
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
7. 粉碎懒汉式单例与饿汉式单例
无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。
- 利用反射破坏单例模式
public static void main(String[] args) {
// 获取类的显式构造器
Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
// 可访问私有构造器
construct.setAccessible(true);
// 利用反射构造新对象
Singleton instance1= construct.newInstance();
// 通过正常方式获取单例对象
Singleton instance2= Singleton.getInstance();
System.out.println(instance1== instance2); // false
}
利用反射,强制访问类的私有构造器,去创建另一个对象
- 利用序列化与反序列化破坏单例模式
public static void main(String[] args) {
// 创建输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
// 将单例对象写到文件中
oos.writeObject(Singleton.getInstance());
// 从文件中读取单例对象
File file = new File("Singleton.file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判断是否是同一个对象
System.out.println(newInstance == Singleton.getInstance()); // false
}
两个对象地址不相等的原因是:readObject() 方法读入对象时,它必定会返回一个新的对象实例,必然指向新的内存地址