单例设计模式原理详解Java/JS/Go/Python/TS不同语言实现

Posted 刀法如飞-专注算法与设计模式

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例设计模式原理详解Java/JS/Go/Python/TS不同语言实现相关的知识,希望对你有一定的参考价值。

简介

单例模式(Singleton Pattern)属于创建型设计模式,这种模式只创建一个单一的类,保证一个类只有一个实例,并提供一个访问该实例的全局节点。

当您想控制实例数目,节省系统资源,并不想混用的时候,可以使用单例模式。单例有很多种实现方式,主要分为懒汉和饿汉模式,同时要通过加锁来避免线程安全。不同语言的单例实现略有差异,可以通过查看不同版本的源码来深入理解其中的差异。

作用

  1. 避免全局使用的类频繁地创建与销毁。
  2. 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

实现步骤

  1. 创建单例类,注意线程安全
  2. 返回全局唯一实例

UML

 

Java代码

单例实现,不同语言有很大不同,跟语言特性有关。请查看其他源码进行比较。

饿汉式(线程安全)

// SingletonEager.java 当类被加载的时候会初始化,静态变量被创建并分配内存空间 
public class SingletonEager 
  private String name = "SingletonEager";
  // 类加载时就初始化,浪费内存
  private static final SingletonEager instance = new SingletonEager();

  // 构造函数是private,不允许实例化
  private SingletonEager() 

  
  public static SingletonEager getInstance() 
    return instance;
  

  public void run() 
    System.out.println("SingletonEager::run() " + this.name);
  

饱汉式

// SingletonLazy.java 懒汉式也叫饱汉式,增加synchronized来保证线程安全
public class SingletonLazy 

  private static SingletonLazy instance;
  private String name;

  private SingletonLazy() 

  

  // 类初始化时,静态变量static的instance未被创建并分配内存空间
  // 当getInstance方法第一次被调用时,再初始化instance变量,并分配内存
  // 相当于延迟到调用时再实例化,加synchronized以便线程安全,不加则存在并发时多个实例的情形
  public static synchronized SingletonLazy getInstance(String name) 
    if (instance == null) 
      instance = new SingletonLazy();
      instance.name = name;
    
    return instance;
  

  public void run() 
    System.out.println("SingletonLazy::run() " + this.name);
  

静态内部类

// SingletonInner.java 静态内部类方式,既实现延迟加载,也保障线程安全。
public class SingletonInner 

  private String name;

  private SingletonInner() 

  

  // 静态内部类利用了类加载初始化机制,外部类加载时,并不会加载内部类,也不会执行
  // 虚拟机会保证方法在多线程环境下使用加锁同步,只会执行一次,因此线程安全
  private static class Inner 
    private static final SingletonInner instance = new SingletonInner();
  

  // 当执行getInstance()方法时,虚拟机才会加载静态内部类
  public static SingletonInner getInstance(String name) 
    if (Inner.instance.name == null) 
      Inner.instance.name = name;
    
    return Inner.instance;
  

  public void run() 
    System.out.println("SingletonInner::run() " + this.name);
  

双重检验懒汉

// SingletonDoubleCheck.java 双重检验懒汉单例,单例模式最优方案,线程安全并且效率高 
public class SingletonDoubleCheck 

  // 定义一个静态私有变量(不初始化,不使用final关键字)
  // 可以使用volatile保证多线程访问时变量的可见性
  // 这样避免了初始化时其他变量属性还没赋值完时,被另外线程调用
  private static volatile SingletonDoubleCheck instance;
  private String name;
  private SingletonDoubleCheck() 

  

  // 延迟到调用时实例化
  public static SingletonDoubleCheck getInstance(String name) 
    if (instance == null) 
      // 在实例化时再synchronized
      synchronized (SingletonDoubleCheck.class) 
        if (instance == null) 
          instance = new SingletonDoubleCheck();
          instance.name = name;
        
      
    
    return instance;
  

  public void run() 
    System.out.println("SingletonDoubleCheck::run() " + this.name);
  


测试调用

    /**
     * 单例模式就是一个类只创建一个实例,以便节省开销和保证统一
     * 对于多线程语言需要注意线程安全和性能之间取得一个平衡
     */
    SingletonEager singletonEager1 = SingletonEager.getInstance();
    SingletonEager singletonEager2 = SingletonEager.getInstance();
    singletonEager1.run();
    singletonEager2.run();
    // 两个实例相等
    System.out.println("singletonEager1 == singletonEager2 ? " + String.valueOf(singletonEager1 == singletonEager2));

    /*********************** 分割线 ******************************************/

    SingletonLazy singletonLazy1 = SingletonLazy.getInstance("singletonLazy1");
    SingletonLazy singletonLazy2 = SingletonLazy.getInstance("singletonLazy2");
    singletonLazy1.run();
    singletonLazy2.run();

    /*********************** 分割线 ******************************************/

    SingletonDoubleCheck singletonDoubleCheck1 = SingletonDoubleCheck.getInstance("singletonDoubleCheck1");
    SingletonDoubleCheck singletonDoubleCheck2 = SingletonDoubleCheck.getInstance("singletonDoubleCheck2");
    singletonDoubleCheck1.run();
    singletonDoubleCheck2.run();

    /*********************** 分割线 ******************************************/

    SingletonInner singletonInner1 = SingletonInner.getInstance("singletonInner1");
    SingletonInner singletonInner2 = SingletonInner.getInstance("singletonInner2");
    singletonInner1.run();
    singletonInner2.run();

Go代码

// DoubleCheckSingleton.go
import (
  "fmt"
  "sync"
)

// 安全懒汉模式的升级版,通过sync的Mutex实现双重检验
type DoubleCheckSingleton struct 
  name string


func (s *DoubleCheckSingleton) Run() 
  fmt.Println("DoubleCheckSingleton::run()", s.name)


// 定义私有变量,用来保存实例
var doubleCheckSingletonInstance *DoubleCheckSingleton
var lock = &sync.Mutex

// 是懒汉模式安升级版,双重检查来来支持延迟实例化单例对象
func GetDoubleCheckSingletonInstance(name string) *DoubleCheckSingleton 
  // 未实例化才进行加锁
  if doubleCheckSingletonInstance == nil 
    lock.Lock()
    defer lock.Unlock()
    // 为了保险,锁住之后再次检查是否已实例化
    if doubleCheckSingletonInstance == nil 
      doubleCheckSingletonInstance = &DoubleCheckSingleton
      doubleCheckSingletonInstance.name = name
    
  

  return doubleCheckSingletonInstance


JS版本

// LazySingleton.js
export class LazySingleton 
  static instance
  constructor(alias) 
    this.alias = alias
  

  // 懒汉模式,延迟实例化,请求实例时判断,如果已经实例化过就直接返回
  // js是单线程语言,无需考虑多线程问题
  static getInstance(alias) 
    if (this.instance === undefined) 
      this.instance = new LazySingleton(alias)
    
    return this.instance
  

  run() 
    console.log(\'LazySingleton::run()\', this.alias)
  

Python语言

# SingletonSafe.py
from threading import Lock, Thread


# 加锁的基于元类的单例模式,基于元类type创建的加强版
class SingletonMeta(type):
    # 线程安全单例模式,适用python3
    _instances = 

    _lock: Lock = Lock()

    def __call__(cls, *args, **kwargs):
        with cls._lock:
            if cls not in cls._instances:
                instance = super().__call__(*args, **kwargs)
                cls._instances[cls] = instance
        return cls._instances[cls]

# 继承SingletonMeta就是单例
class SingletonSafe(metaclass=SingletonMeta):
    name: str = None

    def __init__(self, name: str) -> None:
        self.name = name

    def run(self):
        print(\'SingletonSafe::run()\', self.name)

C语言

// lazy_singleton_safe.c
#include "func.h"
#include <pthread.h>

// 静态指针,未被创建并分配内存空间,指向唯一实例
static LazySingletonSafe *lazy_singleton_safe_instance = NULL;

void lazy_singleton_safe_run(LazySingletonSafe *singleton)

  printf("\\r\\n LazySingletonSafe::run() [name=%s value=%d]", singleton->name, singleton->value);


// 内部私有实例化函数,不公开
static LazySingletonSafe *new_lazy_singleton_safe(char *name)

  LazySingletonSafe *singleton = (LazySingletonSafe *)malloc(sizeof(LazySingletonSafe));
  strcpy(singleton->name, name);
  singleton->run = &lazy_singleton_safe_run;
  return singleton;


// 声明锁
pthread_mutex_t singleton_lock;

// 非线程安全懒汉模式,延迟初始化。多个线程同时调用函数时, 可能会被初始化多次,存在线程不安全问题
LazySingletonSafe *get_lazy_singleton_safe_instance(char *name)

  printf("\\r\\n get_lazy_singleton_safe_instance() [name=%s]", name);
  if (pthread_mutex_init(&singleton_lock, NULL) != 0)
  
    perror("error init mutext:");
  

  // 通过加锁来防止线程并发导致的不安全
  if (lazy_singleton_safe_instance == NULL)
  
    printf("\\r\\n new instance [name=%s]", name);
    pthread_mutex_lock(&singleton_lock);
    lazy_singleton_safe_instance = new_lazy_singleton_safe(name);
    pthread_mutex_unlock(&singleton_lock);
  
  return lazy_singleton_safe_instance;

更多语言版本

不同语言实现设计模式:https://github.com/microwind/design-pattern

单例模式详解

本文主要分享的内容是单例模式的应用场景、常见的单例模式写法、保证线程安全的单例模式策略、反射暴力攻击单例解决方案及原理分析、序列化破坏单例的原理及解决方案。

一、单例模式的应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。在 Spring 框架应用中 ApplicationContext;数据库的连接池也都是单例形式。

二、常见的单例模式写法

2.1 饿汉式单例

这种单例是在类加载的时候就马上初始化了,同时创建单例对象。这种方式不加任何锁,执行效率高,绝对的线程安全,但是缺点就是一开始就已经加载了,不管这个类最后有没有用到都会占用空间,浪费内存。饿汉式适用在单例对象较少的情况。Spring 中 IOC 容器 ApplicationContext 就是典型的饿汉式单例。下面是饿汉式单例的写法

/**
 * 饿汉式单例
 */
public class HungrySingleton 

    private static final HungrySingleton hungrySingleton =new HungrySingleton();
    private HungrySingleton()

    public static HungrySingleton getInstance()
        return hungrySingleton;
    

利用静态代码块加载的写法如下:

public class HungryStaticSingleton 

    private static final HungryStaticSingleton hungryStaticSingleton;
    static
        hungryStaticSingleton = new HungryStaticSingleton();
    
    private HungryStaticSingleton()
    public static HungryStaticSingleton getInstance()
        return hungryStaticSingleton;
    

2.2 懒汉式单例

懒汉式单例的特点就是在被外部类调用的时候才会加载,相比饿汉式,这样可以减少内存空间的浪费,做到“按需加载”。

public class LazySimpleSingleton 
    private LazySimpleSingleton()
    private static LazySimpleSingleton lazy = null;
    public static LazySimpleSingleton getInstance()
        if(lazy == null)
            lazy = new LazySimpleSingleton();
        
        return lazy;
    

但是上面这种写法会带来一定的线程安全问题,当同时运行多个线程环境下 LazySimpleSingleton被实例化了多次。有时,我们得到的运行结果可能是相同的两个对象,实际上是被后面执行的线程覆盖了。所以我们要给 getInstance()加上 synchronized 关键字,使这个方法变成线程同步方法。

public class LazySimpleSingleton 
    private LazySimpleSingleton()
    private static LazySimpleSingleton lazy = null;
    public synchronized static LazySimpleSingleton getInstance()
        if(lazy == null)
            lazy = new LazySimpleSingleton();
        
        return lazy;
    

在以上这种写法中,我们将其中一个线程执行并调用 getInstance()方法时,另一个线程再次调用 getInstance()方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复 RUNNING 状态继续调用 getInstance()
方法,所以这个时候线程安全的问题便解决了。但是,用synchronized 加锁,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。所以这个时候就需要用到双重检查锁的单例模式

//双重检查式单例
public class LazyDoubleCheckSingleton 
    //防止指令重排序
    private volatile static LazyDoubleCheckSingleton lazy = null;
    private LazyDoubleCheckSingleton()
    public static LazyDoubleCheckSingleton getInstance()
        if(lazy == null)
            synchronized (LazyDoubleCheckSingleton.class)
                if(lazy == null)
                    lazy = new LazyDoubleCheckSingleton();
                
            
        
        return lazy;
    

当第一个线程调用 getInstance()方法时,第二个线程也可以调用 getInstance()。当第一个线程执行到 synchronized 时会上锁,第二个线程就会变成 MONITOR 状态,出现阻塞。此时,阻塞并不是基于整个 LazySimpleSingleton 类的阻塞,而是在getInstance()方法内部阻塞。但是,用到 synchronized 关键字,总归是要上锁,对程序性能还是存在一定影响的。

所以我们最好的方式就是使用静态内部类的方式,内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。

//内部类的方式实现
//这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题
public class LazyInnerClassSingleton 

    private static class LazyHolder
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    

    private  LazyInnerClassSingleton();
    public static final LazyInnerClassSingleton getInstance()
        return LazyHolder.LAZY;
    

但是,以上这种内部类的写法,当我们使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会
两个不同的实例。我们通过使用以下代码对这个内部类单例进行暴力破解:

public class LazyInnerClassSingletonTest 

    public static void main(String[] args) 
        try
            //对单例进行破坏
            Class<?> clazz = LazyInnerClassSingleton.class;
            //通过反射拿到私有的构造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);

            //暴力初始化,调用两次构造方法,相当于new了两次
            Object o1 = c.newInstance();
            Object o2 = c.newInstance();
             //对比两个对象是否相同
            System.out.println(o1);
            System.out.println(o2);
            System.out.println(o1 ==o2);

        catch (Exception e)
            e.printStackTrace();
        
    

运行结果如下:

cn.tf.pattern.singleton.lazy.LazyInnerClassSingleton@4b67cf4d
cn.tf.pattern.singleton.lazy.LazyInnerClassSingleton@7ea987ac
false
 

从运行结果,我们可以看出这里创建了两个不同的实例。现在,我们在其构造方法中做一些限制,一旦出现多
次重复创建,则直接抛出异常。来看优化后的代码:

//内部类的方式实现
//这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题
public class LazyInnerClassSingleton 

    private static class LazyHolder
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    

    private  LazyInnerClassSingleton()
        if(LazyHolder.LAZY!=null)
            throw  new RuntimeException("不允许创建多个实例");
        
    ;
    public static final LazyInnerClassSingleton getInstance()
        return LazyHolder.LAZY;
    

当再次去暴力破解后就会提示“不允许创建多个实例”,从而保证单例的实现。

2.3 序列化单例

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。

//序列化单例
public class SeriableSingleton implements Serializable 

    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton();

    public static SeriableSingleton getInstance()
        return INSTANCE;
    

对于这种写法的单例,我们可以通过以下暴力破解的方式来破坏这个单例:

/反序列化时导致单例破坏
public class SeriableSingletonTest 

    public static void main(String[] args) 
        SeriableSingleton s1=null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try
            //将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream os = new ObjectOutputStream(fos);
            os.writeObject(s2);
            os.flush();
            os.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream is = new ObjectInputStream(fis);
            s1 = (SeriableSingleton) is.readObject();
            is.close();

            //反序列化后的对象会重新分配内存,
            //即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当
            //于破坏了单例

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1==s2);


        catch (Exception e)
            e.printStackTrace();
        

    

运行结果如下:
 cn.tf.pattern.singleton.seriable.SeriableSingleton@7b23ec81
cn.tf.pattern.singleton.seriable.SeriableSingleton@6e0be858
 false

可以看出,反序列化后的对象和手动创建的对象是不一致的,在这里被实例化了两次,此时我们为了保证序列化的情况下也能够实现单例,需要将序列化单例优化成如下所示:

//序列化单例
public class SeriableSingleton implements Serializable 

    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton();

    public static SeriableSingleton getInstance()
        return INSTANCE;
    
    //防止被序列化破坏
    private Object readResolve()
        return INSTANCE;
    

对于增加的这个readResolve方法,我们可以在JDK源码中的ObjectInputStream中的readObject()方法中逐步找到,在readObject()中有一个readObject0()方法,进入readObject0()之后可以看到TC_OBJECTD 中判断,调用了 ObjectInputStream 的 readOrdinaryObject()方法,

private Object readObject0(boolean unshared) throws IOException 
    ...
    case TC_OBJECT:
    return checkResolve(readOrdinaryObject(unshared));
    ...

isInstantiable()里面的代码如下:

 boolean isInstantiable() 
        requireInitialized();
        return (cons != null);
    

判断一下构造方法是否为空,构造方法不为空就返回 true。所以从这里可以看出,如果是有无参构造方法就会去实例化。在判断无参构造方法是否存在之后,又调用了 hasReadResolveMethod()方法,就是判断 readResolveMethod 是否为空,不为空就返回 true。

boolean hasReadResolveMethod() 
requireInitialized();
return (readResolveMethod != null);

通过全局查找找到了赋值代码在私有方法
ObjectStreamClass()方法中给 readResolveMethod 进行赋值,来看代码

readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);

我们可以看到在 invokeReadResolve()方法中用反射调用了 readResolveMethod 方法。通过 JDK 源码分析我们可以看出,虽然,增加 readResolve()方法返回实例,解决了单例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大。

2.4 ThreadLocal 线程单例

ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。ThreadLocal将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。

public class ThreadLocalSingleton 

    private static final ThreadLocal<ThreadLocalSingleton> threadLocal =
            new ThreadLocal<ThreadLocalSingleton>()
                @Override
                protected ThreadLocalSingleton initialValue() 
                    return new ThreadLocalSingleton();
                
            ;

    private ThreadLocalSingleton();

    public static ThreadLocalSingleton getInstance()
        return threadLocal.get();
    

 

2.5 注册式单例

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。

2.4.1 容器式单例

容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。写法如下:

/**
 * 容器式单例,容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。
 */
public class ContainerSingleton 

    private ContainerSingleton()
    private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
    public static Object getInstance(String className)
        synchronized (ioc)
            if(!ioc.containsKey(className))
                Object obj = null;
                try
                    obj = Class.forName(className).newInstance();
                    ioc.put(className,obj);
                catch (Exception e)
                    e.printStackTrace();
                
                return obj;
            else
                return ioc.get(className);
            
        
    


2.4.2 枚举登记式单例

枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例的实现。

public enum EnumSingleton 

    INSTANCE;
    private Object data;
    public Object getData()
        return data;
    
    public void setData(Object data)
        this.data = data;
    
    public static EnumSingleton getInstance()
        return INSTANCE;
    

序列化不能破坏枚举式单例,因为枚举类型其实通过类名和 Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。

 private Enum<?> readEnum(boolean unshared) throws IOException 
        if (bin.readByte() != TC_ENUM) 
            throw new InternalError();
        

        ObjectStreamClass desc = readClassDesc(false);
        if (!desc.isEnum()) 
            throw new InvalidClassException("non-enum class: " + desc);
        

        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) 
            handles.markException(enumHandle, resolveEx);
        

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) 
            try 
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
             catch (IllegalArgumentException ex) 
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            
            if (!unshared) 
                handles.setObject(enumHandle, result);
            
        

 

同时反射也不能破坏枚举式单例,因为在JDK 源码中,进入 Constructor 的newInstance()方法,在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型,将会直接抛出异常。

@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    
        if (!override) 
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) 
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            
        
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) 
            ca = acquireConstructorAccessor();
        
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    

3、总结

单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用;可以通过设置全局访问点,严格控制访问。单例模式的缺点就是没有接口,扩展困难,如果要扩展单例对象,只有修改代码。单例模式的重点是私有化构造器保证线程安全、延迟加载、防止序列化和反序列化破坏单例、防御反射攻击单例。

文中提到的代码下载地址:https://github.com/sdksdk0/pattern_design/tree/master/src/main/java/cn/tf/pattern/singleton

以上是关于单例设计模式原理详解Java/JS/Go/Python/TS不同语言实现的主要内容,如果未能解决你的问题,请参考以下文章

单例模式详解

单例设计模式原理详解Java/JS/Go/Python/TS不同语言实现

单例模式详解

Java 单例模式详解

Java 单例模式详解

单例模式(单例设计模式)详解