# 单例模式 > 为什么要使用单例? 单例存在哪些问题? 单例与静态类的区别? 有何替代的解决方案? ## 为什么要使用单例? 创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。其中单例模式、工厂模式、建造者模式、原型模式都是创建型模式。 单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。 ### 处理资源访问冲突 我们先来看第一个例子。在这个例子中,我们自定义实现了一个往文件中打印日志的 Logger 类。具体的代码实现如下所示: ``` public class Logger { private FileWriter writer; public Logger() { File file = new File("/Users/meiying/log.txt"); writer = new FileWriter(file, true); //true表示追加写入 } public void log(String message) { writer.write(mesasge); } } // Logger类的应用示例: public class UserController { private Logger logger = new Logger(); public void login(String username, String password) { // ...省略业务逻辑代码... logger.log(username + " logined!"); } } public class OrderController { private Logger logger = new Logger(); public void create(OrderVo order) { // ...省略业务逻辑代码... logger.log("Created an order: " + order.toString()); } } ``` 在上面的代码中,我们注意到,所有的日志都写入到同一个文件 /Users/meiying/log.txt 中。在 UserController 和 OrderController 中,我们分别创 建两个 Logger 对象。在 Web 容器的 Servlet 多线程环境下,如果两个 Servlet 线程同时分别执行 login() 和 create() 两个函数,并且同时写日志到 log.txt 文件中,那就有可能存在日志信息互相覆盖的情况 那如何来解决这个问题呢?我们最先想到的就是通过加锁的方式:给 log() 函数加互斥锁(Java 中可以通过 synchronized 的关键字),同一时刻只允许一个线程调用执行 log()函数。具体的代码实现如下所示: ``` public class Logger { private FileWriter writer; public Logger() { File file = new File("/Users/wangzheng/log.txt"); writer = new FileWriter(file, true); //true表示追加写入 } public void log(String message) { synchronized(this) { writer.write(mesasge); } } } ``` 可以解决问题吗?不会。因为这种锁是一个对象级别的锁,一个对象在不同的线程下同时调用 log() 函数,会被强制要求顺序执行。但是,不同的对象之间并不共享同一把锁。在不同的线程下,通过不同的对象调用执行 log() 函数,锁并不会起作用,仍然有可能存在写入日志互相覆盖的问题 实际上,要想解决这个问题也不难,我们只需要把对象级别的锁,换成类级别的锁就可以了。让所有的对象都共享同一把锁。这样就避免了不同对象之间同时调用 log() 函数,而导致的日志覆盖问题。具体的代码实现如下所 ``` public void log(String message) { synchronized(Logger.class) { // 类级别的锁 writer.write(mesasge); } } ``` 除了使用类级别锁之外,实际上,解决资源竞争问题的办法还有很多,分布式锁是最常听到的一种解决方案。不过,实现一个安全可靠、无 bug、高性能的分布式锁,并不是件容易的事情。除此之外,并发队列(比如 Java 中的 BlockingQueue)也可以解决这个问题:多个线程同时往并发队列里写日志,一个单独的线程负责将并发队列中的数据,写入到日志文件。这种方式实现起来也稍微有点复杂。 相对于这两种解决方案,单例模式的解决思路就简单一些了。单例模式相对于之前类级别锁的好处是,不用创建那么多 Logger 对象,一方面节省内存空间,另一方面节省系统文件句柄(对于操作系统来说,文件句柄也是一种资源,不能随便浪费)。 我们将 Logger 设计成一个单例类,程序中只允许创建一个 Logger 对象,所有的线程共享使用的这一个 Logger 对象,共享一个 FileWriter 对象,而 FileWriter 本身是对象级别线程安全的,也就避免了多线程情况下写日志会互相覆盖的问题。按照这个设计思路,我们实现了 Logger 单例类。具体代码如下所示: ``` public class Logger { private FileWriter writer; private static final Logger instance = new Logger(); private Logger() { File file = new File("/Users/wangzheng/log.txt"); writer = new FileWriter(file, true); //true表示追加写入 } public static Logger getInstance() { return instance; } public void log(String message) { writer.write(mesasge); } } // Logger类的应用示例: public class UserController { public void login(String username, String password) { // ...省略业务逻辑代码... Logger.getInstance().log(username + " logined!"); } } public class OrderController { private Logger logger = new Logger(); public void create(OrderVo order) { // ...省略业务逻辑代码... Logger.getInstance().log("Created a order: " + order.toString()); } } ``` ### 表示全局唯一类 如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以 对象的形式存在,也理所应当只有一份。 再比如,唯一递增 ID 号码生成器。如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例 ## 如何实现一个单例 ### 饿汉式 饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(在真正用到 IdGenerator 的时候,再创建实例),从名字中我们也可以看出这一点。具体的代码实现如下所示: ``` public class IdGenerator { private AtomicLong id = new AtomicLong(0); private static final IdGenerator instance = new IdGenerator(); private IdGenerator() {} public static IdGenerator getInstance() { return instance; } public long getId() { return id.incrementAndGet(); } } ``` 有人觉得这种实现方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化 如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。 如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如Java 中的 PermGen Space OOM),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性 ### 懒汉式 懒汉式相对于饿汉式的优势是支持延迟加载。具体的代码实现如下所示: ``` public class IdGenerator { private AtomicLong id = new AtomicLong(0); private static IdGenerator instance; private IdGenerator() {} public static synchronized IdGenerator getInstance() { if (instance == null) { instance = new IdGenerator(); } return instance; } public long getId() { return id.incrementAndGet(); } } ``` 不过懒汉式的缺点也很明显,我们给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。量化一下的话,并发度是 1,也就相当于串行操作了。而这个函数是在单例使用期间,一直会被调用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了 ### 双重检测 饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。 在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题。具体的代码实现如下所示: ``` public class IdGenerator { private AtomicLong id = new AtomicLong(0); private static IdGenerator instance; private IdGenerator() {} public static IdGenerator getInstance() { if (instance == null) { synchronized(IdGenerator.class) { // 此处为类级别的锁 if (instance == null) { instance = new IdGenerator(); } } } return instance; } public long getId() { return id.incrementAndGet(); } } ``` 网上有人说,这种实现方式有些问题。因为指令重排序,可能会导致 IdGenerator 对象被new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。 要解决这个问题,我们需要给 instance 成员变量加上 volatile 关键字,禁止指令重排序才行。实际上,只有很低版本的 Java 才会有这个问题。我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序 ### 静态内部类 我们再来看一种比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。具体是怎么做到的呢?我们先来看它的代码实现。 ``` public class IdGenerator { private AtomicLong id = new AtomicLong(0); private IdGenerator() {} private static class SingletonHolder { private static final IdGenerator instance = new IdGenerator(); } public static IdGenerator getInstance() { return SingletonHolder.instance; } public long getId() { return id.incrementAndGet(); } } ``` SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载 ## 如何理解单例模式的唯一性 “一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。定义中提到,“一个类只允许创建唯一一个对象”。那对象的唯一性的作用范围是什么呢?是指线程内只允许创建一个对象,还是指进程内只允许创建一个对象?答案是后者,也就是说,单例模式创建的对象是进程唯一的 ## iOS 侧单例的实现 - GCD `dispatch_once` 保证执行1次 - 实现 `+ (id)allocWithZone:(struct _NSZOne *)zone { } ` 方法。避免单例对象,调用 copy 方法,产生一个新的对象,打破单例效果 实现 `- (id)copyWithZone:(nullable NSZone *)zone { }` 方法,避免单例对象,调用 copy 方法,产生一个新的对象,打破单例效果 ```objective-c + (id)sharedInstance { static Mooc *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[super allocWithZone:NULL] init]; }); return instance; } + (id)allocWithZone:(struct _NSZone *)zone { return [self sharedInstance]; } - (id)copyWithZone:(nullable NSZone *)zone { return self; } ```