Skip to content

Latest commit

 

History

History
83 lines (74 loc) · 5.15 KB

single-mode.md

File metadata and controls

83 lines (74 loc) · 5.15 KB

定义

单例是最常用的设计模式之一,其表达的最主要的意思是一个对象在整个jvm堆内存中只有一个实例,这样可以保证无论从任何代码块获取的单例实例都是唯一的。

单例的优缺点也很明显,优点有以下这些:

  • 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
  • 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
  • 提供了对唯一实例的受控访问。
  • 由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。
  • 避免对共享资源的多重占用。

缺点:

  • 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  • 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

在很多场景单例模式是很有用的,例如配置容器,连接池等,在Java中获取编写一个单例也很简单(暂时屏蔽细节):

  • 第一步:私有化类构造
  • 第二步:内部定义一个类型为类本身的静态私有变量
  • 第三步:提供一个静态共有方法获取这个私有变量(获取之前赋值)

尤其是第三步,我们在获取这个私有变量的时候要对其进行赋值,那么就有两个阶段可以做这件事,

  • 定义静态私有变量的时候直接赋值
  • 调用公有静态方法的时候再赋值

这就引出了两种实现模式:饿汉模式懒汉模式

饿汉模式实现

饿汉从字面上的意思我们可以想到一个特别饥饿的大汉,而对于单例来讲则是形容以迫不及待的方式去将私有实例赋值,代码实现如下:

public class Single {
	private static Single instance = new Single();
	private Single() {}
	public static Single getInstance() {
		return instance;
	}
}

这种模式的好处是不会存在并发下安全隐患,但是坏处也可想而知,对于jvm加载的过程就会将instance变量赋值,也就意味着我们即使没有用到这个单例对象也会将其实例new出来,可想而知,我们的永久带将会为其分配内存,带来的后果是永久代内存变少。

懒汉模式实现

懒汉模式则是在调用公有静态方法时才会为私有变量赋值:

public class Single {
	private volatile static Single instance = null;// 1
	private Single() {}
	public static Single getInstance() {
		if(instance == null) { //2
			synchronized (Single.class) { //3
				if(instance == null) { //4
					instance = new Single(); // 5
				}
			}
		}
		return instance;
	}
}

相比之前的饿汉模式,我们有以下几个改动:

  • 标记1:增加volatile关键字保证5时不会指令重排
  • 标记2:是为了提高程序的效率,当Single对象被创建以后,再获取Single对象时就不用去验证同步代码块的锁及后面的代码,直接返回Single对象
  • 标记3:防止多线程下的重复执行
  • 标记4:同3,当多个线程同时调用getInstance方法,此时instance为空,两个线程可以轻松越过2,来到3抢锁,一个线程率先抢占到并且为instance赋值后,如果没有4if判断,第二个线程也会重复去为instance赋值,这就会导致创建多个实例。

而我们使用volatile则是因为在标记5赋值的时候会发生指令重排的问题!

在Java中看似顺序的代码在JVM中,可能会出现编译器或者CPU对这些操作指令进行了重新排序;在特定情况下,指令重排将会给我们的程序带来不确定的结果.....

对于instance = new Single()这一行代码,JVM执行的指令有多行:

memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址

经重排后如下:

memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址,此时对象还没被初始化
ctorInstance(memory); //2:初始化对象

若有A线程进行完重排后的第二步,且未执行初始化对象。此时B线程来取instance时,发现instance不为空,于是便返回该值,但由于没有初始化完该对象,此时返回的对象是有问题的。

总结

单例模式可以节省内存,相比于静态类更易于扩展。