前言
博主个人社区:开发与算法学习社区
博主个人主页:Killing Vibe的博客
欢迎大家加入,一起交流学习~~
一、何谓单例模式?
所谓的单例模式保证某个类在程序中有且只有一个对象。
现实生活中的单例:
一个类只有一个对象,地球类-地球这一个对象,太阳类-太阳这一个对象。
二、如何控制某个类只有一个对象?
思路如下:
1.要创建类的对象,通过构造方法对象
2.构造方法若是public权限,对于类的外部,就能随意创建对象,无法控制对象个数
3.所以我们考虑把构造方法私有化,类的外部就彻底没法产生对象了
构造方法私有化之后,对于类的外部而言就一个对象都没有,如何构造这唯一的对象(私有化的构造方法只能在类的内部调用),只调用一次构造方法即可。
三、饿汉单例
有了上述的思路,就可以写出饿汉式单例了:
public class SingleTon {
// 惟一的这一个对象
private static SingleTon singleTon = new SingleTon();
private SingleTon() {}
// 调用此方法时,singleTon对象已经产生过了,多线程场景下取回的是同一个单例对象
public static SingleTon getSingleton() {
return singleTon;
}
}
饿汉式单例,天然的线程安全。系统初始化JVM加载类的过程中就创建了这个唯一的对象。
在SingleTon类的外部访问这个唯一的对象,直接通过getSingleTon方法获取这个唯一对象。
总结一下就三步走:
- 构造方法私有化(保证对象的产生个数)
- 单例类的内部提供这个唯一的对象(static)
- 单例类提供返回这个唯一的对象的静态方法供外部使用
四、懒汉单例
只有第一次调用getSingleTon方法,表示外部需要获取这个单例对象时才产生对象。
系统初始化时,外部不需要这个单例对象,就先不产生,只有当外部需要此对象才实例化对象。这种操作称之为懒加载~
举个栗子:
哈希表的构造就是懒加载,构造方法只设置了负载因子。
只有在需要给map中添加元素的时候,表示此时需要table数组,才初始化数组为16。
4.1 单线程下
类加载的时候不创建实例. 第一次使用的时候才创建实例.
class LazySingleton {
private static LazySingleton singleTon;
private LazySingleton() {}
public static LazySingleton getSingle() {
if (singleTon == null) {
singleTon = new LazySingleton();
}
return singleTon;
}
}
但这样的代码在多线程场景下会出现问题,不能保证只有一个对象产生。
三个线程并行调用getSingle()方法,此时singleTon三个线程看到的就都是null,每个线程都创建了一个对象。
4.2 多线程下(简单版)
此时加上 synchronized 可以改善这里的线程安全问题. (把方法锁了)
class LazySingleton {
private static LazySingleton singleTon;
private LazySingleton() {}
public synchronized static LazySingleton getSingle() {
if (singleTon == null) {
singleTon = new LazySingleton();
}
return singleTon;
}
}
但这样同一时间只有一个线程能进入getSingle方法,此时这个方法内部都是单线程操作,其他线程要进入此方法都需要获取锁(锁的粒度太低)
4.3 多线程下(增强版)
以下代码在加锁的基础上, 做出了进一步改动:
- 使用双重 if 判定(double-check), 降低锁竞争的频率.
- 给 instance 加上了 volatile.
public class LazySingleTon {
private static volatile LazySingleTon singleTon;
private LazySingleTon() {}
// 第一次调用获取单例对象方法时才实例化对象
public static LazySingleTon getSingleTon() {
if (singleTon == null) {
// 初始化对象
synchronized (LazySingleTon.class) {
if (singleTon == null) {
singleTon = new LazySingleTon();
}
}
}
return singleTon;
}
}
为什么使用Double-check?
1.使用Double-check的原因就是要降低锁的粒度,以上代码只是单例中最核心的代码,单例模式还有很多其他操作,为了保证其他操作尽可能并发执行,需要往小了“锁”。
2.还需要用第二个if判断的原因就是因为,在同步代码块内部需要再次坚持singleTon是否为空,防止其他线程恢复执行后多次创建了单例对象。
为什么使用Volatile关键字?
双重加锁,使用volatile关键字保证单例对象的初始化不被中断
举个栗子:
假如构造懒汉单例的时候需要初始化 x,y,z 三个变量, 多个线程开始同时运行:
- 当线程t1执行new操作时,还没完全结束,此时 SingleTon !=null
- 对于刚开始执行代码的t2线程来说,它看到singleTon != null 就直接返回了,但是返回的单例对象是一个尚未完全初始化的对象(比如z没来得及初始化为30,可能t2线程的对象z = 0)
- 此时若采用volatile修饰单例对象,由于volatile可以保证可见性,new这个操作就会像有一堵墙(内存屏障),其他线程要执行到return操作,JVM一个保证new操作完全结束后才能执行return语句。
总结
以上就是多线程场景下用Java实现饿汉式单例和懒汉式单例的所有注意事项,纯手打,希望各位老铁能多多支持,有什么疑问可以私信博主~~~感谢支持
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/81762.html