简介
单例模式是设计模式中最常见也最为简单的一种设计模式,确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例的访问方法。通过单例模式可以保证系统中一个类只有一个实例,我们经常使用的图片加载框架Universal-Image-Loader
的实例创建就是使用了单例模式,因为Universal-Image-Loader
中包含有线程池、缓存系统、网络请求等配置很消耗资源,不应该创建多个对象,因此就需要用到单例模式来实现。
作用
单例模式(Singleton
):确保某一个类仅有一个实例,并提供一个访问它的全局访问点。
优势
- 由于有且仅有一个实例,故可以减少内存开销;
- 可以避免对资源的多重占用,避免对同一资源进行多种操作;
- 设置了全局的资源访问点,可以优化和共享全局资源访问。
适用场景
- 应用中某个实例对象需要被频繁访问;
- 应用每次启动只需存在一个实例,如账号、数据库系统等。
单例模式几个关键点
- 构造函数不对外开放,一般为
private
; - 通过一个静态方法或者枚举返回单例类对象;
- 确保单例类的对象有且只有一个,尤其在多线程环境下;
- 确保单例类对象在反序列化时不会重新构建对象。
常用的使用方式
1、饿汉式(线程安全)
public class Singleton {
// 内部定义自己的一个实例,只供内部调用
private static final Singleton instance = new Singleton();
// 私有构造方法,防止被实例化
private Singleton() {
}
// 这里提供一个可供外部直接访问的静态方法
public static Singleton getInstance() {
return instance;
}
}
优点:线程安全。
缺点:初始化的时候就实例化了。
2、懒汉式(线程不安全)
public class Singleton {
// 私有静态实例,防止被引用此处赋值为null,目的是实现延迟加载
private static Singleton instance = null;
// 私有构造方法,防止被实例化
private Singleton() {
}
// 静态方法创建实例
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:延迟加载(使用的时候才被实例化),节省资源。
缺点:线程不安全,在多线程中很容易出现不同步的情况,如在数据库对象进行频繁的读写操作时。
为什么说它是线程不安全的呢?我们假设有两个线程,A
线程和B
线程都要初始化这个实例,此时A
线程比较快,已经判断instance
为null
正在创建实例,而B
线程这时候也再判断,但此时A
线程还没有new
完,instance
还是为空的,所以B
线程也开始new
一个对象出来,这样就相当于创建了两个实例了,因此上面这种设计并不能保证线程是安全的。为了解决线程不安全问题我们可以加同步锁进行实现。
3、懒汉式(线程安全)
public class Singleton {
// 私有静态实例,防止被引用此处赋值为null,目的是实现延迟加载
private static Singleton instance = null;
// 私有构造方法,防止被实例化
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
还可以如下方式写:
public class Singleton {
// 私有静态实例,防止被引用此处赋值为null,目的是实现延迟加载
private static Singleton instance = null;
// 私有构造方法,防止被实例化
private Singleton() {
}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
优点:线程安全。
缺点:每次调用实例都要判断同步锁,造成了不必要的同步开销,导致效率降低。
4、DCL双重检验锁(Double Check Lock 线程安全)
public class Singleton {
private 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
进行修饰,它还是不安全的,有可能会出现null
的问题。为什么呢?这是因为Java
在new
一个对象的时候是无序的。我们假设在这个过程中有线程A
判断为null
了,这个时候它就进入线程锁instance = new Singleton()
了,但是它不是一蹴而就的,而是需要通过以下三步来完成:
- 为
instance
创建内存; new Singleton()
调用构造方法;instance
指向内存区域。
Java
虚拟机在执行上面这三步的时候并不是按照此顺序来执行的,可能会打乱执行顺序,这就是Java
的重排序,比如步骤2
和3
调换一下:
- 为
instance
创建内存; instance
指向内存区域;new Singleton()
调用构造方法。
此时instance
已经指向内存区域了,那么它就不为null
了,而实际上它并未获得构造方法。比如构造方法里面有些参数或者方法你并未获取,如果这个时候线程B
过来,那么线程B
在执行instance
的某些方法时就会报错。虽然这种情况是非常少见的,不过还是暴露了这种问题的存在,所以我们需要用volatile
进行修饰。
由于在JVM
编译的过程中会出现指令重排的优化过程,这就可能会出现instance
实际还没初始化的时候就有可能被分配了内存空间,也就是说可能会出现instance != null
但是又没初始化的情况,这样就会导致返回的instance
报错。在JDK 1.5
之后,官方已经注意到这种问题,因此调整了JVM
并具体化了volatile
关键字,我们可以如下定义:
private volatile static Singleton instance = null;//使用volatile关键字
我们知道volatile
的一个重要属性是可见性,即被volatile
修饰的对象,在不同线程中是可以实时更新的。也就是说线程A
修改了某个被volatile
修饰的值,那么线程B
也会知道它被修改了。同时它还有另一个禁止Java
重排序的作用,这样我们就不用担心上面这种null
的情况出现了。
但是这种方式依旧存在缺点:由于volatile
关键字会屏蔽虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此建议若没有特别的需要,不要大量使用。
优点:延迟加载,避免不必要的同步,线程安全。
缺点:在不同的编译环境下可能会出现不同的问题。
补充:Android
图片加载开源项目Universal-Image-Loader
中使用的就是这种方式。
5、内部类(线程安全)
public class Singleton {
private static class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
优点:延迟加载,线程安全,减少内存开销。
6、枚举(线程安全)
public enum Singleton {
SINGLETON;
public void showMsg() {
}
}
使用方式:
Singleton singleton = Singleton.SINGLETON;
singleton.showMsg();
优点:线程安全,延时加载,防止多次实例化,序列化和反序列化安全,即使是在反序列化的过程中枚举单例也不会重新生成新的实例。
缺点:从Java 1.5
才开始支持。
7、静态式(线程安全)
public class Singleton {
private Singleton() {
}
public static class Holder {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
}
如何进行参数传递呢?我们可以定义一个方法来实现。
public class Singleton {
private Context mContext;
private Singleton() {
}
public static class Holder {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
public void init(Context context) {
this.mContext = context;
}
}
初始化:
Singleton mSingleton = Singleton.Holder.getInstance();
mSingleton.init(this);
优点:需要的时候再去初始化且能保证线程安全。
缺点:对于参数的传递比较不好。
结语:不管以哪种方式实现单例模式,它们的核心原理就是将构造函数私有化,并且通过静态公有方法获取一个唯一的实例,在这个获取的过程中必须保证线程的安全,同时也要防止反序列化导致重新生成实例对象。Android
中常见单例模式的类有InputMethodManager
、AccessibilityManager
、Editable
等。