一文彻底搞懂java单例模式

前言:何谓单例模式?

单例(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:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
指令顺序有可能经过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.枚举类可以有效的避免通过反射来实例化;这两个特点来实现安全的单例)

对于单例模式,是否有个清晰点的认识呢?

后续还有很多“终极篇系列”的文章,第一时间发布在微信公众号【渔村IT圈】,欢迎关注。

你可能感兴趣的:(设计模式,java,多线程)