单例设计模式实现总结

三世轮回 提交于 2020-03-01 19:16:29

单例模式的总体概述

单例模式,属于创建型模式,《设计模式》一书对它做了定义:保证一个类仅有一个实例,并提供一个全局访问点。

单例模式适用于无状态的工具类、全局信息类等场景。例如日志工具类,在系统中记录日志;假设我们需要统计网站的访问次数,可以设置一个全局计数器。

单例模式的优势有

  • 在内存里只有一个实例,减少了内存开销;
  • 可以避免对资源的多重占用;
  • 设置全局访问点,严格控制访问。

单例模式的研究重点大概有以下几个:

  1. 构造私有,提供静态输出接口
  2. 线程安全,确保全局唯一
  3. 延迟初始化
  4. 防止反射攻击
  5. 防止序列化破坏单例模式

多种实现方式与比较

线程安全的饿汉模式

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

    private HungrySingleton() {
    }

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

也可以通过静态代码块的形式实现。实现与静态常量基本相同,只是把实例化过程放到了静态代码块中。

private final static HungrySingleton2 instance;
static {
    instance = new HungrySingleton2();
}

饿汉单例模式的特点有

  • 实现简单
  • 线程安全
  • 类加载时初始化实例

线程安全的懒汉单例模式

懒汉式用于解决延迟初始化问题,用到了才实例化。

public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {

    }

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

对于多线程来说,上面的实现存在竞态条件:先检查后执行,无法保证全局唯一。

通过给getInstance方法添加synchronized修饰,或者同步代码块形式很容易实现线程安全,保证全局唯一。

public synchronized static LazySingleton getInstance() {...}
或者
public static LazySingleton getInstance() {
    synchronized (LazySingleton.class) {
        if (instance == null) {
            instance = new LazySingleton();
        }
    }
    return instance;
}

然而代码会对性能造成影响,在第一个实例创建成功后,我们便不再需要锁。因此外层再对instance做一次空判断,即双重检查锁定。

双重检查锁定和volatile优化

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton instance;

    private LazyDoubleCheckSingleton() {
    }

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

但是JVM即时编译器中存在指令重排序优化。instance赋值语句包含了下面3个操作:

  1. 分配内存给对象
  2. 初始化对象
  3. 设置instance引用,指向刚分配的内存地址

程序执行时,步骤2和3可能出现重排序,导致instance先指向了内存地址,再初始化对象。其他线程外层校验是instance不为空,调用未完成初始化对象的方法会报空指针异常。

禁止指令重排序是volatile的两大特性之一。使用volatile修饰instance,在赋值操作后加入内存栅栏,赋值之前的所有操作均可见。

静态内部类实现延迟加载

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {
    }

    private static class InnerClass {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.INSTANCE;
    }
}

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当外部类第一次被加载时,并不需要去加载InnerClassr,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载InnerClass类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

在静态内部类的初始化阶段(class文件被加载后,被线程使用之前),执行类的初始化,JVM会获取一个类Class对象的初始化锁,锁可以同步多个线程对一个类的初始化。因此,类初始化允许重排序,非构造线程是无法看到重排序的。

单元素枚举实现单例模式

《Effective Java》推荐使用单元素枚举类型实现Singleton,书中这样描述“功能上与公有域方法类似,但更加简洁无偿地提供了序列化机制,绝对防止多次实例化”。

public enum EnumSingleton {
    INSTANCE {
        @Override
        protected void print() {
            System.out.println("使用枚举构建单例模式");
        }
    };

    protected abstract void print();

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        EnumSingleton instance = EnumSingleton.getInstance();
        instance.print();
    }
}

反编译代码

public abstract class EnumSingleton extends Enum
{

    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(singleton/EnumSingleton, name);
    }

    private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

    protected abstract void print();

    public static EnumSingleton getInstance()
    {
        return INSTANCE;
    }

    public static void main(String args[])
    {
        EnumSingleton instance = getInstance();
        instance.print();
    }

    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton $VALUES[];

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0) {
            protected void print()
            {
                System.out.println("enum singleton");
            }
        };
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

从反编译出的代码中可以看出,EnumInstance类在加载时,就把INSTANCE属性初始化好了,和饿汉模式类似。

  • 类final--不能被继承
  • 构造器私有--不能外部实例化
  • 类变量是静态的--类加载初始化

【注】在EnumInstance类中,不定义print()方法的话,class反编译后是final类型的。

各种实现方式的选取

最好的实现方式是枚举,可以避免反射和序列化对单例模式的破坏;不能使用线程不安全的实现方式;如果程序一开始要加载的资源太多,就应该选取懒加载;饿汉单例模式在对象创建需要配置文件时不适用。

下一小节:《如何避免反射和序列化破坏单例模式》

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!