Java设计模式之单例模式

简介: 单例模式

1、什么是单例模式

Ensure a class has only one instance, and provide a global point of access to it.

采取一定的办法保证在整个软件系统中,确保对于某个类只能存在一个实例。单例模式有如下三个特点:

①、单例类只能有一个实例

②、单例类必须自己创建自己的实例

③、单例类必须提供外界获取这个实例的方法

2、单例类的设计思想(Singleton)

①、外界不能创建这个类的实例,那么必须将构造器私有化。

publicclassSingleton {
//构造器私有化
privateSingleton(){
 
}
}

②、单例类必须自己创建自己的实例,不能允许在类的外部修改内部创建的实例。

比如将这个实例用 private 声明。为了外界能访问到这个实例,我们还必须提供 get 方法得到这个实例。因为外界不能 new 这个类,所以我们必须用 static 来修饰字段和方法。

//在类的内部自己创建实例
privatestatic Singleton singleton = new Singleton();

//提供get 方法以供外界获取单例
public Singleton getInstance(){
 return singleton;
}

③、是否支持延迟加载?

有些情况下,创建某个实例耗时长,占用资源多,用的时候也少,我们会考虑在用到的时候才会去创建,这就是延迟加载。

但有些情况,按照 fail-fast 的设计原则(有问题及早暴露),比如某个实例占用资源很多,如果延迟加载,会在程序运行一段时间后OOM,如果在程序启动的时候就创建这个实例,我们就可以立即去修复,不会导致程序运行之后的系统奔溃。

所以,是否支持延迟加载需要结合实际情况考虑。

④、保证线程安全

这个是一定要考虑的,如果你写的单例类存在线程安全问题,那就是伪单例了。

3、单例类的几种实现方式

3.1 单例模式之饿汉模式

publicclassSingleton {
//构造器私有化
privateSingleton(){
 
}
//在类的内部自己创建实例
privatestatic Singleton singleton = new Singleton();

//提供get 方法以供外界获取单例
publicstatic Singleton getInstance(){
 return singleton;
}

}

测试:

publicstaticvoidmain(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1.equals(s2)); //true
}

这种模式在类加载的时候实例 singleton 就已经创建并初始化好了,所以是线程安全的。

不过这种模式不支持延迟加载,有可能这个实例化过程很长,那么就会加大类装载的时间;有可能这个实例现阶段根本用不到,那么创建了这个实例,也会浪费内存。但是还是我们前面说的,是否支持延迟加载,需要结合实际情况考虑。

3.2 单例模式之懒汉模式(线程不安全)

//懒汉模式
publicclassSingleton {
//构造器私有化
privateSingleton(){
 
}
//在类的内部自己创建实例的引用
privatestatic Singleton singleton = null;

//提供get 方法以供外界获取单例
publicstatic Singleton getInstance(){
 if(singleton == null){
  singleton = new Singleton();
 }
 return singleton;
}

}

这种方法达到了 lazy-loading 的效果,即我们在第一次需要得到这个单例的时候,才回去创建它的实例,以后再需要就可以不用创建,直接获取了。但是这种设计在多线程的情况下是不安全的。

我们可以创建两个线程来看看这种情况:

publicclassThreadSingletonextendsThread{
@Override
publicvoidrun() {
 try {
  System.out.println(Singleton.getInstance());
 } catch (Exception e) {
  e.printStackTrace();
 }
}
publicstaticvoidmain(String[] args) {
 ThreadSingleton s1 = new ThreadSingleton();
 s1.start(); //com.ys.pattern.Singleton@5994a1e9
 
 ThreadSingleton s2 = new ThreadSingleton();
 s2.start(); //com.ys.pattern.Singleton@40dea6bc
}
}

很明显:最后输出结果的两个实例是不同的。这便是线程安全问题。那么怎么解决这个问题呢?

参考这篇博客:Java多线程同步:http://www.cnblogs.com/ysocean/p/6883729.html

3.3 单例模式之懒汉模式(线程安全)

这里我们采用同步代码块来达到线程安全

//懒汉模式线程安全
publicclassSingleton {
//构造器私有化
privateSingleton(){
 
}
//在类的内部自己创建实例的引用
privatestatic Singleton singleton = null;

//提供get 方法以供外界获取单例
publicstatic Singleton getInstance()throws Exception{
 synchronized (Singleton.class) {
  if(singleton == null){
   singleton = new Singleton();
  }
 }
 return singleton;
}

}

我们给 getInstance() 方法创建实例时加了一把锁 synchronzed,这样会导致这个方法的并发为1,相当于串行操作,如果这个单例在实际项目中会频繁被调用,那就会频繁加锁,释放锁,会有性能瓶颈,不推荐此种方式。

3.4 单例模式之懒汉模式(线程安全)--双重校验锁

分析:上面的例子我们可以看到,synchronized 其实将方法内部的所有语句都已经包括了,每一个进来的线程都要单独进入同步代码块,判断实例是否存在,这就造成了性能的浪费。那么我们可以想到,其实在第一次已经创建了实例的情况下,后面再获取实例的时候,可不可以不进入这个同步代码块?

//懒汉模式线程安全--双重锁校验
publicclassSingleton {
//构造器私有化
privateSingleton(){
 
}
//在类的内部自己创建实例的引用
privatestatic Singleton singleton = null;

//提供get 方法以供外界获取单例
publicstatic Singleton getInstance()throws Exception{
 if(singleton == null){
  synchronized (Singleton.class) {
   if(singleton == null){
    singleton = new Singleton();
   }
  }
 }
 return singleton;
}

}

以上的真的完美解决了单例模式吗?其实并没有,请看下面:

3.5 单例模式之最终版

我们知道编译就是将源代码翻译成机械码的过程,而Java虚拟机的目标代码不是本地机器码,而是虚拟机代码。编译原理里面有个过程是编译优化,就是指在不改变原来语义的情况下,通过调整语句的顺序,来让程序运行的更快,这个过程称为 reorder。

JVM 只是一个标准,它并没有规定有关编译器优化的内容,也就是说,JVM可以自由的实现编译器优化。

那么我们来再来考虑一下,创建一个变量需要哪些步骤?

①、申请一块内存,调用构造方法进行初始化

②、分配一个指针指向该内存

而这两步谁先谁后呢?也就是存在这样一种情况:先开辟一块内存,然后分配一个指针指向该内存,最后调用构造方法进行初始化。

那么针对单例模式的设计,就会存在这样一个问题:线程 A 开始创建 Singleton 的实例,此时线程 B已经调用了 getInstance的()方法,首先判断 instance 是否为 null。而我们上面说的那种模型, A 已经把 instance 指向了那块内存,只是还没来得及调用构造方法进行初始化,因此 B 检测到 instance 不为 null,于是直接把  instance 返回了。那么问题出现了:尽管 instance 不为 null,但是 A 并没有构造完成,就像一套房子已经给了你钥匙,但是里面还没有装修,你并不能住进去。

解决方案:使用 volatile 关键字修饰 instance

我们知道在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

//懒汉模式线程安全--volatile
publicclassSingleton {
   //构造器私有化
   privateSingleton(){

   }
   //在类的内部自己创建实例的引用
   privatestaticvolatile Singleton singleton = null;

   //提供get 方法以供外界获取单例
   publicstatic Singleton getInstance()throws Exception{
       if(singleton == null){
           synchronized (Singleton.class) {
               if(singleton == null){
                   singleton = new Singleton();
               }
           }
       }
       return singleton;
   }

}

到此我们完美的解决了单例模式的问题。但是 volatile  关键字是 JDK1.5 才有的,也就是 JDK1.5 之前是不能这样用的

3.6 2021年4月补充

上面我们说要加关键字 volatile ,禁止指令重排,防止单例对象new 出来后,并且赋值给 singleton,但是还没来得及初始化这个问题。

现在高版本的 Java(JDK9) 已经在 JDK 内部实现中解决了这个问题,把对象的 new 操作和初始化操作设计为 原子操作。

相关参考链接:

https://shipilev.net/blog/2014/safe-public-construction

https://chriswhocodes.com/vm-options-explorer.html

3.7 单例模式之枚举类

publicenum Singleton{
   INSTANCE;
   privateSingleton(){}
}

通过Java枚举类的自身特性,保证实例创建的线程安全和唯一性。

3.8 单例模式之静态内部类

publicclassInnerSingleton {
   privateInnerSingleton() {
   }

   publicstatic InnerSingleton getInstance() {
       return Inner.instance;
   }

   staticclassInner {
       static InnerSingleton instance = new InnerSingleton();
   }

   publicstaticvoidmain(String[] args) {
       System.out.println(InnerSingleton.getInstance() == InnerSingleton.getInstance());//true
       System.out.println(InnerSingleton.getInstance().equals(InnerSingleton.getInstance()));//true

   }
}

4、单例模式的应用

说了那么多,那么单例模式在实际项目中有啥用呢?

还是根据其核心概念,某个数据在系统中只能存在一份,就可以设计为单例。

1、windows 系统的回收站,我们能在任何盘符删除数据,但是最后都是到了回收站中

2、网站的计数器,不采用单例模式,很难实现同步

3、数据库连接池,可以节省打开或关闭数据库连接所引起的效率损耗,用单例模式来维护,可以大大降低这种损耗。当然对于海量数据系统,会存在多个数据库连接池,比如一个能够快速执行SQL的连接池,还有一个是慢SQL,如果都放在一个池里面,会导致慢SQL执行的时候,长时间占用数据库连接资源,导致其他SQL请求无法响应。

4、系统的配置信息类,通常只存在一个。

目录
相关文章
|
4天前
|
设计模式 测试技术 Python
《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
【7月更文挑战第10天】Page Object Model (POM)是Selenium自动化测试中的设计模式,用于提高代码的可读性和维护性。POM将每个页面表示为一个类,封装元素定位和交互操作,使得测试脚本与页面元素分离。当页面元素改变时,只需更新对应页面类,减少了脚本的重复工作和维护复杂度,有利于团队协作。POM通过创建页面对象,管理页面元素集合,将业务逻辑与元素定位解耦合,增强了代码的复用性。示例展示了不使用POM时,脚本直接混杂了元素定位和业务逻辑,而POM则能解决这一问题。
23 6
|
2天前
|
设计模式 Java 测试技术
《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
【7月更文挑战第12天】在本文中,作者宏哥介绍了如何在不使用PageFactory的情况下,用Java和Selenium实现Page Object Model (POM)。文章通过一个百度首页登录的实战例子来说明。首先,创建了一个名为`BaiduHomePage1`的页面对象类,其中包含了页面元素的定位和相关操作方法。接着,创建了测试类`TestWithPOM1`,在测试类中初始化WebDriver,设置驱动路径,最大化窗口,并调用页面对象类的方法进行登录操作。这样,测试脚本保持简洁,遵循了POM模式的高可读性和可维护性原则。
11 2
|
2天前
|
设计模式 安全 C++
C++一分钟之-C++中的设计模式:单例模式
【7月更文挑战第13天】单例模式确保类只有一个实例,提供全局访问。C++中的实现涉及线程安全和生命周期管理。基础实现使用静态成员,但在多线程环境下可能导致多个实例。为解决此问题,采用双重检查锁定和`std::mutex`保证安全。使用`std::unique_ptr`管理生命周期,防止析构异常和内存泄漏。理解和正确应用单例模式能提升软件的效率与可维护性。
8 2
|
5天前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
18 1
|
3天前
|
设计模式 Java 测试技术
《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
【7月更文挑战第11天】页面对象模型(POM)通过Page Factory在Java Selenium测试中被应用,简化了代码维护。在POM中,每个网页对应一个Page Class,其中包含页面元素和相关操作。对比之下,非POM实现直接在测试脚本中处理元素定位和交互,代码可读性和可维护性较低。
|
5天前
|
设计模式 安全 Java
Java面试题:什么是单例模式?如何在Java中实现单例模式?
Java面试题:什么是单例模式?如何在Java中实现单例模式?
10 0
|
5天前
|
设计模式 安全 Java
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
10 0
|
5天前
|
设计模式 存储 缓存
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
8 0
|
5天前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
9 0
|
5天前
|
设计模式 安全 Java
Java面试题:如何实现一个线程安全的单例模式,并确保其在高并发环境下的内存管理效率?如何使用CyclicBarrier来实现一个多阶段的数据处理任务,确保所有阶段的数据一致性?
Java面试题:如何实现一个线程安全的单例模式,并确保其在高并发环境下的内存管理效率?如何使用CyclicBarrier来实现一个多阶段的数据处理任务,确保所有阶段的数据一致性?
9 0