设计模式1-Singleton

本文最后更新于:3 years ago

什么是设计模式

  设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

然后接下来的一段时间,我会介绍23种设计模式,掌握这23种设计模式,你的代码能力又能更上一层楼。
但是可能说的不是很详细,理解的不是很深,错误可能也会很多,如果有错误还请指出(不胜感激),还是小白。

单例模式

  单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于 创建型模式 ,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

  • 如果想了解单例模式的具体的介绍,菜鸟教程介绍得比较详细↓
    菜鸟教程-单例模式

    单例模式我会介绍8种,但总体上分为四种 饿汉式懒汉式静态内部类方式枚举单例

    结构图


    优缺点

    优点:

    1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
    2、避免对资源的多重占用(比如写文件操作)。

    缺点:

    没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

    使用场景

    1、要求生产唯一序列号。
    2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
    3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

    实现代码

    饿汉式(Eager)

    方法①

    原理:类加载到内存后,就实例化一个单例,JVM保证线程安全

    作用:简单实用,推荐使用

    唯一缺点:不管用到与否,类装载时就完成实例化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class Mgr01 {

    private static final Mgr01 INSTANCE = new Mgr01();

    private Mgr01(){}; //设置Mgr01为私有的

    public static Mgr01 getInstance(){ //作为获取单例的唯一入口
    return INSTANCE;
    }

    public void m(){
    System.out.println("m");
    }

    public static void main(String[] args) {
    Mgr01 mgr01 = Mgr01.getInstance();
    Mgr01 mgr02 = Mgr01.getInstance();
    System.out.println(mgr01 == mgr02); //用于测试是否实例是否唯一
    }

    }

    然后有人就想了,那我不是能够在static代码块中创建实例(那么在类初次被加载的时候,执行static块,并且只会执行一次)
    是的,所以就有了一下的代码。


    方法②

    缺点:同方法①

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class Mgr02 {

    private static final Mgr02 INSTANCE;

    static{
    INSTANCE = new Mgr02();
    }

    public static Mgr02 getInstance(){
    return INSTANCE;
    }

    public void m(){
    System.out.println("m");
    }

    public static void main(String[] args) {
    Mgr01 mgr01 = Mgr01.getInstance();
    Mgr01 mgr02 = Mgr01.getInstance();
    System.out.println(mgr01 == mgr02);
    }

    }

    懒汉式(Lazy Loading)

    方法③

    原理:默认不会实例化,什么时候用什么时候new

    作用:按需初始化

    缺点:多线程访问是会有影响的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class Mgr03 {

    private static Mgr03 INSTANCE;

    private Mgr03(){};

    public static Mgr03 getInstance(){
    if (INSTANCE == null){
    try {
    Thread.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    INSTANCE = new Mgr03();
    }
    return INSTANCE;
    }

    public void m(){
    System.out.println("m");
    }


    public static void main(String[] args) { //测试多线程
    for (int i = 0; i < 100; i++) {
    /* new Thread(new Runnable() {
    @Override
    public void run() {

    }
    }).start();*/

    //函数式接口
    new Thread(()->
    System.out.println(Mgr03.getInstance().hashCode())
    ).start();
    }
    }

    }

    这里存在的问题是:当存在第一个线程调用了getInstance()方法,在判断 INSTANCE 为空后,还未到实例初始化时,
    另一线程也调用了getInstance()方法,判断了 INSTANCE 为空,往下执行 创建了实例,接着第一个线程也创建了实例,
    此时 INSTANCE 在两个线程中已经不再是同一个实例了。

    接着接着,就有人想到,那就用锁来限制多线程。接着就有了下面的方法:


    方法④

    缺点:通过synchronized解决多线程问题,但也带来效率下降

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class Mgr04 {

    private static Mgr04 INSTANCE;

    private Mgr04(){};

    public static synchronized Mgr04 getInstance(){
    if (INSTANCE == null){
    try {
    Thread.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    INSTANCE = new Mgr04();
    }
    return INSTANCE;
    }

    public void m(){
    System.out.println("m");
    }

    public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
    //函数式接口
    new Thread(()->
    System.out.println(Mgr04.getInstance().hashCode())
    ).start();
    }
    }

    }

    接下有人就想,那我能不能通过减小同步代码块的方式提高效率,看下面的方法。


    方法⑤

    缺点:不可行,又导致了方法③中的问题了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class Mgr05 {

    private static Mgr05 INSTANCE;

    private Mgr05(){};

    /**
    *
    */

    public static Mgr05 getInstance(){
    if (INSTANCE == null){
    // 妄图通过减小同步代码块的方式提高效率,然后不可行
    synchronized (Mgr05.class){
    try {
    Thread.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    INSTANCE = new Mgr05();
    }
    }

    return INSTANCE;
    }

    public void m(){
    System.out.println("m");
    }

    public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
    new Thread(()->{
    System.out.println(Mgr05.getInstance().hashCode());
    }).start();
    }
    }

    }

    当存在第一个线程调用了getInstance()方法,当判断 INSTANCE 为空后,还未到锁,
    另一线程也调用了getInstance()方法,判断了 INSTANCE 为空,获得锁,执行剩下的部分,创建实例
    当释放锁后,接着第一个线程获得锁也创建了一个实例,
    此时 INSTANCE 在两个线程中已经不再是同一个实例了
    根本原因:if判断没有和下面的锁进行一体化操作

    又有聪明的人想到那我们是不是能够通过双重检查来解决这情况,答案是可以的,于是乎就有了下面这种情况:


    方法⑥

    缺点:没有缺点,就是完美。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class Mgr06 {

    private static volatile Mgr06 INSTANCE; //volatile 如果想了解,查一下资料

    private Mgr06(){};

    //最完美的方法
    public static Mgr06 getInstance(){
    if (INSTANCE == null){
    // 双重检查
    synchronized (Mgr06.class){
    if (INSTANCE == null){
    try {
    Thread.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    INSTANCE = new Mgr06();
    }

    }
    }

    return INSTANCE;
    }

    public void m(){
    System.out.println("m");
    }

    public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
    new Thread(()->{
    System.out.println(Mgr05.getInstance().hashCode());
    }).start();
    }
    }

    }

    双重检查是饿汉式和懒汉式中最完美的方法,但在我们开发中,是根据情况来判断的,u1s1,方法①不是很好吗?(你不用,你装载它干啥)


    静态内部类方式

    方法⑦

    原理:加载外部类时不会加载内部类,JVM保证单例

    缺点:没有缺点,就很完美。不需要加锁,由JVM帮我们来保证线程安全的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class Mgr07 {

    private Mgr07(){};

    private static class Mgr07Holder{
    private final static Mgr07 INSTANCE = new Mgr07();
    }

    public static Mgr07 getInstance(){
    return Mgr07Holder.INSTANCE;
    }

    public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
    new Thread(()->{
    System.out.println(Mgr07.getInstance().hashCode());
    }).start();
    }
    }
    }

    你以为上面两个就是最完美的单例吗?还是太天真了。
    Java创始人之一Joshua Bloch在Effective Java中推荐关于单例的最完美的方法⑧


    枚举单例

    方法⑧

    原理:枚举。。。

    作用:不仅可以解决线程同步,还可以防止反序列化

    缺点:完美中的完美。。。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public enum Mgr08 {

    INSTANCE;

    public void m(){};

    public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
    new Thread(()->{
    System.out.println(Mgr07.getInstance().hashCode());
    }).start();
    }
    }

    }

    关于反系列化:
    枚举类是没有构造方法的,所以即使拿到了class文件,也没有办法构造它的对象,返回的反序列化只是 INSTANCE


    总结

    单例模式设计的代码比较多,但其实统共也就那么几种原理,并不是用的都是完美的方法,我们在开发过程中,是根据需要来决定用哪一种的(Java开发中,Spring已经帮我们基本上都解决了单例模式设计),学会合理利用设计模式,会让你的代码更有水平!


    本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!