27 - 如何使用设计模式优化并发编程?

在我们使用多线程编程时,很多时候需要根据业务场景设计一套业务功能。其实,在多线程编程中,本身就存在很多成熟的功能设计模式,学好它们,用好它们,那就是如虎添翼了。今天我就带你了解几种并发编程中常用的设计模式。

1、线程上下文设计模式

线程上下文是指贯穿线程整个生命周期的对象中的一些全局信息。例如,我们比较熟悉的 Spring 中的 ApplicationContext 就是一个关于上下文的类,它在整个系统的生命周期中保存了配置信息、用户信息以及注册的 bean 等上下文信息。

这样的解释可能有点抽象,我们不妨通过一个具体的案例,来看看到底在什么的场景下才需要上下文呢?

在执行一个比较长的请求任务时,这个请求可能会经历很多层的方法调用,假设我们需要将最开始的方法的中间结果传递到末尾的方法中进行计算,一个简单的实现方式就是在每个函数中新增这个中间结果的参数,依次传递下去。代码如下:

public class ContextTest {// 上下文类public class Context {private String name;private long idpublic long getId() {return id;}public void setId(long id) {this.id = id;}public String getName() {return this.name;}public void setName(String name) {this.name = name;}}// 设置上下文名字public class QueryNameAction {public void execute(Context context) {try {Thread.sleep(1000L);String name = Thread.currentThread().getName();context.setName(name);} catch (InterruptedException e) {e.printStackTrace();}}}// 设置上下文 IDpublic class QueryIdAction {public void execute(Context context) {try {Thread.sleep(1000L);long id = Thread.currentThread().getId();context.setId(id);} catch (InterruptedException e) {e.printStackTrace();}}}// 执行方法public class ExecutionTask implements Runnable {private QueryNameAction queryNameAction = new QueryNameAction();private QueryIdAction queryIdAction = new QueryIdAction();@Overridepublic void run() {final Context context = new Context();queryNameAction.execute(context);System.out.println("The name query successful");queryIdAction.execute(context);System.out.println("The id query successful");System.out.println("The Name is " + context.getName() + " and id " + context.getId());}}public static void main(String[] args) {IntStream.range(1, 5).forEach(i -> new Thread(new ContextTest().new ExecutionTask()).start());}
}

执行结果:

The name query successful
The name query successful
The name query successful
The name query successful
The id query successful
The id query successful
The id query successful
The id query successful
The Name is Thread-1 and id 11
The Name is Thread-2 and id 12
The Name is Thread-3 and id 13
The Name is Thread-0 and id 10

然而这种方式太笨拙了,每次调用方法时,都需要传入 Context 作为参数,而且影响一些中间公共方法的封装。

那能不能设置一个全局变量呢?如果是在多线程情况下,需要考虑线程安全,这样的话就又涉及到了锁竞争。

除了以上这些方法,其实我们还可以使用 ThreadLocal 实现上下文。ThreadLocal 是线程本地变量,可以实现多线程的数据隔离。ThreadLocal 为每一个使用该变量的线程都提供一份独立的副本,线程间的数据是隔离的,每一个线程只能访问各自内部的副本变量。

ThreadLocal 中有三个常用的方法:set、get、initialValue,我们可以通过以下一个简单的例子来看看 ThreadLocal 的使用:

private void testThreadLocal() {Thread t = new Thread() {ThreadLocal<String> mStringThreadLocal = new ThreadLocal<String>();@Overridepublic void run() {super.run();mStringThreadLocal.set("test");mStringThreadLocal.get();}};t.start();
}

接下来,我们使用 ThreadLocal 来重新实现最开始的上下文设计。你会发现,我们在两个方法中并没有通过变量来传递上下文,只是通过 ThreadLocal 获取了当前线程的上下文信息:

public class ContextTest {// 上下文类public static class Context {private String name;private long id;public long getId() {return id;}public void setId(long id) {this.id = id;}public String getName() {return this.name;}public void setName(String name) {this.name = name;}}// 复制上下文到 ThreadLocal 中public final static class ActionContext {private static final ThreadLocal<Context> threadLocal = new ThreadLocal<Context>() {@Overrideprotected Context initialValue() {return new Context();}};public static ActionContext getActionContext() {return ContextHolder.actionContext;}public Context getContext() {return threadLocal.get();}// 获取 ActionContext 单例public static class ContextHolder {private final static ActionContext actionContext = new ActionContext();}}// 设置上下文名字public class QueryNameAction {public void execute() {try {Thread.sleep(1000L);String name = Thread.currentThread().getName();ActionContext.getActionContext().getContext().setName(name);} catch (InterruptedException e) {e.printStackTrace();}}}// 设置上下文 IDpublic class QueryIdAction {public void execute() {try {Thread.sleep(1000L);long id = Thread.currentThread().getId();ActionContext.getActionContext().getContext().setId(id);} catch (InterruptedException e) {e.printStackTrace();}}}// 执行方法public class ExecutionTask implements Runnable {private QueryNameAction queryNameAction = new QueryNameAction();private QueryIdAction queryIdAction = new QueryIdAction();@Overridepublic void run() {queryNameAction.execute();// 设置线程名System.out.println("The name query successful");queryIdAction.execute();// 设置线程 IDSystem.out.println("The id query successful");System.out.println("The Name is " + ActionContext.getActionContext().getContext().getName() + " and id " + ActionContext.getActionContext().getContext().getId())}}public static void main(String[] args) {IntStream.range(1, 5).forEach(i -> new Thread(new ContextTest().new ExecutionTask()).start());}
}

运行结果:

The name query successful
The name query successful
The name query successful
The name query successful
The id query successful
The id query successful
The id query successful
The id query successful
The Name is Thread-2 and id 12
The Name is Thread-0 and id 10
The Name is Thread-1 and id 11
The Name is Thread-3 and id 13

2、Thread-Per-Message 设计模式

Thread-Per-Message 设计模式翻译过来的意思就是每个消息一个线程的意思。例如,我们在处理 Socket 通信的时候,通常是一个线程处理事件监听以及 I/O 读写,如果 I/O 读写操作非常耗时,这个时候便会影响到事件监听处理事件。

这个时候 Thread-Per-Message 模式就可以很好地解决这个问题,一个线程监听 I/O 事件,每当监听到一个 I/O 事件,则交给另一个处理线程执行 I/O 操作。下面,我们还是通过一个例子来学习下该设计模式的实现。

//IO 处理
public class ServerHandler implements Runnable{private Socket socket;public ServerHandler(Socket socket) {this.socket = socket;}public void run() {BufferedReader in = null;PrintWriter out = null;String msg = null;try {in = new BufferedReader(new InputStreamReader(socket.getInputStream()));out = new PrintWriter(socket.getOutputStream(),true);while ((msg = in.readLine()) != null && msg.length()!=0) {// 当连接成功后在此等待接收消息(挂起,进入阻塞状态)System.out.println("server received : " + msg);out.print("received~\n");out.flush();}} catch (Exception e) {e.printStackTrace();} finally {try {in.close();} catch (IOException e) {e.printStackTrace();}try {out.close();} catch (Exception e) {e.printStackTrace();}try {socket.close();} catch (IOException e) {e.printStackTrace();}}}
}
//Socket 启动服务
public class Server {private static int DEFAULT_PORT = 12345;private static ServerSocket server;public static void start() throws IOException {start(DEFAULT_PORT);}public static void start(int port) throws IOException {if (server != null) {return;}try {// 启动服务server = new ServerSocket(port);// 通过无线循环监听客户端连接while (true) {Socket socket = server.accept();// 当有新的客户端接入时,会执行下面的代码long start = System.currentTimeMillis();new Thread(new ServerHandler(socket)).start();long end = System.currentTimeMillis();System.out.println("Spend time is " + (end - start));}} finally {if (server != null) {System.out.println(" 服务器已关闭。");server.close();}}}public static void main(String[] args) throws InterruptedException{// 运行服务端new Thread(new Runnable() {public void run() {try {Server.start();} catch (IOException e) {e.printStackTrace();}}}).start();}
}

以上,我们是完成了一个使用 Thread-Per-Message 设计模式实现的 Socket 服务端的代码。但这里是有一个问题的,你发现了吗?

使用这种设计模式,如果遇到大的高并发,就会出现严重的性能问题。如果针对每个 I/O 请求都创建一个线程来处理,在有大量请求同时进来时,就会创建大量线程,而此时 JVM 有可能会因为无法处理这么多线程,而出现内存溢出的问题。

退一步讲,即使是不会有大量线程的场景,每次请求过来也都需要创建和销毁线程,这对系统来说,也是一笔不小的性能开销。

面对这种情况,我们可以使用线程池来代替线程的创建和销毁,这样就可以避免创建大量线程而带来的性能问题,是一种很好的调优方法。

3、Worker-Thread 设计模式

这里的 Worker 是工人的意思,代表在 Worker Thread 设计模式中,会有一些工人(线程)不断轮流处理过来的工作,当没有工作时,工人则会处于等待状态,直到有新的工作进来。除了工人角色,Worker Thread 设计模式中还包括了流水线和产品。

这种设计模式相比 Thread-Per-Message 设计模式,可以减少频繁创建、销毁线程所带来的性能开销,还有无限制地创建线程所带来的内存溢出风险。

我们可以假设一个场景来看下该模式的实现,通过 Worker Thread 设计模式来完成一个物流分拣的作业。

假设一个物流仓库的物流分拣流水线上有 8 个机器人,它们不断从流水线上获取包裹并对其进行包装,送其上车。当仓库中的商品被打包好后,会投放到物流分拣流水线上,而不是直接交给机器人,机器人会再从流水线中随机分拣包裹。代码如下:

// 包裹类
public class Package {private String name;private String address;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public void execute() {System.out.println(Thread.currentThread().getName()+" executed "+this);}
}
// 流水线
public class PackageChannel {private final static int MAX_PACKAGE_NUM = 100;private final Package[] packageQueue;private final Worker[] workerPool;private int head;private int tail;private int count;public PackageChannel(int workers) {this.packageQueue = new Package[MAX_PACKAGE_NUM];this.head = 0;this.tail = 0;this.count = 0;this.workerPool = new Worker[workers];this.init();}private void init() {for (int i = 0; i < workerPool.length; i++) {workerPool[i] = new Worker("Worker-" + i, this);}}/*** push switch to start all of worker to work*/public void startWorker() {Arrays.asList(workerPool).forEach(Worker::start);}public synchronized void put(Package packagereq) {while (count >= packageQueue.length) {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}this.packageQueue[tail] = packagereq;this.tail = (tail + 1) % packageQueue.length;this.count++;this.notifyAll();}public synchronized Package take() {while (count <= 0) {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}Package request = this.packageQueue[head];this.head = (this.head + 1) % this.packageQueue.length;this.count--;this.notifyAll();return request;}}
// 机器人
public class Worker extends Thread{private static final Random random = new Random(System.currentTimeMillis());private final PackageChannel channel;public Worker(String name, PackageChannel channel) {super(name);this.channel = channel;}@Overridepublic void run() {while (true) {channel.take().execute();try {Thread.sleep(random.nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();}}}}
public class Test {public static void main(String[] args) {// 新建 8 个工人final PackageChannel channel = new PackageChannel(8);// 开始工作channel.startWorker();// 为流水线添加包裹for(int i=0; i<100; i++) {Package packagereq = new Package();packagereq.setAddress("test");packagereq.setName("test");channel.put(packagereq);}}
}

我们可以看到,这里有 8 个工人在不断地分拣仓库中已经包装好的商品。

4、总结

平时,如果需要传递或隔离一些线程变量时,我们可以考虑使用上下文设计模式。在数据库读写分离的业务场景中,则经常会用到 ThreadLocal 实现动态切换数据源操作。但在使用 ThreadLocal 时,我们需要注意内存泄漏问题,在之前的[第 25 讲]中,我们已经讨论过这个问题了。

当主线程处理每次请求都非常耗时时,就可能出现阻塞问题,这时候我们可以考虑将主线程业务分工到新的业务线程中,从而提高系统的并行处理能力。而 Thread-Per-Message 设计模式以及 Worker-Thread 设计模式则都是通过多线程分工来提高系统并行处理能力的设计模式。

5、思考题

除了以上这些多线程的设计模式,平时你还使用过其它的设计模式来优化多线程业务吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/165641.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis-cluster集群(目的:高可用)

1、特点 集群由多个node节点组成&#xff0c;redis数据分布在这些节点中&#xff0c;在集群中分为主节点和从节点&#xff0c;一个主对应一个从&#xff0c;所有组的主从形成一个集群&#xff0c;每组的数据是独立的&#xff0c;并且集群自带哨兵模式 2、工作原理 集群模式中…

【ZedBoard学习实例1】 VGA显示彩条

ZedBoard学习实例1 VGA显示彩条 ZedBoard学习实例1 VGA显示彩条参考文章改进 ZedBoard学习实例1 VGA显示彩条 参考文章 彩条控制verilog代码 主体参考了该文章的代码&#xff0c;文中还介绍了相关的电路图&#xff0c;还有ZedBoard的手册内容。19201080分辨率显示器的参数 针…

重生之我是一名程序员 37 ——C语言中的栈溢出问题

哈喽啊大家晚上好&#xff01; 今天呢给大家带来一个烧脑的知识——C语言中的栈溢出问题。那什么是栈溢出呢&#xff1f;栈溢出指的是当程序在执行函数调用时&#xff0c;为了保护函数的局部变量和返回地址&#xff0c;将这些数据存储在栈中。如果函数在函数调用时使用了过多的…

Sentinel核心类解读:Entry

默认情况下&#xff0c;Sentinel会将controller中的方法作为被保护资源&#xff0c;Sentinel中的资源用Entry来表示。 Sentinel中Entry可以理解为每次进入资源的一个凭证&#xff0c;如果调用SphO.entry()或者SphU.entry()能获取Entry对象&#xff0c;代表获取了凭证&#xff…

安卓手机便签APP用哪个,手机上好用的便签APP是什么

在日常生活及工作方面&#xff0c;总是有许多做不完的事情需要大家来处理&#xff0c;当多项任务堆叠交叉在一起时&#xff0c;很容易漏掉一些项目&#xff0c;这时候大家会借助经常携带的手机来记录容易忘记的事情&#xff0c;如手机上的闹钟、定时提醒软件都可以用来记录待办…

2023亚太杯数学建模A题思路分析 - 采果机器人的图像识别技术

1 赛题 问题A 采果机器人的图像识别技术 中国是世界上最大的苹果生产国&#xff0c;年产量约为3500万吨。与此同时&#xff0c;中国也是世 界上最大的苹果出口国&#xff0c;全球每两个苹果中就有一个&#xff0c;全球超过六分之一的苹果出口 自中国。中国提出了一带一路倡议…

JDK11新特性

目录 一、JShell 二、Dynamic Class-File Constants类文件新添的一种结构 三、局部变量类型推断&#xff08;var ”关键字”&#xff09; 四、新加的一些实用API 1. 新的本机不可修改集合API 2. Stream 加强 3. String 加强 4. Optional 加强 5. 改进的文件API 五、移…

canvas

Canvas 是 Android 中用于绘制图形的重要类&#xff0c;它提供了许多用于绘制的常用方法。以下是一些常用的 Canvas 方法&#xff1a; 绘制颜色和背景&#xff1a; drawColor(int color): 用指定颜色填充整个画布。drawRGB(int r, int g, int b): 用 RGB 值指定颜色填充整个画布…

进程池,线程池与跨进程数据共享爬取某岸网图片

看教程的时候看到一个&#xff0c;生产者跟消费者的概念比较有意思&#xff0c;但是给的代码有问题无法正常运行&#xff0c;于是我就捣鼓了一下。 基本概念就是&#xff1a; 生产者&#xff1a; 一个进程获取网页没页的图片连接&#xff08;主进程…

Django框架之中间件

目录 一、引入 二、Django中间件介绍 【1】什么是Django中间件 【2】Django中间件的作用 【3】示例 三、Django请求生命周期流程图 四、Django中间件是Django的门户 五、Django中间件详解 六、中间件必须要掌握的两个方法 (1) process_request (2) process_respon…

Redis集群环境各节点无法互相发现与Hash槽分配异常 CLUSTERDOWN Hash slot not served的解决方式

原创/朱季谦 在搭建Redis5.x版本的集群环境曾出现各节点无法互相发现与Hash槽分配异常 CLUSTERDOWN Hash slot not served的情况&#xff0c;故而把解决方式记录下来。 在以下三台虚拟机机器搭建Redis集群—— 192.168.200.160192.168.200.161192.168.200.162启动三台Redis集…

芯知识 | MP3语音芯片IC的优势特征及其在现代科技应用中的价值

随着科技的飞速发展&#xff0c;MP3语音芯片作为一种高度集成的音频处理解决方案&#xff0c;在现代电子产品中发挥着越来越重要的作用。本文将分析MP3语音芯片的优势特征&#xff0c;并探讨其在各个领域的应用价值。 一、MP3语音芯片的优势特征 MP3语音芯片具有多种显著的优…

CC++输入输出流介绍

介绍 C中的输入输出流主要包括标准输入输出流、文件输入输出流和内存数据流。 标准输入输出流可以通过使用cin和cout进行数据的读取和输出文件输入输出流可以通过使用ifstream和ofstream对文件进行读写操作内存数据流可以通过使用stringstream对字符串进行读写操作 应用举例…

服务器租用收费标准是什么?

服务器在企业转型中或者是互联网企业中起着举足轻重的作用&#xff0c;服务器有强大的存储能力和计算能力&#xff0c;能够帮助企业存储大量信息&#xff0c;完成日常工作&#xff0c;服务器租用就是通过正规的IDC服务器商家那里获取服务器资源&#xff0c;根据企业自身需求选择…

Python爬虫-获取汽车之家新车优惠价

前言 本文是该专栏的第10篇,后面会持续分享python爬虫案例干货,记得关注。 本文以汽车之家新车优惠价为例,获取各车型的优惠价,示例图如下: 地址:aHR0cHM6Ly9idXkuYXV0b2hvbWUuY29tLmNuLzAvMC8wLzQyMDAwMC80MjAxMDAvMC0wLTAtMS5odG1sI3B2YXJlYWlkPTIxMTMxOTU= 需求:获…

OpenStack云计算平台

目录 一、OpenStack 1、简介 2、硬件需求 3、网络 二、环境搭建 1、安全 2、主机网络 3、网络时间协议(NTP) 4、OpenStack包 5、SQL数据库 6、消息队列 7、Memcached 一、OpenStack 1、简介 官网&#xff1a;https://docs.openstack.org/2023.2/ OpenStack系统由…

Zynq-7000系列FPGA使用 Video Processing Subsystem 实现图像缩放,提供工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐FPGA图像处理方案FPGA图像缩放方案自己写的HLS图像缩放方案 3、设计思路详解Video Processing Subsystem 介绍 4、工程代码详解PL 端 FPGA 逻辑设计PS 端 SDK 软件设计 5、工程移植说明vivado版本不一致处理FPGA型号不一致处理其他注意事项…

给sprite上增加刷光动效

游戏引擎 —— cocos creator 3.52 此动效给动态修改尺寸的图片增加一层刷光的效果&#xff0c;直接贴代码 CCEffect %{techniques:- passes:- vert: sprite-vs:vertfrag: sprite-fs:fragdepthStencilState:depthTest: falsedepthWrite: falseblendState:targets:- blend: tr…

Charles 网络抓包工具详解与实战指南

文章目录 导读软件版本Charles基本原理核心功能下载及安装界面介绍网络包展示 常用场景介绍PC 端网络抓包移动端网络抓包PC 端配置手机端配置 开启 SSL 代理PC 端和移动端 CA 证书安装Charles 直接安装Charles 下载 CA 文件手动安装 常用操作请求重发请求改写、动态改写断点&am…

Qt+SQLITE数据库设计的会员卡管理系统

一、前言 本项目演示在QT中使用SQLITE数据库存储数据管理的过程。当前以会员卡管理系统为例,写了一个界面,完成会员卡的注册,添加,充值,查询,注销,导出顾客信息EXECL表格 等功能的实现。 演示 SQLITE数据库的建表、增、删、改、查等语句功能实现。 SQLite是一款轻型的…