美文网首页
Java面试 - 单例 - 灵魂八问

Java面试 - 单例 - 灵魂八问

作者: 程序猿蛋蛋哥 | 来源:发表于2021-02-05 11:00 被阅读0次
    程序员.jpg

    目录


    • 1.为什么要使用单例?
    • 2.单例有几种实现方式?
      • <1> 饿汉模式:比较饥饿,立即加载,即类加载时就已经产生了实例
      • <2> 懒汉模式:比较懒,用时再加载,即延迟加载
        • 懒汉1 - 普通懒汉:线程不安全【了解】
        • 懒汉2 - 方法加锁懒汉:线程安全,效率低【了解】
        • 懒汉3 - 实例加锁懒汉:线程不安全【了解】
        • 懒汉4 - 双重检查锁懒汉:线程安全【推荐】
      • <3> 静态内部类模式:线程安全【推荐】
      • <4> 枚举类模式:线程安全,天然防止反射和反序列化调用,调用效率高,不能延时加载
    • 3.那通过一个全局变量来代替单例如何?
    • 4.那使用类的静态方法代替单例如何?或者使用静态内部类代替单例呢?
      • <1>静态方法常驻内存,非静态方法只有使用时才分配内存,是这样的吗?
      • <2>静态方法和非静态方法的区别是什么?
      • <3>那什么时候用静态方法,什么时候用非静态方法(即实例方法)?
    • 5.单例能否被继承?
    • 6.单例是不是线程安全的?
    • 7.单例的线程安全性能否被破坏?哪些方式能破坏?
      • <1> 序列化与反序列化:
      • <2> 通过反射调用私有构造:
      • <3> 单例由不同的类加载器加载,有可能存在多个单例类的实例:
    • 8.什么情况下使用单例?

    1.为什么要使用单例?

    单例优点:

    • 单例能保证一个类仅有唯一的实例,并提供一个全局访问点。
    • 避免对象的重复创建,节省内存,减少每次创建对象的时间开销,有助于Java垃圾回收。

    单例缺点:不适用变化的对象

    2.单例有几种实现方式?

    单例实现方式.png

    <1> 饿汉模式:比较饥饿,立即加载,即类加载时就已经产生了实例

    形式一:
    public class HungrySingleton {
        private static HungrySingleton instance = new HungrySingleton();
        // 构造方法私有化
        private HungrySingleton() { }
        public static HungrySingleton getIntance() {
            return instance;
        }
    }
    
    形式二:使用静态代码块
    public class HungrySingleton {
        private static HungrySingleton instance;
        // 构造方法私有化
        private HungrySingleton() { }
        // 使用静态代码块
        static {
            instance = new HungrySingleton();
        }
        public static HungrySingleton getIntance() {
            return instance;
        }
    }
    

    <2> 懒汉模式:比较懒,用时再加载,即延迟加载

    懒汉1 - 普通懒汉:线程不安全【了解】
    public class Lazy1Singleton {
        private static Lazy1Singleton instance;
        // 构造方法私有化
        private Lazy1Singleton() { }
        
        public static Lazy1Singleton getInstance() {
            if (instance == null) {
                instance = new Lazy1Singleton();
            }
            return instance;
        }
    }
    
    懒汉2 - 方法加锁懒汉:线程安全,效率低【了解】
    形式一:
    public class Lazy2Singleton {
        private static Lazy2Singleton instance;
        // 构造方法私有化
        private Lazy2Singleton() { }
        // 方法加锁
        public static synchronized Lazy2Singleton getInstance() {
            if (instance == null) {
                instance = new Lazy2Singleton();
            }
            return instance;
        }
    }
    形式二:
    public class Lazy2Singleton {
        private static Lazy2Singleton instance;
        // 构造方法私有化
        private Lazy2Singleton() { }
        // 方法中的类加锁
        public static Lazy2Singleton getInstance() {
            synchronized (Lazy2Singleton.class) {
                if (instance == null) {
                    instance = new Lazy2Singleton();
                }
                return instance;
            }
        }
    }
    
    懒汉3 - 实例加锁懒汉:线程不安全【了解】
    public class Lazy3Singleton {
        private static Lazy3Singleton instance;
        // 构造方法私有化
        private Lazy3Singleton() { }
        
        public static Lazy3Singleton getInstance() {
            if (instance == null) {
                synchronized (Lazy3Singleton.class) {
                    instance = new Lazy3Singleton();
                }
            }
            return instance;
        }
    }
    
    懒汉4 - 双重检查锁懒汉:线程安全【推荐】
    public class Lazy4Singleton {
        private static Lazy4Singleton instance;
        // 构造方法私有化
        private Lazy4Singleton() { }
        public static Lazy4Singleton getInstance() {
            if (instance == null) {
                synchronized (Lazy4Singleton.class) {
                    if (instance == null) {
                        instance = new Lazy4Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    <3> 静态内部类模式:线程安全【推荐】

    public class StaticInnerSingleton {
        // 私有静态内部类
        private static class Inner {
            private static StaticInnerSingleton instance = new StaticInnerSingleton();
        }
        // 构造方法私有化
        private StaticInnerSingleton() { }
        
        public static StaticInnerSingleton getInstance() {
            return Inner.instance;
        }
    }
    

    <4> 枚举类模式:线程安全,天然防止反射和反序列化调用,调用效率高,不能延时加载

    public enum EnumSingleton {
        // 枚举元素本身就是单例
        INSTANCE;
        // 添加自己需要的操作
        public void singletonOperation() {
        }
    }
    

    3.那通过一个全局变量来代替单例如何?

    全局变量确实可以提供一个全局访问点,但是它不能防止别人实例化多个对象。

    如果通过外部程序来控制对象产生个数,系统的管理成本会增加,模块间的耦合度也增大。

    4.那使用类的静态方法代替单例如何?或者使用静态内部类代替单例呢?

    先说静态内部类,单例的一种实现模式就是使用静态内部类,所以可以代替。

    对于类的静态方法代替单例,其实问题应该是:为什么要用单例而不是静态方法?

    关于静态方法,需要搞明白以下几点:

    <1> 静态方法常驻内存,非静态方法只有使用时才分配内存,是这样的吗?

    一般认为:怕静态方法占用过多内存而建议使用非静态方法,这个理解是错误的。

    静态方法和非静态方法,在内存里其实都放在方法区中,在一个类第一次被加载时,它会在方法区里把静态方法,非静态方法都加载进去。

    静态方法和非静态方法,都是在第一次加载后就常驻内存,所以方法本身在内存里,没有什么区别。

    <2> 静态方法和非静态方法的区别是什么?

    静态方法里使用的是静态变量,在JVM中保存在方法区里,只有一份。

    非静态方法(即实例方法)里可以使用静态变量和实例变量,实例变量是在Java堆中存放实例,在栈中存放实例的引用,引用指向堆中实例的内存地址,静态变量是保存在方法区里。

    在调用速度上,静态方法比非静态方法(实例方法)快一点点,也可以忽略。

    <3> 那什么时候用静态方法,什么时候用非静态方法(即实例方法)?

    如果一个方法和它所在的实例对象无关,就应该使用静态方法,例如:java.lang.Math类,否则就应该是非静态方法。

    再回到上面的问题:为什么要用单例而不是静态方法?

    如果不需要维护任何状态,仅仅提供全局访问的方法,这种情况考虑使用静态方法,因为静态方法比单例快,静态方法里使用的是静态变量,在编译期已经完成初始化。

    如果要延迟加载,则单例可以懒加载,静态方法不行。

    5.单例能否被继承?

    单例不能被继承,因为单例的构造方法被私有化了,子类的构造方法都会隐式的调用父类默认的构造方法。

    6.单例是不是线程安全的?

    单例是不是线程安全的,得看它的实现方式,见下图:

    单例实现方式.png

    7.单例的线程安全性能否被破坏?哪些方式能破坏?

    单例的线程安全性能被破坏,总结有三种方式:

    破坏单例线程安全性方式.png

    <1> 序列化与反序列化:

    反序列化后的对象是重新实例化的,单例被破坏。

    解法方法:提供一个readResolve方法:反序列化后,新对象上的readResolve()方法就会被调用,该方法返回的对象引用将被返回,取代新对象。

    private Object readResolve() {
        return INSTANCE;
    }
    

    <2> 通过反射调用私有构造:

    反射出来的对象和单例类的对象不是同一个,因此单例被破坏。

    package com.fan.code.singleton;
    public class MySingleton {
        private static MySingleton instance;
        private MySingleton() { }
        public static MySingleton getInstance() {
            if (instance == null) {
                synchronized (MySingleton.class) {
                    if (instance == null) {
                        instance = new MySingleton();
                    }
                }
            }
            return instance;
        }
        // 通过反射得到的MySingleton对象和调用MySingleton.getInstance()得到的对象不是同一个,因此单例被破坏
        public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Class clazz = Class.forName("com.fan.code.singleton.MySingleton");
            Constructor c = clazz.getDeclaredConstructor();
            c.setAccessible(true);
            MySingleton mySingleton = (MySingleton) c.newInstance();
        }
    }
    

    <3> 单例由不同的类加载器加载,有可能存在多个单例类的实例:

    假定不是远端存取,一些servlet容器对每个servlet使用完全不同的类加载器,如果有两个servlet访问一个单例类,它们就都会有各自的实例。

    解法方法:自行指定类加载器,并指定同一个类加载器。

    private static Class getClass(String className) throws ClassNotFoundException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        if (classLoader == null) {
            classLoader = MySingleton.class.getClassLoader();
        }
        return classLoader.loadClass(className);
    }
    

    8.什么情况下使用单例?

    <1> 控制资源的使用,通过线程同步(加锁)来控制资源的并发访问。

    <2> 控制实例产生的数量(1个),达到节约资源的目的。

    <3> 作为通信媒介使用,也就是数据共享,可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信。

    实际应用举例:

    • 数据库连接池:系统使用数据库连接池,主要是节省打开或关闭数据库连接所引起的效率损耗,它属于重量资源,一个应用中只需要保留一份即可,既节省资源又方便管理。
    • 线程池:线程池要方便对池中的线程进行控制。
    • 网站计数器
    • Windows的任务管理器
    • Windows的回收站:在整个系统运行过程中,回收站一直维护着仅有一个实例。
    • 应用程序的日志应用:共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
    • 操作系统的文件管理器:Windows系统是一个多进程多线程系统,创建或删除文件时,会出现多进程或多线程同时操作一个文件,采用单例实现的文件管理器就可以让所有的文件操作都必须通过唯一的实例进行,不会产生混乱现象。

    相关文章

      网友评论

          本文标题:Java面试 - 单例 - 灵魂八问

          本文链接:https://www.haomeiwen.com/subject/hmpwtltx.html