设计模式之----单例模式

什么是设计模式

设计模式,不是一种知识点,它是前人总结出来的对于特定问题的一些解决方案。
有了设计模式,可以让代码变得更加容易理解,同时确保了复用性,可靠性,可扩展性。
当代码很少时,我们往往体会不出设计模式的价值,但当程序的规模扩大到一定量,设计模式的优势会明显的显现出来。

设计模式的分类

设计模式分为3类:

  1. 创建型模式(5种)---->用于解决对象创建的过程

单例模式,工厂方法模式,抽象工厂模式,建造者模式,原型模式

  1. 结构型模式(7种)---->把类或对象通过某种形式结合在一起,构成某种复杂或合理的结构

适配器模式,装饰着模式,代理模式,外观模式,桥接模式,组合模式,享元模式

  1. 行为型模式(11种)---->用来解决类或对象之间的交互,更合理的优化类或对象之间的关系

观察者模式,策略模式,模板模式,责任链模式,解析器模式,迭代子模式,命令模式,状态模式,备忘录模式,访问者模式,中介者模式

单例模式(SingleTon)

它的作用是:解决对象创建的问题,控制当前类只能产生一个唯一的一个对象。
我们先来思考以下,要实现这样的功能,要怎么做?
首先,必须让它的构造方法私有化,不能在其他的main函数里随便调用。
其次,要获得这么一个唯一的对象,我们必须创建出一个对象,那么创建对象的这个过程,即 new 的这行代码放在哪里合适,构造方法里?不行,因为我们本来就是要调用构造方法来创建对象,让构造方法里面去 new 一个这个类的对象,也就是我要创建这个类的同时,再去创建对象,创建对象调用构造方法,还去创建对象,不停的创建对象,无休止的耗费栈内存,会抛出内存溢出的错误java.lang.StackOverflowError(栈内存溢出错误),更别说构造方法还要使用private进行修饰,在本类之外调用不到。
那 new 的过程放在代码块里?代码块不能有返回值,即使执行了new的过程,我也拿不到创建的那个对象,不行。放在普通方法里?普通方法可以有返回值,但是每次调用一次方法,都会创建出一个新的对象,不是唯一的对象,还是不行。最后,我们只能放在属性里:

public SingleTon singleTon = new SingleTon();

结果显而易见,还是不行,每次创建SingleTon这个类的对象,都会初始化一个属性,这个属性继续去 new ,new 的过程继续创建对象,陷入一个死循环,还是会抛出:Exception in thread "main" java.lang.StackOverflowError。这个过程,有点像递归,但它不是递归,它和递归有着异曲同工之妙,递归看似是一个方法的调用,但是递归执行起来是许多个一样的方法,是方法执行到一半的时候,调了另外的一个方法,那个方法长得跟它自己一样,然后这两个方法的参数不一样,是这样的一个过程,第一个方法执行到一半的时候调用了第二个,第一个没执行完等着,第二个又调了第三个,第二个等着,如果一直执行不完,那个之前的调用就一直等着,那么会产生无限的内存,最后堆死了。那为什么是栈内存溢出而不是堆内存溢出呢?因为构造方法的执行是要在栈内存里开辟一块空间的,如果构造方法无休止的调用,最终一定会产生栈内存溢出的错误。

这里简单说一下栈内存和堆内存里面到底存的啥。
栈内存里存:变量空间,执行过程中的临时空间,方法执行体。
推内存里存:通过 new 创建出的对象空间。

那么接下来该怎么解决这个问题呢?
很简单,只需让那个属性被static修饰即可,被static修饰过的属性,在这个类下有且仅有一份,就不会产生栈内存溢出的错误了。

public static SingleTon singleTon = new SingleTon();

被static修饰过的属性还有一个好处,就是不用new,直接类名打点调用即可。
但是,新的问题出现了,既然这个属性只有一份,它就相当于是一级保护对象了,那么用public修饰符去修饰,岂不是很危险吗?我们设想这样一种情况:

	public static void main(String[] args) {
		//SingleTon singleTon = SingleTon.singleTon;
		//singleTon = null;
		SingleTon.singleTon = null;
	}

直接把我们唯一的singleTon对象给闹没了。所以public权限修饰符需要慎用,很危险。
为了保证安全性,我们把public改成private
同时需要提供一个获得该私有属性的共有方法:

	public SingleTon getSingleTon(){
		return singleTon;
	}

问题又来了,我得获得这个唯一的对象,就得调用这个公用的get方法,那么,方法怎么调啊,得先创建对象,对象打点调用。我现在为了获取对象,我得先创建对象,而构造方法私有了,我根本没法创建对象。怎么办呢?
我们很自然的可以想到,这个get方法,也应该用static进行修饰,目的是不用创建对象就可以调用方法。(这里我们要区分,属性利用static修饰的最大意义是让它唯一不产生栈内存溢出,而方法利用static修饰的最大意义是不创建对象即可调用)
这样一来,我们不必担心这个唯一的对象被随便赋值为空的情况:
在这里插入图片描述
上面的代码会报红线错误,提示左边部分必须是一个变量。
到这里,单例模式基本就写完了。
但是,就万事大吉了吗?
以上的代码和思考过程,仅仅是单例模式的一种实现方式:饿汉式。
为什么是饿汉式呢?因为它的属性在类加载的时候就加载出来了,加载出来以后这个对象马上就能用。

饿汉式

public class SingleTon {
	private SingleTon(){
	}
	private static SingleTon singleTon = new SingleTon();
	public static SingleTon getSingleTon(){
		return singleTon;
	}
}

饿汉式带来的问题主要是加载性的问题,假如说我这个类很庞大,加载起来也比较费事,关键是我并不是着急的马上要这个唯一的对象,如果代码执行了很长时间都没有用到这个对象,那这个对象就白白浪费了内存空间,并且增加了服务器启动时的消耗。

懒汉式

懒汉式的意思顾名思义,就是比较懒,不着急去创建那个唯一的实例,什么时候用,才去创建对象。
可以做如下修改,将饿汉式改成懒汉式:

public class SingleTon {
	private SingleTon(){
	}
	private static SingleTon singleTon;//没有创建对象,初始值为null
	public static SingleTon getSingleTon(){
		if(singleTon == null){
			singleTon = new SingleTon();
		}
		return singleTon;
	}
}

这种写法,解决了上一种饿汉式的内存浪费问题,但同时也带来了新的问题,在多线程的情况下,产生线程安全的问题。
在同一时间下,许多人同时并发的调用getSingleTon方法获取单例对象,如果这个对象还没有被初始化,那到底是谁先一步让它new出来,谁后一步让它new出来,线程安全问题由此产生,我们可以通过添加线程锁来控制。
在方法上面加锁:
锁定的是当前调用方法时的那个对象。

	public static synchronized SingleTon getSingleTon(){
		if(singleTon == null){
			singleTon = new SingleTon();
		}
		return singleTon;
	}

这样一来可以控制线程安全的问题,但是又带来了新的问题。
在方法上直接添加了锁,锁定的是当前调用方法时的那个对象。相当于两个人在争抢对象的使用权,谁抢到了,谁先用,用完了,第二个人再用,但是使用对象的这个过程可能会很慢,整个方法执行的过程中,其他人都等着,由此带来了性能问题。
增强锁的性能:锁类模板+双重判断
上面的代码中,也只有new对象的那行代码会产生线程冲突,if语句判断的过程其实不需要加锁。我们不必为了这一行代码而锁上整个方法,使得性能降低。
为啥要锁定当前类的类模板---->我们要锁的不是对象本身,是创建对象时把用来创建对象的模板给锁了。
但是还有一个问题,如果if语句判断的过程不锁,并发访问时可能我在判断的时候你也在判断,而我要锁定的时候你已经锁定了,同样不能保证线程的安全。我们还需要加一层判断来确保严谨和万无一失:

	public static SingleTon getSingleTon(){
	//双重检测模型实现的单例模式
		if(singleTon == null){
			synchronized (SingleTon.class) {
				if(singleTon == null){
					singleTon = new SingleTon();
				}
			}
		}
		return singleTon;
	}

按理说双重检测模型的单例模式已经很完备了,运行起来也足够稳定,但还有一个更好的建议,在属性那块,添加一个volatile修饰符来修饰属性。

private static volatile SingleTon singleTon;

目的是为了保证属性的创建及赋值过程不会产生指令重排序。
怎么理解呢?
我们不妨先来看看对象是如何产生的:
发送一行代码 new SingleTon();----->指令
这个指令在内存中做的三件事情是这样的:

  1. 先开辟内存空间-对象
  2. 对象空间初始化(往对象空间里面摆放信息)
  3. 将对象空间的地址赋予变量存储

正常来讲,这三个过程是按顺序执行的,但是CPU在执行的时候,为了提升自身的性能,第2步和第3步可以进行顺序的变化(也就是说CPU认为2步和3步可以进行指令重排)。

最终的懒汉式单例模式代码体现:

public class SingleTon {
	private SingleTon(){
	}
	
	private static volatile SingleTon singleTon;//没有创建对象,初始值为null
	
	public static SingleTon getSingleTon(){
		if(singleTon == null){
			synchronized (SingleTon.class) {
				if(singleTon == null){
					singleTon = new SingleTon();
				}
			}
		}
		return singleTon;
	}

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章