关于单例模式,思想很简单,就是确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,减少内存开支,
主要有两大实现方式:懒汉式、饿汉式
。
- 饿汉式:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。
- 懒汉式:在类加载时不初始化,等到第一次被使用时才初始化。
打一个很形象的比喻,有两个很饿的老汉,一个很懒、一个很勤快,懒汉
要别人把饭做好送到嘴边的才肯吃;饿汉
呢就是拿着碗等着饭熟,饭一熟立马盛饭开吃
那么在程序里面,懒汉式是用的最多,因为它可以减少性能开销。但是懒汉式在单线程环境中没有任何问题,一旦处于多线程环境,那么它是线程不安全的,而如何保证线程安全,各个语言又会有不同的处理方式,本文以JAVA语言为例,来进行介绍单例模式思想
和实现单例模式的8种方式
首先我们还是以一个非常简单例子为例,介绍单例模式的思想,如果了解了单例模式,可以跳过这小节
1.我是皇帝我独苗
自从秦始皇确立了皇帝这个位置以后,同一时期基本上就只有一个人孤零零地坐在这个位置。这种情况下臣民们也好处理,大家叩拜、谈论的时候只要提及皇帝,每个人都知道指的是谁,而不用在皇帝前面加上特定的称呼,如张皇帝、李皇帝。这一个过程反应到设计领域就是,要求一个类只能生成一个对象(皇帝),所有对象对它的依赖都是相同的,因为只有一个对象,大家对它的脾气和习性都非常了解,建立健壮稳固的关系,我们把皇帝这种特殊职业通过程序来实现。
皇帝每天要上朝接待臣子、处理政务,臣子每天要叩拜皇帝,皇帝只能有一个,也就是一个类只能产生一个对象,该怎么实现呢?对象产生是通过new关键字完成的(当然也有其他方式,比如对象复制、反射等),这个怎么控制呀,但是大家别忘记了构造函数,使用new关键字创建对象时,都会根据输入的参数调用相应的构造函数,如果我们把构造函数设置为private私有访问权限不就可以禁止外部创建对象了吗?臣子叩拜唯一皇帝的过程类图
只有两个类,Emperor代表皇帝类,Minister代表臣子类,关联到皇帝类非常简单。
1 2 3 4 5 6 7 8 9 10 11 12
| public class Emperor { private static final Emperor emperor =new Emperor(); private Emperor(){ public static Emperor getInstance(){ return emperor; } public static void say(){ System.out.println("我就是皇帝某某某...."); } }
|
通过定义一个私有访问权限的构造函数,避免被其他类new出来一个对象,而Emperor自己则可以new一个对象出来,其他类对该类的访问都可以通过getInstance获得同一个对象。
皇帝有了,臣子要出场
1 2 3 4 5 6 7 8
| public class Minister { public static void main(String[] args) { for(int day=0;day<3;day++){ Emperor emperor=Emperor.getInstance();emperor.say(); } } }
|
臣子参拜皇帝的运行结果如下所示。
1 2 3 4 5
| 我就是皇帝某某某....
我就是皇帝某某某....
我就是皇帝某某某....
|
臣子天天要上朝参见皇帝,今天参拜的皇帝应该和昨天、前天的一样(过渡期的不考虑,别找茬哦),大臣磕完头,抬头一看,嗨,还是昨天那个皇帝,老熟人了,容易讲话,这就是单例模式。
2 单例模式的定义
单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:
Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)
单例模式的通用类图如下图所示
Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自行实例化的(在Singleton中自己使用new Singleton())
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Singleton { private static final Singleton singleton = new Singleton(); private Singleton(){ } public static Singleton getSingleton(){ return singleton; } public static void doSomething(){ } }
|
3 单例模式的应用
3.1 单例模式的优点
● 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
● 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。
● 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
● 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
3.2 单例模式的缺点
● 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。
● 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。
● 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。
3.3 单例模式的使用场景
在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式,具体的场景如下:
● 要求生成唯一序列号的环境;
● 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;
● 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
● 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。
4 实现单例模式的8种方式
介绍之前我们先看看java种类的加载顺序
类加载(classLoader)机制一般遵从下面的加载顺序
如果类还没有被加载:
- 先执行父类的静态代码块和静态变量初始化,静态代码块和静态变量的执行顺序跟代码中出现的顺序有关。
- 执行子类的静态代码块和静态变量初始化。
- 执行父类的实例变量初始化
- 执行父类的构造函数
- 执行子类的实例变量初始化
- 执行子类的构造函数
同时,加载类的过程是线程私有的,别的线程无法进入。
如果类已经被加载:
静态代码块
和静态变量
不再重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。
static关键字
一个类中如果有成员变量或者方法被static关键字修饰,那么该成员变量或方法将独立于该类的任何对象。它不依赖类特定的实例,被类的所有实例共享,只要这个类被加载,该成员变量或方法就可以通过类名去进行访问,它的作用用一句话来描述就是,不用创建对象就可以调用方法或者变量,这简直就是为单例模式的代码实现量身打造的。
4.1 静态常量(饿汉模式)
这一种是使用静态常量的方式创建实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class SingletonTest01 { public static void main(String[] args) { SingletonClass01 c1 = SingletonClass01.getInstance(); SingletonClass01 c2 = SingletonClass01.getInstance(); System.out.println(c1 == c2); } } class SingletonClass01 {
private SingletonClass01() {
}
private final static SingletonClass01 instance = new SingletonClass01();
public static SingletonClass01 getInstance() { return instance; } }
|
4.2 静态代码块(饿汉式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class SingletonTest02 { public static void main(String[] args) { SingletonClass02 c1 = SingletonClass02.getInstance(); SingletonClass02 c2 = SingletonClass02.getInstance(); System.out.println(c1 == c2); } }
class SingletonClass02 {
private SingletonClass02() {
}
private static SingletonClass02 instance;
static { instance = new SingletonClass02(); }
public static SingletonClass02 getInstance() { return instance; }
}
|
4.3 线程不安全(懒汉式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public class SingletonTest03 { public static void main(String[] args) {
System.out.println("多线程创建实例======="); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(SingletonClass03.getInstance()); } }).start(); } } }
class SingletonClass03 { private static SingletonClass03 instance; private SingletonClass03() { }
public static SingletonClass03 getInstance() { if (instance == null) { instance = new SingletonClass03(); } return instance; } }
|
多线程环境测试结果
1 2 3 4 5 6 7 8 9 10 11
| 多线程创建实例======= cn.mrxccc.singleton.SingletonClass03@576cc207 cn.mrxccc.singleton.SingletonClass03@576cc207 cn.mrxccc.singleton.SingletonClass03@4bd8a307 cn.mrxccc.singleton.SingletonClass03@576cc207 cn.mrxccc.singleton.SingletonClass03@576cc207 cn.mrxccc.singleton.SingletonClass03@576cc207 cn.mrxccc.singleton.SingletonClass03@576cc207 cn.mrxccc.singleton.SingletonClass03@576cc207 cn.mrxccc.singleton.SingletonClass03@576cc207 cn.mrxccc.singleton.SingletonClass03@576cc207
|
这种是线程不安全的单例模式,从多线程的测试结果可以看出,在创建实例时创建了新的对象,这样的单例模式就是线程不安全单例模式,不推荐该方式
4.4 线程安全,同步方法(懒汉式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public class SingletonTest04 { public static void main(String[] args) {
System.out.println("多线程创建实例======="); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(SingletonClass04.getInstance()); } }).start(); } } }
class SingletonClass04 { private static SingletonClass04 instance; private SingletonClass04() {}
public static synchronized SingletonClass04 getInstance() { if(instance == null) { instance = new SingletonClass04(); } return instance; } }
|
这种是线程安全的单例模式,特点就是给获取实例的方法getInstance()
加了一个synchronized关键字,加上锁使其成为一个同步方法,保证了同一时刻只能有一个线程访问并获得实例,但是缺点也很明显,因为synchronized是修饰整个方法,每个线程访问都要进行同步,而其实这个方法只执行一次实例化代码就够了,每次都同步方法显然效率低下,不推荐
4.5双重检查(懒汉式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class SingletonTest05 { public static void main(String[] args) { SingletonClass05 c1 = SingletonClass05.getInstance(); SingletonClass05 c2 = SingletonClass05.getInstance(); System.out.println(c1 == c2); System.out.println("多线程创建实例======="); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(SingletonClass05.getInstance()); } }).start(); } } }
class SingletonClass05 { private static volatile SingletonClass05 instance;
private SingletonClass05() {}
public static SingletonClass05 getInstance() { if(instance == null) { synchronized (SingletonClass05.class) { if(instance == null) { instance = new SingletonClass05(); } }
} return instance; } }
|
这种写法用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高,是对第四种写法的改进。为什么要做两次判断呢?这是为了线程安全考虑,还是那个场景,对象还没实例化,两个线程A和B同时访问静态方法并同时运行到第一个if判断语句,这时线程A先进入同步代码块中实例化对象,结束之后线程B也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了。
给对象实例加上volatile
保证变量的可见性,是为了防止指令重排,这里介绍指令重排,有点偏移文章重点,暂不介绍,关于什么是指令重排可以网上搜一下相关文章
4.6 静态内部类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public class SingletonTest06 { public static void main(String[] args) {
System.out.println("多线程创建实例======="); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(SingletonClass06.getInstance()); } }).start(); } } }
class SingletonClass06 {
private SingletonClass06() {}
private static class SingletonInstance { private final static SingletonClass06 INSTANCE = new SingletonClass06(); }
public static SingletonClass06 getInstance() { return SingletonInstance.INSTANCE; } }
|
这是很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化。
同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,并且这个过程也是线程安全的。
推荐使用
4.7枚举
这种写法在《Effective JAVA》中大为推崇,它可以解决两个问题:
1)线程安全问题。因为Java虚拟机在加载枚举类的时候会使用ClassLoader的方法,这个方法使用了同步代码块来保证线程安全。
2)避免反序列化破坏对象,因为枚举的反序列化并不通过反射实现。
推荐使用
4.8 静态内部类+防止反射破坏单例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class SingletonTest08 { public static void main(String[] args) throws Exception { Class clazz = SingletonClass08.class; Constructor constructor = clazz.getDeclaredConstructor(null); constructor.setAccessible(true); Object c1 = constructor.newInstance(); Object c2 = SingletonClass08.getInstance(); System.out.println(c1 == c2); } }
class SingletonClass08 {
private SingletonClass08() { if (SingletonInstance.INSTANCE != null) { throw new RuntimeException("不允许创建多个实例"); } }
private static class SingletonInstance { private static final SingletonClass08 INSTANCE = new SingletonClass08(); }
public static SingletonClass08 getInstance() { return SingletonInstance.INSTANCE; } }
|
网上的大部分文章介绍单例模式都没有介绍到这一种,这里使用了抛异常的方式防止反射进行创建多个示例
5 总结
至此,关于单例模式的介绍和场景使用已经介绍完了,有任何疑问和沟通讨论的可以给博主留言哦
其他设计模式介绍:
设计模式-工厂方法模式
设计模式-抽象工厂模式
设计模式-建造者模式
更多精彩内容:mrxccc