毕业多年回头再看单例模式

343 阅读4分钟

这是我参与更文挑战的第9天,活动详情查看: 更文挑战

一、什么是单例模式?

单例对象的类只允许有唯一的一个对象实例。

其实上面这句话概括就够了,但是当年刚开始学java的时候,总是过一段时间就忘掉单例模式。后来在知乎上看到这样一个回答:

image.png

这个比喻其实很好,但是总感觉哪里不对,然后我点开评论:

image.png

心疼答主一秒钟2333,让我记住单例模式的反而是下面这个回答:

image.png

万物皆对象,我们每个人都可以看成一个类,每个人心里都有一个无法忘怀的人。无论你在哪里,在任何时间,因为任何事情想起她,她都是她。

二、怎么实现一个单例模式

单例模式的实现其实主要就是通过私有化构造方法,使其他类的方法不能通过直接new的方式来创建获取这个对象,然后在该类内部写一个静态方法用于获取这个唯一的实例对象。

饿汉式是最简单,最安全的做法:

/**
 * 饿汉模式
 * 类加载到内存后就实例化一个单例,jvm保证线程安全
 */
public class MySingleton {

  private static MySingleton singleton = new MySingleton();

  private MySingleton() {

  }

  public static MySingleton getInstance() {
    return singleton;
  }

}

这种做法唯一的缺点就是不管我们使用与否,这个类都会完成实例化。可能造成一点内存浪费。

那么懒加载要怎么实现呢?

public class MySingleton {

    private static MySingleton singleton;

    private MySingleton() {

    }

    public static MySingleton getInstance() {
        if (singleton == null) {
            singleton = new MySingleton();
        }
        return singleton;
    }
}

这种写法就可以实现懒加载,但是在多线程情况下会有安全问题。 比如两个线程同时请求,thread1 判断singleton==null返回true,但是此时thread1停了,thread2进来,判断singleton==null返回的也是true,那么thread1和thread2都会往下执行,都去new了一个对象实例,那么这个类就有可能存在多个实例。

那怎么解决这种写法的线程安全问题呢? 最简单的做法就是在方法上加synchronized关键字,用来保证线程安全,但是这种做法的粒度太粗了,我们首先可以想到减小同步代码块用以提高效率。

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

但是这么做,会导致和上面例子相同的问题,并不是线程安全的。

那么就可以衍生出DCL(double check lock)单例的写法,就是在synchronized关键字内部再进行一次为空判断:

public class MySingleton {

    private static volatile MySingleton singleton;

    private MySingleton() {

    }

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

多个线程只能依次执行synchronized关键字里面的方法,如果thread1先获取到锁,thread2只能等到thread1实例化对象完成后再进入,这时候里面的判断这个对象实例不为空了,thread2就会直接return。

第一层的为空判断其实主要是为了提高我们的效率,相对于判断操作,加锁的操作是属于比较重的操作,如果有很多个线程参与锁竞争,还是会有一定的消耗。所以第一层的判断是非常有必要的。

最后一个问题,为什么要加volatile关键字?

这里我们需要先理解singleton = new MySingleton();执行的时候都做了哪些操作?

  1. 为我们new出来的对象分配内存
  2. 初始化对象
  3. 将对象引用singleton指向第一步分配的内存地址。

这里的第二第三步是有可能发生指令重排序的, 还是上面的例子,我们的thread1执行到singleton = new MySingleton();这行代码的时候,2,3发生了指令重排序,那么先执行到3的时候,我们的singleton对象引用已经不是null了,thread2就会直接拿到这个半初始化的实例对象。在生产环境中有可能引起很大的问题,所以必须要加volatile关键字,这个关键字除了保证可见性,另外一个功能就是防止指令重排序。

以上,感谢阅读。