设计模式笔记(五): 单例模式

单例模式可以说是最简单的设计模式了,但是也是最容易写错的一个模式。下面来看看几种写法。

懒汉模式(线程不安全)

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的大致流程图(有些地方可能不太准确)


设计模式笔记(五): 单例模式_第1张图片
流程图

除此之外,注意到instance被声明成volatile。在本例中,volatile的主要作用是禁止指令重排。为什么要禁止指令重排呢?因为其实JVM初始化对象的时候一般分为三步。

  1. 为对象开辟内存空间
  2. 执行初始化过程(一般是构造函数)
  3. 将引用指向内存

上述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 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 设计模式》,文中代码示例出自书中。

你可能感兴趣的:(设计模式笔记(五): 单例模式)