文章

设计模式 - 单例模式

单例模式

单例模式是指在应用运行过程中保证某一个类仅有一个实例,并提供一个获取该实例的方法,是一种创建对象的方式。

为什么使用单例模式

节省时间

由于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级的对象而言,是很重要的。

节约资源

不需要频繁创建对象,同时也减轻了 GC 压力

实现单例模式注意的点

  • 单例类只能有一个实例
  • 单例类的构造方法必须是私有的
  • 单例类必须提供一个静态方法给外部获取该类的唯一实例

实现单例模式的几种方法

懒汉式

在调用 getInstance 方法获取实例的时候才实例化单例类,在多线程环境中容易产生多个实例,因为这种写法并不是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LazySingleton {
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {

    }

    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

饿汉式

饿汉式的实现在类初始化的时候就创建该类的实例了,即使没有调用 getInstance() 方法。所以饿汉的缺点就是它并不是一种懒加载的方式。

1
2
3
4
5
6
7
8
9
10
11
12
public class HungrySingleton {

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

单次检查懒汉式

在创建单例类的实例之前用 synchronized 关键字同步该类,确保此时只有一个线程能执行创建实例的代码,但是如果多个线程同时调用 getInstance() 方法,同时判断 lazySingleton == null 那么就会进入 if 代码块此时 A 线程得到 CPU 的控制权进入同步代码块然后创建对象、返回对象,线程 A 完成之后,接着 B 线程得到 CPU 的控制权同样能进入同步代码块创建对象、返回对象。这种情况下返回的两个对象是不同的,所以并不是单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SingleCheckLazySingleton {


    private static SingleCheckLazySingleton lazySingleton = null;

    private SingleCheckLazySingleton() {

    }

    public static SingleCheckLazySingleton getInstance() {
        if (lazySingleton == null) {
            synchronized (DoubleCheckLazySingleton.class) {
                lazySingleton = new SingleCheckLazySingleton();
            }
        }
        return lazySingleton;
    }
}

双重检查懒汉式

双重检查懒汉式就是为了解决上一种方式还存在的问题的,在同步代码块内部再进行一次空判断,如果多个线程同时进入 if 代码块,当有一个线程完成了实例化操作之后,第二个线程在进行实例化操作之前会先检查单例对象是否已经创建,如果已经创建则返回该实例,这样就避免了创建多个实例。
另外,由于 new DoubleCheckLazySingleton() 并不是一个原子操作,使用 volatile 修饰实例变量可以确保指令不会被重排。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DoubleCheckLazySingleton {

    private volatile static DoubleCheckLazySingleton doubleCheckLazySingleton = null;

    private DoubleCheckLazySingleton() {

    }

    public static DoubleCheckLazySingleton getInstance() {
        if (doubleCheckLazySingleton == null) {
            synchronized (DoubleCheckLazySingleton.class) {
                if (doubleCheckLazySingleton == null) {
                    doubleCheckLazySingleton = new DoubleCheckLazySingleton();
                }
            }
        }
        return doubleCheckLazySingleton;
    }
}

防止指令重排序: 防止new DoubleCheckLazySingleton() 时指令重排序导致其他线程获取到未初始化完的对象。doubleCheckLazySingleton = new DoubleCheckLazySingleton(); 这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。1.给 instance 分配内存;2.调用 Singleton 的构造函数来初始化成员变量;3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后报错。

静态内部类

静态内部类也能实现单例模式的懒加载,因为内部静态类是要在有引用了以后才会装载到内存的,所以在第一次调用 getInstance() 之前,Holder 是没有被装载进来的,只有在第一次调用了 getInstance() 之后,里面涉及到了return,return Holder.instance; 才产生了对 Holder 的引用,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {

    }

    public static StaticInnerClassSingleton getInstance() {
        return Holder.STATICINNERCLASSSINGLETON;
    }

    private static class Holder {
        private static StaticInnerClassSingleton STATICINNERCLASSSINGLETON = new StaticInnerClassSingleton();
    }
}

枚举

枚举天然就是单例,是写法最简单的一种

1
2
3
4
public enum EnumSingleton {

    ENUM_SINGLETON
}

总结

实现方式是否线程安全是否懒加载
懒汉式
饿汉式
单次检查懒汉式
双重检查懒汉式
静态内部类
枚举
本文由作者按照 CC BY 4.0 进行授权