系统架构设计之单例模式(上)

单例模式的定义

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例(隐藏所有构造方法),并提供一个全局访问点(getInstance),属于创建型模式
思考:单例因何出现?

单例模式的的适用场景

确保任何情况下都绝对只有一个实例

  • ServletContenxt、ServletConfig、ApplicationContext、DBPool

饿汉式单例

  • 在单例类首次加载时,就立即完成初始化并且创建实例对象,因此绝对线程安全,不存在访问安全的问题
  • 饿汉式单例模式的一般写法
java 复制代码
/**
 * 优点:在类加载时进行初始化,执行效率高,性能高,没有任何的锁
 * 缺点:某些情况下,可能会造成内存浪费
 */
public class HungrySingleton {

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){} //私有化构造方法

    //提供一个全局访问点
    public static HungrySingleton getInstance(){
        return  hungrySingleton;
    }
}
  • 也可使用静态代码快的方式进行初始化,二者没有本质上的差别
java 复制代码
public class HungryStaticSingleton {
    //先静态后动态
    //先上,后下
    //先属性后方法
    private static final HungryStaticSingleton hungrySingleton;
    static {
        hungrySingleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {
    }

    public static HungryStaticSingleton getInstance() {
        return hungrySingleton;
    }
}
  • 饿汉式单例模式适用于单例对象较少的情况。
  • 这样写可以保证绝对线程安全、执行效率比较高。
  • 但是它的缺点也很明显,就是所有对象类加载的时候就实例化。
    • 这样一来,如果系统中有大批量的单例对象存在,那系统初始化是就会导致大量的内存浪费。也就是说不管对象用与不用都占着空间,浪费了内存。
      为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是,单例对象要在被使用时才会初始化

懒汉式单例

  • 当被外部类调用时才创建实例
java 复制代码
/**
 * 优点:节省了内存
 * 缺点:线程不安全
 */
public class LazySimpleSingleton {
    private static LazySimpleSingleton instance;
    private LazySimpleSingleton(){}

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

弊端:如果在多线程环境下,就会出现线程安全问题。

  • 编写测试代码,模拟多线程并发异常
  • 编写线程类 ExecutorThread
java 复制代码
public class ExectorThread implements Runnable{
    public void run() {
        LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + instance);
    }
}
  • 编写测试类
java 复制代码
public class LazySimpleSingletonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("End");
    }
}
  • 进行断点调试,模拟可能出现的三种结果
  • 运行 debug
  • 模拟情况一:两个线程按照正常顺序先后进入getInstance方法,完成单例初始化
  • 情况一预期:两次打印的单例为同一实例
  • 预期结果解释:Thread-0完成 初始化后,Thread-1直接返回Thread-0初始化的对象
  • 模拟情况二:Thread-0Thread-1同时进入 if 代码快中,单例被两度实例化,后者覆盖前者,线程不安全,单实例被覆盖
  • 模拟情况三:Thread-0Thread-1同时进入 if 代码快中,先后完成各种后续流程
  • 预期结果:输出的两个实例不是同一个实例,线程不安全,产生多实例

懒汉式单例的优化

对于懒汉式单例存在线程不安全的问题可使用对getInstance方法进行加锁的方案进行解决

  • 方案一,使用synchronized关键字
java 复制代码
    public synchronized static LazySimpleSingleton getInstance(){
        if(instance == null){
            instance = new LazySimpleSingleton();
        }
        return instance;
    }

我们再来调试。当执行其中一个线程并调用 getlnstance0方法时,另一个线程在调用 getlnstance0方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复到 RUNNING 状态继续调用 getlnstance0方法,如下图所示。

上图完美地展现了 synchronized 监视锁的运行状态,线程安全的问题解决了。但是,用synchronized 加锁时,在线程数量比较多的情况下,如果 CPU分配压力上升,则会导致大批线程阻塞从而导致程序性能大幅下降。

那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式

java 复制代码
/**
 * 优点:性能高了,线程安全了
 * 缺点:可读性难度加大,不够优雅
 */
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();
                    //涉及指令重排序的问题
                    //1.分配内存对象
                    //2.初始化对象
                    //3.设置instance指向的内存地址
                }
            }
        }
        return instance;
    }
}

当第一个线程调用 getlnstance()方法时,第二个线程也可以调用。

当第一个线程执行到synchronized 时会上锁,第二个线程就会变成 MONITOR状态,出现阻塞。

此时,阻塞并不是基于整个 LazySimpleSingleton 类的阻塞,而是在 getlnstance0方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感知不到。但是,用到 synchronized 关键字总归要上锁,对程序性能还是存在一定影响的。

是否更好的方案吗?当然有。我们可以从类初始化的角度来考虑,看下面的代码,采用静态内部类的方式:

java 复制代码
/*
    静态内部类单例写法
  ClassPath : LazyStaticInnerClassSingleton.class
              LazyStaticInnerClassSingleton$LazyHolder.class
   优点:写法优雅,利用了Java本身语法特点,性能高,避免了内存浪费,不能被反射破坏
   缺点:不优雅
 */
public class LazyStaticInnerClassSingleton {

    //使用LazyInnerClassGeneral时,默认会初始化内部类,如果没使用,内部类不加载
    private LazyStaticInnerClassSingleton() {
    }

    //每个关键字都不是多余的,static是为了使单例的空间共享,保证改方法不被重写,重载
    private static LazyStaticInnerClassSingleton getInstance() {
        //返回结果前,先加载内部类
        return LazyHolder.INSTANCE;
    }

    //主类加载时,内部类默认不加载
    private static class LazyHolder {
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }

}

这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronized 的性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。由于这种方式比较简单,就不进行调试了。但是,该方式存在致命的安全问题------能够被反射机制暴力破解,使单例失效

反射破坏单例模拟

上面介绍的单例模式的构造方法除了加上 private关键字,没有做任何处理。如果我们使用反射来调用其构造方法,再调用 qetlnstance()方法,应该有两个不同的实例。现在来看一段测试代码,以 LazyStaticInnerClassSingleton 为例:

java 复制代码
public static void main(String[] args) {
        try {
            //很无聊的情况下,进行破坏
            Class<?> clazz = LazyStaticInnerClassSingleton.class;
            //通过反射获取私有的构造方法
            Constructor<?> c = clazz.getDeclaredConstructor(null);
            //暴力破解
            c.setAccessible(true);

            //执行初始化,创建两个实例,输出结果
            Object instance1 = c.newInstance();

            Object instance2 = c.newInstance();


            System.out.println(instance1);

            System.out.println(instance2);

            System.out.println(instance1 == instance2);


//            Enum

        }catch (Exception e){
            e.printStackTrace();
        }
    }
  • 运行结果

显然,创建了两个不同的实例。那怎么办呢?我们来做一次优化。现在,我们在其构造方法中做些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码:

java 复制代码
    //在构造器中,天骄单例校验,抛出异常
    private LazyStaticInnerClassSingleton() {
        //避免暴力反射破坏
        if (LazyHolder.INSTANCE != null) {
            throw new RuntimeException("不允许非法访问");
        }
    }
  • 运行,看效果

至此,自认为史上最牛的单例模式的实现方式便大功告成。但是,上面看似完美的单例写法还是有可能被破坏。

序列化破坏单例模拟

一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码:

java 复制代码
public class SeriableSingleton implements Serializable {


    //序列化
    //把内存中对象的状态转换为字节码的形式
    //把字节码通过IO输出流,写到磁盘上
    //永久保存下来,持久化

    //反序列化
    //将持久化的字节码内容,通过IO输入流读到内存中来
    //转化成一个Java对象


    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

}
  • 编写测试代码
java 复制代码
    public static void main(String[] args) {

        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try {

            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 运行,查看结果

从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的设计初衷。那么,我们如何保证在序列化的情况下也能够实现单例模式呢?其实很简单,只需要增加在SeriableSingleton 类中增加 readResolve0方法即可。

java 复制代码
 private Object readResolve(){ return INSTANCE;}

进入 ObjectinputStream 类的 readObject()方法代码如下,JDK 对该方法的描述大致如下几点

java 复制代码
private final Object readObject(Class<?> type)
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        if (! (type == Object.class || type == String.class))
            throw new AssertionError("internal error");

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(type, false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
                freeze();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

我们发现,在 readObject()方法中又调用了重写的 readObject0()方法。进入readObject0()方法代码如下:

java 复制代码
    private Object readObject0(Class<?> type, boolean unshared) throws IOException {
        ......
                case TC_OBJECT:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an object to java.lang.String");
                    }
                    return checkResolve(readOrdinaryObject(unshared));

         ....
    }

在 TC_OBJECT 中调用了 ObjectnputStream的readOrdinaryObject()方法

java 复制代码
    private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

       .......

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }
        return obj;
    }

紧接着调用了 ObjectStreamClass 的 isInstantiable()方法,而isInstantiable()方法的代码如下:

java 复制代码
    boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
    }

上述代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回 true。这意味着只要有无参构造方法就会实例化。

这时候其实还没有找到加上 readResolve0方法就避免了单例模式被破坏的真正原因。再回到ObjectlnputStream的 readOrdinaryObject0方法,继续往下看:

判断无参构造方法是否存在之后,,又调用了 hasReadResolveMethod()方法,来看代码

java 复制代码
    /**
     * Returns true if represented class is serializable or externalizable and
     * defines a conformant readResolve method.  Otherwise, returns false.
     */
    boolean hasReadResolveMethod() {
        requireInitialized();
        return (readResolveMethod != null);
    }

上述代码逻辑非常简单,就是判断 readResolveMethod 是否为空,不为空就返回true。那么readResolveMethod 是在哪里赋值的呢?通过全局査找知道,在私有方法 ObjectStreamClass()中给readResolveMethod 进行了赋值,来看代码:

java 复制代码
    private ObjectStreamClass(final Class<?> cl) {
        ............
        readResolveMethod = getInheritableMethod(
                                cl, "readResolve", null, Object.class);
        .........
    }

上面的逻辑其实就是通过反射找到一个无参的readResolve()方法,并且保存下来。现在回到ObjectlnputStream 的 readOrdinaryObject()方法继续往下看,如果readResolve()方法存在则调用invokeReadResolve0方法,来看代码:

java 复制代码
    Object invokeReadResolve(Object obj)
        throws IOException, UnsupportedOperationException{
        requireInitialized();
        if (readResolveMethod != null) {
            try {
                return readResolveMethod.invoke(obj, (Object[]) null);
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getCause();
                if (th instanceof ObjectStreamException) {
                    throw (ObjectStreamException) th;
                } else {
                    throwMiscException(th);
                    throw new InternalError(th);  // never reached
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

我们可以看到,在invokeReadResolve()方法中用反射调用了readResolveMethod 方法。通过 JDK 源码分析我们可以看出,虽然增加 readResolve()方法返回实例解决了单例模式被破坏的问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率加快,就意味着内存分配开销也会随之增大,难道真的就没办法从根本上解决问题吗?下面讲的注册式单例也许能帮助到你。


相关推荐
萧曵 丶2 小时前
Java 安全的单例模式详解
java·开发语言·单例模式
期待のcode21 小时前
Java的单例模式
java·开发语言·单例模式
xxxxxxllllllshi1 天前
深入解析单例模式:从原理到实战,掌握Java面试高频考点
java·开发语言·单例模式·面试
ShineSpark1 天前
C++单例模式的演进:从经典实现到现代线程安全范式
c++·安全·单例模式
爱编码的傅同学2 天前
【单例模式】深入理解懒汉与饿汉模式
java·javascript·单例模式
Geoking.4 天前
【设计模式】理解单例模式:从原理到最佳实践
单例模式·设计模式
一颗青果5 天前
单例模式 | 死锁
linux·服务器·单例模式·1024程序员节
青柠代码录5 天前
【设计模式】A1-单例模式
单例模式·设计模式
coder_xiaoyou7 天前
单例模式_双检锁与静态内部类
java·单例模式