单例模式及其线程安全问题

news/2024/11/20 12:32:52/

目录

1.设计模式

2.饿汉模式

3.懒汉模式

4.线程安全与单例模式


1.设计模式

设计模式是什么?

设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案

这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的

单例模式的作用就是保证某个类在程序中只存在唯一一份实例,不会创建出多个实例(之前学过的JDBC编程,DataSource这样的类就适合单例模式)

特点:

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

单例模式分为"饿汉""懒汉"两种

2.饿汉模式

//饿汉模式
//此处保证只能创建一个实例
class Singleton{private static Singleton instance = new Singleton();//想要使用时,通过Singleton.getInstance()来获取!public static Singleton getInstance(){return instance;}//构造方法私有化,类外无法通过new来调用构造器创建实例!!private Singleton(){}
}
public class Thread {public static void main(String[] args) {Singleton singleton1 = Singleton.getInstance();Singleton singleton2 = Singleton.getInstance();System.out.println(singleton1==singleton2);}
}

构造器私有化之后是不能通过new来调用构造器实例化对象的

此处我们将这个实例设置成私有的,通过get方法来获取,并且将构造方法私有化,不能创建新实例,因此访问这个实例的时候,每次访问得到的是同一个引用 

 

private static Singleton instance = new Singleton();

Singleton这个属性和实例无关,是和类相关的,java代码中的每个类在编译完成后都会得到.class文件,JVM运行时会加载这个文件读取其中的二进制指令,并在内存中构造对应的类对象(Singleton.class),这个过程就是类加载的过程

该模式是如何保证实例唯一呢

1.static修饰的实例instance,让当前实例的属性是类属性.在类加载阶段就被创建,一个类只加载一次,这个实例只创建唯一一份

2.构造方法私有化,类外无法再创建新的实例 

这个单例模式的名称是"饿汉模式",这个名字的来由是与后面的"懒汉模式"相比较得出的,体现在:在类加载阶段,就直接创建出了实例,实在很靠前的阶段给人一种急迫的感觉,所以叫饿汉模式

3.懒汉模式

//懒汉模式
class SingletonLazy{private static SingletonLazy instance = null;public static SingletonLazy getInstance() {if(instance == null){instance = new SingletonLazy();}return instance;}private SingletonLazy(){};
}
public class Thread {public static void main(String[] args) {SingletonLazy singleton3 = SingletonLazy.getInstance();SingletonLazy singleton4 = SingletonLazy.getInstance();System.out.println(singleton3==singleton4);}
}

 

懒汉模式的实例初始情况下是null,并非是在类加载时就创建出来了,而是第一次使用的时候才创建出来的,如果没有使用,那么就不创建了,单从效率来说是更好的选择

4.线程安全与单例模式

上述两种模式在多线程环境下调用getInstance是否是线程安全的呢?

先分析一下饿汉模式

 在饿汉模式中.多线程调用只涉及到了"读" 操作,我们知道多个线程只读一个变量是安全的,那么这个饿汉模式就是安全的

再看懒汉模式

 这里涉及到了"读和写"两个操作,在多线程中调用,是不安全的

上述途中两个线程调用时,由于随机调度和指令重排序的特点,如果在t1线程还没有创建出实例,t2线程就调用,那么instance还是null,继续往下执行,那么t1t2会创建出两个实例,触发多次new操作了,就不满足单例模式这个应用场景的需求了!!导致了线程不安全

如何解决这个线程安全问题呢?

刚才的安全问题根本原因是读,比较,写这三个操作不是原子的,导致了t2读到的值可能是t1没来得及写的(脏读)

加锁肯定是解决线程安全问题的普适方法

public static SingletonLazy getInstance() {if(instance == null){synchronized (SingletonLazy.class){instance = new SingletonLazy();}}return instance;}

这种加锁方式是不可取的!!这里只给new操作加锁了,那么t2还是可能读到t1没来得及写的数据,所以我们要给整个操作加锁! 保证读,比较,new,写这几个操作整体是原子的,正确的加锁方法如下

public static SingletonLazy getInstance() {synchronized (SingletonLazy.class){if (instance == null) {instance = new SingletonLazy();}}return instance;
}

到这里t2读到的就是t1更新过的数据了,是一个非空值,不会触发if条件,也就不能new新的实例了,满足了单例模式的要求

但是我们每个线程调用get时都要加锁,加锁操作也是有开销的,频繁的加锁会降低效率.我们发现一旦有一个实例后,后续调用get时,instance肯定是非空的,就直接触发return,那么就不需要锁了!

所以我们再进行一个判定,如果对象还没创建就加锁,创建过了,就不加锁!

这种方式采用双校验锁机制,安全且在多线程情况下能保持高性能

getInstance() 的性能对应用程序很关键时使用

public static SingletonLazy getInstance() {if(instance == null){synchronized (SingletonLazy.class){if (instance == null) {instance = new SingletonLazy();}}}return instance;}

1处的if语句是判定是否需要加锁!

2处的if语句是判定是否要创建实例对象!

这两个连续相同的if语句在没有加锁的情况下是没有意义的,一个两个效果相同,但是中间加了锁,就可能引起线程阻塞,等到解锁之后,第一个if和第二个if之间对于计算机来说已经沧海桑田了!程序内部的状态,变量的值都可能发生很大改变

这样减少了不必要的加锁,但是还存在内存可见性问题!

假设有很多线程都来调用get,这个时候第一次调用是读内存,后续都是读寄存器/cache,那么就会有被优化的风险!

还有指令重排序引入的线程安全问题,new操作可以拆分为三个步骤

1.申请内存空间

2.调用构造方法,初始化对象

3.把空间地址赋给instance引用

编译器可能会为了提高程序效率将指令执行顺序调整,1不会被调整.23会被调整,单线程情况写123,132没有本质区别,最后都能new出实例对象,但是多线程情况下,t1如果执行132,执行到13后就被切换到t2来执行,此时t1的2还没有执行,instance仍然是一个null,t2却认为t1已经执行完3了,那么此处的引用就是非null的了,按照代码t2会直接返回一个instance引用,可能还会尝试使用引用中的属性,但是这是一个非法的实例对象,它并没有被构造完成!

解决内存可见性,指令重排序问题需要用到关键字--volatile

所以要使用volatile修饰instance!!这样就能解决内存可见性和指令重排序

 线程安全的饿汉版单例模式:

class SingletonLazy{private volatile static SingletonLazy instance = null;public static SingletonLazy getInstance() {if(instance == null){synchronized (SingletonLazy.class){if (instance == null) {instance = new SingletonLazy();}}}return instance;}private SingletonLazy(){};
}


http://www.ppmy.cn/news/5494.html

相关文章

总线一:IIC

一、I2C集成电路总线, 多用于主控制器和从器件间的主从通信。 二、适用场景:在小数据量场合使用,传输距离短。 三、IIC是半双工。IIC的物理层:两条总线线路,一条是串行数据线SDA,一条是串行时钟线SCL,当总…

SSM框架项目实战-CRM(客户关系管理1)

目录​​​​​​​ 1 项目介绍 1.1 crm简介 1.2 业务流程 1.3 crm的技术架构 2 物理模型设计 2.1 crm表的结构 2.2 主键字段 2.2 外键字段 2.3 关于日期和时间的字段 3 搭建项目环境 3.1 添加maven依赖 3.2 添加配置文件 3.3 添加页面和静态资源 ​编辑 4 首页…

第二章:Linux的目录结构-[基础篇]

一:基础介绍 linux的文件系统是采用级层式的数状目录结构,在此结构中的最上层是根目录“/”,然后在此目录下再创建其他的目录。 深刻理解linux树状文件目录是非常重要的,这里我给大家说明一下。 记住一句经典的话:在Li…

设计模式-抽象工厂模式

1、什么是抽象工厂模式 抽象工厂(AbstractFactory)模式的定义:是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。抽象工厂模式是工厂方法模式的…

Kotlin协程笔记:CoroutineScope管理协程

CoroutineScope 是实现协程结构化并发的关键。使用 CoroutineScope,可以批量管理同一个作用域下面所有的协程。 CoroutineScope 与 结构化并发 launch、async 被定义成了 CoroutineScope 扩展函数。在调用 launch 之前,必须先获取 CoroutineScope。 pub…

设计模式之迭代器模式

Iterator design pattern 迭代器模式的概念、迭代器模式的结构、迭代器模式的优缺点、迭代器模式的使用场景、迭代器模式的实现示例、迭代器模式的源码分析 1、迭代器模式的概念 迭代器模式,即提供一种方法来顺序访问聚合对象内的元素,而不暴露聚合对象…

gRPC学习Go版(一)

文章目录微服务入门gRPC是什么proto 服务定义gRPC 优势gRPC入门简单使用一元RPC服务流RPC客户流RPC双工流RPCgRPC底层原理RPC流长度前缀的消息分帧请求消息响应信息通信模式下的消息流微服务入门 现在的软件很少是一个孤立的单体应用运行的,相反更多是通过互联网连接…

Pytorch/Paddle topk 与 Numpy argpartition 函数应用

前言 他们两者都在些搜索、匹配、找相关性的时候会用到。 topk 参数 torch.topk(input, k, dimNone, largestTrue, sortedTrue, *, outNone) paddle.topk(x, k, axisNone, largestTrue, sortedTrue, nameNone) input / x : 输入的多维Tensor,支持的数据类型 float32、float64、…