首页 > 基础资料 博客日记
【Java|多线程与高并发】设计模式-单例模式(饿汉式,懒汉式和静态内部类)
2023-07-24 11:25:53基础资料围观274次
1. 前言
设计模式是一种在软件开发中常用的解决复杂问题的方法论。它提供了一套经过验证的解决方案,用于解决特定类型问题的设计和实现。设计模式可以帮助开发人员提高代码的可重用性、可维护性和可扩展性。
设计模式有很多,本文主要介绍单例模式.
2. 单例模式
单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点来获取该实例。
3. 如何保证一个类只有一个实例
在Java中,通常使用static
关键字来保证 类只有唯一的实例.
在单例模式中,类的构造函数被私有化
,以防止外部代码直接创建实例。然后,通过一个静态方法或静态变量来获取类的唯一实例。
如果实例不存在,则创建一个新的实例并返回;如果实例已存在,则直接返回该实例。
而是实现上述的方法有很多,下面介绍三种常见的实现模式:
- 饿汉式
- 懒汉式
- 静态内部类
4. 饿汉式单例模式
饿汉式单例: 在类加载时就创建实例,保证在任何情况下都有一个实例可用。
代码示例:
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
// 私有化构造函数
}
public static Singleton getInstance() {
return instance;
}
}
代码分析:
私有化构造函数使得外部无法直接创建实例,如果其它线程想到得到Singleton
类的实例,只能通过Singleton
类提供的getInstance()
方法得到.
5. 懒汉式单例模式
懒汉式单例: 延迟加载实例,只有在需要时才创建实例。
代码示例:
public class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy(){
// 私有化构造方法
}
public static SingletonLazy getInstance(){
if (instance == null){
instance = new SingletonLazy();
}
return instance;
}
}
代码分析:
如果后续代码中没有调用getInstance
方法,那么就把创建实例那一步给省下来了.
上述代码中,那种实现单例模式的方式是线程安全的?
答案是 饿汉模式
饿汉模式并没有涉及到修改,而懒汉模式即涉及到读有涉及到修改.那么在多线程模式中就不安全了.
设想一个场景, 如果在懒汉模式下,两个线程同时去调用getInstance方法,如果一个线程读到的instance为null,并给instance进行实例的创建,而另外一个线程读到的instance还是为null,又给instance创建了一次实例.那么instance就被创建多次了
6. 实现线程安全的懒汉式单例
既然懒汉模式在多线程的环境下不安全,那么如果保证懒汉模式在多线程的环境下安全呢?
既然涉及到多线程,就离不开"锁",也就是 synchronized
示例:
通过加锁的方式,解决了线程安全问题,但是又带来了新的问题.
懒汉式单例只是第一次调用getInstance
方法时才会发生线程不安全问题.一旦创建好了,线程就安全了.
但上述通过加锁的方式,会导致线程已经安全时仍然时需要加锁,有些多此一举了
. 且加锁开销比较大,影响效率.
那么如果保证线程安全时不加锁呢?
实例没有创建前,线程不安全.实例创建之后,线程安全. 那么就可以在加锁操作前在进行一次判断.
public class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy(){
}
public static SingletonLazy getInstance(){
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
}
代码分析:
使用双重if
保证只创建一次实例以及保证在线程安全时不进行加锁. 这里要重点进行理解.
但仔细思考上述代码仍然有一个问题.
假如两个线程同时调用getInstance
方法,第一个线程拿到锁,进入第二层if,开始进行new对象.
这里的new操作可以分为:
- 申请内存,得到内存首地址
- 调用构造方法,来初始化实例
- 把内存的首地址赋值给instance引用
上述的new操作,编译器可能会进行"指令重排序". 就可能会导致无序写入
的问题
上述步骤中的2和3,在单线程环境下是可以调换顺序的,并不会影响结果.
但在多线程环境中就会导致无序写入
问题:
- 线程A执行到instance = new Singleton();这行代码时,由于指令重排序,可能会先执行分配内存空间、初始化实例对象、将instance指向内存空间这三个操作的任意组合,而不是按照代码顺序执行。
- 假设线程A执行完了分配内存空间和初始化实例对象的操作,但还没有将instance指向内存空间,此时线程B执行到第一个instance == null的判断,结果为false,就会直接返回instance,但此时instance并没有正确初始化。
为了解决上述问题,就可以使用volatile
关键字. 它可以禁止指令重排序优化,保证instance的写操作先于读操作,从而避免其他线程在没有正确初始化实例时获取到instance。
public class SingletonLazy {
private static volatile SingletonLazy instance = null;
private SingletonLazy(){
}
public static SingletonLazy getInstance(){
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
}
上述就是一个在多线程环境下完全安全的懒汉式单例模式的写法.
7. 静态内部类实现单例模式
除了上述方式,使用静态内部类也能实现单例模式.
该方式利用静态内部类的特性来实现懒加载和线程安全。
代码示例:
public class Singleton {
private Singleton() {
// 私有化构造函数
}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
静态内部类SingletonHolder持有Singleton的唯一实例,当第一次调用getInstance方法时,才会触发SingletonHolder的初始化,从而创建实例。
由于静态内部类的初始化是线程安全的,因此可以确保在多线程环境下只有一个实例被创建。
8. 总结
本文主要介绍了饿汉式,懒汉式和静态内部类三种实现单例模式的方式,其中 懒汉式单例 很重要,要着重理解双重if的含义.
需要注意的是,以上方式都可以在多线程环境下保证单例的正确创建,但在特殊情况下,如使用反射或序列化/反序列化等机制,仍然可能破坏单例的唯一性。
在实际应用中,需要根据具体场景和需求选择合适的单例模式实现方式。
感谢你的观看!希望这篇文章能帮到你!
专栏: 《从零开始的Java学习之旅》在不断更新中,欢迎订阅!
“愿与君共勉,携手共进!”
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签: