首页 > 其他分享 >13第五章:【01】单例模式

13第五章:【01】单例模式

时间:2022-11-05 12:33:09浏览次数:68  
标签:13 01 instance 对象 静态 实例 线程 单例

一、单例设计模式介绍

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只能提供一个取得其对象实例的方法(静态方法)。

通俗地说:单例是需要在内存中永远只能创建一个类的实例。

单例的作用:节约内存和保证共享计算的结果正确,以及方便管理。

单例的适用场景

  • 全局信息类:例如任务管理器对象,或者需要一个对象记录整个网站的在线流量等信息。
  • 无状态工具类:类似于整个系统的日志对象等,我们只需要一个单例日志对象负责记录,管理系统日志信息。
  • 如 Hibernate 的 SessionFactory,它充当数据存储源的代理,并负责创建 Session 对象。SessionFactory 并不是轻量级的,一般情况下,一个项目通常只需要一个 SessionFactory 就够了。

二、单例设计模式八种方式

1、单例设计模式要点

(1)某个类只能有一个实例:构造器私有化

(2)它必须自行创建这个实例,含有一个该类的静态变量/常量来保存这个唯一的实例

(3)它必须自行向整个系统提供这个实例,对外提供获取该实例对象的方法:【1】直接暴露;【2】用静态变量的 get 方法获取

2、单例设计方式

单例模式我们可以提供出 8 种写法,有很多时候我们存在饿汉式单例的概念,以及懒汉式单例的概念。

饿汉式单例的含义是:在获取单例对象之前就已经创建完成了

饿汉式特点:无论在程序中是否会用到该实例对象,都会提前创建出来,可能造成内存浪费。(饿汉式不涉及线程安全问题,在类初始化时直接创建实例对象)

懒汉式的单例是指:在真正需要单例的时候才创建出该对象

懒汉式特点:在程序中,有时候可能需要推迟一些高开销对象的初始化操作,并且只有在使用这些对象的时候才初始化,此时,就需要采用延迟初始化策略。

3、单例模式的八种方式:(加粗代表推荐使用)

  1. 饿汉式(静态常量)

  2. 饿汉式(静态代码块)

  3. 懒汉式(线程不安全)

  4. 懒汉式(线程安全,同步方法)

  5. 懒汉式(线程安全,同步代码块)

  6. 双重检查

  7. 静态内部类(懒汉式,适用于多线程)

  8. 枚举(饿汉式)

三、八种方式实现

1、饿汉式(静态常量)

步骤:

1)构造器私有化(防止 new)

2)定义一个静态常量保存一个唯一的实例对象(单例)

3)向外暴露一个静态的公共方法。

代码实现:

public class Singleton1 {

    //1.构造器私有化,外部不能 new
    private Singleton1() {
    }

    //2.本类内部创建对象实例
    private static final Singleton1 INSTANCE = new Singleton1();

    //3.提供一个公有的静态方法,返回实例对象
    public static Singleton1 getInstance() {
        return INSTANCE;
    }

}

优缺点说明:

(1)优点:写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

(2)缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例对象,则会造成内存的浪费。

(3)这种方式基于 classLoader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类加载,这时初始化 instance 就没有达到 lazy loading 的效果。

(4)总结:这种单例模式可用可能造成内存浪费。

2、饿汉式(静态代码块)

步骤:

1)构造器私有化(防止 new)

2)定义一个静态变量保存一个唯一的实例对象(单例),通过静态代码块初始化单例对象。

3)向外暴露一个静态的公共方法。

代码实现:

public class Singleton2 {

    //1.构造器私有化,外部不能  new
    private Singleton2() {}

    //2.本类内部创建对象实例
    private static final Singleton2 INSTANCE;

    //3.在静态代码块中,创建单例对象
    static {
        INSTANCE = new Singleton2();
    }

    //4.提供一个公有的静态方法,返回实例对象
    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}

优缺点说明

(1)这种方式和静态常量的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码块,初始化类的实例。优缺点和上面一样。

(2)这种方式可以在构造方法有参数的时候,通过静态代码块给变量进行赋值。

(3)结论:这种单例模式可用,但是可能造成内存浪费。

代码实现 2:

public class Singleton {

    private static final Singleton INSTANCE;

    private String info;

    static {
        try {
            Properties pro = new Properties();
            //读取外部配置文件
            pro.load(Singleton.class.getClassLoader().getResourceAsStream("single.properties"));
            //动态获取某个值来给属性赋值
            INSTANCE = new Singleton(pro.getProperty("info"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private Singleton(String info) {
        this.info = info;
    }
}

3、懒汉式(线程不安全)

步骤:

1)构造器私有化

2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)

3)定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,如果有就直接返回;如果没有就创建一个新的单例对象。

代码实现:

public class Singleton3 {

    private static Singleton3 instance;

    private Singleton3() {
    }

    //提供一个静态的公有方法,当使用到该方法时,才去创建 实例对象
    public static Singleton3 getInstance() {
        if (instance == null) {
            //说明这是第一次获取单例对象,需要真正的创建出来
            instance = new Singleton3();
        }
        return instance;
    }

}

优缺点说明:

(1)起到了 Lazy Loading 的效果,但是只能在单线程下使用

(2)如果在多线程下,一个线程进入了 if (instance == null) 判断语句块,还未来的及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

(3)结论:在实际开发中,不要使用这种方式

4、懒汉式(线程安全,同步方法)

步骤:

1)构造器私有化

2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)

3)定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,如果有就直接返回;如果没有就创建一个新的单例对象。

4)为获取单例的方法加锁: 使用 synchronized 关键字

代码实现:

public class Singleton4 {

    private static Singleton4 instance;

    private Singleton4() {

    }

    //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
    public static synchronized Singleton4 getInstance() {
        if (instance == null) {
            //说明这是第一次获取单例对象,需要真正的创建出来
            instance = new Singleton4();
        }
        return instance;
    }
}

优缺点说明:

(1)解决了线程安全问题

(2)效率太低了,每个线程在想获得类的时候,执行 getInstance() 方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类的实例,直接 return 就行了。方法进行同步后效率太低。

使用 synchronized 关键字修改方法包装线程安全,但性能差太多,并发下只能有一个线程正在进入获取单例对象。

(3)结论:在实际开发中,不推荐使用这种方式

5、懒汉式(线程安全,同步代码块)

步骤:

1)构造器私有化

2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)

3)定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,如果有就直接返回;如果没有就创建一个新的单例对象。

4)为获取单例的方法内部的代码加锁: 使用 synchronized 关键字

代码实现:

public class Singleton5 {

    private static Singleton5 instance;

    private Singleton5() {

    }

    //性能得到了优化,但是依然不能保证第一次获取对象的线程安全
    public static Singleton5 getInstance() {
        //判断单例对象的变量是否为 null
        if (instance == null) {
            //很多线程执行到这里来:A,B
            //如果是第一次执行,instance为空,A,B 两个线程都会创建对象,并不安全
            synchronized (Singleton5.class) {
                instance = new Singleton5();
            }
        }
        return instance;
    }

}

优缺点:

(1)对比与上面的方式,性能得到了优化

(2)但本质上还是线程不安全的,多个线程都执行了 if 语句,遇到 synchronized 关键字,会阻塞在这里,所以并不能保证第一次获取对象的线程安全。

(3)结论:在实际开发中,不推荐使用

6、懒汉式 DCL(volatile 双重检查模式 Double Chekc Lock)

步骤:

1)构造器私有化

2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)

3)提供一个方法进行双重检查机制返回单例对象

4)必须使用 volatile 修饰静态的变量

代码实现:

public class Singleton6 {
    // 静态属性,volatile保证可见性和禁止指令重排序
    private volatile static Singleton6 instance = null;

    private Singleton6() {

    }

    //提供一个静态的公有方法,加入双重检查代码,解决线程安全问题,同时解决懒加载问题,同时保证了效率
    public static Singleton6 getInstance() {
        //判断单例对象的变量是否为 null
        //第一次检查
        if (instance == null) {
            //同步锁定代码块
            synchronized (Singleton6.class) {
                //第二重检查锁定
                if (instance == null) {
                    //注意:非原子操作
                    instance = new Singleton6();
                }
            }
        }

        return instance;
    }

}

优缺点:

(1)Double-Check 概念是多线程开发中常使用到的,在代码中,进行了两次 if(instance == null) 检查,这样就可以保证线程安全了

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

(3)优点:线程安全,延迟加载,效率较高

(4)结论:在实际开发中,推荐使用这种单例设计模式

分析:为什么要使用 volatile 保证安全?

(1) 禁止指令重排序

对象实际上创建要进行如下几个步骤:

a. 分配内存空间;

b. 调用构造器,初始化实例;

c. 返回地址给引用;

所以,new Singleton() 是一个非原子操作,编译器可能会重排序【构造函数可能在整个对象初始化完成前执行完毕,即赋值操作(只是在内存中开辟一片存储区域后直接返回内存的应用)在初始化对象前完成】。而 线程 C 在线程 A 赋值完时判断 instance 就不为 null,此时 C 拿到的将是一个没有初始化完成的半成品。这样是很危险的。因为极有可能线程 C 会继续拿着个没有初始化的对象的数据进行操作。此时容易触发“NPE 异常 NullPointException”。

图解:

(2) 保证可见性

a. 由于可见性问题,线程 A 在自己的工作线程内创建了实例,但此时还未同步到主存中;此时线程 C 在主存中判断 instance 还是 null,那么线程 C 又将在自己的工作线程中创建一个实例,这样就创建了多个实例。

b. 如果加上了 volatile 修饰 instance 之后,保证了可见性,一旦线程 A 返回了实例,线程 C 可以立即发现 Instance 不为 null。

7、静态内部类

步骤:

1)构造器私有化

2)定义一个静态内部类,在静态内部类中定义一个实例对象的常量

3)提供一个方法进行获取内部类的常量对象。

代码实现:

public class Singleton7 {

    private Singleton7() {

    }

    //写一个静态内部类,该中有一个静态属性 Singleton
    private static class SingletonInnerClass {
        private static final Singleton7 INSTANCE = new Singleton7();
    }

    //提供一个静态的公有方法,直接返回 静态内部类的成员变量
    public static Singleton7 getInstance() {
        return SingletonInnerClass.INSTANCE;
    }

}

优缺点:

(1)这种方式采用了类装载的机制来保证初始化实例只有一个线程。

(2)静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance() 方法,才会装载内部类,从而完成 Singleton 的实例化。

(3)类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

(4)优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高

(5)结论:推荐使用

JVM 在类初始化阶段(即在 Class 被加载后,且线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM 会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以通过静态内部类的方式实现延迟初始化方案。

小结

  1. 静态内部类是在被调用时才会被加载,这种方案实现了懒汉单例的一种思想,需要用到的时候才去创建单例,加上 JVM 的特性,这种方式又实现了线程安全的创建单例对象

  2. 通过对比基于 volatile 的双重检查锁定方案和基于类初始化方案的对比,我们会发现基于类初始化的方案的实现代码更简洁。但是基于 volatile 的双重检查锁定方案有一个额外的优势:除了可以对静态字段实现延迟加载初始化外,还可以对实例字段实现延迟初始化

8、枚举

代码实现:

public enum Singleton8 {

    INSTANCE;  //属性

    private Singleton8() {

    }
}

优缺点分析:

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

(2)这种方式是 Effective Java 作者 Josh Bloch 提倡的方式;

(3)结论:推荐使用。

三、单例模式在 JDK 应用的源码分析

1、JDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)

2. 代码分析+代码说明

四、单例模式注意事项和细节说明

1、单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。

2、当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new

3、单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或消耗资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如:数据源、Session 工厂等。)

标签:13,01,instance,对象,静态,实例,线程,单例
From: https://www.cnblogs.com/niujifei/p/16859967.html

相关文章

  • 2022-2023-1 20201324《信息安全系统设计与实现(上)》第12章
    1块设备I/O缓冲区文件系统使用一系列I/O缓冲区作为块设备的缓存内存。当进程试图读取(dev,blk)标识的磁盘块时,它首先在缓冲区缓存中搜索分配给磁盘块的缓冲区。如果该缓......
  • [APIO2010] 特别行动队
    Statement传送门Solution先考虑最暴力的\(dp\),也就是\(f_i=\max_{j=0}^if_j+a(s_i-s_j)^2+b(s_i-s_j)+c\),其中\(s_i\)表示\(x_i\)的前缀和.那么此时我们可以把式子拆......
  • miui13更新无法显示运行中程序/运行app处理方案
     一、背景手机更新到13.0.09或是更高版本后,系统内置应用也会升级更新。我的手机更新到13.0.14上后,手机管家(安全中心)也升级到了6.3.30版本,使用后发现无法显示......
  • [LeetCode] 2131. Longest Palindrome by Concatenating Two Letter Words
    Youaregivenanarrayofstrings words.Eachelementof words consistsof two lowercaseEnglishletters.Createthe longestpossiblepalindrome bysele......
  • 001.Vim文本编辑器
    1.关于Vim的介绍  2.关于Vim的三种模式  3.Vim常用快捷键    4.例子4.1  将80端口调整为8900端口(不是文本中的8080端口) ......
  • MS17_010漏洞利用
    用到的试验机kaliip192.168.1.106windows2008  ip  192.168.1.107用kali扫描靶机端口   这里看到445端口是开放的,可以利用17_010使用命令msfconsole启......
  • 013.Mybatis数据插入操作
    1.Mybatis数据写操作操作  2.步骤2.1在good.xml中添加SQL语句<insertid="insert"parameterType="com.imooc.mybatis.entity.Goods">INSERTINTOt......
  • 2022-2023-1 20221302《计算机基础与程序设计》第十周学习总结
    作业信息这个作业属于那个班级 https://edu.cnblogs.com/campus/besti/2022-2023-1-CFAP作业要求  https://www.cnblogs.com/rocedu/p/9577842.html#WEEK10作业目标......
  • day09-Tomcat01
    Tomcat011.WEB开发介绍WEB,在英文中WEB表示网/网络资源,它用于表示WEB服务器(主机)供浏览器访问的资源WEB服务器(主机)上供外界访问的Web资源为:静态web资源(如html页面):指web......
  • P8618 [蓝桥杯 2014 国 B] Log 大侠
    简要题意给你一个长度为\(n\)的正整数序列\(a\),有\(m\)个询问,每一个询问给出一个区间\([l,r]\)。定义函数\(f(x)=\lfloor\log_{2}(x)+1\rfloor\)。将\([l,r]\)的......