前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >如何停止/中断一个运行中的线程

如何停止/中断一个运行中的线程

作者头像
喜欢天文的pony站长
发布于 2020-06-29 04:17:28
发布于 2020-06-29 04:17:28
2K00
代码可运行
举报
文章被收录于专栏:RabbitMQ实战RabbitMQ实战
运行总次数:0
代码可运行

# 面试题:

  • 如何正确地停止/中断一个运行中的线程
  • 哪些情况下线程会停止
  • 如何处理不可中断的阻塞

# 核心思想

  • 使用interrupt()来通知,而不是强制。

# 代码演示

  • 场景1:run()方法中没有sleep()/wait()等会响应中断的方法。 1.1 线程未处理中断:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 正确停止线程---run()方法内没有sleep()或者wait()方法-未处理中断信号
 *
 * @author futao
 * @date 2020/6/6
 */
public class StopThreadWithoutSleepWait implements Runnable {

    @Override
    public void run() {
        unHandleInterrupt();
    }

    /**
     * 未处理中断
     */
    public void unHandleInterrupt() {
        int num = 0;
        //打印最大整数一半的范围内10000的倍数
        while (num <= Integer.MAX_VALUE / 2) {
            if (num % 10000 == 0) {
                System.out.println(num + "是10000倍数");
            }
            ++num;
        }
        System.out.println("任务执行完毕");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThreadWithoutSleepWait());
        //启动线程
        thread.start();
        //增加子线程处于运行状态的可能性
        Thread.sleep(500L);
        //尝试中断子线程
        thread.interrupt();
    }
}
  • 期望:子线程在执行500毫秒之后停下来。
  • 结果:线程并没有停下来。原因是:我们并未处理线程的中断信号。

1.2 对程序进行改进:响应中断。

  • 在while循环条件中判断当前线程是否被中断(Thread.currentThread().isInterrupted()),如果未被中断才继续执行,被中断则跳出while循环。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.futao.learn.threads.c_如何停止线程;

/**
 * 正确停止线程---run()方法内没有sleep()或者wait()方法
 *
 * @author futao
 * @date 2020/6/6
 */
public class StopThreadWithoutSleepWait implements Runnable {

    @Override
    public void run() {
        handleInterrupt();
    }

    /**
     * 响应中断
     */
    public void handleInterrupt() {
        int num = 0;
        //加入线程未被中断的条件
        while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
            if (num % 10000 == 0) {
                System.out.println(num + "是10000倍数");
            }
            ++num;
        }
        System.out.println("任务执行完毕");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThreadWithoutSleepWait());
        //启动线程
        thread.start();
        //增加子线程处于运行状态的可能性
        Thread.sleep(500L);
        //尝试中断子线程
        thread.interrupt();
    }
}
  • 期望:线程在500毫秒之后响应中断,停下来。
  • 结果:线程成功响应中断,提前结束。
  • 总结可得出:线程调用者可以向线程发出中断请求,但是线程中断的权利控制在线程代码的编写者是否响应了你的中断请求。线程代码的编写者比调用者更加了解线程应不应该被停止,何时停止。
  • 场景2:run()方法中存在sleep()/wait()等会响应中断的方法。(响应中断的方法会抛出InterruptedException) 2.1 sleep()在while循环外
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 中断线程-run()方法中有sleep()或者wait()方法
 *
 * @author futao
 * @date 2020/6/6
 */
public class StopThreadWithSleep {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            int num = 0;
            while (!Thread.currentThread().isInterrupted() && num <= 300) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的整数倍");
                }
                ++num;
            }
            try {
                //sleep()方法会响应中断,且响应中断的方式为抛出InterruptException异常--- sleep interrupted
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程执行完毕");
        });
        //启动线程
        thread.start();
        //等待while循环执行完毕
        Thread.sleep(200L);
        //当线程处于sleep()状态时进行中断
        thread.interrupt();
    }
}
  • 预期:程序执行完while循环之后,阻塞在sleep()方法,此时进行中断,sleep()方法响应该中断,抛出InterruptedException,打印异常堆栈。
  • 测试:符合预期。

2.2 无法停止的线程:sleep()方法在while循环内。

  • 你预期下面代码的执行结果是怎样的?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 3. 无法停止的线程
 *
 * @author futao
 * @date 2020/6/6
 */
public class CantStopThread {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            int num = 1;
            while (num <= 1000 && !Thread.currentThread().isInterrupted()) {
                if (num % 2 == 0) {
                    System.out.println(num + "是2的整数倍");
                }
                ++num;
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程执行完毕");
        });

        //启动线程
        thread.start();
        //主线程休眠500毫秒
        Thread.sleep(500L);
        //中断线程
        thread.interrupt();
    }
}
  • 预期:线程在第一次进入while循环时,进入休眠1000毫秒状态,在500毫秒时主线程向子线程发出中断信号,sleep()方法响应中断,打印异常堆栈,下次再进入while循环时,因为线程被设置成了中断状态,所以while中条件不成立,不应该继续执行。 但是实际上是这样吗?
  • 结果:slee()响应了中断,打印了异常堆栈。但是线程并没有停下来,而是继续执行。就像什么都没有发生一样。
  • 原因:sleep()在响应了中断之后,清除了线程的中断状态。那么while判断时不知道线程被中断了。
  • 查看sleep()方法的描述:当InterruptedException异常被抛出后,线程的中断状态将被清除。
  • 类似的,查看Object.wait()的方法描述。
  • 类似的会响应中断的方法还有那些?
  • 响应中断的方法总结
    • Object.wait()/wait(long)/wait(long,int)
    • Thread.sleep(long)/sleep(long,int)
    • Thread.join()/join(long)/join(long,int)
    • juc.BlockingQueue.take()/put(E)
    • juc.Lock.lockInterruptibly()
    • juc.CountDownLatch.await()
    • juc.CyclicBarrier.await()
    • juc.Exchanger.exchange(V)
    • jio.InterruptibleChannel相关方法
    • jio.Selector相关方法
  • 那么,线程响应中断后应该怎么处理。

# 线程中断的最佳实践:

  • 传递中断
  • 不想或无法传递:恢复中断
  • 核心思想:不应屏蔽中断
  1. 传递中断:在方法签名中将中断异常抛出,而不是生吞,交给调用者处理。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 正确停止线程的方式1-抛出中断
 * 优先在方法签名中抛出该异常
 *
 * @author futao
 * @date 2020/6/6
 */
public class RightWayToStopThread implements Runnable {

    @Override
    public void run() {
        while (true) {
            System.out.println("running...");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("响应中断,跳出循环,停止线程");
                break;
            }
        }
    }

    /**
     * 业务方法应该将中断异常抛出,将异常传递给上层--传递中断
     *
     * @throws InterruptedException
     */
    private void throwInMethod() throws InterruptedException {
        System.out.println("业务执行中.....");
        Thread.sleep(2000L);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayToStopThread());
        thread.start();
        Thread.sleep(1000L);
        thread.interrupt();
    }
}
  • 结果:
  1. 不想或无法传递时:应该恢复中断(Thread.currentThread().interrupt())
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 正确停止线程的方式2
 * 恢复中断
 *
 * @author futao
 * @date 2020/6/6
 */
public class RightWayToStopThreadReInterrupt implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("running...");
            throwInMethod();
        }
        System.out.println("线程任务执行完毕");
    }

    private void throwInMethod() {
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            System.out.println("感知到中断请求。");
            System.out.println("重新设置中断信号");
            //尝试恢复中断
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayToStopThreadReInterrupt());
        thread.start();
        Thread.sleep(1000L);
        thread.interrupt();
    }
}
  • 结果:

# 线程中断的相关方法

  • 预期下面代码的执行结果?
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 线程中断的相关方法
 *
 * @author futao
 * @date 2020/6/7
 */
public class InterruptMethod {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("线程任务执行中...");
            while (true) {
            }
        });

        //启动线程
        thread.start();
        System.out.println(thread.isInterrupted());
        //向线程发送中断信号
        thread.interrupt();
        System.out.println(thread.isInterrupted());
        System.out.println(thread.isInterrupted());
        System.out.println(Thread.interrupted());
        System.out.println(thread.isInterrupted());
        System.out.println(thread.interrupted());
        System.out.println(thread.isInterrupted());
    }
}
  • 结果:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
false
true
true
false
true
false
true
  • 分析:
    1. 线程处于运行状态,且没有程序给线程发送中断信号。所以非中断状态
    2. 调用了中断方法,所以线程状态状态为true
    3. 由于thread.isInterrupted()并不会清除线程的中断状态,所以多次调用,返回的结果一样,依旧为已中断
    4. Thread.interrupted()判断的是执行这行代码的线程的中断状态。这里是主线程,所以为未中断。且该方法调用之后,会将执行该方法的线程的中断状态清除。
    5. 因为Thread.interrupted()清除的是执行代码的线程的中断状态,所以不印象子线程的中断状态,所以子线程的中断状态仍然为true。
    6. 如果子线程对象直接调用静态方法interrupted(),返回的也是执行这段代码的线程的中断状态。此时为主线程,状态为未中断。
    7. 子线程对象直接调用静态方法interrupted()并不会清除调用对象的线程中断状态,而是清除执行这段代码的线程的中断状态。所以子线程的中断状态不影响。
  • 为什么通过子线程对象来执行静态方法static boolean interrupted()清除的是执行者的中断状态呢?查看源码发现,静态方法static boolean interrupted()会先获取到当前执行这段代码的线程,清除其中断状态,并返回中断状态。
  • 总结:
    1. thread.interrupt() 给线程发送中断信号,设置线程thread的中断状态为true。
    2. thread.isInterrupted() 判断线程thread是否被中断。且不改变线程的中断状态
    3. Thread.interrupted()/thread.interrupted() 判断执行这行代码的线程的中断状态,并且清除其中断状态。
    4. private native boolean isInterrupted(boolean ClearInterrupted); native方法,真正判断线程中断状态和清除中断状态的代码。thread.isInterrupted()Thread.interrupted()/thread.interrupted()最终调用的都是这个方法。
  • Q:如何清除线程的中断状态? 执行Thread.interrupted();这行代码的线程的中断状态会被清除。

# 哪些情况下线程会停止

  1. 线程run()方法正常执行完毕。(可借助线程中断机制提前结束run()方法)
  2. 线程发生了未捕获的异常。

# 错误的停止线程的方式

  • 被弃用的stop()suspend()resume()
  • 使用volatile设置boolean标记位的方式,不可靠

# 如何处理不可中断的阻塞

  • 并不是所有的阻塞都会响应中断,例如IO中的InputStream.read()。处理这类问题的方式要视情况而定,大概思路是手动编写程序检测线程的中断状态,如果线程被中断,则手动调用例如InputStream.close()方法来关闭流,实现停止线程。

# 本文源代码

https://github.com/FutaoSmile/learn-thread/tree/master/src/main/java/com/futao/learn/threads/c_%E5%A6%82%E4%BD%95%E5%81%9C%E6%AD%A2%E7%BA%BF%E7%A8%8B

# 系列文章

Java多线程:线程的创建与启动

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 喜欢天文 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Delphi使用NativeXml处理XML(二)
4.1.类(Classes) 4.1.1.TComponentAccess类   TComponentAccess = class(TComponent) 4.1.1.1.ComponentState   property ComponentState; 4.1.1.2.SetComponentState   procedure SetComponentState(const AState: TComponentState); 4.1.2.TNativeXml类   TNativeXml = class(TPersistent)   TNativeXml是XML文件的载体。创建一个TNativeXml,然后使用方法LoadFromFile、LoadFromStream或ReadFromString加载XML文档到内存中。或者从头开始使用Root.NodeNew添加节点,并最终SaveToFile和SaveToStream保存结果为一个XML文档。用属性Xmlformat = xfReadable确保缩进(易读)输出。 4.1.2.1.AbortParsing   property AbortParsing: boolean;   如果您使用一个SAX-like方式的OnNodeNew和OnNodeLoaded事件,并要中止解析过程,设置AbortParsing为True。例如:
Vaccae
2019/07/25
1.5K0
[重点]delphi 实现 根据给定的标题去《中国青年报》网上电子报数据中查找匹配的内容,并从该内容中取出引题、正题、副题、作者和正文。
项目要求:根据给定的标题去《中国青年报》网上电子报数据中查找匹配的内容,并从该内容中取出引题、正题、作者和正文。
全栈程序员站长
2022/09/07
7210
delphi枚举wmi
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs,SiMath,SimEncrypt, StdCtrls,ActiveX,ComObj,JwaWbemCli; type TForm1 = class(TForm) Button1: TButton; Memo: TMemo; Button2: TB
战神伽罗
2019/07/24
2.1K0
Python For Delphi---
svn checkout http://python4delphi.googlecode.com/svn/trunk/ python4delphi-read-only
py3study
2020/01/10
2.8K0
计算器(delphi)
1 unit Unit1; 2 3 interface 4 5 uses 6 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 7 Dialogs, StdCtrls, bsSkinData, BusinessSkinForm, Mask, bsSkinBoxCtrls, bscalc, 8 bsSkinCtrls; 9 10 type 11
小爷毛毛_卓寿杰
2019/02/13
3K0
delphixe5 android,Delphi XE5 Android手机端转换Ansi字符串
本文章介绍了Delphi XE5 Android手机端转换Ansi字符串,本代码是“浙江-樵夫”开发的一个程序,用来对字符串进行转换,主要代码是:
全栈程序员站长
2022/09/09
3470
奖学金评分系统(系统分析与设计版与Delphi实现代码)
在奖学金评比过程中,学生综合测评是学校普遍采用的评比手段。对学生实施综合素质测评的目的在于正确评价学生的综合素质,为评奖学金提供依据,实现学生教育管理工作的标准化、制度化和科学化,引导和促进学生德、智、体、美全面发展。
小爷毛毛_卓寿杰
2019/02/13
9580
奖学金评分系统(系统分析与设计版与Delphi实现代码)
学习笔记:7z在delphi的应用
最近做个发邮件的功能,需要将日志文件通过邮件发送回来用于分析,但是日志文件可能会超级大,测算下来一天可能会有800M的大小。所以压缩是不可避免了,delphi中的默认压缩算法整了半天不太好使,就看了看7z,在windows下有dll那么就用它吧。 下载7z.dll,还有一个delphi的开发sdk文件,sevenzip.pas。有这两个就可以了。 压缩 使用超级简单 procedure TForm1.Button1Click(Sender: TObject); var Arch: I7zOutArchi
用户1105954
2018/01/12
2.7K1
FTP my IP
程序功能,获取主机ip然后上传到指定的ftp,检测时间间隔每30min。程序源代码编译即可直接运行。
obaby
2023/02/28
2480
Delphi使用NativeXml处理XML(四)
4.2.1.ComponentCreateFromXmlFile   unit NativeXmlObjectStorage   function ComponentCreateFromXmlFile(const FileName: string; Owner: TComponent; const Name: string): TComponent;   从文件名称为FileName的XML文件读取并创建组件。为了成功地从头开始创建组件,组件的类必须事先调用RegisterClass进行登记。添加到指定Owner组件的子组件列表。这通常是一个形式。指定Name作为创建的组件的新组件的名称。 注:它调用ComponentCreateFromXmlStream实现。 4.2.2.ComponentCreateFromXmlNode   function ComponentCreateFromXmlNode(ANode: TXmlNode; Owner: TComponent; const Name: string): TComponent;   从TXmlNode类型的ANode节点读取并创建组件。为了成功地从头开始创建组件,组件的类必须事先调用RegisterClass进行登记。添加到指定Owner组件的子组件列表。这通常是一个形式。指定Name作为创建的组件的新组件的名称。 注:它使用TsdXmlObjectReader类实现。 4.2.3.ComponentCreateFromXmlStream   function ComponentCreateFromXmlStream(S: TStream; Owner: TComponent; const Name: string): TComponent;   从XML流类型的S中读取并创建组件。为了成功地从头开始创建组件,组件的类必须事先调用RegisterClass进行登记。添加到指定Owner组件的子组件列表。这通常是一个形式。指定Name作为创建的组件的新组件的名称。 注:它调用ComponentCreateFromXmlNode实现。 4.2.4.ComponentCreateFromXmlString   function ComponentCreateFromXmlString(const Value: string; Owner: TComponent; const Name: string): TComponent;   从XML字符串类型的Value中读取并创建组件。为了成功地从头开始创建组件,组件的类必须事先调用RegisterClass进行登记。添加到指定Owner组件的子组件列表。这通常是一个形式。指定Name作为创建的组件的新组件的名称。 注:它调用ComponentCreateFromXmlStream实现。 4.2.5.ComponentSaveToXmlFile   procedure ComponentSaveToXmlFile(AComponent: TComponent; const FileName: string; AParent: TComponent);   存储组件AComponent所有公布的属性到名为FileName 的XML文件。指定AParent为了储存Parent正确的方法和事件参考。 注:它调用ObjectSaveToXmlFile实现。 4.2.6.ComponentSaveToXmlNode   procedure ComponentSaveToXmlNode(AComponent: TComponent; ANode: TXmlNode; AParent: TComponent);   存储组件AComponent所有公布的属性到TXmlNode格式的ANode中。指定AParent为了储存Parent正确的方法和事件参考。 注:它调用ObjectSaveToXmlNode实现。 4.2.7.ComponentSaveToXmlStream   procedure ComponentSaveToXmlStream(AComponent: TComponent; S: TStream; AParent: TComponent);   存储组件AComponent所有公布的属性到XML格式的流S中。指定AParent为了储存Parent正确的方法和事件参考。 注:它调用ObjectSaveToXmlStream实现。 4.2.8.ComponentSaveToXmlString   function ComponentSaveToXmlString(AComponent: TComponent; AParent: TComponent): string;   存储组件AComponent所有公布的属
Vaccae
2019/07/24
1.9K0
Delphi中使用RTTI
运行期类型信息(RTTI)是一种语言特征,能使应用程序在运行时得到关于对象的信息。
Vaccae
2019/07/24
2K0
.NET 2.0 Web Services的bug?
今天测试.net 2.0的WebService,发现了一个大问题。就是无法获取参数,参数永远是null。当然了使用.net调用没有任何问题,web测试页也正常。不论是Delphi7还是java调用的结果的都是一样的,难道是.net 2.0的Bug? 测试结果发现:值类型参数全部为缺省值,引用类型全部为null WebService的代码如下:
用户3135539
2018/09/12
6510
raw socket (DELPHI开发平台)
下面给出一个通过自定义源IP地址和源端口演示如何发送UDP数据包的例子,或许对你了解有所帮助.你也可以设计自己的协议,如发送SY N 数据浪涌,或其它类型的自定义协议。 { Raw 数据包 Sender 使用:Delphi + Winsock 2
战神伽罗
2019/09/02
6970
delphi 使用windowsmediaplayer播放视频
delphi7中原本自带的一个Tmediaplayer控件,但是发现有不少视频是无法播放的,于是就想到了用windowsmediaplayer来进行播放。
Vaccae
2019/07/25
3.1K0
delphi 使用windowsmediaplayer播放视频
【Delphi】 Thread.Queue与Synchronize的区别
前话: 其实大家要学会看源码, 我接下来要说的这些东东,与其等别人讲,还不如自己搞几个代码试一下,印象还深刻点 TThread.Queue和TThread.Synchronize的区别,
战神伽罗
2019/07/24
1.9K0
delphi完美的线程注入和卸载
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
战神伽罗
2019/07/24
1K0
蛋疼的挂掉360云查杀的小玩意儿
其实就是个域名屏蔽的列表,做个小工具便于免杀。没什么高深的技术。 最后蛋疼的贴点代码: procedure TForm1.FormCreate(Sender: TObject); begin GetSystemDirectory(sysdir,256); hosts := sysdir + '\drivers\etc\hosts'; backup := sysdir + '\drivers\etc\hosts.bak'; //ShowMessage(hosts); if FileExists(hosts
obaby
2023/02/28
3410
蛋疼的挂掉360云查杀的小玩意儿
截取程序的网络封包(Delphi Hook API)
有时候我们需要对其它应用程序发送和接收的网络数据进行拦截,比如要对IE发送的**头进行分析,得到请求的地址等.这次我们可以用一些例如WPE, Sniffer之类的工具来达到目的.但是工具功能有限,要想实现更强大的功能,还是我们自己动手来DIY吧. 拦截网络数据封包的方法有三种,一是将网卡设为混杂模式,这次就可以监视到局域网上所有的数据包,二是HOOK目标进程的发送和接收的API函数,第三种方法是自己实现一个代理的DLL.在这里我们使用HOOK API的方法,这样易于实现,而且也不会得到大量的无用数据(如第一种方法就会监视到所有的网络数据). 下面是一个尽量简化了的API HOOK的模版,原理是利用消息钩子将DLL中的代码注入到目标进程中,再用GetProcAddress得到API函数入口地址,将函数入口址改为自己定义的函数入口,这样就得到了API函数的相应参数,处理完后,再改回真实API函数入口地址,并调用它. HOOK.DLL的代码:
战神伽罗
2019/07/24
1.9K0
截取程序的网络封包(Delphi Hook API)
数组类型与数组指针的巧妙利用
本例通过存取结构, 慢慢引入了数组类型与指针的一些使用方法; 其中六个小例子的测试内容和结果都是一样的.
Vaccae
2019/07/24
8400
学习笔记:delphi多线程知识
最近一直在温习旧的知识,刚好学习了一下Java的线程安全方面的知识,今天想起之前一直做的Delphi开发,所以还是有必要温习一下,看看这些不同的编程语言有什么不同之处。 Delphi的线程同步方法: 1、临界区 申明一个临界资源 FLock   : TRTLCriticalSection; 先初化一个临界资源对象 InitializeCriticalSection(FLock) 销毁临界资源对象 DeleteCriticalSection(FLock) procedure TSaveThread.Push
用户1105954
2018/01/12
1.2K0
推荐阅读
相关推荐
Delphi使用NativeXml处理XML(二)
更多 >
LV.1
山东瑞意博医疗设备有限公司技术经理
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档