关于java:这应该是全网最全的单例模式总结了吧面试官都被我说懵了

33次阅读

共计 8168 个字符,预计需要花费 21 分钟才能阅读完成。

前言

单例模式是面向对象的编程语言 23 种设计模式之一,属于创立型设计模式。次要用于解决对象的频繁创立与销毁问题,因为单例模式保障一个类仅会有一个实例。大部分对单例模式应该都晓得一些,但面试的时候可能答复不会很残缺,不能给本人加分,甚至扣分。

繁多的知识点并不能对本人在面试的时候带来加分,而零碎的常识树则会让面试官另眼相看*,而本文会零碎的介绍单例模式的根底版本与完满版本,基本上将单例模式的内容齐全包含。如果认为有不同的意见能够留言交换。

单例模式最重要的就是保障一个类只会呈现一个实例,那么超过一个就不能被称为是单例,所有其代码形成如下特点。

  1. 私有化结构器,禁止从内部创立单例对象。
  2. 提供一个全局的拜访点获取单例对象。

什么是全局拜访点?好吧,下面的话语太文邹邹了,如果我说公共的静态方法呢?

饿汉、懒汉

次要分为饿汉模式和懒汉模式。那何为饿汉?何为懒汉?

小丽的爸爸从小生存很艰辛,经验了饥荒年代,所以对食物十分缓和。当小丽去上学的时候,不论小丽是否须要,都会给小丽筹备很多的零食。

而小明的爸爸则是一个十分懈怠的人,所有的事件都会到最初才去做,所有事件 只有当有他人来叫他的时候,他才会把事件做完 这样就引出了咱们对饿汉模式和懒汉模式的定义:

饿汉模式:不论单例对象是否被应用,都会先创立出一个对象。饿汉模式存在资源节约的问题,因为很有可能对象创立进去只会永远都不会被应用到。

代码如下:

package demo.single;
/**
 * 饿汉模式
 */
public class HungrySingle {
    /**
     * 饿汉模式,不论 hungrySingle 对象是否有应用到,都会先创立进去
     * 因为饿汉模式在对象应用之前就曾经被创立,所以是不会存在线程平安问题
     */
    private static HungrySingle hungrySingle = new HungrySingle();
    /**
     * 私有化结构器,禁止内部创立
     */
    private HungrySingle(){}
    /**
     * 提供获取实例的办法
     */
    public static HungrySingle getInstance(){return hungrySingle;}
}

懒汉模式:不会先将对象创立进去,而是等到有人应用的时候才会创立。相比饿汉模式,懒汉模式不会存在资源节约的状况,所以根本都会抉择懒汉模式。

代码如下:

package demo.single;
/**
 * 懒汉模式
 */
public class LazySingle {
    /**
     * 懒汉模式,不会先创建对象,而是在调用的时候才会创建对象
     */
    private static LazySingle lazySingle = null;
    private LazySingle() {}
    /**
     * 调用的时候创建对象并返回
     */
    public static LazySingle getInstance(){if(lazySingle == null){lazySingle = new LazySingle();
        }
        return lazySingle;
    }
}

小李:面试官,您看我这样的解释可还行。

面试官:单线程下是挺好的,如果在多线程环境下呢?

小李:这个我晓得,加锁啊!

面试官:出门左转电梯中转!

其实加锁也没答错,关键问题在于如何加锁!

间接将获取实例的办法内容写入同步代码块中,解决了多线程平安的问题,然而并发效率的问题又裸露了进去。你想啊,当初锁住了这办法,而无论单例的对象是否创立,都会通过获取锁、开释锁的过程。这样的性能显然是不能承受的。

小李:我想想啊~~~!Emmmmm…! 有了,咱们能够在同步代码块外层加一个判断,如果对象曾经创立则间接返回。

面试官:这样解决了一部分的并发效率问题,然而如果在创立的时候同时有很多的线程拜访,是不是也会有并发的效率问题呢?再优化优化。

小李一想,的确是这样,如果对象还没有创立进去的时候,就有很多的线程来拜访,也会呈现问题,假如有两个线程同时拜访,当 A 线程优先争抢到锁,A 进入同步代码块执行,此时 B 没有争抢到锁,将处于期待状态,而当 A 线程执行实现后开释锁,B 进入同步代码块执行,此时 B 线程同样会创立出一个对象,毁坏了单例。

小李:面试官,我明确了,能够在同步代码块中再加一层 if 判断,如果对象曾经创立,就间接返回即可。

Double Check

下面最初的后果就是咱们常说的 Double Check, 即双重锁查看。双重锁查看在很多中央都被使用到,代码如下。

package demo.single;
/**
 * 懒汉模式
 */
public class LazySingle {
    /**
     * 懒汉模式,不会先创建对象,而是在调用的时候才会创建对象
     */
    private static LazySingle lazySingle = null;
    private LazySingle() {}
    /**
     * 调用的时候创建对象并返回
     */
    public static LazySingle getInstance(){
        //first check
        if(lazySingle == null){synchronized (LazySingle.class){
                //double check
                if(lazySingle == null){lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }
}

面试官:小李,你多线程运行一下代码看看呢。

小李:好勒!如同挺失常啊。等等,如同不对,这里还是呈现了多个对象!!!啊~~,这是为什么啊,我都懵了,这齐全超出了我的能力范畴。

面试官:哈哈,小子,这下晓得谁是大佬了吧?我来给你好好解释一下,其实,这和咱们的代码没有关系,失常来讲,应该不会呈现这样的问题,然而咱们都晓得,代码在运行过程中,会被编译成一条一条的指令运行,而 JVM 在运行时,在保障单线程最终后果不会受影响的状况下,对指令进行优化,就有可能对指令进行重排序,同样会毁坏单例。

lazySingle = new LazySingle();
// 这样一段代码在运行时会生成 3 条指令,即:1\. 分配内存空间 2\. 创建对象 3\. 指向援用
// 失常状况下是会依照 1 2 3 程序执行,但 JVM 优化器进行指令重排后,则可能变为:1\. 分配内存空间 3\. 指向援用  2\. 创建对象 
// 在单线程下,这样的优化没有问题,然而多线程下,线程是在争抢 CPU 工夫碎片的。假如 A 刚刚执行完 1 3 // 条指令,此时 B 争抢到工夫碎片,发现对象不为空了,就间接返回,但此时对象还没有真正被创立。B 调用
// 此对象就会抛出异样
// 而 volatile 关键字润饰的变量能够禁止指令重排序,则能够保障指令会是 1 2 3 程序执行。// 加上 volatile 润饰
private volatile  static LazySingle lazySingle = null;

小李:终于解决了,好难啊,一个简略的单例模式竟然有这么多的细节。

面试官:你认为这就完了?

外部类的单例

应用外部类的形式能够十分完满的实现单例模式,而实现代码也非常简单。

package demo.single;
 
/**
 * 外部类的形式实现单例
 */
public class InnerSingle {
    /**
     * 私有化结构器
     */
    private InnerSingle(){}
    /**
     * 公有外部类
     */
    private static class Inner{
        //Jingtai 外部类持有外部类的对象
        public static final InnerSingle SINGLE = new InnerSingle();}
    /**
     * 返回动态外部类持有的对象
     */
    public static InnerSingle getInstance(){return Inner.SINGLE;}
}
 

能够看到,代码中并没有呈现同步办法或者同步代码块,那么动态外部类的形式是如何做到平安的单例模式呢?

  1. 外部类加载的时候,不会立刻加载外部类,而是在调用的时候会加载外部类。
  2. 不论多少线程拜访,JVM 肯定会保障类被正确的初始化,即动态外部类的形式是在 JVM 层面保障了线程平安

当然,这样也有一些毛病,那就是在创立单例对象的时候,如果须要传参,那么动态外部类的形式会十分麻烦。

毁坏单例

那么,下面的单例曾经完满了吗?并没有,看我如何将单例给毁坏掉。

反射毁坏

反射能够绕过公有结构器的限度,创建对象。当然失常的调用是不会产生单例被毁坏的状况,然而如果偏偏有人不走寻常路呢,比方上面的调用。

package demo.single;
 
import java.lang.reflect.Constructor;
/**
 * 反射毁坏单例
 */
public class RefBreakSingleTest {public static void main(String[] args) throws Exception {
        // 获取类对象
        Class<LazySingle> lazySingleClass = LazySingle.class;
        // 获取结构器
        Constructor<LazySingle> constructor = lazySingleClass.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        // 创建对象
        LazySingle lazySingle = constructor.newInstance(null);
        System.out.println(lazySingle);
        System.out.println(LazySingle.getInstance());
        System.out.println(lazySingle == LazySingle.getInstance());
    }
}

很显著看到呈现了两个不同的兑现,显然,单例被毁坏了!对于这样的状况该如何禁止呢?在网上查阅了很多材料,大部分是应用变量控制法,即在类中增加一个变量用于判断单例类的结构器是否有被调用,代码如下。

    // 增加变量管制, 避免反射毁坏
   private static boolean isInstance = false;
   private volatile  static LazySingle lazySingle = null;
   private LazySingle() throws Exception {if(isInstance){throw new Exception("the Constructor has be used");
       }
       isInstance = true;
   }

再次调用测试代码,发现不能再创立多个单例对象,程序抛出了异样。

然而别忘了,属性也是能够通过反射批改的(count、instance 的判断反射都能绕过)。

public class RefBreakSingleTest {public static void main(String[] args) throws Exception {
        // 获取类对象
        Class<LazySingle> lazySingleClass = LazySingle.class;
 
        // 获取结构器
        Constructor<LazySingle> constructor = lazySingleClass.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        // 创建对象
        LazySingle lazySingle = constructor.newInstance(null);
        System.out.println(lazySingle);
        Field isInstance = lazySingleClass.getDeclaredField("isInstance");
        isInstance.setAccessible(true);
        isInstance.set(null,false);
        System.out.println(LazySingle.getInstance());
        System.out.println(lazySingle == LazySingle.getInstance());
    }
}

单例再次被毁坏,感觉是不是曾经快解体了,一个单例咋这么多事呢!!既然公有属性、公有办法在内部都能通过反射获取,那有没有反射不能获取的呢?我在网上也找到了另外一种写法,即公有外部类的来持有实例控制变量,而我也通过测试,发现反射同样可能绕过从而毁坏单例。

package demo.pattren.single;
 
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
 
public class BreakInnerTest {public static void main(String[] args) throws Exception {
        Class<LazySingle> lazySingleClass = LazySingle.class;
//        // 获取结构器
        Constructor<LazySingle> constructor = lazySingleClass.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        // 创建对象
        LazySingle lazySingle = constructor.newInstance(null);
        // 获取外部类的类对象
        Class<?> aClass = Class.forName("demo.pattren.single.LazySingle$InnerClass");
        Method[] methods = aClass.getMethods();
        Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
        System.out.println(declaredConstructors);
        Constructor<?> declaredConstructor = declaredConstructors[0];
        declaredConstructor.setAccessible(true);
        // 创立外部类须要传入一个外部类的对象
        Object o = declaredConstructor.newInstance(lazySingle);
        // 胜利绕过
        methods[0].invoke(o);
    }
}

目前网上根本都是这两种,然而反射都是可能绕过判断进行毁坏。能够这样认为,这种形式反射是能够毁坏的,不能 100% 保障单例不被毁坏。欢送各位提供完满的示例。

序列化毁坏

Java 的 IO 提供了对象流,用来将对象写入磁盘、从磁盘读取对象的性能。这也成为了单例的毁坏点。

    public static void main(String[] args) throws Exception {
        // 失常的形式获取单例对象
        InnerSingle instance = InnerSingle.getInstance();
 
        // 写入磁盘
        FileOutputStream fos = new FileOutputStream("d:/single");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(instance);
        oos.close();
        fos.close();
 
        // 从磁盘读取对象
        FileInputStream fis = new FileInputStream("d:/single");
        ObjectInputStream ois = new ObjectInputStream(fis);
        InnerSingle innerSingle = (InnerSingle) ois.readObject();
 
        System.out.println(instance);
        System.out.println(innerSingle);
        System.out.println(innerSingle == instance);
    }

而序列化的形式 JVM 提供了一种机制,能够避免单例被毁坏,即在单例类中增加 readResovle 办法。

    // 在反序列化时,readResolve 办法,则间接返回该办法指定的对象    private  Object readResolve(){        return getInstance();    }

测试后果:

序列化没有再毁坏单例,而这所有 JDK 是如何解决的呢?

public final Object readObject()
        throws IOException, ClassNotFoundException
    {if (enableOverride) {return readObjectOverride();
        }
        int outerHandle = passHandle;
        try {
            // 要害代码,最终返回的是此办法返回的对象
            Object obj = readObject0(false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
 //more code but not importent

持续深刻, 发现 readObject0 办法的要害代码如下

        byte tc;
        // 取出文件的一个字节,判断读取的对象类型
        while ((tc = bin.peekByte()) == TC_RESET) {bin.readByte();
            handleReset();}
        depth++;
        totalObjectRefs++;
        try {switch (tc) {
                case TC_NULL:
                    return readNull();
                case TC_ENUM:
                    return checkResolve(readEnum(unshared));
                // 判断为对象类
                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));
                //more othrer case

持续追踪 readOrdinaryObject 办法,发现 readReslove 的要害代码

        // 判断是否有 readReslove 办法(desc.hasReadResolveMethod())
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {   
           // 执行 readReslove
            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);
                    }
                }
                // 最终返回 readReslove 办法的执行后果
                handles.setObject(passHandle, obj = rep);
            }
        }
        return obj;

枚举单例 – 最完满的单例模式

大神 Josh Bloch 在《Effective Java》中极力推荐应用枚举的形式来实现单例。

package demo.single;
 
public enum EnumSingle {
    SINGLE;
    public void doJob(){System.out.println("doJob");
    }
}

枚举类型是单例模式的最佳抉择,次要得益于 JVM 对于枚举类型的反对:

  1. JVM 保障枚举类型的每个实例仅存在一份
  2. 枚举类型的序列化与反序列化不会毁坏其单例的个性(下面的源码大家能够去找一找)
  3. 反射也不能毁坏枚举单例

能够说,枚举人造就是单例的,那么你会抉择枚举作为单例吗?

最初

感激你看到这里,文章有什么有余还请斧正,感觉文章对你有帮忙的话记得给我点个赞,每天都会分享 java 相干技术文章或行业资讯,欢送大家关注和转发文章!

正文完
 0