Closed AlexiaChen closed 1 year ago
title: 单例模式的线程安全 date: 2017-02-27 15:48:40 tags:
在写单例模式的时候,一般我们都需要保证这个单例类的线程安全,当然,网络上有大部分“解决方案了”,加锁和双重检查锁配合来“保证”单例类的线程安全,可是,如果把指令重排序也考虑到其中的话,这样的写法,就是非线程安全了。
随便列出网络上几篇博文的单例模式都不是线程安全的:
以下是网络上大部分的“经典”的“线程安全”单例模式实现:
@ actually not thread safe class singleton { private: static singleton* p{nullptr}; public: static std::mutex m_mutex; static singleton* getInstance(); public: singleton(const singleton& s) = delete; }; singleton* singleton:: getInstance() { // double-check locker if (p == nullptr) // first check { { std::lock_guard<std::mutex> lock(m_mutex); // lock if (p == nullptr) // second check p = new singleton(); // but this may cause non-thread-safe } } return p; }
首先,先讲解一下,指令重排会发生在系统的好几个层面,我分别用C++和Java来讲解:
C++ 被编译器编译成机器码的时候,机器码的顺序可能被重排过,在程序运行的时候,在CPU内部也可能会选择性的又进行一次重排,总共2次,重排是必定会发生的。
Java 被编译器编译成字节码的时候会重排一次,JVM执行字节码的时候又进行一次重排,JVM执行的字节码最终也是变成机器码在CPU内部又会进行重排,总共是3次。
所以了,到头来,指令重排是不可能消除的,这是编译器和CPU优化的领域,就不进行过多的探讨。
下面来讲解一下以上的单例类的代码,以及为什么它在指令重排序的情况下会变成非线程安全:
我上面把代码的注释写了,问题就出现在new 那里,熟悉C++的开发者可能知道new语句的大概执行动作,我把它分解成如下几步:
注意了,如果编译器生成的代码,和CPU内部的执行顺序永远是按以上的顺序执行,那么永远都不用担心线程安全的问题,但是由于happens-before语义,2和3步骤没有严格的依赖顺序,编译器有些时候为了优化,完全可以把3放到2时候执行,2也会放到3时候执行。那么这样情况下,可以就会变成下面这样了:
还想深入了解happens-before的,可以看这里。
那么,指令重排下,以上的单例代码可能变成以下:
@ actually not thread safe class singleton { private: static singleton* p{nullptr}; public: static std::mutex m_mutex; static singleton* getInstance(); public: singleton(const singleton& s) = delete; }; singleton* singleton:: getInstance() { // double-check locker if (p == nullptr) // first check { { std::lock_guard<std::mutex> lock(m_mutex); // lock if (p == nullptr) // second check { //以下是抽象出来的伪代码 memory_addr = malloc(); //1:分配对象的内存空间 p = memory_addr; //2:设置p指向刚分配的内存地址 //注意,此时对象还没有被初始化! singleton_constructor(memory); //3:初始化对象 } } } return p; }
聪明的人,一下子就可以看出来以上的代码在多线程下可能会导致的问题了, 还不明白的话,我做了一个线程执行时间表:
线程B拿到一个未初始化的对象(对象状态不合法)去操作,结果肯定就出错了。
那么如何写才能保证线程安全呢? 我给出几种方案:
@ thread safe // 以下代码虽然是C++ 11的,但是你可以把它理解非C++ 11,如果是C++ 11的话,从标准上就保证 静态初始化就是线程安全的,完全没必要像下面这样做了 class singleton { private: static atomic < singleton* > p{nullptr}; public: static std::mutex m_mutex; static singleton* getInstance(); public: singleton(const singleton& s) = delete; }; singleton* singleton:: getInstance() { // double-check locker if (p == nullptr) // first check { { std::lock_guard<std::mutex> lock(m_mutex); // lock if (p == nullptr) // second check { p = new singleton(); } } } return p; }
如果是C++ 11的代码,完全没必要这么繁琐了,可以直接像下面这么干:
// C++ 11标准保证这是线程安全的,当然也得看编译器产商怎么实现了,比如悲剧的是 // VS2013下这么做是非线程安全的,但VS2015下绝对是线程安全 // 参考: https://msdn.microsoft.com/en-gb/library/hh567368.aspx singleton*& getInst() { static singleton* p = new singleton(); return p; }
详情请戳这里。
如果是Java代码,建议按以下这么干,保证线程安全:
public class singleton { private static class Holder { static final singleton INSTANCE = new singleton(); } public static singleton getInstance() { return Holder.INSTANCE; } // rest of class omitted }
以上Java代码完全不需要加锁,Java的类加载器已经保证必定会在访问类的时候,最先初始化singleton。
详情请戳这里和这里。
EOF
title: 单例模式的线程安全 date: 2017-02-27 15:48:40 tags:
线程安全
在写单例模式的时候,一般我们都需要保证这个单例类的线程安全,当然,网络上有大部分“解决方案了”,加锁和双重检查锁配合来“保证”单例类的线程安全,可是,如果把指令重排序也考虑到其中的话,这样的写法,就是非线程安全了。
随便列出网络上几篇博文的单例模式都不是线程安全的:
以下是网络上大部分的“经典”的“线程安全”单例模式实现:
首先,先讲解一下,指令重排会发生在系统的好几个层面,我分别用C++和Java来讲解:
C++ 被编译器编译成机器码的时候,机器码的顺序可能被重排过,在程序运行的时候,在CPU内部也可能会选择性的又进行一次重排,总共2次,重排是必定会发生的。
Java 被编译器编译成字节码的时候会重排一次,JVM执行字节码的时候又进行一次重排,JVM执行的字节码最终也是变成机器码在CPU内部又会进行重排,总共是3次。
所以了,到头来,指令重排是不可能消除的,这是编译器和CPU优化的领域,就不进行过多的探讨。
下面来讲解一下以上的单例类的代码,以及为什么它在指令重排序的情况下会变成非线程安全:
我上面把代码的注释写了,问题就出现在new 那里,熟悉C++的开发者可能知道new语句的大概执行动作,我把它分解成如下几步:
注意了,如果编译器生成的代码,和CPU内部的执行顺序永远是按以上的顺序执行,那么永远都不用担心线程安全的问题,但是由于happens-before语义,2和3步骤没有严格的依赖顺序,编译器有些时候为了优化,完全可以把3放到2时候执行,2也会放到3时候执行。那么这样情况下,可以就会变成下面这样了:
还想深入了解happens-before的,可以看这里。
那么,指令重排下,以上的单例代码可能变成以下:
聪明的人,一下子就可以看出来以上的代码在多线程下可能会导致的问题了, 还不明白的话,我做了一个线程执行时间表:
线程B拿到一个未初始化的对象(对象状态不合法)去操作,结果肯定就出错了。
那么如何写才能保证线程安全呢? 我给出几种方案:
如果是C++ 11的代码,完全没必要这么繁琐了,可以直接像下面这么干:
详情请戳这里。
如果是Java代码,建议按以下这么干,保证线程安全:
以上Java代码完全不需要加锁,Java的类加载器已经保证必定会在访问类的时候,最先初始化singleton。
详情请戳这里和这里。
EOF