Design Pattern
1、概述
2、创建型模式
2.1 单例模式
2.2 工厂模式
3、结构型模式
3.1 代理模式
3.2 装饰器模式
4、行为型模式
4.1 策略模式
-
+
tourist
register
Sign in
单例模式
## 1 含义 1. 单例模式(Singleton Design Pattern)是指**保证一个类只能有一个实例**,并且**提供一个全局访问点**。 2. 单例模式的实现需要**三个必要条件**: 1. 单例类的**构造函数必须是私有的**,这样才能**将类的创建权控制在内部**,从而**使得类的外部不能创建类的实例**。 2. 单例类**通过一个私有的静态变量来存储其唯一实例**。 3. 单例类**通过提供一个公开的静态方法**,**使得外部使用者可以访问类的唯一实例**。 > 因为**单例类的构造函数是私有的**,所以**单例类不能被继承**。 > 3. 实现单例类时,需要考虑三个问题: 1. 创建单例对象时,是否**线程安全**。 2. 单例对象的创建,是否**延时加载**。 3. 获取单例对象时,是否需要**锁**(锁会导致低性能)。 ## 2 优缺点 ### 2.1 优点 1. 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例,这样就可以**防止其他对象对自己的实例化**,**确保所有的对象都访问一个实例**,**避免对共享资源的多重占用**。 2. 单例模式具有一定的**伸缩性**,**类自己来控制实例化进程**,因此在改变实例化进程上有相应的伸缩性。 3. 由于**在系统内存中只存在一个对象**,因此可以**节约系统资源**,当**需要频繁创建和销毁对象时**,单例模式无疑可以**提高系统的性能**。 ### 2.2 缺点 1. **不适用于变化的对象**,如果同一类型的对象总是要**在不同的用例场景发生变化**,单例就**会引起数据的错误**,**不能保存彼此的状态**。 2. 由于单例模式**没有抽象层**,因此单例类的**扩展有很大的困难**。 3. **滥用单例会带来一些负面影响**,例如: 1. 为了节省资源**将数据库连接池对象设计为单例类**,可能会导致**共享连接池对象的程序过多而出现连接池溢出**。 2. 如果**实例化的对象长时间不被利用**,**系统会认为是垃圾而被回收**,这将**导致对象状态的丢失**。 ## 3 使用场景 在下面几个场景中适合使用单例模式: 1. 有**频繁实例化然后销毁**的情况,也就是频繁的`new` 对象,可以考虑单例模式。 2. **创建对象耗时过多或者耗资源过多**,但又**经常用到的对象**。 3. **频繁访问 IO 资源的对象**,例如**数据库连接池**或**访问本地文件**。 下面举几个例子来说明一下: ### 3.1 网站在线人数统计 1. 这个其实就是**全局计数器**,也就是说**所有用户在相同的时刻获取到的在线人数数量都是一致的**,要实现这个需求,**计数器就要全局唯一**,也就正好可以用单例模式来实现。 2. 当然这里**不包括分布式场景**,因为**计数是存在内存中的**,并且还**要保证线程安全**。 3. 下面代码是一个简单的计数器实现: ```java public class Counter { private static class CounterHolder{ private static final Counter counter = new Counter(); } private Counter(){ System.out.println("init..."); } public static final Counter getInstance(){ return CounterHolder.counter; } private AtomicLong online = new AtomicLong(); public long getOnline(){ return online.get(); } public long add(){ return online.incrementAndGet(); } } ``` ### 3.2 配置文件访问类 1. 项目中经常需要一些**环境相关的配置文件**,比如短信通知相关的、邮件相关的。 2. 这里就以读取一个 `properties` 文件配置为例: 1. 如果我们使用的是`Spring`,可以用`@PropertySource` 注解实现,默认就是单例模式。 2. 如果不用单例的话,每次都要`new` 对象,然后重新读一遍配置文件,很影响性能,如果用单例模式,则只需要读取一遍就好了。 3. 以下是文件访问单例类简单实现: ```java public class SingleProperty { private static Properties prop; private static class SinglePropertyHolder{ private static final SingleProperty singleProperty = new SingleProperty(); } /** * config.properties 内容是 test.name=kite */ private SingleProperty(){ System.out.println("构造函数执行"); prop = new Properties(); InputStream stream = SingleProperty.class.getClassLoader() .getResourceAsStream("config.properties"); try { prop.load(new InputStreamReader(stream, "utf-8")); } catch (IOException e) { e.printStackTrace(); } } public static SingleProperty getInstance(){ return SinglePropertyHolder.singleProperty; } public String getName(){ return prop.get("test.name").toString(); } public static void main(String[] args){ SingleProperty singleProperty = SingleProperty.getInstance(); System.out.println(singleProperty.getName()); } } ``` ### 3.3 数据库连接池 1. **数据库连接池**的实现,也包括线程池,之所以做池化的原因,是因为**新建连接很耗时**,如果**每次新任务来了都新建连接**,那么**对性能的影响就会很大**,所以一般的做法是**在一个应用内维护一个连接池**,这样当任务进来时,如果有空闲连接,可以直接拿来用,**省去了初始化的开销**。 2. 所以用单例模式正好可以实现**一个应用内只有一个线程池的存在**,**所有需要连接的任务**,**都要从这个连接池来获取连接**,如果不使用单例,那么应用内就会出现多个连接池,那也就没什么意义了。 3. 如果我们使用`Spring` 的话,并集成了`Druid` 或`C3P0`,这些**成熟开源的数据库连接池**,一般也都是**默认以单例模式实现**的。 ## 4 实现方法 ### 4.1 饿汉式 1. **饿汉式的单例实现比较简单**,在**类加载的时候**,**静态示例 `instance` 就已创建并初始化好了**。 2. 具体代码如下: ```java public class Singleton { private static final Singleton instance = new Singleton(); private Singleton () {} public static Singleton getInstance() { return instance; } } ``` 3. 优点: 1. 单例对象的创建时**线程安全**的,因为`instance`**作为类成员变量的实例化发生在类 `Singleton` 类加载的初始化阶段**,**初始化阶段是执行类构造器 `<clinit>()` 方法的过程**: 1. `<clinit>()` 方法是**由编译器自动收集类中的所有静态**(`static`)**类变量的赋值动作和静态语句块**(`static{}`)**中的语句合并产生的**,因此`private static final Singleton instance = new Singleton();` 也会被放入到这个方法中。 2. **虚拟机会保证一个类的 `<clinit>()` 方法在多线程环境中被正确的加锁**、**同步**,**如果多线程同时去初始化一个类**,那么**只会有一个线程去执行这个类的**`<clinit>()`**方法**,**其他线程都需要阻塞等待**,**直到活动线程执行**`<clinit>()`**方法完毕**。 3. 需要注意的是,其他线程虽然会被阻塞,但如果执行`<clinit>()` 方法的那条线程退出`<clinit>()` 方法后,**其他线程唤醒后不会再次进入**`<clinit>()`**方法**,因此**同一个类加载器下**,**一个静态类只会初始化一次**。 2. **获取单例对象时不需要加锁**。 4. 缺点: 1. 单例对象的创建**不是延时加载**,因为当**类加载完成后就会对 `instance` 进行初始化**,**然后被分配内存完成实例化**,如果我们自始至终都没有使用过这个实例对象,这就**会造成内存的浪费**。 ### 4.2 懒汉式 1. 懒汉式为了**支持延时加载**,**将对象的创建延迟到了获取对象的时候**,但为了线程安全,不得不**为获取对象的操作加锁**,这就**导致了低性能**。 2. 并且这把锁只有在第一次创建对象时有用,之后每次获取对象,这把锁都是一个累赘。 3. 具体代码如下: ```java public class Singleton { private static final Singleton instance; private Singleton () {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } ``` 4. 优点: 1. 对象的创建是**线程安全**的。 2. **支持延时加载**。 5. 缺点: 1. **获取对象的操作被加上了锁**,**影响了并发度**。 2. 如果单例对象需要频繁使用,那这个缺点就是无法接受的。 3. 如果单例对象不需要频繁使用,那这个缺点也无伤大雅。 ### 4.3 双重检测 1. 饿汉式和懒汉式的单例都有缺点,双重检测的实现方式解决了这两者的缺点。 2. 双重检测将懒汉式中的 `synchronized` 方法改成了 `synchronized` 代码块,具体代码如下: ```java public class Singleton { private static Singleton instance; private Singleton () {} public static Singleton getInstance() { if (instance == null) { // 第一重检测,判断对象是否为空,主要为了当对象已经创建时不需要加锁,直接返回对象即可 synchronized(Singleton.class) { // 注意这里是类级别的锁 if (instance == null) { // 第二重检测,判断对象是否为空,主要为了避免多线程并发时多次创建对象 instance = new Singleton(); } } } return instance; } } ``` 3. 这种实现方式在 Java 1.4 及更早的版本中有些问题,就是[指令重排序](https://notebook.ricear.com/project-34/doc-527),具体如下: 1. 问题主要出现在创建对象的语句 `instance = new Singleton();` 上,因为在 Java 中创建一个对象并非是原子操作,该操作可以被分解成三行伪代码: ```java // 1:分配对象的内存空间 memory = allocate(); // 2:初始化对象 ctorInstance(memory); // 3:设置 instance 指向刚分配的内存地址 instance = memory; ``` 2. 上面三行伪代码中的 2 和 3 可能被重排序,重排序之后的伪代码是这样的: ```java // 1:分配对象的内存空间 memory = allocate(); // 3:设置 instance 指向刚分配的内存地址 instance = memory; // 2:初始化对象 ctorInstance(memory); ``` 3. 在单线程程序下,重排序不会对最终结果产生影响,但是在并发的情况下,可能会导致某些线程访问到未初始化的变量,例如: | 时间 | 线程 A | 线程 B | | ------- | --------------------------------------- | ----------------------------------------------------------------------------------- | | $t_1$ | $A_1$:分配对象内存空间 | | | $t_2$ | $A_3$:设置 $instance$ 指向内存空间 | | | $t_3$ | | $B_1$:判断 $instance$ 是否为空 | | $t_4$ | | $B_2$:由于 $instance$ 不为 $null$,线程 $B$ 将访问 $instance$ 引用的对象 | | $t_5$ | $A_2$:初始化对象 | | | $t_6$ | $A_4$:访问 $instance$ 引用的对象 | | 4. 按照这样的顺序执行,线程 $B$ 将会获得一个未初始化的对象,并且自始至终,线程 $B$ 无需获取锁。 5. 要解决这个问题,需要给 `instance` 成员变量加上 `volatile` 关键字,从而禁止指令重排序,具体可参考[2.13 Volatile 原理](https://notebook.ricear.com/project-34/doc-528),修改后的代码如下: ```java public class Singleton { private static volatile Singleton instance; private Singleton () {} public static Singleton getInstance() { if (instance == null) { // 第一重检测,判断对象是否为空,主要为了当对象已经创建时不需要加锁,直接返回对象即可 synchronized(Singleton.class) { // 注意这里是类级别的锁 if (instance == null) { // 第二重检测,判断对象是否为空,主要为了避免多线程并发时多次创建对象 instance = new Singleton(); } } } return instance; } } ``` 6. 优点: 1. 对象的创建是**线程安全**的。 2. **支持延时加载**。 3. **获取对象时不需要加锁**。 ### 4.4 静态内部类 1. 用静态内部类的方式实现单例类,利用了 **Java 静态内部类的特性**: 1. **Java 加载外部类的时候**,**不会创建内部类的实例**,**只有在外部类使用到内部类的时候**,**才会创建内部类实例**。 2. **对于一个类**,**JVM 在仅用一个类加载器加载他时**,**静态变量的赋值在全局只会执行一次**。 2. 具体代码如下: ```java public class Singleton { private Singleton () {} private static class SingletonInner { private static final Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonInner.instance; } } ``` 3. `SingletonInner` 是一个**静态内部类**,当**外部类 `Singleton` 被加载的时候**,并**不会创建 `SingletonInner` 实例对象**,**只有当调用 `getInstance()` 方法时**,`SingletonInner` **才会被加载**,这个时候才会**创建 `instance`**,**这样就做到了延时加载**。 4. `instance` **的唯一性**、**创建过程的线程安全性**,都**由 JVM 来保证**,具体可参考[3.4 类的生命周期和加载过程](https://notebook.ricear.com/project-34/doc-536)。 5. 优点: 1. 对象的创建时**线程安全**的。 2. **支持延时加载**。 3. **获取对象时不需要加锁**。 ### 4.5 枚举 1. **用枚举来实现单例**,是**最简单的方式**,这种实现方式**通过 Java 枚举类型本身的特性**,**保证了实例创建的线程安全性和实例的唯一性**: 1. 当我们使用 `enum` 来**定义一个枚举类型**的时候,编译器会**自动帮我们创建一个 `final` 类型的类继承 `Enum` 类**,所以**枚举类型不能被继承**,同时**这个类中的属性和方法都是 `static` 类型**的,因此**创建一个 `enum` 类型是线程安全的**,具体原因可参考[3.1 饿汉式]()的优点中关于单例对象创建时是线程安全的原因的叙述。 2. 定义的一个枚举类: ```java public enum T { SPRING,SUMMER,AUTUMN,WINTER; } ``` 3. 反编译后的部分信息: ```java public static final T SPRING; public static final T SUMMER; public static final T AUTUMN; public static final T WINTER; private static final T $VALUES[]; static { SPRING = new T("SPRING", 0); SUMMER = new T("SUMMER", 1); AUTUMN = new T("AUTUMN", 2); WINTER = new T("WINTER", 3); $VALUES = (new T[] { SPRING, SUMMER, AUTUMN, WINTER }); } ``` 2. 具体代码如下: ```java public enum Singleton { INSTANCE; // 该对象全局唯一 } ``` 3. 优点: 1. **写法简单**。 2. 对象的创建时**线程安全**的。 3. **获取对象时不需要加锁**。 4. 缺点: 1. **不支持延时加载**。 ## 5 总结 | 实现方法 | 线程安全 | 延时加载 | | ---------- | -------- | -------- | | 饿汉式 | 是 | 否 | | 懒汉式 | 是 | 是 | | 双重检测 | 是 | 是 | | 静态内部类 | 是 | 是 | | 枚举 | 是 | 否 | ## 参考文献 1. [设计模式面试题(总结最全面的面试题!!!)](https://juejin.cn/post/6844904125721772039)。 2. [单例模式的五种实现方式及优缺点](https://www.cnblogs.com/codeshell/p/14177102.html)。 3. [单例模式的使用场景和 Java 静态块的使用](https://zhuanlan.zhihu.com/p/37382515)。 4. [设计模式【1.3】-- 为什么饿汉式单例是线程安全的?](https://segmentfault.com/a/1190000038672035) 5. [【单例深思】饿汉式与类加载](https://blog.csdn.net/gavin_dyson/article/details/69668946)。 6. [Hungry Chinese Style of Singleton Design Pattern](https://www.programmersought.com/article/16796508047). 7. [单例陷阱——双重检查锁中的指令重排问题](https://juejin.cn/post/6844904062551343118)。 8. [单例模式-静态内部类实现及原理剖析](https://www.cnblogs.com/niuyourou/p/11892617.html)。 9. [K:枚举的线程安全性及其序列化问题](https://www.cnblogs.com/MyStringIsNotNull/p/8018243.html)。
ricear
July 10, 2021, 2:48 p.m.
©
BY-NC-ND(4.0)
转发文档
Collection documents
Last
Next
手机扫码
Copy link
手机扫一扫转发分享
Copy link
Markdown文件
share
link
type
password
Update password