懒汉单例
首先,写一个比较简单的懒汉模式的单例
public class SimpleSingleton {
private static SimpleSingleton singleton;
private SimpleSingleton() {
}
public static SimpleSingleton getInstance() {
if (singleton == null) { // 1.判断是否为空
singleton = new SimpleSingleton(); //2.进行初始化的操作
}
return singleton;
}
}
懒汉的意思呢,就是我特别懒,我想吃东西的时候我才会去准备.
对应单例来说,就是每次使用都要尝试去创建单例对象。
以上的单例模式存在着一定的问题,首先都会想到的,就是多线程的问题。
因为1,2并不是原子性的操作
如果在多线程访问的情况下,其中一个线程执行到了第2步
在初始化成员变量时如果没有执行完,这个时候另外的线程 进行 第1步,判断成员变量依然为空,将执行instance 的 初始化,如此,会出现多个线程都进行初始化操作,从而获取不同的单例对象,就不符合单例的要求了。
既然原理了解后,可以进行下测试,借用IDEA的Debug 工具
将第一个线程Thread0 阻塞在 instance 初始化时,继续执行其他线程
最终的结果,则为 Thread0 和 其它线程获取的 对象并非同一个
如果存在多线程问题,我们想到的第一方法就是 加锁,如下所示
public static SimpleSingleton getInstance() {
synchronized (SimpleSingleton.class) {
if (singleton == null) { // 1.判断是否为空
singleton = new SimpleSingleton(); //2.进行初始化的操作
}
}
return singleton;
}
这样第1步和第2步就变成了原子操作,但是也导致了多线程的串行化,对效率存在一定的影响,
因在此基础上又出现了一种解决方案,通过Double Checking Locking(DCL) 将 第1步前置,相当于instance初始化完成后的其他线程获取实例时并不会存在加锁和解锁的开销,毕竟锁还是比较影响性能的。
经过修改后,代码如下所示
public static SimpleSingleton getInstance() {
if (null == singleton) {
synchronized (SimpleSingleton.class) {
if (singleton == null) { // 1.判断是否为空
singleton = new SimpleSingleton(); //2.进行初始化的操作
}
}
}
return singleton;
}
但以上代码 依然会存在问题,这就要回到之前第2步 在底层执行的操作问题,分为三个操作:
(1)分配一块内存
(2)在内存上初始化成员变量
(3)将instance 引用指向内存
由于操作(2) 和 (3)之前会存在 重排序,即先将instance执向内存,在初始化成员变量。
此时,另外一个线程可能拿到一个未完全初始化的对象。
如果直接访问单例对象中的成员变量,就可能出错。
造成了一个典型的“构造函数溢出”问题。
解决方法也比较简单,使用volatile 对instance 进行修饰,代码如下:
private volatile static SimpleSingleton singleton;
private SimpleSingleton() {
}
public static SimpleSingleton getInstance() {
if (null == singleton) {
synchronized (SimpleSingleton.class) {
if (singleton == null) { // 1.判断是否为空
singleton = new SimpleSingleton(); //2.进行初始化的操作
}
}
}
return singleton;
}
这样多线程安全的问题就解决了。至于 volatile 关键字的作用,这要讲起来内容可就多了,暂时只谈单例相关的/
当然懒汉的单例模式在实际项目中使用的并不是很多,下面分析一下饿汉的单例模式。
饿汉单例
饿汉的意思就是我一直都是饿的,因此只好把食物都准备好,我想吃就吃;
public class HungrySingleton {
private final static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
看样子饿汉比较简单哈,示例可以参考 Java 中的Runtime 类,非常典型的饿汉模式
存在问题,序列化和反序列化问题
public class HungrySingleton implements Serializable {
private final static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singletonFile"));
out.writeObject(instance);
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("singletonFile"));
HungrySingleton newInstance = (HungrySingleton) inputStream.readObject();
System.out.println(instance);
System.out.println(newInstance);
}
}
执行结果:instance 和 newInstance 并非同一个对象
这就需要去仔细走读下反序列化的源码了,通过代码走读会发现 反序列化 通过了 反射的方法创建了对象,则造成了反序列化后的对象与序列化之前并不是同一个
具体代码走读,可参考链接 单例、序列化和readResolve()方法 - 掘金
解决方案的代码
public class HungrySingleton implements Serializable {
private final static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
// 用于解决序列化和反序列化的问题
public Object readResolve() {
return instance;
}
}
在反序列化时,会调用readResolve方法,通过反射获取原来的对象,对进行替换,这样,就可以保证序列化前后的对象是同一个了。
既然在反序列化过程中,出现了这个问题,那么在项目中也有可能有人会使用反射来创建该对象,如此就又会出现问题,
简单的解决办法就是,禁止该实例通过反射进行创建
public class HungrySingleton implements Serializable {
private final static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
if (instance != null) {
throw new RuntimeException("禁止通过反射创建");
}
}
public static HungrySingleton getInstance() {
return instance;
}
public Object readResolve() {
return instance;
}
}
当然,这个方法不能用于懒汉模式,因为懒汉模式是属于延迟创建,不能创建时进行抛错吧。
静态内联单例
饿汉单例还存在着延迟加载的问题,例如 instance 会随项目一同初始化,有可能在之后的项目运行中,很长时间都不会用到,但是却一直常驻内存,浪费资源 。
可以通过 Inner Class 的方法解决
public class StaticInnerClassSingleton {
public static class InnerClass {
static {
System.out.println("inner class");
}
private static StaticInnerClassSingleton staticInnerClassSingleton;
}
private StaticInnerClassSingleton() {}
public static StaticInnerClassSingleton getInstance() {
System.out.println("get instance");
return InnerClass.staticInnerClassSingleton;
}
public static void main(String[] args) {
StaticInnerClassSingleton.getInstance();
}
}
这样就保证了当需要该对象时才会在内存中初始化。
容器单例
public class ContainerSingleton {
private static HashMap map = new HashMap<String, Object>();
private ContainerSingleton() {}
public static void putInstance(String key, Object o) {
if (key != null && key.length() > 0 && o != null){
map.put(key,o);
}
}
public static Object getInstance(String key) {
return map.get(key);
}
}
直接上代码吧,这个也是项目中比较常见的,借用map的key-value结构,保证单例的唯一性。
具体的示例可参考 Spring 中的 SingletonBeanRegistry 接口
枚举单例
最简单,最安全的写法
public enum EnumSingleton {
INSTANCE {
};
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
ThreadLocal 单例
和线程相关的单例模式
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal(){
@Override
protected Object initialValue () {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton()
{}
public static ThreadLocalSingleton getInstance(){
return threadLocal.get();
}
}
Mybatis 中的 ErrorContext使用了该单例模式
不过要注意垃圾回收的问题