vlambda博客
学习文章列表

一文彻底搞懂单例模式

前言:何谓单例模式?


单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例。本文介绍3种常见懒汉式+2种常见饿汉式+1种静态内部类实现方式(懒汉式)+枚举实现(饿汉式)。


一、3种常见懒汉式

* 第一版(线程不安全)(懒汉式)

public class Singleton { private Singleton() {} //私有构造函数 private static Singleton instance = null; //单例对象 //静态工厂方法 public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}

以上例子,显然在并发的时候是线程不安全的,因为假如两个线程同时判断【instance==null】,那么都会走到new Singleton()这一步,然后拿到两个不同的对象引用。


* 第二版(线程安全,但有可能返回一个没有初始化完成的instance对象)(懒汉式)

public class Singleton { private Singleton() {} //私有构造函数 private static Singleton instance = null; //单例对象 //静态工厂方法 public static Singleton getInstance() {  if (instance == null) { //双重检测机制 synchronized (Singleton.class){ //同步锁 if (instance == null) { //双重检测机制 instance = new Singleton(); } } } return instance; }}

像这样两次判空的机制叫做双重检测机制。有人可能会问,为啥不直接对getInstance方法加锁,这样就不用双重检测,只要一个检测了?其实这里是为了提高效率,如果不为null,就没有必要再去获取锁释放锁了。但是仍然有一个小问题。这里涉及到JVM指令重排。

java中简单的一句instance = new Singleton()会被编译器编译为如下指令:

memory =allocate(); //1:分配对象的内存空间

ctorInstance(memory); //2:初始化对象

指令顺序有可能经过jvm和cpu的指令重排,导致2和3对调,这样的话可能出现对象不为null但是实际上还未完成初始化,这样的对象return回去,也会出现问题(其实当多个线程要共用一个对象时都应该注意这个问题)。为了避免这种情况,出现了以下第三版这种写法。


* 单例模式第三版(加个volatile修饰,防止重排序)(懒汉式)

public class Singleton { private Singleton() {} //私有构造函数 private volatile static Singleton instance = null; //单例对象 //静态工厂方法 public static Singleton getInstance() { if (instance == null) { //双重检测机制 synchronized (Singleton.class){ //同步锁 if (instance == null) { //双重检测机制 instance = new Singleton(); } } } return instance; }}

以上,第三版就解决了指令重排的问题。


二、2种常见饿汉式

以上三种都说懒汉式,另外,还有两种是饿汉式的(其实都是利用classloader在初始化的时候先加载static属性或static块的机制来实现的),如下:

饿汉1:public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; }}


这种基于classloder机制避免了多线程的同步问题,初始化的时候就给装载了。但是现在,没有懒加载的效果了。这是最简单的一种实现。


饿汉2(变种):public class Singleton { private static Singleton instance = null; static { instance = new Singleton(); } private Singleton (){} public static Singleton getInstance() { return instance; }}


和上面饿汉1差不多,都是在本类初始化即实例化instance。


三、1种静态内部类实现方式

那么除了以上实现方式,单例是否还有其他的实现方式呢?答案是肯定的:可以通过静态内部类实现单例模式。

用静态内部类实现单例模式:(懒汉式)

public class Singleton { private static class LazyHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static Singleton getInstance() { return LazyHolder.INSTANCE; }}


这里有几个需要注意的点:

1.从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。

2.INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。(在调用的时候才会加载静态内部类)


四、用枚举实现单例模式

扩展:

1. 单例模式有个公共的问题,无法防止反射来重复构建对象(因为反射可以获取到类的私有构造方法),这个怎么避免呢?

答案是可以用枚举来实现单例(饿汉式),如下:

class Resource{}public enum SomeThing { INSTANCE; private Resource instance; SomeThing() { instance = new Resource(); } public Resource getInstance() { return instance; }}

上面的类Resource是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。获取资源的方式很简单,只要 SomeThing.INSTANCE.getInstance() 即可获得所要实例。(其实就是利用了1.枚举类的构造函数只会执行一次;2.枚举类可以有效的避免通过反射来实例化;这两个特点来实现安全的单例)