单例模式的含义:
程序运行过程中,希望某个实体(对象)在内存中只有一个实例。
使用条件:
- 该实例的创建比较消耗资源;
- 该实例有单一性,比如某个 Fragment,不希望每次使用时就重新创建。
比如使用 Glide 加载图片,获取 Glide 对象的过程就是一个单例模式。因为 Glide 对象的创建过程相当复杂,而且在使用的过程中只需要一个可用 Glide 对象。
public static Glide get(Context context) {
if(glide == null) {
Class var1 = Glide.class;
synchronized(Glide.class) {
if(glide == null) {
Context applicationContext = context.getApplicationContext();
List modules = (new ManifestParser(applicationContext)).parse();
GlideBuilder builder = new GlideBuilder(applicationContext);
...
glide = builder.createGlide();
...
}
}
}
return glide;
}
单例模式的创建:
一、饿汉式
一般来说创建对象首先想到构造函数,那么单例的创建可用先从某对象的构造函数说起。
- 首先来说因为该类的单例对象只创建一次,可用考虑使用 static 修饰,这样在 JVM 加载该类的时候就会自动创建对象;
- 接下来我们不希望其他类执行该单例类的构造方法再去创建单例对象,所以把构造函数的属性设置为 private;(感叹 Java 的封装设计)
- 那么存在单例对象就需要把这个对象暴露出去,通过 public 的方法无疑是一种比较好的方式。
这里为什么不直接把单例设置为 public 呢?这里就联系到设计原则:迪米特原则了,其他类对该单例类了解的尽量少。其他类获取该单例类的对象只需要通过其方法暴露出来即可,而不需要了解单例具体是怎么创建的。假如该单例类创建的过程变得更加复杂,其他类的调用还是通过这个简单的方法获得对象而不用关心单例类增加了哪些代码。
public class Singleton {
// 私有单例变量,在 JVM 加载时就会自动创建
private static Singleton INSTANCE = new Singleton();
// 创建私有构造方法,避免其他类再创建对象
private Singleton(){
}
// 向其他类暴露获取实例的方法
public static Singleton newInstance(){
return INSTANCE;
}
}
那么这种创建方式就是 饿汉式,这种创建方式特点:
- 优点:创建简单、线程安全:因为依赖 JVM 的类加载机制,执行类初始化的时候 JVM 会获取一个锁,可以同步多个线程对一个类完成初始化
- 缺点:不适合创建复杂,占用内存大的单例对象
- 应用场景:初始化简单、创建速度快
二、懒汉式
1. 懒汉式(普通使用)
或许我们需要更加灵活地创建和销毁某个单例对象,又或许需要使用的单例对象比较复杂占用内存比较多,所以需要一种灵活的按需创建的方式:
- 与之前的逻辑类似,依旧不想被其他类通过构造方法或直接获取单例对象,所以构造函数和单例对象应该是 private 的;
- 需要一个方法把单例对象暴露出去,这个方法中灵活创建单例对象。
public class Singleton {
// 定义私有变量并设置初始为 null
private static Singleton INSTANCE = null;
// 创建私有构造方法,避免其他类再创建对象
private Singleton(){
}
// 向其他类暴露获取实例的方法,需要时再创建
public static Singleton newInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
这种创建方式就是懒汉式,但是这种方式存在明显的问题,就是线程安全问题:
-
缺陷:在多线程的情况下调用获取单例对象 INSTANCE,假设线程 A 调用
newInstance()
时单例对象还未创建,执行创建过程。线程 B 也来到这个方法来判断单例对象是否创建,这时由线程 A 申请创建的单例对象还未创建成功,于是又执行一次创建过程。这样在内存中就存在两个 Singleton 对象的实例,单例模式也就失去了意义。
2. 懒汉式(同步锁)
既然这种方式有缺陷而且是线程安全问题,那么就加个同步锁 synchronized
来访问创建单例对象的部分:
public class Singleton {
// 定义私有变量并设置初始为 null
private static Singleton INSTANCE = null;
// 创建私有构造方法,避免其他类再创建对象
private Singleton(){
}
// 加入同步锁,确保同一时刻只有一个线程访问该方法
public static synchronized Singleton newInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
// *第二种写法
=========================================================
public static Singleton newInstance(){
if (INSTANCE == null){
synchronized (Singleton.class){
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
- 优点:防止多个线程同步访问
newInstance()
反复创建实例 - 缺点:每次访问都要进行线程同步,造成过多的同步开销(加锁 = 耗时、耗能)
3. 懒汉式(双重校验)
public class Singleton {
// 使用 volatile 关键字
private static volatile Singleton INSTANCE = null;
// 创建私有构造方法,避免其他类再创建对象
private Singleton(){
}
public static Singleton newInstance(){
// 第一重判断
if (INSTANCE == null){
synchronized (Singleton.class){
// 第二重判断
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
- 优点:多加入一层判断,一定程度上避免多次进入同步锁方法。
volatile
关键字保证线程在使用该变量时,都去堆里拿最新的数据。 - 缺点:判断略多,容易混淆。
三、静态内部类
利用 Java 静态类的特点来创建单例对象。
public class Singleton {
private static class SingletonStatic{
// 在静态内部类中加载外部类静态对象
private static Singleton INSTANCE = new Singleton();
}
// 私有构造器
private Singleton(){
}
// 获取静态内部类实例化的对象
public static Singleton newInstance(){
return SingletonStatic.INSTANCE;
}
}
上面代码可以看出:
- 使用静态内部类来创建外部类静态对;
- 使用
newInstance()
时才会加载静态内部类,创建单例对象并返回; - 该静态内部类只会被 JVM 加载一次,利用这个特性解决线程同步问题。
四、枚举方式创建单例
public enum Singleton {
INSTANCE;
// 隐藏默认的空的私有构造方法
// private Singleton(){}
}
// 使用
Singleton singleton = Singleton.INSTANCE;
这种创建方式利用枚举的特性保证了 按需加载、线程同步。
参考资料: