多线程编程设计模式(单例,阻塞队列,定时器,线程池)(一)

简介: 多线程编程设计模式(单例,阻塞队列,定时器,线程池)(一)

💕"只有首先看到事情的可能性,才会有发生的机会。"💕

作者:Mylvzi

文章主要内容:多线程编程设计模式(单例,阻塞队列,定时器,线程池)

本文主要讲解多线程编程中常用到的设计模式,包括单例模式,阻塞队列,定时器和线程池,包括所有设计模式在java标准库的使用,源码讲解,模拟实现

一.设计模式的概念

简单来说,设计模式就是程序员的棋谱,在日常的开发中,我们经常会遇到一些经典场景,针对这些经典场景,大佬们就总结出了一套行之有效的代码规范,帮助我们更加合理规范的进行编程(就像象棋开局最经典的当头炮,马来跳一样,这就是一个经典场景)

同时呢,设计模式也是计算机中非常重要的一门学科,日常最常使用的设计模式有两种

  1. 单例模式
  2. 工厂模式

下面来分别进行讲解

二.单例模式

1.概念

单例模式指的是单个实例,即我们希望有的类只有一个对象,在之前的学习过程其实我们也遇到过,比如Mysql中JDBC的DataSource,它用于描述服务器的位置,他就只能有一个对象.

日常生活中其实也能找到类似的场景,比如你只能有一个对象,你要是有多个对象肯定是要出问题的,还要被骂渣男,为了保证单例,大佬们就创建出了属于单例的设计模式,来帮助我们应对这个场景

可能有人会说,不就是一个实例吗,那我就new一次不就行了么?当然是可以的,但是你怎么就能保证你只new一次呢?人非圣贤孰能无过,是人总会犯错的,但是机器不会啊,我们可以让机器帮助我们检查我们写的代码是否满足单例模式,不满足直接就报错了.不要认为自己一定不会犯错~有机器帮你检查不是更香吗

其实,通过机器校验来避免编程错误也是一种很常见的方式,有很多我们经常犯的错误都可以交给编译器进行检查,比如:

  1. final 我们希望一个变量不被修改,就将其设置为final,一旦我们修改,编译器就报错
  2. interface 当我们规定一个类实现一个接口,就必须要重写接口内部的抽象方法,否则会报错
  3. @override 重写方法 如果重写方法时,方法名称,返回值,参数列表错误,编译器就会报错

但是呢,对于单例模式来说,在语法上并没有类似上述的检查机制,只能通过一些编程技巧来让机器给我们检查

2.单例模式的分类

单例模式主要分为两类:

  1. 饿汉模式
  2. 懒汉模式

下面进行讲解

1.饿汉模式

首先创建出一个类,类名称为SingleTon

class SingleTon {}

单例模式最重要的是实例只有一个,那么我们就将其设置为类成员

class SingleTon {
  // instance 就是唯一的实例
  private static SingleTon instance = new SingleTon();
}

还要保证类外只能获取到instance,而不能对instance进行修改,所以只提供getInstance方法即可,同时将构造方法也设置为私有的,这样在类外就无法构造新的实例

class SingleTon {
  // instance 就是唯一的实例
  private static SingleTon instance = new SingleTon();
  //  只提供获取方法  不提供set方法
  public static SingleTon getInstance() {
    return instance;
  }
  // 将构造方法设置为私有的
  private SingleTon(){};
  public static void main(String[] args) {
    // 直接通过类来获取唯一的实例
        SingleTon s = SingleTon.getInstance();
        // 如果尝试再次创建一个新的实例  就会报错
//        SingleTon s1 = new SingleTon();
    }
}

这样就完成了饿汉模式的设计,总结来说,需要注意的点有三个

  1. 将唯一的实例设置为static
  2. 只提供获取方法,不提供set设置方法
  3. 将构造方法设置为私有的,保证类外无法创建新的实例

接下来看懒汉模式的设计

2.懒汉模式

这个词在计算机世界中其实是一个褒义词,可以说,正是因为人类的,才促进了计算机的快速发展,当然不仅仅是计算机,还有其他很多物品的出现都是因为,蒸汽机的出现帮助我们少走很多路,我们只需坐在车里休息即可,计算器的出现让我们不用在拿着纸笔算,Excle的出现帮助我们节省了很多重复操作…等等等等所以,其实是一种追求效率的体现(比如笔者最近的博客就是通过MD进行编写的,感觉效率大大提高,还有Vim)

理解了懒的含义,就很容易理解懒汉模式了,懒汉模式就是比饿汉模式效率更高的单例模式,他们最主要的区别在于创建唯一实例的时机不同

我们观察下饿汉模式下,唯一实例的创建时机

可见,饿汉模式下,唯一的实例instance是在类加载的时候就创建了,但是说,我们真的需要一开始就使用么?很多情况下并不是这样的,我们并不需要提前准备,而是应该做到随用随创,我们什么时候想使用,再去创建出来唯一的实例.

就像大学中的很多水课要考试一样,如果一开始就认真听课,努力去背诵,不如等到考试前几天再去背,反正都是背了立马就忘记,帮助我们省去了很多的无意义时间,懒汉模式就是这样,我们并不需要一开始就创建出实例,而是在我们第一次使用的时候再去创建,下面是懒汉模式的代码实现

class SingleTonLazy {
  // instance是唯一的实例  现将其设置为null
  private static SingleTonLazy instance = null;
  // 只提供获取方法
  public static SingleTonLazy getInstance() {
    if(instance == null) {
      // 为null 代表是第一次使用 需要创建
      instance = new SingleTonLazy();
    }
    return instance;
  }
  // 将构造方法设置为私有的
  private SingleTonLazy(){ };
}

懒汉模式虽然效率提高了,但是,他也带来了一些线程安全问题,什么安全问题呢 ?我们学习过一个最经典的线程安全问题就是多个线程针对同一个变量进行修改就会引发线程安全问题,在懒汉模式中,创建实例的时机就会引发这样的问题,我们是先进行判断再去创建一个实例,创建这个过程就是一个修改的过程,有可能线程1先判断完之后,并没有创建出实例,而是紧接着线程2去执行判断+创建这样的操作,此时已经通过线程2 创建出了唯一的实例,但是线程1已经判断完了,他只会执行new部分的代码,这样两个线程都创建出了一个实例,这就违背了单例模式

如何解决呢?使用synchronized进行加锁,保证判断+创建这两步操作是原子的

// 只提供获取方法
  public static SingleTonLazy getInstance() {
    synchronized(SingleTonLazy.class) {
      if(instance == null) {
        // 为null 代表是第一次使用 需要创建
        instance = new SingleTonLazy();
      }
    }
    return instance;
  }

加了锁之后就正确了吗?不是的,此时又产生了一个调度开销问题.观察我们的代码,虽然我们加了锁,保证两个线程不会发生交叉执行的情况,但是我们之后每次获取实例的时候都需要先进行加锁,判断instance是否为null,但实际上我们只需要在第一次创建实例的时候进行判断+创建实例,一旦instance被创建好,以后就不需要进行判断了,直接return instance即可.而且,判断之前还需要进行加锁,也就是以后的每次获取都需要加锁,但加锁其实有一定的开销的,每次都加锁会影响效率的~

解决方法也很简单,我们只需在第一次加锁即可,之后的每次直接return

// 只提供获取方法
    public static SingleTonLazy getInstance() {
        // 最外面一层的if 加锁的频率太多了  是为了减少加锁的次数  避免不必要的开销  
        // 属于一种优化操作
        // 内层的if 仅仅是懒汉模式的特性  只有在调用的时候采取创建出对象
        if(instance == null) {
            // 加锁 是保证if 和new 操作的原子性
            synchronized (SingleTonLazy.class) {
                if (instance == null) {
                  // 为null 代表是第一次使用 需要创建
                    instance = new SingleTonLazy();
                }
            }
        }
        return instance;
    }

注意理解两层if的实际含义与作用,外层的if是为了减少锁的开销,内层的if是懒汉模式的特征

写到这里,懒汉模式的代码其实还没写完(汗流浃背了吧),还存在一些问题,这就涉及到我们之前学习过的指令重排序问题,指令重排序是编译器的一种优化方式,编译器为了提高效率有可能更改指令的执行顺序,对于懒汉模式来说,其中的new操作底层其实分为三步执行:

  1. 在内存中为对象开辟空间
  2. 创建出具体的对象
  3. 将对象在内存中的地址返还给引用instance

其中1的执行顺序是固定的,只能排在第一位,但是2,3的执行顺序是可以改变的!可以是先传地址,再创建(3,2)也可以先创建,在传地址(2,3),如果是(3,2)就可能出现问题!!!

如果发生了指令重排序,就有可能引发上述问题.

这个问题其实在现实生活中也能遇到,比如买房子,买的房子分为两类:精装房和毛坯房,无论是精装的还是毛坯的,都需要现有房子对应的空间,但是是先装修还是先给钥匙是不固定的,先装修再给钥匙就是精装房(先创建具体对象,再返还地址)先给钥匙,再装修就是毛坯房(先给地址,再创建具体的对象),但是最后的结果是一样的,你都买到了房子

如何解决呢?使用volatile修饰变量来解决指令重排序!

private static volatile SingleTonLazy instance = null;

懒汉模式的完整代码:

class SingleTonLazy {
    // 初始设置为null  这样就不会在类加载 的时候就创建出实例
    // 添加volatile是为了禁止new操作的 指令重排序
    private static volatile SingleTonLazy instance = null;
  // 只提供获取方法
    public static SingleTonLazy getInstance() {
        // 最外面一层的if 加锁的频率太多了  是为了减少加锁的次数  避免不必要的开销  
        // 属于一种优化操作
        // 内层的if 仅仅是懒汉模式的特性  只有在调用的时候采取创建出对象
        if(instance == null) {
            // 加锁 是保证if 和new 操作的原子性
            synchronized (SingleTonLazy.class) {
                if (instance == null) {
                  // 为null 代表是第一次使用 需要创建
                    instance = new SingleTonLazy();
                }
            }
        }
        return instance;
    }
    // 将构造方法设置为私有
    private SingleTonLazy() { };
}

说明:有的同学可能对于指令重排序哪里有些问题,认为为什么线程1执行完new操作的1,3之后会调度到线程2呢?他不是加锁了吗?要知道只有两个线程执行到加锁代码部分的时候才会发生锁竞争,另一个线程要等待,但实际上,我们的线程2并没有执行到加锁部分,在外层的if中判定instance非空,直接跳到return部分,并没有进入到加锁部分,也就不涉及到两个线程之间的锁竞争.

总结一下懒汉模式代码的注意事项

  1. 懒汉模式和饿汉模式最大的区别在于instance的创建时机,懒汉模式是随用随创,饿汉模式是在类加载的时候就直接创建
  2. 为了避免两个线程针对同一变量进行修改这样的线程安全问题,我们对判断+创建部分的代码进行synchronized加锁
  3. 为了进一步的减少开销,我们在加锁的外围进一步进行判断,保证只有第一次获取对象的时候加锁,其余时间不加锁
  4. new操作可能涉及到指令重排序问题,还要进一步的对实例instance使用volatile进行修饰,禁止编译器进行指令重排序

补充:如果继续深究懒汉模式的代码,他其实还有两个问题需要考虑:

  1. 使用反射能否打破单例?
  2. 使用序列化/反序列化能否打破单例 ?

这两种情况非常少见,但这里也给出对应的场景,一下内容了解即可

多线程编程设计模式(单例,阻塞队列,定时器,线程池)(二)+https://developer.aliyun.com/article/1413586

目录
相关文章
|
24天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
18 3
|
24天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
24天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
28 2
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
43 1
C++ 多线程之初识多线程
|
24天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
28 1
|
24天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
34 1
|
24天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
24 1
|
2月前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
45 6
|
1月前
|
存储 运维 NoSQL
Redis为什么最开始被设计成单线程而不是多线程
总之,Redis采用单线程设计是基于对系统特性的深刻洞察和权衡的结果。这种设计不仅保持了Redis的高性能,还确保了其代码的简洁性、可维护性以及部署的便捷性,使之成为众多应用场景下的首选数据存储解决方案。
41 1