上一篇博文讲了有关java和内存那些事情,今天来延申一下,结合设计模式的单例模式,来说说线程安全那些事情。
想要解锁更多新姿势?请访问https://tengshe789.github.io/
单例模式
单例模式大家应该都不陌生,为了保证系统中,应用的类一个类只有一个实例。传统课本上单例模式分两种,一种饿汉式,一种懒汉式。对应的代码如下:
懒汉式
1 | /** |
懒汉式的实例是在第一次使用时创建的,相应的静态工厂办法会先判断有没有实例,没有实例在进行创建。
然而这种创建方法时线程不安全的,如果有两个线程,同一时刻拿到单例对象,要去静态工厂办法访问,由于工厂办法没有锁,那么很有可能这两个线程最终会拿到两个实例。
饿汉式
1 | /** |
相对于上面那种懒汉式,饿汉式是线程安全的。直接将单例对象用static修饰,把实例对象放到堆内存中,保证了多个线程在访问时的可见性。但是缺点也是很大的,正是由于把实例对象放到堆内存中,这样应用一加载就会看到对应实例,极大浪费内存。
尝试用synchronized改造懒汉式
插一句嘴,synchronized
底层原理,主要是两个指令实现的,分别是monitorenter
和monitorexit
指令,下面是我从网络上找到的对应指令的解释:
monitorenter
:每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit
:执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
毫无疑问,懒汉式的性能是出色的,我们为什么不在懒汉式的基础上使用synchronized
修饰呢?
下面是相关代码:
1 | /** |
加了synchronized
修饰后的工厂方法,意味着在同一时间内只允许一个线程访问。这毫无疑问是线程安全的。但是这同时是不被推荐的,为什么呢?和上面使用static
修饰的懒汉模式不同,这个工厂方法,在同一时间段内只允许一个线程访问,极大的限制cpu资源,性能极其差!
双重同步锁单例模式
那么我们吸取上面的教训,可不可以在使用synchronized
修饰基础上在加以改进呢?
1 | /** |
很多人会认为这种办法是最佳的解决办法了,其实不是,这也是线程不安全的。怎么说呢?
当线程进入同步锁,走到instance = new SingletonExample4();
时,JVM会进行如下操作:
- memory = allocate() 分配对象的内存空间
- ctorInstance() 初始化对象
- instance = memory 设置instance指向刚分配的内存
单线程情况下肯定没问题,但是在多线程情况下,JVM和CPU的优化中可能会执行指令重排。上面的第二步和第三步中,由于没有前后必然关系,cpu可能随时调换第二步和第三步的执行顺序。也就是会发生132这种顺序。
单例对象 volatile + 双重检测机制
吸取教训继续改进!
1 | /** |
volatile
的相关功能请看我之前的博客。到这里,懒汉模式改进就完成了。
枚举模式-》最安全
1 | /** |
最推荐的是使用枚举类实现单例模式,这是线程安全的。JVM会保证枚举类中的构造方法只调用一次,因此使用枚举会保证只实例化一次。
参考资料
全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的👉个人博客 👈吧!
谢谢你那么可爱,还一直关注着我~❤😝