单例模式可以说是最简单的设计模式了,但是也是最容易写错的一个模式。下面来看看几种写法。
懒汉模式(线程不安全)
public class Singleton1 {
private static Singleton1 instence;
private Singleton1() {
}
public static Singleton1 getInstance() {
if (instence == null) {
instence = new Singleton1();
}
return instence;
}
}
首先在类里声明了私有的静态字段,但是先不初始化,在第一次获取实例的时候,如果对象没有被初始化过,就先初始化,最后返回对象实例,否则直接返回对象实例。
单线程下这种写法没有什么问题,还节省了一些启动时的资源。但在多线程环境下,可能会实例化多个对象,不符合单例的要求。
懒汉模式(线程安全)
public class Singleton2 {
private static Singleton2 instence = new Singleton2();
private Singleton2() {
}
public static Singleton2 getInstance() {
return instence;
}
}
在声明静态字段的时候进行初始化,因为字段是静态字段,所以在该类被加载的时候就会执行初始化操作了,而类加载的过程肯定是线程安全的(这由JVM保证的)。这种方式在类加载的到时候会比较慢。
懒汉模式(线程安全)
public class Singleton3 {
private static Singleton3 instence;
private Singleton3() {
}
public synchronized static Singleton3 getInstance() {
if (instence == null) {
instence = new Singleton3();
}
return instence;
}
}
和线程不安全的懒汉模式只是在方法上多了synchronized 关键字,即给该方法上了内置锁。这种方法确实是线程安全的,每次访问都是有同步开销,造成不必要的资源浪费,而且其实大部分情况下是不需要同步操作的,不推荐这种方式。
双重检查模式(DCL)
public class Singleton4 {
private volatile static Singleton4 instence;
private Singleton4() {
}
public static Singleton4 getInstance() {
if (instence == null) {
synchronized (Singleton4.class) {
if (instence == null) {
instence = new Singleton4();
}
}
}
return instence;
}
}
之所以要双重检查,是因为有两次判断instance == null。这样做比上面的那种同步算是减少了锁的粒度,减少了一些同步开销。下图是两个线程执行getInstance的大致流程图(有些地方可能不太准确)
流程图
除此之外,注意到instance被声明成volatile。在本例中,volatile的主要作用是禁止指令重排。为什么要禁止指令重排呢?因为其实JVM初始化对象的时候一般分为三步。
- 为对象开辟内存空间
- 执行初始化过程(一般是构造函数)
- 将引用指向内存
上述2,3的顺序完全可以调换,因为他们之间不存在依赖关系,也许JVM会为了优化二调换顺序。如果调换了顺序会怎样呢?假设线程A已经获取了锁,并且执行到了将引用指向内存(注意,执行初始化过程还没开始,此时instance已经不为null),这时候线程C调用getInstance方法,然后判断instance == null ? 结果会是false,最后返回instance,这样调用方就会拿到一个无效的instance!!volatile的作用就是保证先执行初始化过程,再将引用指向内存,这样就可以避免上述的异常情况。
静态内部类单例模式
public class Singleton5 {
private Singleton5() {
}
public static Singleton5 getInstance() {
return Singleton5Holder.instance;
}
private static class Singleton5Holder{
private static final Singleton5 instance = new Singleton5();
}
}
这里静态内部类是不会被提前加载的,只有再第一次调用getInstance的时候才会加载该类。所以这种方式既保证了线程安全,又不会导致类的提前加载,防止了资源浪费。是比较推荐的一种做法。
枚举单例
public enum Singleton6 {
INSTANCE;
}
对,就一行代码,因为枚举类型默认是单例的,而且是线程安全的。但是说句实话,枚举的可读性挺差的,所以很少在项目中看到这样的单例写法。
使用容器实现单例模式
public class Singleton7 {
private static Map<String, Object> objMap = new HashMap<>();
private Singleton7() {
}
public void addInstance(String key, Object instance) {
objMap.putIfAbsent(key, instance);
}
public Object getInstance(String key) {
return objMap.get(key);
}
}
这里使用HashMap作为容器实现单例,putIfAbsent()方法保证了如果容器里某个键的值为null的时候才插入,这就只会有一个实例对象。
Spring的IOC容器就是类似的方式实现的。
小结
到此,一共写了7中单例模式(已经不少了吧)。各个方式都有各自的优缺点,具体使用哪种方式,应该取决于项目,多多斟酌。
单例模式的定义:确保一个类只有一个实例,并提供全局访问点
本系列文章参考书籍是《Head First 设计模式》,文中代码示例出自书中。
网友评论