【设计模式】单例模式是什么?如何实现单例模式?单例模式常见问题?

目录

1. 单例模式

保证一个类只有唯一的一个实例,并提供一个全局的访问点。

1. 单例模式的三要素

  1. 私有静态实例引用
  2. 私有的构造方法
  3. 返回静态实例的静态公有方法

白话文:1个构造方法 + 1个引用(不一定有实例)+ 1个获取单例的方法

2. 优点

  1. 在内存中只有一个对象,节省内存空间;
  2. 避免频繁的创建销毁对象,可以提高性能;(只有一个实例)
  3. 避免对共享资源的多重占用,简化访问;
  4. 为整个系统提供一个全局访问点。(提供了获取单例的方法)

3. 单例的实现方式(思路:思考一下Java创建对象的几种方式?好像解题思路也不对。。。基本都是通过new出来的)

1. 饿汉模式单例(在类加载时会初始化)

饿汉在类加载的时候就会初始化,所以不会有线程安全的问题,getInstance不需要有任何操作,直接拿到instance就行

1. new方式
public class SingletonDemo {
	// 在类加载的时候直接new这个实例
    private static SingletonDemo instance = new SingletonDemo();
    private SingletonDemo(){}
    public static SingletonDemo getInstance(){
        return instance;
    }
}

在类加载的时候直接new这个实例

2. 静态代码块
public class SingletonDemo {
    private static SingletonDemo instance = null;
    static{
        instance = new SingletonDemo();
    }
    private SingletonDemo(){}
    public static SingletonDemo getInstance(){
        return instance;
    }
}

静态代码块在类加载的时候就会执行

题外话:静态代码块相比直接new的方式,到底好在哪里?TODO

3. 枚举实例化
public enum SingletonDemo {
    INSTANCE;
    public SingletonDemo getInstance(){
        return INSTANCE;
    }
}
1. 优点:防止反射问题、防止反序列化问题、防止clone
2. 枚举反编译后的结果:
public final class SingletonDemo extends Enum<SingletonDemo> {
	public static final SingletonDemo SINGLETONDEMO;
	public static SingletonDemo[] values();
	public static SingletonDemo valueOf(String s);
	static {};
}

// 这是Enum类
public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
    // 名称
    private final String name;
    public final String name() {
        return name;
    }
    // 序号
    private final int ordinal;
    public final int ordinal() {
        return ordinal;
    }
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
}
3. 枚举为什么不能通过反射创建实例?(要看源码Constructor的newInstance方法)

源码分析:Class类通过反射调用Constructor类的newInstance方法创建实例

public final class Constructor<T> extends Executable {
    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        // 原因在这里:如果这个Class类是属于Enum的话,则会报异常,创建失败
        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;
    }
}

如果这个Class类是属于Enum的话,则会报异常,创建失败

4. 枚举为什么可以避免序列化问题?(重写了readObject方法)

反序列化创建实例的本质是调用Object的readObject方法,而Enum类的方法一调用就会报异常

public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
    /**
     * prevent default deserialization
     */
    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        // 原因在这里:直接报“不能序列化枚举”异常
        throw new InvalidObjectException("can't deserialize enum");
    }
}

原因是:Enum重写了Object的readObject方法,当调用的时候会直接报异常

5. 枚举为什么可以避免clone问题?(重写了clone方法)
public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
	
    protected final Object clone() throws CloneNotSupportedException {
    	// 原因在这里:直接报“不支持Clone”异常
        throw new CloneNotSupportedException();
    }
}

原因是:Enum重写了Object的clone方法,当调用的时候会直接报异常

2. 懒汉模式单例(在类加载时不初始化,调用获取单例的方法时候再初始化)

在调用静态方法getInstance时会实例化,

1. 在静态方法中使用双重校验(保证只创建一个实例)
public class SingletonDemo {
    private volatile static SingletonDemo instance;
    private SingletonDemo(){}
    public static SingletonDemo getInsatance(){
    	// 第一次校验,如果已经创建好实例的话,就不用去获取锁了
        if (instance == null) {
        	// A、B两个线程同时到这里来了,A获取了锁,B在这里阻塞等待
            synchronized (SingletonDemo.class) {
            	// 第二次校验,防止未创建实例时,A获取锁创建了实例,B之后获取锁又创建了实例
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return singletonDemo;
    }
}

注意:这里用到了双重校验机制,2个IF分别有什么作用要记住,一个都不能删掉

2. 在静态方法中调用静态内部类
public class SingletonDemo {
	// 静态内部类
    private static class SingletonHolder{
        private static final SingletonDemo instance = new SingletonDemo();
    }
    private SingletonDemo(){}
    public static final SingletonDemo getInsatance(){
        return SingletonHolder.instance;
    }
}

注意:静态内部类在类加载的时候,是不会被扫描JVM到的,所以不会在类加载的时候实例化

3. 基于容器实现单例(单例注册表)

Spring创建Bean的方式(有兴趣可以深入了解下)

spring的BeanDefinition(因为BeanDefinition)通过ConcurrentHashMap实现单例注册表的特殊方式实现单例模式

public class ContainerSingleton {
    private ContainerSingleton() {
    }
    // ioc容器本质就是一个ConcurrentHashMap(确认过了,是的)
    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

	// 获取实例的方法,不同的是需要有入参
    public static Object getInstance(String className) {
        Object instance = null;
        // 第一次校验
        if (!ioc.containsKey(className)) {
            synchronized (ContainerSingleton.class) {
            	// 第二次校验
                if (!ioc.containsKey(className)) {
                    try {
                        instance = Class.forName(className).newInstance();
                        ioc.put(className, instance);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    return instance;
                }
            }
        }
        return ioc.get(className);
    }
}

本质是采用在静态方法中使用双重校验实现,区别是存放实例的地方变了

题外话:为什么BeanFactory和ApplicationContext都是调用BeanDefinition来初始化实例的,BeanDefinition使用懒加载的方式实现,BeanFactory可以理解,但是ApplicationContext是如何在Spring容器启动的时候去创建实例的?TODO

4. 非枚举创建的单例,存在的问题

原文链接:为什么要用枚举实现单例模式(避免反射、序列化问题)

1. 可以使用反射机制调用私有构造器,创建第二个实例

解决:修改构造方法,当调用的时候直接抛异常就行

2. 序列化前后,实例发生改变

public class SerSingleton implements Serializable {
    private volatile static SerSingleton uniqueInstance;
    private  String content;
    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
    private SerSingleton() {
    }

    public static SerSingleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (SerSingleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new SerSingleton();
                }
            }
        }
        return uniqueInstance;
    }


    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SerSingleton s = SerSingleton.getInstance();
        s.setContent("单例序列化");
        System.out.println("序列化前读取其中的内容:"+s.getContent());
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
        oos.writeObject(s);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("SerSingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        SerSingleton s1 = (SerSingleton)ois.readObject();
        ois.close();
        System.out.println(s+"\n"+s1);
        System.out.println("序列化后读取其中的内容:"+s1.getContent());
        System.out.println("序列化前后两个是否同一个:"+(s==s1));
    }
}

控制台:

序列化前读取其中的内容:单例序列化
com.lxp.pattern.singleton.SerSingleton@135fbaa4
com.lxp.pattern.singleton.SerSingleton@58372a00
序列化后读取其中的内容:单例序列化
序列化前后两个是否同一个:false

任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。”当然,这个问题也是可以解决的,想详细了解的同学可以翻看《effective java》第77条:对于实例控制,枚举类型优于readResolve

因为readObject方法的存在,导致每次序列化前后的对象

解决:重写readObject方法并在方法内直接抛异常

3. 可以通过clone拷贝的方式去创建一个新的实例

解决:继承clone并在方法内直接抛异常

5. 单例模式常见面试题(TODO)

问题来源是在这里,答案是我自己总结的
原文链接:https://blog.csdn.net/androidzhaoxiaogang/article/details/6832364

1. 哪些类是单例模式的后续类?在Java中哪些类会成为单例?

这里它们将检查面试者是否有对使用单例模式有足够的使用经验。他是否熟悉单例模式的优点和缺点。

2. 你能在Java中编写单例里的getInstance()的代码?

使用双重校验或静态内部类

很多面试者都在这里失败。然而如果不能编写出这个代码,那么后续的很多问题都不能被提及。

3. 在getInstance()方法上同步有优势还是仅同步必要的块更优优势?你更喜欢哪个方式?

只锁创建实例的那块代码就行,大多数情况下都是获取已经创建的实例

这确实是一个非常好的问题,我几乎每次都会提该问题,用于检查面试者是否会考虑由于锁定带来的性能开销。因为锁定仅仅在创建实例时才有意义,然后其他时候实例仅仅是只读访问的,因此只同步必要的块的性能更优,并且是更好的选择。

4. 什么是单例模式的延迟加载或早期加载?你如何实现它?

问的就是对类加载和性能开销的理解(这个问题在【八股文】JVM篇的时候再讲)

这是和Java中类加载的载入和性能开销的理解的又一个非常好的问题。我面试过的大部分面试者对此并不熟悉,但是最好理解这个概念。

5. JDK中的单例模式的实例有哪些?(好问题)

1.JDK中的饿汉模式(JVM启动时实例化)
1. java.lang.Runtime类(JVM进程启动的,供所有线程使用)

Runtime类封装了Java运行时的环境。每一个java程序实际上都是启动了一个JVM进程,那么每个JVM进程都是对应这一个Runtime实例,此实例是由JVM为其实例化的。每个Java应用程序都有一个 Runtime类实例,使应用程序能够与其运行的环境相连接。

由于Java是单进程的,所以,在一个JVM中,Runtime的实例应该只有一个。所以应该使用单例来实现。

1. 源码如下:
package java.lang;

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }
    /** Don't let anyone else instantiate this class */
    private Runtime() {}

Runtime是通过饿汉模式的new方式创建实例的(实现的方法有点老了)

2. 为什么Runtime类要用饿汉模式?

Runtime很重要(封装了Java运行时的环境),让JWM去实例化,总不可能让JAVA应用程序去实例化吧

2. JDK中的懒汉模式(GUI包下)(JVM启动时不初始化)
1. java.awt.Toolkit#getDefaultToolkit()
2. java.awt.GraphicsEnvironment#getLocalGraphicsEnvironment()
3. java.awt.Desktop#getDesktop()

这三个类都是在JDK的GUI包下的,并不常使用,所以为了节省资源,使用了懒汉模式

这是个完全开放的问题,如果你了解JDK中的单例类,请共享给我。

6. 单例模式的两次检查锁是什么?

说的就是DCL双重校验机制,在懒汉模式下的

7. 你如何阻止使用clone()方法创建单例实例的另一个实例?

单例类去实现Object中的clone方法并直接抛出异常就可以了

问:枚举类默认能防止clone创建实例吗?可以的,Enum类重写了clone方法

该类型问题有时候会通过如何破坏单例或什么时候Java中的单例模式不是单例来被问及。

8. 如何阻止通过使用反射来创建单例类的另一个实例?

在私有的构造方法中抛出异常

开放的问题。在我的理解中,从构造方法中抛出异常可能是一个选项。

9. 如何阻止通过使用序列化来创建单例类的另一个实例?

实现Object的readObject方法并抛出异常

又一个非常好的问题,这需要Java中的序列化知识并需要理解如何使用它来序列化单例类。该问题是开放问题。

10. Java中的单例模式什么时候是非单例?

问的其实就是什么情况会导致多个实例,如未加锁的并发访问反射调用构造方法通过序列化的readObject创建实例通过Object的clone方法(浅拷贝或深拷贝)创建实例等等(没等等了好像就这么多吧)

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值