从零开始学习Java设计模式 | 创建型模式篇:单例设计模式
Posted 李阿昀
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 创建型模式篇:单例设计模式相关的知识,希望对你有一定的参考价值。
从本讲开始,咱们就要开始正式学习23种设计模式了。当然,我们得按照顺序来学,首先先来学习23种设计模式里面的第一类模式,即创建型模式。
创建型模式的主要关注点是"怎样创建对象?",它的主要特点是"将对象的创建与使用分离"。也就是说,如果我们作为一个使用者的话,那么我们可以直接通过某种方式去获取别人创建好的对象,至于别人是如何创建的,我们并不关注。这样,就可以降低系统的耦合度了,使用者并不需要关注对象的创建细节,尤其是那些创建特别麻烦的对象。于是,创建型模式的好处就这样体现出来了。
此外,我们还应知道,创建型模式分为以下几种:
- 单例模式
- 工厂方法模式
- 抽象工厂模式
- 原型模式
- 建造者模式
注意,在本讲中,我们先来学习第一种创建型模式,即单例模式。
概述
单例模式(Singleton Pattern)是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例设计模式的概念讲完之后,接下来我们再来看一下单例设计模式的结构。
结构
单例设计模式主要有两种角色,它们分别是:
- 单例类:该类只能创建一个对象
- 访问类:该类其实就是测试类,即使用单例类
实现
在介绍单例设计模式的实现之前,我们先来看一下单例设计模式的分类,单例设计模式可分为如下两类:
- 饿汉式:类加载就会导致该单实例对象被创建。也就是说类加载的时候,该类的对象就创建好了
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
所以,我们如何去区分饿汉式和懒汉式呢?只需要去判断该单实例对象是否是在类加载的时候创建即可,若是,则是饿汉式,若不是,则是懒汉式。
饿汉式
在本节,我们先来看一下饿汉式是如何来实现的。大家要注意了,饿汉式又可分为多种实现方式,在这里,我只介绍两种,它们分别是:
- 静态成员变量方式
- 静态代码块方式
下面,我们先来看静态成员变量这种实现方式。
实现方式一:静态成员变量方式
打开咱们的maven工程,并在com.meimeixia包下再创建一个子包,即pattern,在该包下存放的都是我们编写的有关设计模式的代码,然后在pattern包下再创建一个子包,即singleton,这表明编写的有关单例设计模式的代码我们都会放在该包下。
由于单例设计模式有多种实现方式,要想讲清楚每一种实现方式,我们还得在com.meimeixia.pattern.singleton包下分别创建多个子包来存放每一种实现方式所对应的代码,例如com.meimeixia.pattern.singleton.demo1包里面存放的就是饿汉式第一种实现方式(即静态成员变量方式)所对应的代码。
以上工作都做好之后,下面咱们就得来正式编写代码实现饿汉式的第一种实现方式(即静态成员变量方式)了。
首先,在com.meimeixia.pattern.singleton.demo1包下创建一个单例类,不妨我们就命名为Singleton。
那如何创建该类呢?只要大家跟着我的思路一步一步来,相信大家肯定能创建出来。
- 私有构造方法:为什么要私有构造方法呢?因为私有了构造方法之后,外界就访问不到这个构造方法了,访问不到的话外界就无法去创建对象了
- 在本类中创建一个本类对象供外界去使用
- 提供一个公共的访问方式,让外界获取该对象
经过上述三步,相信你很快就能创建出Singleton类了。
package com.meimeixia.pattern.singleton.demo1;
/**
* 饿汉式:静态成员变量
* @author liayun
* @create 2021-05-28 18:07
*/
public class Singleton {
// 1. 私有构造方法
private Singleton() {}
// 2. 在本类中创建本类对象
private static Singleton instance = new Singleton();
// 3. 提供一个公共的访问方式,让外界获取该对象
public static Singleton getInstance() {
return instance;
}
}
注意,以上该类中对外提供的公共访问方法,除了使用public修饰之外,还使用了static修饰,这是为什么呢?因为外界无法创建Singleton类的对象,既然不能创建对象的话,那么就无法去调用其非静态方法了,所以这里面我们对外要提供的是静态方法。而且,由于静态的不能直接访问非静态的,所以instance成员变量还得使用static来修饰。
然后,创建一个测试类,这里我们不妨就命名为Client。
package com.meimeixia.pattern.singleton.demo1;
/**
* @author liayun
* @create 2021-05-28 18:16
*/
public class Client {
public static void main(String[] args) {
// 创建Singleton类的对象
Singleton instance = Singleton.getInstance();
Singleton instance1 = Singleton.getInstance();
// 判断获取到的两个是否是同一个对象
System.out.println(instance == instance1);
}
}
可以看到,两次获取到Singleton类的对象之后,我们还得再来判断一下获取到的两个对象是不是同一个对象。
此时,我们运行一下以上测试类,如下图所示,可以看到打印结果是true,也就是说两次获取到Singleton类的对象是同一个对象,这样,我们就已经保证Singleton这个类只能创建一个对象了。
实现方式二:静态代码块方式
饿汉式的第一种实现方式我们已经实现了,接下来,我们再来看一下饿汉式的第二种实现方式,即静态代码块方式。
首先,在com.meimeixia.pattern.singleton包下再创建一个子包,即demo2,这表明编写的饿汉式的第二种实现方式的代码我们都会放在该包下。
然后,在com.meimeixia.pattern.singleton.demo2包下创建一个单例类,该类我们同样命名为Singleton。
同理,只要大家跟着我的思路一步一步来,相信你一定能创建出该类。
- 私有构造方法:私有构造方法其实就是为了让外界不能创建该类的对象
- 在成员位置声明一个该类的成员变量,不过不要给其赋值
- 在静态代码块中进行赋值
- 对外提供一个公共的访问方式,让外界获取该对象
经过上述四步,相信你很快就能创建出Singleton类了。
package com.meimeixia.pattern.singleton.demo2;
/**
* 饿汉式:静态代码块
* @author liayun
* @create 2021-05-28 18:29
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 声明Singleton类型的变量
private static Singleton instance;
// 在静态代码块中进行赋值
static {
instance = new Singleton();
}
// 对外提供获取该类对象的方法
public static Singleton getInstance() {
return instance;
}
}
你会发现,这和饿汉式的第一种实现方式唯一的区别是:对于饿汉式的第一种实现方式来说,在单例类里面是在成员位置声明了一个该类的成员变量,声明的同时并直接为其进行了赋值;而对于饿汉式的第二种实现方式而言,在单例类里面是先在成员位置声明了一个该类的成员变量,然后再在静态代码块里面对其进行赋值。除此之外,这两种实现方式就差不多了,其他就没有什么区别了。
紧接着,创建一个测试类,同理我们不妨命名为Client。
package com.meimeixia.pattern.singleton.demo2;
/**
* @author liayun
* @create 2021-05-28 23:47
*/
public class Client {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
Singleton instance1 = Singleton.getInstance();
// 判断两次获取到的Singleton对象是否是同一个对象
System.out.println(instance == instance1);
}
}
最后,我们运行一下以上测试类,若打印结果是true,则说明我们确实已经做到了单例;若打印结果是false,则说明我们写的代码是有问题的。如下图所示,可以看到打印结果是true,也就是说两次获取到Singleton类的对象是同一个对象,当然了,多次获取到的肯定也是同一个对象,这不用说。
至此,我就将饿汉式的两种实现方式讲解完了。那么我们不妨再来总结一下饿汉式有什么特点?不管是饿汉式的第一种实现方式还是第二种实现方式,它们都属于是在类加载的时候就已经创建了该类的对象。
大家不妨设想一下这种情况,如果我们只是对单例类进行了一个类加载的操作,并没有去获取该类的对象,那么这个对象是不是已经存在于内存中了啊!当然了,前提是使用饿汉式的方式来实现单例。此时,如果我们一直不用它的话,那么你会发现它是一直存在于内存中的,这势必就会造成内存的浪费。也就是说,饿汉式的另外一个特点是会造成内存的浪费。
由于饿汉式会浪费内存,所以我们还得去学习一下懒汉式,下面我就会讲到。
懒汉式
饿汉式的两种实现方式讲完之后,接下来我们来看一下懒汉式。当然,懒汉式它也有多种实现方式,下面我会一一介绍给大家。
接下来,我们先来看懒汉式的第一种实现方式,即线程不安全的方式。
实现方式一:线程不安全
首先,在com.meimeixia.pattern.singleton包下再创建一个子包,即demo3,这表明编写的懒汉式的第一种实现方式的代码我们都会放在该包下。
然后,在com.meimeixia.pattern.singleton.demo3包下创建一个单例类,该类我们同样命名为Singleton。
同理,只要大家跟着我的思路一步一步来,相信你一定能创建出该类。
- 私有构造方法
- 在成员位置声明一个该类的成员变量,不过不要给其赋值
- 对外提供一个公共的访问方式
经过以上三个步骤,可能有些同学写出来的Singleton类是下面这样的。
package com.meimeixia.pattern.singleton.demo3;
/**
* 懒汉式
* @author liayun
* @create 2021-05-29 17:57
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 声明Singleton类型的变量instance
private static Singleton instance; // 只是声明了一个该类型的变量,并没有对其进行赋值
// 对外提供访问方式
public static synchronized Singleton getInstance() {
instance = new Singleton();
return instance;
}
}
写出来之后,他还很得意,他说懒汉式的概念不就是在首次使用单实例对象的时候才创建的嘛,首次使用无非就是调用以上getInstance方法呗,所以我在该方法里面给其赋值并返回出去,不是合情合理的吗?是,懒汉式的概念理解得是没错,但他没意识到一个问题,就是外界每调用一次getInstance方法,都会发现又重新new了一个对象,这样,多次获取到的肯定就不是同一个对象了。
你要是不信的话,不妨编写一个测试类来测试一下。
package com.meimeixia.pattern.singleton.demo3;
/**
* @author liayun
* @create 2021-05-29 18:03
*/
public class Client {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
Singleton instance1 = Singleton.getInstance();
// 判断两次获取到的Singleton对象是否是同一个对象
System.out.println(instance == instance1);
}
}
运行以上测试类之后,如下图所示,你会发现打印结果是false,为什么是false呢?上面我已经分析过了,这里不再赘述。
而我们现在想要的是以上Singleton类只能创建一个对象,所以我们得在Singleton类的getInstance方法里面加上一个如下if判断。
package com.meimeixia.pattern.singleton.demo3;
/**
* 懒汉式
* @author liayun
* @create 2021-05-29 17:57
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 声明Singleton类型的变量instance
private static Singleton instance; // 只是声明了一个该类型的变量,并没有对其进行赋值
// 对外提供访问方式
public static Singleton getInstance() {
// 判断instance是否为null,如果为null,那么说明还没有创建Singleton类的对象
// 如果没有创建的话,那么我们就创建一个并返回;如果有创建,那么直接返回即可
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
代码改进之后,不妨再来运行一下测试类,如下图所示,发现打印结果是true,这说明咱们多次获取到的是同一个对象。
你觉得以上Singleton类写的有没有什么问题啊?肯定是存在问题的,如果是多线程环境下,那么就会出现线程安全问题。
为什么这么说呢?假设现在是多线程环境,两个线程同时调用getInstance方法,线程1拿到cpu的执行权,它在调用getInstance方法时,首先肯定是要做一个判断的,做完判断之后,它会进入到判断里面。此时,如果线程2拿到了cpu的执行权,那么线程1就会处于等待状态,同样,线程2在调用getInstance方法时也是要进行判断的,你觉得线程2还能进入到判断里面吗?
肯定能够啊!因为此时if判断条件是成立的,即instance是等于null的,这一切都是由于线程1还处于等待状态,还未执行下面的代码而导致的。也就是说,只要线程2获取到了cpu的执行权,那么它也会进入到判断里面。这样,以上Singleton类创建的就不是单个对象了,而是多个。
实现方式二:线程安全
既然以上实现方式在多线程环境下会出现线程安全问题,那么我们就得改进代码了,如何进行改进呢?很简单,只须在以上Singleton类的getInstance方法上加上一个同步关键字(即synchronized)即可。
package com.meimeixia.pattern.singleton.demo3;
/**
* 懒汉式
* @author liayun
* @create 2021-05-29 17:57
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 声明Singleton类型的变量instance
private static Singleton instance; // 只是声明了一个该类型的变量,并没有对其进行赋值
// 对外提供访问方式
public static synchronized Singleton getInstance() {
// 判断instance是否为null,如果为null,那么说明还没有创建Singleton类的对象
// 如果没有创建的话,那么我们就创建一个并返回;如果有创建,那么直接返回即可
if (instance == null) {
// 线程1等待,线程2获取到cpu的执行权,也会进入到该判断里面
instance = new Singleton();
}
return instance;
}
}
由于涉及到多线程,如果我们暂时不创建多线程的话,那么是演示不出来效果的,而且即使创建出来多线程,也不一定能演示出来效果,所以,我们只好做如下分析了。
假设线程1现在拿到了cpu的执行权,它在调用getInstance方法时,首先肯定是要做一个判断的,做完判断之后,它会进入到判断里面。此时,如果线程2拿到了cpu的执行权,那么线程1就会处于等待状态,这样,当线程2调用getInstance方法时,线程2现在还能进入到判断里面吗?很显然,进不来,因为这里面有一个同步锁,而线程1还在判断里面等待,它并没有释放这个同步锁,所以线程2是进不来的,它只能在外面等着。那么这样的话,线程2就只能等线程1执行完,而线程1执行完,就意味着instance变量已经被赋好值了,如此一来,就解决掉了线程不安全的问题了。
因此,以上两种实现方式要进行选择的话,我们肯定首选线程安全的这种实现方式。
实现方式三:双重检查锁
接下来,我们来看一下懒汉式的第三种实现方式,即双重检查锁方式。在讲解双重检查锁方式之前,我们先讨论一下上面懒汉式线程安全的那种实现方式所存在的问题,也即在getInstance方法上加了锁之后所导致的性能问题。
先看下面这句话:
对于getInstance方法来说,绝大部分的操作都是读操作,而读操作本身就是线程安全的,所以我们没必要让每个线程必须持有锁才能调用该方法,故我们需要调整加锁的时机。
看完上面这句话之后,有些同学可能不甚理解什么是读操作。没关系,我为大家解释一下,大家不妨看一下以上懒汉式的第二种实现方式所对应的代码,可以看到,第一次调用getInstance方法的时候,会先判断instance是否为null,此时肯定是为null的,所以就会创建一个Singleton类的对象,并将其赋给instance,这是一个赋值的操作,我们又可以称为写操作。
赋完值之后,接着就要把instance的值进行一个返回了,即将当前类的对象返回。后续每一次调用getInstance方法时,由于if判断语句中的条件不再成立(这是因为instance已经被赋予值了),所以都会直接将instance的值进行返回。于是,后续每一次调用getInstance方法,将instance直接返回的这个操作,我们就可以把它称为读操作了。
明确了读操作之后,我们继续看以上那句话的后半段描述,说我们没必要让每个线程必须持有锁才能调用getInstance方法,为什么呢?因为这会导致程序运行的性能更加低下。所以,我们才要调整加锁的时机,由此也产生了一种新的实现方式——双重检查锁方式。
既然是双重检查,那么肯定是两次判断了。接下来,我们就来看一下具体的代码实现。
首先,在com.meimeixia.pattern.singleton包下再创建一个子包,即demo4,这表明编写的懒汉式的第三种实现方式的代码我们都会放在该包下。
然后,在com.meimeixia.pattern.singleton.demo4包下创建一个单例类,该类我们同样命名为Singleton。
同理,只要大家跟着我的思路一步一步来,相信你一定能创建出该类。
- 私有构造方法
- 在成员位置声明一个该类的成员变量,不过不要给其赋值
- 对外提供一个公共的访问方式,在该方法里面记得要做两次判断
经过以上三个步骤,相信大家能写出来下面这样的Singleton类。
package com.meimeixia.pattern.singleton.demo4;
/**
* 双重检查锁方式
* @author liayun
* @create 2021-05-29 18:48
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 声明Singleton类型的变量
private static /*volatile*/ Singleton instance;
// 对外提供公共的访问方式
public static Singleton getInstance() {
// 第一次判断,如果instance的值不为null,那么就不需要抢占锁了,直接返回对象即可
if (instance == null) {
synchronized (Singleton.class) {
// 第二次判断
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
可以看到,对于getInstance方法来说,我们是做了两次判断的。
- 第一次判断:判断instance是否等于null,若是,则需抢占锁;若不是,则直接返回instance即可,由于并没有去持有锁,所以效率就可以得到提升了
- 第二次判断:如果第一次判断时,instance等于null,那么进来判断里面之后我们得先持一把锁,
以上是关于从零开始学习Java设计模式 | 创建型模式篇:单例设计模式的主要内容,如果未能解决你的问题,请参考以下文章
从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式
从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式