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.pswp.cn/news/165641.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/165641.shtml
英文地址,請注明出處:http://en.pswp.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是一款輕型的…