单例模式成型版

单例模式可以说是最容易理解的模式了,也是应用最广的模式之一,先看看定义吧。


定义

确保单例类只有一个实例,并且这个单例类提供一个函数接口让其他类获取到这个唯一的实例。

什么时候需要使用单例模式呢:如果某个类,创建时需要消耗很多资源,即new出这个类的代价很大;或者是这个类占用很多内存,如果创建太多这个类实例会导致内存占用太多。

关于单例模式,虽然很简单,无需过多的解释,但是这里还要提个醒,其实单例模式里面有很多坑。我们去会会单例模式。最简单的单例模式如下:

public class TestSingle {

    private static TestSingle instance = null;

    private TestSingle(){}

    public static TestSingle getInstance(){
        if (instance== null){
            instance = new TestSingle();
        }
        return instance;
    }

}
如果是单线程下的系统,这么写肯定没问题。可是如果是多线程环境呢?这代码明显不是线程安全的,存在隐患:某个线程拿到的 instance可能是 null,可能你会想,这有什么难得,直接在 getInstance()函数上加 sychronized关键字不就好了。可是你想过没有,每次调用 getInstance()时都要执行同步,这带来没必要的性能上的消耗。注意,在方法上加 sychronized关键字时,一个线程访问这个方法时,其他线程无法同时访问这个类其他 sychronized方法。的我们看看另外一种实现:

public class TestSingle {

    private static TestSingle instance = null;

    private TestSingle() {
    }

    public static TestSingle getInstance() {
        if (instance == null) {
            synchronized (TestSingle.class) {
                if (instance == null) {
                    instance = new TestSingle();
                }
            }
        }
        return instance;
    }

}

为什么需要2次判断是否为空呢?第一次判断是为了避免不必要的同步,第二次判断是确保在此之前没有其他线程进入到sychronized块创建了新实例。这段代码看上去非常完美,但是,,, 却有隐患!问题出现在哪呢?主要是在 instance=new Singleton();这段代码上。这段代码会编译成多条指令,大致上做了3件事:

(1)给Singleton实例分配内存
(2)调用Singleton()构造函数,初始化成员字段
(3)将instance对象指向分配的内存(此时instance就不是null啦~)

上面的(2)和(3)的顺序无法得到保证的,也就是说,JVM可能先初始化实例字段再把instance指向具体的内存实例,也可能先把instance指向内存实例再对实例进行初始化成员字段。考虑这种情况:一开始,第一个线程执行instance=new Singleton();这句时,JVM先指向一个堆地址,而此时,又来了一个线程2,它发现instance不是null,就直接拿去用了,但是堆里面对单例对象的初始化并没有完成,最终出现错误~ 。

看看另外一种方式:


public class TestSingle {

    private volatile static TestSingle instance = null;

    private TestSingle() {
    }

    public static TestSingle getInstance() {
        if (instance == null) {
            synchronized (TestSingle.class) {
                if (instance == null) {
                    instance = new TestSingle();
                }
            }
        }
        return instance;
    }

}

相比前面的代码,这里只是对 instance变量加了一个 volatile关键字 volatile关键字的作用是:线程每次使用到被 volatile关键字修饰的变量时,都会去堆里拿最新的数据。换句话说,就是每次使用instance时,保证了instance是最新的。注意: volatile关键字并不能解决并发的问题,关于 volatile请查看其它相关文章。但是 volatile能解决我们这里的问题。




































发布了124 篇原创文章 · 获赞 141 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/weixin_36838630/article/details/76253478