单例模式:顾名思义就是在程序运行期间,单例对象的类保证只有一个实例存在。
优点:1.实例控制:阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都
访问唯一实例。
2.灵活性:类自己控制了实例的过程,所以类可以更灵活的更改实例化过程。
3.提高性能:由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
4.安全性:避免对共享资源的多重占用。
缺点: 1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3.单例类的职责过重,在一定程度上违背了“单一职责原则”。
4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
实现方式:
实现方式主要分为两种:
一:懒汉模式:在全局的实例被第一次被使用时构建(分为线程不安全版本和线程安全版本);
二:饿汉模式:指全局的单例实例在类装载时构建。
日常我们使用的是懒汉模式,按需加载才能做到资源的利用的最大化。本文还有介绍到《Effective Java》一书中优化的实现方式。
一:懒汉模式
1.最简单的实现方式:
//懒汉式实现 v1
public class Single1 {
private static Single1 instance;
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
2.为了防止外部类的误调用,将构造函数设置为私有:
//懒汉式实现 v2
public class Single1 {
private static Single1 instance;
private Single1(){}
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
上面的实现方式有线程安全问题,当多线程工作时,有多个线程同时对instance == null进行判断为true时,会创建多个实例,这样一来就不是单例模式了。
对于上面的问题,有线程安全版本的懒汉模式实现方式
3.synchronized版本
对于getInstance()方法使用synchronized关键字修饰。
//懒汉式实现 v3
public class Single1 {
private static Single1 instance;
private Single1(){}
public static synchronized Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
但是用synchronized修饰后,同一时间只能有一个线程调用该方法,其他线程强制被等待,对于代码的执行效率有负面的影响。
4.为了解决上面的问题,出现了双重检查(Double-Check)的版本。
// Version 3
public class Single3 {
private static Single3 instance;
private Single3() {}
public static Single3 getInstance() {
if (instance == null) {
synchronized (Single3.class) {
if (instance == null) {
instance = new Single3();
}
}
}
return instance;
}
}
但是,对的还是有但是,但是instance = new Single3();这句话并非一个原子操作,他涉及三个步骤:
1.给 instance分配内存
2.调用 Single3 的构造函数来初始化成员变量,形成实例
3.将instance对象指向分配的内存空间(执行完这步 instance才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
所以有了第五个版本
5.volatile版本
只需要给instance的声明加上volatile关键字即可,Version5版本:
// Version 5
public class Single4 {
private static volatile Single4 instance;
private Single4() {}
public static Single4 getInstance() {
if (instance == null) {
synchronized (Single4.class) {
if (instance == null) {
instance = new Single4();
}
}
}
return instance;
}
}
volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。
注意:volatile阻止的不singleton = new
Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if
(instance == null))。
二:饿汉模式
由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够免疫许多由多线程引起的问题。实现方式如下:
//饿汉式实现
public class SingleB {
private static final SingleB INSTANCE = new SingleB();
private SingleB() {}
public static SingleB getInstance() {
return INSTANCE;
}
}
饿汉模式是看起来很完美的实现方式,但是还是存在缺点:
由于INSTANCE的初始化是在类加载时进行的,而类加载是由类加载器完成的,所以开发者对于他初始化的时机很难掌控。
1.初始化过早,造成资源的浪费;
2.初始化时有可能依赖其他数据,很难保证其他数据会在他初始化之前准备好。
什么条件会触发类被加载:
1.new一个对象时;
2.使用反射方式创建类实例时;
3.子类被加载时,如果父类没有被加载,就先加载父类;
4.jvm启动时执行的主类会被优先加载。
三. 《Effective Java》第一版推荐了一个中和写法:
// Effective Java 第一版推荐写法
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种写法非常巧妙:
对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。
同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。
——它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现。
四.《Effective Java》第二版中推荐了枚举写法:
// Effective Java 第二版推荐写法
public enum SingleInstance {
INSTANCE;
public void fun1() {
// do something
}
}
// 使用
SingleInstance.INSTANCE.fun1();
极简,且线程安全。
单例模式的适用场景:
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
1.需要频繁实例化然后销毁的对象。
2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
3.有状态的工具类对象。
4.频繁访问数据库或文件的对象。