单例模式

Post on Aug 05, 2021 by Wei Lin

概述

什么是单例模式?

单例模式及C++实现代码

单例模式(Singleton Pattern)即一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

实现思路:

  • 单例类只能有一个实例,使用一个静态指针指向该唯一实例;
  • 单例类必须自己创建自己的唯一实例;
  • 单例类必须给所有其他对象提供这一实例;
  • 唯一一个公有函数可以获取唯一的实例;
  • 构造函数私有,避免外部创建该类的实例。

C++**代码示例

#include <iostream>
using namespace std;

class Singleton {
public:
	static Singleton* getInstance() { return instance; };	//获取静态对象必须要用静态方法

private:
	Singleton() {};		//构造函数私有,避免外部创建该类的实例
	//把复制构造函数和=操作符也设为私有,防止被复制
	Singleton(const Singleton&) {};
	Singleton& operator=(const Singleton&) { return *this; };

	static Singleton* instance;	//static对象不能在类内初始化
};

Singleton* Singleton::instance = new Singleton();	//static对象初始化

int main() {
	Singleton* singleton1 = Singleton::getInstance();
	Singleton* singleton2 = Singleton::getInstance();

	if (singleton1 == singleton2)//比较是否为同一对象,实际上是判断地址是否相同
		cout << "singleton1 = singleton2\n";
	system("pause");
	return 0;
}

多线程并发时,怎么保证只创建一个实例?

彻头彻尾理解单例模式与多线程

在单线程环境下,单例模式根据实例化对象时机的不同,有两种经典的实现:

  • 饿汉式单例在单例类被加载时候,就实例化一个对象并交给自己的引用;
  • 懒汉式单例只有在真正使用的时候才会实例化一个对象并交给自己的引用。

但在多线程环境下,情形就发生了变化:由于饿汉式单例在类被加载时就会实例化一个对象并交给自己的静态引用,因此可以直接用于多线程而不会出现问题;但懒汉式单例本身是非线程安全的,因此可能会出现多个实例的情况。

常见的单例模式

6种常见的单例模式——简书

单例模式之「双重校验锁」——CSDN

饿汉式

饿汉式是指类加载时就进行对象的实例化。它是绝对线程安全的,因为在线程还没调用前就实例化了,不可能存在访问安全问题。

饿汉表示一开始就实例化,有点迫不及待的意思。

public class Singleton {
    //生成单例对象
    private static final Singleton mSingleton = new Singleton();

    //私有化构造方法
    private Singleton() {
    }

    //获取单例对象
    public static Singleton getInstance() {
        return mSingleton;
    }
}

优点:线程安全。

缺点:某些情况下,造成内存浪费,因为对象未被使用的情况下就会被初始化,如果一个项目中的类多达上千个,在项目启动的时候便开始初始化可能并不是我们想要的。

懒汉式—单线程

懒汉式是当第一次使用的时候才创建对象,合理占用资源。

public class Singleton {

    private static Singleton mSingleton;

    // 构造器私有化
    private Singleton() {}

    public static Singleton getInstance() {
        if (mSingleton == null) {
            mSingleton = new Singleton();
        }
        return mSingleton;
    }
}

优点:与饿汉式相比,在使用式才创建,不占用资源。

缺点:在单线程下能够完美运行,但是在多线程下存在安全隐患,有可能会创建多个对象。

上面代码在单线程下没有问题,但多个线程同时调用getInstance()方法时,由于没有加锁,可能会出现一下情况:

  • 这些线程可能会创建多个对象;
  • 可能会得到一个未完全初始化的对象(JVM指令重排)。

懒汉式—方法加锁

为解决懒汉式单例在单线程下的问题,于是可以对getInstance()加同步锁。

public class Singleton {
    //声明单例对象
    private static Singleton mSingleton;

    //私有化构造方法
    private Singleton() {
    }

    //同步该方法获取单例对象
    public static synchronized Singleton getInstance() {
        //当该对象为空的时候创建该对象
        if (mSingleton == null) {
            mSingleton = new Singleton();
        }
        return mSingleton;
    }
}

优点:多线程下不会出现问题。

缺点:对整个方法加锁会降低性能。因为只有当第一次调用 getInstance() 时才需要执行new方法创建对象。之后再次调用 getInstance() 时就只是简单的返回mSingleton,这一步是不需要同步锁的。

所以没必要对整个方法加锁,只需要对new部分加锁即可。

懒汉式—方法内加锁(单锁)

只对new部分加锁,可以提高性能。但是加在mSingleton判空前或后有不同的效果。而且这些方法都会带来一些隐患,所以实际场景中,不应该使用这类方法。

方式1:锁加在mSingleton == null之前

public class Singleton {

    private static Singleton mSingleton;

    // 构造器私有化
    private Singleton() {
    }

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

优点:可以解决多线程下创建多个对象的问题。

缺点:

  • 仍然无法解决指令重排的问题,需要对mSingleton对象加volatile 关键字;
  • 如果mSingleton已经被创建,在只需要返回mSingleton对象时仍然需要进入synchronized代码块判断mSingleton是否为null,性能方面还可以继续改进。

方式2:锁加在mSingleton == null之后

public class Singleton {

    private static Singleton mSingleton;

    // 构造器私有化
    private Singleton() {
    }

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

可以实现在持锁前先判断mSingleton是否为null,但是并无法解决“懒汉式—单线程”中所说的两个问题,即多线程可能会创建多个对象以及JVM指令重排。

懒汉式—双重校验锁

所以为解决多线程以及JVM指令重排的问题,就需要双重判断以及volatile 关键字。

public class Singleton {
    //声明单例对象
    private volatile static Singleton mSingleton;

    //私有化构造方法
    private Singleton() {
    }

    //同步该方法获取单例对象
    public static synchronized Singleton getInstance() {
        //当该对象为空的时候先同步这个对象
        if (mSingleton == null) {
            synchronized (Singleton.class) {
                //再判断是否为空
                if (mSingleton == null) {
                    //如果还空的话 就创建对象
                    mSingleton = new Singleton();
                }
            }
        }
        //返回该对象实例
        return mSingleton;
    }
}
  • 加 volatile 是为了禁止指令重排序,也就是为了解决问题②,即避免某个线程获取到其他线程没有初始化完全的对象。
  • 第1次判断是为了性能考虑;
  • 第2次判断是为了避免多线程下重复创建对象;

静态内部类式

public class Singleton {

    // 构造器私有化
    private Singleton() {
    }

    public static Singleton getInstance() {
        // 返回内部类中的singleton对象
        return SingletonHolder.mSingleton;
    }

    // 第一次加载Singleton类的时候不用调用该类
    // 只有调用getInstance()方法时才会创建该对象。
    private static class SingletonHolder {
        static final Singleton mSingleton = new Singleton();
    }
}

为什么静态内部类方式可以保证线程安全?

  • JVM在类的初始化阶段,也就是SingletonHolder被加载后,在被线程使用之前,都是类的初始化阶段,在这个阶段会执行类的初始化;
  • 在执行类的初始化期间呢,JVM会获取一个锁,这个锁可以同步多个线程对一个类的初始化,基于这个特性,可以实现基于静态内部类的、并且是线程安全的延迟初始化方案。

枚举式

单例模式之枚举——CSDN

以上多种单例模式都各有缺点:

  • 有的类加载时就初始化,浪费内存
  • 有的不保证多线程安全
  • 有的因为加了synchronized同步锁导致并发效率较低
  • 以上的单例模式都能通过反射,反序列化,克隆等方式被破坏
public enum Singleton {
    INSTANCE;

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

其它

JVM指令重排

jvm指令重排——CSDN

单例模式之「双重校验锁」——CSDN

反射破坏单例模式

单例模式之枚举——CSDN