001 创建单例

文章目录

  • 饿汉模式
  • 懒汉模式
    • 线程不安全
    • 懒汉式(线程安全)
    • “双重检查锁定”(Double-Checked Locking, DCL)实现单例(线程安全)
      • 例子 1:两个线程几乎同时请求单例实例
      • 例子 2:多个线程在不同时间点请求单例实例
      • 例子 3:线程在单例实例创建过程中被阻塞
      • 例子 4:线程在实例已创建后进入同步块
      • 例子 5:高并发下的单例获取
      • 例子 6:异常处理中的单例获取
      • 例子 7:延迟初始化的效果
    • 静态内部类实现单例(线程安全)
      • 类定义
      • 构造方法
      • 获取单例的方法
      • 静态内部类
      • 线程安全性
      • 例子1:直接通过`getInstance`方法获取单例
      • 例子2:在并发环境中验证单例的唯一性

多线程(Multithreading)
“多线程”指的是同时执行多个线程以完成不同的任务。在操作系统中,线程是进程中的一个执行单元,它负责程序的执行流。多线程允许程序在同一时间内执行多个操作,从而提高了程序的总体执行效率和响应能力。在多线程环境中,需要注意线程同步和数据一致性的问题,以避免竞态条件和数据损坏。

并发(Concurrency)
“并发”是指在同一时间段内,多个任务或操作被交替执行,从用户的角度来看,这些任务似乎是同时进行的。并发并不意味着这些任务在同一时刻物理上并行执行,而是在一个时间段内通过时间分片技术交替执行。并发可以通过多线程、多进程、异步I/O等方式实现。

饿汉模式

饿汉式的单例实现比较简单,其在类加载的时候,静态实例instance 就已创建并初始化好了。


public class Singleton {private static final Singleton instance = new Singleton();private Singleton () {}public static Singleton getInstance() {return instance;}
}

饿汉式单例优缺点:

优点:
单例对象的创建是线程安全的;
获取单例对象时不需要加锁。
缺点:单例对象的创建,不是延时加载。
积极加载的问题在于:如果这个类除了加载一次,但其他场景并未调用,那么这个对象就白白被创建,浪费了内存资源.

一般认为延时加载可以节省内存资源。但是延时加载是不是真正的好,要看实际的应用场景,而不一定所有的应用场景都需要延时加载。

懒汉模式

线程不安全

与饿汉式对应的是懒汉式,懒汉式为了支持延时加载,将对象的创建延迟到了获取对象的时候


private static Singleton instance = null;public static Singleton getInstance() {if (null == instance){instance = new Singleton();}return instance;
}

这是懒汉式中最简单的一种写法,只有在方法第一次被访问时才会实例化,达到了懒加载的效果。但是这种写法有个致命的问题,就是多线程的安全问题。假设对象还没被实例化,然后有两个线程同时访问,那么就可能出现多次实例化的结果,所以这种写法不可采用。

懒汉式(线程安全)

但为了线程安全,不得不为获取对象的操作加锁,这就导致了低性能。

并且这把锁只有在第一次创建对象时有用,而之后每次获取对象,这把锁都是一个累赘(双重检测对此进行了改进)。

    private static Singleton instance = null;public static synchronized Singleton getInstance() {if (null == instance){instance = new Singleton();}return instance;}
}

懒汉式单例优缺点:

优点:
对象的创建是线程安全的。
支持延时加载。
缺点:获取对象的操作被加上了锁,影响了并发度。
如果单例对象需要频繁使用,那这个缺点就是无法接受的。
如果单例对象不需要频繁使用,那这个缺点也无伤大雅。

在Java中,当我们谈论“同步开销”时,我们指的是使用synchronized关键字来同步代码块或方法时所引入的性能成本。同步是为了防止多个线程同时访问和修改共享资源,从而导致数据不一致或其他线程安全问题。但是,同步也会带来一些性能上的损失,因为线程之间需要竞争锁,而且在等待锁的过程中可能会被阻塞,这些都是需要消耗计算资源的。

“同步代码块”是指使用synchronized关键字包裹的一段代码,它确保在同一时间只有一个线程可以执行这段代码。同步代码块通常用于保护对共享资源的访问,以避免并发问题。

“双重检查锁定”(Double-Checked Locking, DCL)实现单例(线程安全)

第一次 null 检查是在同步块外部进行的,它能够快速过滤掉那些不需要进入同步块的线程(即当单例实例已经被创建时)。而第二次 null 检查则是在确保线程安全的前提下,进一步确认实例是否已经被创建,从而避免不必要的实例创建过程。

双重检查锁定的主要目的是确保单例实例只被创建一次,并且在多线程环境下保持线程安全,同时尽量减少同步带来的性能开销。

private static Singleton instance = null;//用于存储单例的实例public static Singleton getInstance() {//因为本身"锁"资源就是一个比较昂贵的资源,为了避免跑得慢的线程去抢这个把锁,所以此处也要进行非空判断.if (null == instance){//先进来的部分线程在此处排队//当多个线程同时尝试获取单例实例时,由于 synchronized 关键字的存在,只有一个线程能够进入同步块,其他线程将被阻塞,等待锁被释放。这就形成了一种“排队”的效果。synchronized (Singleton.class){//此时判断的是已经抢到锁资源,进行排队的线程是否"来晚了"//看他有没有获得实例引用//防止二次创建//在同步块内部的第二次 null 检查。这是因为在多线程环境下,有可能出现两个线程同时通过了第一次 null 检查,但只有一个线程能够获得锁并进入同步块。如果没有内部的第二次 null 检查,那么第二个获得锁的线程可能会再次创建单例实例,从而导致单例模式失效。内部的第二次 null 检查确保了即使在这种情况下,单例实例也只会被创建一次。if (null == instance){instance = new Singleton();}}}return instance;} 

这种写法用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高,是对第三种写法的改进。为什么要做两次判断呢?这是为了线程安全考虑,还是那个场景,对象还没实例化,两个线程A和B同时访问静态方法并同时运行到第一个if判断语句,这时线程A先进入同步代码块中实例化对象,结束之后线程B也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了。

这种实现方式在 Java 1.4 及更早的版本中有些问题,就是指令重排序,可能会导致 Singleton 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化,就被另一个线程使用了。

要解决这个问题,需要给 instance 成员变量加上 volatile 关键字,从而禁止指令重排序。

而高版本的 Java 已在 JDK 内部解决了这个问题,所以高版本的 Java 不需要关注这个问题。

双重检测单例优点:

对象的创建是线程安全的。
支持延时加载。
获取对象时不需要加锁。

例子 1:两个线程几乎同时请求单例实例

线程A和线程B几乎同时调用getInstance()方法。
两者都发现instance是null,因此都尝试进入synchronized块。
假设线程A先获得了锁,它将创建一个Singleton实例并将其赋值给instance变量。
当线程A释放锁后,线程B将获得锁并进入synchronized块。但是,由于已经有了内部的第二次null检查,线程B将发现instance已经不再是null,因此它不会创建新的Singleton实例。
线程B将返回已经创建的Singleton实例。

例子 2:多个线程在不同时间点请求单例实例

线程A首先调用getInstance()方法,发现instance是null,于是它创建一个Singleton实例并返回。
稍后,线程B调用getInstance()方法。此时,由于instance已经非空,线程B将直接返回已经创建的Singleton实例,而不会进入synchronized块。
更多的线程在不同时间点调用getInstance(),但它们都将直接返回同一个Singleton实例,而不会创建新的实例。

例子 3:线程在单例实例创建过程中被阻塞

线程A进入synchronized块并开始创建Singleton实例。
在线程A还没有完成实例创建时,线程B尝试调用getInstance()方法。
线程B将被阻塞,直到线程A完成实例的创建并释放锁。
一旦线程A释放锁,线程B将获得锁并进入synchronized块。但是,由于内部的null检查,它将发现instance已经不再是null,并且不会尝试重新创建Singleton实例。
线程B(以及任何后续线程)将返回同一个已经创建的Singleton实例。

例子 4:线程在实例已创建后进入同步块

线程A首先调用getInstance()方法,并成功创建了Singleton实例。
稍后,线程B进入getInstance()方法,并通过了第一次null检查(尽管此时实例已经被创建)。
线程B进入synchronized块,但由于内部的第二次null检查,它发现instance已经非空,因此不会重新创建实例。
线程B直接返回已创建的Singleton实例。
这个例子展示了即使在实例已经存在的情况下,线程仍然可能会进入同步块,但由于内部的第二次检查,它不会重复创建实例。

例子 5:高并发下的单例获取

在高并发场景下,多个线程(线程A、B、C、D等)几乎同时调用getInstance()方法。
由于多线程的竞态条件,这些线程可能都会通过第一次的null检查。
这些线程将尝试获取Singleton.class的锁以进入同步块。
只有一个线程(例如线程A)将获得锁,并创建Singleton实例。
其他线程(B、C、D等)将被阻塞,直到线程A释放锁。
当线程A释放锁后,其他线程将依次获得锁,但由于内部的null检查,它们将不会重新创建实例。
所有线程最终都将返回同一个Singleton实例。
这个例子强调了在高并发环境下,DCL模式如何确保只有一个实例被创建,并且所有线程最终都获得相同的实例。

例子 6:异常处理中的单例获取

线程A进入getInstance()方法并开始创建Singleton实例。
在创建过程中,线程A遇到了一个异常(例如,内存不足),导致实例创建失败。
异常被捕获并处理,但没有重新尝试创建实例。
稍后,线程B调用getInstance()方法,并发现instance仍为null。
线程B成功创建Singleton实例并返回。
这个例子展示了在异常处理中,如果实例创建失败,DCL模式能够允许其他线程在后续尝试中成功创建实例。

例子 7:延迟初始化的效果

在程序启动初期,没有线程调用getInstance()方法,因此Singleton实例尚未被创建。
当程序运行一段时间后,某个功能需要Singleton实例时,线程A首次调用getInstance()方法。
线程A发现instance为null,并进入同步块创建实例。
后续对getInstance()的调用都将返回此先前创建的实例。
这个例子展示了DCL模式如何实现单例的延迟初始化,即只在首次需要时才创建实例。

静态内部类实现单例(线程安全)

静态内部类也称作Singleton Holder, 也就是单持有者模式, 是线程安全的, 也是懒惰模式的变形.

JVM加载类的时候, 有这么几个步骤:

①加载 -> ②验证 -> ③准备 -> ④解析 -> ⑤初始化

需要注意的是:

JVM在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类(SingletonHolder)的属性/方法被调用时才会被加载, 并初始化其静态属性(instance).

/*** 静态内部类模式, 也称作Singleton Holder(单持有者)模式: 线程安全, 懒惰模式的一种, 用到时再加载*/
final class StaticInnerSingleton {/** 禁用构造方法 */private StaticInnerSingleton() { }/*** 通过静态内部类获取单例对象, 没有加锁, 线程安全, 并发性能高* @return SingletonHolder.instance 内部类的实例*/public static StaticInnerSingleton getInstance() {return SingletonHolder.instance;}/** 静态内部类创建单例对象 */private static class SingletonHolder {private static StaticInnerSingleton instance = new StaticInnerSingleton();}
}

这个StaticInnerSingleton类是一个实现了单例设计模式的Java类。单例设计模式确保一个类只有一个实例,并提供一个全局访问点来获取这个唯一实例。这个特定的实现方式被称为“静态内部类”或“Singleton Holder”模式,它结合了线程安全和延迟初始化的优点。

类定义

final class StaticInnerSingleton: 这个类被声明为final,意味着它不能被继承。这是为了确保单例的唯一性和稳定性。

构造方法

private StaticInnerSingleton() { }: 私有构造方法确保外部代码不能直接通过new关键字创建StaticInnerSingleton的实例。这是实现单例模式的关键步骤之一,因为它防止了外部代码的非法实例化

获取单例的方法

public static StaticInnerSingleton getInstance(): 这是一个公开的静态方法,用于获取单例对象。由于它是静态的,你可以直接通过类名调用它,而不需要创建类的实例。

静态内部类

private static class SingletonHolder: 这是一个私有的静态内部类。由于它是私有的,外部代码无法直接访问它。静态内部类在这里起到了关键作用,因为它允许延迟初始化单例对象。
private static StaticInnerSingleton instance = new StaticInnerSingleton();: 在这个静态内部类中,我们创建了StaticInnerSingleton的一个静态实例。由于这个内部类是私有的,并且这个实例是静态的,它只会在第一次被引用时才会被初始化(即,当getInstance()方法首次被调用时)。这种延迟初始化的特性使得这个单例模式实现既是线程安全的,又是懒惰初始化的。

线程安全性

这种实现方式的线程安全性是由Java的类加载机制保证的。在Java中,类的加载和初始化是线程安全的,也就是说,当多个线程同时尝试加载和初始化一个类时,Java虚拟机(JVM)会确保这个类只被加载和初始化一次。因此,即使多个线程同时调用getInstance()方法,SingletonHolder类(以及其中的instance字段)也只会被初始化一次,从而保证了单例的唯一性。

“静态内部类”或“Singleton Holder”模式是一种高效且线程安全的单例实现方式。它结合了懒惰初始化和线程安全的优点,而且由于其简洁和高效的特性,在实际开发中经常被使用

多线程环境下创建单例对象需要确保线程安全,即无论多少个线程同时尝试获取单例,都应该得到同一个实例,且该实例只被创建一次。StaticInnerSingleton 类使用了静态内部类(也称为嵌套类)来实现这一点,这是一种既线程安全又延迟初始化的单例实现方式。

下面,我将结合StaticInnerSingleton类,给出几个多线程创建单例的例子。

例子1:直接通过getInstance方法获取单例

多个线程可以同时调用getInstance方法,但是由于静态内部类的特性,SingletonHolder.instance只会在首次被引用时初始化,因此是线程安全的。

public class SingletonDemo {  public static void main(String[] args) {  // 模拟多线程环境  Runnable task = () -> {  StaticInnerSingleton instance = StaticInnerSingleton.getInstance();  System.out.println(Thread.currentThread().getName() + ": " + instance);  };  // 创建并启动多个线程来获取单例对象  Thread t1 = new Thread(task, "Thread-1");  Thread t2 = new Thread(task, "Thread-2");  Thread t3 = new Thread(task, "Thread-3");  t1.start();  t2.start();  t3.start();  }  
}

在这个例子中,我们创建了三个线程,每个线程都尝试获取StaticInnerSingleton的实例。由于使用了静态内部类实现单例,因此无论多少个线程同时尝试获取,都只会得到一个相同的实例。

例子2:在并发环境中验证单例的唯一性

我们可以进一步验证在多线程环境中单例的唯一性。

import java.util.HashSet;  
import java.util.Set;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.TimeUnit;  public class SingletonConcurrencyTest {  public static void main(String[] args) throws InterruptedException {  // 创建一个固定大小的线程池  ExecutorService executor = Executors.newFixedThreadPool(10);  Set<StaticInnerSingleton> instances = new HashSet<>();  // 提交多个任务来验证单例的唯一性  for (int i = 0; i < 100; i++) {  executor.submit(() -> {  StaticInnerSingleton instance = StaticInnerSingleton.getInstance();  synchronized (instances) {  instances.add(instance);  }  });  }  // 关闭线程池并等待所有任务完成  executor.shutdown();  executor.awaitTermination(1, TimeUnit.MINUTES);  // 验证是否所有实例都是相同的(即集合中只有一个元素)  System.out.println("Number of unique instances: " + instances.size()); // 应该输出 1  }  
}

在这个例子中,我们创建了一个包含100个任务的线程池,每个任务都尝试获取StaticInnerSingleton的实例并将其添加到一个同步集合中。最后,我们验证集合中实例的数量,以确认是否所有线程都获取到了相同的单例对象。如果单例实现是正确的,那么集合中应该只有一个唯一的实例。

这些例子展示了如何在多线程环境中安全地创建和使用单例对象,同时验证了StaticInnerSingleton类的线程安全性和单例保证。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/16782.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IntelliJ IDEA工具的常用快捷键使用

1&#xff0e;单行注释&#xff1a; ctrl / 2. 多行注释&#xff1a; ctrl shift / 3&#xff0e;查看源码&#xff1a;按 ctrl 别松手&#xff0c;鼠标移动到对应的类名下方&#xff0c;出现下划线&#xff0c;点击过去&#xff0c;可以查看类源码。 4&#xff0e;多行编辑…

RTDETR结合CVPR2024最新图像增强算法!让你的模型无惧风雨【含端到端推理脚本】

如何有效地探索雨痕的多尺度表示对于图像去雨是很重要的。与现有的基于Transformer的方法相比,这些方法主要依赖于单一尺度的雨痕外观,我们开发了一个端到端的多尺度Transformer,利用各种尺度中潜在有用的特征来促进高质量的图像重建。为了更好地探索空间变化的雨痕的常见退…

el-table自定义表头数据不更新

我的表头是有三层的&#xff0c;中间一层展示对应的数据&#xff0c;所以需要自定义&#xff0c;官方的文档显示的写法如下&#xff1a; <el-table-column><template slot“header”><div>{{dayData.supply}}、{{dayData.use}}</div></template>…

Effective C++(1)

文章目录 1. 让自己习惯C条款1&#xff1a;视C为一个语言联邦条款2&#xff1a;尽量以const、enum、inline替换#define条款03&#xff1a;尽可能使用 const条款4&#xff1a;确保对象在使用之前被初始化 1. 让自己习惯C 条款1&#xff1a;视C为一个语言联邦 今天的C已经是多个…

原生html和js实现瀑布流布局(macyjs插件,不依赖于jquery,纯原生)

官网地址 方式一&#xff1a;在github上找到项目&#xff0c;复制demo/assets/css/macy.css&#xff0c;以及/dist/macy.js 直接引入项目 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv&qu…

如何理解类的符号引用?类的直接引用?

在Java中&#xff0c;符号引用&#xff08;symbolic reference&#xff09;和直接引用&#xff08;direct reference&#xff09;是理解Java类加载和内存管理的重要概念。它们涉及到JVM如何在运行时处理类、方法、字段等的引用。下面是对这两个概念的详细解释&#xff1a; 符号…

junit-platform-engine旧版本无法更新问题

现象&#xff1a; 运行groovy测试类&#xff0c;一直使用的是低版本的junit-platform-engine-1.5.2.jar。即使在最外层强制升级版本也没有用 解决&#xff1a; 在最外层pom.xml引入高版本的父pom即可 <dependencyManagement><dependencies><dependency>&…

满帮集团 Eureka 和 ZooKeeper 的上云实践

作者&#xff1a;胡安祥 满帮集团&#xff0c;作为“互联网物流”的平台型企业&#xff0c;一端承接托运人运货需求&#xff0c;另一端对接货车司机&#xff0c;提升货运物流效率。2021 年美股上市&#xff0c;成为数字货运平台上市第一股。根据公司年报&#xff0c;2021 年&a…

网络协议——FTP(简介、搭建FTP服务端)

一、简介 1、什么是FTP&#xff1f; FTP&#xff08;File Transfer Protocol&#xff0c;文件传输协议&#xff09; TCP/IP 协议组的协议之一。常用20&#xff08;数据&#xff09;、21&#xff08;命令&#xff09;端口作为通讯端口。&#xff08;22为SSH端口&#xff09;F…

C++ 指针占用的大小是多少

作为一个C程序员&#xff0c;我们可以详细探讨指针在C中的大小&#xff0c;以及通过代码示例来演示这一点。 C 指针占用的大小是多少 指针的大小代码示例代码解释运行结果结论 指针的大小 指针的大小主要取决于系统的架构&#xff08;如32位或64位&#xff09;和编译器的实现…

就业班 第三阶段(ELK) 2401--5.22 day3 filebeat+elk云部署

kafka集群 Windterm同步输入&#xff0c;多台机子可以同时输入同步输入 启动kafka需要启动两个 第一个 [rootkafka1 ~]# cd /usr/local/kafka_2.11-2.0.0/ [rootkafka1 ~]# nohup bin/zookeeper-server-start.sh config/zookeeper.properties &第二个 [rootkafka1 ~]#…

apache BeanUtils

一、populate 1、介绍 BeanUtils.populate(Object bean, Map properties) 方法实在org.apache.commons.beanutils.BeanUtils包下的一个一个方法。 该方法的方法头 此方法中&#xff0c;有两个参数&#xff0c;Object bean 为一个实体类&#xff0c;Map properties为一个map集…

20232810 肖峰 2023-2024-2 《网络攻防实践》实验十一

一、实践内容 &#xff08;1&#xff09;web浏览器渗透攻击 任务&#xff1a;使用攻击机和Windows靶机进行浏览器渗透攻击实验&#xff0c;体验网页木马构造及实施浏览器攻击的实际过程。 实验步骤&#xff1a; ①选择使用Metasploit中的MS06-014渗透攻击模块 ②选择PAYLOAD为任…

Linux 36.3@Jetson Orin Nano之系统安装

Linux 36.3Jetson Orin Nano之系统安装 1. 源由2. 命令行烧录Step 1&#xff1a;下载Linux 36.3安装程序Step 2&#xff1a;下载Linux 36.3根文件系统Step 3&#xff1a;解压Linux 36.3安装程序Step 4&#xff1a;解压Linux 36.3根文件系统Step 5&#xff1a;安装应用程序Step …

# Mybatis 高级用法和tk.mybatis使用

Mybatis 高级用法和tk.mybatis使用 文章目录 Mybatis 高级用法和tk.mybatis使用使用SelectProvider、InsertProvider、UpdateProvider、DeleteProviderSelectProvider使用例子 tk.mybatis引入依赖查询实现实体映射类实体类规范 dao层调用dao 使用SelectProvider、InsertProvide…

eBay运营账号防关联成功的关键因素是什么?

一、什么是ebay eBay如今的发展现状呈现出积极且充满活力的态势。作为全球知名的在线拍卖和购物平台&#xff0c;随着全球消费者对线上购物的需求不断增长&#xff0c;这为卖家提供了广阔的市场空间和盈利机会&#xff0c;但多账号的运营若处理不好容易引起账号被关联&#xf…

基于生命周期评价法的农田温室气体排放估算;农田CH4和N2O排放模拟;农田碳库模型和土壤呼吸等

目录 专题一 温室气体排放模拟研究 专题二 农田CH4和N2O排放模拟 专题三 农田碳库模型和土壤呼吸 专题四 基于生命周期评价法的农田温室气体排放估算 专题五-六 基于过程模型的温室气体排放模拟 专题七 案例模拟与疑难解答 更多应用 农业是甲烷&#xff08;CH4&#xff…

前端面试题日常练-day37 【面试题】

题目 希望这些选择题能够帮助您进行前端面试的准备&#xff0c;答案在文末。 1. 在jQuery中&#xff0c;以下哪个方法用于隐藏一个元素&#xff1f; a) .hide() b) .remove() c) .toggle() d) .fadeIn() 2. 哪个jQuery方法用于在元素的前面插入新的HTML内容&#xff1f; a…

全球前五!ATFX 2024年Q1业绩狂飙,6240亿美元交易量彰显实力

5月&#xff0c;密集发布的报告显示&#xff0c;强者恒强是差价合约行业不变的竞争逻辑。而ATFX最新展现的业绩无疑是这一逻辑的有力例证。依照惯例&#xff0c;知名行业媒体Finance Magnates日前公布了全球经纪商最为关注的2024年第一季度行业报告。报告数据显示&#xff0c;A…

如何使用Java中的PreparedStatement来防止SQL注入

SQL注入的原理 SQL注入是一种常见的网络攻击方式&#xff0c;攻击者通过在应用程序的输入字段中插入恶意的SQL代码&#xff0c;利用程序对用户输入数据的合法性没有判断或过滤不严的漏洞&#xff0c;欺骗数据库执行非授权的任意查询&#xff0c;从而获取、修改、删除或添加数据…