如何写一个线程安全的单例模式?

单例模式的简单实现

单例模式大概是流传最为广泛的设计模式之一了。一份简单的实现代码大概是下面这个样子的:

class singleton
{
public:
static singleton* instance()
{
if (inst_ != nullptr) {
inst_ = new singleton();
}
return inst_;
}
private:
singleton(){}
static singleton* inst_;
}; singleton* singleton::inst_ = nullptr;

这份代码在单线程的环境下是完全没有问题的,但到了多线程的世界里,情况就有一点不同了。考虑以下执行顺序:

  1. 线程1执行完if (inst_ != nullptr)之后,挂起了;
  2. 线程2执行instance函数:由于inst_还未被赋值,程序会inst_ = new singleton()语句;
  3. 线程1恢复,inst_ = new singleton()语句再次被执行,单例句柄被多次创建。

所以,这样的实现是线程不安全的。

有问题的双重检测锁

解决多线程的问题,最常用的方法就是加锁呗。于是很容易就可以得到以下的实现版本:

class singleton
{
public:
static singleton* instance()
{
guard<mutex> lock{ mut_ };
if (inst_ != nullptr) {
inst_ = new singleton();
}
return inst_;
}
private:
singleton(){}
static singleton* inst_;
static mutex mut_;
}; singleton* singleton::inst_ = nullptr;
mutex singleton::mut_;

这样问题是解决了,但性能上就不那么另人满意,毕竟每一次使用instance都多了一次加锁和解锁的开销。更关键的是,这个锁也不是每次都需要啊!实际我们只有在创建单例实例的时候才需要加锁,之后使用的时候是完全不需要锁的。于是,有人提出了一种双重检测锁的写法:

...
static singleton* instance()
{
if (inst_ != nullptr) {
guard<mutex> lock{ mut_ };
if (inst_ != nullptr) {
inst_ = new singleton();
}
}
return inst_;
}
...

我们先判断一下inst_是否已经初始化了,如果没有,再进行加锁初始化流程。这样,虽然代码看上去有点怪异,但好像确实达到了只在创建单例时才引入锁开销的目的。不过遗憾的是,这个方法是有问题的。Scott Meyers 和 Andrei Alexandrescu 两位大神在C++ and the Perils of Double-Checked Locking 一文中对这个问题进行了非常详细地讨论,我们在这儿只作一个简单的说明,问题出在:

	inst_ = new singleton();

这一行。这句代码不是原子的,它通常分为以下三步:

  1. 调用operator new为singleton对象分配内存空间;
  2. 在分配好的内存空间上调用singleton的构造函数;
  3. 将分配的内存空间地址赋值给inst_。

如果程序能严格按照1-->2-->3的步骤执行代码,那么上述方法没有问题,但实际情况并非如此。编译器对指令的优化重排、CPU指令的乱序执行(具体示例可参考《【多线程那些事儿】多线程的执行顺序如你预期吗?》)都有可能使步骤3执行早于步骤2。考虑以下的执行顺序:

  1. 线程1按步骤1-->3-->2的顺序执行,且在执行完步骤1,3之后被挂起了;
  2. 线程2执行instance函数获取单例句柄,进行进一步操作。

由于inst_在线程1中已经被赋值,所以在线程2中可以获取到一个非空的inst_实例,并继续进行操作。但实际上单例对像的创建还没有完成,此时进行任何的操作都是未定义的。

现代C++中的解决方法

在现代C++中,我们可以通过以下几种方法来实现一个即线程安全、又高效的单例模式。

使用现代C++中的内存顺序限制

现代C++规定了6种内存执行顺序。合理的利用内存顺序限制,即可避免代码指令重排。一个可行的实现如下:

class singleton {
public:
static singleton* instance()
{
singleton* ptr = inst_.load(memory_order_acquire);
if (ptr == nullptr) {
lock_guard<mutex> lock{ mut_ };
ptr = inst_.load(memory_order_relaxed);
if (ptr == nullptr) {
ptr = new singleton();
inst_.store(ptr, memory_order_release);
}
} return inst_;
}
private:
singleton(){};
static mutex mut_;
static atomic<singleton*> inst_;
}; mutex singleton::mut_;
atomic<singleton*> singleton::inst_;

来看一下汇编代码:

可以看到,编译器帮我们插入了必要的语句来保证指令的执行顺序。

使用现代C++中的call_once方法

call_once也是现代C++中引入的新特性,它可以保证某个函数只被执行一次。使用call_once的代码实现如下:

class singleton
{
public:
static singleton* instance()
{
if (inst_ != nullptr) {
call_once(flag_, create_instance);
}
return inst_;
}
private:
singleton(){}
static void create_instance()
{
inst_ = new singleton();
}
static singleton* inst_;
static once_flag flag_;
}; singleton* singleton::inst_ = nullptr;
once_flag singleton::flag_;

来看一下汇编代码:

可以看到,程序最终调用了__gthrw_pthread_once来保证函数只被执行一次。

使用静态局部变量

现在C++对变量的初始化顺序有如下规定:

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

所以我们可以简单的使用一个静态局部变量来实现线程安全的单例模式:

class singleton
{
public:
static singleton* instance()
{
static singleton inst_;
return &inst_;
}
private:
singleton(){}
};

来看一下汇编代码:

可以看到,编译器已经自动帮我们插入了相关的代码,来保证静态局部变量初始化的多线程安全性。

全文完。

【多线程那些事儿】如何使用C++写一个线程安全的单例模式?的更多相关文章

  1. 用 Java 写一个线程安全的单例模式(Singleton)?

    请参考答案中的示例代码,这里面一步一步教你创建一个线程安全的 Java 单例类.当我们说线程安全时,意思是即使初始化是在多线程环境中,仍然能保证单个实例.Java 中,使用枚举作为单例类是最简单的方式 ...

  2. 死磕 java线程系列之自己动手写一个线程池

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...

  3. 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理

    摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ...

  4. 死磕 java线程系列之自己动手写一个线程池(续)

    (手机横屏看源码更方便) 问题 (1)自己动手写的线程池如何支持带返回值的任务呢? (2)如果任务执行的过程中抛出异常了该怎么处理呢? 简介 上一章我们自己动手写了一个线程池,但是它是不支持带返回值的 ...

  5. 在Linux下写一个线程池以及线程池的一些用法和注意点

    -->线程池介绍(大部分来自网络)  在这个部分,详细的介绍一下线程池的作用以及它的技术背景以及他提供的一些服务等.大部分内容来自我日常生活中在网络中学习到的一些概念性的东西. -->代码 ...

  6. 原来热加载如此简单,手动写一个 Java 热加载吧

    1. 什么是热加载 热加载是指可以在不重启服务的情况下让更改的代码生效,热加载可以显著的提升开发以及调试的效率,它是基于 Java 的类加载器实现的,但是由于热加载的不安全性,一般不会用于正式的生产环 ...

  7. python_way ,day11 线程,怎么写一个多线程?,队列,生产者消费者模型,线程锁,缓存(memcache,redis)

    python11 1.多线程原理 2.怎么写一个多线程? 3.队列 4.生产者消费者模型 5.线程锁 6.缓存 memcache redis 多线程原理 def f1(arg) print(arg) ...

  8. 用JAVA写一个多线程程序,写四个线程,其中二个对一个变量加1,另外二个对一个变量减1

    package com.ljn.base; /** * @author lijinnan * @date:2013-9-12 上午9:55:32 */ public class IncDecThrea ...

  9. 迅雷笔试题 (JAVA多线程)启动三个线程,分别打印A B C,现在写一个程序 循环打印ABCABCABC

    题目:http://wenku.baidu.com/view/d66187aad1f34693daef3e8a.html 启动三个线程,分别打印A B C,现在写一个程序 循环打印ABCABCABC. ...

随机推荐

  1. MySQL表操作过程的基础代码解析

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. MySQL 的表有很多种,对表的操作主要是增删改查,今天来浅谈一下这些操作的底层代码和流程,以下以 tmp table为例 ...

  2. Linux环境监控工具汇总

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. Linux 操作系统有诸多自带和第三方的监控工具,以下从不同维度来整理常用的一些监控工具. CPU top(经典的Linu ...

  3. Spring Boot部署方法

    Spring Boot部署方法     网上搜到的部署方法无非是打成jar包,然后shell执行nohup java调用jar命令,或者是打成war包然后部署到tomcat或者jetty容器上面. S ...

  4. CSS3(一)

    (一)CSS简介 CSS主要用来美化页面.布局页面 1.HTML的局限性 HTML主要是做结构,显示元素内容.只关注内容,但是丑.虽然可以做简单的样式,但是非常臃肿和繁琐 2.CSS网页的美容师 CS ...

  5. 简单创建一个SpringCloud2021.0.3项目(一)

    目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 新建父模块和注册中心 1. 新建父模块 2. 新建注册中心Eureka 3. 新建配置中心Config 4. 新建两个业务服务 1. ...

  6. 【Traefik二次开发】服务 Service 开发

    Service 定义 https://doc.traefik.io/traefik/routing/services/ The Services are responsible for configu ...

  7. 【读书笔记】C#高级编程 第四章 继承

    (一)继承的类型 1.实现继承和接口继承 在面向对象的编程中,有两种截然不同的继承类型:实现继承和接口继承. 实现继承:表示一个类型派生于一个基类型,它拥有该基类型的所有成员字段和函数.在实现继承中, ...

  8. Taurus.MVC 微服务框架 入门开发教程:项目集成:3、客户端:其它编程语言项目集成:Java集成应用中心。

    系列目录: 本系列分为项目集成.项目部署.架构演进三个方向,后续会根据情况调整文章目录. 开源地址:https://github.com/cyq1162/Taurus.MVC 本系列第一篇:Tauru ...

  9. 【Azure 环境】Azure Resource Graph Explorer 中实现动态数组数据转换成多行记录模式 - mv-expand

    问题描述 想对Azure中全部VM的NSG资源进行收集,如果只是查看一个VM的NSG设定,可以在门户页面中查看表格模式,但是如果想把导出成表格,可以在Azure Resource Graph Expl ...

  10. Mysql 安全加固经验总结

    本文为博主原创,转载请注明出处: 目录 1.内网部署Mysql 2. 使用独立用户运行msyql 3.为不同业务创建不同的用户,并设置不同的密钥 4.指定mysql可访问用户ip和权限 5. 防sql ...