UML类图熟悉后,设计模式算是正式开篇了,下面我想先说下为啥要写设计模式专栏。
其实,对于一个已从业三年、还在持续打怪的同学,最普遍的建议是,学习重点应该放在代码重构、架构设计等方面。我认可这种说法,因为确实有一定的经验需要进阶了嘛。但同时,我也觉得不那么绝对,尤其最近在回顾到一些设计模式的书时,想做总结的想法愈发强烈。
所以我梳理了此次开设专栏的目的:
一、前几年自己忙于业务开发,代码基础和写作能力都是马马虎虎,想借此机会做些梳理、总结和提升;
二、最近重读设计模式,发现其实大部分设计模式在生产环境中使用的很少,所以很多人是看完就忘。我想,或许是我没有使用过,或许有些设计模式确实没有实战作用,总之,挖一下常见设计模式的应用场景,帮助自己、也帮助学习设计模式的同学做下总结。
网上讲解设计模式的文章其实很多,我只想从自己的观点出发去和大家聊一聊。
单例模式是什么
单例模式(Singleton Pattern),属于创建型模式的一种。使用单例模式可以确保在一个进程中,某个类只有一个实例,并提供一个全局访问点。
常见的UML类图如下:(工具:Mac版draw.io,UML图如有疑问,传送门:初探设计模式(一)10分钟速成UML类图)
为什么要有单例模式
单例模式的优点是什么?
保证唯一
:有些类在业务概念上就是唯一的(比如唯一ID生成器、配置信息、连接池),适合设计为单例类;提升性能
:对象的创建需要消耗资源,有些对象创建时消耗资源较多(比如访问IO、数据库),当需要频繁创建、销毁时,为了提升性能,使用单例就比较合适
单例模式有缺点吗?
最大的缺点在于扩展性
,单例模式非面向接口编程,违背了依赖倒置原则。举个例子,如果针对不同的场景(比如生成订单号、退款单号)需要采用不同的ID生成器,就不太好支持了。
怎么实现单例模式
总结
实现方式 | 是否延迟加载 | 是否线程安全 | 使用情况 |
---|---|---|---|
饿汉式 | 否 | 是 | 最简单,常用 |
懒汉式 | 是 | 否 | 线程不安全,几乎不用 |
synchronized懒汉式 | 是 | 是 | 并发度太低,几乎不用 |
双重校验锁DCL | 是 | 是 | 常用,注意对变量加volatile |
静态内部类 | 是 | 是 | 常用 |
枚举 | 是 | 是 | 不常用,但《Effective Java》推荐使用 |
标准代码怎么写
手写一个单例,这是面试官最常问的一个问题。那么,我们来看下几种常见的实现方式。 注意几点:
- 私有构造器,避免外部通过new创建实例
- 考虑加载方式:预加载/懒加载(延迟加载)
- 考虑创建对象时的线程安全问题
- 考虑获取对象时的并发性能
饿汉式:不管用不用,先初始化完成(预加载)
顾名思义,不管三七二十一,上来先创建好。即类加载的时候就把静态实例创建、初始化完成
。
特点:线程安全,但对资源有浪费(即使没有用,也做了初始化)
public class Singleton {
/**
* 【类加载】时就把静态实例初始化完成
*/
private static final Singleton INSTANCE = new Singleton();
/**
* 私有的构造方法
*/
private Singleton() {
}
/**
* 公开的全局访问点
*/
public static Singleton getInstance() {
return INSTANCE;
}
}
懒汉式:用的时候再做初始化(懒加载)
特点:懒加载,解决了饿汉式资源浪费的问题;但每次获取实例时都要获取同步锁(线程安全的懒汉式),会导致性能下降,不支持高并发(并发度实际上只有1,相当于串行操作)。
线程不安全的懒汉式
这种写法,多线程并发调用getInstance()获取单例时,可能会创建多个实例:
- 线程A执行到对象初始化instance=new Singleton();还没获得实例
- 线程B执行到if时,就会判断为true,也进入对象初始化代码
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优化:线程安全的懒汉式
对公开的全局访问点方法加synchnorized锁即可。
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重校验锁(DCL,Double Checked Locking)
特点:针对懒汉式加锁的性能问题做了优化,只有对象为空时,才去获取同步锁。
未使用volatile的双重校验锁
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // Single Checked
synchronized(Singleton.class) { // 类级别锁
if (instance == null) { // Double Checked
instance = new Singleton();
}
}
}
return instance;
}
}
待修改,单写一篇博客整理下。 传送门 The "Double-Checked Locking is Broken" Declaration
由于JVM的指令重排序优化,这种方式可能导致出错。因为instance = new Singleton();不是原子操作:
- 给instance分配内存;(instance=null)
- 调用构造函数,初始化成员变量(instance=null)
- 将instance实例指向分配的内存空间(instance!=null) 我们预期的执行顺序是1-2-3,实际执行可能被优化为1-3-2,当按照1-3-2顺序执行时,如果执行完3,另一个线程针对Single Check的校验结果就会返回true,但实际上intance还未被初始化,执行出错。
下图是Singleton instance = new Singleton();这行代码对应的Java字节码(Java8):
- new: 创建一个对象,并将其引用值压入栈顶;
- dup: 复制栈顶一个字长的数据,将复制后的数据压栈;
- invokespecial: 调用构造器进行对象的初始化,这一步会消耗掉操作数栈顶的引用(上一步dup出来的数据)作为传给构造器的“this”参数
- astore: 把操作数栈顶的引用消耗掉,保存到指定的局部变量Singleton instance去
优化:使用volatile的双重校验锁
给instance变量增加volatile关键字修饰。volatile可禁止指令重排序优化。
public class Singleton {
// volatile关键字修饰,禁止指令重排序优化
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // Single Checked
synchronized(Singleton.class) {
if (instance == null) { // Double Checked
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类
个人理解,这是静态内部类+饿汉式,只要不使用内部类,JVM就不会去加载这个单例类。
特点:懒加载,线程安全。
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举单例
public enum Singleton {
// 类似 public static final Singleton INSTANCE;
INSTANCE;
}
生产环境怎么用
使用建议
- 简单,不需要节约资源:饿汉式
- 需要考虑资源(延迟加载):
- 双重校验锁单例
- 静态内部类单例
- 枚举单例
使用场景
- ID生成器
- 数据/资源管理器等
- Spring默认采用单例模式来管理Bean对象