当前位置: 移动技术网 > IT编程>软件设计>设计模式 > 大话西游之猿类单例,饿汉式、懒汉式

大话西游之猿类单例,饿汉式、懒汉式

2020年07月22日  | 移动技术网IT编程  | 我要评论

“曾经有一份真挚的感情摆在我的面前我没有珍惜,等我失去的时候才追悔莫及,人间最痛苦的事莫过于此,你的剑在我的咽喉上刺下去吧,不用在犹豫了!如果上天能给我一次再来一次的机会,我会对哪个女孩说三个字:我爱你,如果非要在这份爱上加一个期限,我希望是一万年!”

这是大话西游中的经典台词,而我们猿类世界的表白台词是:

你就是我的单例,我的唯一!

我们回到猿类世界中,我们接下来看:

单例模式是比较常见的一种设计模式,目的是保证一个类只能有一个实例,而且实例化之后并向整个系统提供这个实例,避免实例的频繁创建和释放,节约内存,提高效率。

应用场景
许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。
1、比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置信息由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理;
2、单例模式提供整个应用生命周期的上下文Context;
3、计算机中的打印机也是采用单例模式设计的,一个系统中可以存在多个打印任务,但是只能有一个实例同步处理打印任务;
4、Web页面的计数器也是用单例模式实现的。

一般将单例模式的实现分为两种,分别是饿汉式、懒汉式。
饿汉式:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。
懒汉式:在类加载时不初始化,等到第一次被使用时才初始化。

1、饿汉式 (可用)

package com.test.singleton;

public class Singleton {
	//加上final修饰符,则变量s变成了常量,只能赋一次值。
    //对于final变量:
    //如果是基本数据类型的变量,则其数值一旦初始化之后便不能更改;
    //如果是引用类型的变量,则在初始化之后便不能再让其指向另一个对象
	private static final Singleton s = new Singleton();
	
	//构造方法private修饰,保证类无法被外部实例化
	private Singleton() {
	}
	
	//对外方法public static修饰,直接通过Singleton.getInstance()的方式获取单例
	public static Singleton getInstance() {
		return s;
	}
}

这种方式是线程安全的,可用。是比较常见的写法,在类加载的时候就完成了实例化,避免了多线程的同步问题。当然也有缺点,因为类加载时就实例化了,没有达到Lazy Loading (懒加载) 的效果,如果该实例没被使用,内存就浪费了。

2、普通的懒汉式 (线程不安全,不可用)

package com.test.singleton;

public class Singleton2 {
	//不用final修饰
	//如果用final修饰的话,s就会一直为null,getInstance()方法无法给它赋值了
	private static Singleton2 s = null;
	
	//构造方法private修饰,保证类无法被外部实例化
	private Singleton2() {
	}
	
	//对外方法public static修饰,直接通过Singleton2.getInstance()的方式获取单例
	public static Singleton2 getInstance() {
		if(s == null) { 	
			//如果线程1执行完s==null判断,还没有执行到下面的语句,此时s还是null。
			//这时候线程2来了,判断s==null也是true,也进入到了此处。
			//这样下面的语句就会被执行两次,线程1和线程2拿到的是不同的对象,出现线程不安全的问题。
			s = new Singleton2();
		}
		return s;
	}
}

这是懒汉式的一种写法,只有在方法第一次被访问时才会实例化,达到了懒加载的效果。但是这种写法有个致命的问题,就是多线程的安全问题。假设对象还没被实例化,然后有两个线程同时访问,那么就可能出现多次实例化的结果,所以这种写法不可采用。

3、同步方法的懒汉式 (可用)

package com.test.singleton;

public class Singleton3 {
	//不用final修饰
	//如果用final修饰的话,s就会一直为null,getInstance()方法无法给它赋值了
	private static Singleton3 s = null;
	
	//构造方法private修饰,保证类无法被外部实例化
	private Singleton3() {
	}
	
	//对外方法public static修饰,直接通过Singleton3.getInstance()的方式获取单例
	//加上synchronized修饰,所有线程访问该方法时同步访问,可解决线程安全问题。
	//缺点是锁的粒度大,导致效率低下
	public static synchronized Singleton3 getInstance() {
		if(s == null) { 	
			//如果线程1执行完s==null判断,还没有执行到下面的语句,此时s还是null。
			//这时候线程2来了,判断s==null也是true,也进入到了此处。
			//这样下面的语句就会被执行两次,线程1和线程2拿到的是不同的对象,出现线程不安全的问题。
			s = new Singleton3();
		}
		return s;
	}
}

这种写法是对getInstance()加了锁处理,保证了同一时刻只能有一个线程访问并获得实例,但是缺点也很明显,效率不高,因为synchronized是修饰整个方法,每个线程访问都要进行同步,而其实这个方法只执行一次实例化代码就够了,每次都同步方法显然效率低下,为了改进这种写法,就有了下面的双重检查懒汉式。

4、双重检查懒汉式 (可用,推荐)

package com.test.singleton;

public class Singleton4 {
	//不用final修饰
	//如果用final修饰的话,s就会一直为null,getInstance()方法无法给它赋值了
	private static Singleton4 s = null;
	
	//构造方法private修饰,保证类无法被外部实例化
	private Singleton4() {
	}
	
	//对外方法public static修饰,直接通过Singleton4.getInstance()的方式获取单例
	public static Singleton4 getInstance() {
		if(s == null) { 	
			//静态方法同步的时候,使用的锁,不能是this,而是类.class
			//线程1、2、3...在此处排队
			//线程1拿到类锁,进入到同步代码块中,此时s==null为true,new一个对象赋值给s
			//线程1释放类锁,线程2拿到类锁,进入到同步代码块中,此时s==null为false,就不会新new对象了
			//这样就解决了线程安全的问题
			synchronized(Singleton4.class) {
				if(s == null) {
					s = new Singleton4();
				}
			}
		}
		return s;
	}
}

这种写法用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高,是对第三种写法的改进。为什么要做两次判断呢?这是为了线程安全考虑,还是那个场景,对象还没实例化,两个线程1和2同时访问静态方法并同时运行到第一个if判断语句,这时线程1先进入同步代码块中实例化对象,结束之后线程2也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了。

但是此处有一个潜在问题,变量s需要加上关键字volatile进行修饰。

/**
  * 
  * 加上volatile,禁止指令重排,造成的bug。
  * s = new Singleton4();看似一句话,但实际上是一个非原子操作,大致分为三个步骤:
  * 1、memory = allocate(); //1:分配对象的内存空间
  * 2、ctorInstance(memory); //2:初始化对象
  * 3、instance = memory; //3:设置instance指向刚分配的内存地址
  * 但是经过指令重排序后如下:
  * 1、memory = allocate(); //1:分配对象的内存空间
  * 2、instance = memory;   //3:设置instance指向刚分配的内存地址,此时对象还没被初始化,但是instance已经不为null了。如果此时来个一个新的线程则会直接返回还没有初始化的对象,进发生bug
  * 3、ctorInstance(memory); //2:初始化对象
  *
  */
private static volatile Singleton4 s = null;

一旦一个变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。

具体volatile的说明后续会再写文章解释。

5、静态内部类 (可用,推荐)

package com.test.singleton;

public class Singleton5 {
	//构造方法private修饰,保证类无法被外部实例化
	private Singleton5() {
	}
	
	private static class SingletonInstance{
		private static final Singleton5 s = new Singleton5();
	}
	
	//对外方法public static修饰,直接通过Singleton4.getInstance()的方式获取单例
	public static Singleton5 getInstance() {
		return SingletonInstance.s;
	}
}

这是很多开发者推荐的一种写法。

我们把Singleton5实例放到一个静态内部类中,这样可以避免静态实例Singleton5类的加载阶段就创建对象。静态变量初始化是在SingletonInstance类初始化时触发的,并且由于静态内部类只会被加载一次, 所以这种写法也是线程安全的。

考虑反射:由于在调用 SingletonInstance.s 的时候,才会对单例进行初始化,而且通过反射,是不能从外部类获取内部类的属性的。所以这种形式,很好的避免了反射入侵。

考虑多线程:由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。

优势:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。

劣势:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久代的对象。创建的单例,一旦在后期被销毁,不能重新创建。

欢迎朋友们关注转发点赞,谢谢~~

在这里插入图片描述

本文地址:https://blog.csdn.net/sl1202/article/details/107485776

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网