目录
设计模式引入
饿汉模式
懒汉模式
单例模式总结
设计模式引入
1.1.什么是设计模式
(1)设计模式就是一种代码的套用模板。例如:一类题型的步骤分别有哪些,是可以直接套用的。
(2)像棋谱,也是一种设计模式。根据棋谱去下棋,棋术自然不会很差。
(3)官方定义就是说:设计模式就是一套经过反复使用、多人知晓、经过分类、代码设计经验的总结
(4)使用代码设计模式可以提高代码的下限。更容易被人理解,使用等等
本文我们主要介绍设计模式中的单例设计模式
1.2.单例设计模式
(1)单例模式,就是只有单个对象。
(2)单例模式,在整个代码进程中的某个类,有且只会产生一个对象,也不会多new出来前提的对象(这个类就是单例模式,它只会产生一个对象)
(3)如何保证只会有一个对象呢?那就需要程序员通过代码去设计,用代码去限制和规范。
(4)根本上可以保证对象是唯一的,这种设计模式就称为单例模式
(5)单例模式,本节内容介绍两种最常用的:饿汉模式和懒汉模式
饿汉模式
1.1.概念引入
(1)什么是饿汉模式?听名字就是一个很饿的汉子
(2)饿汉模式,就是在类加载的时候,就完成了对象的实例化
(3)我们把迫不及待的实例化对象这种行为,称为饿汉模式
1.2.代码设计
(1)类体
这个名字是可以随便起的
class Singleton {
}
(2)实例化对象
这里直接实例化一个饿汉对象,赋值给instance引用。这个引用是private static类型的,外部访问不到,而且在类加载的时候,就把对象给初始化好了。
class Singleton {
private static Singleton instance = new Singleton();//一加载就实例化对象,称为饿汉
}
(3)提供获取对象的方法
class Singleton {
private static Singleton instance = new Singleton();//一加载就实例化对象,称为饿汉
//用于外部获取饿汉对象
public static Singleton getInstance() {
return instance;
}
private Singleton() {
//私有构造方法,外部无法再进实例化
}
}
外部通过调用这个方法,就可以拿到对象的一个引用;多次调用改方法,获得的对象都是同一份。
(4)私有构造方法
这是最关键的一步,将构造方法设置为私有的,外部就无法在new对象了。下面这也是“饿汉模式”的完整代码了
class Singleton {
private static Singleton instance = new Singleton();//一加载就实例化对象,称为饿汉
//用于外部获取饿汉对象
public static Singleton getInstance() {
return instance;
}
private Singleton() {
//私有构造方法,外部无法再进实例化
}
}
(5)验证是否同一个对象
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
我们发现,这两个对象都是一样的。
(6)验证外部不可new
以上就饿汉模式代码设计的全部了,还有一些小问题,下面接着介绍。
1.3.线程安全等问题
(1)线程安全问题
在多线程代码中,对于饿汉设计模式的代码,是线程安全的。
原因:饿汉模式在加载类的时候,对象就已经实例化好了,比线程通过get方法去获得改对象的引用更快;后续线程获取对象的时候,都是获取到的同一个变量。
(2)保证唯一对象问题
初心:类似饿汉模式的代码设计,都是提供给外部使用的,外部是无法new对象的;即使内部可以,但是设计者不会那么蠢。
外部new对象怎么办:外部是可以通过反射的方式再new一次对象,但是这种方式我们不考虑,反射本身的开销和成本也很大。就像有人下定决心要偷你家东西,你也无法防住。
懒汉模式
1.1.概念引入
(1)听名字,看似懒,其实是高效率
(2)懒汉模式:类很懒,当程序员需要对象而去调用时,它才会实例化对象
(3)我们把这种不着急实例化对象的,称为“懒汉模式”
1.2.代码设计
(1)类体
老样子,名字随意
class SingletonLazy {
}
(2)对象的引用
因为很懒,所以一开始是不实例化对象的
class SingletonLazy {
private static SingletonLazy instance = null;//一开始不初始化对象
}
(3)对外开放对象
只有在第一次调用改方法时,才会去实例化一个对象
class SingletonLazy {
private static SingletonLazy instance = null;//一开始不初始化对象
public static SingletonLazy getInstance() {//什么时候调用,才会初始化对象
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
(4)关闭构造方法
只有这样,外部才没有办法去new对象
class SingletonLazy {
private static SingletonLazy instance = null;//一开始不初始化对象
public static SingletonLazy getInstance() {//什么时候调用,才会初始化对象
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {
}
}
上面就是一个和饿汉模式类似的代码结构了
(5)同一个对象
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
(6)外部不可new
这里打断一下,上面的代码并不完整,因为上面的代码是一个线程不安全的代码。
线程安全的完整代码:接下来介绍
class SingletonLazy {
private static volatile SingletonLazy instance = null;//一开始不初始化对象
private static Object locker = new Object();
public static SingletonLazy getInstance() {//什么时候调用,才会初始化对象
if(instance == null) {
synchronized (locker) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
1.3.线程安全问题
(1)为什么不安全?
在类加载完之后,并没有实例化出对象;此时两个线程去调用getInstance()方法,就很大可能会创造出两个对象,这就违背了一个对象原则了。
(2)为什么会有线程不安全问题?
我们从指令的执行和线程调度方面切入分析。下面都是按照发生线程安全问题的情况下
1)有两个线程t1、t2,先后调用了改方法,并且下面的条件都成立,并且进入
if(instance == null)
像这样先后进入if语句之后,他们就会创造出两个实例,这就是线程不安全的问题。
上面产生的问题就是由于操作不是原子性造成的,我们只需要进行加锁操作就好。
1.4.设计线程安全代码
(1)加锁之后
private static Object locker = new Object();
public static SingletonLazy getInstance() {//什么时候调用,才会初始化对象
synchronized (locker) {
if(instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
加锁之后,同一个时间就只会有一个线程进入if语句并且实例化对象,当该线程解锁之后,对象已经创造好了;此时第二个线程不再阻塞,并进入if语句,但是此时条件已经不成立便不会进入。此时,就不会由于多线程代码而产生问题了。
(2)判断是否要加锁
上面的代码产生线程安全问题的主要原因是:由于对象没有实例化好,但是,当对象实例化好之后再去调用该方法,是不会产生任何线程问题的。此时,锁就现得很笨重,因此,我们只需要让第一次调用时加锁就好,于是得出下面的改进代码
private static Object locker = new Object();
public static SingletonLazy getInstance() {//什么时候调用,才会初始化对象
if(instance == null) {
synchronized (locker) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
这就是双重if判断,但是两个if语句发挥的作用不一样。
第一个if:判断是否要加锁
第二个if:判断是否要创建对象
(3)预防指令重排序
上述代码就是最终的安全代码了吗?不,还不是,还会存在一个问题,那就是指令重排序造成的线程安全问题。
instance = new SingletonLazy();
new对象这个代码大概可以分成三步
1.申请内存空间
2.调用构造方法(对内存空间进行初始化)
3.把此时内存空间的地址,赋值给instance引用
指令执行的顺序大概有两种:123或者132。在单线程中,两种顺序都不会出现问题,但是在多线程中,132的顺序是会出现问题的。
问题:当执行的顺序是132时,t1线程执行完3之后,也就是赋值给了instance引用,此时不为空,然后被调度走,t2线程就可以返回这个引用,此时这个对象内部是没有初始化的,里面的值都是0,t2线程拿着这个引用去做一些事情,就会引起一些问题。
所以,我们要加上volatile关键字。
private static volatile SingletonLazy instance = null;//一开始不初始化对象
可见,volatile关键字表面上是预防内存可见性问题,更深的是预防指令重排序问题。给变量加上,可以预防对该变量发生一下指令重排序,可以保证intance引用存放的是完整对象。
线程安全的懒汉模式代码:
class SingletonLazy {
private static volatile SingletonLazy instance = null;//一开始不初始化对象
private static Object locker = new Object();
public static SingletonLazy getInstance() {//什么时候调用,才会初始化对象
if(instance == null) {
synchronized (locker) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
单例模式总结
1.单例模式引用场景
(1)在实际的业务中,有的类,只需要有一个对象就够了
(2)比如,要写一个类,用来实现一些功能,可以加载上百G的数据。所以这个类只需要创造一个对象,就可以管理完这些数据了。如果是多个实例,消耗的内存也更加大。
2.懒汉模式的优势
(1)在程序加载中,如果有多个单例模式且是饿汉模式,那么在加载的时候就需要花费很多的时间。
(2)如果是懒汉模式,在程序加载的时候不会一下子创造出很多的对象,因此,时间效率就得到了提高。