设计模式——单例模式

256 阅读5分钟

设计模式——单例模式

1. 概述

单例模式使用的要点就是保证对应类的实例全局唯一,这有利于协调系统整体的行为

2. 定义

确保某一个类只有一个实例,而且自行实例化向整个系统提供这个实例

3. 使用场景

避免产生多个对象消耗过多的资源或者只应该存在一个(类似于工具类),比如像处理IO或数据库资源的对象实例

4. UML类图

image-20220724184008984

类图中体现出来的构成元素非常简单:

  • 调用方
  • 单例类(Singleton)

单例类实现的关键

  1. 构造函数不对外开放,通常设置为private,外部无法直接new

  2. 通过一个静态方法或者枚举返回单例类的实例

  3. 确保实例有且仅有一个,注意考虑多线程的场景

  4. 确保单例类对象在反序列化时不会重新构建

5. 简单示例

5.1. 饿汉式

实践出真知,接下来通过简单的例子了解一下单例模式的核心思想

public class Enemy {

    void attack() {
        System.out.println(this + "攻击主角");
    }
}
public class Boss extends Enemy {  // 饿汉式

    private static final Boss mInstance = new Boss();  // 唯一的实例,也有一种采用static{}的写法

    public static Boss getInstance() {
        return mInstance;
    }

    private Boss() {  // 只能在类内部用new
		
    }
}
public class Test {

    public static void main(String[] args) {
        Enemy enemy1 = new Enemy();
        enemy1.attack();
        Enemy enemy2 = new Enemy();
        enemy2.attack();
        Boss boss1 = Boss.getInstance();
        boss1.attack();
        Boss boss2 = Boss.getInstance();
        boss2.attack();
    }
}

image-20220724232958066

以上写法属于饿汉式,饿汉式的特点便是甭管用不用,都会创建一个实例

饿汉式是利用ClassLoader机制确保线程安全的,只要类加载就会去实例化,但是这样的做法会造成一定程度的内存浪费,因为不一定会用到实例,没有按需加载

5.2. 懒汉式

懒汉式注重的就是”懒“,能躺着就不会坐着,只有需要才会做,这是最大程度地利用资源

如果体现到游戏中,饿汉式的思想就是游戏一加载,可能就给Boss创建出来了,明显有些浪费,因为就算创建出来玩家一开始也碰不到

等到加载到对应关卡,才去创建才更加合理些,这就是懒加载的思想

接下来改下Boss的代码

public class Boss extends Enemy {

    private static Boss mInstance;   // 声明但并未直接实例化

    public static Boss getInstance() {
        if (mInstance == null) {   // 没有才创建
            mInstance = new Boss();
        }
        return mInstance;   // 有就直接给你
    }

    private Boss() {

    }
}

主要的修改在于mInstance成员和getInstance()方法,现在是先声明,然后用的时候没有,才去new一个,后面的有了这个实例,就可以直接返回给你用了

似乎看上去挺合理的,但是别忘了多线程场景

因为一个线程去new对象的时候,很可能另一线程也判断mInstance == null,于是它不知道线程1去new了,于是线程2也去new,这样就有了2个实例,当然,也可能更多

image-20220727230838042

		Runnable r = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Boss boss = Boss.getInstance();  
                boss.attack();
            }
        };
        for (int i = 0; i < 1000; i++) {   // 1000个线程
            Thread t = new Thread(r, "线程" + i);
            t.start();
        }

image-20220727231613922

线程多的时候就增加了出现这种问题的可能性,并且本身在代码层面上也是有疏漏的,只能说在单线程上是可以保持单例的

既然出现了线程安全问题,那么就需要考虑去解决呀,于是乎,加上synchronized来解决

  public static synchronized Boss getInstance() {   // 只要调用该方法,线程就会强制排队
        if (mInstance == null) {
            mInstance = new Boss();
        }
        return mInstance;
    }

这样的确是解决了线程安全问题,但是,这样就大大降低了多线程的效率,因为getInstance()可能会频繁使用到,其实只需要确保创建的那次不出问题就行了

5.3. 双重检查(DCL)

由之前懒汉式的问题出发,首先是线程安全问题,这里用到volatile关键字,这样该变量的修改多线程间就可见了

第一次是判断实例,防止非必要同步,第一次可能进来多个线程

然后进入同步块,这里一次只能进来一个,第一个进来的判断为空,创建,立即更新并对其他线程可见,已经进来的发现无事可做,但会走同步块,没进第一个的则直接返回实例

public class Boss extends Enemy {

    private static volatile Boss mInstance;   // volatile保证线程间可见性

    public static synchronized Boss getInstance() {
        if (mInstance == null) {    // 把更改刷新,防止多同步
            synchronized (Boss.class) {  // 保证一次一个线程
                if (mInstance == null) {
                    mInstance = new Boss();
                }
            }
        }
        return mInstance;
    }

    private Boss() {

    }
}

image-20220728230625826

5.4. 静态内部类

这是充分利用了类加载,在getInstance()调用时装载内部类完成初始化,以此保证线程安全和懒加载

public class Boss extends Enemy {
    
    public static synchronized Boss getInstance() {    // 调用时装载
        return BossHolder.mInstance;       
    }

    private Boss() {

    }

    private static class BossHolder {  // 静态内部类
        private static final Boss mInstance = new Boss();
    }
}

5.5. 枚举

终极而简便的方式,利用枚举的特性,能够在创建实例时保证线程安全,即使是在反序列化过程也能保证单例,而不需要像之前几种一样,需要额外进行处理

enum Boss {
    INSTANCE;
    public void attack() {
        System.out.println(this.hashCode() + "攻击主角");
    }
}

6. Android场景

单例模式应用的场景很广泛,这里给出一个WindowManager相关的例子,对应的类WindowManagerGlobal,完成WindowManagerImpl各个方法的具体实现,与WMS的窗口处理绘制流程相关

image.png

可以发现,这里使用的正好是线程安全的懒汉式,使用同步块包裹住了判空和实例的创建,保证多个线程的环境下只返回同一实例