设计模式 创建型 单例模式

238 阅读5分钟

何时使用单例?

当我们需要统一管理资源,共享资源的时候就需要使用单例模式。

单例存在的问题?

  • 优点
    • 单例模式提供了唯一实例的受控访问,因为单例模式封装了他的唯一实例,所以他可以严格控制客户怎样以及何时访问它。
    • 由于系统中只存在一个资源,对于一些需要频繁创建和销毁的对象单例模式可以提高性能。
    • 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例模式相似的方法来来获得指定个数的对象实例,既节省系统资源,又解决了单例对象共享过多有损性能的问题。
  • 缺点
    • 由于单例模式没有抽象层,因此单例类的扩展有很大的困难。
    • 单例类的职责过重,在一定程度上违背了‘单一职责的原则’。因为单例类既承担了工厂的角色,提供了工厂方法,又充当了产品的角色,包含了一些业务方法,将产品的创建和产品本身的功能融合到一起。
    • 现在很多面相对象语言的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为他是垃圾,自动销毁回收。下次使用时再重新实例化,这将导致共享的单例对象状态的丢失。

科普:什么叫懒汉模式?什么叫饿汉模式?

  • 懒汉模式:特点就是懒,不用的时候就是睡觉(不实例化)。
  • 饿汉模式:特点就是饿,一上来就是吃(实例化)。

实现方式

最简单的懒汉实现,非线程安全

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

为什么说上面的实现是线程不安全的呢?因为两个线程几乎同时调用 getInstance() 第一个进入的线程判断 instance 为空,开始走这一行 instance = new Singleton();。注意,是开始走这一行,并未完成实例化! 此时第二个线程也走到 if (instance == null)此时判断也为空。那么 这两个线程都会得到一个新的实例,那么就产生两个实例。

懒汉实现,线程安全

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

通过在 getInstance()方法前,加一个synchronized同步,来一个要等里面的代码执行完了,第二个线程再进去,当第二个线程进去的时候,instance就不为空了,就可以实现单例。但是多数时候我们并不是两个或多个线程同时来访问,我们并不需要去同步。这样做其实造成了不必要的开销。

懒汉模式,双重检验锁

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {//Single Checked
            synchronized (Singleton.class) {
                if (instance == null) {//Double Checked
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

先检测再同步。Single Checked足以应付多数检测。当一个以上的线程同时访问时就用上了Double Checked防止多线程下重复创建实例。即使我们这么想到这么吊的方法,还是不行。。。为啥?因为instance = new Singleton() 这个不是原子操作。当我们 new 的时候 JVM 大概做了这些事:

  1. 给instance分配内存
  2. 调用 Singleton的构造函数,来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就不为 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

懒汉模式,双重检验锁,禁止指令重排

public class Singleton {
    private volatile static Singleton instance;//声明成 volatile

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {//Single Checked
            synchronized (Singleton.class) {
                if (instance == null) {//Double Checked
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

恶汉模式

public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

是这样的,因为单例的实例 instance 被声明称 static 和 final 了,在第一次加载类到内存中就会被实例化。所以创建实例本身是线程安全的。一上来就加载,所以是饿汉。

缺点:太着急加载。有时候我们想加点料也不给机会。有时候我们创建实例需要依赖参数或者配置文件。这样就不能使用饿汉模式。

恶汉模式,静态内部类

public class Singleton {
    private Singleton() {
    }

    public static final Singleton getInstance() {
        return SingleHolder.INSTANCE;
    }

    private static class SingleHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。能完美应对多数场景。

枚举单例

public enum Singleton {
     //理解为 public static final Singleton INSTANCE;
     INSTANCE;
 }

单元素的枚举类型已经成为实现Singleton的最佳方法。