单例模式

前言

  单例模式,这个最简单的设计模式,有无数开发者在网络上写过样本,我相信只要混过的,都能闭着眼睛把单例写出来,并不稀奇。

  但是很多人写单例,都是背着写出来的,认为写法是固定的,其实并非如此。

是戴套还是结扎?

  很多夫妻都会遇到的问题:怎样确保只生一个小孩?

  我认为,摆在面前的有两种方式:戴套和结扎。戴套是在外面处理,结扎是在内部处理,都可以达到这个效果,但是哪种更优呢?

  接下来我们就探讨一下,单例模式所要解决的问题,与解决问题的思路演变。

方案一:调用方处理

原始类:

public class User {
}  

我要确保这个User类只实例化一次,我可以这样子做:

    public static User user = null;

    public static void main(String[] args) {
        if(null == user){
            user = new User();
        }
        User user1 = user;
        User user2 = user;
    }

  这样是不是只实例化一次了? 用一个变量来保存User对象,如果变量是Null就实例化User,(也可以采用反射来实现)。

 

  问题:

  如果这样子做的话,程序其他地方也要使用这个对象该怎么办?

  必须使用全局变量来保存该对象,得靠程序之间的约定才能保持单例。

 

  然而,全局变量会鼓励开发人员,用许多全局变量指向许多小对象造成不必要的引用污染,并且,对于调用方来说,很累,保持单例,到底是你的事情还是我的事情?

 

  显然,单例模式倾向于内部处理,在外部处理就是普通的程序逻辑处理,并不能称作模式。

方案二:内部处理

改写后的User类:

public class User {
    private static User user = null;
    private User() {
    }
    public static User getInstance(){
        if(null == user){
            user = new User();
        }
        return user;
    }
}  

  一般来说,我们使用的单例模式都是这个样子,私有静态变量保存实例、私有构造器拒绝构造、公有静态方法对外提供实例。

  但是当多线程的情况下就会出问题了,当线程A执行到user = new User()这行代码的时候,但还没获得对象(对象的初始化是需要时间的),此时线程B执行到if(null == user)判断,那么线程B的判断结果也是真,于是两个线程都进去各自new了一个User对象,内存中就出现了两个对象。

第一次优化:同步锁

为了解决多线程场景下单例出现多个实例的问题,我们把getInstance()方法上一个同步锁:

    public synchronized static User getInstance(){
        if(null == user){
            user = new User();
        }
        return user;
    }  

  不管出现多少个线程,全部给我排队,等上一个线程离开该方法之后,才可进入,这样即可解决问题。

 

  但是这样又衍生出了问题,因为严格来说,只有第一次实例化这个对象的时候需要线程同步,避免出现多个实例,一旦User对象被实例化出来之后,就不需要对多线程进行同步了,同步一个方法可能造成程序执行效率下降一百倍,每次同步会严重拖垮程序性能。

 

  所以同步锁虽然解决了多线程问题,但是付出了性能作为代价,这并不是最优的方案。

第二次优化:双重检查加锁

既然在方法上加锁得不偿失,那么我就先判断是否是null,是null之后我再对该对象加锁:

public class User {
    private volatile static User user = null;
    private User() {
    }
    public static User getInstance(){
        if(null == user){
            synchronized (User.class){
                if(null == user){
                    user = new User();
                }
            }
        }
        return user;
    }
}

  只有第一次实例化的时候user才是null,所以进入代码块只有一次,进去之后,再让所有的线程同步,同步之后再检查一下user是不是null。

 

  这里再次检查有什么意义呢?有意义。

  请注意user变量的定义多了一个volatile关键字,该关键字可以让所有使用该变量的线程都能实时更新变量的最新状态。

  运行流程:线程AB同时进入第一个判断,然后同步,A先进synchronized,B再外面等,A在里面new了User对象,退出,B再进,此时如果不判断的话,B也会再次new一个,正因为user变量是用volatile来定义的,所以Anew了对象后,B线程的user对象也会更新到最新值,也就不等于null了。

 

  这次优化完美解决了多线程的问题,但是仍然感觉有问题,问题就是怪怪的。。。。

  因为解决的方案并不优雅,两个相同的判断,中间再插一根同步锁,显得这段代码对程序员来说有点像修bug填坑,并非是一个漂亮的设计模式。

第三次优化:饿汉式单例

  我们从根本上思考:多线程问题主要是由于user对象是Null,多个线程同时去获取实例才引发的麻烦。

  如果user对象从一开始就不是Null呢?

  好主意!

 

立刻改写代码:

public class User {
    private final static User user = new User();
    private User() {
    }
    public static User getInstance(){
        return user;
    }
}  

  直接在变量初始化的时候就实例化对象,利用这个做法,JVM在加载这个类时就会创建User这个对象,并且特意加了final关键字,则user变量的值一旦在初始化之后便不能更改。

  这种单例模式也被称作饿汉式单例,因为程序启动后,尽管没有线程来访问,内存中也已经存在了User对象。

  

  经过两个方案,三次优化,目前,这种饿汉写法的单例可以被称作简单优雅并无副作用的单例模式。

 

posted @ 2019-01-29 13:58  不该相遇在秋天  阅读(743)  评论(3编辑  收藏  举报