为什么说单例模式的饿汉式是线程安全的?

一、类加载的方式是按需加载,且只加载一次

因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。单例就是该类只能返回一个实例。

换句话说,在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例。

也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。即饿汉式单例天生就是线程安全的。

二、单例模式几种实现

单例模式特点

私有构造方法private SingleTon1()
私有静态全局变量private static SingleTon1 singleton = new SingleTon1()
公有静态方法public static getInstance()

instance为什么一定要是static的?

1.通过静态的类方法(getInstance) 获取instance,该方法是静态方法,instance由该方法返回(被该方法使用),如果instance非静态,无法被getInstance调用;

2.instance需要在调用getInstance时候被初始化,只有static的成员才能在没有创建对象时进行初始化。且类的静态成员在类第一次被使用时初始化后就不会再被初始化,保证了单例;

3.static类型的instance存在静态存储区,每次调用时,都指向的同一个对象。其实存放在静态区中的是引用,而不是对象。而对象是存放在堆中的。

单例模式的构造方法为什么私有?

1.设置private以后,每次new对象的时候都要调用构造方法。而private的权限是当前类,那么其他类new对象的时候一定会失败。
2.设置成private是考虑封装性,防止在外部类中进行初始化,也就不是单例了。

饿汉式:

//基于JVM的类加载器机制避免了多线程的同步问题,对象在类装载时就实例化
public class SingleTon1(){
  private SingleTon1(){
  }
  private static SingleTon1 singleton = new SingleTon1();
  public static getInstance(){
     return singleton ;
  }
}

懒汉式:

//能够在getInstance()时再创建对象,所以称为懒汉式。
//这种实现最大的问题就是不支持多线程。因为没有加锁同步。
public class SingleTon2(){
   private SingleTon2(){
   }
   private static SingleTon2 singleton2 = null;
   public static getInstance(){
     if(singleton2 == null){
	   singleton2 = new SingleTon2();
	 }
	  return singleton2 ;
   }
}

加同步锁synchronized的懒汉模式:

//除第一次使用,getInstance()需要同步,后面getInstance()不需要同步;每次同步,效率很低。
public class SingleTon3(){
   private SingleTon3(){
   }
   private static SingleTon3 singleton3 = null;
   public synchronized static getInstance(){
     if(singleton3 == null){
	   singleton3 = new SingleTon3();
	 }
	  return singleton3 ;
   }
}

双重锁模式:

//安全且在多线程情况下能保持高性能。
//实例变量需要加volatile 关键字保证易变可见性
public class SingleTon4{
  private SingleTon4(){
  }
  private volatile static SingleTon4 singleton4 = null;
  public static SingleTon4 getSingleton(){
     if(singleton4 == null){
        synchronized (SingleTon4.class){
          if(singleton4 == null){
 		      singleton4 = new SingleTon3();
 		  }
		}
	 }
	  return singleton4 ;
  }
}

静态内部类模式:

//利用了JVM类加载机制来保证初始化实例对象时只有一个线程,
//静态内部类SingletonHolder类只有第一次调用getInstance方法时,才会装载从而实例化对象
public class Singleton5{
  private SingleTon5 (){
  }
  private static class SingletonHolder{
       private static final Singleton5 = new Singleton5();
  }
  public static final Singleton5 getInstance() {
       return SingletonHolder.Singleton5 ;
  }
} 

三、单例模式VS静态类(静态属性/方法)

把类中所有属性/方法定义成静态也可以实现"单例"。 静态类不用实例化就可以使用,虽然使用比较方便,但失去了面向对象的一些优点,适用于一些过程简单且固定、不需要扩展变化、不需要维护任何状态的类方法,如java.lang.Math,里面每种计算方法基本都是固定不变的。那为什么需要用"NEW"单例模式,而不把类中所有属性/方法定义成静态的?
单例模式保证一个类对象实例的唯一性,有面向对象的特性,虽然扩展不容易,但还是可以被继承(protected权限的构造方法)、重写方法等。

四、Java反射攻击破坏单例模式

给实例构造函数protected或private权限,可以通过相关反射方法,改变其权限,创建多个实例。比如:

public class Test {
     public static void main(String args[]) {
		    private Singleton() {};
            Singleton singleton = Singleton.getInstance();
        	try {
            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            Singleton singletonnew = constructor.newInstance();
            System.out.println(singleton == singletonnew);
            //输出结果为 false
        } catch (Exception e) {
 
        }     
    }
}

解决方案:可以给构造函数加上判断限制创建多个实例,如下:

private Singleton() {
     if (null != Singleton.singleton) {
         throw new RuntimeException();
    }
}

五、单例模式中的单例对象会不会被垃圾回收

对于JDK1.2后的JVM HotSpot来说,判断对象可以回收需要经过可达性分析,由于单例对象被其类中的静态变量引用,所以JVM认为对象是可达的,不会被回收。
另外,对于JVM方法区回收,由堆中存在单例对象,所以单例类也不会被卸载,其静态变量引用也不会失效

六、多JVM/ClassLoader的系统使用单例类

不同ClassLoader加载同一个类,对类本身的对象(Singleton.class)来说是不一样的,所以可以创建出不同的单例对象,对不同JVM的情况更是如此,这些在JavaEE开发中还是比较常见。
所以,在多JVM/ClassLoader的系统使用单例类,需要注意单例对象的状态,最好使用无状态的单例类。

七、Spring(IOC框架)实现的单例

Spring的一个核心功能控制反转(IOC)或称依赖注入(DI):
高层模块通过接口编程,然后通过配置Spring的XML文件或注解来注入具体的实现类(Bean)。
这样的好处的很容易扩展,想要更换其他实现类时,只需要修改配置就可以了。通过IOC容器来实现,其默认生成的Bean是单例的(在整个应用中(一般只用一个IOC容器),只创建Bean的一个实例,多次注入同一具体类时都是注入同一个实例)

IOC容器来实现过程简述如下:
当需要注入Bean时,IOC容器首先解析配置找到具体类,然后判断其作用域(@Scope注解);
如果是默认的单例@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON),则查找容器中之前有没有为其创建了Bean实例;
如果有则直接注入该Bean实例,如果没有生成一个放到容器中保存(ConcurrentHashMap – map.put(bean_id, bean)),再注入。

注:其中解析配置查找具体类、生成Bean实例和注入过程都是通过Java反射机制实现的。

从上面可以了解到,Spring实现的单例和我们所说的单例设计模式不是一个概念:
前者是IOC容器通过Java反射机制实现,后者只是一种编程方法(套路)。
但总的来说,它们都可以实现“单例”。

参考如下:
https://blog.youkuaiyun.com/Ricky_Monarch/article/details/99407326
https://blog.youkuaiyun.com/tjiyu/article/details/76572617
https://blog.youkuaiyun.com/qq_36523667/article/details/79014324
https://blog.youkuaiyun.com/naerna/article/details/80498633
https://blog.youkuaiyun.com/zcw4237256/article/details/79670608

<think>我们正在讨论单例模式中的线程安全问题,特别是静态函数如何确保线程安全。在单例模式中,我们通常使用一个静态函数(如`getInstance()`)来获取单例对象。这个静态函数需要保证在多线程环境下只创建一个实例。 根据引用[3]和引用[4],我们可以总结几种实现线程安全单例模式的方法,其中静态函数(`getInstance()`)的实现是关键。 以下是几种常见的线程安全实现方式: 1. **内部静态变量(Meyers' Singleton)**(C++11及以上): 在C++11标准中,静态局部变量的初始化是线程安全的。因此,我们可以利用这一特性,在静态函数中定义一个静态局部变量,从而确保线程安全。 示例代码(来自引用[4]): ```cpp class Singleton { public: Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; static Singleton& getInstance() { static Singleton instance; // 线程安全的初始化(C++11及以上) return instance; } void doSomething() { // ... } private: Singleton() {} // 私有构造函数 }; ``` 这种方法的原理是:C++11标准规定,静态局部变量的初始化只会在一个线程中发生,并且其他线程会等待初始化完成。因此,无需加锁。 2. **双重检查锁定(Double-Checked Locking)**: 在C++11之前,或者当不能依赖编译器实现静态局部变量线程安全时,可以使用双重检查锁定。但需要注意,在C++11之前,双重检查锁定可能会因为指令重排而出现问题,因此需要配合内存屏障或使用`volatile`关键字(如引用[5]所示)以及互斥锁。 示例代码(引用[5]): ```cpp #include <iostream> #include <mutex> using namespace std; class Singleton { public: static Singleton* getInstance() { if (instance == nullptr) { // 第一次检查,避免不必要的加锁 lock_guard<std::mutex> guard(mtx); // 加锁 if (instance == nullptr) { // 第二次检查,确保只有一个实例被创建 instance = new Singleton(); } } return instance; } private: static Singleton* volatile instance; // 使用volatile防止指令重排(在C++11中,atomic提供更好的解决方案) static mutex mtx; Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; }; // 初始化静态成员 Singleton* volatile Singleton::instance = nullptr; mutex Singleton::mtx; ``` 注意:在C++11及以后,使用`std::atomic`和`std::mutex`可以更安全地实现双重检查锁定,但C++11之后的版本更推荐使用内部静态变量的方式。 3. **饿汉式单例(Eager Initialization)**: 在程序启动时(即任何线程启动之前)就初始化单例对象,这样就不存在线程安全问题。但是,这可能会导致程序启动变慢,并且如果单例对象很大且很少使用,会造成资源浪费。 示例代码: ```cpp class Singleton { public: static Singleton& getInstance() { return instance; } private: static Singleton instance; // 在类外初始化 Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; }; // 在.cpp文件中初始化 Singleton Singleton::instance; ``` 4. **使用std::call_once**: 另一种线程安全的实现方式是使用`std::call_once`,这需要配合`std::once_flag`。 示例代码(参考引用[3]): ```cpp #include <mutex> class Singleton { public: static Singleton& getInstance() { std::call_once(initFlag, []() { instance.reset(new Singleton); }); return *instance.get(); } private: static std::unique_ptr<Singleton> instance; static std::once_flag initFlag; Singleton() {} // 禁止拷贝和赋值 }; // 初始化静态成员 std::unique_ptr<Singleton> Singleton::instance; std::once_flag Singleton::initFlag; ``` 总结:在C++11及以后,最简洁且线程安全单例模式实现方式是使用局部静态变量(Meyers' Singleton)。这种方式由语言标准保证线程安全,代码简洁,且延迟初始化(懒汉式)避免了不必要的开销。 因此,在静态函数`getInstance()`中,通过局部静态变量实现单例是推荐的做法,它确保了线程安全,且无需显式加锁。 引用[4]和引用[3]都提到了这种方法的优势,并指出它是线程安全的(在C++11及以上环境中)。 注意:如果需要在程序退出时进行资源清理,单例对象应避免使用全局变量或静态变量,因为销毁顺序可能引发问题。但是,局部静态变量在程序退出时会自动销毁,因此是安全的。 所以,回答用户的问题:在单例模式中,静态函数(`getInstance()`)可以通过以下方式确保线程安全: - 使用C++11的局部静态变量(推荐) - 使用双重检查锁定(注意使用内存屏障或`std::atomic`) - 使用`std::call_once` - 饿汉式初始化(在程序启动时初始化) 其中,局部静态变量的方法是最简洁且被广泛接受的。</think>### C++ 单例模式中静态函数的线程安全实现 在单例模式中,静态函数(通常是 `getInstance()`)负责创建和返回单例对象。其线程安全的核心在于**确保多线程环境下只创建一次实例**。以下是三种主流实现方式及其原理: --- #### 1. 局部静态变量法(C++11 起推荐) **原理**:利用 C++11 标准对局部静态变量初始化的**线程安全保证**(编译器自动插入同步机制) ```cpp class Singleton { public: static Singleton& getInstance() { // 静态函数 static Singleton instance; // 线程安全的初始化 return instance; } // 禁用拷贝和赋值 Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; private: Singleton() {} // 私有构造函数 }; ``` **优势**: - 简洁高效(编译器自动处理线程同步) - 符合 RAII 原则(自动销毁) - 延迟初始化(首次调用时创建)[^4] --- #### 2. 双重检查锁定(Double-Checked Locking) **原理**:通过**互斥锁+双重判空**避免不必要的锁竞争 ```cpp #include <mutex> class Singleton { public: static Singleton* getInstance() { if (!instance) { // 第一次检查(无锁) std::lock_guard<std::mutex> lock(mtx); if (!instance) instance = new Singleton(); // 第二次检查 } return instance; } private: static Singleton* volatile instance; // volatile 防止指令重排 static std::mutex mtx; Singleton() {} }; // 初始化静态成员 Singleton* volatile Singleton::instance = nullptr; std::mutex Singleton::mtx; ``` **关键点**: - `volatile` 确保内存可见性(防止 CPU 缓存导致脏读) - 双重检查减少锁竞争开销 - 需手动管理内存(析构问题)[^5] --- #### 3. `std::call_once` 方法 **原理**:使用 C++11 的**一次性执行机制**保证初始化原子性 ```cpp #include <mutex> class Singleton { public: static Singleton& getInstance() { std::call_once(initFlag, []() { instance.reset(new Singleton); }); return *instance; } private: static std::unique_ptr<Singleton> instance; static std::once_flag initFlag; Singleton() {} }; // 初始化静态成员 std::unique_ptr<Singleton> Singleton::instance; std::once_flag Singleton::initFlag; ``` **优势**: - 标准库保障线程安全 - 完美支持延迟初始化 - 无需手动管理锁[^3] --- ### 线程安全的核心要素 1. **初始化原子性** 确保实例创建过程不可分割(局部静态变量、`call_once` 或互斥锁实现) 2. **内存可见性** 使用 `volatile` 或原子操作(如 `std::atomic`)防止指令重排和缓存不一致 3. **禁止拷贝构造** 显式删除拷贝构造函数和赋值运算符(`= delete`) 4. **析构安全性** 优先使用自动析构(局部静态变量)或智能指针管理生命周期 > 推荐选择:**C++11 及以上优先使用局部静态变量法**,代码最简洁且由语言标准保障安全[^4]。旧标准项目可用双重检查锁定(需配合 `volatile`)。 --- ### 性能对比 | 方法 | 线程安全 | 延迟初始化 | 代码复杂度 | C++ 版本要求 | |--------------------|----------|------------|------------|--------------| | 局部静态变量 | ✅ | ✅ | ⭐ | C++11 | | 双重检查锁定 | ✅ | ✅ | ⭐⭐⭐⭐ | C++03 | | `std::call_once` | ✅ | ✅ | ⭐⭐ | C++11 | | 饿汉式(全局初始化)| ✅ | ❌ | ⭐ | 任何版本 | > 注:饿汉式通过全局初始化保证线程安全,但丧失延迟初始化优势[^3]。 --- ### 总结 静态函数 `getInstance()` 实现线程安全的关键在于: 1. **控制实例化时机**:首次调用时创建(懒汉式) 2. **同步机制**:编译器内置同步(C++11 局部静态)、互斥锁或 `call_once` 3. **内存屏障**:使用 `volatile` 或原子操作保证可见性 4. **防拷贝**:禁用拷贝语义确保唯一性 在 C++11+ 项目中,**局部静态变量法是最优解**,兼顾安全性、简洁性和性能[^4]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值