Java单例模式学习记录

在项目开发中常常能碰见的设计模式就是单例模式了,而实现的方式最多见的有两种:饿汉和饱汉(懒汉)。因为平常接触较多而研究的不够深刻,致使面试的时候被询问到后有点没底,这里记录一下学习的过程。java


饿汉实现

饿汉的名字由来就是由于很饿很着急,因此在类加载时即建立实例对象,实现以下:面试

public class Singleton {
	
	private static final Singleton singleton = new Singleton();
	
	private Singleton(){
		
	}
	
	public static Singleton getInstance(){
		return singleton;
	}

饿汉模式自己就是线程安全的,为何是线程安全的呢?缘由是这样的,JVM虚拟机在执行类加载的初始化阶段,能保证一个类的<clinit>方法在多线程环境下可以被正确的加锁,同步,若是多线程初始化一个类,那么只有一个线程会去执行这个类的<clinit>方法,其余须要阻塞,更况且咱们还加入了final关键字,若是某个成员是final的,JVM规范作出以下明确的保证:一旦对象引用对其余线程可见,则其final成员也必须正确的赋值了。设计模式

所以居于上述两点可以保证饿汉单例正确的在多线程环境下运行。安全


饱汉实现

饱汉的实现跟饿汉不一样,饱汉只在调用获取实例的时候才会进行new对象的过程,简单的实现以下:多线程

public class Singleton {

    private static Singleton singleton;

    private Singleton() {

    }

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

在单线程的环境中,使用该模式是彻底没有问题的,不会涉及到临界问题,而在多线程模式下,那么就不能保证了。假设有两个线程A和B,A线程判断singleton==null了,这时候进行singleton = new Singleton()操做,在该步尚未完成时,线程B进入了方法体中,判断singleton==null,因为A尚未实例化完成Singleton,致使singleton==null成立,B线程也执行了singleton = new Singleton()的操做,那么就不能保证在只有单次赋值的状况了,也就不能保证每一个线程中的Singleton对象是同样的。并发

那么改进方式也很简单,既然有临界问题,那么咱们就加个锁来保证线程的安全性问题:app

public class Singleton {

    private static Singleton singleton;

    private Singleton() {

    }

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

这个方式就能保证单例模式的正常使用了,可是因为咱们每次调用getInstance()的时候都要进行加锁/解锁的操做,在多线程中,在CPU调度切换不一样线程时候会发生上下文切换,上下文切换时候,JVM须要去保存当前线程对应的寄存器使用状态,以及代码执行的位置等等,那么确定是会有必定的开销的。并且当线程因为等待某个锁而被阻塞的时候,JVM一般将该线程挂起,挂起线程和恢复线程都是须要转到内核态中进行,频繁的进行用户态到内核态的切换对于操做系统的并发性能来讲会形成不小的压力。所以上面的写法实际上相对来讲较为低效,那么,这个时候咱们进行优化变成以下代码:性能

public class Singleton {

    private static Singleton singleton;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (singleton == null) {//1
            synchronized (Singleton.class) {
                if (singleton == null) {//2
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

在调用synchronized前提早判断一步是否singleton == null,若是不等于null,那么说明已经赋值成功,若是等于null,那么在执行加锁操做就能够了。因此加两次判空的主要缘由就是由于避免重复加/解锁的操做,浪费系统资源。学习

那么上面的实现还会不会有问题呢?首先分析一下 singleton = new Singleton()这句话底层执行的过程:优化

  1. 在堆中分配Singleton对象内存

  2. 填充Singleton对象的必要信息+具体数据初始化+末位填充

  3. 把singleton引用指向这个对象的堆内地址

自己 singleton = new Singleton()不是一个原子操做,实例化过程会通过上面的三个步骤,并且JVM在遵照as-if-serial语义的状况下,容许进行指令重排序的过程,也就是能够执行1-3-2的操做的。

那么在一些极端的状况就可能会出现问题:

  • 线程A和线程B同时访问getInstance()方法,首先A先访问步骤1,因为第一次访问,因此确定会走到singleton = new Singleton()中,这时候JVM进行了重排序优化1-3-2的过程。
  • 线程B在线程A实例化single的时候恰巧走到了步骤1当中,同时线程A中在执行3,即把singleton引用指向这个对象的堆内地址,因为这时候在锁外访问的步骤1,不遵循happen-before原则,线程B看到singleton引用不为空了,那么就直接返回singleton引用了,那么代码就出不符预期的问题。

解决方式也简单,使用volatile,经过volatile的语义禁止指令重排序功能,那么就解决了上面的问题了,正确代码以下:

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
相关文章
相关标签/搜索