关于设计模式:一文彻底搞懂单例模式SingletonPattern

106次阅读

共计 3267 个字符,预计需要花费 9 分钟才能阅读完成。

文章已收录我的仓库:Java 学习笔记与收费书籍分享

设计动机

正如其名,单例模式保障一个类只有一个实例,那么为什么须要设计单例模式?

对一些类来说,只有一个实例是很重要的,例如一台电脑只应该由一个文件系统,生产厂商不应该为一台电脑配置两个文件系统;一个利用应该有一个专属的日志对象,而不应该一会儿写到这里一会儿写到那里;一个程序中往往只有一个线程池,由一个线程池治理线程,而不应该应用多个线程池,那样会使得线程乱套并且难以保护;在形象工厂中,具体的工厂类也只应该存在一个 ……

诸如此类的要求,咱们都须要保障一个类只有一个实例,此时便能够应用单例模式。

设计

咱们必须要避免用户实例化多个对象,解决办法是让类本人保留它的惟一实例,并将构造函数对外暗藏,并裸露特定静态方法返回惟一实例,这样用户将无奈本人实例化多个对象而只能通过类对外裸露的静态方法获取惟一实例。

代码示例

假如需要如下:一个应用程序的全局状态中须要同一个日志对象。

一、懒汉模式

懒汉模式并不间接初始化实例,而是等到实例被应用时才初始化它,防止不必要的资源节约。

// 日志文件类
class LogFile {
    // 惟一实例
    private static LogFile logFile = null;

    // 构造方法对外暗藏
    private LogFile(){};
    
    // 对外裸露的办法
    public static LogFile getInstance() {
        // 懒汉模式
        if (logFile == null) {logFile = new LogFile();
        }
        return logFile;
    }
}

public class Test {public static void main(String[] args) {var s1 = LogFile.getInstance();
        var s2 = LogFile.getInstance();
        System.out.println(s1 == s2); // 输入 true,产生的是同一个对象
    }
}

这里的代码在单线程中运行良好,logFile 属于临界区资源,因而这样的写法是线程不平安的,一开始实例为 null,线程 A 执行完 if 判断句后在执行 logFile = new LogFile() 前被调度到线程 B,此时线程 B 看到的实例也为空,因为 A 还没有初始化,所以线程 B 初始化实例,当回到线程 A 时,线程 A 将继续执行结构 logFile 的语句,此时 logFile 曾经被初始化两次了,它们 A 与 B 拿到的曾经不是同一个实例了。

一个简略的解决办法是为它们上锁:

public static LogFile getInstance() {synchronized (LogFile.class) {
        // 懒汉模式
        if (logFile == null) {logFile = new LogFile();
        }
    }
    return logFile;
}

然而这样上锁效率太低了,还不如采纳饿汉式,因为即时当 logFile 不为空时,多个线程也必须排队获取实例,而事实上并不需要排队,当 logFile 不为空时,多个线程该当能够同时获取 logFile 实例,因为它们仅仅只是读取实例而并不会更改实例,共享读是线程平安的。

一个更好的解决办法是采纳双重查看锁定:

public static LogFile getInstance() {if (logFile == null) {synchronized (LogFile.class) {
            // 懒汉模式
            if (logFile == null) {logFile = new LogFile();
            }
        }
    }
    return logFile;
}

通过在外层加一重判断,咱们解决了上述所说的问题,当初代码的效率曾经够高了——仅仅在最开始的阶段才会波及到加锁。

留神,下面的代码依然是线程不平安的,如果要想线程平安,咱们必须为 logFile 实例的申明加上 volatile 关键字,即:

private volatile static LogFile logFile;

要想了解这一点,咱们必须要了解 new 一个对象的过程,大抵的过程如下:

  1. 申请内存空间,对空间内字段采纳默认初始化(此时对象为 null)。
  2. 调用类的构造方法,进行初始化(此时对象为 null)。
  3. 返回地址(执行实现后对象不为 null)。

如果不加 volatile 关键字,Java 虚拟机可能在保障可串行化的前提下产生指令重排,即虚拟机可能先执行第 3 步再执行第 2 步(比拟常见的),初始化对象时虚拟机思考的仅仅只是单线程的状况,此时的指令重排并不会影响到单线程的运行,因而为了加快速度,指令重排这种状况是可能呈现的。

如果从多线程角度来看,如果产生了指令重排,线程 A 在 new 对象时执行第一部后先执行了第三步,此时对象曾经不为 null 了,然而对象还没被结构好,尽管这个时候线程 A 还持有锁,但这对线程 B 毫无影响——线程 B 闯入发现对象不为 null 而间接拿走一个还未结构齐全的对象实例——基本不会通过第一层判断而申请锁。

加上 volatile 关键字能够保障可见性并且禁止指令重排。

但从解决问题的角度来看,咱们还有更好的解决办法——动态外部类:

// 日志文件类
class LogFile {
    // 实例交给动态外部类保存
    private static class LazyHolder {private static LogFile logFile = new LogFile();
    }

    // 构造方法对外暗藏
    private LogFile(){};

    // 对外裸露的办法
    public static LogFile getInstance() {return LazyHolder.logFile;}
}

public class Test {public static void main(String[] args) {var s1 = LogFile.getInstance();
        var s2 = LogFile.getInstance();
        System.out.println(s1 == s2); // 输入 true,产生的是同一个对象
    }
}

动态外部类的成果是最好的,动态外部类只有在其成员变量或办法被援用时才会加载,也就是说只有当咱们第一次拜访类的时候实例才会被初始化实现,咱们将实例委托给动态外部类帮忙初始化,虚拟机对动态外部类的加载是线程平安的,咱们防止本人采纳上锁机制而委托给虚拟机,这样的效率是十分高的。

懒汉式能够防止无用垃圾对象的产生——只有在它们应用时才初始化它,但咱们也必须为此多编写一些代码来保障它的安全性,如果某个类并不是很罕用的话,应用懒汉式能够肯定水平的节约资源。

二、饿汉式

饿汉式模式在加载时便初始化单例,使得用户获取时实例曾经被初始化。

// 日志文件类
class LogFile {
    // 惟一实例
    private static LogFile logFile = new LogFile();

    // 构造方法对外暗藏
    private LogFile(){};

    // 对外裸露的办法
    public static LogFile getInstance() {return logFile;}
}

public class Test {public static void main(String[] args) {var s1 = LogFile.getInstance();
        var s2 = LogFile.getInstance();
        System.out.println(s1 == s2); // 输入 true,产生的是同一个对象
    }
}

饿汉式是线程平安的,因为 logFile 曾经被初始实现,因而饿汉式比懒汉式效率更高,但与此同时,如果实例在全局都没用上的话,饿汉式模式将会产生垃圾从而耗费资源。

优缺点总结

次要长处:

  1. 提供了对惟一实例的受控拜访。
  2. 零碎中内存只存在一个对象,节约零碎的的资源。
  3. 单例模式能够容许可变的数目的实例。

次要毛病:

  1. 可扩展性比拟差。
  2. 单例类,职责过重,在肯定水平上违反了 ” 繁多职责准则 ”。
  3. 滥用单例将带来一些负面的问题,如为了节俭资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而呈现的连接池溢出(大家都用一个池子,可能池子吃不消),如果实例化对象长时间不必零碎就会被认为垃圾对象被回收,这将导致对象状态失落。

正文完
 0