Java设计模式之单例模式详解

发布时间 - 2026-01-11 00:11:37    点击率:

单例模式是非常常见的设计模式,其含义也很简单,一个类给外部提供一个唯一的实例。下文所有的代码均在github

源码整个项目不仅仅有设计模式,还有其他JavaSE知识点,欢迎Star,Fork

单例模式的UML图

单例模式的关键点

通过上面的UML图,我们可以看出单例模式的特点如下:

1、构造器是私有的,不允许外部的类调用构造器
2、提供一个供外部访问的方法,该方法返回单例类的实例

如何实现单例模式

上面已经给出了单例模式的关键点,我们的实现只需要满足上面2点即可。但是正因为单例模式的实现方式比较宽松,所以不同的实现方式会有不同的问题。我们可以对单例模式的实现做一下分类,看一看有哪些不同的实现方式。

1根据单例对象的创建时机不同,可以分为饿汉模式和懒汉模式。饿汉是指在类加载的时候,就创建了对象。但是创建对象有时比较消耗资源,会造成类加载很慢,但是优点是获取对象的速度很快,因为早已经创建好了嘛。懒汉就是相对饿汉而言,在需要返回单例对象的时候,在创建对象,类加载的时候,并不初始化,好处与缺点也不言而喻

2.根据是否实现线程安全,可以分为普通的懒汉模式这种线程不安全的写法,和饿汉模式,双重检查锁的懒汉模式,以及通过静态内部类或者枚举类等实现的线程安全的写法。

一个线程不安全的单例模式

public class SimpleSingleton {

  private static SimpleSingleton simpleSingleton;

  private SimpleSingleton(){

  }

  public static SimpleSingleton getInstance(){
    if (simpleSingleton == null) {
      simpleSingleton = new SimpleSingleton();
    }
    return simpleSingleton;
  }
}

首先,我们可以看出这是一个懒汉模式的实现。因为只有在getInstance的时候,才会真正创建单例的对象。但是为什么他是线程不安全的呢,是因为可能会有2个线程同时进入if (simpleSingleton == null)的判断,就是同时创建了simpleSingleton对象。

DCL懒汉模式

上面的方法可以看出是存在线程不安全的问题的,我们可以用同步关键字synchronized来实现线程安全。我们先逐步分析,先用synchronized来改写上面的懒汉模式,代码如下:

public class DCLSingleton {

  private static DCLSingleton singleton;
  private DCLSingleton(){
  }

  public synchronized static DClSingleton getSingleton(){
    if (singleton == null) {
      singleton = new DCLSingleton();
    }
    return singleton;
  }

}

这样,就有效的保证了不会有两个线程同时执行该方法,但这个效率也太低了吧。因为在创建实例之后,每次得到实例对象,还是需要进行同步,synchronized的同步保证代价是比较大的,因此可以在此基础上进行改造。在已经创建好之后,就不需要同步了,我们可以改成如下的形式:

  public static DCLSingleton getSingleton(){
    if (singleton == null) {
      synchronized (DCLSingleton.class) {
        if (singleton == null) {
          singleton = new DCLSingleton();
        }
      }
    }
    return singleton;
  }

其他代码不变,只看这个方法。该方法的两重if (singleton == null)可以有效地保证线程安全。比如,当两个线程同时进入该方法的时候,第一个if,两者都是进入,下面的代码,但是碰到同步代码块,只能有一个先进入,进入的时候,继续判断,再次判断为空,才会真正创建对象。如果不进行,第二个判断,那些对于第一个进入的线程而言,确实创建了对象,但是第二个线程,他紧接着也会执行创建对象的操作,因为不知道第一个线程已经创建成功。因此,需要两次判空。
但是真的就如此简单的保证了线程安全吗?我们仔细分析一下这个过程,singleton = new DCLSingleton();这个代码实际上是3个操作。

1.给DCLSingleton实例分配内存
2.调用DCLSingleton()的构造函数,初始化成员字段
3.将singleton对象指向分配的内存空间。

在JDK1.5以前,上面的3个执行顺序是不固定的,有可能是1-2-3,或者1-3-2。如果是1-3-2,则在第一个线程执行完第三步以后,第二个线程立即执行,但还没有真正的进行初始化,所以就会使用的时候出错。在JDK1.5以后,我们可以用volatile关键字来保证该1-2-3的顺序执行。所以,除了getSingleton()方法要改成上面的样子以外,还需要对private static DCLSingleton singleton; 改写成private static volatile DCLSingleton singleton; 这样,就真正保证了线程同步的懒汉写法的单例模式。

饿汉写法

饿汉写法有很多变形,但无论是哪一种变形,都能保证线程安全,因为饿汉写法是在类加载的时候,就完成了对象的初始化,类加载保证了他们天生是线程安全的。下面给出常见的2中饿汉写法

public class HungrySingleton {
  private static final HungrySingleton singleton = new HungrySingleton();

  private HungrySingleton(){

  }

  public static HungrySingleton getSingleton(){
    return singleton;
  }
}
public class HungrySingleton {
  private static final HungrySingleton singleton = new HungrySingleton();

  private HungrySingleton(){

  }

//  public static HungrySingleton getSingleton(){
//    return singleton;
//  }
}

这两种对初始化单例的对象上面,都是一致的, 通过final来保证对象的唯一。不同的是,调用单例对象的方式,第一种是通过getSingleton()第二种是通过类.类变量的形式。

静态内部类实现单例模式

双重检查锁(DCL)实现单例模式,虽然解决了线程不安全的问题,以及保证了资源的懒加载,在需要的时候,才会进行实例化的操作。但是在某些情况下(比如JDK低于1.5)会出现DCL失效,所以有一种很简洁且依旧是懒加载的方法实现单例模式。写法如下:

public class StaticSingleton {

  private StaticSingleton(){
  }
  public static final StaticSingleton getInstance(){
    return Holder.singleton;
  }

  private static class Holder{
    private static final StaticSingleton singleton = new StaticSingleton();
  }
}

通过静态内部类的形式,实现单例类的初始化,其特性同样是通过ClassLoader来保证其单例对象的唯一,但是这是懒加载的,因为只有在Holder类被调用的时候,即getInstance调用的时候,才会加载Holder类从而实现创建对象。

枚举类实现单例模式

直接看代码:

public enum EnumSingleton {
  SINGLETON;
  public void doSometings(){
    
  }
}

使用的时候,直接通过EnumSingleton.SINGLETON.doSomethings()枚举类天生特性是保证不会有两个实例,并且只有在第一次访问的时候才会被实例化,是懒加载的情况。

真的不会再次创建新的对象吗?

在常规调用单例类的getInstance()方法的情况下,使用线程安全的写法确实不会创建新的对象,但是Java提供了很多奇特的技巧和使用,下面这些使用会破坏掉常规的单例。

  • 反序列化
  • 反射
  • 克隆
  • 分布式环境下,多个类加载器

在除了枚举实现单例模式的方法以外,其余所有方法碰到上述四种情况,都会重新创建对象。原因如下:

  • 反序列化会调用一个特殊的readResolve()方法来创建新的对象。我们可以重写该方法,让他返回原来的instance,而不是重新创建一个。
  • 反射会得到私有的构造函数,只能在构造函数中加一个判断,如果对象不为null,则扔出一个运行时异常,如果不这样,只有枚举能解决,因为枚举自带的特性。
  • 克隆,因为直接拷贝的内存空间的内容,所以只有自己重写单例类的clone方法,如果不这样,也只有枚举能解决,因为枚举没有克隆方法。
  • 多分布式环境,因为我们上述很多种单例的写法,都是依赖于类加载器的特性,但是static的作用只负责到类加载器,所以当工程中存在多个类加载器的时候,就会创建多个实例,这种通常就需要第三方库来解决。

什么时候用单例模式,用哪一种写法的单例模式

单例模式有两种比较适合的使用场景。
第一种是创建某个对象,需要的代价比较大,为了避免频繁的创建和销毁对象从而引起的对资源的浪费,会考虑使用单例模式。
第二种是这个对象必须只有一个,有多个会造成不可预估的错误,或者程序的混乱,比如只会有一个序号生成器,一个缓存等等。
针对使用的单例模式,如果需要理解的加载资源,就是用饿汉写法,在Android应用中,很多对象需要在启动的时候,立即就使用,比如启动时,需要拉取相机配置的类管理缩略图的cache类等等。如果不是立即需要,或者不是贯穿应用始终的,就不需要使用饿汉写法,可以考虑懒汉写法用(DCL或者静态内部类实现)这两种在一般情况下都不会出现问题。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


# java  # 单例模式  # java设计模式之单例模式学习  # 浅析Java设计模式编程中的单例模式和简单工厂模式  # Java设计模式之单例模式实例详解【懒汉式与饿汉式】  # java设计模式之单例模式的详解及优点  # 深入解析Java的设计模式编程中单例模式的使用  # Java设计模式之单例模式实例分析  # 简单讲解在Java编程中实现设计模式中的单例模式结构  # java 设计模式之单例模式  # Java设计模式系列之深入浅出单例模式  # 加载  # 才会  # 第一个  # 多个  # 都是  # 不安全  # 我们可以  # 第二个  # 可以看出  # 就会  # 会有  # 就不  # 可以用  # 重写  # 比较大  # 提供一个  # 这两种  # 哪一种  # 第二种  # 第一种 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: Laravel如何优化应用性能?(缓存和优化命令)  Laravel怎么防止CSRF攻击_Laravel CSRF保护中间件原理与实践  详解免费开源的DotNet二维码操作组件ThoughtWorks.QRCode(.NET组件介绍之四)  Laravel怎么多语言本地化设置_Laravel语言包翻译与Locale动态切换【手册】  如何用AWS免费套餐快速搭建高效网站?  网站广告牌制作方法,街上的广告牌,横幅,用PS还是其他软件做的?  如何在建站之星绑定自定义域名?  实现点击下箭头变上箭头来回切换的两种方法【推荐】  如何在 Go 中优雅地映射具有动态字段的 JSON 对象到结构体  无锡营销型网站制作公司,无锡网选车牌流程?  详解MySQL数据库的安装与密码配置  Laravel如何创建自定义中间件?(Middleware代码示例)  网站建设整体流程解析,建站其实很容易!  javascript基于原型链的继承及call和apply函数用法分析  香港服务器如何优化才能显著提升网站加载速度?  北京网站制作的公司有哪些,北京白云观官方网站?  Laravel Seeder怎么填充数据_Laravel数据库填充器的使用方法与技巧  智能起名网站制作软件有哪些,制作logo的软件?  如何在橙子建站上传落地页?操作指南详解  手机钓鱼网站怎么制作视频,怎样拦截钓鱼网站。怎么办?  Laravel Blade组件怎么用_Laravel可复用视图组件的创建与使用  php json中文编码为null的解决办法  微信小程序 canvas开发实例及注意事项  如何在HTML表单中获取用户输入并结合JavaScript动态控制复利计算循环  网站制作怎么样才能赚钱,用自己的电脑做服务器架设网站有什么利弊,能赚钱吗?  悟空识字如何进行跟读录音_悟空识字开启麦克风权限与录音  如何实现建站之星域名转发设置?  动图在线制作网站有哪些,滑动动图图集怎么做?  Linux后台任务运行方法_nohup与&使用技巧【技巧】  如何快速登录WAP自助建站平台?  Laravel怎么调用外部API_Laravel Http Client客户端使用  如何在IIS7中新建站点?详细步骤解析  如何快速查询网站的真实建站时间?  如何快速搭建支持数据库操作的智能建站平台?  Laravel怎么清理缓存_Laravel optimize clear命令详解  Laravel怎么判断请求类型_Laravel Request isMethod用法  Java遍历集合的三种方式  Laravel怎么使用Collection集合方法_Laravel数组操作高级函数pluck与map【手册】  Laravel安装步骤详细教程_Laravel环境搭建指南  Java类加载基本过程详细介绍  利用JavaScript实现拖拽改变元素大小  如何快速搭建高效可靠的建站解决方案?  Android okhttputils现在进度显示实例代码  Laravel storage目录权限问题_Laravel文件写入权限设置  laravel怎么配置和使用PHP-FPM来优化性能_laravel PHP-FPM配置与性能优化方法  Laravel广播系统如何实现实时通信_Laravel Reverb与WebSockets实战教程  Swift中switch语句区间和元组模式匹配  javascript中对象的定义、使用以及对象和原型链操作小结  Win11怎么关闭透明效果_Windows11辅助功能视觉效果设置  电视网站制作tvbox接口,云海电视怎样自定义添加电视源?