饿汉模式,懒汉模式线程安全问题

饿汉模式:
上来不管三七二十一直接创建对象再说。

饿汉模式创建方式:

1.先创建一个私有的构造函数(防止其它地方直接实例化)

2.定义私有变量

3.提供公共的获取实例的方法

复制代码
public class Single {
    //饿汉模式
    //先创建一个表示单例的类
    static class Singleton {
        //创建一个无法在该类外部实例化的私有构造方法
        private Singleton(){};
        //创建一个static成员,表示Singleton的唯一实例
        private static Singleton instance = new Singleton();
        //获取实例的唯一方法
        public static Singleton getInstance() {
            return instance;
        }
    }
}
复制代码

 


懒汉模式:
当程序启动之后并不会进行初始化,在什么时候调用什么时候初始化。

懒汉模式:

复制代码
public class Single1 {
    //懒汉模式
    static class Singleton {
        private Singleton() {}
        private static Singleton instance = null;
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
}
复制代码

 

类加载的时候,没有立刻实例化,第一次调用getInstance()的时候,才真的实例化.

如果要是代码一整场都没有调用getInstance,此时实例化的过程也就被省略掉了,

我们一般成懒汉模式为“懒加载”或者“延时加载”,“懒汉模式"比“饿汉模式"效率更高,有很大的可能是“实例用不到",此时就节省了实例化的开销。

线程安全问题:
了解了懒汉模式和饿汉模式的代码之后,如何分辨懒汉模式和饿汉模式是否线程安全呢,首先确认什么情况会导致线程不安全:

1.线程的调度是抢占式执行.

2.修改操作不是原子的

3.多个线程同时修改同一个变量

4.内存可见性

5.指令重排序

饿汉模式,懒汉模式线程安全吗?
了解了线程安全之后,对于饿汉模式来说,多线程同时调用getInstance(),由于getInstance()里只做了一件事:读取instance实例的地址,也就是多个线程在同时读取同一个变量,并没有构成多个线程同时修改同一个变量这一情况,所以说饿汉模式是线程安全的。

而对于懒汉模式来说,多线程调用getInstance(),getInstance()做了四件事情~

1.读取 instance 的内容

2.判断 instance 是否为 null

3.如果 instance 为null,就 new实例 (这就会修改 intance 的值)

4.返回实例的地址

由于懒汉模式造成了多个线程同时修改同一个变量这一情况,所以说懒汉模式是线程不安全的。

在instance实例化之前调用getInstance(),存在线程安全问题,如果已经把实例创建好了,后面再去并发调用getInstance()就是线程安全的了,而此时也就变成了饿汉模式。

为了解决“多个线程同时修改同一个变量”造成线程不安全的问题,采用加锁的解决方案,这里使用sychronized来解决线程不安全的问题。

复制代码
public class Single2 {
    //懒汉模式
    static class Singleton {
        private Singleton() {}
        privatestatic Singleton instance = null;
        public static Singleton getInstance() {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
            return instance;
        }
    }
 
}
复制代码

 

然而在上面的代码中,即使instance已经实例化了,但是每次调用getInstance()还是会涉及加锁解锁,实际上此时已经不需要了,所以要实现在instance实例化之前调用的时候加锁,之后不加锁,就引出了双重检验锁版本。

复制代码
public class Single2 {
    //懒汉模式
    static 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;
        }
    }
}
复制代码

 

编译器优化:

只有第一次读操作从内存中读取数据同时存放在CPU的寄存器中,因为从寄存器中读取数据速度远大于从内存中读取,所以后续的读操作就直接从寄存器中读取数据。

此时我们已经基本完成懒汉模式的设计,但是懒汉模式在多线程情况下由于编译器优化还会出现一种特殊情况,某个线程可能会进行多次读操作:

线程1第一次读取instance为空,而线程2未释放锁时,加锁失败,与此同时其他线程也读取到instance为空,于是实例化instance后线程2释放锁,此时线程1加锁成功,但读取instance仍然为null。此时的线程2进行了多次读操作,然而由于编译器优化,导致线程2没有读到最新的数据,即实例化的instance。

此时需要使用volatile来解决这种特殊情况带来的问题。

volatile的作用:保持内存可见性,禁止编译器进行某种场景的优化(一个线程在读,一个线程在写,修改对于读线程来说可能没有生效)

懒汉模式最终版本:

复制代码
public class Single2 {
    //懒汉模式
    static class Singleton {
        private Singleton() {}
        //volatile 避免内存可见性引出的问题
        private volatile static Singleton instance = null;
        public static Singleton getInstance() {
            //双重检验锁
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}
复制代码

 

总结:
为了保证懒汉模式线程安全,涉及到三个要点:

1.加锁保证线程安全

2.双重if保证效率

3. volatile避免内存可见性引来的问题.

内容转自 (33条消息) 面试官问我:饿汉模式,懒汉模式线程安全吗?我用这个直接绝杀!__RailGun_的博客-CSDN博客

posted @   tetrachloroethylene  阅读(265)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示