单例模式(Singleton)- 最易懂的设计模式解析

前言

今天我来全面总结一下Android开发中最常用的设计模式 - 单例模式。

关于设计模式的介绍,可以看下我之前写的:1分钟全面了解“设计模式”


目录

设计模式系统:单例模式.jpg


1. 引入

####1.1 解决的是什么问题
之前说过,设计模式 = 某类特定问题的解决方案,那么单例模式是解决什么问题的解决方案呢?

  • 含义:单例 =一个实例;
  • 解决的问题:降低对象之间的耦合度
  • 解决方法:单例模式,即实现一个类只有一个实例化对象,并提供一个全局访问点

1.2 实例引入

接下来我用一个实例来对单例模式进行引入

  • 背景:小成有一个塑料生产厂,但里面只有一个仓库。
  • 目的:想用代码来实现仓库的管理
  • 现有做法: 建立仓库类和工人类

    其中,仓库类里的quantity=商品数量;工人类里有搬运方法MoveIn(int i)和MoveOut(int i)。

  • 出现的问题:通过测试发现,每次工人搬运操作都会新建一个仓库,就是货物都不是放在同一仓库,这是怎么回事呢?(看下面代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package scut.designmodel.SingletonPattern;
//仓库类
class StoreHouse {
private int quantity = 100;
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public int getQuantity() {
return quantity;
}
}
//搬货工人类
class Carrier{
public StoreHouse mStoreHouse;
public Carrier(StoreHouse storeHouse){
mStoreHouse = storeHouse;
}
//搬货进仓库
public void MoveIn(int i){
mStoreHouse.setQuantity(mStoreHouse.getQuantity()+i);
}
//搬货出仓库
public void MoveOut(int i){
mStoreHouse.setQuantity(mStoreHouse.getQuantity()-i);
}
}
//工人搬运测试
public class SinglePattern {
public static void main(String[] args){
StoreHouse mStoreHouse1 = new StoreHouse();
StoreHouse mStoreHouse2 = new StoreHouse();
Carrier Carrier1 = new Carrier(mStoreHouse1);
Carrier Carrier2 = new Carrier(mStoreHouse2);
System.out.println("两个是不是同一个?");
if(mStoreHouse1.equals(mStoreHouse2)){//这里用equals而不是用 == 符号,因为 == 符号只是比较两个对象的地址
System.out.println("是同一个");
}else {
System.out.println("不是同一个");
}
//搬运工搬完货物之后出来汇报仓库商品数量
Carrier1.MoveIn(30);
System.out.println("仓库商品余量:"+Carrier1.mStoreHouse.getQuantity());
Carrier2.MoveOut(50);
System.out.println("仓库商品余量:"+Carrier2.mStoreHouse.getQuantity());
}
}

结果:

1
2
3
4
两个是不是同一个?
不是同一个
仓库商品余量:130
仓库商品余量:50

2. 单例模式介绍

2.1 解决的问题(应用场景)

  • 冲突:从上面的结果可以看出,工人类操作的明显不是同一个仓库实例。
  • 目标:全部工人操作的是同一个仓库实例
  • 单例模式就是为了解决这类问题的解决方案:实现一个类只有一个实例化对象,并提供一个全局访问点

2.2 工作原理

在Java中,我们通过使用对象(类实例化后)来操作这些类,类实例化是通过它的构造方法进行的,要是想实现一个类只有一个实例化对象,就要对类的构造方法下功夫:
单例模式的原理

单例模式的一般实现:(含使用步骤)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
//1. 创建私有变量 ourInstance(用以记录 Singleton 的唯一实例)
//2. 内部进行实例化
private static Singleton ourInstance = new Singleton();
//3. 把类的构造方法私有化,不让外部调用构造方法实例化
private Singleton() {
}
//4. 定义公有方法提供该类的全局唯一访问点
//5. 外部通过调用getInstance()方法来返回唯一的实例
public static Singleton newInstance() {
return ourInstance;
}
}

好了,单例模式的介绍和原理应该了解了吧?那么我们现在来解决上面小成出现的“仓库不是一个”的问题吧!

2.3 实例介绍

小成使用单例模式改善上面例子的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package scut.designmodel.SingletonPattern;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//单例仓库类
class StoreHouse {
//仓库商品数量
private int quantity = 100;
//自己在内部实例化
private static StoreHouse ourInstance = new StoreHouse();;
//让外部通过调用getInstance()方法来返回唯一的实例。
public static StoreHouse getInstance() {
return ourInstance;
}
//封闭构造函数
private StoreHouse() {
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public int getQuantity() {
return quantity;
}
}
//搬货工人类
class Carrier{
public StoreHouse mStoreHouse;
public Carrier(StoreHouse storeHouse){
mStoreHouse = storeHouse;
}
//搬货进仓库
public void MoveIn(int i){
mStoreHouse.setQuantity(mStoreHouse.getQuantity()+i);
}
//搬货出仓库
public void MoveOut(int i){
mStoreHouse.setQuantity(mStoreHouse.getQuantity()-i);
}
}
//工人搬运测试
public class SinglePattern {
public static void main(String[] args){
StoreHouse mStoreHouse1 = StoreHouse.getInstance();
StoreHouse mStoreHouse2 = StoreHouse.getInstance();
Carrier Carrier1 = new Carrier(mStoreHouse1);
Carrier Carrier2 = new Carrier(mStoreHouse2);
System.out.println("两个是不是同一个?");
if(mStoreHouse1.equals(mStoreHouse2)){
System.out.println("是同一个");
}else {
System.out.println("不是同一个");
}
//搬运工搬完货物之后出来汇报仓库商品数量
Carrier1.MoveIn(30);
System.out.println("仓库商品余量:"+Carrier1.mStoreHouse.getQuantity());
Carrier2.MoveOut(50);
System.out.println("仓库商品余量:"+Carrier2.mStoreHouse.getQuantity());
}
}

结果:

1
2
3
4
两个是不是同一个?
是同一个
仓库商品余量:130
仓库商品余量:80

从结果分析,使用了单例模式后,仓库类就只有一个仓库实例了,再也不用担心搬运工人进错仓库了!!!

2.4 优点

  • 提供了对唯一实例的受控访问;
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能;
  • 可以根据实际情况需要,在单例模式的基础上扩展做出双例模式,多例模式;

2.5 缺点

  1. 单例类的职责过重,里面的代码可能会过于复杂,在一定程度上违背了“单一职责原则”。
  2. 如果实例化的对象长时间不被利用,会被系统认为是垃圾而被回收,这将导致对象状态的丢失。

3. 单例模式的实现方式

3.1 一般情况

饿汉式(最简单的单例实现方式)

1
2
3
4
5
6
7
8
9
10
class Singleton {
private static Singleton ourInstance = new Singleton();
private Singleton() {
}
public static Singleton newInstance() {
return ourInstance;
}
}

应用场景:

  • 要求直接在应用启动时加载并初始化
  • 单例对象要求初始化速度非常快且占用内存非常小

懒汉式

懒汉式与饿汉式最大的区别是单例的初始化操作的时机

  • 饿汉式:自动进行单例的初始化
  • 懒汉式:有需要的时候才手动调用newInstance()进行单例的初始化操作
1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
private static Singleton ourInstance = null;
private Singleton() {
}
public static Singleton newInstance() {
if( ourInstance == null){
ourInstance = new Singleton();
}
return ourInstance;
}
}

应用场景:

  • 单例初始化的操作耗时比较长而应用对于启动速度又有要求
  • 单例的占用内存比较大
  • 单例只是在某个特定场景的情况下才会被使用,即按需延迟加载单例。

3.2 多线程下的单例模式实现

在多线程的情况下:

  • 对于“饿汉式单例模式”:适用,因为JVM只会加载一次单例类;
  • 对于“懒汉式单例模式”:不适用,因为“懒汉式”在创建单例时是线程不安全的,多个线程可能会并发调用 newInstance 方法从而出现重复创建单例对象的问题。

解决方案1:同步锁

使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成instance被多次实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
private static Singleton ourInstance = null;
private Singleton() {
}
public static Singleton newInstance() {
synchronized (Singleton.class){
if( ourInstance == null){
ourInstance = new Singleton();
}
}
return ourInstance;
}
}

解决方案2:双重校验锁

在同步锁的基础上( synchronized (Singleton.class) 外)添加了一层if,这是为了在Instance已经实例化后下次进入不必执行 synchronized (Singleton.class) 获取对象锁,从而提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Singleton {
private static Singleton ourInstance = null;
private Singleton() {
}
public static Singleton newInstance() {
if( ourInstance == null){
synchronized (Singleton.class){
if( ourInstance == null){
ourInstance = new Singleton();
}
}
}
return ourInstance;
}
}

解决方案3:静态内部类

在JVM进行类加载的时候会保证数据是同步的,我们采用内部类实现:在内部类里面去创建对象实例。
只要应用中不使用内部类 JVM 就不会去加载这个单例类,也就不会创建单例对象,从而实现“懒汉式”的延迟加载和线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
//在装载该内部类时才会去创建单例对象
private static class Singleton2{
private static Singleton ourInstance = new Singleton();
}
private Singleton() {
}
public static Singleton newInstance() {
return Singleton2.ourInstance;
}
}

解决方案4:枚举类型

最简洁、易用的单例实现方式,(《Effective Java》推荐)

1
2
3
4
5
6
7
8
9
public enum Singleton{
//定义一个枚举的元素,它就是Singleton的一个实例
instance;
public void doSomething(){
}
}

使用方式如下:

1
2
Singleton singleton = Singleton.instance;
singleton.doSomething();


5. 总结

本文主要对单例模式进行了全面介绍,包括原理和实现方式,接下来我会继续讲解其他设计模式,有兴趣可以继续关注有兴趣可以继续关注Carson_Ho的最易懂的设计模式系列!!!!


欢迎关注Carson_Ho的简书!

不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度

文章目录
  1. 1. 前言
  2. 2. 目录
  3. 3. 1. 引入
    1. 3.0.0.1. 1.2 实例引入
  • 4. 2. 单例模式介绍
    1. 4.0.0.1. 2.1 解决的问题(应用场景)
    2. 4.0.0.2. 2.2 工作原理
    3. 4.0.0.3. 2.3 实例介绍
    4. 4.0.0.4. 2.4 优点
    5. 4.0.0.5. 2.5 缺点
  • 5. 3. 单例模式的实现方式
    1. 5.0.1. 3.1 一般情况
      1. 5.0.1.1. 3.2 多线程下的单例模式实现
      2. 5.0.1.2. 解决方案1:同步锁
      3. 5.0.1.3. 解决方案2:双重校验锁
      4. 5.0.1.4. 解决方案3:静态内部类
      5. 5.0.1.5. 解决方案4:枚举类型
  • 6. 5. 总结
    1. 6.0.1. 欢迎关注Carson_Ho的简书!
  • ,