设计模式—单例模式(Singleton)

单例模式是一种常用的设计模式,确保一个类只有一个实例并提供全局访问点。常见应用场景包括唯一序列生成、资源控制等。单例模式有内存优化、避免资源多重占用等优点,但也存在扩展困难和职责过重的缺点。常见的单例实现方式包括饿汉式、懒汉式、双重校验锁、静态内部类和枚举,其中枚举是防止反射构造的最安全实现。

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

一、什么是单例模式:

单例(Singleton)模式是一种常用的创建型设计模式。

简单来说就是一个类只能构建一个对象的设计模式。

核心作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。

二、单例模式的应用场景:

1、需要生成唯一序列的环境

2、需要频繁实例化然后销毁的对象。

3、创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 

4、方便资源相互通信的环境

举个例子:

1、windows桌面上的回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。

也就是说整个windows系统运行过程中只会维护一个回收站实例。

2、一般网站上统计实时在线人数的计数器也是单例模式。

三、单例模式的优缺点:

优点:

1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。

2、避免对资源的多重占用(比如写文件操作)。

缺点:

1、不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。 
2、由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。 
3、单例类的职责过重,在一定程度上违背了“单一职责原则”。

四、单例模式的实现:

单例模式大致的实现步骤:

1、私有构造函数,防止被实例化

2、持有私有静态实例

3、公开静态工厂方法,获取唯一可用的对象

单例模式的几种实现方式:

1、饿汉式

它是在类装载时实例化对象,所以不支持懒加载,但它是线程安全的,也是平常使用较多的一种方式。

/** 单例模式-饿汉式-线程安全 */
public class Singleton {
	// 私有构造函数,防止被实例化
	private Singleton(){}	
	// 单例对象 类加载时创建instance 避免了多线程同步问题
	private static Singleton instance = new Singleton();	
	// 静态工厂方法,获取唯一可用的对象
	public static Singleton getInstance() {
		return instance;
	}
}

2、懒汉式

需要用时实例化对象,支持懒加载,非线程安全。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。

/** 单例模式-懒汉式-非线程安全 */
public class Singleton {
	// 私有构造函数,防止被实例化
	private Singleton(){}	
	// 单例对象 此处赋值为null,目的是实现延迟加载
	private static Singleton instance = null;	
	// 静态工厂方法,创建唯一可用的对象
	public static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

那么如何让他支持多线程,变成线程安全呢?  加锁 synchronized

/** 单例模式-懒汉模式-线程安全 */
public class Singleton {
	// 私有构造函数,防止被实例化
	private Singleton(){};
	// 单例对象	此处赋值为null,目的是实现延迟加载
	private static Singleton instance = null;
	// 静态工厂方法,创建唯一可用的对象
	public static synchronized Singleton getInstance(){
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

这种方式具备很好的懒加载(lazy loading),能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。

那么继续优化看第三种实现方式 ↓

3、双检锁/双重校验锁

这种方式采用双锁机制支持懒加载、线程安全且在多线程情况下能保持高性能。

/** 单例模式-双检锁/双重校验锁-线程安全 */
public class Singleton {
	// 私有构造函数,防止被实例化
	private Singleton(){};
	// 单例对象	此处赋值为null,目的是实现延迟加载
	private static Singleton instance = null;
	// 静态工厂方法,创建唯一可用的对象
	public static Singleton getInstance(){
		// 双检锁/双重校验锁
		if (instance == null) {
			//同步锁
			synchronized(Singleton.class){
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

认真看的同学可能会发现其实这个Singleton 类虽然也加了锁synchronized 但是并没有解决多线程问题。

试想线程A走到第11行代码,Singleton 类第一次创建实例,同时线程B进来走到第8行代码。

这种情况下线程B第8行代码中 if (instance == null)就很有可能返回false,从而获取到未初始化完成的 instance

为什么 if (instance == null) 会有可能返回false 呢?  这里就涉及到了JVM编译器和CPU的指令重排

指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:

memory =allocate();    //1:分配对象的内存空间 

ctorInstance(memory);  //2:初始化对象 

instance =memory;     //3:设置instance指向刚分配的内存地址 

但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:

memory =allocate();    //1:分配对象的内存空间 

instance =memory;     //3:设置instance指向刚分配的内存地址 

ctorInstance(memory);  //2:初始化对象 

当线程A执行完1,3时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行  if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象

如何避免这一情况呢?我们需要在instance对象前面增加一个修饰符volatile。

/** 单例模式-双检锁/双重校验锁-线程安全 */
public class Singleton {
	// 私有构造函数,防止被实例化
	private Singleton(){};
	// 单例对象	此处赋值为null,目的是实现延迟加载
	// 添加 volatile 是为了操作此对象时防止JVM和CPU指令重排
	private volatile static Singleton instance = null;
	// 静态工厂方法,创建唯一可用的对象
	public static Singleton getInstance(){
		// 双检锁/双重校验锁
		if (instance == null) {
			//同步锁
			synchronized(Singleton.class){
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

volatile修饰符阻止了变量访问前后顺序的指令重排,保证了指令的执行顺序。

如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。

5、登记式/静态内部类

这种方式通过静态内部类支持懒加载、线程安全且在多线程情况下能保持高性能。

/** 单例模式-登记式/静态内部类-线程安全 */
public class Singleton {
	// 私有构造函数,防止被实例化
	private Singleton(){};
	// 此处使用一个内部类来维护单例
	private static class SingletonFatory{
		private static Singleton instance = new Singleton();
	}
	// 静态工厂方法,获取唯一实例对象
	public static Singleton getInstance(){
		return SingletonFatory.instance;
	}
	// 如果该对象被用于序列化,可以保证对象在序列化前后保持唯一性
	public Object readResolve() {
		return getInstance();
	}
}

instance 对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类SingletonFatory 被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。代码中readResolve()方法在下面补充。
6、枚举

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化(防止反射构造对象,反射构造对象会在下面做具体补充),不过不支持懒加载。

package test;
/** 单例模式-枚举-线程安全 */
public enum Singleton {
	INSTANCE;

	public void whateverMethod(){
		System.out.println("枚举类型实现单例模式!");
	}

        public static void main(String[] args) {
		Singleton.INSTANCE.whateverMethod();
	}
}

为了好理解点我加了个main方法实现调用,这样一看可能还会有点蒙(大神跳过),下面我写一种好理解点的。

package test;
/** 单例模式-枚举-线程安全 */
public class EnumSingleton {
	// 私有构造函数
	private EnumSingleton(){};
	public static EnumSingleton getInstance(){
            return Singleton.INSTANCE.getInstance();
        }
	//枚举-静态常量,隐式地用static final修饰过
	private enum Singleton{
            INSTANCE;
            private EnumSingleton singleton;
            //JVM会保证此方法绝对只调用一次
            //枚举实际上是类,这里是构造方法
            private Singleton(){
                singleton = new EnumSingleton();
            }
            public EnumSingleton getInstance(){
                return singleton;
            }
        }
}

 

下面做两点补充:

1、如果该单例类需要序列化则需加 readResolve() 方法,来确保对象在序列化前后保持唯一性;

具体实现在上面第5种实现方式代码里有增加readResolve() 方法。

2、反射构造对象:

以上第1-5种单例实现方式都有一个共同的问题:无法防止利用反射构造对象重复构建对象,下面我们在饿汉式单例模式的基础上来实现一下反射构造对象。

代码可以简单归纳为三个步骤:

1、获得单例类的构造器。

2、把构造器设置为可访问。

3、使用newInstance方法构造对象。

/** 单例模式-饿汉式-线程安全 */
public class Singleton3 {
	// 私有构造函数,防止被实例化
	private Singleton3(){}	
	// 单例对象 类加载时创建instance 避免了多线程同步问题,但容易产生垃圾对象
	private static Singleton3 instance = new Singleton3();	
	// 静态工厂方法,获取唯一可用的对象
	public static Singleton3 getInstance() {
		return instance;
	}
	
	public static void main(String[] args) throws Exception{
		//获得构造器
		Constructor con = Singleton.class.getDeclaredConstructor();
		//设置为可访问
		con.setAccessible(true);
		//构造两个不同的对象
		Singleton singleton1 = (Singleton)con.newInstance();
		Singleton singleton2 = (Singleton)con.newInstance();
		//验证是否是不同对象
		System.out.println(singleton1.equals(singleton2));
	}
}

运行结果:

false

最后为了确认这两个对象是否真的是不同的对象,我们使用equals方法进行比较。毫无疑问,比较结果是false。

第6种实现单例模式的方法(枚举)可以有效防止反射构造对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值