单例模式是常用的软件设计模式之一,同时也是设计模式中最简单的形式之一,在单例模式中对象只有一个实例存在。单例模式的实现方式有两种,分别是懒汉式和饿汉式。
1、饿汉式
饿汉式在类加载时已经创建好实例对象,在程序调用时直接返回该单例对象即可,即在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建,如下是饿汉式的代码:
public class Singleton {
private static Singleton singleton = new Singleton();
/**
* 私有化构造方法
*/
private Singleton(){
}
/**
* 直接调用方法获取单例对象
*/
public static Singleton getSingleton() {
return singleton;
}
}
如果创建的单例对象比较大,由于在类加载的过程中就加载了,那么会影响应用程序启动的速度;饿汉式不会存在线程安全的问题,因为类加载的过程中就创建了,并且程序只会运行一次。
2、懒汉式
懒汉式指全局的单例实例在第一次被使用时再创建,后面就不会创建实例对象,代码如下所示:
public class Singleton {
private static Singleton singleton;
/**
* 私有化构造方法
*/
private Singleton(){
}
/**
* 判断当前的对象是否存在,如果存在就直接返回,如果不存在就创建一个对象
*/
public static Singleton getSingleton() {
if (singleton==null) {
singleton = new Singleton();
}
return singleton;
}
}
在高并发下,上述的懒汉式会存在线程安全问题(会创建多个实例对象),如下所示:
图片
如果线程1和线程2同时调用getSingleton方法时候,并且都通过了第17行代码的检查,那么线程1和线程2就创建了两个对象出来了。那么懒汉式如何保证线程安全呢?
2.1方法级别锁
图片
如果直接在方法级别上增加锁,可以解决线程安全的问题,但是在高并发下会导致性能下降(因为多个线程执行到这个方法之后就会阻塞等待锁资源)。
众所周知,锁是用来锁住临界资源(即就是多线程同时竞争的资源),在懒汉模式中临界资源是创建对象这段代码(singleton = new Singleton();),那么锁只需要锁住创建对象的代码就可以了。
2.2同步代码块的锁
图片
这里为什么要加一个先判断空的操作呢?目的是为了提升性能,因为一旦对象创建好了之后,后面的线程直接判断对象是否创建好了,创建好了之后在高并发下线程就不需要在锁位置阻塞等待了。但是这种方式在高并发下也是存在线程安全的问题,如下所示:
图片
假设线程1和线程2同时到了17行代码处,并且当前的对象也是null,此时线程1先获取到锁,线程1下创建了一个新对象完成后锁释,随后线程2获取到锁后也创建了一个对象,那么这就无法保证只有一个单例对象。所以锁内部还需要再增加一个检查,如下所示:
图片
当上一个获取锁的线程创建对象成功之后,下一个线程获取到锁的时候,再去判断一下这个对象是否创建成功,如果创建成功就不再创建新的对象。这种方法我们称为Double Check Lock,完整的代码如下所示:
public class Singleton {
private static Singleton singleton;
/**
* 私有化构造方法
*/
private Singleton(){
}
/**
* 判断当前的对象是否存在,如果存在就直接返回,如果不存在就创建一个对象
*/
public static Singleton getSingleton() {
if (singletnotallow==null) {
synchronized(Singleton.class) {
if (singletnotallow==null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
但是在高并发这块还是存在线程的安全问题,因为创建对象的过程有三个步骤,如下所示:
(1)内存分配和赋予默认值
图片
(2)执行初始化方法赋予初始化值
图片
(3)建立指针指向堆上对象
图片
如果在高并发下,假设步骤2和步骤3发生了指令重排,此时就可能会出现如下的情况:
图片
栈里面的指针指向堆上的对象Singleton,但是现在由于指令重排指向一个空(空表示内存上真的什么都没有,连对象都解析不出来),这就出现一系列的问题,所以这里我们就需要禁止指令重排,所以我们需要添加volatile关键字来限制执行重排。
图片
完整的代码的如下所示:
public class Singleton {
private static volatile Singleton singleton;
/**
* 私有化构造方法
*/
private Singleton(){
}
/**
* 判断当前的对象是否存在,如果存在就直接返回,如果不存在就创建一个对象
*/
public static Singleton getSingleton() {
if (singletnotallow==null) {
synchronized(Singleton.class) {
if (singletnotallow==null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
总结:
(1)饿汉式在类加载的时候就实例化,并且创建单例对象,饿汉式无线程安全问题。
(1)懒汉式默认不会实例化,外部什么时候调用什么时候创建。懒汉式在多线程下是线程不安全的,所以我们可以通过双重检查的方式来保证其线程安全问题。