读书笔记-Java并发编程的艺术-第4章(Java并发编程基础)-第3节(线程间通信)

文章目录

  • 4.3 线程间通信
    • 4.3.1 volatile和synchronized 关键字
    • 4.3.2 等待/通知机制
    • 4.3.3 等待/通知的经典范式
    • 4.3.4 管道输入 / 输出流
    • 4.3.5 Thread.join()的使用
    • 4.3.6 ThreadLocal的使用

4.3 线程间通信

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,这将会带来巨大的价值。

4.3.1 volatile和synchronized 关键字

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。

关键字 volatile 可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

举个例子,定义一个表示程序是否运行的成员变量boolean on=true,那么另一个线程可能对它执行关闭动作(on=false),这里涉及多个线程对变量的访问,因此需要将其定义成为volatile boolean on=true,这样其他线程对它进行改变时,可以让所有线程感知到变化,因为所有对on变量的访问和修改都需要以共享内存为准。但是,过多地使用 volatile是不必要的,因为它会降低程序执行的效率。

关键字sychronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可则性和排他性。

在下面代码中,使用了同步块和同步方法,通过使用javap工具查看生成的class文件信息来分析synchronized关键字的实现细节,示例如下:

public class Synchronized {public static void main(String[] args) {// 对Synchronized Class对象进行加锁synchronized (Synchronized.class) {}// 静态同步方法,对Synchronized Class对象进行加锁m();}public static synchronized void m() {}
}

打开cmd,在 Synchronized.class 所在目录执行javap -v Synchronized.class(即javap -verbose Synchronized.class),部分输出如下:

D:\project1\java8\java8\xin-javademo\target\classes\com\xin\demo\threaddemo\bookdemo>javap -v Synchronized.classpublic static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATIC  // 方法修饰符Code:stack=2, locals=3, args_size=10: ldc           #2                  // class com/xin/demo/threaddemo/bookdemo/Synchronized2: dup3: astore_14: monitorenter  // 监视器进入,获取锁5: aload_16: monitorexit   // 监视器退出,释放锁7: goto          1510: astore_211: aload_112: monitorexit13: aload_214: athrow15: invokestatic  #3                  // Method m:()V18: returnpublic static synchronized void m();descriptor: ()Vflags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED  // 方法修饰符Code:stack=0, locals=0, args_size=00: returnLineNumberTable:line 14: 0
}
SourceFile: "Synchronized.java"

上面class信息中,对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized 所保护对象的监视器。

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法、而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。

下图描述了对象、对象的监视器、同步队列和执行线程之间的关系。

在这里插入图片描述

从上图可以看到,任意线程对Object(Object由synchronized保护)的访问,首先获得Obiect的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程使其重新尝试对监视器的获取。

4.3.2 等待/通知机制

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”(what)和“怎么做”(How),在功能层面上实现了解耦,体系结构上具备了良好的伸缩性,但是在Java语言中如何实现类似的功能呢?

简单的办法是让消费者线程不断地循环检查变量是否符合预期,如下面代码所示,在while循环中设置不满足的条件,如果条件满足则退出while循环,从而完成消费者的工作。

while (value != desire) {
Thread.sleep(1000);
}
doSomething();

上面这段伪代码在条件不满足时就睡眠一段时间,这样做的目的是防止过快的“无效”尝试,这种方式看似能够解实现所需的功能,但是却存在如下问题。

  • 1. 难以确保及时性。在睡眠时,基本不消耗处理器资源,但是如果睡得过久,就不能及时发现条件已经变化,也就是及时性难以保证。
  • 2. 难以降低开销。如果降低睡眠的时间,比如休眠1毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。

以上两个问题,看似矛盾难以调和,但是Java通过内置的等待/通知机制能够很好地解决这个矛盾并实现所需的功能。

等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.0bject上,方法和描述如下表所示。

等待/通知的相关方法:

方法名称描述
notify()通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁
notifyAll()通知所有等待在该对象上的线程
wait()调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返圆,需要注意,调用wait()方法后,会释放对象的锁
wait(long)超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n秒,如果没有通知就超时返回
wait(long, int)对于超时时间更细粒度的控制,可以达到纳秒

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态。而另一个线程B调用了对象〇的notify()或者 notifyAll()方法,线程A收到通知后从对象O的 wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对对象上的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之同的交互工作。

在如下代码中,创建了两个线程–WaitThread和NotifyThread,前者检查flag值是否为false,如果符合要求,进行后续操作,否则在lock上等待,后者在睡眠了一段时间后对lock进行通知。

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;public class WaitNotify {static boolean flag = true;static Object lock = new Object();public static void main(String[] args) throws InterruptedException {Thread waitThread = new Thread(new Wait(), "WaitThread");waitThread.start();TimeUnit.SECONDS.sleep(1);Thread notifyThread = new Thread(new Notify(), "NotifyThread");notifyThread.start();}static class Wait implements Runnable {@Overridepublic void run() {// 加锁,拥有lock的Monitorsynchronized (lock) {// 当条件不满足时,继续wait,同时释放了1ock的锁while (flag) {try {System.out.println(Thread.currentThread() + " flag is true. wait@ "+ new SimpleDateFormat("HH:mm:ss").format(new Date()));lock.wait();} catch (InterruptedException e) {}}// 条件满足时,完成工作System.out.println(Thread.currentThread() + " flag is false. running@ "+ new SimpleDateFormat("HH:mm:ss").format(new Date()));}}}static class Notify implements Runnable {@Overridepublic void run() {// 加锁,拥有lock的Monitorsynchronized (lock) {// 获取lock的锁,然后进行通知,通知时不会释放lock的锁// 直到当前线程释放了lock后,WaitThread才能从wait方法中返回System.out.println(Thread.currentThread() + " hold lock. notify@ "+ new SimpleDateFormat("HH:mm:ss").format(new Date()));lock.notifyAll();flag = false;SleepUtils.second(5);}// 再次加锁synchronized (lock) {System.out.println(Thread.currentThread() + " hold lock again. sleep@ "+ new SimpleDateFormat("HH:mm:ss").format(new Date()));SleepUtils.second(5);}}}
}

打印:

Thread[WaitThread,5,main] flag is true. wait@ 00:44:15
Thread[NotifyThread,5,main] hold lock. notify@ 00:44:16
Thread[NotifyThread,5,main] hold lock again. sleep@ 00:44:21
Thread[WaitThread,5,main] flag is false. running@ 00:44:26

上述第3行和第4行输出的顺序可能会互换,而上述例子主要说明了调用wait()、notify()以及 notifyAll() 时需要注意的细节,如下。

  • 1. 使用wait()、notify()和notifyAll()时需要先对调用对象加锁
  • 2. 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
  • 3. notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或 notifyAll()的线程释放锁之后,等待线程才有机会从 wait()返回。
  • 4. notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态曲WAITING 变为 BLOCKED。
  • 5. 从 wait()方法返回的前提是获得了调用对象的锁。

从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改。

在这里插入图片描述

在上图中,WaitThread 首先获取了对象的锁,然后调用对象的 wait()方法,从而放弃了锁并进人了对象的等待队列 WaitQueue 中,进入等待状态。由于 WaitThread 释放了对象的锁,NotifyThread 随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从 WaitQueue移到 SynchronizedQueue中,此时WaitThread 的状态变为阻塞状态。NotifyThread 释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行。

4.3.3 等待/通知的经典范式

从4.3.2节中的WaitNotify示例中可以提炼出等待/通知的经典范式,该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。

等待方遵循如下原则。

  • 获取对象的锁。
  • 如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。
  • 条件满足则执行对应的逻辑。

对应的伪代码如下。

synchronized(对象) {while() {对象.wait();}对应的处理逻辑
}

通知方遵循如下原则。

  • 获得对象的锁。
  • 改变条件。
  • 通知所有等待在对象上的线程。

对应的伪代码如下。

synchronized(对象) {改变条件对象.notifyAll();
}

4.3.4 管道输入 / 输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。

管道输人/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和 PipedWriter,前两种面向字节,而后两种面向字符。

在下面代码中,创建了printThread,它用来接受main 线程的输入,任何main 线程的输入均通过 PipedWriter写人,而printThread在另一端通过 PipedReader将内容读出并打印。

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;public class Piped {public static void main(String[] args) throws Exception {PipedWriter out = new PipedWriter();PipedReader in = new PipedReader();// 将输出流和输入流进行连接,否则在使用时会抛出IOExceptionout.connect(in);Thread printThread = new Thread(new Print(in), "PrintThread");printThread.start();int receive = 0;try {while ((receive = System.in.read()) != -1) {out.write(receive);}} finally {out.close();}}static class Print implements Runnable {private PipedReader in;public Print(PipedReader in) {this.in = in;}@Overridepublic void run() {int receive = 0;try {while ((receive = in.read()) != -1) {System.out.println((char) receive);}} catch (IOException ex) {}}}
}

运行该示例,输人一组字符串,可以看到被 printThread 进行了原样输出。

Repeat my words.
Repeat my words.

对于 Piped 类型的流,必须先要进行绑定,也就是调用connect()方法,如果没有将输入/输出流绑定起来,对于该流的访问将会抛出异常。

4.3.5 Thread.join()的使用

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从 thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和 join(long millis, int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程tread在给定的超时时间里没有终止,那么将会从该超时方法中返回。

在下面代码中,创建了10个线程,编号0~9,每个线程调用前一个线程的join()方法,也就是线程0结束了,线程1才能从join()方法中返回,而线程0需要等待 main 线程结束。

public class Join {public static void main(String[] args) {Thread previous = Thread.currentThread();for (int i = 0; i < 10; i++) {// 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回Thread thread = new Thread(new Domino(previous), String.valueOf(i));thread.start();previous = thread;}}static class Domino implements Runnable {private Thread thread;public Domino(Thread thread) {this.thread = thread;}@Overridepublic void run() {try {thread.join();} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + " terminate.");}}
}

打印:

0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.

从上述输出可以看到,每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结束通知)。

下面代码是JDK中Thread.join()方法的源码(进行了部分调整)。

public final synchronized void join() throws InterruptedException {// 条件不满足,继续等待while (isAlive) {wait(0);}// 条件符合,方法返回
}

当线程终止时,会调用线程自身的nofityAll()方法,会通知所有等待在该线程对象上的线程、可以看到join()方法的逻辑结构与4.3.3节中描述的等待/通知经典范式一致,即加锁、循环和处理逻辑3个步骤。

4.3.6 ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存铙结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

可以通过set(T) 方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。

在下面代码中,构建了一个常用的 Profiler类,它具有 begin() 和 end()两个方法,而end()方法返回从begin()方法调用开始到end()方法被调用时的时间差,单位是毫秒。

import java.util.concurrent.TimeUnit;public class Profiler {private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal() {protected Long initialValue() {return System.currentTimeMillis();}};public static final void begin() {TIME_THREADLOCAL.set(System.currentTimeMillis());}public static final long end() {return System.currentTimeMillis() - TIME_THREADLOCAL.get();}public static void main(String[] args) throws InterruptedException {Profiler.begin();TimeUnit.SECONDS.sleep(1);System.out.println("Cost: " + Profiler.end() + " mills");}
}

打印:

Cost: 1000 mills

Profiler可以被复用在方法调用耗时统计的功能上,在方法的入口前执行begin()方法,在方法调用后执行end()方法,好处是两个方法的调用不用在一个方法或者类中、比如在AOP(面向方面编程)中,可以在方法调用前的切人点执行begi0方法,而在方法调用后的切入点执行end()方法,这样依旧可以获得方法的执行耗时。

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

斯坦福大学博士在GitHub发布的漫画机器学习小抄,竟斩获129k标星

斯坦福大学数据科学博士Chris Albon在GitHub上发布了一份超火的机器学习漫画小抄&#xff0c;发布仅仅一天就斩获GitHub榜首标星暴涨120k&#xff0c;小编有幸获得了一份并把它翻译成中文版本&#xff0c;今天给大家分享出来&#xff01; 轻松的画风配上让人更容易理解的文字讲…

【深度学习基础】环境搭建 Linux报错bash: conda: command not found...

目录 一、 问题描述二、 解决方法1. 在.bashrc文件中添加环境变量2. 卸载重装anconda 三、总结 一、 问题描述 在linux环境下安装anaconda时候报错&#xff1a;bash: conda: command not found… 二、 解决方法 1. 在.bashrc文件中添加环境变量 参考链接&#xff1a;【解决】…

打开ps提示dll文件丢失如何解决?教你几种靠谱的方法

在日常使用电脑过程中&#xff0c;由于不当操作&#xff0c;dll文件丢失是一种常见现象。当dll文件丢失时&#xff0c;程序将无法正常运行&#xff0c;比如ps&#xff0c;pr等待软件。此时&#xff0c;我们需要对其进行修复以恢复其功能&#xff0c;下面我们一起来了解一下出现…

window.matchMedia

matchMedia() 返回一个新的 MediaQueryList 对象&#xff0c;表示指定的媒体查询字符串解析后的结果。 const width ref(); const myFunction (x) > {if (x.matches) {// 媒体查询document.body.style.backgroundColor "yellow";width.value "yellow&quo…

氛围感视频素材高级感的去哪里找啊?带氛围感的素材网站库分享

亲爱的创作者们&#xff0c;大家好&#xff01;今天我们来聊聊视频创作中至关重要的一点——氛围感。一个好的视频&#xff0c;不仅要有视觉冲击力&#xff0c;还要能够触动观众的情感。那我们应该去哪里寻找这些充满氛围感且高级的视频素材呢&#xff1f;别急&#xff0c;我这…

解答 | http和https的区别,谁更好用

TTP&#xff08;超文本传输协议&#xff09;和HTTPS&#xff08;安全超文本传输协议&#xff09;的主要区别在于安全性和数据传输的方式。 一、区别 1、协议安全性&#xff1a; HTTP&#xff1a;使用明文形式传输数据&#xff0c;不提供数据加密功能&#xff0c;数据在传输过…

数字信号处理教程(3)——z变换

在连续时间域中的每一种分析方法&#xff0c;在离散时间域中想必也能得到对应一种分析方法。连续傅里叶变换对应着离散傅里叶变换&#xff08;DFT&#xff09;&#xff0c;而在拉普拉斯变换则是对应着z变换。z变换能够将信号表示成离散复指数函数的线性组合。连续傅里叶变换可以…

Java如何自定义注解及在SpringBoot中的应用

注解 注解&#xff08;Annotation&#xff09;&#xff0c;也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性&#xff0c;与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面&#xff0c;用来对这些元素进行说…

搜维尔科技:OptiTrack在NAB2024展示了一系列业界领先的媒体技术

广泛的显示和动作捕捉跟踪技术组合涵盖无与伦比的室内和室外 LED 解决方案、前沿技术演示以及最新的软件和硬件产品 可视化技术领域的全球领导者 Planar及其附属公司 3D 跟踪系统的全球领导者OptiTrack宣布&#xff0c;两家公司将在 2024 年全国广播协会 (NAB) 展会上展示其最全…

Studying-代码随想录训练营day34| 62.不同路径、63.不同路径II、343.整数拆分、96.不同的二叉搜索树

第34天&#xff0c;动态规划part02&#xff0c;牢记五部曲步骤&#xff0c;编程语言&#xff1a;C 目录 62.不同路径 63.不同路径II 343.整数拆分 96.不同的二叉搜索树 总结 62.不同路径 文档讲解&#xff1a;代码随想录不同路径 视频讲解&#xff1a;手撕不同路径 题目…

接口测试(3)

接口自动化 # 获取图片验证码import requestsresponse requests.get(url"http://kdtx-test.itheima.net/api/captchaImage")print(response.status_code) print(response.text) import requestsurl "http://kdtx-test.itheima.net/api/login" header_da…

爱情和友情触动人心

在这个纷繁复杂的娱乐圈里&#xff0c;爱情与友情的故事总是能触动人心&#xff0c;而“于适前女友包场于适新电影”的新闻&#xff0c;无疑为这个充满故事的舞台又添上了一抹温暖的色彩。这不仅仅是一场电影的包场&#xff0c;更是一段超越常规情感的展现&#xff0c;不禁感叹…

网络安全——防御实验

防御实验一 拓扑结构展示&#xff1a; 一、 根据题目&#xff0c;先为办公区做安全策略主要策略有以下几点&#xff1a; 1、书写名称和描述&#xff0c;名称和描述要明确&#xff0c;让除本人以外的人也能理解 2、确定源地址为办公区&#xff0c;目标地址为DMZ区 3、确定时间…

Xilinx zc706 USB电路解析

作者 QQ群&#xff1a;852283276 微信&#xff1a;arm80x86 微信公众号&#xff1a;青儿创客基地 B站&#xff1a;主页 https://space.bilibili.com/208826118 参考 USB OTG检测原理 USB3320 USB_ID为低电平时候&#xff0c;为host模式&#xff0c;USB_ID为悬空&#xff08;高…

锅总反驳李彦宏说的“不要卷模型,要卷应用”

李彦宏的观点是大家不要卷模型&#xff0c;要卷应用&#xff0c;但我认为这种看法是荒谬的。以下是24条反驳李彦宏观点的论点和论据&#xff1a; 模型的准确性直接决定应用的质量和用户体验&#xff1a; 论据&#xff1a;在自然语言处理、计算机视觉等领域&#xff0c;模型的准…

哦华为仓颉语言

本来我不太想说的&#xff0c;奈何有不少粉丝提问提到了这语言&#xff0c;目前的情况我不透露太多&#xff0c;看过这课程C实现一门计算机编程语言到手撸虚拟机实战的懂的自然懂。 在互联网领域几乎大部分应用软件运行在X86 LINUX上居多&#xff0c;如果你有问题可以先学习这…

C++入门基础(2)

目录 一、引用: 1、定义&#xff1a; 2、特性&#xff1a; 3、引用的使用&#xff1a; 4、const引用&#xff1a;控制权限 const引用定义: const引用可以接收3种对象&#xff1a; 1、正常对象&#xff1a; 2、临时对象&#xff1a; 3、const对象&#xff1a; 总结&…

怎么转播别人的直播

转播别人的直播&#xff0c;特别是实现无缝的实时转播&#xff0c;可以通过一些平台的功能来实现&#xff0c;比如快手和抖音。下面是一个基本的步骤说明&#xff0c;但请注意&#xff0c;具体操作可能会因平台更新或政策变化而有所不同&#xff1a; 找到想要转播的直播间&…

新品茶叶如何一炮而红?营销高手的实战指南!

茶叶&#xff0c;作为中国传统的饮品&#xff0c;已经深入到了我们的日常生活。 随着生活水平的提升&#xff0c;人们对茶叶的需求也在水涨船高。 面对市场上琳琅满目的新品茶叶&#xff0c;如何让自家品牌脱颖而出&#xff0c;确实是现在头疼的问题。下面给大家分享一些目前…

PLC通信网关有什么功能特点?PLC通信网关工作原理-天拓四方

在工业自动化日益发展的今天&#xff0c;PLC已成为工业控制领域的核心设备。工业自动化与信息化深度融合&#xff0c;PLC的应用日益广泛。PLC通信网关&#xff0c;作为工业物联网的重要组成部分&#xff0c;扮演着连接PLC与云平台的桥梁角色&#xff0c;实现设备间的数据交换与…