手把手教你吃透单例模式,看完这个直接和面试官中门对狙!!!

320 阅读7分钟

单例模式

概念:

 java中单例模式是一种常见的设计模式

  • 单例模式的特点:
  1. 单例类只能有一个实例
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类必须给所有其他对象提供这一实例
  • 单例模式构成结构:
  1. 私有的静态的实例对象 private static instance
  2. 私有的构造函数(保证在该类外部,无法通过new的方式来创建对象实例) private Singleton(){}
  3. 公有的、静态的、访问该实例对象的方法 public static Singleton getInstance(){}

  单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。


1.饿汉式

饿汉式顾名思义就是饿疯了,饿的人就会饥不择食,急急忙忙的,不管三七二十一拿起啥就吃,对应我们单例模式就是加载类的时候直接不管用不用就直接初始化对象,且仅初始化一次,线程安全,但是如果不使用的话会浪费内存,直接上代码

public class Hungry {
	//1.私有构造器
	private Hungry() {	
	}
	//2.类的内部创建static final对象
	private final static Hungry HUNGRY= new Hungry();
	//3.向外暴露一个静态的公共方法,getInstance
	public static Hungry getInstance() {
		return HUNGRY;
	}
	public static void main(String[] args) {
		Hungry instance1 = Hungry.getInstance();
		Hungry instance2 = Hungry.getInstance();
		System.out.println(instance1);
		System.out.println(instance2);
	}
	//多线程线程安全
}

运行结果:

在这里插入图片描述


2.懒汉式

懒汉式也和这个名字一样,就是懒嘛,不到迫不得已的时候就不做,只有非得要做的时候才初始化对象

但是懒汉式也分好多种,我暂且分为这三种

1. 特别懒
2. 懒得有水平
3. 我只是看起来懒

第一种特别懒,只进行一个简单的null判断,真的是懒到家了

//1.普通懒汉式
public class LazyMan {
    //1.私有构造器
	private LazyMan() {
	}
	//2.static对象
	private static LazyMan lazyMan;
	//3.向外暴露静态方法getInstance
	public static LazyMan getInstance() {
		if (lazyMan == null) //如果对象为null,创建对象
			lazyMan = new LazyMan();
		return lazyMan;
	}

偷懒势必会付出代价, 这样在单线程下是可以运行,但是到了多线程下可能会出大问题,当多个线程同时走到lazyMan == null ,然后会new出多个对象出来,单例被破坏

public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		for (int i = 0; i < 10; i++) {//创建十个线程去获取实例
			new Thread(() -> {
				lazyMan.getInstance();
			}).start();//单例失效
		}

结果:不只一个对象,单例失效

在这里插入图片描述


第二种,使用双重检测锁模式+volatile防止指令重排,也叫DCL懒汉式,

  • 通过synchronized锁住创建实例的过程,保证一次只有一个线程访问,当lazyMan != null 直接跳过,节省了加锁造成的性能损耗;

  • new对象的过程不是一个原子性操作,使用volatile关键字防止指令重排

	//2.双重检测锁模式+volatile防止指令重排  (DCL懒汉式)	
	public static LazyMan getInstance2() {
		if (lazyMan == null) {
			//加锁
			synchronized (LazyMan.class) {
				if (lazyMan == null) {
					lazyMan = new LazyMan();//不是一个原子性操作
					/**
					 * 1.分配内存空间
					 * 2.执行构造方法,初始化对象
					 * 3.把这个对象指向这个空间
					 * 
					 * 内存速度与cpu速度差异可能导致顺序错误
					 * 线程A 执行顺序 123
					 * 线程B 执行顺序 132,此时lazyMan还没有完成构造,导致对象为null
					 * 
					 */		 
				}
			}
		}
		return lazyMan;
	}
public static void main(String[] args) {
			for (int i = 0; i < 10; i++) {
				new Thread(() -> {
					System.out.println(lazyMan.getInstance2());
				}).start();//单例
		}

结果:只有一个对象,单例成功实现

在这里插入图片描述

注意:但是,这就能百分之一百确保是单例么?

我们知道,在小学六年级曾学过一门特别霸道的东西,反射机制,对,就是那个可以把你扒得裤衩都不剩的反射机制,接下来教你如何通过反射来破解单例,开整开整

  • LazyMan.class.getDeclaredConstructor(null); 获取无参构造
  • declareConstructor.setAccessible(true);取消Java 语言访问检查,可以直接访问private成员
  • declareConstructor.newInstance(); 创建对象实例
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		LazyMan instance1 =  LazyMan.getInstance2();
		//反射
		Constructor<LazyMan> declareConstructor = LazyMan.class.getDeclaredConstructor(null);//获取无参构造
		declareConstructor.setAccessible(true);//取消Java 语言访问检查
		LazyMan instance2 = declareConstructor.newInstance();
		
		System.out.println(instance1);
		System.out.println(instance2);
	}	

结果:单例被破坏

在这里插入图片描述

所以,有法子可以防止么? 可以,通过修改构造方法,加上隐藏的标志位,当试图通过反射去创建多个对象时,直接抛出异常

DCL懒汉式+标志位(修改无参构造抛出异常)
public class LazyMan {
	private  static volatile LazyMan lazyMan;//保证不会指令重排
	private static boolean flag = false;
	private LazyMan() {
		if (flag == false) {
			flag = true;
		}
		else {
			throw new RuntimeException("不要试图通过反射破坏单例模式!!!");
		}
	}

public static LazyMan getInstance3() {
		if (lazyMan == null) {
			//加锁
			synchronized (LazyMan.class) {
				if (lazyMan == null) {
					lazyMan = new LazyMan();//不是一个原子性操作
					/**
					 * 1.分配内存空间
					 * 2.执行构造方法,初始化对象
					 * 3.把这个对象指向这个空间
					 * 
					 * 线程A 执行顺序 123
					 * 线程B 执行顺序 132,此时lazyMan还没有完成构造,导致对象为null
					 * 
					 */
				}
			}
		}
		return lazyMan;
	}
	
	public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		LazyMan instance1 =  LazyMan.getInstance3();
		System.out.println(instance1);
		//反射
		Constructor<LazyMan> declareConstructor = LazyMan.class.getDeclaredConstructor(null);//获取无参构造
		declareConstructor.setAccessible(true);//取消Java 语言访问检查
		LazyMan instance2 = declareConstructor.newInstance();
		System.out.println(instance2);

	}
}

结果:只能创建一个单例,当你试图通过霸王条款去破坏单例时,不好意思,不可以!!! 在这里插入图片描述


3.静态内部类

当Hoder被加载的时候,其内部类并不会被初始化,所以可以保证当Hoder被装载到JVM的时候,不会实例化单例类,当外部调用getInstance方法的时候,才会加载内部类InnerClass,从而实例化,同时由于实例的建立是在类初始化时完成的,所以天生对多线程友好,getInstance方法也不需要进行同步

public class Hoder {
	private Hoder() {
	}
	public static Hoder getInstance() {
		return InnerClass.HODER;
	}
	//静态内部类
	public static class InnerClass {
		private static final Hoder HODER = new Hoder();
	}
	
	public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
			Hoder hoder = Hoder.getInstance();
			Hoder hoder2 = Hoder.getInstance();
			Constructor<Hoder> constructor = Hoder.class.getDeclaredConstructor(null);
		    constructor.setAccessible(true);
		    Hoder hoder3 = constructor.newInstance();
			System.out.println(hoder);
			System.out.println(hoder2);
			System.out.println(hoder3);
		}
}

结果:可以实现正常的多线程安全,但是仍然抵挡不住反射的侵袭!!! 在这里插入图片描述


难道,好人都会失败吗?难道就没有一种可以抵抗反射大魔王的方法么? 哈哈哈哈,jdk1.5之后,枚举横空出世,它对反射说,不好意思,你不可能射到我,这又是为什么呢?我们从newInstance方法的源码可以一探究竟 在这里插入图片描述 当反射的对象为枚举时,直接抛出异常:不能通过反射创建枚举对象,接下来看看枚举如何实现单例模式

4.枚举

借用 《Effective Java》一书中的话,因为其功能完善,使用简洁,无偿地提供了序列化机制,在面对复杂的序列化或者反射攻击时任然可以绝对防止多次实例化等优点,所以枚举是实现单例模式的最佳方式,直接上代码

public enum EnumSingle {
		INSTANCE;
		//通过一个objNameString属性来展示枚举的单例
	    private String objNameString;
	    public void setObjName(String objString) {
	    	this.objNameString = objString;
	    }
	    public String getObjName() {
	    	return objNameString;
	    }
	    
	public static void main(String[] args) {
		EnumSingle instance1 = EnumSingle.INSTANCE;
		EnumSingle instance2 = EnumSingle.INSTANCE;
		instance1.setObjName("kyrie");
		System.out.println(instance1.getObjName());//kyrie
		
		instance2.setObjName("Harden");;
		System.out.println(instance1.getObjName());//harden
		System.out.println(instance2.getObjName());//harden
		
		System.out.println();
		
		for (int i = 0; i < 10; i++) {
			new Thread(() -> 
			System.out.println(EnumSingle.INSTANCE.getObjName())//全是harden
					).start();
			}
	
		}

结果:我们可以看到,枚举确实是单例,无论你怎样操作都只能得到一个对象,点个赞!!!

在这里插入图片描述


5.总结

饿汉式普通懒汉式DCL双检锁懒汉式静态内部类类枚举
多线程下安全多线程下不安全多线程下安全多线程安全多线程安全
会有内存浪费延迟加载不浪费内存延迟加载不浪费内存延迟加载不浪费内存不能延迟加载
反射可破坏反射可破坏反射可破坏反射可破坏最安全的办法

下一期:我们聊聊工厂模式,静态工厂,工厂方法,抽象工厂到底有什么区别呢? 我们拭目以待把!