【JavaEE】单例模式如何保证在多线程环境下线程安全高可用?
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
文章目录
1 单例模式回顾
单例模式是设计模式的一种。而设计模式就是针对我们实际开发中写代码所遇到的不同场景所设立的解决方案。在笔者JavaSE阶段的文章中我们曾经聊到过单例模式这里给出文章链接有兴趣的读者可以自行阅读单例模式懒汉式与饿汉式的实现 在本文中笔者也会进行必要的总结。下面进入正文
所谓类的单例设计模式就是采用一定的方法保证在整个软件系统中对某一个类只能存在一个对象实例并且该类只提供一个取得其对象实例的方法。常见的实现方式有两种懒汉式和饿汉式。
这里的懒汉和饿汉可以这样理解对于饿汉来说肚子很饿是比较着急去吃饭的体现出一个着急的态度而对于懒汉来说在计算机中这属于褒义词指的是“不着急”只有当你需要的时候我才给你提供需要的东西反而更加高效。 所以说
- 懒汉式的单例模式是比较着急去创建对象实例的
- 饿汉式的单例模式不太着急去创建对象实例只有在需要的时候才去创建。
现在饿汉式和懒汉式的区别我们了解清楚了无非就是创建对象的时机不同。 但是无论是哪种实现方式都需要满足一个大前提对于该类只能存在一个对象实例。
因此为了做到这点我们可以考虑将构造器私有化这样在外部就没有办法手动创建对象。同时在类中我们需要手动创建一个对象并对外提供一个公有的方法使得该创建的对象能够被外部所获取。
下面我们来具体聊一聊饿汉式与懒汉式该如何实现单例模式。
2 饿汉式单例模式的实现
对于饿汉式的实现主要步骤如下饿汉主要体现在直接创建对象
- 先将构造器私有化 避免用户直接在外部 new 新对象。
- 在类的内部属性部分直接创建一个对象我们已经将构造器私有化了因此在外部没有办法 new 新对象所以只能在类的内部创建对象。
- 向外提供一个公共的静态方法用于外部引用该对象。创建好了对象后如果对外不提供公共方法我们就没有办法使用这个被创建的对象。
这里有一点值得说明静态为什么需要使用static去修饰对象和方法呢
在类的外部我们无法使用 new 新建对象也就是说我们只能通过 类名.xxx 的方式来返回创建的对象因此获取对象的对外公共方法需要使用 static 关键字修饰。而该方法中需要返回类中创建的对象静态方法只能使用静态变量因此对象属性也应当设置为 static。
实现代码如下
/**
* @author 兴趣使然黄小黄
* @version 1.0
* 单例模式的饿汉式实现
*/
@SuppressWarnings({"all"})
public class Singleton {
// 提供一个对象
private static Singleton instance = new Singleton();
// 防止外部new对象
private Singleton(){}
// 使得外部能够访问到创建的对象
public static Singleton getInstance(){
return instance;
}
}
3 懒汉式单例模式的实现
对于懒汉式的实现主要步骤如下懒汉主要体现在不直接创建对象而是在需要的时候再创建
- 先将构造器私有化 避免用户直接在外部 new 新对象。
- 在类的内部 声明一个对象引用但是不创建 即先不 new
- 向外提供一个公共的静态方法用于创建对象并返回对象引用。 需要在该方法中创建对象并返回给外部。
实现代码如下
/**
* @author 兴趣使然黄小黄
* @version 1.0
* 单例模式的懒汉式实现
*/
@SuppressWarnings({"all"})
public class Singleton2 {
// 提供一个对象的引用
private static Singleton2 instance = null;
// 防止外部new对象
private Singleton2(){}
// 用于创建对象或使得外部能够访问到创建的对象
public static Singleton2 getInstance(){
if (instance == null){
instance = new Singleton2();
}
return instance;
}
}
4 单例模式的线程安全问题分析
以上我们成功实现了饿汉式与懒汉式的单例模式。在串行、单线程环境中饿汉式和懒汉式实现的单例模式都没有什么问题。那么如果是在多线程环境呢这样就存在了一些线程安全问题。
什么是线程安全问题
线程安不安全指的是在多线程环境中代码是否会产生bug。如果在多线程环境中不会产生bug那么我们就说这是线程安全的如果在多线程环境中会产生bug我们就说这样的实现方式是不安全的。
什么样的情况下会导致线程安全问题呢
如果多个线程只是读取同一个变量、资源而不作修改此时仍然是线程安全的。如果存在修改则可能会出现线程安全问题。
对于饿汉式单例模式的实现由于其仅仅只是读取了变量的内容因此是线程安全的
而懒汉式单例模式的实现就不一样了。在懒汉模式中既包含了读又包含了写且读写还不是原子的分成了两个步骤~ 所以存在线程安全问题。
比如两个进程g1g2同时进入该方法需要返回给 g1 g2 对象实例。当 g1 进入该方法后进行判断不为 null因此创建一个对象而在创建对象的过程中g2 刚好也进入了该方法也被判断成了不为 null此时g1, g2就指向不同的对象了所以它是线程不安全的。
可能文字叙述还是有些抽象下面对例子进行画图描述以时间为顺序同样进程还是用 g1g2来表示
由于进程调度的时机是不确定的以上只是列举了一种可能导致线程安全问题的调度方式。
那么如何保证懒汉式单例模式的实现是线程安全的呢
5 线程安全的懒汉式实现
如何实现线程安全
很简单既然你说读和写分成了两个步骤会出现线程安全问题那我 加锁 不就完了
嗯很好这样就保证了在进行读和写操作的时候只有一个线程在操作就可以避免另一个线程同时进入 if 语句去创建对象了
可是这样真的好吗
我们简单分析一下对于懒汉式的实现是何时会导致线程不安全
对于懒汉式的代码来说线程不安全是发生在 instance 被初始化之前在未初始化的时候即 instance == null多个线程去调用 getInstance 可能就同时涉及到读和改的问题引发线程安全问题。
但是一旦 instance 不是null已经被初始化后getInstance方法就只剩下读操作了此时一定是线程安全的。
那么我们刚刚的加锁方式就有了些问题无论是 instance 初始化之前还是 instance 初始化之后都会导致阻塞存在大量的锁竞争
而我们知道对于加锁操作来说加锁确实可以让代码保证线程安全但是也会付出相应的代价 最典型的代价就是程序运行变慢了。本来我多个线程运行的好好的你给我加了把锁其他线程不就进不去了吗一定程度上的阻塞
因此我们需要做出如下改进再加一个判断判断 instance 是否被初始化如果已经初始化就不必进入加锁的代码块了
但是这样还会有问题即 内存可见性 问题。
如果多个线程都去调用 getInstance方法就会造成大量的读 instance 操作。而我们知道对于在内存读和在寄存器读来说后者的效率更高。因此编译器会自作主张对代码进行优化将读内存操作优化成读寄存器操作。
也就是说即使某个线程将instance初始化了但是由于编译器的优化在其他线程的视角看起来instance 还是null。
也就是说这里的内存可见性问题会导致第一个if语句失效即不该加锁的时候还会加锁。因此我们需要保证内存可见即使用 volatile
对 instance 进行修饰。
完整代码如下
/**
* @author 兴趣使然黄小黄
* @version 1.0
* 单例模式的懒汉式实现
* 保证线程安全
*/
@SuppressWarnings({"all"})
public class Singleton2 {
// 提供一个对象的引用
private static volatile Singleton2 instance = null;
// 防止外部new对象
private Singleton2(){}
// 用于创建对象或使得外部能够访问到创建的对象
public static Singleton2 getInstance(){
if (instance == null){
synchronized(Singleton2.class){
if (instance == null){
instance = new Singleton2();
}
}
}
return instance;
}
}
6 总结
对于饿汉式单例模式由于只存在对同一变量的读操作不会导致线程安全问题。
对于懒汉式单例模式实现线程安全主要有如下三步
- 在正确的位置加锁
- 双重 if 判定
- volatile
以上便是本文的全部内容啦创作不易如果你有任何问题欢迎私信感谢您的支持