【设计模式】单例模式 - 其实很简单

本文详细介绍了Java中的单例模式,包括其概念、特点以及两种常见实现方式:饿汉式和懒汉式。饿汉式在类加载时即初始化单例,保证线程安全;懒汉式则在首次调用时实例化,可能存在线程安全问题。为解决这个问题,文章讨论了如何通过synchronized和双重检查锁实现线程安全的懒汉式单例,同时也提到了volatile关键字在防止指令重排中的作用。

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

一、什么是单例模式
当我们要调用一个类时,如果不是单例模式,那么每次调用都会去创建这个类的实例对象。而如果是单例模式的话,这个类在内存中只会创建唯一一个实例对象,每次我们要调用这个类的时候,就去获取这个实例对象,而不需要(也不能)多次创建实例对象。
那么,为什么需要单例模式呢?这是因为有些类的实例对象在创建和销毁的时候开销较大,而且这些对象完全是可以被复用的话,频繁地创建和销毁对象就会造成不必要的性能浪费。

二、单例模式的3个特点
我们暂且把实现了单例模式的类称为单例类,单例类有以下3个特点:
①单例类的构造函数必须是private的,这是为了确保外部无法创建单例类的实例对象。
试想下如果构造函数不是private的,那么程序员就可以从外部new一个单例类对象,那么就可能有多个单例类对象了。
②单例类中实例化了一个自己的对象。
③单例类中实现了一个提供其实例对象的接口。

三、饿汉式和懒汉式
饿汉式和懒汉式是两种常见的单例模式实现方式。区别在于,饿汉式在单例类中定义对象变量时就将对象实例化了,而懒汉式会等到第一次调用单例类的时候才会去实例化对象。这样讲好像有点抽象,看一下下面代码就可以了。
首先是饿汉式的实现代码:

public class Singleton{
	//private的构造函数
	private Singleton(){
	}
	//定义一个变量保存自己的对象,并实例化对象
	private static final Singleton singleton = new Singleton();
	//提供一个返回singleton的接口
	public static Singleton getInstance(){
		return singleton;
	}
}

饿汉式的代码逻辑很简单,就是按照我们在上一节提到的三点去实现就好了。这里注意一下getInstance()方法是public static的,因为我们要通过类来调用这个方法,所以必须是static的。而类中的实例对象必须是private static的,因为静态方法(这里的getInstance())只能访问静态变量;而我的代码中还加了final,这是为了避免被变量被修改。

看完了饿汉式,接下来我们来实现一下懒汉式:
懒汉式的基本写法:

public class Singleton{
	//private的构造函数
	private Singleton(){
	}
	//定义一个变量保存自己的对象
	private static Singleton singleton = null;
	//提供一个返回singleton的接口
	public static Singleton getInstance(){
		//第一次调用单例类的时候需要实例化对象
		if(singleton == null){
			singleton = new Singleton();
		}
		return singleton;
	}

}

上面这段代码就是懒汉式的基本写法了,与饿汉式的最大区别就是在定义变量保存实例对象时,懒汉式没有立刻去实例化对象,而是等到被第一次调用时才去实例化,这个过程也被称为懒加载。
但是还有一个区别,饿汉式由于在定义变量的时候就创建对象了,所以是线程安全的,可以保证对象只被实例化一次(静态变量只有在类被加载时才会被初始化)。而懒汉式则无法保证线程安全(可能有多个线程同时进入到if语句中),因此我们需要对懒汉式进行一个改进,使它变成线程安全的。
懒汉式之改进方法一:用synchronized给整个方法加锁:

public class Singleton{
	//private的构造函数
	private Singleton(){
	}
	//定义一个变量保存自己的对象
	private static Singleton singleton = null;
	//提供一个返回singleton的接口
	public static synchronized Singleton getInstance(){
		//第一次调用单例类的时候需要实例化对象
		if(singleton == null){
			singleton = new Singleton();
		}
		return singleton;
	}
}

这种方式确实是线程安全的。但是这种方式给整个方法上锁了,因此每次进入这个方法都需要获得锁,效率不高。而我们真正的目的是为了避免多个线程执行singleton = new Singleton();这句实例化对象的语句,因此我们可以把锁的范围缩小一点。
懒汉式之改进方法二:用synchronized给指令上锁(为了便于观看,我把多余的注释删掉了):

public class Singleton{
	private Singleton(){}
	//加了volatile
	private volatile static Singleton singleton = null;
	
	public static Singleton getInstance(){
		if(singleton == null){
			synchronized(Singleton.class){
				//双重检查锁
				if(singleton == null){
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
}

代码中我们依旧使用synchronized进行上锁,不过这次只锁住了一条语句。但在锁住的语句中我们又进行了一次判空,这就构成了双重检查锁,为什么我们要这么做呢?这是因为虽然只能有一个线程获得锁并执行实例对象的语句(即singleton = new Singleton();),但可能有多个线程同时进入了if语句中,一旦获得锁的线程执行完实例对象语句并释放锁之后,其他线程就会立刻抢占锁并且又执行了一次实例对象语句,这就导致对象被多次实例化了。所以我们利用双重检查锁,在实例化之前又进行了一次判空,从而避免这个问题。
此外,我们还在定义变量的时候使用了volatile关键字private volatile static final Singleton singleton = null;
这是因为JVM内部可能会进行一个指令重排的操作:实例化对象的语句,虽然代码只有一行,但在JVM中实际是分为3个步骤:①分配内存。②初始化对象。③变量指向对象的地址。这3个步骤不一定是按顺序执行的。为了执行效率,JVM有时会对指令进行重排,也就是打乱它们的执行顺序。
指令重排会导致我们上述代码中出现问题:假如现在我们有一个线程a执行了实例化对象的语句,指令重排后的执行顺序是①③②,当a执行完③准备执行②的时候,线程b获得了锁并进入到if语句中,由于此时变量中已经有了地址,所以不是null,所以b就return了变量,但此时实例对象还没有被初始化,因此程序就会出错。
为了防止这个问题,所以我们在定义变量的时候使用了volatile关键字,这个关键字就能解决上面这个问题。(关于volatile的用法,在此不做展开,只要知道我们在这里用volatile是为了解决指令重排问题即可。)

四、总结一下
本篇博客介绍了什么是单例模式,以及单例模式常见的两种实现方式:饿汉式和懒汉式。饿汉式的代码比较简单,而懒汉式则要注意线程不安全的问题,因此要考虑如何使用synchronized上锁,以及双重检查锁和volatile关键字都是针对什么问题才使用的。

补充问题:
多个线程是否可以同时获得单例类的实例对象并使用这个对象呢?如果可以,它们之间会不会产生干扰?
留给大家思考。

----完结----

参考资料:

  • https://www.bilibili.com/video/BV1pt4y1X7kt?spm_id_from=333.337.search-card.all.click
  • https://blog.youkuaiyun.com/jason0539/article/details/23297037?spm=1001.2014.3001.5502
  • https://blog.youkuaiyun.com/weixin_41949328/article/details/107296517?spm=1001.2014.3001.5502
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值