浅析设计模式 - 单例模式

本文目录:

概述

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

现实世界模型:

比如古代一个国家只能有一个皇帝

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

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

简单设计

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

单例模式-类图

要正确的写好单例,需要关注两点:

  • 多线程下的初始化是否是线程安全的
  • 性能如何

主要分两种方法,懒汉和饿汉,区别在初始化唯一实例的时机。懒汉延迟初始化,饿汉在类加载的时候就初始化了

同时 Java 还有一个特殊的实现方式,使用枚举实现

懒汉

延迟加载单例,只有第一次使用的时候才进行实例化

懒汉需要做一些线程安全的处理

如果没有进行多线程处理,比如:

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

假设有两个线程,A 线程和 B 线程,两个线程同时调用 getInstance

当 A 线程执行 instance = new Singleton(); 语句还没结束时,instance 还为 null

这时候 B 线程进入语句 if (instance == null) ,条件成立,也进入 instance = new Singleton();

这时候,这个类就会产生两个实例,都被外界拿去使用了

保证延迟加载并且线程安全的做法主要有这三种:

方法加同步

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

直接在 getInstance() 方法加同步,线程安全

缺点是调用 getInstance 比较密集的场景,同步方法频繁调用,性能低

静态内部类

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

利用 Java 的类加载机制,来延迟初始化对象实例。类被主动调用的时候,如果没有加载会执行加载

我们这里使用的是静态内部类,在没有被访问前,还没有进行加载。在使用 getInstance 方法后,该静态内部类被主动调用,于是开始了对这个类的类加载过程。

因为 Java 的类加载过程是同步的,包括类静态成员的初始化也是同步的,这个做法无需再单独加锁

ClassLoader 加载类的代码:

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

可见有对加载进行同步处理

双重检查锁定

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;  
    }  
} 

双重检查锁定和同步静态方法对比,把锁的粒度降低,所以只有在判断当前实例为空,才会进入被锁住的代码。这样子提高了使用的性能,保证在实例存在的情况下,不会因为互斥锁导致多个线程阻塞等待的现象

同时 volatile 关键字还确保了,虚拟机进行指令优化的时候,不会进行重排序导致对象延迟初始化

volatile 关键字的主要功能有三:

  • 防止指令重排序
  • 保证内存可见性
  • 保证 long 或者 double 等基本类型的原子操作

如果没有加 volatile 关键字,由于虚拟机的指令排序优化,会把对象创建延迟到使用的时候。会出现 singleton 返回不会 null 的时候,但是还没有执行初始化对象实例,当另一个线程拿到对象实例,即使不为 null,但实际还是未初始化的对象

饿汉

在类加载后就实例化出一个静态对象出来,

静态工厂方法

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

饿汉始终是线程安全的

他的缺点后面即使没有使用单例,也会一直存在,某些场景下会耗费内存(比如某个进程一直都没有使用到那个单例,而这个单例持有大量内存,造成不必要的浪费)

枚举

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

创建枚举默认是线程安全的,同时不需要担心反序列化导致重新创建新对象

应用

单例模式应用范围很广,可以在这里见到

控制实例的访问,需要访问一定在同一实例上进行的,比如 HttpClient

控制资源的使用,和线程池或者对象池配合使用,资源类型的单例,避免创建多个池浪费资源,节约内存

控制对象的创建,不需要多次重复创建,和工厂模式配合使用,一些工厂只需要进程中只保持一份,

优缺点

优势:

  • 控制所有访问在唯一实例上进行

  • 避免频繁创建和销毁对象带来的性能消耗

  • 节省内存,避免可共享资源的重复创建

缺点:

  • 如果职责过多,会导致单例类违背单一职责原则(拆分出其他模块来执行,单例做一个入口)
  • 一些不必要的内存引用,会造成内存溢出(要有内存释放机制)
  • 一些错误的引用,比如引用了不再使用的对象(典型的有对 Activity 的引用),会造成内存泄漏

使用注意

单例模式,实例一旦初始化,就会在内存中一直存在,要注意几点

  • 不能滥用,要确定该实例是需要在整个进程中长期使用的
  • 如果是资源单例的话,要考虑适当地释放一些很少使用的资源,避免页面增长造成内存泄漏
  • 实例的成员变量,不要持有生命周期短的对象,如果持有要记得释放,否则会导致内存一直无法回收而造成内存泄漏
  • 如果一个类是单例设计,不要用反射去生成它的实例,反射直接破坏对这个类的设计
  • 使用懒汉式要注意线程安全

猜你喜欢

转载自blog.csdn.net/firefile/article/details/80546892