自学内容网 自学内容网

设计模式之单例模式


参考文章1: https://blog.csdn.net/qq_32099833/article/details/111601001
参考文章2: https://blog.csdn.net/qq_32099833/article/details/103604486
参考文章3: https://developer.aliyun.com/article/1429465
参考书籍:《设计模式之禅(第2版)》

一、单例模式介绍

1.1 单例故事(我是皇帝我独苗)

自从秦始皇确立了皇帝这个位置以后,同一时期基本上就只有一个人孤零零地坐在这个
位置。这种情况下臣民们也好处理,大家叩拜、谈论的时候只要提及皇帝,每个人都知道指
的是谁,而不用在皇帝前面加上特定的称呼,如张皇帝、李皇帝。这一个过程反应到设计领
域就是,要求一个类只能生成一个对象(皇帝),所有对象对它的依赖都是相同的,因为只
有一个对象,大家对它的脾气和习性都非常了解,建立健壮稳固的关系,我们把皇帝这种特
殊职业通过程序来实现。

皇帝每天要上朝接待臣子、处理政务,臣子每天要叩拜皇帝,皇帝只能有一个,也就是
一个类只能产生一个对象,该怎么实现呢?对象产生是通过new关键字完成的(当然也有其
他方式,比如对象复制、反射等),这个怎么控制呀,但是大家别忘记了构造函数,使用
new关键字创建对象时,都会根据输入的参数调用相应的构造函数,如果我们把构造函数设
置为private私有访问权限不就可以禁止外部创建对象了吗?

1.2 单例简介(啊可西)

Ensure a class has only one instance, and provide a global point of access to it.
(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

单例模式也就是指在整个运行时域中,一个类只能有一个实例对象。

那么为什么要有单例模式呢?

这是因为有的对象的创建和销毁开销比较大,比如数据库的连接对象。所以我们就可以使用单例模式来对这些对象进行复用,从而避免频繁创建对象而造成大量的资源开销。

1.3 人非圣人(单例优缺点)

– 优点

  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地
    创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

  • 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要
    比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一
    个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM
    垃圾回收机制)

  • 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在
    内存中,避免对同一个资源文件的同时写操作。

  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单
    例类,负责所有数据表的映射处理。

– 缺点

  • 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途
    径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它
    要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊
    情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。

  • 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行
    测试的,没有接口也不能使用mock的方式虚拟一个对象。

  • 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单
    例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

1.4 单例有点原则

  • 阻止类被通过常规方法实例化(私有构造方法private)
  • 保证实例对象的唯一性(以静态方法或者枚举返回实例)
  • 保证在创建实例时的线程安全(确保多线程环境下实例只有一个)
  • 对象不会被外界破坏(确保在有序列化、反序列化时不会重新构建对象)

二、单例模式的实现方式

2.1 懒加式

懒汉模式,顾名思义就是懒,没有对象需要调用它的时候不去实例化,有人来向它要对象的时候再实例化对象,因为懒,比我还懒(不要乱说哦)

2.1.1懒加载(线程不安全)

class Singleton {

    // 保证构造方法私有,不被外界类所创建
    private Singleton(){

    }

    // 初始化对象为null
    private static Singleton instance = null;

    // 暴露给外部调用的方法
    public static Singleton getInstance(){
        // 判断是否被构造过,保证对象的唯一
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

从上面我们可以看到,通过public class Singleton我们可以全局访问该类;通过私有化构造方法,能够避免该对象被外界类所创建;以及后面的getInstance方法能够保证创建对象实例的唯一。

但是我们可以看到,这个实例不是在程序启动后就创建的,而是在第一次被调用后才真正的构建,所以这样的延迟加载也叫做懒加载

然而我们发现getInstance这个方法在多线程环境下是线程不安全的—如果有多个线程同时执行该方法会产生多个实例。比如AB两个线程同时进入getInstance方法都判断得instance等于空,然后先后都创建了实例,那么就相当于创建了两个实例,这是线程不安全的。

那么该怎么办呢?我们想到可以将该方法变成线程安全的,加上synchronized关键字。

2.1.1懒加载(线程安全)

class Singleton {

    // 保证构造方法私有,不被外界类所创建
    private Singleton(){

    }
    // 初始化对象为null
    private static Singleton instance = null;

    //判断是否被构造过,保证对象的唯一,而且synchronize也能保证线程安全
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

但是我们知道,如果一个静态方法被synchronized所修饰【synchronized锁范围】,会把当前类的class 对象锁住,会增大同步开销,降低程序的执行效率。所以可以从缩小锁粒度角度去考虑,把synchronized放到方法里面去,也就是让其修饰同步代码块,如下所示:

class Singleton {

    // 保证构造方法私有,不被外界类所创建
    private Singleton(){

    }
    // 初始化对象为null
    private static Singleton instance = null;

    public static  Singleton getInstance(){
        if(instance == null){
            // 利用同步代码块,锁的是当前实例对象
            synchronized (Singleton.class){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

但是这个时候,我们发现if(instance == null)是没有锁的,所以当两个线程都执行到该语句并都判断为true时,还是会排队创建新的对象,那么有没有新的解决方式?

2.1.1懒加载(线程安全,双重检测锁)

class Singleton {

    // 保证构造方法私有,不被外界类所创建
    private Singleton(){

    }
    // 初始化对象为null
    private static Singleton instance = null;
    
    public static  Singleton getInstance(){
        // 第一次判断
        if(instance == null){
            // 利用同步代码块,锁的是当前实例对象
            synchronized (Singleton.class){
                // 第二次判断
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

我们在上一节的代码上再加上一次判断,就是双重检测锁(Double Checked Lock, DCL)【双检锁/双重检测锁】。但是上述代码也存在一些问题,比如在instance = new Singleton() 这行代码中,它并不是一个原子操作,实际上是有三步:

  • 给对象实例分配内存空间
  • new Singleton() 调用构造方法,初始化成员字段
  • 将 instance对象指向分配的内存空间

所以会涉及到内存模型中的指令重排,那么这个时候可以用 volatile关键字来修饰 instance对象,防止指令重排,写出如下代码:

class Singleton {

    // 保证构造方法私有,不被外界类所创建
    private Singleton(){

    }
    // 初始化对象,加上volatile防止指令重排
    private volatile static Singleton instance = null;

    public static  Singleton getInstance(){
        // 第一次判断
        if(instance == null){
            synchronized (Singleton.class){
                // 第二次判断
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

2.1.1懒加载(线程安全,CAS乐观锁)

public class Singleton {
   
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
    private static Singleton instance;

    private Singleton(){
   }
    public static final Singleton getInstance() {
   
        for(;;) {
   
            Singleton instance = INSTANCE.get();
            if(instance != null) {
   
                return instance;
            }
            instance = new Singleton();
            if(INSTANCE.compareAndSet(null, instance)) {
   
                return instance;
            }
        }
    }
}

CAS 是一种乐观锁,依赖于底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并发度,但是如果忙等待一直执行不成功,也会对CPU造成较大的执行开销。

2.2 饿汉式

饿汉模式,就是它很饿,它的对象早早的就创建好了(懒汉是有人管它要了再创建)I’m hungry

饿汉(线程安全)

不同于懒加载的延迟实现实例,我们也可以在程序启动时就加载好单例对象:

public class Singleton {
   
    /**保证构造方法私有,不被外界类所创建**/
    private Singleton() {
   }
    /**直接获取实例对象**/
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
   
        return instance;
    }
}

这样的好处是线程安全,单例对象在类加载时就已经被初始化,当调用单例对象时只是把早已经创建好的对象赋值给变量。缺点就是如果一直没有调用该单例对象的话,就会造成资源浪费。除此之外还有其他的实现方式。

2.3 静态内部类

public class Singleton {
   
    /**保证构造方法私有,不被外界类所创建**/
    private Singleton() {
   }
    /**利用静态内部类获取单例对象**/
    private static class SingletonInstance {
   
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
   
        return SingletonInstance.instance;
    }
}

静态内部类的方法结合了饿汉方式,它们都采用了类加载机制来保证当初始化实例时只有一个线程执行,从而保证了多线程下的安全操作。原因就是JVM在类初始化阶段时会创建一个锁,该锁可以保证多个线程同步执行类初始化工作。

但是静态内部类不会在程序启动时创建单例对象,它是在外界调用 getInstance方法时才会装载内部类,从而完成单例对象的初始化工作,不会造成资源浪费。

然而这种方法也存在缺点,它可以通过反射来进行破坏。

2.4 枚举

枚举是《Effective Java》作者推荐的单例实现方式,枚举只会装载一次,无论是序列化、反序列化、反射还是克隆都不会新创建对象。因此它也不会被反射所破坏。

public enum Singleton {
    INSTANCE;
}

所以这种方式是线程安全的,而且无法被反射而破坏

三、 JDK中的单例

java.lang.Runtime

Every Java application has a single instance of class Runtime that allows the application to interface with the environment in which the application is running. The current runtime can be obtained from the getRuntime method.

An application cannot create its own instance of this class.

每个java程序都含有唯一的Runtime实例,保证实例和运行环境相连接。当前运行时可以通过getRuntime方法获得

我们来看看具体的代码:

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

    public static Runtime getRuntime() {
   
        return currentRuntime;
    }

    private Runtime() {
   }

我们发现这就是单例模式的饿汉加载方式。

java.awt.Desktop

类似的,在java.awt.Desktop中也存在单例模式的使用,比如:

public class Desktop {
   

    private DesktopPeer peer;

    private Desktop() {
   
        peer = Toolkit.getDefaultToolkit().createDesktopPeer(this);
    }
    //懒加载
    public static synchronized Desktop getDesktop(){
   
        if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
        if (!Desktop.isDesktopSupported()) {
   
            throw new UnsupportedOperationException("Desktop API is not " +
                                                    "supported on the current platform");
        }

        sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
        Desktop desktop = (Desktop)context.get(Desktop.class);

        if (desktop == null) {
   
            desktop = new Desktop();
            context.put(Desktop.class, desktop);
        }

        return desktop;
    }

这种方法就是一种延迟加载的方式。

Spring Bean 作用域

比较常见的就是Spring Bean作用域里的单例了,这个比较常见,可以通过配置文件进行配置:

<bean class="..."></bean>

四、单例模式的扩展

如果一个类可以产生多个对象,对象的数量不受限制,则是非常容易实现的,直接使用new关键字就可以了,如果只需要一个对象,使用单例模式就可以了,但是如果要求一个类只能产生两三个对象呢?

一般情况下,一个朝代的同一个时代只有一个皇帝,那有没有出现两个皇帝的情况呢?确实有,就出现在明朝,那三国期间的算不算?不算,各自称帝,各有各的地盘,国号不同。大家还记得《石灰吟》这首诗吗?作者是谁?于谦。他是被谁杀死的?明英宗朱祁镇。对,就是那个在土木堡之变中被瓦剌俘虏的皇帝,被俘虏后,他弟弟朱祁钰当上了皇帝,就是明景帝,估计刚当上皇帝乐疯了,忘记把他哥哥朱祁镇升级为太上皇,在那个时期就出现了两个皇帝,这期间的大臣是非常郁闷的,为什么呀?因为可能出现今天参拜的皇帝和昨天
的皇帝不相同,昨天给那个皇帝汇报,今天还要给这个皇帝汇报一遍。
在这里插入图片描述

public class Emperor {
     //定义最多能产生的实例数量
     private static int maxNumOfEmperor = 2;    
     //每个皇帝都有名字,使用一个ArrayList来容纳,每个对象的私有属性
     private static ArrayList<String> nameList=new ArrayList<String>(); 
     //定义一个列表,容纳所有的皇帝实例
     private static ArrayList<Emperor> emperorList=new ArrayList<Emperor>();
     //当前皇帝序列号
     private static int countNumOfEmperor =0;   
     //产生所有的对象
     static{
             for(int i=0;i<maxNumOfEmperor;i++){  
                     emperorList.add(new Emperor("皇"+(i+1)+"帝"));
             }
     }
     private Emperor(){
             //世俗和道德约束你,目的就是不产生第二个皇帝
     }  
     //传入皇帝名称,建立一个皇帝对象
     private Emperor(String name){
             nameList.add(name);
     }  
     //随机获得一个皇帝对象
     public static Emperor getInstance(){
             Random random = new Random();
             //随机拉出一个皇帝,只要是个精神领袖就成
             countNumOfEmperor = random.nextInt(maxNumOfEmperor);
             return emperorList.get(countNumOfEmperor);         
     }
     //皇帝发话了
     public static void say(){
             System.out.println(nameList.get(countNumOfEmperor));               
     }
 }

在Emperor中使用了两个ArrayList分别存储实例和实例变量。当然,如果考虑到线程安全问题可以使用Vector来代替。

public class Minister {
     public static void main(String[] args) {
             //定义5个大臣
             int ministerNum =5;                
             for(int i=0;i<ministerNum;i++){
                     Emperor emperor = Emperor.getInstance();
                     System.out.print("第"+(i+1)+"个大臣参拜的是:");
                     emperor.say();
             }
     }
 }

这种需要产生固定数量对象的模式就叫做有上限的多例模式,它是单例模式的一种扩展,采用有上限的多例模式,我们可以在设计时决定在内存中有多少个实例,方便系统进行
扩展,修正单例可能存在的性能问题,提供系统的响应速度。例如读取文件我们可以在系统启动时完成初始化工作,在内存中启动固定数量的reader实例,然后在需要读取文件时就可以快速响应。

五、序列号生成器案例

5.1 序列号生成器

设计一个全局的序列号生成器,返回long类型,要求生成的序列号全局唯一且递增。

5.2 DCL懒汉式

DCL全称是:Double Check Lock,双重检查加锁,是【懒汉式】的一种实现方式。实例默认不会创建,只有当客户端真正需要访问实例时,实例才会被创建,重点是控制并发访问。

public class SerialGenerator_DCL {
private long code;
private static volatile SerialGenerator_DCL instance;

public synchronized long next() {
return ++code;
}

public static SerialGenerator_DCL getInstance() {
if (instance == null) {
synchronized (SerialGenerator_DCL.class) {
if (instance == null) {// recheck
instance = new SerialGenerator_DCL();
}
}
}
return instance;
}
}

5.3 内部类实现单例

【懒汉式】的另外一种实现方式,通过内部类来持有实例对象,当单例类被Load时,内部类并不会被加载,也就不会创建实例,只有当客户端真正需要访问实例时,内部类才会被加载并创建实例。

public class SerialGenerator_InnerClass {
private long code;

private SerialGenerator_InnerClass() {
}

public synchronized long next() {
return ++code;
}

private static class InnerClass {
final static SerialGenerator_InnerClass INSTANCE = new SerialGenerator_InnerClass();
}

public static SerialGenerator_InnerClass getInstance() {
return InnerClass.INSTANCE;
}
}

5.4 单例枚举

《Effective Java》的作者Joshua Bloch在《用私有构造器或者枚举类型强化Singleton属性》一节中提出了实现单例的另一种方式:包含单个元素的枚举类型。

public enum SerialGenerator_Enum {
INSTANCE;

private long code;

public synchronized long next() {
return ++code;
}
}

六、单例安全测试

6.1 单例代码

public class Single {
private volatile static Single single;
private Single(){}

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

6.2 反射攻击

一个很严重的问题:反射可以调用私有构造器。

public class Client {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//获取Single的构造器
Constructor<Single> constructor = Single.class.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
Single s1 = constructor.newInstance();
Single s2 = constructor.newInstance();
System.out.println(s1);
System.out.println(s2);
}
}
//输出:
unit1.item3.Single@511d50c0
unit1.item3.Single@60e53b93

改造单例
弄清楚原因之后,进行改造就很简单了,只需要在私有构造器内稍作判断即可。

public class Single {
private static Single single;
private Single(){
synchronized (Single.class){
if (single != null) {
throw new RuntimeException("拒绝再次实例.");
}
single = this;
}
}

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

6.3 序列化破坏

当单例类允许被序列化后,如果不做处理,单例模式也会被破坏。

public class Client {
public static void main(String[] args) throws Exception {
//获取实例
Single s1 = Single.getInstance();

//序列化
File file = new File("/Users/panchanghe/temp/Single.obj");
OutputStream outputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(s1);
objectOutputStream.flush();
objectOutputStream.close();
outputStream.close();

//反序列化
InputStream inputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
Single s2 = (Single) objectInputStream.readObject();
objectInputStream.close();
inputStream.close();
System.out.println(s1);
System.out.println(s2);
}
}

输出如下:

unit1.a.Single@6f94fa3e
unit1.a.Single@4e50df2e

反序列化后得到的是一个新的实例。
任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。
要想解决也很简单,编写readResolve()方法,如下:

//防止序列化对单例的破坏
private Object readResolve() {
return single;
}

6.4 解决

单元素的枚举类型已经成为实现Singleton的最佳方法。——《Effective Java》
举实现单例非常简单,即可以防止反射攻击,还可以防止序列化破坏,因为Enum重写了readObject方法。

public enum  EnumSingle {
INSTANCE;

public static EnumSingle getInstance(){
return INSTANCE;
}

//属性、方法照常写...
public void func(){
System.out.println("func....");
}
}

反射攻击验证

Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor();

如上方法去获取构造器会报错:NoSuchMethodException。
一旦将类声明为枚举类型,该类就自动继承了Enum,查看Enum的源码,会发现其只有一个构造器:

protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

通过获取父类的构造器来实例化,测试代码修改如下:

Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingle enumSingle = constructor.newInstance("instance",0);

输出如下:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at unit1.item3.Client.main(Client.java:18)

反射时抛出异常,不能反射创建枚举对象。
查看newInstance()源码,发现如下:
在这里插入图片描述
Java反射时会进行检查,如果是枚举类型,会抛出异常,不允许反射实例化。
序列化破坏验证

public class Client {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, IOException, ClassNotFoundException {
//获取实例
EnumSingle s1 = EnumSingle.getInstance();

//序列化
File file = new File("/Users/panchanghe/temp/Single.obj");
OutputStream outputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(s1);
objectOutputStream.flush();
objectOutputStream.close();
outputStream.close();

//反序列化
InputStream inputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
EnumSingle s2 = (EnumSingle) objectInputStream.readObject();
objectInputStream.close();
inputStream.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
}

输出如下:

INSTANCE
INSTANCE
true

使用枚举来实现,即使是序列化也不会破坏其单例模式。


原文地址:https://blog.csdn.net/qq_42730111/article/details/141289286

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!