单例模式(一):单例的七种实现

本文深入解析单例模式的原理及应用场景,探讨其在系统资源管理中的优势,如数据库连接池和日志管理。并详细介绍了单例模式的多种实现方式,包括饿汉模式、懒汉模式、双检锁模式、Holder模式和枚举方式,每种方式的优缺点及适用场景。

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

单例模式是什么?

单例模式:某个类只能有一个实例,提供一个全局的访问点。
不管什么时候,都要确保某一个类只有一个实例,而且是自行实例化并向整个系统提供这个实例,这样的一个类称为单例类,它提供全局访问的方法。
单例模式是一种对象创建型模式。

单例模式为什么存在?

单例模式存在的意义是什么?简单来说就是单例模式的需求场景是哪些?
其实单实例模式在我们平常的使用中是很常见的,比如我们经常会调用某个数据的连接池。
那个就是单例模式,为了保证整个系统中只有一个连接池,所有的人访问同一个连接池。
为什么需要保证系统里面只有一个这样的对象呢?
主要为了节约资源,就拿我们数据库连接池来说,你一个连接池管理50个链接和两个连接池分别管理25个链接,其实使用的复杂度是不一样的,如果是两个,如果有个被占满了,你就选择那个没有占满的;同时你不知道啥时候会出现占满的情况,这就导致你每次用连接池的时候都要进行判断,这样操作无疑就增加了复杂度,而且没啥必要。而且系统生成两个对象还造成了不必要的资源浪费,堆栈都要开辟空间,这些都是没啥必要的资源浪费。
还有日志,这个就是典型的单例了,每个java类中,只要保证只要对应本
意义:减少内存开销,节约系统资源。

单例模式如何实现?

(一)饿汉思想

这个可以保证只有一个实例,因为在类初始化的时候就创建了实例了。
缺点: 堆内存中驻留很长时间,但是却不一定会被使用。

/**
 * hungry mode 
 * @author wanghanwei.wb
 */
public final class SingletonHungry {

	private byte[] date = new byte[1024];
	
	private static SingletonHungry instance = new SingletonHungry();
	
	private SingletonHungry() {
		
	}
	
	public static SingletonHungry getInstance() {
		return instance;
	}
}


(二)懒汉思想

优点: 在需要的时候才被实例化
缺点: 存在线程安全的问题,

public final class SingletonLazy {
	
	private byte[] date = new byte[1024];
	
	private static SingletonLazy instance = null;
	
	private SingletonLazy() {
		
	}
	public SingletonLazy getInsatnce() {
		if(instance == null) {
			instance = new SingletonLazy();
		}
		return instance;
	}
}

(三)懒汉思想+同步方法

优点: 没有了并发的问题
缺点: 每次只能有一个线程获取getInstance()方法的使用权,别的线程同时访问的时候只能等待,性能不佳。

public final class SingletonSync {
	private byte[] data = new byte[1024];
	private static SingletonSync instance = null;
	private SingletonSync() {
		
	}

	public static synchronized SingletonSync getInstance() {
		if (instance == null) {
			instance = new SingletonSync();
		}
		return instance;
	}
}


(四)double check 实现方式

优点: 解决高并发的问题的同时,也解决了每次只能有一个线程访问的难题
缺点: 因为CPU的指令重排的,依然存在风险。

/**
 * double check 
 * @author wanghanwei.wb
 *
 */
public class SingletonDoubleCheck {
	private byte[] data = new byte[1024];
	private static SingletonDoubleCheck instance = null;
		private SingletonDoubleCheck() {
	}

	public static SingletonDoubleCheck getInstance() {
//		first check
		if (instance == null) {
		// 要保证所有的线程用的是同一个 单例对象,所以需要锁了class对象。
			synchronized (SingletonDoubleCheck.class) {
//				second check
				if (instance == null) {
					instance = new SingletonDoubleCheck();
				}
			}
		}
		return instance;
	}
}

(五)double check 安全实现方式

double check的潜在危险:因为cpu的指令重排导致的问题:比如说instance已经不是null了,但是依然没有执行实例化的代码,所以获取conn和socket依然为null,使用者会有空值异常。
这么说你可能会迷糊,但是这个重要的就是new的过程:
这里引用《极客时间-Java并发编程实战-01 | 可见性、原子性和有序性问题:并发编程Bug的源头》

我们以为的 new 操作应该是:
在堆内存中分配一块内存 M;
在内存 M 上初始化 Singleton 对象;
然后 M 的地址赋值给 栈内存上的instance 变量。

但是实际上优化后的执行路径却是这样的:
在堆内存中分配一块内存 M;
将 M 的地址赋值给 栈内存上的 instance 变量;
最后在内存 M 上初始化 Singleton 对象。

所以引用instance不为空的时候,实际上引用对应的堆内存依然没有null,但是别的线程判断instance == null的时候,就已经不会判断为null了,就会让别的线程执行别的逻辑。
错误代码

import java.net.Socket;
import java.sql.Connection;
/**
 * 		存在因为指令重排而导致的,别的对象还没被初始化,导致的,别的对象被使用的时候报空指针。
 * @author wanghanwei.wb
 *
 */
public final class SingletonDoubleCheckDanger {
	
	private byte[] data = new byte[1024];
	
	private static SingletonDoubleCheckDanger instance  = null;
	
	Connection conn;
	
	Socket socket;
	
	private SingletonDoubleCheckDanger() {
		this.conn = null;// 初始化conn,这里用null代替步骤,大家意会就好
		this.socket  = null;// 初始化socket,这里用null代替步骤,大家意会就好
	}
	
	public static SingletonDoubleCheckDanger getInstance() {
		if (instance == null) {
//			要保证所有的线程用的是同一个 单例对象,所以需要锁了class对象。
			synchronized (SingletonDoubleCheckDanger.class) {
				if (instance == null) {
					instance = new SingletonDoubleCheckDanger();
				}
			}
		}
		return instance;
	}
}

用关键字volatile来防止指令重排。
正确代码

import java.net.Socket;
import java.sql.Connection;

public class SingletonDoubleCheckSafe {
	
	private byte[] data = new byte[1024];
	//关键词来一个,防止指令重排
	private volatile static SingletonDoubleCheckSafe instance  = null;
	
	Connection conn;
	
	Socket socket;
	
	private SingletonDoubleCheckSafe() {
		this.conn = null;// 初始化conn
		this.socket  = null;// 初始化socket
	}
	
	public static SingletonDoubleCheckSafe getInstance() {
		if (instance == null) {
//			要保证所有的线程用的是同一个 单例对象,所以需要锁了class对象。
			synchronized (SingletonDoubleCheckDanger.class) {
				if (instance == null) {
					instance = new SingletonDoubleCheckSafe();
				}
			}
		}
		return instance;
	}
}

(六)Holder 方式

通过定义一个静态的内部类,根据类在初始化的时候会调用< clinit >()方法,来执行static成员变量赋值,以及执行static域的代码。< clinit >()方法是个同步方法,可以保证有序性,原子性,可见性,这个方法使用的最广。

public final class SingletonHolder {

	private byte[] data = new byte[1024];
	
	private SingletonHolder() {
	}
	
	private static class Holder {
		private static SingletonHolder instance = new SingletonHolder();
	} 
	
	public static SingletonHolder getInstance() {
		return Holder.instance;
	}
}

(七)枚举方式

《Effective Java》很推崇这种写法,但是本质上还是个饿汉思想实现的

public enum SingletonEnum {
	
	INSTANCE;
	
	private byte[] data = new byte[1024];
	SingletonEnum() {
		System.out.println(" instance will be initialized immediately.");
	}
	
	public static void method() {
//		调用的例子
		System.out.println("22222222222222");
	}
	
	public void getOtherMethod() {  
//		调用的例子
		System.out.println("111111111111111");
    }  
	public static SingletonEnum getInstance() {
		return INSTANCE;
	}
	
	public static void main(String[] args0) {
		SingletonEnum.method();
		SingletonEnum.INSTANCE.getOtherMethod();
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值