单例模式大全,反射拆解!你面试要的8种单例都在这!

《单例模式大全,反射拆解!你面试要的8种单例都在这!》

本文讲述单例设计模式的8种方式,反射和单例的相爱相杀

文章目录

一. 单例模式

单例简介

单例模式:类的对象有且只有一个

单例模式(Singleton),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。

单例模式实现思路

首先控制对象的产生数量:将构造方法私有化(从源头控制对象数量,控制构造方法)

构造方法私有化:

  • 任何其他类均无法参生此对象(本质是任何他类均无法调用构造方法,所以无法产生对象)

  • 唯一的一个对象产生于类内部

  • 唯一的属性为<静态属性>,并且类中提供静态方法取得此对象。因为类的外部无法产生对象,因此无法调用对象方法
    【扩充】

Java实例化对象的5种方式:

  1. new
  2. 反射
  3. 工厂方法返回对象
  4. 调用对象的clone()方法
  5. 通过序列化 / 反序列化

二. 单例模式的10种写法

1. 饿汉式–静态常量

饿汉式单例,顾名思义,就是很饥渴,一上来就new产生实例化对象

/** * 饿汉式三个核心组成 * 1.构造方法私有化 * 2.类内部提供静态私有域 * 3.类内部提供静态方法返回唯一对象 */

class Singletons { 
    //唯一的对象在类加载时产生
    private final static Singletons single = new Singletons();

    //构造方法私有化

    private Singletons() {  }

    //静态方法-----为什么是静态方法??
    //因为在类的外部无法产生对象,因此无法调用对象方法
    //通过getter方法取得唯一的对象
    public static Singletons getInstance(){ 
        return single;
    }
}

public class HungrySingleton01 { 
    public static void main(String[] args) { 
        //不能直接new,而是通过 Singleton.getInstance()静态方法取得类中已经产生好的对象
        Singletons single = Singletons.getInstance();
        Singletons single1 = Singletons.getInstance();
        // single == single1
    }
}

因为是静态常量,single和single1一定是同一个对象,所在的内存地址是相同的

饿汉式单例 (静态常量)

【优点】:书写简单,类加载时就完成了实例化,避免了线程同步问题

【缺点】:在类加载就完成实力化,没有达到懒加载的效果。如果从始至终没有使用过这个实例对象,会造成内存浪费

【总结】

  • 可用,但是可能会造成内存资源的浪费

2. 饿汉式–静态代码块

class Singleton02 { 
    private static Singleton02 single;

    private Singleton02() {  }

    static { 
        single = new Singleton02();
    }

    public static Singleton02 getInstance(){ 
        return single;
    }
}

public class HungrySingleton02 { 
    public static void main(String[] args) { 
        Singleton02 single = Singleton02.getInstance();
        Singleton02 single1 = Singleton02.getInstance();
        // single == single1
    }
}

饿汉式单例 (静态代码块)

这种方式的优缺点和上面第一种静态变量的没差别,区别就是初始化的位置不同,初始化的过程放到了静态代码块。

3. 懒汉式–线程不安全

当第一次去使用Singleton对象的时候才会为其产生实例化对象

通过一个静态公有方法,当使用到该方法时,才创建对象(懒汉式)

/** * @Author: Mr.Q * @Description:懒汉式单例---线程不安全 * 特点: 当第一次去使用Singleton对象的时候才会为其产生实例化对象的操作. */
class Singleton { 

    private static Singleton single;

    //private 声明无参构造
    private Singleton() {  }

    //静态公有方法,当使用到该方法时,才创建对象(懒汉式)
    public static Singleton getInstance(){ 
        if(single == null) { 
            single = new Singleton();
        }
        return single;
    }
}

public class LazySingleton { 
    public static void main(String[] args) { 
        Singleton single = Singleton.getInstance();
        Singleton single1 = Singleton.getInstance();
        // single == single1
    }
}

懒汉式单例 (线程不安全)

【优缺点】

这种写法是存在线程安全问题的。类比于上面两种饿汉式单例模式,它们在没有调用时虽然会造成内存资源的浪费,但是是安全的。因为在类加载时就完成了实例化,避免了线程同步问题。

但是这种懒汉式写法,起到了懒加载效果,但是只能在单线程下使用

【线程安全问题分析】

在多线程场景下,一个线程进入了getInstance方法的if条件判断if(single == null),还没来得及继续向下执行,另一个新进入的线程也通过了这个判断语句,这是就会产生多个实例,就不是单例的了。

所以此方法在多线程场景下不可使用。

4. 懒汉式–同步方法

既然线程不安全,那我们给他加把锁在getInstance方法上保证线程安全。

/** * @Author: Mr.Q * @Description:懒汉式单例---同步方法(效率太低) */
class Singleton04 { 

    private static Singleton04 single;

    //private 声明无参构造
    private Singleton04() {  }

    //静态公有方法,当使用到该方法时,才创建对象(懒汉式)
    public synchronized static Singleton04 getInstance(){ 
        if(single == null) { 
            single = new Singleton04();
        }
        return single;
    }

    public void print() { 
        System.out.println("懒汉式单例(线程安全),同步方法效率太低");
    }
}

public class LazySingleton04 { 
    public static void main(String[] args) { 
        Singleton04 single = Singleton04.getInstance();
        single.print();
    }
}

懒汉式单例(同步方法)

【优点】:解决了线程不安全的问题

【缺点】

  • 效率太低。每个线程想要获取类的实例时,都要等在getInstance这个同步方法外,串型执行。但是由于是单例模式,只会产生一个实例化对象,第一个线程实例化完对象之后,后面的线程便不需要执行if的条件判断了,直接return即可,但是在进入同步方法时每次都要等待,效率太低。

5. 懒汉式–同步代码块

先来说一种错误示范

if条件中添加同步代码块

《单例模式大全,反射拆解!你面试要的8种单例都在这!》

这段代码看起来很完美,很可惜,它是有问题。主要在于single= new Singleton()这句,这并非是一个原子操作。

【分析】:

  • 在多线程场景下,同时有多个线程进入到了if条件内,但是只有一个线程A获取到了锁资源,其余的线程都在阻塞等待
  • 等到线程A执行完之后,由于此时其他线程已经进入if条件中,不会再去判断。所以下一个线程进入同步代码块,继续产生对象。此时,变破坏了单例模式。

此处由于不是原子操作,编译器可能会产生指令重排的问题,所以需要保证原子性,同时加上双重if条件判断。懒汉式 (同步代码块)正确的写法应该是双重检查DCL

6. 双重检查DCL

volatile关键字修饰,轻量级锁,可以使值修改后立即更新到主存

private volatile static SafeSingleton single = null;

【这里添加volatile的原因是】

single = new SafeSingleton();

创建对象这条语句不是原子操作

new关键字创建对象的过程分为三步:

  1. 分配内存空间;

  2. 堆内存上创建对象(执行构造方法);

  3. 将对象的引用指向堆内存;

由于不是原子操作,就可能产生指令重排的问题。

步骤2和步骤3可能会被编译器指令重排,1 -> 2 -> 3的执行顺序变为了1 -> 3 -> 2

先把对象的引用指向堆空间,然后再在堆上创建对象。(理解为图书馆占座位,人还没到,但是位置上却被占用了)

《单例模式大全,反射拆解!你面试要的8种单例都在这!》

判断非空,但是实际拿到的对象还未完成初始化去创建,就会出现空指针异常

所以要防止指令重排,保证有序性,及时通知其线程single的实时状态,就必须加上volatile关键字来防止指令重排,保证1 -> 2 -> 3的执行顺序。

class SafeSingleton { 

    //使用volatile关键字保其可见性
    private volatile static SafeSingleton single = null;

    private SafeSingleton() {  }

    //同步代码块上锁
    public static SafeSingleton getInstance() { 
        if(single == null) { 
            synchronized (SafeSingleton.class) { 
                //双重检查
                if (single == null) { 
                    single = new SafeSingleton();
                }
            }
        }
        return single;
    }

    public void print() { 
        System.out.println("双重检测锁的DCL单例");
    }
}

public class ReflectDCL { 
    public static void main(String[] args) { 
        //静态方法取得类中已经产生好的对象
        SafeSingleton single = SafeSingleton.getInstance();
        single.print();
    }
}

【双重检查分析】

  • Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if(single == null)的检查,这样就可以保证线程安全了。

  • 这样,实例化代码只用执行一次,后面再次访问时,判断if(single == null)直接 return实例化对象,也避免的反复进行方法同步

  • 线程安全;延迟加载;效率较高

7. 静态内部类

我们首先对静态内部类做一个回顾还好面试官还没问,赶紧把【内部类】的知识点补上

静态内部类也是作为一个外部类的静态成员而存在,创建一个类的静态内部类对象不需要依赖其外部类对象

  • 在外部类加载时,静态内部类不会被立即加载,而是在外部类中被使用时才会加载,这符合懒加载的策略。

  • 当我们在外部类中调用静态内部类时,会被加载,并且只会被加载一次,在加载时线程是安全的,保障了线程的安全性。

class StaticInner { 

    private StaticInner() {  }

    //静态内部类
    private static class Singleton { 
        private static final StaticInner INSTANCE = new StaticInner();
    }

    public static StaticInner getSingleton() { 
        return Singleton.INSTANCE;
    }

    public void print() { 
        System.out.println("静态内部类的线程安全的懒汉式单例");
    }
}

public class StaticInnerSingle06 { 
    public static void main(String[] args) { 
        StaticInner single = StaticInner.getSingleton();
        single.print();
    }
}

静态内部类

  1. 这种方式采用了类装载的机制来保证初始化实例时只有一个线程
  2. 静态内部类方式在外部类被加载时并不会立即变例化,而是在需要实例化时,调用getSingleton方法,才会装载 Singleton内部类,从而完成外部类的实例化。
  3. 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的
  4. 优点:避免了线程不安全,利用静态内部类的特点实现延迟加载,效率高

8. 枚举

这借助DK15中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

  • 枚举是基于饿汉式来实现单例模式的
  • 无法通过反射、反序列化来拆解(这也是推荐的原因)
  • 线程安全的静态内部类,JVM在类加载时保证了线程安全
enum Singleton { 
    INSTANCE; //属性

    public static Singleton getInstance() { 
        return Singleton.INSTANCE;
    }
}

public class Enum07 { 
    public static void main(String[] args) { 
        Singleton single = Singleton.getInstance();
        Singleton single1 = Singleton.getInstance();
        System.out.println(single == single1);
    }
}

9. ThreadLocal

多于多线程资源贡献,同步机制采用时间换空间的做法,因为资源只有一份,让不同的线程排队去访问。

ThreadLocal采取的是空间换时间ThreadLocal为每一个线程提供了一份变量副本,人手一份同时访问,不存在相互的竞争和干扰。

class Singleton { 
    private Singleton() {  }

    private static final  ThreadLocal<Singleton> single = new ThreadLocal<Singleton> () { 
        @Override
        protected Singleton initialValue() { 
            return new Singleton();
        }
    };

    public static Singleton getInstance() { 
        return singleton.get();
    }
}

public class ThreadLocalSingleton { 
    public static void main(String[] args) { 
        Singleton single = Singleton.getInstance();
        Singleton singles = Singleton.getInstance();
        System.out.println(single == singles);
    }
}

10. CAS原子实现

import java.util.concurrent.atomic.AtomicReference;

class Singleton { 

    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();

    private Singleton() {  }

    public static Singleton getInstance() { 
        while (true) { 
            Singleton singleton = INSTANCE.get();
            if(singleton != null) { 
                return singleton;
            }
            singleton = new Singleton();
            if(INSTANCE.compareAndSet(null, singleton)) { 
                return singleton;
            }
        }
    }
}

public class SingletonDemo { 
    public static void main(String[] args) { 
        Singleton single1 = Singleton.getInstance();
        Singleton single2 = Singleton.getInstance();
        System.out.println(single1 == single2); //true
    }
}

三. 反射!为所欲为?

先来对反射的内容做个回顾反射,就是要为所欲为

DCL双重检查破坏

通过反射或者序列化会破坏单例,我们就以线程安全的DCL单例来说明。

还是tittle6的代码,我们通过反射来破坏

public static void main(String[] args) throws Exception { 
    SafeSingleton single = SafeSingleton.getInstance();
    Constructor<SafeSingleton> dc = SafeSingleton.class.getDeclaredConstructor();
    dc.setAccessible(true);
    SafeSingleton singleCopy = dc.newInstance();
    //false,单例被破坏
    System.out.println(singleCopy == single);
}

结果输出:false

输出为false,说明单例模式创建了两个对象,被反射破坏了。那如何解决呢?

首先,反射是通过无参构造来创建class对象的,我们在SafeSingleton的构造中再加一把锁来判断:

class SafeSingleton { 

    //使用volatile关键字保其可见性
    private volatile static SafeSingleton single = null;

    private SafeSingleton() { 
        synchronized (SafeSingleton.class) { 
            if (single != null) { 
                throw new RuntimeException("Don't destroy by reflection");
            }
        }
    }

    //同步代码块上锁
    public static SafeSingleton getInstance() { 
        if(single == null) { 
            synchronized (SafeSingleton.class) { 
                //双重检查
                if (single == null) { 
                    single = new SafeSingleton();
                }
            }
        }
        return single;
    }
}

public class ReflectDCL { 
    public static void main(String[] args) throws Exception { 
        SafeSingleton single = SafeSingleton.getInstance();
        Constructor<SafeSingleton> dc = SafeSingleton.class.getDeclaredConstructor();
        dc.setAccessible(true);
        SafeSingleton singleCopy = dc.newInstance();
        //false,单例被破坏
        System.out.println(singleCopy == single);
    }
}

《单例模式大全,反射拆解!你面试要的8种单例都在这!》

问题解决,此时反射无法创建对象。

问题又双出现

刚才单例的对象是通过私有构造方法创建的,即调用了getInstance()方法。但是,我不用这样创建,我唯一一个对象也是通过反射来创建呢?

SafeSingleton single = SafeSingleton.getInstance();

换成

SafeSingleton single = dc.newInstance();

《单例模式大全,反射拆解!你面试要的8种单例都在这!》

这时,单例模式又出幺蛾子了,又被反射获取了!

《单例模式大全,反射拆解!你面试要的8种单例都在这!》

问题解决

我们可以通过添加一个标志位flag来判断,防止反射破坏

class SafeSingleton03 { 

    //使用volatile关键字保其可见性
    private volatile static SafeSingleton03 single = null;
    //添加标志位
    private static boolean flag = false;

    private SafeSingleton03() { 
        synchronized (SafeSingleton03.class) { 
            if (flag == false) { 
                flag = true;
            }else { 
                throw new RuntimeException("Don't destroy by reflection");
            }
        }
    }

    //同步代码块上锁
    public static SafeSingleton03 getInstance() { 
        if(single == null) { 
            synchronized (SafeSingleton03.class) { 
                //双重检查
                if (single == null) { 
                    single = new SafeSingleton03();
                }
            }
        }
        return single;
    }

    public static void main(String[] args) throws Exception { 
        Constructor<SafeSingleton03> dc = SafeSingleton03.class.getDeclaredConstructor();
        dc.setAccessible(true);
        SafeSingleton03 single = dc.newInstance();
        SafeSingleton03 singleCopy = dc.newInstance();

        System.out.println(single);
        System.out.println(singleCopy);
        System.out.println(single == singleCopy);
    }
}

再次执行,我们发现标志位法可以拦截两次反射的破坏。

《单例模式大全,反射拆解!你面试要的8种单例都在这!》

问题又双叒出现

在反射中,我们不仅可以获取构造方法呀,还可以获取成员变量呀。那flag通过反射获取并修改,不就有不行了?

class SafeSingleton03 { 

    //使用volatile关键字保其可见性
    private volatile static SafeSingleton03 single = null;
    //添加标志位
    private static boolean flag = false;

    private SafeSingleton03() { 
        synchronized (SafeSingleton03.class) { 
            if (flag == false) { 
                flag = true;
            }else { 
                throw new RuntimeException("Don't destroy by reflection");
            }
        }
    }

    //同步代码块上锁
    public static SafeSingleton03 getInstance() { 
        if(single == null) { 
            synchronized (SafeSingleton03.class) { 
                //双重检查
                if (single == null) { 
                    single = new SafeSingleton03();
                }
            }
        }
        return single;
    }

    public static void main(String[] args) throws Exception { 
        Constructor<SafeSingleton03> dc = SafeSingleton03.class.getDeclaredConstructor();
        dc.setAccessible(true);
        SafeSingleton03 single = dc.newInstance();

        //再次通过反射修改属性值
        Field flag = SafeSingleton03.class.getDeclaredField("flag");
        flag.setAccessible(true);
        flag.set(dc,false);

        SafeSingleton03 singleCopy = dc.newInstance();

        System.out.println(single);
        System.out.println(singleCopy);
        System.out.println(single == singleCopy);
    }
}

通过代码验证,我们发现确实又双出现问题了!

《单例模式大全,反射拆解!你面试要的8种单例都在这!》

那这,又该怎么搞?

问题,就出在了newInstance方法上,通过反射来创建对象。

我们点开源码看看

《单例模式大全,反射拆解!你面试要的8种单例都在这!》

咦,枚举自带单例模式,反射还破坏不了。是这样吗?我们继续验证

问题最终解决

测试反射能否破坏枚举式单例

enum EnumSingleton { 
    INSTANCE;
}

public class EnumTest { 
    public static void main(String[] args) throws Exception { 
        EnumSingleton single = EnumSingleton.INSTANCE;
        Constructor<EnumSingleton> dc = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
        dc.setAccessible(true);
        EnumSingleton singleCopy = dc.newInstance();

        System.out.println(single);
        System.out.println(singleCopy);
        System.out.println(single == singleCopy);
    }
}

至于为什么反射获取的构造方法传入String、int参数,需要通过Jad反编译来查看。不能传入空参构造,否则出现的是NoSuchMethodException

出现源码中抛出的异常IllegalArgumentException

《单例模式大全,反射拆解!你面试要的8种单例都在这!》

程序最终抛出:java.lang.IllegalArgumentException: Cannot reflectively create enum objects异常

四. 单例模式的应用

单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:

  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 有状态的工具类对象。 -频繁访问数据库或文件的对象。

比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

以下都是单例模式的经典使用场景:

a. 资源共享的情况下

资源共享的情况下,避免由于资源操作时导致的性能或损耗等

  • 如上述中的日志文件,应用配置

b. 控制资源的情况下

控制资源的情况下,方便资源之间的互相通信

  • 如线程池等

【控制资源应用场景】:

  1. Web应用的配置对象的读取。一般也应用单例模式,这个是由于配置文件是共享的资源。
  2. 数据库连接池的设计一般也是采用单例模式。因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
  3. 多线程的线程池的设计一般也是采用单例模式。这是由于线程池要方便对池中的线程进行控制。
  4. 操作系统的文件系统。也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
  5. 外部资源,每台计算机有若干个打印机.但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件
  6. Windows的Task Manager(任务管理器)就是很典型的单例模式,想想看,是不是呢,你能打开两个windows task manager吗?不信你自己试试看哦~
  7. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  8. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  9. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  10. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.

【小结】

在JDK中,java.lang.Runtime就是经典的单例模式

《单例模式大全,反射拆解!你面试要的8种单例都在这!》

掌握这样一些单例模式的奇淫技巧,在历经反射的重重爆破之后,相信你会对单例模式有新的了解!

    原文作者:iqqcode
    原文地址: https://blog.csdn.net/weixin_43232955/article/details/106758218
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞