文章目录
- 一、什么是设计模式?
- 二、单例模式
- 2.1、饿汉模式
- 2.2、懒汉模式
- 2.3、多线程情况下调用 饿汉模式与懒汉模式 谁是安全的??(重点)
- 三、工厂模式
- 3.1、什么是工厂模式?
- 3.1.1、构造方法存在的缺陷
- 3.1.1.1、构造方法的名字必须固定是类名
一、什么是设计模式?
设计模式就相当于菜谱,有了菜谱/秘籍,就能够根据菜谱上的指引/步骤做出许多从前不会的美食,就算不会下厨的人,拥有了食谱,他的厨艺也能够得到提升和保障。
因此设计模式就是程序员的菜谱,设计模式中介绍了许多典型场景,以及针对这些典型场景的处理办法。按照设计模式来写的代码不会很差,在一定的规范范围里。
设计模式有很多种,不止23种,今天主要介绍两种常见、常用的:
1、单例模式
2、工厂模式
二、单例模式
单例模式对应的场景:希望代码中的有些对象在整个程序中只有一个实例(即对象只能 new 一次)。
譬如说:JDBC中的数据源DataSource这样的对象就只需要一个即可,因此这个对象就可以使用单例模式。
对于我们程序员来说,如果有些对象在整个程序中只需要有一个实例即可,那我们程序员就只new一次就好了,为什么还需要引入单例模式??那是因为对于程序来说,人是不靠谱的,就需要引入单例模式,此时由编译器来进行严格的检查(对于代码中只能new一次的对象,如果尝试new了多次,编译器就会直接报错来进行提示),确保某些对象处于单例模式。
接下来介绍一下在Java中单例模式的两种写法:
2.1、饿汉模式
饿汉模式:程序启动进行类加载之后,立即创建出实例。(比较迫切)
代码例子:
class Singleton{
/**
* 此处就期望类Singleton只有一个实例
*/
// 静态成员:instance
// static 静态的,与类相关的,整个程序中只有一份
private static Singleton instance = new Singleton();
// 通过此方法,随时获取到刚刚的 instance 变量
public static Singleton getInstance(){
return instance;
}
// 作出限制:禁止别人去 new 这个类的实例——>将构造方法变成private
private Singleton(){
}
}
public class testHunger {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
// 此时我们再new类的实例,直接编译报错(编译器校验了)
// Singleton s3 = new Singleton();
// 我们可以发现 s1、s2 这两个都是同一个对象
System.out.println(s1.equals(s2)); //true
// s1、s2 和 s3 不是同一个对象
// System.out.println(s3.equals(s1)); //false
// System.out.println(s3.equals(s2)); //false
}
}
但是上述代码中,我们将类的构造方法改成private,此时别人就一定不能创建多个实例了吗??其实还是可以通过 反射机制 ,在当前单例模式中,创建出多个实例。
但是反射属于 “非常规” 的编程手段,正常开发的时候,慎重使用,因为 滥用反射,会给代码带来极大风险,譬如会让代码变得抽象,日后难以维护,还破坏了Java的封装性。
当然Java中也有其他方式书写单例模式来防止反射,但此处不做过多介绍,可以自行查阅资料了解。
2.2、懒汉模式
懒汉模式:第一次使用实例的时候才创建实例,否则能不创建就不创建。(比较佛系)
/**
* 代码示例:
* 单例模式中的懒汉模式
*/
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){
}
}
public class testLazy {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
// 同样也是可以通过非常规手段:反射机制 获取类的实例
Class<SingletonLazy> s3 = SingletonLazy.class;
System.out.println(s3.equals(s1));
System.out.println(s1.equals(s2));
}
}
2.3、多线程情况下调用 饿汉模式与懒汉模式 谁是安全的??(重点)
饿汉模式:
懒汉模式:
上述逻辑,也是一个经典的面试题!
那我们如何保证懒汉模式的线程安全呢?
由于上述引起线程安全的原因是 if语句与new语句不是一个整体,因此出现了线程安全问题,此时我们通过将if语句和new语句打包成一个整体来解决懒汉模式的线程安全问题。
加锁之后,线程在cpu上的执行流程:(当加了锁之后,线程t2执行if语句时,就会发现此时的instance是一个非null的值,)
虽然加了锁,解决了线程安全问题,但是还存在问题:加锁是一个高成本的操作,会引起阻塞等待。加锁的基本原则应该是:非必要,不加锁。不能无脑加锁,如果无脑加锁,就会导致程序的执行效率受到影响。
此时我们对懒汉模式的代码中加的锁,导致后续每次调用 getInstance() 方法都要加锁,但是这是不合理且不必要的。**懒汉模式线程不安全主要是因为首次 new 对象时,才存在的。一旦将对象 new 出来后,后续再调用 getInstance() 方法就不存在线程安全问题了。**但是我们现在的加锁是:首次 new 对象调用时,加锁了。后续调用,也加锁了。但实际上后续调用不必加锁,因为后续调用后if条件就进不去了,此时也就不再涉及到修改操作了,全是读操作。但我们把不该加锁的地方加上锁了,很影响程序的执行效率。
对加锁操作做出以下修改:
两个if语句之间存在加锁操作,加锁就会引起线程阻塞等待,究竟阻塞等待多久,不知道,有可能第一个if语句与第二个if语句间隔沧海桑田,因此在这个长久的时间间隔里,可能别的线程就把instance改了。
上述对加锁操作做了修改之后,还存在一个问题:
已知一般 instance = new SingletonLazy();可以大致分成3个步骤:
1、给对象创建出内存空间,得到内存地址。
2、在空间上调用构造方法,对对象进行初始化。
3、把内存地址赋值给 instance引用。
假设现在代码的执行顺序由123变成132,但是在执行步骤3之后,进行了线程切换,此时还没来得及执行步骤2,即给对象初始化,就调度给别的线程了,此时别的线程执行的时候,判断instance不为空了,于是就直接返回instance,并且后续代码中可能会使用 instance 中的一些属性或者方法,但是由于多线程下出现了指令重排序的问题导致线程安全问题,此时的instance是个空引用,即拿到的这个对象,是个没有进行初始化,不完整的对象。
解决办法:给变量 instance 加上 volatile 之后,此时针对 instance 的赋值操作,就不会产生上述的指令重排序了,其执行顺序必然是遵循1、2、3执行。
加上 volatile 还有一个另外的用途:避免此处赋值操作的指令重排序。
懒汉模式的最终代码版本:
/**
* 代码示例:
* 单例模式中的懒汉模式
*/
class SingletonLazy{
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if (instance == null){
synchronized (SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
public class testLazy {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
// 同样也是可以通过非常规手段:反射机制 获取类的实例
Class<SingletonLazy> s3 = SingletonLazy.class;
System.out.println(s3.equals(s1));
System.out.println(s1.equals(s2));
}
}
懒汉模式代码的3大要点:
1、加锁
2、双重if
3、volatile
从字面上理解/区分 饿汉模式和懒汉模式 会比较抽象,举2个例子加深一下理解:
譬如说日常生活中,吃完饭之后需要洗碗,有的人习惯吃完饭后立即洗碗,有的人习惯吃完饭后将碗放到一边,等到下顿饭要吃的时候才洗碗。一般大家会觉得吃完饭之后立即洗碗是一种高效率的生活方式,但其实在不考虑卫生的情况下,吃完饭后不洗碗,等到下次开饭再洗碗然后接着用,其实效率会更高。比如说这顿吃完有4个碗,下顿开饭只需要用到2个碗,此时就只需要洗2个碗来用就行了,效率大大提高了。
还譬如说:假设编辑器打开一个很大的文件,有的编辑器会一下尝试把所有内容都加载到内存中,再显示出来,这是典型的饿汉模式。有的编辑器,则只加载一部分内容(一个屏幕能显示的内容),其他部分,当用户翻页想要浏览时,再加载出来,这是典型的懒汉模式。
三、工厂模式
3.1、什么是工厂模式?
工厂,顾名思义是用来生产的。那么对应到我们代码上,工厂模式就是用来生产对象的。那么具体是怎么进行生产对象的呢??一般我们创建对象都是通过 new 的形式,使用构造方法来把对象创建出来,但构造方法存在一些缺陷,因此此时就可以使用 工厂模式 解决 上述问题。
3.1.1、构造方法存在的缺陷
3.1.1.1、构造方法的名字必须固定是类名
但是有时候有的类需要多种不同的构造方式,可由于构造方法名必须与类名一致,此时就只能使用方法重载(参数的个数和类型需要有差别)的方式来区分了。
譬如有一个坐标系类 Point。在此类中想要通过两种构造方式进行创建对象,一种是笛卡尔坐标系构造,一种是按照极坐标构造:
public class Point{
public Point(double x,double y){
}
public Point(double r,double a){
}
}
这两种构造方式的意义并不一样,但是使用构造方法表示出来时,由于其参数个数和类型都一致,因此无法构成方法重载,因此上述代码就会直接编译报错。那此时就可以使用工厂模式来解决上述问题:1、即不使用构造方法了,使用普通的方法来构造对象,这样方法名字就可以是任意的了。2、在普通方法的内部,再来new对象。
由于普通方法的目的是为了创建出对象来,因此这样的普通方法一般得是静态的。(因为要创建实例却又依赖实例,因此这样的普通方法设定为 静态的)
public class Point{
//由于这两方法的方法名不同,此时不会编译报错,
public static makePointXY(double x,double y){
//我们再在方法内部 new 出对象
Point p = Point.makePointXY(10,20);
}
public static makePointRA(double r,double a){
}
}
这样的操作,我们就称为 “工厂模式”,这样的方法我们就称为 “工厂方法”