1、为什么要使用单例?
一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
2、单例的实现
饿汉式
1 2 3 4 5 6 7 8
| class EagerSingleton { private static final EagerSingleton instance = new EagerSingleton(); private EagerSingleton() { }
public static EagerSingleton getInstance() { return instance; } }
|
饿汉式带来的问题是一上来就会创建对象,一定程度上造成资源的浪费。不过优点在于编写方式简单,容易理解。
懒汉式
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class IdGenerator { private AtomicLong id = new AtomicLong(0); private static IdGenerator instance; private IdGenerator() {} public static synchronized IdGenerator getInstance() { if (instance == null) { instance = new IdGenerator(); } return instance; } public long getId() { return id.incrementAndGet(); } }
|
双重检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class LazySingleton { private volatile static LazySingleton instance = null;
private LazySingleton() { }
public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; } }
|
双重校验的懒汉式解决了饿汉式一上来就创建对象的问题,一定程度上做到了按需创建。同时双重校验机制只会在第一次创建实例时,有锁的介入,一旦实例创建成功,下次再获取实例就不会进入锁块了。
不过有个问题要注意,CPU 指令重排序可能导致在 LazySingleton 类的对象被关键字 new 创建并赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。这样,另一个线程就使用了一个没有完整初始化的 LazySingleton 类的对象。要解决这个问题,我们只需要给 instance 成员变量添加 volatile 关键字来禁止指令重排序即可。
静态内部类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Singleton { private Singleton() { }
private static class HolderClass { private final static Singleton instance = new Singleton(); }
public static Singleton getInstance() { return HolderClass.instance; }
public static void main(String args[]) { Singleton s1, s2; s1 = Singleton.getInstance(); s2 = Singleton.getInstance(); System.out.println(s1==s2); } }
|
静态内部类方式创建单例是最建议采用的一种方式,代码量较懒汉式少,同时做到了多线程安全,同时没有锁的消耗;对比饿汉式,静态内部类只有在第一次使用的时候才会创建实例,保证了对象按需获取。
instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。
枚举
1 2 3 4 5 6 7 8 9 10
| public enum IdGenerator { INSTANCE; private AtomicLong id = new AtomicLong(0); public long getId() { return id.incrementAndGet(); } }
|
这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。
3、单例存在哪些问题?
▪ 单例对 OOP 特性的支持不友好
▪ 单例会隐藏类之间的依赖关系
▪ 单例对代码的扩展性不友好
▪ 单例对代码的可测试性不友好
▪ 单例不支持有参数的构造函数
4、单例与静态类的区别?
静态方法无法支持延迟加载
5、有何替代的解决方案?
▪ 静态方法来实现
▪ 通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证
6、如何理解单例模式中的唯一性?
通常指进程级别保证唯一性
7、如何实现线程唯一的单例?
通过 HashMap 的方式或者 ThreadLocal
8、如何实现集群环境下的单例?
▪ 把这个单例对象序列化并存储到外部共享存储区(比如文件)
▪ 为了保证安全,操作时要考虑加锁,用完后主动释放
9、如何实现一个多例模式?
可以通过 HashMap 限定某个类仅能创建3个对象
10、对于 Java 语言来说,单例类对象的唯一性的作用范围并非进程,而是类加载器(Class Loader),为什么?
Java 中加载一个类是通过 ClassLoaderX + pacakge.class 的方式(双亲委派+类全限定名)唯一确定一个类。而单例指的是一个类对应一个对象。所以可以说 Java 中的单例是类加载器级别的。
参考