设计模式——单例模式(java:一步步优化至最优写法)

目录

一、概念

二、分类

三、代码实现

饿汉式:在系统加载时就创建类的单例。

方法一:静态常量法:将唯一实例obj设置成静态常量

方法二:静态代码块:将类的实例化放在静态代码块中,与上述的静态常量一致,都是在类装载时创建单例,因此优缺点一致

方法三:静态内部类 : Singleton2在加载的时候不会被实例化,而是在需要实例化时(调用getInstance()),才会装载静态内部类,从而完成Singleton2的实例化。多线程下可以实现单例

懒汉式:在需要单例的时候才去创建

方法一:只判断一次+不加任何同步锁  多线程下不能实现单例,错误示范!

方法二:用synchronized对getInstance()方法加同步锁,可以实现单例

方法三:双重检查,不能实现单例,错误示范!

方法四:双重校验+volatile,这种写法是最好的,面试必备,直接上这种代码。

四、多线程下测试这些单例类

五、适用场合

六、参考文章


一、概念

  • 单例类只能有一个实例

  • 必须自行创建自己的唯一实例

  • 向所有其他对象提供这一实例

二、分类

饿汉式在系统加载时就创建类的单例(不管我是否需要,都会先创建该类唯一实例)。

懒汉式在需要单例的时候才去创建

三、代码实现

核心思想(重要)

  • 定义私有静态对象作为该类的唯一实例obj;
  • 私有化构造函数,保证用户不可以直接通过构造函数创建该类实例,或直接访问该类实例。
  • 定义一个公有的getInstance()方法去获得该类的唯一单例obj。

饿汉式:在系统加载时就创建类的单例。

方法一:

静态常量法:将唯一实例obj设置成静态常量

class Singleton0{
	private final static Singleton0 obj = new Singleton0();
	private Singleton0() {
		System.out.println("我被new出来了");//每生成一个实例就打印这句话,便于测试。
	}
	public static Singleton0 getInstance() {
		return obj;
	}
} 

特点:多线程下可以保证单例,但是会造成资源的浪费(不管我们需不需要这个唯一单例,它都会创建出来这个单例。如果我们根本不需要单例时,就会造成资源浪费)

方法二:

静态代码块:将类的实例化放在静态代码块中,与上述的静态常量一致,都是在类装载时创建单例,因此优缺点一致

class Singleton1{
	private static Singleton1 obj;
	static {
		obj = new Singleton1();
	}
	private Singleton1() {
		System.out.println("我被new出来了");
	}
	public static Singleton1 getInstance() {
		return obj;
	}
}

方法三:

静态内部类 : Singleton2在加载的时候不会被实例化,而是在需要实例化时(调用getInstance()),才会装载静态内部类,从而完成Singleton2的实例化。多线程下可以实现单例

class Singleton2{
	private Singleton2() {
		System.out.println("我被new出来了");
	}
	public static class SingletonInstance{
		private static final Singleton2 obj = new Singleton2();
	}
	public static Singleton2 getInstance() {
		return SingletonInstance.obj;
	}
}

 

懒汉式:在需要单例的时候才去创建

方法一:

只判断一次+不加任何同步锁  多线程下不能实现单例,错误示范!

class Singleton3{
	private static Singleton3 obj = null;
	private Singleton3() {
		System.out.println("我被new出来了");
	}
	public static Singleton3 getInstance() {
		if(obj == null) {
			obj = new Singleton3();
		}
		return obj;
	}
}

比如线程1运行到if(obj == null)这行,还没运行到下一行创建obj对象时,线程2也运行到这个判断条件,
此时线程1,2对obj读取的值都为null,故而他俩都会去创建类的实例,这样无法保证唯一实例。

TimeThread AThread B
T1检查到obj为空 
T2 检查到obj为空
T3 初始化对象obj
T4 返回对象obj
T5初始化对象obj 
T6返回对象obj 

可以看出,该类被实例化出了两次,无法满足唯一实例的条件。



方法二:

用synchronized对getInstance()方法加同步锁,可以实现单例

class Singleton4{
	private static Singleton4 obj = null;
	private Singleton4() {
		System.out.println("我被new出来了");
	}
	public static synchronized Singleton4 getInstance() {
		if(obj == null) {
			obj = new Singleton4();
		}
		return obj;
	}
}

Time

Thread A

Thread B

T1

对方法加上同步锁

 

T2

 

若此时也对该方法尝试加同步锁,则因获取不到锁而阻塞

T3

检查到对象为空

 

T4

初始化对象obj

 

T5

返回对象obj,并释放方法的同步锁

 

T6

 

获取到方法的同步锁

T7

 

检查到obj不为空

T8

 

返回对象obj,并释放方法的同步锁

缺点:但是这样给整个方法都加上了同步锁,即每次想得到一次单例都会给此方法加锁,这样使线程之间退化成串行化的执行。
上面方法效率太低,我们希望:只在该方法执行第一次实例化的时候加锁,如果该单例已被new出,则不进入同步区域,直接返回即可 

我的理解是:只给需要加锁的阶段加同步锁,其余阶段不上锁。比如有两个线程t1,t2。t1线程对a,b操作,t2对b,c操作
因此只需要t1对b操作阶段,t2对b操作阶段加同步锁,其余阶段正常执行即可。没必要对两线程整个执行周期都加上锁。(优化成下一个代码)

方法三:

用synchronized对类加同步锁+两次检查   这个看测试结果似乎能实现单例,但其实没有volatile修饰的obj实例的这个类也是一个错误方法!

我们希望:只对第一次创建实例时加同步锁,其余阶段不加锁。

class Singleton5{
	private static Singleton5 obj = null;
	private Singleton5() {
		System.out.println("我被new出来了");
	}
	public static Singleton5 getInstance() {
		if(obj == null) {
			synchronized(Singleton5.class) {
				if(obj == null) {
					obj = new Singleton5(); //error
				}
			}
		}
		return obj;
	}
}


双重检查锁:

第一次检查obj是否被初始化(不去获得锁,只是查看是否满足加锁的条件而言),若已被初始化则返回。

第二次检查obj查看在加锁阻塞期间,是否已经有其他线程先一步对obj做了初始化。

Time

Thread A

Thread B

T1

检查到对象obj为空

 

T2

 

检查到对象obj为空

T3

获取类的同步锁

 

T4

 

尝试获取锁未果,阻塞

T5

再次检查到obj为空

 

T6

创建出obj对象,之后释放锁

 

T7

返回obj对象

 

T8

 

获取到锁,继续执行

T9

 

再次检查obj发现不为空

T10

 

释放锁,并返回obj对象

这样看来似乎单例没问题呀,注意这里有一个隐患对构造函数来说,编译器为了优化进行指令重排序,请看:

实例化对象的那行代码(标记为error的那行),实际上可以分解成以下三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向刚分配的内存空间

但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:

  1. 分配内存空间
  2. 将对象指向刚分配的内存空间
  3. 初始化对象

Time

Thread A

Thread B

T1

检查到对象obj为空

 

T2

获取类的同步锁

 

T3

再次检查到obj为空

 

T4

为obj分配内存空间

 

T5

将obj指向刚分配的内存空间

 

T6

 

检查到对象obj不为空

T7

 

访问并返回obj(此时对象还未初始化,得到一个初始化未完成的对象)

T8

初始化obj

 

表格中,T7时刻线程B对obj的访问,访问的是一个初始化未完成的对象,发生错误。 

方法四:

双重校验+volatile,这种写法可以实现单例,面试必备,直接上这种代码。

双重校验+volatile 与上一个类不同的是,该类中对这个单例obj加了volatile关键字。这个关键字的作用是:使用了volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。在volatile和synchronized两端线程安全的保护下,保证多线程下单例的创建。

class Singleton6{
	private static volatile Singleton6 obj = null;
	private Singleton6() {
		System.out.println("我被new出来了");
	}
	public static Singleton6 getInstance() {
		if(obj == null) {
			synchronized(Singleton6.class) {
				if(obj == null) {
					obj = new Singleton6();
				}
			}
		}
		return obj;
	}
}

Time

Thread A

Thread B

T1

检查到对象obj为空

 

T2

获取类的同步锁

 

T3

再次检查到obj为空

 

T4

为obj分配内存空间

 

T5

将obj指向刚分配的内存空间

 

T6

初始化obj

 

T7

 

检查到对象obj不为空

T8

 

返回obj

 

四、多线程下测试这些单例类

这里开了10w个线程来测试这些类是否生成唯一实例

public static void main(String[] args) {
		Runnable r = new Runnable() {			
			public void run() {
				//==================饿汉式+静态常量法================
				//Singleton0.getInstance();
				//==================饿汉式+静态代码块================
				//Singleton1.getInstance();
				//==================饿汉式+静态内部类================
				//Singleton2.getInstance();
				//==================懒汉式+一次校验+错误示范================
				//Singleton3.getInstance();
				//==================懒汉式+synchronized方法================
				//Singleton4.getInstance();
				//==================懒汉式+synchronized类================
				//Singleton5.getInstance();
				//==================懒汉式+synchronized类+volatile================
				Singleton6.getInstance();
			}
		};
		for(int i = 0; i < 100000; i ++) { //这里开10w个线程来测试多线程下是否安全
			(new Thread(r)).start();
		}
	}

截图如下:(不多言,一切尽在截图中)

 

 

五、适用场合

  • 需要频繁的进行创建和销毁的对象;
  • 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
  • 工具类对象;
  • 频繁访问数据库或文件的对象。

优点:该类只存在一个对象,节省了系统资源(省去了对象的频繁创建与销毁),提高了系统性能。

缺点:当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。例如本文中要创建一个单例类的实例时,你要知道getInstance()这个函数名。

六、参考文章

参考文章:

https://www.cnblogs.com/zhaoyan001/p/6365064.html 将单例模式讲的好

https://www.cnblogs.com/xz816111/p/8470048.html 这篇将volatile关键字的必要性将的好

感谢大佬们的博客分享,使我对单例模式有了更深的理解。本文在此文章的基础上,完善了一些解释说明和测试用例,加入了自己学习过程中的理解,有不正之处希望指出!

总结:

单例类,顾名思义,该类只产生唯一实例,需要该类自动创建这个单例对象并向所有其他类实例提供这个对象。

根据这个唯一实例的初始化时机,可分为饿汉式和懒汉式。

饿汉式在类加载时该唯一实例就要被初始化,因此想到与类加载阶段有关的,静态常量/静态代码块,此二种方法大同小异,都只会在类加载时被执行,故而可保证创建出的实例唯一。但该方法资源耗费太大,不管我们是否需要单例类实例,都会给我们创建出来。我们希望,只在我们需要的时候(调用getInstance())去创建,因此借助静态内部类的概念,将初始化操作放在静态内部类中,并且只有当我们调用getInstance()才去装载静态内部类。 饿汉式借助类加载机制,绝对保证单例唯一。

懒汉式在需要单例的时候才去初始化。可对方法加synchronized同步锁,这个可以保证单例,但效率不高,每一次获取单例对象时(不管是第一次还是其余次)都需要对整个方法加锁。我们希望,第一次的时候加个同步锁,其他次判断出obj不为空直接返回即可,因此我们只给new那个小区域加个同步锁,并且还要双重检查(第一次检查是看是否满足加锁的条件,第二次检查是看在获取锁阻塞的时候是否有其他线程先一步初始化了obj)最关键一点,为了解决指令重排序带来的得到未初始化的obj这个隐患,要用volatile修饰obj。

  • 15
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值