当前位置: 首页 > news >正文

单例模式:线程安全,以及volatile关键字

对于OOP语言而言,每new() 一个对象,就会有一个对象实例生成。但是很多时候需要在程序运行时全局使用同一个实例,避免生成多余的实例或者资源浪费。这类设计模式就是单例模式。

单例模式有如下要求:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

其中第二和第三点都是为了给第三点打补丁,如果不是由单例类本身创建和提供实例,那么就没办法在全局中确保使用同一个实例。单例模式通过设置私有的构造函数以防止外部创建对象,为此,需要设置一个对外接口getInstance()方法提供对象。

饿汉式

public class SingleObject {  // 1.饿汉模式,类加载时通过静态关键字就创建好对象private static SingleObject instance = new SingleObject();  private SingleObject() {  }  public static SingleObject getInstance() {  return instance;  }  }

饿汉式单例模式认为对象总会使用,因此提前创建好对象。这种方式线程安全,但是在类加载时就初始化,浪费内存,容易产生垃圾对象。

测试可以发现得到的是同一对象:

public class Main {  public static void main(String[] args) {  for (int i = 0; i < 50; i++) {  SingleObject instance = SingleObject.getInstance();  System.out.println(instance);  }  }  
}

输出:

SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
……

懒汉式

public class SingleObject {  //2. 懒汉模式  private static SingleObject instance;  private SingleObject() {  }  public static SingleObject getInstance() {  if (instance == null) {  instance = new SingleObject();  }  return instance;  }  }

和饿汉式单例模式不同,懒汉式单例模式只在外部第一次需要对象的时候才创建。这种方式也被称为懒加载(Lazyload),spring中的@Lazy注解通过类似的方式实现类的懒加载。避免了饿汉式浪费内存的问题。但是在多线程并发的情况下,由于多个线程可能同时进入判断if (instance == null),可能会导致多个实例的创建。

并发情况下测试代码:

public class Main {  public static void main(String[] args) throws InterruptedException {  int threadCount = 10;  CountDownLatch countDownLatch = new CountDownLatch(threadCount);  Runnable runnable = new Runnable() {  @Override  public void run() {  SingleObject instance = SingleObject.getInstance();  System.out.println(instance + ",线程" +  Thread.currentThread().getName());  countDownLatch.countDown();  }  };  for (int i = 0; i < threadCount; i++) {  new Thread(runnable).start();  }  }  
}

运行结果:

SingleObject.SingleObject@65a9a79e,线程Thread-4
SingleObject.SingleObject@65a9a79e,线程Thread-5
SingleObject.SingleObject@65a9a79e,线程Thread-2
SingleObject.SingleObject@6517209b,线程Thread-9 //新实例
SingleObject.SingleObject@65a9a79e,线程Thread-7
SingleObject.SingleObject@65a9a79e,线程Thread-3
SingleObject.SingleObject@65a9a79e,线程Thread-8
SingleObject.SingleObject@65a9a79e,线程Thread-1
SingleObject.SingleObject@2f992f4f,线程Thread-0 //新实例
SingleObject.SingleObject@65a9a79e,线程Thread-6
Process finished with exit code 0

懒汉式+双重检查

线程安全的加锁方式

避免线程不安全的方式最常用的就是加锁,对于懒汉式的改进很简单,就是直接加锁。

public class SingleObject {  //2. 懒汉模式  private static SingleObject instance;  private SingleObject() {  }  public static SingleObject getInstance() {  synchronized (SingleObject.class){  if (instance == null) {  instance = new SingleObject();  }  }  return instance;  }  
}

双重检查

加锁很重要的一点就是性能差,会影响效率。为此给出双重检查版本。

public class SingleObject {  //2. 懒汉模式  private static SingleObject instance;  private SingleObject() {  }  public static SingleObject getInstance() {  if (instance == null){  // 第一次检查,如果不为null,直接返回synchronized (SingleObject.class){  if (instance == null) {  // 第二次检查instance = new SingleObject();  }  }  }  return instance;  }  
}

结果

SingleObject.SingleObject@7a5f6ff8,线程Thread-5
SingleObject.SingleObject@7a5f6ff8,线程Thread-9
SingleObject.SingleObject@7a5f6ff8,线程Thread-7
SingleObject.SingleObject@7a5f6ff8,线程Thread-2
SingleObject.SingleObject@7a5f6ff8,线程Thread-3
SingleObject.SingleObject@7a5f6ff8,线程Thread-4
SingleObject.SingleObject@7a5f6ff8,线程Thread-8
SingleObject.SingleObject@7a5f6ff8,线程Thread-0
SingleObject.SingleObject@7a5f6ff8,线程Thread-1
SingleObject.SingleObject@7a5f6ff8,线程Thread-6Process finished with exit code 0

volatile关键字

private volatile static SingleObject instance;  

然而以上的双重检查并不能完全保障完美实现单例模式,因为instance = new Singleton()并非原子操作,包含给对象实例分配内存空间,调用构造方法初始化对象,将对象指向分配的内存空间三步。这三步可能由于提高执行效率而改变执行顺序。

a. memory = allocate() //分配内存  写操作
b. ctorInstanc(memory) //初始化对象 写操作
c. instance = memory   //设置instance指向刚分配的地址 写操作

被volatile关键字修饰的变量被称为volatile变量。通过插入内存屏障禁止指令重排序,保证写操作前的代码不会重排到写操作之后,读操作后的代码不会重排到读操作之前。此时会按照a-b-c的顺序执行。

如果没有加上volatile关键字,上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行instance = new Singleton()时,B线程进来执行到第一次if (instance == null)。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接返回一个未初始化的对象。

参考

Java设计模式—单例模式的实现方式和使用场景-阿里云开发者社区
Java中Volatile关键字详解 - 郑斌blog - 博客园
Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)-CSDN博客

http://www.wxhsa.cn/company.asp?id=2903

相关文章:

  • lilctf 部分wp - Elma
  • 用 Python 和 Tesseract 实现验证码识别
  • Java 和 Tesseract 实现验证码识别
  • 基于 Weiler–Atherton 算法的 IoU 求解
  • Selenium应用中的核心JavaScript操作技巧
  • 25.9.13 字符编码标准
  • 哭了,散了,明白了
  • 用 Java 和 Tesseract 实现验证码识别
  • Microsoft-Activation-Scripts,好用,记录一下。
  • 双重map 的赋值初始化
  • 0voice-1.4.1
  • 9.13 模拟赛 T3
  • Docker应用 - FileBrowser
  • AI踩坑之Nlog使用
  • 论文解读-《OpenGSL A Comprehensive Benchmark for Graph Structure Learning》 - zhang
  • Cmake介绍
  • Git 生成 ssh key
  • 基础篇:消息队列理论部分,另一种环境搭建Docker运行RabbitMQ
  • 项目案例作业1:学生信息管理系统(面向对象初步接触)
  • P1097 合唱队形
  • 一生一芯学习:pa2.1 RTFM
  • Linux网络:初识网络 - 详解
  • 20250909比赛总结
  • 又寸入生白勺司烤
  • Ubuntu 安装 GIPM
  • 手动下载vscode扩展的方法
  • GAS_Aura-Aura Projectile
  • CF1583F Defender of Childhood Dreams
  • scrollArea无法滚动
  • 时间序列分析(1)