Skip to content

Latest commit

 

History

History
173 lines (130 loc) · 6.52 KB

单例模式.md

File metadata and controls

173 lines (130 loc) · 6.52 KB

一、单例模式

单例模式属于创建型模式, 是 Java 中最简单的设计模式之一, 它提供了一种创建对象的最佳方式,

这种模式涉及到一个单一的类, 该类负责创建自己的对象, 同事确保只有单个对象被创建, 提供唯一对象的创建方式, 可以直接访问, 不需要实例化该类的对象

注意:

  1. 单例类只能有一个实例
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类必须给其他所有对象提供这一实例

二、适用场景

  1. 需要生成唯一序列的环境
  2. 需要频繁实例化然后销毁的对象
  3. 创建对象时耗时过多或者耗资源过多, 但是有经常用到的对象
  4. 方便资源互相通信的环境

三、单例模式的优缺点

优点:

  1. 在内存中只有一个对象, 节省内存空间;
  2. 避免频繁的创建销毁对象, 可以提高性能;
  3. 避免对共享资源的多重占用, 简化访问;
  4. 为整个系统提供一个全局访问点.

缺点:

  1. 不适用于变化频繁的对象;
  2. 滥用单例模式带来的一些问题, 如为了节省资源将数据库连接池对象设计为单例, 可能会导致共享连接池对象的程序过多而出现连接池溢出;
  3. 如果实例化的对象长时间不被利用, 系统会认为该独享是垃圾而被回收, 可能导致对象状态的丢失;

四、懒汉式

像不安全的懒汉式一样, 这种方式也具有很好的懒加载, 适用于多线程, 但是效率很低

优点: 第一次调用才会初始化, 避免内存浪费

缺点: 必须加锁才能保证单例, 加锁会影响性能

public class Singleton2 {
    private static Singleton2 singleton2;

    private Singleton2() {}

    // synchronized 保证线程安全
    public synchronized static Singleton2 getSingleton2() {
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}

五、饿汉式

这种方式比较常用, 但是容易产生垃圾对象, 饿汉式是线程安全的, 不是懒加载的

优点: 没有加锁, 执行效率会高

缺点: 类加载的时候就初始化, 浪费内存

懒汉式基于 classloader 机制避免了多线程的同步问题, 不过, instance 在类加载的时候就实例化了

public class Singleton3 {
    private static Singleton3 singleton3 = new Singleton3();

    private Singleton3() {}

    public static Singleton3 getSingleton3() {
        return singleton3;
    }
}

六、双检锁/双重校验锁

要求 jdk 必须大于 1.5, 这种方式是懒加载 且 线程安全的

这种方式采用双锁机制, 安全且在多线程的情况下能保持高性能

public class Singleton4 {
    /** 
     * jdk1.5 及以上, volatile 可以防止 cpu 重排序, 如果不加 volatile 在理论上可能会出现获取到空对象的情况, 原因如下:
     * 
     * 在Java中,对象的创建大致分为以下几个步骤:
     *  1.分配内存
     *  2. 初始化对象(调用构造函数)
     *  3. 将对象的引用指向内存地址
     *  
     *  在多线程环境中,如果不进行适当的同步,CPU的重排序可能导致线程看到的对象状态不一致。在对象创建过程中,CPU重排序可能发生在以下步骤:
     *  1. 分配内存
     *  2. 将对象引用指向内存地址
     *  3. 初始化对象
     */
    private volatile static Singleton4 singleton4;

    private Singleton4() {}

    public static Singleton4 getSingleton4() {
        if(singleton4 == null){
            synchronized (Singleton4.class) {
                if (singleton4 == null) {
                    singleton4 = new Singleton4();
                }
            }
        }
        return singleton4;
    }
}

七、登记式/静态内部类

这种方式不仅是懒加载的, 而且是线程安全的, 这种方式能达到双检锁方式一样的效果, 但是实现起来更简单。

这种方式只适用于静态域的情况, 双检锁的方式可以在实例域需要延迟初始化时使用。

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 饿汉式式 不同的是:第 饿汉式式 只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

public class Singleton5 {
    private Singleton5() {
    }

    private static class SingletonHolder {
        static final Singleton5 INSTANCE = new Singleton5();
    }

    public static final Singleton5 getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public static void main(String[] args) {
        Singleton5Runnable singleton5Runnable = new Singleton5Runnable();
        new Thread(singleton5Runnable).start();
    }
}

八、枚举

要求 jdk1.5 及以上, 不是懒加载的, 是多线程安全的

这种方式没有被广泛采用, 但是这是实现单例模式的最佳方式, 它更简洁, 这种方式是 《Effective Java》作者Josh Bloch 提倡的方式, 它不仅仅能够避免多线程同步问题, 而且还自动支持序列机制, 防止反序列化重新创建的对象, 绝对防止多次实例化

public enum Singleton6 {
    instance;
    private EnumResource enumResource;

    private Singleton6() {
        enumResource = new EnumResource();
    }

    public EnumResource getInstance() {
        return enumResource;
    }
}

class EnumResource {
    public static void main(String[] args) {
        System.out.println(Singleton6.instance.getInstance());
        System.out.println(Singleton6.instance.getInstance());
        System.out.println(Singleton6.instance.getInstance());
    }
}

九、总结

一般情况下,不建议使用 懒汉方式, 建议使用 饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用 登记方式。如果涉及到反序列化创建对象时,可以尝试使用 枚举方式。如果有其他特殊的需求,可以考虑使用 双检锁方式。