原创

设计模式

  • 设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。
  • 设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
  • 设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
  • 设计模式不是一种方法和技术,而是一种思想。设计模式和具体的语言无关,学习设计模式就是要建立面向对象的思想,尽可能的面向接口编程,低耦合,高内聚,使设计的程序可复用。
  • 学习设计模式能够促进对面向对象思想的理解,反之亦然。它们相辅相成。

设计模式分类

设计模式按照功能分为三类23种:

  1. 创建型(5种):工厂模式、抽象工厂模式、单例模式(重点)、原型模式、构建者模式
  2. 结构型(7种):适配器模式、装饰模式、代理模式(重点) 、外观模式、桥接模式、组合模式、享元模式
  3. 行为型(11种):模板方法模式、策略模式、观察者模式、中介者模式、状态模式、责任链模式、命令模式、迭代器模式、访问者模式、解释器模式、备忘录模式

设计模式注意事项:

每个设计模式都有自己的优缺点,要根据实际情况,去选择合适的设计模式

创建型

为什么要使用创建型设计模式?

  • 有些时候开发人员不想要知道对象的创建细节,只是想要创建一个可以使用的对象。所以可以将创建的工作给到专业的类。
  • 由于要使用的类是第三方包提供的,我们基本上不了解如何去构造该对象。

简单工厂

一个工厂类做所有的事情,可以生产所有需要的对象(万能工厂)

优缺点

优点:

  • 很明显,简单工厂的特点就是“简单粗暴”,通过一个含参的工厂方法,我们可以实例化任何产品类。
    缺点:
  • 任何”东西“的子类都可以被生产,负担太重。当所要生产产品种类非常多时,工厂方法的代码量可能会很庞大。
  • 违反了开闭原则。

改造方案:

  1. spring改造
  2. 工厂方法

工厂方法

工厂会是多个,生产一个产品(抽象概念),针对不同的需求,可以创建不同的工厂,问题是如何创建工厂?

先有造工厂的方法(标准也叫接口),在根据标准去造工厂,由这个工厂创建不同的对象

优缺点

优点:

  • 工厂方法模式就很好的减轻了工厂类的负担,把某一类/某一种东西交由一个工厂生产;(对应简单工厂的缺点1)
  • 同时增加某一类”东西“并不需要修改工厂类,只需要添加生产这类”东西“的工厂即可,使得工厂类符合开闭原则。
    缺点:
  • 对于某些可以形成产品族(一组产品)

抽象工厂

工厂会是多个,生产多个产品(抽象概念),这多个产品属于一个产品族

构建者

将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。

构建者模式和工厂模式很类似,区别在于构建者模式是一种个性化产品的创建。而工厂模式是一种标准化的产品创建。

构建者模式角色

  1. 产品:要生产的对象
  2. 导演类:如何去订制一个对象
  3. 构建者类:完成私人订制功能的具体类

构建者模式与常用的set模式区别:使用builder模式(一般是使用内部builder类实现)时,尚未产生对象,还不能被使用。所以,不用担心对象污染。

原型模式

原型模式虽然是创建型的模式,但是与工厂模式没有关系,从名字即可看出,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。

就是说不用new的方式创建对象,而是直接复制对象,分为浅复制和深复制(用的底层C语言的clone和serialize方式)。

浅拷贝

只有对象的基本类型和String类型进行复制,而引用类型只是复制了引用,没有将引用类型对应的对象进行复制。
需要实现clonable
/* 浅复制 */
public Object clone() throws CloneNotSupportedException {
    Prototype proto = (Prototype) super.clone();
    return proto;
}

深拷贝

引用类型对应的对象,也在内存中复制一份。
需要实现serializable
/* 深复制 */
public Object deepClone() throws IOException, ClassNotFoundException {
    /* 写入当前对象的二进制流 */
    ByteArrayOutputStream bos = new ByteArrayOutputStream(); 
    ObjectOutputStream oos = new ObjectOutputStream(bos); 
    oos.writeObject(this);
    /* 读出二进制流产生的新对象 */
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bis);
    return ois.readObject();
}

单例模式

单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:
1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。

单例模式分成两种实现方式:懒汉式和饿汉式

饿汉式

在类初始化时就将对象创建出来

public class Student1 {

    // 2:成员变量初始化本身对象
    // 静态类在进行类加载(静态变量初始化)的时候,在JVM内部是保证线程安全的
    private static Student1 student = new Student1();

    // 1.构造私有
    private Student1() {}

    // 3:对外提供公共方法获取对象
    public static Student1 getSingletonInstance() {
        return student;
    }

    public void sayHello(String name) {
        System.out.println("hello," + name);
    }
}

饿汉式不存在线程安全问题,但是会浪费内存资源

懒汉式

在第一次使用该类的时候

public class Student2 {

    //1:构造私有
    private Student2(){}

    //2:定义私有静态成员变量,先不初始化
    private static Student2 student = null;

    //3:定义公开静态方法,获取本身对象
    public static Student2 getSingletonInstance(){
        //没有对象,再去创建
        if (student == null) {
            student = new Student2();
        }
        //有对象就返回已有对象
        return student;
    }
}

节省内存资源,但是存在线程安全问题

懒汉式三种线程安全写法

  1. 双重检查锁
  2. 静态内部类
  3. 枚举

并发编程三大特性

  1. 原子性: CPU操作指令必须是原子的;广义上是字节码指令是原子的
  2. 有序性: 狭义上指的是CPU操作指令是有序执行的;广义上指的是字节码指令是有序执行的
  3. 可见性: 在多核CPU下,不同的CPU缓存之间是相互不可见的

双重检查锁

首先来看几个问题:

1. 如何保证原子性?
加锁(synchronize、Lock)

2. 指令重排序?
JIT即时编译器的优化策略,happend-before六大原则。
两行代码之后的操作,执行结果不存在影响,就可以发生指令重排序。

3. 对象在JVM中的创建步骤
Student student = new Student(); 过程中
1.new:开辟JVM堆中的内存空间
2.将内存空间初始化(指的就是对象的成员变量初始化为0值)
3.将内存空间地址(引用地址)赋值给引用类型的变量
结论:在new对象的时候,JIT即时编译器会根据运行情况,对对象创建的过程进行指令重排序(132)

4. 线程执行需要竞争CPU时间片来执行

由以上几点,当线程A双重检查锁单例中进行对象创建的时候释放时间片,线程B进入单例并识别对象不为空,并调用单例中的属性时,会有NullPointer风险。

所以在双重检查锁中,使用volatile关键字。该关键字有几个作用:

  1. 禁止被它修饰的变量发生指令重排序。是通过内存屏障实现的。
  2. 禁止使用CPU缓存(保证可见性),内部实现是被volatile关键词修饰的变量,在修改之前都需要将CPU缓存中的数据刷新到主内存中。
public class Student5 {

    private volatile static Student5 student;

    private Student5() {
    }

    public static Student5 getSingletonInstance() {
        if (student == null) {
            // B线程检测到student不为空
            synchronized (Student5.class) {
                if (student == null) {
                    student = new Student5();
                    // A线程被指令重排了,刚好先赋值了;但还没执行完构造函数。
                }
            }
        }
        return student;// 后面B线程执行时将引发:对象尚未初始化错误。
    }

}

静态内部类

类似于饿汉式。

public class Student {

    private Student() {}

    /*
     * 此处使用一个内部类来维护单例 JVM在类加载的时候,是互斥的,所以可以由此保证线程安全问题
     */
    private static class SingletonFactory {
        private static Student student = new Student();
    }

    /* 获取实例 */
    public static Student getSingletonInstance() {
        return SingletonFactory.student;
    }

}

原理:当使用Student.getSingletonInstance()时,相当于SingletonFactory的静态初始化,而SingletonFactory使用饿汉式,所以线程安全。

破坏单例的方式

  1. 反射攻击
    public class SingletonAttack {
     public static void main(String[] args) throws Exception {
         reflectionAttack();
     }
     public static void reflectionAttack() throws Exception { 
         //通过反射,获取单例类的私有构造器
         Constructor constructor = DoubleCheckLockSingleton.class.getDeclaredConstructor(); //设置私有成员的暴力破解
         constructor.setAccessible(true); // 通过反射去创建单例类的多个不同的实例 
         DoubleCheckLockSingleton s1 = (DoubleCheckLockSingleton)constructor.newInstance(); // 通过反射去创建单例类的多个不同的实例 DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)constructor.newInstance();
         s1.tellEveryone();
         s2.tellEveryone();
         System.out.println(s1 == s2);
     }
    }
    
  2. 序列化攻击
    就是深复制
    public class SingletonAttack {
     public static void main(String[] args) throws Exception {
         serializationAttack();
     }
     public static void serializationAttack() throws Exception { // 对象序列化流去对对象进行操作
         ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serFile"));
         //通过单例代码获取一个对象
         DoubleCheckLockSingleton s1 = DoubleCheckLockSingleton.getInstance(); //将单例对象,通过序列化流,序列化到文件中 outputStream.writeObject(s1); // 通过序列化流,将文件中序列化的对象信息读取到内存中
         ObjectInputStream inputStream = new ObjectInputStream(new
         FileInputStream(new File("serFile"))); //通过序列化流,去创建对象
         DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)inputStream.readObject();
         s1.tellEveryone();
         s2.tellEveryone();
         System.out.println(s1 == s2);
     }
    }
    

枚举单例

public enum EnumSingleton {
    INSTANCE;
    public void tellEveryone() {
        System.out.println("This is an EnumSingleton " + this.hashCode());
    } 
}
  1. 枚举单例预防反射攻击

java枚举类都隐式继承Enum抽象类,而Enum抽象类没有无参构造方法,只有一个

protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

而在暴力破解时使用反射Constructor时

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enumobjects");

又不允许Enum类进行实例化。

  1. 枚举单例防御序列化攻击

在使用ObjectInputStream类时,对枚举类型有一个专门的readEnum()方法来处理,其简要流程如下:

  • 通过类描述符取得枚举单例的类型EnumSingleton;
  • 取得枚举单例中的枚举值的名字(这里是INSTANCE);
  • 调用Enum.valueOf()方法,根据枚举类型和枚举值的名字,获得最终的单例。
    这种处理方法与readResolve()方法大同小异,都是以绕过反射直接获取单例为目标。不同的是,枚举对序列化的防御仍然是JDK内部实现的。
综上所述,枚举单例确实是目前最好的单例实现了,不仅写法非常简单,并且JDK能够保证其安全性, 不需要我们做额外的工作。
正文到此结束
本文目录