Java面试 - 单例 - 灵魂八问

程序员.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系统是一个多进程多线程系统,创建或删除文件时,会出现多进程或多线程同时操作一个文件,采用单例实现的文件管理器就可以让所有的文件操作都必须通过唯一的实例进行,不会产生混乱现象。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容