(一)Java设计模式学习:浅析单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式的特点:

  • 单例类只能有一个实例
  • 单例类必须自己创建自己的唯一实例(构造器私有)
  • 单例类必须给所有其他对象提供这一实例

优点:

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
  • 避免对资源的多重占用(比如写文件操作)

缺点: 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化

单例模式主要分为饿汉式和懒汉式两种,下面对这两种方式做一下介绍

一、饿汉式

饿汉式,根据表面意思,就是他很饿。表现出来就是在类加载时就完成了初始化。


public class singleton {
    
    
	
	// 直接初始化对象
	private final static singleton single = new singleton();  
	
	private singleton() {
    
    }	//构造器私有
	
	public static singleton getInstance() {
    
    
		return single;
	}
}

饿汉式避免了多线程的同步问题,但是不管后面用不用得上,在类加载时直接初始化,没有达到Lazy Loading (懒加载) 的效果,极有可能造成内存浪费

二、懒汉式

懒汉式就是在类加载时先不初始化,等到第一次被使用时才初始化。

// 线程不安全
public class singleton {
    
    
	
	private static singleton single;
	
	private singleton() {
    
    }
	
	public static singleton getInstance() {
    
    
		if(single==null) {
    
    	//不为null说明已经初始化过了,不需要再进行初始化
			single = new singleton();
		}
		return single;
	}
}

懒汉式不会造成内存浪费。上面的代码在单线程下没有问题,但是在多线程下极有可能出现问题。

类加载是有顺序的,主要分为 加载 —> 连接 —> 初始化。假设对象还没被实例化,然后有两个线程同时访问,那么就可能出现多次实例化的结果。

详细类加载过程可以看下面这篇文章
(二)浅谈JVM的类加载知识

懒汉式的多线程安全问题可以使用DCL解决:

DCL懒汉式(双重检测锁模式)

就是在实例化前加锁,防止并发情况下出现多次实例化


public class singleton {
    
    

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

进行两次 if (singleton == null)检查,这样可以保证线程安全,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。

DCL虽然保证了线程安全,但是又会出现另一个问题:不能保证new过程的原子性操作
new过程主要有三步:

  • 1、分配内存空间
  • 2、执行构造方法、初始化对象
  • 3、把对象指向空间

正常的顺序是123,但是也有可能出现132的情况,就是还没初始化先指向内存空间。这时候如果有两个线程同时访问,第一个线程不会有问题,会造成第二个线程查询到 single 不为空,直接返回 single,但是此时 single 还没有完成初始化,是个空对象,会出现问题。这个解决办法就是加上 volatile 关键字。


public class singleton {
    
    
	// 加上volatile关键字,保证按照123顺序执行,使其成为原子性操作
	private volatile static singleton single;
	
	private singleton() {
    
    }
	
	public static singleton getInstance() {
    
    
		if(single==null) {
    
    
			synchronized (singleton.class) {
    
    
				if(single == null)
					single = new singleton();
			}	
		}
		return single;
	}	
}

volatile 关键字的主要作用是保证变量的可见性并且防止指令重排序。

三、如何破坏单例模式

单例模式其实是可以破坏的,用的方式就是Java的反射机制

1、调用原始构造和反射构造情况下
import java.lang.reflect.Constructor;

public class singleton {
    
    

	private volatile static singleton single;
	private singleton() {
    
    } // 无参构造方法
	public static singleton getInstance() {
    
    
		if(single==null) {
    
    
			synchronized (singleton.class) {
    
    
				if(single == null)
					single = new singleton();
			}	
		}
		return single;
	}
	public static void main(String args[]) throws Exception{
    
    
		singleton instance1 = single.getInstance();
		// 获取空参构造器,调用无参构造方法
		Constructor<singleton> deConstructor = singleton.class.getDeclaredConstructor(null);
		// 破坏私有构造器,无视私有化
		deConstructor.setAccessible(true);
		// 通过反射创建对象
		singleton instance2 = deConstructor.newInstance();
		
		System.out.println(instance1);
		System.out.println(instance2);
	}
	
}

输出如下:
在这里插入图片描述
结果发现两个实例地址不一样,本来单例只有一个实例,两个应该相同,结果却不一样,说明反射已经破坏了单例模式

如何防止被反射破坏

简单的就是在无参构造器里加锁

import java.lang.reflect.Constructor;
public class singleton {
    
    
	private volatile static singleton single;
	private singleton() {
    
    
		synchronized (singleton.class) {
    
    
			if(single!=null)
				throw new RuntimeException("不要试图使用反射破坏单例模式");
		}
	}
	public static singleton getInstance() {
    
    
		if(single==null) {
    
    
			synchronized (singleton.class) {
    
    
				if(single == null)
					single = new singleton();
			}	
		}
		return single;
	}
	public static void main(String args[]) throws Exception{
    
    
		singleton instance1 = single.getInstance();
		Constructor<singleton> deConstructor = singleton.class.getDeclaredConstructor(null);
		deConstructor.setAccessible(true);
		singleton instance2 = deConstructor.newInstance();
		System.out.println(instance1);
		System.out.println(instance2);
	}
}
2、都调用反射构造

如果不调用原始的getInstance()方法,都调用反射创建的方法,也会造成问题,例如

import java.lang.reflect.Constructor;

public class singleton {
    
    

	private volatile static singleton single;
	private singleton() {
    
    } // 无参构造方法
	public static singleton getInstance() {
    
    
		if(single==null) {
    
    
			synchronized (singleton.class) {
    
    
				if(single == null)
					single = new singleton();
			}	
		}
		return single;
	}
	public static void main(String args[]) throws Exception{
    
    
		// 获取空参构造器,调用无参构造方法
		Constructor<singleton> deConstructor = singleton.class.getDeclaredConstructor(null);
		// 无视私有构造器
		deConstructor.setAccessible(true);
		// 通过反射创建对象
		singleton instance1 = deConstructor.newInstance();
		singleton instance2 = deConstructor.newInstance();
		
		System.out.println(instance1);
		System.out.println(instance2);
	}
	
}
// 输出(两个值还是不一样)
// forTest.singleton@2ff4acd0
// forTest.singleton@54bedef2

综上,通过反射很容易破坏单例模式,那么如何解决呢?
下面介绍终极解决办法:

四、如何防止单例模式被反射破坏

因为反射不能破坏枚举类型,所以通过枚举可以防止单例模式被破坏。如果有反射去破坏枚举,程序会直接报错:java.lang.IllegalArgumentException: Cannot reflectively create enum objects(不能使用反射机制创建一个枚举对象)。

import java.lang.reflect.Constructor;

public enum EnumSingle {
    
    
	INSTANCE;
	
	public EnumSingle getInstance() {
    
    
		return INSTANCE;
	}
}
class test{
    
    
	public static void main(String []args) throws Exception{
    
    
		EnumSingle instance1 = EnumSingle.INSTANCE;
		
		Constructor<EnumSingle> deConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
		deConstructor.setAccessible(true);
		EnumSingle instance2 = deConstructor.newInstance();
		System.out.println(instance1);
		System.out.println(instance2);
	}
}
// 输出:
// java.lang.IllegalArgumentException: Cannot reflectively create enum objects

由于枚举类不能在外部实例化对象,并且无偿提供了序列化机制,绝对防止了多次实例化。单元素枚举已经成为实现Singleton的最佳方式。

注意:Constructor<EnumSingle> deConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
这里使用的是有参构造器,因为枚举编译后其实不存在无参构造器,源码里面看是有无参构造,但是编译后是有参构造,具体可以通过jad.exe查看反编译后的文件(如下图)
在这里插入图片描述

本文来源:(以上看不懂的,或者枚举那里不懂的,强烈推荐看下面这个视频)
【狂神说Java】单例模式-23种设计模式系列

猜你喜欢

转载自blog.csdn.net/qq_42908549/article/details/107497047