Java设计模式——单例模式(Singleton)

引用部分摘自https://www.cnblogs.com/restartyang/articles/7770856.html
git地址:https://github.com/liuwang12138/design-pattern.git

一、单例模式的概念

单例模式(Singleton),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

包括网上对于单例模式的定义也有很多,其实单例模式简单理解,就是在工程中,某个类仅需要,并且仅允许一个实例的存在。
单例模式一般被用于项目中的Manager或是Factory。举个简单的例子,windows中的资源管理器,就是典型的单例模式。

二、单例模式的优劣

优点:
1.在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
2.单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
3.提供了对唯一实例的受控访问。
4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
5.允许可变数目的实例。
6.避免对共享资源的多重占用。
缺点:
1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3.单例类的职责过重,在一定程度上违背了“单一职责原则”。
4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

个人觉得,单例模式最大的优势在于对实例的访问控制,同时节约系统资源。

三、代码实现
要实现单例模式,就要杜绝其他类能够直接new出单例对象,因此,要实现单例模式的类,其构造方法必须私有,同时,向外暴露一个获取实例的静态方法,用于其他类获取实例对象。代码如下:

//饿汉式
public class Mgr01 {
	private static final Mgr01 INSTANCE = new Mgr01();
	private Mgr01() {}		//构造方法必须是private
	public static Mgr01 getInstance() {
		return INSTANCE;
	}
}

public class Mgr02 {
	private static final Mgr02 INSTANCE;
	static {
		INSTANCE = new Mgr02();
	}
	private Mgr02() {}
	public static Mgr02 getInstance() {
		return INSTANCE;
	}
}

上面的两种方法称为饿汉式单例模式,可以看到,类中初始化了一个私有静态的Mgr01实例,每次外部调用getInstance()方法获取Mgr01对象,返回的都是这个实例,从而实现单例模式。
实际上,这种单例的实现方式已经很好了,也比较简单,但它依然存在一个问题。INSTANCE实例的初始化,是在类加载到内存的那一刻就进行了,不管用到与否。而如果没有用到,显然没有必要进行实例化的。

看下面这段代码

//懒汉式 Lazy Loading
public class Mgr03 {
	private static Mgr03 INSTANCE;
	private Mgr03() {}
	public static Mgr03 getInstance() {
		if(INSTANCE == null) {
			INSTANCE = new Mgr03();
		}
		return INSTANCE;
	}
}

上面代码称为懒汉式(Lazy Loading),可以看到,在类加载到内存中的那一刻,私有静态变量INSTANCE并没有初始化。当外部调用getInstance()方法想要获取实例时,Mgr03类会先检查一下这个INSTANCE对象是否为空,如果为空,也就是首次获取的时候,就新建一个实例并赋给INSTANCE。如果不为空,也就是不是首次获取,就把已经存在的INSTANCE对象返回过去。
懒汉式的单例模式,虽然实现了根据需要初始化实例对象,但会带来线程不安全的问题。我们做一个测试。

public class Main {
	public static void main(String[] args) {
		for(int i=0; i<100; i++) {
			new Thread( () -> 
				System.out.println(Mgr3.getInstance()).hashCode()
			).start();
		}
	}
}

在这个测试类里,我new出100个线程,同时获取Mgr03的实例对象,并输出实例对象的hashCode。
看一下输出
在这里插入图片描述
从输出中我们可以发现,第一个线程获取到的Mgr03实例,与其他线程获取到的Mgr03实例是不一样的。
这是为什么呢?
我们看Mgr03里的代码,在最开始的时候,INSTANCE对象是空值。在多线程的执行状态下,第一个线程执行getInstance()方法,首先判断Mgr03的INSTANCE对象是否为空,发现为空,执行

INSTANCE = new Mgr03();

这句话。

然而,就在这句话还没有执行的时候,第二个线程new出来了,并执行了getInstance()方法。注意,这个时候,还没有执行上面这句话,也就是说,此时的INSTANCE对象依然为空,因此,依然会进入if语句里面的代码块。
上面这句话执行完成,此实,INSTANCE对象不再为空,下一个线程执行getInstance()方法时,就不会进入if语句里面的代码块了。
因此我们说,这种实现单例模式的代码是有问题的!


将上面的懒汉式的单例模式代码修改一下,使其变得线程安全,可以想到,用synchronized关键字,将这个方法锁住。
public class Mgr04 {
	private static Mgr04 INSTANCE;
	private Mgr04() {}
	public static synchronized Mgr04 getInstance() {
		if(INSTANCE == null) {
			INSTANCE = new Mgr04();
		}
		return INSTANCE;
	}
}

再用上面的测试代码测试,发现已经测试成功了,获取到的实例的hashCode都是完全一样的。然后,这种方式会大大降低多线程程序的运行效率。
于是,有人提出,通过缩小synchronized关键字包裹的代码块,从而提高效率。

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

然而这种方式显然也是不行的,原理与03类似,如果两个线程都进入到

if(INSTANCE == null )

里面,虽然会将Mgr05.class对象锁住,但依然会执行两次new Mgr05()

于是,衍生出了下面这种方式:双重检查,缩小synchronized代码块

public class Mgr06 {
	private static volatile Mgr06 INSTANCE;
	private Mgr06() {}
	public static Mgr06 getInstance() {
		if(INSTANCE == null) {
			sychronized (Mgr05.class) {
				if(INSTANCE == null) {
					INSTANCE = new Mgr06();
				}
			}
		}
		return INSTANCE;
	}
}

这种方式,再多线程的环境下去测试,发现是没有问题的。注意到在定义INSTANCE对象时,加了一个volatile关键字,这与Java的指令重排是有关的。
详细原因,请参考我的下一篇文章
https://blog.csdn.net/Jarvenman/article/details/103969667

额,继续往下,然后,单例模式还可以是下面这种,利用私有静态内部类的方式去实现。

public class Mgr07 {
	private Mgr07() {}
	private static class MgrHolder {
		private static final Mgr07 INSTANCE = new Mgr07();
	}
	public static Mgr07 getInstance() {
		return MgrHolder.INSTANCE;
	}
}

这里我们看,首先把Mgr07的构造方法设为私有,然后定义了一个私有静态内部类,我们首先明确一件事:就是当类被加载时,他的静态内部类是不会被加载的,因此,当Mgr07.class被加载到内存中是时候,MgrHolder没有被加载,因此INSTANCE对象不会被初始化,只有当调用了getInstance()方法时,才会加载内部类,进而初始化INSTNCE对象,并返回给调用者。

在多线程的环境下测试也是通过的,线程安全。

然后,《Effective Java》的作者,提出了一种,用枚举实现的单例模式

public enum Mgr08 {
	INSTANCE
}

外部调用代码:

public class Main {
	public static void main(String[] args) {
		new Thread( () ->
			System.out.println(Mgr08.INSTANCE)
		).start();
	}
}

利用这种方式实现的单例模式,不仅可以解决线程的同步问题,还可以防止反序列化。
额,反序列化,可以不深究。


不用深究!

好吧,非要深究,也可以。
Java的反射机制是可以把Class文件载入到内存,然后new出一个对象的,这也就意味着,在其他七种方式中,其他方法得到了实例对象后,就可以根据其Class,反序列化得到另外的对象。
而枚举类则不同,Java里的枚举类是没有构造方法的,所以无法根据枚举单例new出其他的对象。


就这些了。
再多真的不知道了。

以上内容,参考自马士兵老师 设计模式系列课程。
git地址:https://github.com/liuwang12138/design-pattern.git

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值