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

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

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

作者: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

目录
相关文章
|
4月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
195 0
|
1月前
|
设计模式 缓存 安全
【JUC】(6)带你了解共享模型之 享元和不可变 模型并初步带你了解并发工具 线程池Pool,文章内还有饥饿问题、设计模式之工作线程的解决于实现
JUC专栏第六篇,本文带你了解两个共享模型:享元和不可变 模型,并初步带你了解并发工具 线程池Pool,文章中还有解决饥饿问题、设计模式之工作线程的实现
140 2
|
1月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
158 1
|
4月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
5月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
357 5
|
6月前
|
设计模式 Java 数据库连接
【设计模式】【创建型模式】工厂方法模式(Factory Methods)
一、入门 什么是工厂方法模式? 工厂方法模式(Factory Method Pattern)是一种创建型设计模式,它定义了一个用于创建对象的接口,但由子类决定实例化哪个类。工厂方法模式使类的实例化延迟
193 16
|
6月前
|
设计模式 负载均衡 监控
并发设计模式实战系列(2):领导者/追随者模式
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发设计模式实战系列,第二章领导者/追随者(Leader/Followers)模式,废话不多说直接开始~
199 0
|
6月前
|
设计模式 监控 Java
并发设计模式实战系列(1):半同步/半异步模式
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发设计模式实战系列,第一章半同步/半异步(Half-Sync/Half-Async)模式,废话不多说直接开始~
186 0
|
6月前
|
设计模式 安全 Java
并发设计模式实战系列(12):不变模式(Immutable Object)
🌟 大家好,我是摘星!🌟今天为大家带来的是并发设计模式实战系列,第十二章,废话不多说直接开始~
160 0
|
6月前
|
设计模式 算法 Java
设计模式觉醒系列(04)策略模式|简单工厂模式的升级版
本文介绍了简单工厂模式与策略模式的概念及其融合实践。简单工厂模式用于对象创建,通过隐藏实现细节简化代码;策略模式关注行为封装与切换,支持动态替换算法,增强灵活性。两者结合形成“策略工厂”,既简化对象创建又保持低耦合。文章通过支付案例演示了模式的应用,并强调实际开发中应根据需求选择合适的设计模式,避免生搬硬套。最后推荐了JVM调优、并发编程等技术专题,助力开发者提升技能。