多线程设计模式【线程安全、 Future 设计模式、Master-Worker 设计模式 】(一)-全面详解(学习总结---从入门到深化)

2023-07-13 20:20:29

 

目录

Single Thread Execution 设计模式

 线程安全

 Future 设计模式

Master-Worker 设计模式 

 生产者消费者设计模式

定义不可变对象的策略


 

Single Thread Execution 设计模式

机场过安检

Single Thread Execution 模式是指在同一时刻只能有一个线程去访问共享资源,就 像独木桥一样每次只允许一人通行,简单来说, Single Thread Execution 就是采用排 他式的操作保证在同一时刻只能有一个线程访问共享资源。 相信大家都有乘坐飞机的经历,在进入登机口之前必须经过安全检査,安检口类似于独木桥,每次只能通过一个人,工作人员除了检査你的登机牌以外,还要联网检查身份证信息以及是否携带危险物品,如下图所示。

 

 非线程安全

先模拟一个非线程安全的安检口类,旅客(线程)分别手持登机牌和身份证接受工作人 员的检查,示例代码如下所示。

package com.tong.chapter14;
public class FlightSecurity {
    private int count = 0;
    private String boardingPass = "null";// 登机牌
    private String idCard = "null";// 身份证
    public void pass(String boardingPass, String idCard) {
             this.boardingPass = boardingPass;
             this.idCard = idCard;
             this.count++;
             check();
}
    private void check() {
    // 简单的业务,当登机牌和身份证首位不相同时则表示检查不通过
    if (boardingPass.charAt(0) != idCard.charAt(0)) {
          throw new RuntimeException("-----Exception-----" + toString());
   }
}
@Override
public String toString() {
           return "FlightSecurity{" + "count=" + count + ", boardingPass='" + boardingPass + '\'' + ", idCard='" + idCard + '\'' + '}';
    }
}

FlightSecurity 比较简单,提供了一个 pass 方法,将旅客的登机牌和身份证传递给 pass 方法,在 pass 方法中调用 check 方法对旅客进行检查,检查的逻辑也足够的简单, 只需要检测登机牌和身份证首位是否相等(当然这样在现实中非常不合理,但是为了使测试简单我们约定这么做),我们看以下代码所示的测试。

package com.tong.chapter14;
public class FlightSecurityTest {
       static class Passengers extends Thread {
       // 机场安检类
       private final FlightSecurity flightSecurity;
       // 旅客身份证
       private final String idCard;
       // 旅客登机牌
       private final String boardingPass;
       public Passengers(FlightSecurity flightSecurity, String idCard, String boardingPass) {
       this.flightSecurity = flightSecurity;
       this.idCard = idCard;
       this.boardingPass = boardingPass;
}
@Override
public void run() {
   while (true) {
         // 旅客不断地过安检
         flightSecurity.pass(boardingPass, idCard);
       }
    }
}
public static void main(String[] args) {
      // 定义三个旅客,身份证和登机牌首位均相同
       final FlightSecurity flightsecurity = new FlightSecurity();
       new Passengers(flightsecurity, "Al23456", "AF123456").start();
       new Passengers(flightsecurity, "B123456", "BF123456").start();
       new Passengers(flightsecurity, "C123456", "CF123456").start();
    }
}

看起来每一个客户都是合法的,因为每一个客户的身份证和登机牌首字母都一样,运行 上面的程序却出现了错误,而且错误的情况还不太一样,运行多次,发现了两种类型的错误信息,程序输出如下:

java.lang.RuntimeException: -----Exception-----FlightSecurity{count=218,boardingPass='AF123456', idCard='B123456'}
java.lang.RuntimeException: -----Exception-----FlightSecurity{count=676,boardingPass='BF123456', 

首字母相同检查不能通过和首字母不相同检查不能通过,为什么会出现这样的情况呢? 首字母相同却不能通过?更加奇怪的是传入的参数明明全都是首字母相同的,为什么会出现首字母不相同的错误呢。

 问题分析

首字母相同却未通过检查

1)线程 A 调用 pass 方法,传人”A123456”“AF123456”并且对 idcard 赋值成功,由 于 CPU 调度器时间片的轮转,CPU 的执行权归 B 线程所有。

2) 线程 B 调用 pass 方法,传入”B123456”“BF123456”并且对 idcard 赋值成功, 覆盖 A 线程赋值的 idCard。

3)线程 A 重新获得 CPU 的执行权,将 boardingPass 赋于 AF123456,因此 check 无 法通过。

4)在输出 toString 之前,B 线程成功将 boardingPass 覆盖为 BF123456。

为何出现首字母不相同的情况 

1)线程 A 调用 pass 方法,传入”A123456”“AF123456”并且对 id Card 赋值成功,由 于 CPU 调度器时间片的轮转,CPU 的执行权归 B 线程所有。

2)线程 B 调用 pass 方法,传入”B123456”“BF123456”并且对 id Card 赋值成功,覆 盖 A 线程赋值的 idCard。

3)线程 A 重新获得 CPU 的执行权,将 boardingPass 赋于 AF123456,因此 check 无 法通过。

4)线程 A 检查不通过,输出 idcard=”A123456”和 boardingPass=”BF123456”。

 线程安全

上面出现的问题说到底就是数据同步的问题,虽然线程传递给 pass 方法的两个参数能 够百分之百地保证首字母相同,可是在为 FlightSecurity 中的属性赋值的时候会出现多个线程交错的情况,结合我们之前所讲内容可知,需要对共享资源增加同步保护,改进代码如下。

public synchronized void pass(String boardingPass, String idCard) {
       this.boardingPass = boardingPass;
       this.idCard = idCard;
       this.count++;
       check();
}

修改后的 pass 方法,无论运行多久都不会再出现检查出错的情况了,为什么只在 pas 方法增加 synchronized 关键字, check 以及 toString 方法都有对共享资源的访问,难道它们不加同步就不会引起错误么?由于 check 方法是在 pass 方法中执行的,pass 方法加同步已经保证了 single thread execution,因此 check 方法不需要增加同步, toString 方法原因与此相同。

何时适合使用 single thread execution 模式呢?答案如下。

A. 多线程访问资源的时候,被 synchronized 同步的方法总是排他性的。

B. 多个线程对某个类的状态发生改变的时候,比如 Flightsecurity 的登机牌以及身 份证。

 在 Java 中经常会听到线程安全的类和线程非安全的类,所谓线程安全的类是指多个线 程在对某个类的实例同时进行操作时,不会引起数据不一致的问题,反之则是线程非安全的类,在线程安全的类中经常会看到 synchronized 关键字的身影

 Future 设计模式

Future 模式有点类似于商品订单。比如在网购时,当看重某一件商品事,就可以提交 订单,当订单处理完成后,在家里等待商品送货上门即可。或者说更形象的我们发送 Ajax 请求的时候,页面是异步的进行后台处理,用户无须一直等待请求的结果,可以继续浏览或 操作其他内容。

Master-Worker 设计模式 

Master- Worker 模式是常用的并行计算模式。它的核心思想是系统由两类进程协作工 作: Master 进程和 Worker 进程。 Master 负责接收和分配任务,Worker 负责处理子任 务。当各个 Worker-子进程处理完成后,会将结果返回给 Master,由 Master 做归纳和总 结。其好处是能将一个大任务分解成若干个小任务,并行执行,从而提高系统的吞吐量。

 具体代码实现逻辑图如下:

 生产者消费者设计模式

生产者和消费者也是一个非常经典的多线程模式,我们在实际开发中应用非常广泛的思 想理念。在生产消费模式中:通常由两类线程,即若干个生产者的线程和若干个消费者的线程。生产者线程负责提交用户请求,消费者线程则负责具体处理生产者提交的任务,在生产者和消费者之间通过共享内存缓存区进行通信。

 具体代码逻辑实现思路:

 Immutable 不可变对象设计模式

不可变对象一定是线程安全的。

关于时间日期 API 线程不安全的问题

想必大家对 SimpleDateFormat 并不陌生。SimpleDateFormat 是 Java 中一个非常 常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微 妙和难以调试的问题,因为 DateFormat 和 SimpleDateFormat 类不都是线程安全的, 在多线程环境下调用 format() 和 parse() 方法应该使用同步代码来避免问题。关于时间日期 API 的线程不安全问题直到 JDK8 出现以后才得到解决。

 关于线程不安全的代码示例如下:

package com.tong.chapter18.demo01;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;
public class SimpleDateFormatThreadUnsafe {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
      // 初始化时间日期 API
      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
      // 创建任务线程,执行任务将字符串转成指定格式日期
      Callable<Date> task = () -> sdf.parse("20200808");
      // 创建线程池,数量为 10
      ExecutorService pool = Executors.newFixedThreadPool(10);
      // 构建结果集
      List<Future<Date>> results = new ArrayList<>();
      // 开始执行任务线程,将结果添加至结果集
      for (int i = 0; i < 10; i++) {
             results.add(pool.submit(task));
       }
      // 打印结果集中的内容
      // 在任务线程执行过程中并且访问结果集内容就会报错
      for (Future<Date> future : results) {
             System.out.println(future.get());
     }
      // 关闭线程池
        pool.shutdown();
    }
}

运行结果如下:

 我们先自己来解决一下这个问题,线程不安全,我给它放到 ThreadLocal 中是否可行呢?

package com.tong.chapter18.demo01;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 将每次需要格式转换的参数都放入 ThreadLocal 中进行
*/
public class DateFormatThreadLocal {
      private static final ThreadLocal<DateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));
      public static Date convert(String source) throws ParseException {
         return df.get().parse(source);
    }
}

然后格式化日期代码如下:

package com.tong.chapter18.demo01;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;
public class SimpleDateFormatThreadSafe {
     public static void main(String[] args) throws ExecutionException,InterruptedException {
        // 初始化时间日期 API
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
        // 创建任务线程,执行任务将字符串转成指定格式日期
        //Callable<Date> task = () -> sdf.parse("20191020");
        // 使用 ThreadLocal 处理非线程安全
        Callable<Date> task = () -> DateFormatThreadLocal.convert("20191020");
        // 创建线程池,数量为 10
         ExecutorService pool = Executors.newFixedThreadPool(10);
        // 构建结果集
         List<Future<Date>> results = new ArrayList<>();
        // 开始执行任务线程,将结果添加至结果集
         for (int i = 0; i < 10; i++) {
               results.add(pool.submit(task));
          }
        // 打印结果集中的内容
        // 在任务线程执行过程中并且访问结果集内容就会报错
         for (Future<Date> future : results) {
               System.out.println(future.get());
          }
         // 关闭线程池
         pool.shutdown();
    }
}

上面的程序不管运行多少次都不会再出现线程不安全的问题。

定义不可变对象的策略

如何定义不可变对象呢?官方文档描述如下:

 参考官网文档后设计一个不可变对象,如下:

package com.tong.chapter18.demo02;
public final class Person {
       private final String name;
       private final String address;

       public Person(final String name, final String address) {
            this.name = name;
            this.address = address;
        }

       public String getName() {
             return name;
        }

       public String getAddress() 
            return address;
        }
@Override
public String toString() {
       return "Person{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}';
  }
}

更多推荐

Scala编程语言

Scala编程语言一、Scala引入1、学习Scala的目的2、Scala的基本概念二、Scala环境搭建1、安装步骤2、配置环境变量3、测试Scala4、Scala与idea的集成5、关联源码6、class和object说明三、常用语法、变量和数据类型1、注释2、变量和常量3、标识符的命名规范4、字符串输出5、键盘输

Oracle 游标&子程序&触发器

文章目录一、游标1.隐式游标2.显示游标3.REF游标二、子程序1.存储过程1.1语法结构1.2案例讲解2.存储函数2.1语法结构2.2案例讲解3.程序包三、触发器1.触发器的基本讲解2.触发器的类型2.1语句级触发器2.2行级触发器2.3限制行级触发器一、游标游标的作用:处理多行数据,类似与java中的集合1.隐式游

Node.js(初学者)

🎬岸边的风:个人主页🔥个人专栏:《VUE》《javaScript》⛺️生活的理想,就是为了理想的生活!目录必备条件在VisualStudioCode中试用NodeJS使用Express创建自己的第一个NodeJSWeb应用尝试使用Node.js模块必备条件在Windows或适用于Linux的Windows子系统上安

FreeSWITCH 1.10.10 简单图形化界面8 - 讯时FXO网关SIP注册公网IPPBX落地

FreeSWITCH1.10.10简单图形化界面8-讯时FXO网关SIP注册公网IPPBX落地0、界面预览1、创建一个话务台2、创建PBX分机中继并设置呼入权限3、设置呼出规则4、设置分机呼出权限5、设置FXO网关相关信息6、设置FXO网关中继线路呼入号码7、设置FXO网关呼叫路由(呼入及呼出)8、查看SIP中继状态F

室内探索无人机,解决复杂环境下的任务挑战!

前言室内探索无人机是一种专为在室内环境中进行任务的无人机系统。相比传统的人员部署,室内探索无人机具有更高的灵活性和机动性,能够在复杂的室内环境中执行任务,用于未知环境的探索和特定目标的搜索。为完成无人机室内搜索与识别等复杂任务,阿木实验室推出了一套全新的室内无人机探索系统。该系统集成了自主定位、视觉SLAM模块、路径规

使用 Sahi 实现 Web 自动化测试

目录Web测试背景Sahi的特性和优势:基于上下文的页面识别机制:隐式页面加载响应等待机制:Sahi的工作原理:第一步:录制第二步:精炼脚本第三步:回放Sahi是TytoSoftware旗下的一个基于业务的开源Web应用自动化测试工具Sahi运行为一个代理服务器,并通过注入JavaScript来访问Web页面中的元素。

MongoDB 2023年度纽约 MongoDB 年度大会话题 -- MongoDB 数据模式与建模

开头还是介绍一下群,如果感兴趣PolarDB,MongoDB,MySQL,PostgreSQL,Redis,Oceanbase,等有问题,有需求都可以加群群内有各大数据库行业大咖,CTO,可以解决你的问题。加群请联系liuaustin3,在新加的朋友会分到2群(共1300人左右1+2+3+4)3群即将突破400(目前3

电力系统直流潮流分析【N-1】(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。⛳️座右铭:行百里者,半于九十。📋📋📋本文目录如下:🎁🎁🎁目录💥1概述📚2运行结果🎉3参考文献🌈4Matlab代码及文档讲解💥1概述该程序接受一个感受矩阵B=[NxN]和注入功

day28IO流(字节流&字符流)

1.IO概述1.1什么是IO生活中,你肯定经历过这样的场景。当你编辑一个文本文件,忘记了ctrl+s,可能文件就白白编辑了。当你电脑上插入一个U盘,可以把一个视频,拷贝到你的电脑硬盘里。那么数据都是在哪些设备上的呢?键盘、内存、硬盘、外接设备等等。我们把这种数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为

Pikachu XSS(跨站脚本攻击)

文章目录Cross-SiteScriptingXSS(跨站脚本)概述反射型[xss](https://so.csdn.net/so/search?q=xss&spm=1001.2101.3001.7020)(get)反射型xss(post)存储型xssDOM型xssDOM型xss-xxss-盲打xss-过滤xss之ht

Flutter 中的单元测试:从工作流基础到复杂场景

对Flutter的兴趣空前高涨——而且早就应该出现了。Google的开源SDK与Android、iOS、macOS、Web、Windows和Linux兼容。单个Flutter代码库支持所有这些。单元测试有助于交付一致且可靠的Flutter应用程序,通过在组装之前先发制人地提高代码质量来确保不会出现错误、缺陷和缺陷。在本

热文推荐