一、饿汉模式
饿汉模式是在类加载的时候就初始化了一份单例对象,所以他不存在线程安全问题。
优点:不存在线程安全问题,天然的线程安全
缺点:在类加载的时候就已经创建了对象,如果后续代码里没有使用到单例,就浪费了资源,通过反射还是可以去new的
public Class Single {
private static Single single = new Single();
private Single() {}
public static Single getInstance() {
return single;
}
}
二、懒汉模式(处理线程安全)
懒汉模式顾名思义懒汉,没有提前去准备,而是用到的时候准备,比如洗碗,饿汉模式是吃完饭立马去洗,不管下次是点外卖还是家里吃(用到洗的碗)。懒汉模式就是,不要管下一顿从外卖还是家里做,啥时候用到碗啥时候洗。代码1如下:
public class Single {
private static Single single;
private Single(){}
public static Single getInstance(){
// 如果是第一次获取就创建
if (single == null){
single = new Single();
}
return single;
}
}
此时如果两个线程同时去调用了这个getInstance方法,两个线程线拿single去与null比较,当线程1比较之后if成立,此时线程2也比较成立都进入if,此时就new出来了两个对象,实例创建出来了多个。此时我们发现我们需要通过加锁来把这个比较与new操作打包成原子性的,代码2如下:
public class Single {
private static Single single;
private Single(){}
public static Single getInstance(){
// 加锁
synchronized (Single.class){
if (single == null){
single = new Single();
}
}
return single;
}
}
此时我们就解决了上述两个线程同时进入if 里new出多个实例的问题,解决了实例还没有被创建时的线程安全问题,但是如果实例已经被创建好了之后,后续多个线程去调用获取实例时,依旧要去加锁,但是此时加锁的开销大但没有线程问题,所以此时我们需要在锁的外层再加一个判断,判断是第一次创建实例还是实例已经创建线程是直接调用。代码3如下:
public class Single {
private static Single single;
private Single(){}
public static Single getInstance(){
if (single == null){
// 如果是第一次去创建实例,就加锁解决线程安全问题
synchronized (Single.class){
if (single == null){
single = new Single();
}
}
}
return single;
}
}
上述代码解决了后续访问加无效锁开销大的问题,此时如果实例没有创建,有两个线程调用这个方法,线程1拿到了锁进入第二层if去new对象,new的本质上有三步操作(1.申请内存 2.调用构造方法初始化实例 3.把内存的首地址给single引用)在单线程的情况下指令2与3先执行哪个效果都一样,在多线程情况下,如果发送指令重排序执行步骤为1 3 2,在线程1执行了1 3后在执行2之前(也就是single这个引用他有了地址但是内存上的数据无效),此时线程2调用了这方法,在第一层if里判断时single非空不进入if直接return,就返回了一个不完整的对象。后续使用这个单例对象进行解引用,就会出现问题。要解决这个指令重排序带来的线程安全问题我们可以使用volatile这个关键字,它禁止了指令重排序,它既能够保证内存可见性,也能解决指令重排序问题,最终代码如下:
public class Single {
private static volatile Single single;
private Single(){}
public static Single getInstance(){
// 如果是第一次获取就创建
synchronized (Single.class){
if (single == null){
single = new Single();
}
}
return single;
}
}
此时我们可以回顾一下:懒汉模式单例模式为什么要加锁?为什么有两个if? 为什么要加volatile关键字?
三、静态内部类
类在初次加载的时候,会初始化静态变量、静态代码块、静态方法但是不会加载静态内部类。静态内部类只有在使用的时候才会被加载,因此在使用静态内部类实现单例模式的时候,是满足懒加载的。而且这种实现方式,在获取单例时,是只获取不去修改的,所以他跟饿汉模式一样是天然线程安全的。他既有饿汉模式的优点,也有懒汉模式的优点。
public class Single {
private Single() {}
private static class SingleHolder {
private static final Single single = new Single();
}
public static final Single getInstance() {
return SingleHolder.single;
}
}
四、枚举
枚举也是在第一次被调用的时候才会去加载,因此他也满足懒加载,并且他也是只获取单例对象,并不能修改,所以他也是线程安全的
public enum Single {
SINGLE;
public Single getInstance(){
return SINGLE;
}
}