单例模式

292 阅读5分钟

你真的理解单例模式了吗

  • 定义:保证一个类仅有一个实例,并提供一个全局访问点
  • 类型:创建型
  • 使用场景:想确保任何情况下都绝对只有一个实例,单服务下的网站计数器,集群服务下的共享计数器,线程池、数据库的连接池、windows中的任务管理器等
  • 优点:在内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用;设置了全局点,严格控制访问
  • 缺点:没有接口,扩展困难
饿汉式
public class HungrySingleton {
    private final static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton(){}
    public static HungrySingleton getInstance(){
        return instance;
    }
}

相对简单就不过多讨论了

懒汉式
public class LazySingleton {
    private static LazySingleton instance = null;
    private LazySingleton(){}
    public static LazySingleton getInstance(){
        if(instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

以上代码在单线程环境中完全能够满足需要,但是一旦在多线程的环境中单例的可靠性就不能保证了

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

解决方案将该方法加一个同步锁,但众所周知,加锁、释放锁操作是个开销蛮大的操作,能不能进一步优化呢

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

大名鼎鼎的双重检查上场了,这样的话,就不必每个getInstance方法都加上一道锁,但是这样会不会还有什么问题?会的。new并不是原子操作,cpu在执行过程中可能会将new的指令进行重排序。

new 大致分为三个步奏

1、分配一个内存给这个对象 2、给这个对象进行初始化 3、将地址赋给instance引用

这里的二三两步可能会交换执行顺序

1、分配一个内存给这个对象 3、将地址赋给instance引用 2、给这个对象进行初始化

那么可能另外一个线程得到的实例对象可能是未初始化完成的

public 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;
    }
}

解决方案就是将该引用变量使用volatile修饰,该关键字的内存语义是,1、对其它线程立即可见;2、禁止指令重排序。由此完美解决问题

还有没有什么方案可以实现懒加载呢?答案是有的,可以使用静态内部类

public class LazySingleton {
    private static class InnerClass{
        public static LazySingleton instance = new LazySingleton();
    }
    private LazySingleton(){}
    public static LazySingleton getInstance(){
        return InnerClass.instance;
    }
}

在这里会不会产生线程安全的问题呢?其实类加载的时候JVM会帮我们上一道锁。所以没必要当心。

看到这里大家会不会认为这已经完美了,但是我还想杠一杠

实例化对象好像不只使用new指令一条路子。如果使用的其它方式好像这个单例也不是太单例

  • 反序列化来了,如果有序列化反序列化的需要实现了Serializable接口,就需要注意这个问题了
public class Main {
    public static void main(String[] args) throws Exception {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
        HungrySingleton o = (HungrySingleton) ois.readObject();
        System.out.println(instance == o);//false
    }
}

如何避免这个问题呢?

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,通过源码阅读我们可以发现

private Object readOrdinaryObject(boolean unshared)
    throws IOException
{
    //....省略的一些代码
    Object obj;
    try {
    	// 如果类实现了Serializable接口 就反射创建实例对象
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        //...
    }
	// ...
	// 如果这个对象实现了readResolve方法,就返回的该方法返回的引用
    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        Object rep = desc.invokeReadResolve(obj);
        //...
        if (rep != obj) {
            // Filter the replacement object
            handles.setObject(passHandle, obj = rep);
        }
    }
    return obj;
}
  • 反射来了
public class Main {
    public static void main(String[] args) throws Exception {
        HungrySingleton instance = HungrySingleton.getInstance();
        Class<HungrySingleton> cls = HungrySingleton.class;
        Constructor<HungrySingleton> constructor = cls.getDeclaredConstructor();
       constructor.setAccessible(true);
        HungrySingleton instance1 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
        System.out.println(instance == instance1);
    }
}

反射调用近乎无解,如果真正的搞一个单例对象,我们可以尝试Enum

public enum EnumSingleton {
    INSTANCE;
}

序列化和反序列化试一下

public class Main {
    public static void main(String[] args) throws Exception {
        EnumSingleton instance = EnumSingleton.INSTANCE;
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("enum_singleton"));
        oos.writeObject(instance);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("enum_singleton"));
        EnumSingleton instance1 = (EnumSingleton) ois.readObject();
        System.out.println(instance);//INSTANCE
        System.out.println(instance1);//INSTANCE
        System.out.println(instance == instance1);//true
    }
}

强行用反射的话会抛异常

public class Main {
    public static void main(String[] args) throws Exception {
       EnumSingleton instance = EnumSingleton.INSTANCE;
        Class<EnumSingleton> cls = EnumSingleton.class;
        Constructor<EnumSingleton> constructor = cls.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        EnumSingleton instance1 = constructor.newInstance("niyulu", 6);
        System.out.println(instance);
        System.out.println(instance1);
        System.out.println(instance == instance1);
    }
}

output

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at top.luyuni.design.pattern.creational.singleton.Main.main(Main.java:11)

分析序列化不创建新对象原因

private Enum<?> readEnum(boolean unshared) throws IOException {
	// 拿到枚举名称
    String name = readString(false);
    Enum<?> result = null;
    Class<?> cl = desc.forClass();
    if (cl != null) {
        try {
            @SuppressWarnings("unchecked")
            // 直接从常量池中获取枚举类
            Enum<?> en = Enum.valueOf((Class)cl, name);
            result = en;
        //...
    }

    handles.finish(enumHandle);
    passHandle = enumHandle;
    return result;
}

分析反射报错原因

public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
           IllegalArgumentException, InvocationTargetException
{
    // 如果类型为枚举直接抛一个不支持的异常
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

使用工具反编译枚举类

public final class EnumSingleton extends Enum
{

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

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(top/luyuni/design/pattern/creational/singleton/EnumSingleton, name);
    }

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

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

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

由反编译结果可知私有构造器,类加载时创建对象完全符合饿汉式的单例模式,并且还有io,和reflect相关类保驾护航