兔小白:大家好,我是兔小白,今年刚刚毕业,还是一名初级软件工程师。虽然我的编程经验有限,但痴迷于技术,一钻研起技术来可以说废寝忘食。最近我正在跟熊小猫学习设计模式。原本令我觉得枯燥、难懂的设计模式,在他的讲解下变得生动有趣,我不但学得快,记得也牢。
熊小猫:大家好,我是兔小白的技术经理熊小猫,已经工作5年多,编程经验比较丰富。我平时喜欢钻研技术,每当学习了新技术或者有了心得体会,都会和同事们分享、交流。指导兔小白,帮助初级程序员成长,也是我的工作职责。最近我在给兔小白讲解设计模式,如果你也感兴趣,欢迎加入进来,一起学习!
异常忙碌的项目经理
兔小白:我参与的第一个项目终于要上线了!最近项目经理忙得团团转,这两周一直在加班。
熊小猫:项目快要上线时事情比较多,开发工作在收尾,客户又提了一些需求变更,经过评估也要做,进度面临很大压力;上线前还要做安全扫描,解决安全问题;UAT的问题也需要和客户沟通。最要紧的是项目二期已经开始准备了……项目经理最近确实忙得不可开交。没事,你狮哥他挺得住!要和客户沟通。最要紧的是项目二期已经开始准备了……项目经理最近确实忙得不可开交。没事,你狮哥他挺得住!
兔小白:公司为什么不能再安排一位项目经理呢?
熊小猫:一个项目一般只有一位项目经理。项目经理需要掌握项目的所有上下文,统筹大局。咱们的项目运行到收尾阶段,项目上下文非常复杂,即使再派一位项目经理,也只能做一些辅助工作,作用有限,搞不好还会出乱子。项目总会有张有弛,关键时刻只能项目经理扛一下。
兔小白:还真是这样,一个项目中可以有很多产品经理、程序员、测试人员,但是项目经理通常只有一个。
熊小猫:程序中的对象也有同样的情况,有时全局只能存在一个实例。例如,程序中负责协调工作的对象,需要了解程序运行的上下文和状态,统一安排资源。如果出现两个协调者,就会产生冲突。
兔小白:如果两个项目经理之间没有协调好,一个让我开发新需求,另一个让我改Bug,我可就抓狂了!
熊小猫:程序里也有类似的场景。例如,线程池管理器要全局唯一,才能控制好线程数量,若两个线程池管理器各管各的,线程数量就要翻倍了。
兔小白:协调工作确实只能由一个实例来承担。程序如何做到一个类在全局只生成一个实例呢?
熊小猫:单例模式就是用来解决这个问题的。单例模式的目的很明确,确保一个类在全局中只存在一个实例。程序中任何实例化该类的地方,获取的都是全局唯一的实例。单例模式的实现有一些技巧,咱们打开电脑,边写边讲。
懒汉式实现单例模式
熊小猫:你觉得要保证一个类只有一个实例,首先应该做什么?
兔小白:应该先把构造对象的“大门”关起来,不能随意通过new关键字构造对象。
熊小猫:没错,单例类要禁止使用new关键字实例化对象,也就是说,构造方法不能被暴露。单例类需要提供其他获取单例对象的方法。咱们看看程序如何实现。以项目经理为例,用单例模式实现项目经理类。
public class ProjectManager {
private static ProjectManager instance;
private ProjectManager() {
}
public static ProjectManager getInstance() {
if(instance==null){
instance = new ProjectManager();
}
return instance;
}
}
首先,使用private修饰构造方法,对外不再提供构造方法。getInstance方法是客户端访问ProjectManager实例的唯一方法,这是一个静态方法,可以通过类直接访问。为了确保ProjectManager实例唯一,该方法首先判断是否已经创建了实例,如果已经创建,则直接返回该实例,否则先创建实例再返回。
熊小猫:我们再来看看客户端代码,代码中声明了两个项目经理对象,这两个对象其实指向了同一个实例。代码中对这两个对象的等值判断可以证明这一点。
ProjectManager zhangsan = ProjectManager.getInstance();
ProjectManager lisi = ProjectManager.getInstance();
if (zhangsan == lisi) {
System.out.println("两位项目经理对象指向同一实例");
}
兔小白:程序中任何需要使用ProjectManager对象的地方,只能通过getInstance方法获取实例。getInstance方法是确保单例的关键。
熊小猫:确实如此,这版代码在单线程场景下可以确保单例,但在多线程场景下存在漏洞。问题出在判断instance是否为null上,当多个线程同时执行到这一语句时,都会得到true的结果。每个线程都将继续向下执行,分别创建实例,从而创建出多个实例。
兔小白:原来问题出在这里!早就听说多线程编程容易出问题。
熊小猫:解决多线程并发的问题也很简单,加上synchronized代码块,就能确保同一时间只有一个线程执行创建单例实例的代码。此外,还需要使用volatile修饰instance变量。volatile关键字用来解决多线程开发中的指令重排和变量可见性问题。这两个问题会导致某个线程在初始化instance实例还未完成时,其他线程就可以获取instance实例并返回。咱们看代码如何实现。
public class ProjectManager {
private static volatile ProjectManager instance;
private ProjectManager() {
}
public static ProjectManager getInstance() {
if (instance == null) {
synchronized (ProjectManager.class) {
if (instance == null) {
instance = new ProjectManager();
}
}
}
return instance;
}
}
熊小猫:由于synchronized代码块是串行执行的,存在性能问题,因此在进入synchronized代码块之前先判断一次instance是否为null,降低synchronized代码块被执行的可能性。毕竟只有在第一次执行getInstance方法时,instance才可能为null。一旦instance创建完成,synchronized代码块不会被再次执行。
兔小白:第一次判断instance是否为null,我听明白了。但是为什么在synchronized代码块中又判断了一次instance是否为null呢?
熊小猫:当两个线程并发执行时,都会执行第一次instance是否为null的判断。当执行到synchronized代码块时,其中一个线程先获取锁继续执行,创建instance实例;另一个线程等待锁被释放后,获取锁继续执行synchronized代码块。如果不再判断一次instance是否为null,将再次创建新的实例。下图详细展示了我所描述的导致错误的过程。
兔小白:没想到确保只有一个实例这么难!
熊小猫:因为要考虑多线程并发的问题嘛!以后你做多线程开发时可一定要小心。程序在第一次调用getInstance方法时,才会创建单例实例,所以这种单例模式的实现方式叫作懒汉式。与之相对应的还有一种叫作饿汉式的单例模式实现方式。
饿汉式实现单例模式
熊小猫:所谓“饿汉式”,就是不管单例对象是否被用到,程序都会提前创建好单例实例。如果不担心内存占用,可以考虑采用饿汉式实现单例模式,代码更简洁,当然,也是线程安全的。我们来看看代码实现。
public class ProjectManager {
private static ProjectManager instance = new ProjectManager();
private ProjectManager() {
}
public static ProjectManager getInstance() {
return instance;
}
}
熊小猫:类加载时会初始化静态成员变量,因此只要类加载完,就会完成instance变量的初始化。创建单例实例发生在类加载的过程中,因此不存在线程安全问题。当客户端调用getInstance方法时,可以直接获取已经创建好的实例。
兔小白:这种实现方式简洁多了!懒汉式和饿汉式实现方式,你推荐哪一种呢?
熊小猫:这两种实现方式都很常用,但各自有其适合的场景。懒汉式实现方式在客户端第一次调用getInstance方法时创建实例,避免了内存浪费。一般情况下,推荐使用线程安全的懒汉式实现方式。饿汉式实现方式的代码简洁、线程安全,但不管是否使用单例对象,程序都会提前在类加载时创建单例实例,占用内存。如果对内存的使用没有严格限制,那么推荐使用饿汉式实现方式。
单例模式的适用场景
熊小猫:单例模式的结构非常简单,下面是单例模式结构图。
熊小猫:Singleton类需要私有化构造方法,getInstance方法负责创建和返回唯一实例。getInstance方法的两种实现方式刚才已经讲过,在工作中可以直接拿来使用。
单例模式的优点如下。
(1)单例类在内存中只存在一个实例,减少了内存的使用。
(2)单例类的实例化严格受控。客户端只能通过单例类提供的getInstance方法访问单例实例。统一出口,便于管理。
(3)线程间共享单例实例,适合中心化工作。
兔小白:针对最后一个优点,如果使用不当会出现线程安全的问题!
熊小猫:没错,在多线程的场景下使用单例模式一定要小心。
单例模式的缺点如下。
(1)构造单例实例的逻辑在单例类内部实现,和单一职责原则存在冲突。
(2)单例实例的属性被线程共享,处理不当会引发线程安全问题。例如,对于游戏人物的血量属性,程序必须保证对血量的处理是线程安全的,否则当人物同时受到不同来源的攻击时,血量计算会出错。
单例模式的优、缺点都很鲜明。单例模式在与之匹配的场景下才能发挥优势。如果使用不当,反而会出现线程安全问题。
下面是适合使用单例模式的场景。
(1)中心化工作场景。例如,负责全局协调、对象管理、资源调度的类,需要保证它的实例全局唯一,否则对资源的管理和使用难以做到同步。
(2)没有成员变量的类适合使用单例模式。若类没有任何成员变量,则创建多个实例没有意义,且单例实例也不存在线程安全问题。这个场景比较常见的是工具类,使用静态类来实现也能达到同样的效果。
熊小猫:使用单例模式时,特别需要注意一点,虽然单例模式能够节省内存开销,但这只是它的附加价值,不应该为了节省内存而刻意使用单例模式,否则会带来更麻烦的线程安全问题。在使用单例模式时,需要注意多线程对单例对象内部属性的访问,做好线程同步。
兔小白:想不到单例模式这么复杂,里面居然有这么多学问。
熊小猫:由于牵扯到多线程,问题的复杂度会升高一个等级。多线程编程的学问可不少呢!等学完设计模式,咱们再深入研究多线程编程!
在实际项目中,设计模式容易被不合时宜地运用,也就是我们常说的过度设计。在本书中,针对“过度设计”这个难题,作者通过类似下面的插图来提醒读者。
书中各种设计模式的类图,全部采用手绘的方式,一笔一笔地画出来,在确保可读性的同时,增加一些趣味性,让读者更容易接受。
本书采用“兔小白”和“熊小猫”对话的方式,展开讲解。在排版阶段,编辑建议将对话的人物名字改为人物头像,根据场景,呈现不同的表情。这是一个很好的主意,作者动手绘制了“表情包”!虽然增加了大量工作,但是让对话变得生动起来。
最后说说书中的点睛之笔—— “设计模式之城” 折页。
既然设计模式就存在于日常生活中,那么可以用一张城市图,画出与设计模式相对应的生活场景。这样不但方便读者记忆设计模式,而且读者可以使用此图快速找到与问题相匹配的设计模式。