理解单例模式

本文详细介绍了单例模式的概念,包括线程安全与效率的权衡,以及懒汉模式、饿汉模式、静态内部类和双重校验锁四种实现方式。其中,volatile关键字在保证线程可见性和禁止指令重排序方面的角色也得到了详细阐述。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

理解单例模式
首先我们来对单例模式的概念了解一下。
单例模式:对象单例设计,就是设计类时保证类的实例在内存中只有一份。

实现方式有:
1)内部设计实现(对类自身进行设计)
2)外部设计实现(对类的对象提供一种池)

那么我们 要思考的是:如何保证类的设计在内存中只有一份类的实例?
下面我们介绍几种模式:
一:懒汉模式
懒汉模式-1:
缺陷:线程不安全的单例设计(适用单线程)
``java`
class Singleton01{
//构造方法私有化,不允许外界直接构建对象

private Singleton01() {
	System.out.println("Singleton01()");
}
private static Singleton01 instance;
public  static Singleton01 getInstance() {
	if(instance == null) {
		instance = new Singleton01();
	}
	return instance;

}

}
此类的设计存在线程不安全?
思考:导致线程不安全的原因:
1):多个线程并发执行
2):多个线程有共享数据集
3):多个线程在共享数据集上的操作是非原子(不可再分对象)操作。
原子操作:必须是一个线程执行完了一个操作,其它线程才能执行操作


懒汉模式-2:
优点:线程安全
缺陷:低效率 (synchronized:要让多个线程在这个代码块上顺序执行)

class Singleton02{
	//构造方法私有化,不允许外界直接构建对象
	private Singleton02() {
		System.out.println("Singleton02()");
	}
	private static Singleton02 instance;
	//保证线程安全可以加锁synchronized,但是加锁后会按顺序执行,效率降低
	//synchronized:保证代码的原子性(不能同时又多个线程对这个代码块进行访问)
	//synchronized:要让多个线程在这个代码块上顺序执行
	//此设计虽然保证了安全 ,但是性能会被降低
	public  synchronized static  Singleton02 getInstance() {
		if(instance == null) {
			instance = new Singleton02();
		}
		return instance;
	}
}

二:饿汉模式
优点:线程安全且高效无阻塞的单例设计
适用:适用小对象,频繁使用
此单例的缺陷:隐式加载都会执行类的初始化,会先初始化类变量(1次)后续多线程访问的时候已经有值了。
可能会对资源占用较多,尤其是对大对象(长时间不用的对象),造成内存占用浪费。

class Singleton05{
	//大对象(长时间不用)
	//int[] array= new int[2048];
	//构造方法私有化,不允许外界直接构建对象
	private Singleton05() {
		System.out.println("Singleton05()");
	}

	//类加载的时候构建类的实例,类变量初始化一次
	private  static Singleton05 instance = new Singleton05();

	public  static  Singleton05 getInstance() {
		return instance;
	}
	public static void show() {

	}
}

三:静态内部类
优点:线程安全且高效无阻塞的单例设计
适用:适用,大对象,频繁用(多线程高并发,大量访问 )

class Singleton06{
	int[] array= new int[2048];
	//构造方法私有化,不允许外界直接构建对象
	private Singleton06() {
		System.out.println("Singleton06()");
	}
	//通过内部类实现属性的延迟初始化(懒加载/延迟加载)
	static class inner{
		private  static Singleton06 instance = new Singleton06();
	}

	public  static  Singleton06 getInstance() {
		return inner.instance;
	}
	//public static void show() {}  访问show方法是内部类不会被加载

}

四:双重校验锁
优点:线程安全且高效有阻塞或少阻塞的单例设计
适用:大对象,稀少用(并发小,并发访问少 )

class Singleton04{
	//构造方法私有化,不允许外界直接构建对象
	private Singleton04() {
		System.out.println("Singleton04()");
	}
	
	//记住:当多个线程对一个共享变量进行操作时,使用volatile关键字
	private static volatile Singleton04 instance;

	public  static  Singleton04 getInstance() {
		//静态方法的锁,默认是类名.class.字节码对象
		//实例方法的锁,默认是this(关键字)
		if(instance==null) {
			synchronized(Singleton04.class) {
				if(instance == null) {
					instance = new Singleton04();
				}
			}
		}
		return instance;
	}
}

volatile关键字(修饰类中属性)的作用:
1)保证线程的可见性
分析:操作系统给JVM开辟内存空间,运行程序可能是多个CPU,CPU执行程序要把代码读到CPU
CPU中有cache(高速缓冲存储器),CPU执行计算会做以下内容:
①会把主内存(操作系统给JVM开辟内存空间)的数据读到CPU内部的cache中
②操作系统和硬件进行调度的时候,需要 总线 来传递数据到CPU
CPU要读取内存空间的数据,是通过一个线程thread来读的 总线(bus)
总线(bus):计算机各种功能部件之间传递信息的公共通信干线。

保证线程的可见性:
一旦有一个线程修改了变量的取值,还会把这个值写回到主内存,一旦写回到主内存,如果这个
变量使用volatile关键字修饰,会立刻向总线发一个消息,这个消息就是通知其它线程我这个
变量的值已经改变了,其它线程不要修改值往主内存写了。然后这个CPU会立刻设置为这个线程
中的cache中的数据失效,它再想修改就要从主内存中重新去读取。
操作系统立刻通知总线(bus)其它线程这个变量值已经发生改变,你给原先CPU的值已经失效了。(底层做的事)

2)禁止指令重排序:
如instance = new Singleton04();会有如下过程
内存分配空间–>属性初始化–>调用构造方法–>为instance 赋值
可能存在内存分配空间后,有内存地址,直接为instance 赋值内存地址
这就没有按照顺序执行,加volatile关键字禁止指令重排序,按顺序执行
3)但不保证其原子性


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值