实用的设计模式04-单例模式

单例模式,简单但是又不简单。简单是指原理很容易懂,即保证一个类只存在一个对象;不简单是指实现方式非常多样,而且需要注意的点非常多。

1、思考如何实现单例模式

现在我们不去考虑单例模式的标准实现,而是让我们自己考虑,如何再代码中实现让一个类只能实例化一次,为了简单一点,我们先不考虑多线程并发的情况。
思考

  1. 首先,不能多次实例化,那么构造方法就不能随意调用,我们需要控制对构造方法的调用
  2. 全局唯一,刚好对应的是static的作用,static修饰的变量,是该类所有实例所共有的,如下例:
public class Sky {
    /**
     * 天空颜色
     */
    private static String color = "blue";

    public void color(){
        System.out.println("今天的天空颜色是:"+color);
    }

    public static void main(String[] args) {
        Sky sky1 = new Sky();
        Sky sky2 = new Sky();

        sky1.color();
        sky2.color();

        //修改color
        Sky.color = "gray";

        sky1.color();
        sky2.color();
    }
}

输出
今天的天空颜色是:blue
今天的天空颜色是:blue
今天的天空颜色是:gray
今天的天空颜色是:gray

所以,我们可以在类中添加一个static修饰成员变量 A ,类型就是该类的类型;然后把构造方法改成私有的,并且在类中提供一个静态方法可以返回该类的成员变量 A ,当然返回之前需要先判断A是否为null。

2、懒汉式

public class Sky {
    private static Sky sky = null;
    /**
     * 私有构造方法
     */
    private Sky() {
    }

    public static Sky getInstance(){
        if (Sky.sky==null){
            sky = new Sky();
        }
        return sky;
    }
    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        Sky sky1 = Sky.getInstance();
        Sky sky2 = Sky.getInstance();

        System.out.println(sky1==sky2);

    }
}

输出:true

因为只有在调用Sky.getInstance()之后才会创建对象,像懒汉一样,饿了才去讨饭,所以称为懒汉式。
优点是延迟加载,真正用到对象的时候才去实例化,提高了资源利用率。
缺点时存在并发访问无效的问题,具体请看下面的双重锁检测。

3、饿汉式

上述代码中只有在调用Sky.getInstance()之后才会创建对象,如果想在简单一点可以这样做

public class Sky {
    private static Sky sky = new Sky();
    /**
     * 私有构造方法
     */
    private Sky() {
    }

    public static Sky getInstance(){
        return sky;
    }
    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        Sky sky1 = Sky.getInstance();
        Sky sky2 = Sky.getInstance();

        System.out.println(sky1==sky2);

    }
}

输出:true

上述代码不管调不调用Sky.getInstance(),对象早就存在,像饿汉一样饥渴,所以称为饿汉式。
一般不采用此种方式,因为在类初始化时就创建了对象,有可能这个对象在某些业务逻辑张不会用到,这样就造成了资源浪费。
但是也有优点,那就是不存并发访问的问题。

4、双重检测锁式

懒汉模式存在并发访问的问题。如下测试:

public class Sky {

    private static Sky sky = null;

    /**
     * 私有构造方法
     */
    private Sky() {
        System.out.println("我被创建了");
    }

    public static Sky getInstance(){
        if (Sky.sky==null){
            sky = new Sky();
        }
        return sky;
    }

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {

        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                Sky.getInstance();
            }).start();
        }

    }
}

输出:
我被创建了
我被创建了
我被创建了
我被创建了
我被创建了
我被创建了
我被创建了

可以看到,对象被执行了多次,这显然不符合我们的预期,我们的预期是在程序的整个生命周期,该类只被实例化一次。
想要解决这个问题,首先得弄明白问题的根源。代码中创建了20个线程,但是根据输出结果,构造方法被调用了7次,重复执行测试代码发现,构造方法被调用的次数并不完全相同。那这是为什么呢?
原因在每个线程中Sky.getInstance();的调用时间,可能存在上一个线程刚刚执行完判断语句,还未创建对象,如下图所示:
在这里插入图片描述

解决并发异常,很自然的们会想到锁 。那么加锁就会涉及到两个问题,对谁加锁?加在哪?我们主要讨论锁加载哪的问题。至于对谁加锁,因为我们实在静态方法内部加锁,所以要锁的对象是类的Class对象(不理解的话简单记住就行)。

情形一:对整个if语句加锁,
在这里插入图片描述
思考以下这种加锁形式可以解决问题?当然是可以的,但是存在很大问题,每个线程都只能等待上个线程执行完if语句块的内容才能接着执行,这样所有的线程就变成了串行执行,对性能的影响十分大。

情形二:仅仅对if内的new语句加锁
在这里插入图片描述
这种加锁形式也是行不通的,它甚至没有解决并发带来的类重复实例化的问题,还是会导致饿汉式单例模式中的并发问题。

情形三:最终解决办法,双重检测锁式
在这里插入图片描述
首先不会导致情形一中使得程序串行执行的问题,同时同步语句块内部的if判断语句也解决了类重复实例化的问题

完整代码:

public class Sky {
    private static Sky sky = null;
    /**
     * 私有构造方法
     */
    private Sky() {
        System.out.println("我被创建了");
    }

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

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                Sky.getInstance();
            }).start();
        }
    }
}

输出:
我被创建了

其实情形三到这里还没有结束,因为还存在JVM指令重排的问题,所以完整的双重检测锁式还需在成员变量sky上加上volatile关键字。

	/**
     * volatile是为了解决指令重排的问题
     */
    private static volatile Sky sky = null;

5、静态内部类式

先看实现代码

public class Sky2 {
    //静态内部类
    public static class inner{
        private static final Sky2 sky2 = new Sky2();
    }

    /**
     * 私有的构造方法
     */
    private Sky2(){
        System.out.println("我被创建了");
    }
    
    public static Sky2 getInstance(){
        return inner.sky2;
    }

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                Sky2.getInstance();
            }).start();
        }
    }
}
输出:
我被创建了

此种实现方式,乍一看和饿汉模式相同,但实际上并不是,使用静态内部类实现和懒汉模式一样,也是一种懒加载的方式。
为什么可以这样?原因在于外部类与静态内部类的加载顺序,外部类初次加载,会初始化静态变量、静态代码块、静态方法,但不会加载内部类和静态内部类,只有在调用静态内部类的方法时候才会初始化,所以就实现了懒加载。
所以静态内部类的实现方式结合了懒汉和饿汉式优点,是一种很不错的单例模式实现方式。

http://t.csdn.cn/yFdmF
所以修正一下的说法便是,静态内部类单例模式的核心原理为对于一个类,JVM在仅用一个类加载器加载它时,静态变量的赋值在全局只会执行一次!
使用静态内部类的优点是:因为外部类对内部类的引用属于被动引用,不属于前面提到的三种必须进行初始化的情况,所以加载类本身并不需要同时加载内部类。在需要实例化该类是才触发内部类的加载以及本类的实例化,做到了延时加载(懒加载),节约内存。同时因为JVM会保证一个类的()方法(初始化方法)执行时的线程安全,从而保证了实例在全局的唯一性。

使用静态内部类的优点是:因为外部类对内部类的引用属于被动引用,不属于前面提到的三种必须进行初始化的情况,所以加载类本身并不需要同时加载内部类。在需要实例化该类是才触发内部类的加载以及本类的实例化,做到了延时加载(懒加载),节约内存。同时因为JVM会保证一个类的()方法(初始化方法)执行时的线程安全,从而保证了实例在全局的唯一性。

6、反射对单例的破坏

查看如下测试代码

public class Sky {

    /**
     * volatile是为了解决指令重排的问题
     */
    private static volatile Sky sky = null;

    /**
     * 私有构造方法
     */
    private Sky() {
        System.out.println("我被创建了");
    }

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

    /**
     * 测试
     */
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        final Sky sky1 = Sky.getInstance();
		//拿到构造器
        Constructor<Sky> skyConstructor = Sky.class.getDeclaredConstructor(null);
        skyConstructor.setAccessible(true);
		//创建对象
        final Sky sky2 = skyConstructor.newInstance(null);

        System.out.println(sky1);
        System.out.println(sky2);
    }
}
输出:
我被创建了
我被创建了
SingletonPattern.statictest.Sky@23fc625e
SingletonPattern.statictest.Sky@3f99bd52

可以看到,构造器被执行了两遍,显然与预期不符。
问题探究:为什么使用反射回再次实例化对象?因为Sky.class.getDeclaredConstructor(null);方法可以拿到对象私有的构造器,使用可以实例化对象。
解决方式探索

  1. 我们能不能阻止反射创建对象,再创建对象之前首先判断对象是否已存在
    在这里插入图片描述
    因为判空的时候可能会有线程同步到达,所以仍然需要加锁。
public class Sky {

    /**
     * volatile是为了解决指令重排的问题
     */
    private static volatile Sky sky = null;

    /**
     * 私有构造方法
     */
    private Sky() throws Exception {
        synchronized (Sky.class){
            if (Sky.sky !=null){
                throw new Exception("对象已存在!");
            }
        }
        System.out.println("我被创建了");
    }

    public static Sky getInstance() throws Exception {
            if (Sky.sky == null) {
                synchronized (Sky.class) {
                    if (Sky.sky == null) {
                        sky = new Sky();
                    }
                }
            }
        return sky;
    }

    /**
     * 测试
     */
    public static void main(String[] args) throws Exception {

        final Sky sky1 = Sky.getInstance();

        Constructor<Sky> skyConstructor = Sky.class.getDeclaredConstructor(null);
        skyConstructor.setAccessible(true);

        final Sky sky2 = skyConstructor.newInstance(null);

        System.out.println(sky1);
        System.out.println(sky2);
    }
}

输出:
我被创建了
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
	at SingletonPattern.statictest.Sky.main(Sky.java:52)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:567)
	at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)
Caused by: java.lang.Exception: 对象已存在!
	at SingletonPattern.statictest.Sky.<init>(Sky.java:25)
	... 11 more

可见,这样就阻止了反射创建对象。
但是,上面的测试代码种,两个对象,一个是通过单例获取,一个是通过反射获取,这样是可以的。但是当两个都是反射获取时,因为都是直接调用构造方法,不会执行单例的代码(getInstance()方法种获取单例的代码),也就是private static volatile Sky sky = null;种sky的值一值为空,所以仍然回实例化两次。

public class Sky {
    /**
     * volatile是为了解决指令重排的问题
     */
    private static volatile Sky sky = null;
    /**
     * 私有构造方法
     */
    private Sky() throws Exception {

        synchronized (Sky.class){
            if (Sky.sky !=null){
                throw new Exception("对象已存在!");
            }
        }
        System.out.println("我被创建了");
    }
    public static Sky getInstance() throws Exception {
            if (Sky.sky == null) {
                synchronized (Sky.class) {
                    if (Sky.sky == null) {
                        sky = new Sky();
                    }
                }
            }
        return sky;
    }

    /**
     * 测试
     */
    public static void main(String[] args) throws Exception {

        //final Sky sky1 = Sky.getInstance();

        Constructor<Sky> skyConstructor = Sky.class.getDeclaredConstructor(null);
        skyConstructor.setAccessible(true);

        final Sky sky1 = skyConstructor.newInstance(null);
        final Sky sky2 = skyConstructor.newInstance(null);

        System.out.println(sky1);
        System.out.println(sky2);
    }
}

输出:
我被创建了
我被创建了
SingletonPattern.statictest.Sky@23fc625e
SingletonPattern.statictest.Sky@3f99bd52

最终问题解决:能不能想办法阻止反射创建对象呢,或者说存不存在某种反射无法创建的对象呢?
让我们翻开skyConstructor.newInstance(null);的源码,往下找,可以发现:
在这里插入图片描述

7、单例模式的最优解–枚举式

实现代码,将需要单例的类声明成一个枚举类型,关于枚举类型的详细说明这里就不探讨了,不熟悉的朋友可以去再回顾一下。
枚举主要有以下特点:

  1. 构造方法天生就是私有的;
  2. 枚举对象无法通过反射创建;
public enum EnumSky {
    /*实例*/
    INSTANCE;

    private String color = "blue";

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

	//构造方法默认就是私有的
    EnumSky(){
        System.out.println("我被创建了!");
    }
    public void color(){
        System.out.println("今天的天空颜色是:"+color);
    }
}

测试:

public class Test {
    public static void main(String[] args) {
        try {
            EnumSky sky1 = EnumSky.INSTANCE;
            EnumSky sky2 = EnumSky.INSTANCE;
            Class[] params = {String.class,int.class};
            Constructor<EnumSky> constructor = EnumSky.class.getDeclaredConstructor(params);
            constructor.setAccessible(true);
            EnumSky sky3 = constructor.newInstance(null);

            System.out.println(sky1);
            System.out.println(sky2);
            System.out.println(sky3);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
输出:
我被创建了!
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:493)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
	at SingletonPattern.statictest.Test.main(Test.java:19)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:567)
	at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)

测试结果可以看到,不仅单例,而且无法反射创建,解决了反射破坏单例的问题。

8、总结

单例模式实现方式众多,最安全的方式就是采用枚举类,但是上面所说的众多方式都是默认需要单例的类由我们自己实现。但是再实际场景中,需要单例创建的类很多并不似由我们自己定义,而且引用的已有的类。非常典型的一个例子就是窗体程序的开发,很多时候我们想要某个窗体是单例创建的,这时候应该怎么做的呢?其实这种情况下无法保证绝对安全的单例,因为它是个普通的类,一定可以反射创建。所以只能是我们去实现一个另外一个类来控制该类的实例化,凡是需要该类实例的情况都使通过我们自定义的类来实现。

作为控制实例化的类,上述的几种方式都可以,无非是将需要单例的类的实例作为我们创建的控制类的一个成员变量而已。下面我们使用枚举的方式来创建一个单例的JFrame类。

说明:JFrame就是一个窗体类,贴一下JDK中的描述
An extended version of java.awt.Frame that adds support for the JFC/Swing component architecture. You can find task-oriented documentation about using JFrame in The Java Tutorial, in the section [How to Make Frames](https://docs.oracle.com/javase/tutorial/uiswing/components/frame.html).

public enum SingletonJFrame {
    /**实例*/
    INSTANCE;

    private JFrame frame;

    SingletonJFrame(){
        System.out.println("我被创建了!");
        frame = new JFrame();
    }

    public JFrame getInstance(){
        return frame;
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        JFrame instance1 = SingletonJFrame.INSTANCE.getInstance();
        JFrame instance2 = SingletonJFrame.INSTANCE.getInstance();
    }
}
输出:
我被创建了!

9、补充

  1. 如何解决序列化破坏单例的问题
    在把一个对象反序列化后,也会创建出一个对象的示例,可能存在多次反序列化导致单例破坏的情况,可采用如下方法解决:在类中新建一个方法
public Object readResolve() {
 	return INSTANCE;//将单例对象返回,这样反序列化时就不会再次创建对象了
}

注意:枚举单例不会存在反序列化问题。

  1. 单例类最好用final修饰,这样可一避免子类对破环单例。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值