4

单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。当系统需要某个类只能有一个实例时,就可以采用单例模式。

保证单例模式仅有一个实例的核心思想是构造方法私有化,即不允许外部调用该类的构造方法。基于此思想,主要有以下两种实现方式:

直接实例化

直接实例化这种方式也称作“饿汉式”,它直接定义了静态成员变量 s,并通过 new Singleton() 完成了初始化,之后不再变化,是线程安全的。
这种方式也存在一定的资源浪费,当没有使用 Singleton 对象时,程序依然会创建 Singleton 对象。

public class Singleton {
    private Singleton() {}
    private static final Singleton s = new Singleton();
    public static Singleton getInstance() {
        return s;
    }
}

延迟实例化

既然直接实例化浪费资源,那么我们是否可以考虑,在程序需要该对象的时候才创建它呢?当然可以!
与直接实例化稍不同,单例成员变量 s 初始为 null,它在方法 getInstance() 内部完成延迟实例化,并返回单例对象。

public class Singleton {
    private Singleton() {}
    private static Singleton s = null;
    public static Singleton getInstance() {
        if (s == null) {
            s = new Singleton();
        }
        return s;
    }
}

这种方式存在线程安全问题。例如,假设两个线程调用 getInstance() 方法,线程 1 执行完 if(s == null),条件成立,在执行实例化语句 s = new Singleton() 之前,线程 2 来了,此时线程 2 执行 if(s == null),依然成立,进入 if 语句体。这种情况带来的后果是:程序两次创建了对象,这并不符合我们对单例模式的定义。

针对这种情况,可以有以下四种解决方法:

完全同步

完全同步方法,是在方法上加上 synchronized 同步。当多线程同时访问 getInstance() 方法时,多线程是“串行”的。

public class Singleton {
    private Singleton() {}
    private static Singleton s = null;
    public static synchronized Singleton getInstance() {
        if (s == null) {
            s = new Singleton();
        }
        return s;
    }
}

这种方法,多线程每次访问 getInstance() 都必须“串行”运行,效率比较低。

部分同步

部分同步方法通过双重锁部分同步机制获得单例对象。因为代码中有两行相同的语句 if(s == null),故而叫做双重锁。第一个 if 语句可并行,当多线程均满足该条件, synchronized 修饰的代码必须串行运行。这样的话,其实只需要在第一次创建对象(通过了第一个 if 判断)的时候进行同步,效率较高。

public class Singleton {
    private Singleton() {}
    private volatile static Singleton s = null;
    public static Singleton getInstance() {
        if (s == null) {
            synchronized(Singleton.class) {
                if (s == null) {
                    s = new Singleton();
                }
            }
        }
        return s;
    }
}

注意,volatile关键字是确保当 s 被初始化成 Singleton 实例时,多个线程可以正确处理 s,即内存可见性

静态内部类

通过静态内部类 Inner 来实现单例对象。虚拟机加载应用程序字节码时,单例对象并不会立即创建,当第一次运行 Inner.s 时,单例对象才动态生成。这种实现方式无 synchronized 关键字,提高了效率。

public class Singleton {
    private Singleton() {}
    private static class Inner {
        private static final Singleton s = new Singleton();
    }
    public static Singleton getInstance() {
        return Inner.s;
    }
}

枚举

这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次。调用的时候只需要 Singleton.INSTANCE 即可。

public enum Singleton {
    INSTANCE;

    // var here
    public int var;

    // methods here
    public void otherMethods() {
        System.out.println("write other methods here...");
    }
}

enum 实现 Singleton 的三个特性:自由序列化线程安全保证单例

首先, enum 是由 class 实现的,它可以有 member 和 member function。另外,由于 enum 是通过继承 Enum 类实现的,enum 结构不能作为子类继承其他类,但可以用来实现接口。此外 enum 类不能被继承,在反编译中,可以发现该类由 final 修饰。

其次,enum 有且仅有 private 的构造器,防止外部的额外构造,这恰好与单例模式吻合。

而对于序列化和反序列化,因为每一个枚举类型和枚举变量在 JVM 中都是唯一的,即 Java在序列化和反序列化枚举时做了特殊的规定,枚举的 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve 等方法是被编译器禁用的,因此也不存在实现序列化接口后调用readObject 会破坏单例的问题。

(完)


参考资料

👉 《Head First 设计模式》
👉 《Java 设计模式及应用案例分析》
👉 《Java 枚举 enum 以及应用:枚举实现单例模式》


yanglbme
202 声望16 粉丝

GitHub: Https://github.com/yanglbme