美文网首页JavaJava 杂谈Java服务器端编程
唯一实例与中心化——单例模式

唯一实例与中心化——单例模式

作者: RunAlgorithm | 来源:发表于2019-05-13 23:09 被阅读0次

1. 定义

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

一句话总结,就是唯一实例,中心化

现实世界的模型:

  • 日本只有一个天皇。
  • 一个国家只有一个央行。

单例模式需要满足以下的特点:

  • 单例类全局只有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一个实例。

2. 设计

单例类的类图可以简单表示为: 单例模式

如果正确地设计单列,需要关注两个点:

  • 线程安全。
  • 性能。

因为单例类需要自己实例化自身,并且要确保在多线程环境下不会产生多个实例,而且并发下性能到达最优。

根据实例化的时机,分为两种:

  • 饿汉模式,类加载的时候就初始化。

  • 懒汉模式,延迟初始化。

2.1. 懒汉:错误方式

为了实现懒汉,延迟初始化单例,我们把实例化的过程推迟到第一次访问单例上。于是就有了这样的代码

public class Singleton {
    private static Singleton instance;
    private Singleton (){}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

看上去很美好,实则有一个很大的隐患,多线程调用 getInstance 方法会产生多实例。

简单分析一下,假设 A 线程和 B 线程同时调用

  • A 线程执行 instance = new Singleton(); ,语句还没结束, instance 还为 null
  • 这时候 B 线程进入语句 if (instance == null) ,条件成立,也进入 instance = new Singleton();
  • 两个线程均建立了实例

为了确保能够延迟初始化,并且做到线程安全,下面会开始介绍

2.2. 懒汉:方法直接加同步(不要用)

最简单粗暴的方式,获取实例的方法直接上锁:

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}  

确实线程安全了。

但是 synchronized 是互斥锁,悲观锁,getInstance 被调用频繁的情况下性能低下。

这样吧,我们把锁的粒度

2.3. 懒汉:双重检查锁定(推荐,记得 volatile)

双重检查锁定,本质是对锁的一个粒度优化。

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getInstance() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
} 

只有判断实例为空,才会进入同步代码。实例存在的话就直接返回了。

所以只有在创建的那一刹那,并且有多线程并发,会有一个互斥等待的过程。

因为进入同步代码,有可能其他线程已经实例化完毕,所以还要再检查一下是否已经实例化(判空),这就是双重检查。

因为 JVM 的指令优化,指令重排序现象会导致对象延迟初始化,所以其他线程读到实例不为空的时候,可能还没初始化。

所以需要加个 volatile 关键字,禁止重排序优化。

2.4. 懒汉:静态内部类(非常推荐)

这是一个比较机智的做法,利用 JVM 类加载机制,来延迟初始化对象实例

public class Singleton {    
    private static class LazyHolder {    
       private static final Singleton INSTANCE = new Singleton();    
    }    
    private Singleton (){}    
    public static Singleton getInstance() {    
       return LazyHolder.INSTANCE;    
    }
}

什么时候会进行类加载?

这里的静态内部类,外部的 Singleton 加载的时候并不会引起它的加载。因为虽然是它的内部静态类,但编译成字节码文件后是两个单独的类。

在调用 getInstance 方法后,静态内部类被主动调用,触发类加载流程。

而类加载流程是天然同步的,我们可以从源码上看到:

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
 {
    synchronized (getClassLoadingLock(name)) {
        ...
        return c;
    }
}

我们也就不需要再进行加锁了。

2.5. 饿汉:静态工厂方法

在单例类加载的时候,马上实例化单例对象。

public class Singleton {  
    private Singleton() {}  
    private static final Singleton singleton = new Singleton();  
    public static Singleton getInstance() {  
        return singleton;  
    }  
}  

始终是线程安全的。

缺点是,及时没有调用该单例,也会在类使用的时候一开始就创建好了,在一些对性能要求高的场景会消耗性能。(这个在客户端场景比较常见)

2.6. 枚举实现

public enum Singleton {
    INSTANCE;
    private Singleton() {
    }
}

创建枚举默认是线程安全的

3. 应用

什么时候需要使用单例?

控制实例的访问,所有的访问必须在单一实例上进行。

控制资源的使用,和线程池、连接池等配合使用,资源型单例,避免创建多个池导致资源的浪费。

控制对象的创建,不需要重复创建的对象,和工厂模式配合,比如实现工厂实例的唯一。

在客户端环境,比如 Android 开发,因为对应用启动的性能要求高,不希望应用一加载马上进行单例实例化,所以对懒汉模式应用较多。

3.1. JDK:Runtime

Runtime 可以获取 JVM 运行环境的信息。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    
    ...
}

4. 特点

4.1. 优势

  • 中心化:控制所有的访问在唯一实例上进行
  • 性能优化:避免频繁创建和销毁对象带来的性能损耗
  • 内存优化:避免可共享资源的重复创建

4.2. 缺点

  • 职责膨胀:如果承担的职责过多,违背单一职责原则

    优化思路

    把职责再重新剥离出去,或者与其他设计模式一起使用,单例仅作为入口

  • 内存泄漏:如果成员变量引用了本来该释放的对象,引起泄漏,进而导致 OOM(内存溢出)

    优化思路

    第一,注意引用对象的生命周期

    第二,如果确定要引用,需要有良好的内存释放机制。比如该成员变量为缓存池,考虑使用弱引用或者软引用,在虚拟机垃圾回收阶段或者内存紧张的时候进行对象的回收

4.3. 注意事项

  • 不要滥用:实例和进程生命周期一致,长期不使用白白占用内存
  • 不要反射:发射生成实例,会破坏单例的设计。(之前有遇到坑,继承的第三方服务反射一个单例类导致不唯一)
  • 注意引用:成员变量尽量不去持有生命周期短的对象,非要持久需要注意释放机制
  • 注意性能:根据业务来决定是否需要延迟初始化(用来抉择懒汉还是饿汉)
  • 注意线程安全:使用懒汉模式,注意线程安全避免产生多个实例

相关文章

  • 唯一实例与中心化——单例模式

    1. 定义 单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提...

  • Java Singleton 单例模式

    单例模式 属于创建型模式 自行完成实例化,私有化构造函数 单例模式的目标 实例唯一性 线程安全性 任何情况都需要确...

  • 单例模式(Singleton)

    单例模式的特点 单例模式只能有一个实例。 单例类必须创建自己的唯一实例。 单例类必须向其他对象提供这一实例 应用场...

  • python高级中的单例、异常、模块

    一、单例模式 单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,单例模式...

  • 单例

    单例的定义 单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例单例模式。 注:单例模式只应在...

  • 单例模式

    一、概述   单例模式目的是维护系统中全局唯一的实例化对象,并对外提供全局访问的方法。 二、示例   单例模式分为...

  • 面试之单例设计模式

    一、单例模式 单例模式分为两种:饿汉模式与懒汉模式1.1 懒汉:在第一次用到类实例的时候才会去实例化。1.2 ...

  • 设计模式(单例模式)

    单例模式 单例模式有以下特点: 单例类只能有一个实例 单例类必须自己创建自己的唯一实例 单例类必须给所有其他对象提...

  • 单例模式

    单例模式 一个类只有唯一的实例全局可以访问该实例可以自动实例化适用场景:全局的模态框,购物车,个人中心等,状态管理...

  • Thread-safe singleton

    单例模式的概念 单例模式就是确保只有一个实例,而且自行实例化并向整个系统传递这个实例,这个类就称作为单例类 单例模...

网友评论

    本文标题:唯一实例与中心化——单例模式

    本文链接:https://www.haomeiwen.com/subject/axidaqtx.html