并发编程的12条规范

1. 获取单例对象需要保证线程安全

我们在获取单例对象的时候,要确保线性安全哈。

比如双重检查锁定(Double-Checked Locking)的单例模式,就是一个经典案例,你在获取单实例对象的时候,就需要保证线性安全,比如加synchronized确保现象安全,代码如下:

public class Singleton {private volatile static Singleton instance;private Singleton() { }public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

大家在写资源驱动类、工具类、单例工厂类的时候,都需要注意获取单例对象需要保证线程安全。

2. 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

使用线程池时,如果没有给线程池一个有意义的名称,将不好排查回溯问题。

反例

public class TianLuoBoyThreadTest {public static void main(String[] args) throws Exception {ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20));executorOne.execute(()->{System.out.println("谢谢谢谢谢谢");throw new NullPointerException();});}
}

运行结果:

Exception in thread "pool-1-thread-1" java.lang.NullPointerExceptionat com.example.dto.TianLuoBoyThreadTest.lambda$main$0(ThreadTest.java:17)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)at java.lang.Thread.run(Thread.java:748)

可以发现,默认打印的线程池名字是pool-1-thread-1,如果排查问题起来,并不友好。因此建议大家给自己线程池自定义个容易识别的名字。其实用CustomizableThreadFactory即可,正例如下

public class ThreadTest {public static void main(String[] args) throws Exception {ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20),new CustomizableThreadFactory("TianluoBoy-Thread-pool"));executorOne.execute(()->{System.out.println("谢谢谢谢谢谢");throw new NullPointerException();});}
}

3. 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

日常开发中,我们经常需要使用到多线程。线程资源要求通过线程池提供,而不允许显式创建线程

因为如果显示创建线程,可能造成系统创建大量同类线程而导致消耗完内存。使用线程池主要有这些好处:

  • 帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。

  • 提高响应速度:如果任务到达了,相对于从线程池拿线程,重新去创建一条新线程执行,速度肯定慢很多。

  • 重复利用:线程用完,再放回池子,可以达到重复利用的效果,节省资源。

反例(显式创建线程):

public class DirectThreadCreation {public static void main(String[] args) {for (int i = 0; i < 10; i++) {Thread thread = new Thread(new WorkerThread("Task " + i));thread.start();}}
}class WorkerThread implements Runnable {private String taskName;public WorkerThread(String taskName) {this.taskName = taskName;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " executing " + taskName);// 执行任务的具体逻辑}
}

正例(线程池):

public class ThreadPoolExample {public static void main(String[] args) {// 创建固定大小的线程池ExecutorService executor = Executors.newFixedThreadPool(5);// 提交任务给线程池执行for (int i = 0; i < 10; i++) {Runnable task = new WorkerThread("Task " + i);executor.execute(task);}// 关闭线程池executor.shutdown();}
}class WorkerThread implements Runnable {private String taskName;public WorkerThread(String taskName) {this.taskName = taskName;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " executing " + taskName);// 执行任务的具体逻辑}
}

4. SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为static,必须加锁

SimpleDateFormat 是线程不安全的类,因为它内部维护了一个 Calendar 实例,而 Calendar 不是线程安全的。因此,在多线程环境下,如果多个线程共享一个 SimpleDateFormat 实例,可能会导致并发问题。

如果需要在多线程环境下使用SimpleDateFormat,可以通过加锁的方式来确保线程安全。

public class SafeDateFormatExample {private static final Object lock = new Object();private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static void main(String[] args) {Runnable task = () -> {try {parseAndPrintDate("2022-01-01 12:30:45");} catch (ParseException e) {e.printStackTrace();}};// 启动多个线程来同时解析日期for (int i = 0; i < 5; i++) {new Thread(task).start();}}private static void parseAndPrintDate(String dateString) throws ParseException {synchronized (lock) {Date date = sdf.parse(dateString);System.out.println(Thread.currentThread().getName() + ": Parsed date: " + date);}}
}

5. 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式

这是因为Executors 返回的线程池:

  • FixedThreadPool 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM

  • CachedThreadPool :允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

反例:

public class NewFixedTest {public static void main(String[] args) {ExecutorService executor = Executors.newFixedThreadPool(10);for (int i = 0; i < Integer.MAX_VALUE; i++) {executor.execute(() -> {try {Thread.sleep(10000);} catch (InterruptedException e) {//do nothing}});}}
}

使用 Executors的newFixedThreadPool创建的线程池,是会有坑的,它默认是无界的阻塞队列,如果任务过多,会导致OOM问题。运行一下以上代码,出现了OOM。

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceededat java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)at com.example.dto.NewFixedTest.main(NewFixedTest.java:14)

这是因为ExecutorsnewFixedThreadPool使用了无界的阻塞队列的LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo代码设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终出现OOM。

ThreadPoolExecutor 创建的时候,需要明确配置线程池参数,可以避免资源耗尽风险。

6. 高并发的时候,同步调用要考虑锁的粒度。

高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

通俗易懂讲就是,在保证数据安全的情况下,尽可能使加锁的代码块工作量尽可能的小。因为在高并发场景,为了防止超卖等情况,我们经常需要加锁来保护共享资源。但是,如果加锁的粒度过粗,是很影响接口性能的。 再比如,我们不推荐在加锁的代码块中,再调用RPC 方法。

对于锁的粒度,我给大家个代码例子哈:

比如,在业务代码中,有一个ArrayList因为涉及到多线程操作,所以需要加锁操作,假设刚好又有一段比较耗时的操作(代码中的slowNotShare方法)不涉及线程安全问题。反例加锁,就是一锅端,全锁住:

//不涉及共享资源的慢方法
private void slowNotShare() {try {TimeUnit.MILLISECONDS.sleep(100);} catch (InterruptedException e) {}
}//错误的加锁方法
public int wrong() {long beginTime = System.currentTimeMillis();IntStream.rangeClosed(1, 10000).parallel().forEach(i -> {//加锁粒度太粗了,slowNotShare其实不涉及共享资源synchronized (this) {slowNotShare();data.add(i);}});log.info("cosume time:{}", System.currentTimeMillis() - beginTime);return data.size();
}

正例:

public int right() {long beginTime = System.currentTimeMillis();IntStream.rangeClosed(1, 10000).parallel().forEach(i -> {slowNotShare();//可以不加锁//只对List这部分加锁synchronized (data) {data.add(i);}});log.info("cosume time:{}", System.currentTimeMillis() - beginTime);return data.size();
}

7. HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升。

HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升。在开发过程中可以使用其它数据结构或加锁来规避此风险。

在普通的 HashMap 中,可能出现死锁的场景通常与多线程并发修改 HashMap 的结构有关。这种情况下,多个线程同时对 HashMap 进行插入、删除等操作,可能导致链表形成环,进而导致死锁。

比如这个例子,演示了多线程同时对 HashMap 进行修改可能导致死锁的情况:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;public class HashMapDeadlockExample {public static void main(String[] args) throws InterruptedException {final Map<String, String> hashMap = new HashMap<>();final CountDownLatch latch = new CountDownLatch(2);// 线程1向HashMap中插入元素Thread thread1 = new Thread(() -> {for (int i = 0; i < 100000; i++) {hashMap.put(String.valueOf(i), String.valueOf(i));}latch.countDown();});// 线程2删除HashMap中的元素Thread thread2 = new Thread(() -> {for (int i = 0; i < 100000; i++) {hashMap.remove(String.valueOf(i));}latch.countDown();});thread1.start();thread2.start();// 等待两个线程执行完成latch.await();// 打印HashMap的大小System.out.println("HashMap size: " + hashMap.size());}
}

解决或规避这个问题的方式可以使用使用ConcurrentHashMap ConcurrentHashMap 是 HashMap 的线程安全版本,它使用了分段锁(Segment)来提高并发性能,减小锁的粒度,降低了并发冲突的可能性。

8.使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown方法。

使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。

CountDownLatch 是一个多线程同步工具,它的作用是允许一个或多个线程等待其他线程完成操作。在这里,你想要使用 CountDownLatch 实现异步转同步操作,确保每个线程退出前都调用countDown方法。给个代码示例,演示了如何使用 CountDownLatch 实现这种同步:

import java.util.concurrent.CountDownLatch;public class AsyncToSyncExample {public static void main(String[] args) throws InterruptedException {int numThreads = 3; // 假设有3个线程// 创建一个 CountDownLatch,计数器初始化为线程数量CountDownLatch latch = new CountDownLatch(numThreads);// 启动多个线程for (int i = 0; i < numThreads; i++) {Thread thread = new Thread(() -> {try {// 线程执行的业务逻辑doSomeWork();} catch (Exception e) {e.printStackTrace();} finally {// 无论如何,都需要调用 countDown 方法latch.countDown();}});thread.start();}// 等待所有线程完成,最多等待5秒(超时时间可以根据实际情况调整)if (!latch.await(5000, java.util.concurrent.TimeUnit.MILLISECONDS)) {// 超时处理逻辑System.out.println("Timeout while waiting for threads to finish.");} else {// 所有线程执行完成后的逻辑System.out.println("All threads have finished their work.");}}private static void doSomeWork() {// 模拟线程执行的业务逻辑try {Thread.sleep(2000);System.out.println(Thread.currentThread().getName() + " has finished its work.");} catch (InterruptedException e) {e.printStackTrace();}}
}

9. 多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行。

在 Timer 运行多个 TimerTask 时,如果其中一个 TimerTask 抛出了未捕获的异常,将导致整个 Timer 终止,而未抛出异常的任务也将停止执行。这是因为 Timer 的设计导致一个任务的异常会影响到整个 Timer 的执行。代码如下:

import java.util.Timer;
import java.util.TimerTask;public class TimerTaskExample {public static void main(String[] args) {Timer timer = new Timer();// 任务1,抛出异常TimerTask task1 = new TimerTask() {@Overridepublic void run() {System.out.println("Task 1 is running...");throw new RuntimeException("Exception in Task 1");}};// 任务2TimerTask task2 = new TimerTask() {@Overridepublic void run() {System.out.println("Task 2 is running...");}};// 安排任务1和任务2执行timer.schedule(task1, 0, 1000);timer.schedule(task2, 0, 1000);}
}

使用 ScheduledExecutorService 则没有这个问题:

public class ScheduledExecutorExample {public static void main(String[] args) {ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);// 任务1,每隔2秒执行一次,可能抛出异常scheduler.scheduleAtFixedRate(() -> {try {System.out.println("Task 1 is running...");throw new RuntimeException("Exception in Task 1");} catch (Exception e) {e.printStackTrace();}}, 0, 2, TimeUnit.SECONDS);// 任务2,每隔3秒执行一次scheduler.scheduleAtFixedRate(() -> {try {System.out.println("Task 2 is running...");} catch (Exception e) {e.printStackTrace();}}, 0, 3, TimeUnit.SECONDS);}
}

10. 避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed 导致的性能下降。

虽然 Random实例的方法是线程安全的,但是当多个线程共享相同的Random 实例并竞争相同的 seed 时,可能会因为竞争而导致性能下降。这是因为 Random 使用一个原子变量来维护其内部状态,当多个线程同时调用 nextInt 等方法时,可能会发生竞争,从而影响性能。

大家可以看下这个例子哈:

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class SharedRandomPerformanceExample {public static void main(String[] args) throws InterruptedException {int numThreads = 10;int iterations = 1000000;// 共享一个 Random 实例Random sharedRandom = new Random();// 使用多线程执行任务ExecutorService executorService = Executors.newFixedThreadPool(numThreads);for (int i = 0; i < numThreads; i++) {executorService.execute(() -> {for (int j = 0; j < iterations; j++) {int randomNumber = sharedRandom.nextInt();// 模拟使用随机数的业务逻辑}});}executorService.shutdown();executorService.awaitTermination(1, TimeUnit.MINUTES);```}
}

在这个例子中,多个线程共享相同的 Random 实例 sharedRandom,并且在循环中调用 nextInt方法。由于 Random 内部使用CAS操作来维护其状态,多个线程可能会竞争同一 seed导致性能下降。

如果你希望避免这种竞争,可以考虑为每个线程创建独立的 Random 实例,以确保每个线程都有自己的状态。在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程持有一个实例。

11.并发修改同一记录时,避免更新丢失,需要加锁。

并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version作为更新依据。

如果每次访问冲突概率小于20%,推荐使用乐观锁,因为证明并发不是很高。否则使用悲观锁。乐观锁的重试次数不得小于3 次。

12. 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。

线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。在多线程环境中,当需要对多个资源、数据库表或对象同时加锁时,为了避免死锁,所有线程必须保持一致的加锁顺序。这就是所谓的"锁顺序规范"。

大家有兴趣可以看下这个例子哈,两个线程按照相同的顺序加锁以避免死锁:

public class DeadlockExample {private static final Object lockA = new Object();private static final Object lockB = new Object();public static void main(String[] args) {Thread thread1 = new Thread(() -> {synchronized (lockA) {System.out.println("Thread 1 acquired lockA");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB) {System.out.println("Thread 1 acquired lockB");}}});Thread thread2 = new Thread(() -> {// 保持一致的加锁顺序,先尝试获取 lockA,再获取 lockBsynchronized (lockA) {System.out.println("Thread 2 acquired lockA");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB) {System.out.println("Thread 2 acquired lockB");}}});thread1.start();thread2.start();}
}

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

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

相关文章

bee工具的使用及创建第一个项目

前提文章&#xff1a;beego的安装及配置参数说明-CSDN博客 提示&#xff1a;beego框架下项目需要再GOPATH/src下进行开发&#xff0c;我的GOPATH是C:\Users\leell\go web项目创建 通过 bee new 创建web项目 C:\Users\leell\go\src>bee new beego-web 2024/01/15 21:40:0…

详细讲解Python连接Mysql的基本操作

目录 前言1. mysql.connector2. pymysql 前言 连接Mysql一般有几种方法&#xff0c;主要讲解mysql.connector以及pymysql的连接 后续如果用到其他库还会持续总结&#xff01; 对于数据库中的表格,本人设计如下:(为了配合下面的操作) 1. mysql.connector mysql.connector 是一…

自动化的自动化(1)--OPCUA2HTML5

现在的自动化工程师是令人沮丧的&#xff0c;他们努力地实现各个行业的自动化系统&#xff0c;自己却停留在敲键盘的手工劳作的阶段&#xff0c;该解放自己了。这就是“自动化实现自动化”的话题。 OPC 统一架构&#xff08;简称 OPC UA&#xff09;是现代工厂自动化中用于机器…

漏洞复现-Yearning front 任意文件读取漏洞(附漏洞检测脚本)

免责声明 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直接或者间接的…

二级C语言备考7

一、单选 共40题 &#xff08;共计40分&#xff09; 第1题 &#xff08;1.0分&#xff09; 题号:7098 难度:中 第1章 下列叙述中正确的是 A:一个算法的空间复杂度大,则其时间复杂度也必定大 B:一个算法的空间复杂度大,则其时间复杂度必定小 C:一个…

半监督学习 - 自监督学习(Self-Supervised Learning)

什么是机器学习 自监督学习既不是纯粹的半监督学习&#xff0c;也不是纯粹的无监督学习&#xff0c;而是介于两者之间的一种学习范式。在自监督学习中&#xff0c;模型从数据本身中生成标签&#xff0c;而不是依赖外部的人工标签。这使得自监督学习可以利用未标签的大量数据进…

身体互联网 (IoB)

现在&#xff0c;我们的互联网网关就是我们手中的一个小设备。 普渡大学副教授施里亚斯森表示。 我们不断地看着这个盒子&#xff0c;我们低着头走路&#xff0c;我们把大部分时间都花在它上面。如果我们不想让这种未来继续下去&#xff0c;我们就需要开发新技术。相反&#x…

#RAG##AIGC#检索增强生成 (RAG) 基本介绍和入门实操示例

本文包括RAG基本介绍和入门实操示例 RAG 基本介绍 通用语言模型可以进行微调以实现一些常见任务&#xff0c;例如情感分析和命名实体识别。这些任务通常不需要额外的背景知识。 对于更复杂和知识密集型的任务&#xff0c;可以构建基于语言模型的系统来访问外部知识源来完成任…

系统架构11 - 数据库基础(上)

数据库基础 数据库基本概念概述三级模式、两级映像概念模式外模式内模式二级映像逻辑独立性物理独立性 数据库设计需求分析概念结构设计逻辑结构设计物理设计数据库实施阶段据库运行和维护阶段 数据模型E-R模型关系模型模型转换E-R图的联系 关系代数 数据库基本概念 概述 数据…

openpyxl绘制图表

嘿&#xff0c;你是不是在处理Excel文件时感到束手无策&#xff1f;是不是想要一个简单而又强大的工具来处理数据分析和图表制作&#xff1f;别担心&#xff0c;我们有解决方案&#xff01;让我向你介绍openpyxl&#xff0c;这是一个Python库&#xff0c;专门用于处理Excel文件…

使用MDT将系统升级为Win10

创建升级到Win10任务序列 右键选择Task Sequence &#xff0c;然后选择NEW Task Sequence 填写ID name 默认配置 选择企业版 默认配置 填写组织 输入Administrator密码 点击下一步 等待完成 我们提前安装一台WIN7&#xff0c;并激活 选择打开 选择是 选择我们要创建好的升级win…

Vue+Koa2 搭建前后端分离项目

VueKoa2 搭建前后端分离项目 简单的一个 Demo 演示: Vue 和 Koa2 在本地搭建前后端分离项目. 重点: 跨域 当前环境: 1, Vite 搭建的 Vue 前端项目 ( 也就是 Vue 3 了 ) . 2, Koa2 搭建的 后端项目. 前端项目在 localhost: 5173 端口下运行, 后端项目在 localhost: 3000 端口…

2.3 数据链路层03

2.3 数据链路层03 2.3.7 以太网交换机 1、以太网交换机的基本功能 以太网交换机是基于以太网传输数据的交换机&#xff0c;以太网交换机通常都有多个接口&#xff0c;每个接口都可以直接与一台主机或另一个以太网交换机相连&#xff0c;一般都工作在全双工方式。 以太网交换…

C程序技能:彩色输出

在终端上输出的字体总是单色&#xff0c;但在一些场景彩色输出更能满足需求&#xff0c;在Linux环境中&#xff0c;可以使用终端控制字符来设置输出字符的颜色&#xff0c;在部分版本的Windows系统中也可以使用。本文参考一些文献简要介绍一下在Windows下彩色输出的方法。 1. …

Rust-借用和生命周期

生命周期 一个变量的生命周期就是它从创建到销毁的整个过程。其实我们在前面已经注意到了这样的现象&#xff1a; 然而&#xff0c;如果一个变量永远只能有唯一一个入口可以访问的话&#xff0c;那就太难使用了。因此&#xff0c;所有权还可以借用。 借用 变量对其管理的内存…

贪心算法part03算法

贪心算法part03 ● 1005.K次取反后最大化的数组和 ● 134. 加油站 ● 135. 分发糖果 1.leetcode 1005.K次取反后最大化的数组和 https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/description/ class Solution {public int largestSumAfterKNegation…

从零开始:生产环境如何部署 Bytebase

Bytebase 是面向研发和 DBA 的数据库 DevOps 和 CI/CD 协同平台。目前 Bytebase 在全球类似开源项目中 GitHub Star 数排名第一且增长最快。 Bytebase 的架构 Bytebase 是一个单体架构 (monolith)&#xff0c;前端是 Vue3 TypeScript&#xff0c;后端是 Go。前端利用 Go 1.6 …

winform-TreeView的添加节点展开所有节点

文章速览 1、添加节点核心代码示例 2、展开节点核心代码示例注意 坚持记录实属不易&#xff0c;希望友善多金的码友能够随手点一个赞。 共同创建氛围更加良好的开发者社区&#xff01; 谢谢~ 1、添加节点 核心代码 TreeView.Nodes.Add()示例 foreach (var item in content){…

MySQL面试题 | 09.精选MySQL面试题

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

linux下485通信调试记录

1、使用linux下使用串口调试助手 linux下可以安装并使用下述串口调试工具进行串口测试&#xff1a; 1.1、cutecom cutecom是linux下常用的图形化串口调试软件&#xff0c;通过以下命令安装并打开&#xff1a; sudo apt-get install cutecom sudo cutecom显示如下&#xff1…