07 创建型模式-单例模式

1.单例模式介绍

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,此模式保证

某个类在运行期间,只有一个实例对外提供服务,而这个类被称为单例类。

2.使用单例模式要做的两件事
  1. 保证一个类只有一个实例
  2. 为该实例提供一个全局访问节点
3. 单例模式结构

4.1 单例模式之饿汉式:

在类加载期间初始化静态实例,保证 instance 实例的创建是线程安全的 ( 实例在

类加载时实例化,有JVM保证线程安全).

特点: 不支持延迟加载实例(懒加载) , 此中方式类加载比较慢,但是获取实例对象

比较快。

问题: 该对象足够大的话,而一直没有使用就会造成内存的浪费。

java 复制代码
public class Singleton_01 {

	//1. 私有构造方法
	private Singleton_01(){}

	//2. 在本类中创建私有静态的全局对象
	private static Singleton_01 instance = new Singleton_01();

	//3. 提供一个全局访问点,供外部获取单例对象
	public static Singleton_01 getInstance(){
		return instance;
	}

}
4.2懒汉式(线程不安全)

此种方式的单例实现了懒加载,只有调用getInstance方法时 才创建对象.但是如

果是多线程情况,会出现线程安全问题.

java 复制代码
public class Singleton_02 {

	//1. 私有构造方法
	private Singleton_02(){}

	//2. 在本类中创建私有静态的全局对象
	private static Singleton_02 instance;

	//3. 通过判断对象是否被初始化,来选择是否创建对象
	public static Singleton_02 getInstance(){
		if(instance == null){
			instance = new Singleton_02();
		}
		return instance;
	}

}

注意:

假设在单例类被实例化之前,有两个线程同时在获取单例对象,线程A在执

行完if (instance == null) 后,线程调度机制将 CPU 资源分配给线程B,此

时线程B在执行 if (instance == null)时也发现单例类还没有被实例化,这样

就会导致单例类被实例化两次。为了防止这种情况发生,需要对

getInstance() 方法同步处理。改进后的懒汉模式.

懒汉式(线程安全)

原理 : 使用同步锁 synchronized 锁住 创建单例的方法 ,防止多个线程同时调

用,从而避免造成单例被多次创建。

java 复制代码
public class Singleton_03 {

	//1. 私有构造方法
	private Singleton_03(){}

	//2. 在本类中创建私有静态的全局对象
	private static Singleton_03 instance;

	//3. 通过添加synchronize,保证多线程模式下的单例对象的唯一性
	public static synchronized Singleton_03 getInstance(){
		if(instance == null){
			instance = new Singleton_03();
		}
		return instance;
	}

}

缺点

懒汉式的缺点也很明显,我们给 getInstance() 这个方法加了一把大锁

(synchronzed),导致这个函数的并发度很低。量化一下的话,并发度

是 1,也就相当于串行操作了。而这个函数是在单例使用期间,一直会被调

用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,

如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶

颈,这种实现方式就不可取了。

双重校验

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一

种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方

式。

实现步骤:

  1. 在声明变量时使用了 volatile 关键字,其作用有两个:
    保证变量的可见性 :当一个被volatile关键字修饰的变量被一个线程修改的时
    候,其他线程可以立刻得到修改之后的结果。
    屏蔽指令重排序 :指令重排序是编译器和处理器为了高效对程序进行优化的手
    段,它只能保证程序执行的结果时正确的,但 是无法保证程序的操作顺序与代
    码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。
  2. 将同步方法改为同步代码块. 在同步代码块中使用二次检查,以保证其不被
    重复实例化 同时在调用getInstance()方法时不进行同步锁,效率高。
java 复制代码
public class Singleton_04 {

	//1. 私有构造方法
	private Singleton_04(){}

	//2. 使用 volatile保证变量的可见性
	private volatile static Singleton_04 instance = null;

	//3. 对外提供静态方法获取对象
	public static Singleton_04 getInstance(){
		//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
		if(instance == null){
			synchronized (Singleton_04.class){
				//抢到锁之后再次进行判断是否为null
				if(instance == null){
					instance = new Singleton_04();
				}
			}
		}
		return instance;
	}

}
静态内部类

原理 根据静态内部类 的特性(外部类的加载不影响内部类),同时解决了按

需加载、线程安全的问题,同时实现简洁。

  1. 在静态内部类里创建单例,在装载该内部类时才会去创建单例
  2. 线程安全:类是由 JVM 加载,而 JVM 只会加载1遍,保证只有1个单例
java 复制代码
public class Singleton_05 {

	private static class SingletonHandler{
		private static Singleton_05 instance = new Singleton_05();
	}

	private Singleton_05(){}

	public static Singleton_05 getInstance(){
		return SingletonHandler.instance;
	}

}
反射对于单例的破坏

反射技术过于强大,它可以通过 setAccessible() 来修改构造器,字段,方法

的可见性。单例模式的构造方法是私有的,如果将其可见性设为 public ,那么

将无法控制对象的创建。

java 复制代码
public class Test_Reflect {

	public static void main(String[] args) {
		try {
			//反射中,欲获取一个类或者调用某个类的方法,首先要获取到该类的Class 对象。
			Class<Singleton_05> clazz = Singleton_05.class;
			//getDeclaredXxx: 不受权限控制的获取类的成员.
			Constructor c = clazz.getDeclaredConstructor(null);
			//设置为true,就可以对类中的私有成员进行操作了
			c.setAccessible(true);
			Object instance1 = c.newInstance();
			Object instance2 = c.newInstance();
			System.out.println(instance1 == instance2);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

解决方法之一 : 在单例类的构造方法中 添加判断 instance != null 时,直接抛

出异常

java 复制代码
public class Singleton_05 {
	private static class SingletonHandler{
		private static Singleton_05 instance = new Singleton_05();
	}
	private Singleton_05(){
		if(SingletonHandler.instance != null){
			throw new RuntimeException("不允许非法访问!");
		}
	}
	public static Singleton_05 getInstance(){
		return SingletonHandler.instance;
	}
}

上面的这种方式使代码简洁性遭到破坏,设计不够优雅.

序列化对于单例的破坏
java 复制代码
public class Test_Serializable {
	@Test
	public void test() throws Exception{
		//序列化对象输出流
		ObjectOutputStream oos = new ObjectOutputStream(newFileOutputStream("tempFile.obj"));
		oos.writeObject(Singleton.getInstance());
		//序列化对象输入流
		File file = new File("tempFile.obj");
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
		Singleton Singleton = (Singleton) ois.readObject();
		System.out.println(Singleton);
		System.out.println(Singleton.getInstance());
		//判断是否是同一个对象
		System.out.println(Singleton.getInstance() == Singleton);//false
	}
}

class Singleton implements Serializable{
	private volatile static Singleton singleton;

	private Singleton() {}

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

输出结构为false,说明:
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就 破坏了
Singleton的单例性 。

解决方案:

java 复制代码
/**
* 解决方案:只要在Singleton类中定义readResolve就可以解决该问题
* 程序会判断是否有readResolve方法,如果存在就在执行该方法,如果不存在-
-就创建一个对象
*/
private Object readResolve() {
	return singleton;
}



readOrdinaryObject方法的代码片段

java 复制代码
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
//此处省略部分代码
Object obj;
try {
//通过反射创建的这个obj对象,就是本方法要返回的对象,也
可以暂时理解为是ObjectInputStream的readObject返回的对象。
//isInstantiable:如果一个serializable的类可以在运行
时被实例化,那么该方法就返回true
//desc.newInstance:该方法通过反射的方式调用无参构造方
法新建一个对象。
obj = desc.isInstantiable() ? desc.newInstance()
: null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
return obj;
}



5 枚举(推荐方式)

java 复制代码
public enum Singleton_06{

	INSTANCE;
	private Object data;

	public Object getData() {
		return data;
	}

	public void setData(Object data) {
		this.data = data;
	}

	public static Singleton_06 getInstance(){
		return INSTANCE;
	}
}


相关推荐
用户37215742613516 分钟前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊1 小时前
Java学习第22天 - 云原生与容器化
java
渣哥3 小时前
原来 Java 里线程安全集合有这么多种
java
间彧3 小时前
Spring Boot集成Spring Security完整指南
java
间彧4 小时前
Spring Secutiy基本原理及工作流程
java
Java水解5 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆7 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学7 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole7 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端
华仔啊7 小时前
基于 RuoYi-Vue 轻松实现单用户登录功能,亲测有效
java·vue.js·后端