怎么写一个“完美”的单例模式

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhujiangtaotaise/article/details/50598329

单例模式大家接触甚至都写过好多了,那么是不是所有的单例模式都堪称完美呢?呵呵,不一定。我之前写的一个单例模式就很有问题,什么问题呢?大家请看我写代码:

class SingleTon {
    private static SingleTon mSingleTon;

    private SingleTon() {

    }

    public static SingleTon getInstance() {
        if (mSingleTon == null) {
            mSingleTon = new SingleTon();
        }
        return mSingleTon;

    }
    ...
    代码省略
    ...
}

如果实在多线程中呢?接着分析下。假设有2个线程,线程a和线程b,线程a执行到语句9还没有执行完语句10,而这时候线程b执行到语句9,由于线程a还没有实例化mSingleTon变量,所以线程b执行到语句9的时候mSingleTon 是 null,以至于这两个线程都会实例化各自的变量。那么你的单例模式就不再是“单例”了。

那么该怎么写呢?大家都会想到想到用synchronized 同步快或同步方法。那么具体应该怎么写呢?我们改进的代码如下:

public static SingleTon getInstance() {
        if (mSingleTon == null) {
            synchronized (SingleTon.class) {
                if (mSingleTon == null) {
                    mSingleTon = new SingleTon();
                }
            }
        }

        return mSingleTon;
    }

分析下: 假设线程a和线程b同时执调用getInstance()方法,由于这时候mSingleTon == null,所以a和b线程都满足语句2的if语句。然后假设线程a获得了SingleTon类的锁而进入lock语句(synchronized (SingleTon.class)),此时线程b只能在lock语句外等待,直到线程a执行完synchronized 同步块的代码而释放锁。线程a执行到语句4的时候,由于此时mSingleTon 仍然是null,所以执行语句5,对mSingleTon 变量实例化。完成后,线程a释放SingleTon类的锁。线程b进入lock语句,执行语句4,由于线程a已经对mSingleTon 进行实例化了,此时mSingleTon 不再是null,从而执行语句10,返回SingleTon的实例。

大家想一想,如果没有最外面的if (mSingleTon == null)这条判断语句行不行?

public static SingleTon getInstance() {
            synchronized (SingleTon.class) {
                if (mSingleTon == null) {
                    mSingleTon = new SingleTon();
                }
            }

        return mSingleTon;
    }

答案是肯定的。分析下,当有2个线程同时调用getInstance()方法,其中一个线程获取锁而进入lock语句,另外一个线程等在lock语句外面,进入lock语句的线程对mSingleTon实例化完成后释放锁。另外一个线程进入lock语句,由于此时mSingleTon 不为null,所以直接返回mSingleTon。

既然不要嘴外层的if语句单例模式仍然是正确的,那么我们为何还要“画蛇添足”呢?

这就涉及到性能问题了,如果不要最外层的if语句,那么每次调用getInstance()方法时,都会执行synchronized (SingleTon.class),这是非常耗性能的。而加上了最外层的if语句后,那么就只有在第一次,也就是 singleTton == null 成立时的情况下执行一次锁定以实现线程同步。

好了,这篇写完了,欢迎指正。
随着后来对单例使用的更多,发现我上面写的还是有问题的,需要在这一行 private static SingleTon mSingleTon 代码加上 violate关键字。

为什么呢?
因为初始化一个对象并使一个引用指向它并不是原子操作的,导致了可能会出现引用指向了对象并未初始化好的那块堆内存,使用volatile修饰对象引用,防止重排序即可解决。

举个例子:

student = new Student(); 这行代码大致可分为如下4个步骤:
1. 栈内存开辟空间给 student
2. 堆内存开辟空间准备初始化对象
3. 初始化对象
4. 栈中引用指向这个堆内存空间地址

指令重排之后可能会是1、2、4、3;这样重排之后对单个线程来说效果是一样的,所以JVM认为是合法的重排序,但是在多线程环境下就会出问题,这里到4的时候help已经指向了一块堆内存!=null ,只是这块堆内存还没初始化就直接返回了,使用的时候抛NullPointException。使用volatile修饰对象引用,防止重排序即可解决。

推荐一种更好的实现单例的方法。
饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。而使用静态内部类的方式来实现既可以实现延迟加载,又可以保证线程安全,不影响系统性能。

例子如下:

class Singleton {  
    private Singleton() {  
    }  

    private static class HolderClass {  
            private final static Singleton instance = new Singleton();  
    }  

    public static Singleton getInstance() {  
        return HolderClass.instance;  
    } 
}  

由于静态内部类没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。

猜你喜欢

转载自blog.csdn.net/zhujiangtaotaise/article/details/50598329