volatile关键字有什么作用?
- 保持内存可见性
- 禁止重排序
- 防止编译器优化
什么叫内存可见性?
当一个数据(或者多线程中的共享数据)发生改变时,这个改变能够立刻被自己(或者其他线程)感知到。
自己的数据,为什么还说立刻被自己
感知到呢?因为当一个经过初始化的数据立即被使用时,编译器会进行优化,直接从寄存器中将这个数据取出,而不是从内存中取。因为是从寄存器直接取出的数据,因此内存中的数据我并不知道是否发生改变(虽然单线程下这个数据并不会发生改变),这就是内存不可见
。
而对于多线程的共享数据就好理解了,这里假设有线程A和线程B,共享数据 s=10
:
当线程A访问一个 s 时会先将数据保存到寄存中,然后进行下面的操作,可是如果将 s 加入到寄存器后线程B将内存中的数据改为了20,然而线程A并不知道,线程A会继续从寄存器中取出 s 进行操作,这就是内存不可见。
volatile 就可以用来解决这个问题,无论何时,当访问一个数据的时候,都必须从内存中重新获取数据而不能直接用保存在寄存器中的数据。这样就保证了内存的可见性。
什么是禁止重排序?
重排序是一种编译器优化代码的手段,举例说明:
Singleton* instance = new Singleton();
上面代码中,我们试图为一个类对象指针申请一块儿空间,如果没有优化,编译器会分三步做:
1)分配一块儿内存
2)在此内存上调用构造函数进行初始化操作
3)将指针指向该内存块
而经过编译器优化,编译器同样会分三步,但是顺序有所不同
1)分配一块儿内存
2)将指针指向该内存块
3)在此内存上调用构造函数进行初始化操作
这就是重排序,当在多线程条件下可能会出现问题。这正是懒汉方式的单例模式下需要考虑的问题:下面是懒汉方式单例模式的代码实现:
为了防止锁冲突影响效率,使用了双重判断,但是与此同时带来的问题是:可能会访问未被初始化的空间,就是重排序导致
的。
假设线程A和线程B,线程A第一次调用GetInstance()
此时会加锁然后进入 #4行
,上面讲过分三步进行,假设这是经过编译器优化的,并且执行到第二步,时间片轮转到线程B。线程B同样获取这个单例对象,判断instance
并不为空(因为线程A中执行了第二步),就直接使用了这个对象,然而这个对象还没有被初始化(线程A没有执行第三步),就出问题了。
解决的方案就是将instance
用volatile关键字修饰,禁止重排序。
class Singleton {
public:
static Singleton* GetInstance() {
//二重判断
if (instance == nullptr) { // #1
pthread_mutex_lock(&_mutex); // #2
if (instance == nullptr) { // #3
printf("第一次实例化~~\n");
instance = new Singleton(); // #4
}
pthread_mutex_unlock(&_mutex); // #5
}
return instance;
}
private:
Singleton(){
pthread_mutex_init(&_mutex, NULL);
printf("Singeton~~\n");
}
//volatile 防止编译器优化,造成调用未初始化的对象
static Singleton* volatile instance;
static pthread_mutex_t _mutex;
};
Singleton* volatile Singleton::instance = nullptr;
pthread_mutex_t Singleton::_mutex;
什么是编译器优化?
volatile 的上面两点作用,其实都使用到了编译器优化。
编译器的优化为的是提高代码执行效率,避免没有必要的代码步骤,通常这是好的。
但是有时候我们也并不希望编译器替我们优化,最明显的就是调试代码的时候,生成的debug版本就是没有经过编译器优化的。volatile 同样也可以起到帮助我们禁止编译器优化的作用。