设计模式创建型-单例模式

Posted 六六学java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式创建型-单例模式相关的知识,希望对你有一定的参考价值。

文章目录


一、单例模式

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

单例模式有三个要点:

  1. 某个类只能有一个实例
  2. 它必须自行创建这个实例
  3. 它必须自行向整个系统提供这个实例。

二、单例模式的八种实现方式

2.1、饿汉式(静态常量)

/*饿汉式(静态常量)*/
public class Singleton1 
    //创建一个私有构造器,不让其他类new
    private Singleton1()
    //创建一个静态常量
    public static final Singleton1 INSTANCE = new Singleton1();
    //实例方法,方法是静态是为了通过类名调用
    public static Singleton1 newInstance()
        return INSTANCE;
    

    public static void main(String[] args) 
        Singleton1 s1 = Singleton1.newInstance();
        Singleton1 s2 = Singleton1.newInstance();
        //比较两个实例是否相等 结果:true
        System.out.println(s1==s2);
    

优缺点:

  • 优点:简单,类加载的时候就完成了实例化,避免了线程安全问题。
  • 缺点:如果没用到这个实例,也会实例化,浪费了内存。

2.2、饿汉式(静态代码块)

/*饿汉式(静态代码块)*/
public class Singleton2 
    //创建一个私有构造器,不让其他类new
    private Singleton2()
    //定义一个静态实例
    public static Singleton2 instance;
    //静态代码块中实例化对象
    static 
         instance= new Singleton2();
    
    //提供一个公有静态方法,放回实例化对象
    public static Singleton2 newInstance()
        return instance;
    

    public static void main(String[] args) 
        Singleton2 s1 = Singleton2.newInstance();
        Singleton2 s2 = Singleton2.newInstance();
        //比较两个实例是否相等 结果:true
        System.out.println(s1==s2);
    

优缺点跟上面的静态常量一样

2.3、懒汉式(线程不安全)

/*
 * 懒汉式
 * 实例是在使用的时候创建,但线程不安全,会创建多个对象
 * */
public class Singleton3 
    //定义instance静态变量
    private static Singleton3 instance;
    private Singleton3()
    //初始化方法,实现懒加载,需要时才创建对象
    public static Singleton3 newInstance() throws InterruptedException 
        //没有实例,则创建对象
        if (instance == null)
            //让线程睡一下,创造多线程进入条件
            Thread.sleep(20);
            instance = new Singleton3();
        
        //实例化过,直接返回
        return instance;
    

    public static void main(String[] args) 
        for (int i = 0; i < 100; i++) 
            //创建多线程,实现Runnable接口,重写run方法
            new Thread(new Runnable() 
                @Override
                public void run() 
                    try 
                        //通过哈希码,看对象是否一样
                        System.out.println(Singleton3.newInstance().hashCode());
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
            ).start();
        
    

优缺点:

  • 优点:起到了懒加载效果,需要时才创建对象,但只适合在单线程下使用。
  • 缺点:在多线程情况下,一个线程 进入了if (instance == null)判断语句块,还未来得及往下执行,另一个线程又进来了,这时就产生了多个实例,造成线程不安全。

2.4、懒汉式(线程安全,同步方法)

/*
 * 懒汉式(线程安全,加入同步方法)
 * */
public class Singleton4 
    //定义instance静态变量
    private static Singleton4 instance;
    private Singleton4()
	//加入同步方法,保证只有一个线程进入
    public static synchronized Singleton4 newInstance() throws InterruptedException 
        //没有实例,则创建对象
        if (instance == null)
            //让线程睡一下,创造多线程进入条件
            Thread.sleep(20);
            instance = new Singleton4();
        
        //实例化过,直接返回
        return instance;
    

    public static void main(String[] args) 
        for (int i = 0; i < 100; i++) 
            //创建多线程,实现Runnable接口,重写run方法
            new Thread(new Runnable() 
                @Override
                public void run() 
                    try 
                        //通过哈希码,看对象是否一样
                        System.out.println(Singleton4.newInstance().hashCode());
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
            ).start();
        
    

方式一是一个实例,同时锁住了空判断和创建实例,线程安全。但是这就相当于全部锁住了,就跟同步方法的效果一样,线程安全但效率很低

方式二不是一个实例,线程不安全,原因是一个线程进入了空判断,还没往下执行,另一个线程来了,其中一个线程拿到锁,往下执行创建了实例,执行完释放锁后,另一个线程也往下执行了并创建对象,两者创建的对象并不一致。

2.5、双重检查

public class Singleton6 
    private static Singleton6 instance;
    private Singleton6();
    public static Singleton6 newInstance() throws InterruptedException 
        //双重检查,是单例
        if (instance == null)
            //首先判断实例是否为空,空就上锁
            synchronized (Singleton6.class)
                //上锁后,如果上面new出了个对象,此时在这判断是否为空,不为空就直接返回了,确保了只有一个实例
                if (instance == null)
                    Thread.sleep(20);
                    instance = new Singleton6();
                
            
        
        return instance;
    
    public static void main(String[] args) 
        for (int i = 0; i < 100; i++) 
            new Thread(() -> 
                try 
                    System.out.println(Singleton6.newInstance().hashCode());
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            ).start();
        
    

双重检查实际上就是在懒汉式(同步代码块)的内部再添加了一个判断,这样就保证线程安全

2.6、静态内部类

public class Singleton7 
    private Singleton7() 
    //静态内部类里实例化对象,在Singleton7加载的时候,SingletonInstance内部类不加载,只在实例的时候加载
    private static class SingletonInstance
    //静态属性,实例化对象
        private static final Singleton7 INSTANCE = new Singleton7();
    
    //提供一个静态的公有方法,返回SingletonInstance类的实例
    public static Singleton7 newInstance()
        return SingletonInstance.INSTANCE;
    
    public static void main(String[] args) 
        for (int i = 0; i < 100; i++) 
            new Thread(()->
                System.out.println(Singleton7.newInstance().hashCode());
            ).start();
        
    

这种方式采用了类加载的机制来保证初始化实例时只有一个线程,线程安全。静态内部类在 Singleton7 类被加载时并不会立即实例化,而是在调用 newInstance方法的时候才会实例化静态内部类,通过SingletonInstance类调用实例,从而完成 Singleton 的实例化。类的静态属性只会在第一次加载类的时候初始化,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

2.7、枚举

package com.s.singleton;
/**
 * 枚举
 */
public enum  Singleton8 
    INSTANCE;
    public static void main(String[] args) 
        for (int i = 0; i < 100 ; i++) 
            new Thread(()->
                    System.out.println(Singleton8.INSTANCE.hashCode())).start();
        
    

枚举实现是单例的,线程安全,不仅可以解决线程同步,还可以防止反序列化。《Effective Java》作者 Josh Bloch 提倡的方式。

创建型模式 单例模式

创建型模式 单例模式

 

 

/**
 * 创建型模式 单例模式 懒汉式
 * GoF对单例模式的定义是:保证一个类、只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。
 *
 * 实现单例步骤常用步骤
 * a) 构造函数私有化
 * b) 提供一个全局的静态方法(全局访问点)
 * c) 在类中定义一个静态指针,指向本类的变量的静态变量指针
 *
 */

#include <iostream>

class Singelton
{
private:
    static Singelton * m_pls;

    Singelton() // 构造函数不是线程安全函数
    {
        std::cout << "Singelton 构造函数执行" << std::endl;
    }
public:
    static Singelton *getInstance()
    {
        if (m_pls == nullptr) // 只有在使用的时候,才去创建对象。 1 每次获取实例都要判断 2 多线程会有问题 
        {
            m_pls = new Singelton;
        }
        return m_pls;
    }

    static Singelton *FreeInstance()
    {
        if (m_pls != nullptr)
        {
            delete m_pls;
            m_pls = nullptr;
        }
        return m_pls;
    }
};

Singelton * Singelton::m_pls = nullptr;


void mytest()
{
    Singelton * p1 = Singelton::getInstance();
    Singelton * p2 = Singelton::getInstance();
    if (p1 == p2)
    {
        std::cout << "是同一个对象" << std::endl;
    }
    else
    {
        std::cout << "不是同一个对象" << std::endl;
    }
    Singelton::FreeInstance();

    return;
}


int main()
{
    mytest();

    system("pause");
    return 0;
}

 

/**
 * 创建型模式 单例模式 饿汉式
 * GoF对单例模式的定义是:保证一个类、只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。
 *
 * 实现单例步骤常用步骤
 * a) 构造函数私有化
 * b) 提供一个全局的静态方法(全局访问点)
 * c) 在类中定义一个静态指针,指向本类的变量的静态变量指针
 *
 */

#include <iostream>

class Singelton
{
private:
    static Singelton * m_pls;

    Singelton() 
    {
        std::cout << "Singelton 构造函数执行" << std::endl;
    }
public:
    static Singelton *getInstance()
    {
        return m_pls;
    }

    static Singelton *FreeInstance()
    {
        if (m_pls != nullptr)
        {
            delete m_pls;
            m_pls = nullptr;
        }
        return m_pls;
    }
};

// 饿汉式
// 不管你创建不创建实例,均把实例new出来
Singelton * Singelton::m_pls = new Singelton;


void mytest()
{
    Singelton * p1 = Singelton::getInstance();
    Singelton * p2 = Singelton::getInstance();
    if (p1 == p2)
    {
        std::cout << "是同一个对象" << std::endl;
    }
    else
    {
        std::cout << "不是同一个对象" << std::endl;
    }
    Singelton::FreeInstance();

    return;
}


int main()
{
    mytest();

    system("pause");
    return 0;
}

 

 

================

来源 https://juejin.im/post/5d692773f265da03986c0832

C++ 线程安全的单例模式总结

什么是线程安全?

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。


如何保证线程安全?

  1. 给共享的资源加把锁,保证每个资源变量每时每刻至多被一个线程占用。
  2. 让线程也拥有资源,不用去共享进程中的资源。如: 使用threadlocal可以为每个线程的维护一个私有的本地变量。

什么是单例模式?

单例模式指在整个系统生命周期里,保证一个类只能产生一个实例,确保该类的唯一性。

单例模式分类

单例模式可以分为懒汉式和饿汉式,两者之间的区别在于创建实例的时间不同:

  • 懒汉式:指系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例。(这种方式要考虑线程安全,需要加 双重检查锁定模式(DCLP) 
  • 饿汉式:指系统一运行,就初始化创建实例,当需要时,直接调用即可。(本身就线程安全,没有多线程的问题)

单例类特点

  • 构造函数和析构函数为private类型,目的禁止外部构造和析构
  • 拷贝构造和赋值构造函数为private类型,目的是禁止外部拷贝和赋值,确保实例的唯一性
  • 类里有个获取实例的静态函数,可以全局访问

01 普通懒汉式单例 ( 线程不安全 )

///////////////////  普通懒汉式实现 -- 线程不安全 //////////////////
#include <iostream> // std::cout
#include <mutex>    // std::mutex
#include <pthread.h> // pthread_create

class SingleInstance
{

public:
    // 获取单例对象
    static SingleInstance *GetInstance();

    // 释放单例,进程退出时调用
    static void deleteInstance();
	
	// 打印单例地址
    void Print();

private:
	// 将其构造和析构成为私有的, 禁止外部构造和析构
    SingleInstance();
    ~SingleInstance();

    // 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
    SingleInstance(const SingleInstance &signal);
    const SingleInstance &operator=(const SingleInstance &signal);

private:
    // 唯一单例对象指针
    static SingleInstance *m_SingleInstance;
};

//初始化静态成员变量
SingleInstance *SingleInstance::m_SingleInstance = NULL;

SingleInstance* SingleInstance::GetInstance()
{

	if (m_SingleInstance == NULL)
	{
	    m_SingleInstance = new (std::nothrow) SingleInstance;  // 没有加锁是线程不安全的,当线程并发时会创建多个实例
	}

    return m_SingleInstance;
}

void SingleInstance::deleteInstance()
{
    if (m_SingleInstance)
    {
        delete m_SingleInstance;
        m_SingleInstance = NULL;
    }
}

void SingleInstance::Print()
{
	std::cout << "我的实例内存地址是:" << this << std::endl;
}

SingleInstance::SingleInstance()
{
    std::cout << "构造函数" << std::endl;
}

SingleInstance::~SingleInstance()
{
    std::cout << "析构函数" << std::endl;
}
///////////////////  普通懒汉式实现 -- 线程不安全  //////////////////

// 线程函数
void *PrintHello(void *threadid)
{
    // 主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收
    pthread_detach(pthread_self());

    // 对传入的参数进行强制类型转换,由无类型指针变为整形数指针,然后再读取
    int tid = *((int *)threadid);

    std::cout << "Hi, 我是线程 ID:[" << tid << "]" << std::endl;

    // 打印实例地址
    SingleInstance::GetInstance()->Print();

    pthread_exit(NULL);
}

#define NUM_THREADS 5 // 线程个数

int main(void)
{
    pthread_t threads[NUM_THREADS] = {0};
    int indexes[NUM_THREADS] = {0}; // 用数组来保存i的值

    int ret = 0;
    int i = 0;

    std::cout << "main() : 开始 ... " << std::endl;

    for (i = 0; i < NUM_THREADS; i++)
    {
        std::cout << "main() : 创建线程:[" << i << "]" << std::endl;
        
        indexes[i] = i; //先保存i的值
		
        // 传入的时候必须强制转换为void* 类型,即无类型指针
        ret = pthread_create(&threads[i], NULL, PrintHello, (void *)&(indexes[i]));
        if (ret)
        {
            std::cout << "Error:无法创建线程," << ret << std::endl;
            exit(-1);
        }
    }

    // 手动释放单实例的资源
    SingleInstance::deleteInstance();
    std::cout << "main() : 结束! " << std::endl;
	
    return 0;
}
复制代码

普通懒汉式单例运行结果:

从运行结果可知,单例构造函数创建了两个个,内存地址分别为0x7f3c980008c00x7f3c900008c0,所以普通懒汉式单例只适合单进程不适合多线程,因为是线程不安全的。


02 加锁的懒汉式单例 ( 线程安全 )

///////////////////  加锁的懒汉式实现  //////////////////
class SingleInstance
{

public:
    // 获取单实例对象
    static SingleInstance *&GetInstance();

    //释放单实例,进程退出时调用
    static void deleteInstance();
	
    // 打印实例地址
    void Print();

private:
    // 将其构造和析构成为私有的, 禁止外部构造和析构
    SingleInstance();
    ~SingleInstance();

    // 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
    SingleInstance(const SingleInstance &signal);
    const SingleInstance &operator=(const SingleInstance &signal);

private:
    // 唯一单实例对象指针
    static SingleInstance *m_SingleInstance;
    static std::mutex m_Mutex;
};

//初始化静态成员变量
SingleInstance *SingleInstance::m_SingleInstance = NULL;
std::mutex SingleInstance::m_Mutex;

SingleInstance *&SingleInstance::GetInstance()
{

    //  这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,
    //  避免每次调用 GetInstance的方法都加锁,锁的开销毕竟还是有点大的。
    if (m_SingleInstance == NULL) 
    {
        std::unique_lock<std::mutex> lock(m_Mutex); // 加锁
        if (m_SingleInstance == NULL)
        {
            m_SingleInstance = new (std::nothrow) SingleInstance;
        }
    }

    return m_SingleInstance;
}

void SingleInstance::deleteInstance()
{
    std::unique_lock<std::mutex> lock(m_Mutex); // 加锁
    if (m_SingleInstance)
    {
        delete m_SingleInstance;
        m_SingleInstance = NULL;
    }
}

void SingleInstance::Print()
{
	std::cout << "我的实例内存地址是:" << this << std::endl;
}

SingleInstance::SingleInstance()
{
    std::cout << "构造函数" << std::endl;
}

SingleInstance::~SingleInstance()
{
    std::cout << "析构函数" << std::endl;
}
///////////////////  加锁的懒汉式实现  //////////////////
复制代码

加锁的懒汉式单例的运行结果:

从运行结果可知,只创建了一个实例,内存地址是0x7f28b00008c0,所以加了互斥锁的普通懒汉式是线程安全的


03 内部静态变量的懒汉单例(C++11 线程安全)

///////////////////  内部静态变量的懒汉实现  //////////////////
class Single
{

public:
    // 获取单实例对象
    static Single &GetInstance();
	
	// 打印实例地址
    void Print();

private:
    // 禁止外部构造
    Single();

    // 禁止外部析构
    ~Single();

    // 禁止外部复制构造
    Single(const Single &signal);

    // 禁止外部赋值操作
    const Single &operator=(const Single &signal);
};

Single &Single::GetInstance()
{
    // 局部静态特性的方式实现单实例
    static Single signal;
    return signal;
}

void Single::Print()
{
    std::cout << "我的实例内存地址是:" << this << std::endl;
}

Single::Single()
{
    std::cout << "构造函数" << std::endl;
}

Single::~Single()
{
    std::cout << "析构函数" << std::endl;
}
///////////////////  内部静态变量的懒汉实现  //////////////////
复制代码

内部静态变量的懒汉单例的运行结果:

-std=c++0x编译是使用了C++11的特性,在C++11内部静态变量的方式里是线程安全的,只创建了一次实例,内存地址是0x6016e8,这个方式非常推荐,实现的代码最少!

[root@lincoding singleInstall]#g++  SingleInstance.cpp -o SingleInstance -lpthread -std=c++0x
复制代码


04 饿汉式单例 (本身就线程安全)

////////////////////////// 饿汉实现 /////////////////////
class Singleton
{
public:
    // 获取单实例
    static Singleton* GetInstance();

    // 释放单实例,进程退出时调用
    static void deleteInstance();
    
    // 打印实例地址
    void Print();

private:
    // 将其构造和析构成为私有的, 禁止外部构造和析构
    Singleton();
    ~Singleton();

    // 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
    Singleton(const Singleton &signal);
    const Singleton &operator=(const Singleton &signal);

private:
    // 唯一单实例对象指针
    static Singleton *g_pSingleton;
};

// 代码一运行就初始化创建实例 ,本身就线程安全
Singleton* Singleton::g_pSingleton = new (std::nothrow) Singleton;

Singleton* Singleton::GetInstance()
{
    return g_pSingleton;
}

void Singleton::deleteInstance()
{
    if (g_pSingleton)
    {
        delete g_pSingleton;
        g_pSingleton = NULL;
    }
}

void Singleton::Print()
{
    std::cout << "我的实例内存地址是:" << this << std::endl;
}

Singleton::Singleton()
{
    std::cout << "构造函数" << std::endl;
}

Singleton::~Singleton()
{
    std::cout << "析构函数" << std::endl;
}
////////////////////////// 饿汉实现 /////////////////////
复制代码

饿汉式单例的运行结果:

从运行结果可知,饿汉式在程序一开始就构造函数初始化了,所以本身就线程安全的


特点与选择

  • 懒汉式是以时间换空间,适应于访问量较小时;推荐使用内部静态变量的懒汉单例,代码量少
  • 饿汉式是以空间换时间,适应于访问量较大时,或者线程比较多的的情况

 

=================

双重检查锁定模式(DCLP)在无锁编程方面是有点儿臭名昭著案例学术研究的味道。直到2004年,使用java开发并没有安全的方式来实现它。在c++11之前,使用便捷式c+开发并没有安全的方式来实现它。由于引起人们关注的缺点模式暴露在这些语言之中,人们开始写它。一组高调的java聚集在一起开发人员并签署了一项声明,题为:“双重检查锁定坏了”。在2004年斯科特 、梅尔斯和安德烈、亚历山发表了一篇文章,题为:“c+与双重检查锁定的危险”对于DCLP是什么?这两篇文章都是伟大的引物,为什么呢?在当时看来,这些语言都不足以实现它。

在过去。java现在可以为修订内存模型,为thevolatileeyword注入新的语义,使得它尽可然安全实现DCLP.同样地,c+11有一个全新的内存模型和原子库使得各种各样的便捷式DCLP得以实现。c+11反过来启发Mintomic,一个小型图书馆,我今年早些时候发布的,这使得它尽可能的实现一些较旧的c/c++编译器以及DCLP.

在这篇文章中,我将重点关注c++实现的DCLP.

什么是双重检查锁定?

假设你有一个类,它实现了著名的Singleton 模式,现在你想让它变得线程安全。显然的一个方法就是通过增加一个锁来保证互斥共享。这样的话,如果有两个线程同时调用了Singleton::getInstance,将只有其中之一会创建这个单例。

1
2
3
4
Singleton* Singleton::getInstance() { Lock lock; // scope-based lock, released automatically when the function returns if (m_instance == NULL) {
        m_instance = new Singleton;
    } return m_instance;
}

这是完全合法的方法,但是一旦单例被创建,实际上就不再需要锁了。锁不一定慢,但是在高并发的条件下,不具有很好的伸缩性。

双重检查锁定模式避免了在单例已经存在时候的锁定。不过如Meyers-Alexandrescu的论文所显示的,它并不简单。在那篇论文中,作者描述了几个有缺陷的用C++实现DCLP的尝试,并剖析了每种情况为什么是不安全的。最后,在第12页,他们给出了一个安全的实现,但是它依赖于非指定的,特定平台的内存屏障(memory barriers)

(译注:内存屏障就是一种干预手段. 他们能保证处于内存屏障两边的内存操作满足部分有序)

1
2
3
4
5
6
7
8
Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance; ... // insert memory barrier if (tmp == NULL) {
        Lock lock;
        tmp = m_instance; if (tmp == NULL) {
            tmp = new Singleton; ... // insert memory barrier m_instance = tmp;
        }
    } return tmp;
}

这里,我们可以发现双重检查锁定模式是由此得名的:在单例指针m_instance为NULL的时候,我们仅仅使用了一个锁,这个锁使偶然访问到该单例的第一组线程继续下去。而在锁的内部,m_instance被再次检查,这样就只有第一个线程可以创建这个单例了。

这与可运行的实现非常相近。只是在突出显示的几行漏掉了某种内存屏障。在作者写这篇论文的时候,还没有填补此项空白的轻便的C/C++函数。现在,C++11已经有了。

用 C++11 获得与释放屏障

你可以用获得与释放屏障 安全的完成上述实现,在我以前的文章中我已经详细的解释过这个主题。不过,为了让代码真正的具有可移植性,你还必须要将m_instance包装成原子类型,并且用放松的原子操作(译注:即非原子操作)来操作它。这里给出的是结果代码,获取与释放屏障部分高亮了。

1
2
3
4
5
6
7
8
9
10
11
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
 
Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire); if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) {
            tmp = new Singleton; std::atomic_thread_fence(std::memory_order_release); m_instance.store(tmp, std::memory_order_relaxed);
        }
    } return tmp;
}

即使是在多核系统上,它也可以令人信赖的工作,因为内存屏障在创建单例的线程与其后任何跳过这个锁的线程之间,创建了一种同步的关系。Singleton::m_instance充当警卫变量,而单例本身的内容充当有效载荷。

所有那些有缺陷的DCLP实现都忽视了这一点:如果没有同步的关系,将无法保证第一个线程的所有写操作——特别是,那些在单例构造器中执行的写操作——可以对第二个线程可见,虽然m_instance指针本身是可见的!第一个线程具有的锁也对此无能为力,因为第二个线程不必获得任何锁,因此它能并发的运行。

如果你想更深入的理解这些屏障为什么以及如何使得DCLP具有可信赖性,在我以前的文章中有一些背景信息,就像这个博客早前的文章一样。

使用 Mintomic 屏障

Mintomic 是一个小型的C语言的库,它提供了C++11原子库的一个功能子集,其中包含有获取与释放屏障,而且它是运行于更老的编译器之上的。Mintomic依赖于这样的假设 ,即C++11的内存模型——特殊的是,其中包括无中生有的存储 ——因为它不被更老的编译器支持,不过这已经是我们不通过C++11能做到的最佳程度了。记住这些东西可是若干年来我们在写多线程C++代码时的环境。无中生有的存储(Out-of-thin-air stores)已被时间证明是不流行的,而且好的编译器也基本上不会这么做。

这里有一个DCLP的实现,就是用Mintomic来获取与释放屏障的。和前面使用C++11获取和释放屏障的例子比起来,它基本上是等效的。

1
2
3
4
5
6
7
8
9
10
11
12
mint_atomicPtr_t Singleton::m_instance = { 0 };
mint_mutex_t Singleton::m_mutex;
 
Singleton* Singleton::getInstance() {
    Singleton* tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance); mint_thread_fence_acquire(); if (tmp == NULL) {
        mint_mutex_lock(&m_mutex);
        tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance); if (tmp == NULL) {
            tmp = new Singleton; mint_thread_fence_release(); mint_store_ptr_relaxed(&m_instance, tmp);
        }
        mint_mutex_unlock(&m_mutex);
    } return tmp;
}

为了实现获取与释放屏障,Mintomic试图在所有它所支持的平台上,生成最有效的机器代码。举个例子,这里是Xbox 360上的机器代码结果,Xbox 360是基于PowerPC的。在这个平台上,单行的lwsync是一条最精简的指令,它既可以获取也可以释放屏障。

如果启用了优化,之前基于C++11的例子也可以(理想情况下将会)生成完全相同的PowerPC机器代码。可惜的是,我并没有找来兼容C++11的PowerPC编译器来证明它。

使用c++ 11低级排序约束

C++11的获取与释放屏障可以正确的实现DCLP,而且应该能够针对当今大多数的多核设备,生成优化的机器代码(就像Mintomic做的那样),但是它们似乎不是非常时髦。在C++11中获得同等效果的首选方法,应该是使用基于低级排序约束的原子操作。正如我先前所说,一条写释放(write-release)可以同步于一条读获取(read-acquire)。

1
2
3
4
5
6
7
8
9
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
 
Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(std::memory_order_acquire); if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) {
            tmp = new Singleton; m_instance.store(tmp, std::memory_order_release); }
    } return tmp;
}

从技术上说,这种无锁的同步形式,比使用独立屏障的形式,要不那么严格;上面的操作只是意味着阻止它们自己周围的内存重新排序,这与独立的屏障不同,后者意味着阻止所有相邻的操作的特定类型的内存重排序。尽管如此,在x86/64, ARMv6/v7,以及 PowerPC架构上,对于这两种形式,可能的最好代码都是相同的。例如,在一篇早前文章中,我演示了在ARMv7编译器上,C++11低级排序约束是如何发送dmb指令的,而这也正是你在使用独立屏障时所期待的同样事情。

这两种形式有可能会生成不同机器代码的一个平台是Itanium。Itanium可以使用一条单独的CPU指令,ld.acq来实现C++11的load(memory_order_acquire),并可以使用st.rel来实现store(tmp, memory_order_release)。我很想研究一下这些指令与独立屏障之间的性能差异,可是我找不到可用的Itanium机器。

另一个这样的平台是最近出现的ARMv8架构。ARMv8提供了ldar和stlr指令,除了它们也增强了stlr指令以及任何后续的ldar指令之间的存储加载排序以外,其它的都与Itanium的ld.acq和st.rel指令很相似。事实上,ARMv8的这些新指令意在实现C++11的SC原子操作,这在后面会讲到。

使用 C++11的顺序一致原子

C++11提供了一种完全不同的方法来写无锁代码。(我们可以认为在某些特定的代码路径上DCLP是“无锁”的,因为并不是所有的线程都具有锁。)如果在所有原子库函数上,你忽略了可选的std::memory_order参数,那么默认值std::memory_order_seq_cst就会将所有的原子变量转变为顺序一致的(sequentially consistent) (SC)原子。通过SC原子,只要不存在数据竞争,整个算法就可以保证是顺序一致的。SC原子Java 5+中的volatile变量非常相似。

这里是使用SC原子的一个DCLP实现。如之前所有例子一样,一旦单例被创建,第二行高亮将与第一行同步

1
2
3
4
5
6
7
8
9
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
 
Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(); if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(); if (tmp == nullptr) {
            tmp = new Singleton; m_instance.store(tmp); }
    } return tmp;
}

SC原子被认为可以使程序员更容易思考。其代价是生成的机器代码似乎比之前的例子效率要低。例如,这里有有一些关于上面代码清单的x64机器代码,由Clang 3.3在启用代码优化的条件下生成:

由于我们使用了SC原子,保存到m_instance是由xchg指令实现的,在x64上它具有内存屏障作用。这比x64中DCLP实际需要的指令更强。只需一条简单的mov指令就可以做这项工作。不过这并不十分要紧,因为在单例首次创建的代码路径上,xchg指令只下发一次。

另一方面,如果你给PowerPC 或 ARMv6/v7编译SC原子指令,你十有八九会得到糟糕的机器代码。其中的细节,请看Herb Sutter的atomic<> 武器说话,第 2部分的00:44:25 - 00:49:16段落。

使用 C++11 的数据相关性排序

在上面所有我给出的例子中,在创建单例的那个线程,与其后任何越过锁的线程之间,有一种同步的关系。警卫变量就是单例指针,有效载荷是单例自身的内容。在本例中,有效载荷被认为是警卫指针的一个相关性数据。

人们后来发现,当存在数据相关性时,上面所有例子中都用到的读获取(read-acquire)操作,将极富杀伤力!我们用消费操作(consume operation)来替代它要好一点。消费操作很酷,因为它们消除了PowerPC中的一条lwsync指令,以及ARMv7中的一条dmb指令。在将来的一篇文章中,我将更多的谈论到有关数据相关性和消费操作的内容。

使用C++11中的静态初始化器

有些读者已经知道这篇文章的妙语:如果你想得到一个线程安全的实例,C++11不允许你跳过以上的所有步骤。你可以简单使用一个静态初始化器

1
2
3
Singleton& Singleton::getInstance() {
    static Singleton instance; return instance;
}

让我们回到6.7.6节查看C++11的标准:

如果控制进入申明同时变量将被初始化的时候,那么并发执行将会等到初始化的完成。

由编译器来临时代替实现的细节,DCLP明显是一个不错的选择。不能保证编译器将会使用DCLP,但一些(也许更多)却碰巧发生了。使用the-std=c++0x选项对ARM进行编译,生成了下面的一些机器码,这些机器码是由GCC 4.6生成的。

由于单例创建于固定地址,为了同步的目的,编译器引进了一个独立的警卫变量。特别需要注意的是,在最初读到这个警卫变量之后,并没有现成的dmb指令可以用来获取内存屏障。警卫变量是指向单例的指针,因此编译器可以利用数据相关性,省略掉这种dmb指令。__cxa_guard_release对警卫变量执行了一个写释放(write-release)操作,这样只要警卫变量已设置,在读消费(read-consume)之前就建立了依赖顺序,就像前面所有例子里那样,基于对内存的重新排序,整个事情开始变得有弹性。

如你所见,我们已伴随C++11走过了一段漫长的道路。双重检查锁定是一种稳定的模式,而且还远不止此!

就个人而言,我常常想,如果是需要初始化一个单例,最好是在程序启动的时候做这个事情。但是显然DCLP可以拯救你于泥潭。而且在实际的使用中,你还可以用DCLP来将任意数值类型存储到一个无锁的哈希表。在以后的文章中会有更多关于它的论述。

 

=============== End

 

以上是关于设计模式创建型-单例模式的主要内容,如果未能解决你的问题,请参考以下文章

创建型模式 单例模式

创建型设计模式(单例模式)

设计模式之美-创建型-单例模式

浅析设计模式——创建型模式之Singleton(单例模式)

23天读懂23种设计模式:单例模式(创建型)

创建型模式:单例模式