软件设计模式的种类以及单例模式和volatile关键字的作用

1. 什么是软件设计模式

​ 软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用多数人知晓的代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。

2. 设计模式的分类

  • 创建型模式

    ​ 用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。GoF(四人组)书中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。

  • 结构型模式

    ​ 用于描述如何将类或对象按某种布局组成更大的结构,GoF(四人组)书中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。

  • 行为型模式

    ​ 用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF(四人组)书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。

设计模式总共有三类23种。

由于篇幅太长,本篇只讨论单例设计模式

3. 单例设计模式

​ 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式

​ 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

3.1案例模式的结构

单例模式的主要有以下角色:

  • 单例类。只能创建一个实例的类.
  • 访问类。使用单例类

3.2 单例模式的实现

单例设计模式分类两种:

​ 饿汉式:类加载就会导致该单实例对象被创建。

​ 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。

3.2.1 饿汉式
public class Test01 {
    public static void main(String[] args) {
        Student student = Student.getInstance();
        Student student1 = Student.getInstance();
        System.out.println(student);
        System.out.println(student1);
    }
}
/**
 * 饿汉式
 *      静态变量创建类的对象
 */
class Student{
    private String name;
    // 私有构造方法
    private Student(){};
    // 在成员位置创建该类的对象
    private static Student student = new Student();
    // 对外提供静态方法获取该对象
    public static Student getInstance(){
        return student;
    }
}

运行结果:

在这里插入图片描述

​ 该方式在成员位置声明Student类型的静态变量,并创建Student类的对象student。student对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

​ 我们也发现,运行的结果两个对象的地址的相同的,这是因为单例模式只创建一个对象,返回的对象都是同一个。

3.2.2 懒汉式

方式一(线程不安全):

/**
 * 懒汉式
 *  线程不安全
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance;

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {

        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

​ 从上面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。

为什么会出现线程安全问题呢?

​ 我们可以发现,多线程的情况下,如果对象还没有被创建按,可能会有n个线程同时进入if方法语句,这就导致了创建了n个对象,前几次返回的对象和之后返回的对象不一致,就出现了线程安全问题。

接下来我们通过加锁的方式来解决线程安全问题:

方式二(线程安全但效率低):

/**
 * 懒汉式
 *  线程安全
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance;

    //对外提供静态方法获取该对象
    public static synchronized Singleton getInstance() {

        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

​ 该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

​ 所以有没有既能保证线程安全又能提高效率的方法呢,我们可以通过双重检查锁来提高效率。

方式三(双重检查锁):

​ 我们来讨论一下懒汉模式中加锁的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式

/**
 * 双重检查方式
 */
public class Singleton { 

    //私有构造方法
    private Singleton() {}

    private static Singleton instance;

   //对外提供静态方法获取该对象
    public static Singleton getInstance() {
		//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为null
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

​ 通过以上代码,我们可以看出,只有在instance对象为null时线程才会进行抢锁来创建对象,一旦对象创建完毕,之后的线程便不会再进入抢锁的阶段,既解决了线程安全问题,又提高了效率。

​ 双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

​ 要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

加入volatile关键字:

/**
 * 双重检查方式
 */
public class Singleton {

    //私有构造方法
    private Singleton() {}

    private static volatile Singleton instance;

   //对外提供静态方法获取该对象
    public static Singleton getInstance() {
		//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
        if(instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为空
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

​ 添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

​ 还有,这里的private static volatile Singleton singleton = null;中的volatile也必不可少,volatile关键字可以防止jvm指令重排优化。

​ 如果不知道什么是volatile关键字和jvm指令重排,可以看之后第四小节的介绍。

3.3 单例模式的有点和缺点

优点:

​ 单例类只有一个实例,节省了内存资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能;单例模式可以在系统设置全局的访问点,优化和共享数据,例如前面说的Web应用的页面计数器就可以用单例模式实现计数值的保存。

缺点:

​ 单例模式一般没有接口,扩展的话除了修改代码基本上没有其他途径。

3.4 懒汉式和饿汉式的区别

​ 懒汉模式的优点便是在代码中没有使用的情况下,不会去加载单例类的资源不会造成资源的浪费。缺点也很明显,加锁同步会带来程序运行效率的损失。

​ 饿汉模式的优缺点恰好与懒汉模式相反,如果明确知道单例对象在程序代码中用的很频繁,就可以考虑使用饿汉模式了。

​ 可能有许多小伙伴不知道什么是volatile关键字和指令重排,接下来我们来聊一聊volatile关键字。

4. volatile关键字

4.1 volatile关键字的作用

​ 在java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。

4.2 禁止指令重排

​ 首先,我们得先知道什么是指令重排。如上面代码种的创建对象的操作 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:

  1. 第一步是给 singleton 分配内存空间。
  2. 第二步开始调用 Singleton 的构造函数等,来初始化 singleton。
  3. 第三步,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。

​ 这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。

​ 如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错,详细流程如下图所示:

在这里插入图片描述

4.3 保证可见性

​ 除了禁止指令重排,volatile关键字的第二个作用,保证变量在多线程运行时的可见性。

public class Test01 {
    public static void main(String[] args) throws Exception{
        T t=new T();
        t.start();

        Thread.sleep(2000);
        System.out.println("主线程设置t线程的参数来停止损失");
        t.setFlag(false);
    }
}
class T extends Thread{
    private volatile boolean flag=true;

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        System.out.println("进入run方法");
        while(flag){
            ...
        }
    }
}

​ 通过上面的代码我们可以发现,当第一个线程A进入到run方法中时,第二个线程B修改了flag的值为false,而此时线程A并不会看到flag的值变成了false,会一直循环下去,也就出现了可见性的问题。

​ 在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前 的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数 据的不一致。 要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。

在这里插入图片描述

  • 33
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值