`
su1216
  • 浏览: 672042 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
Group-logo
深入入门正则表达式(jav...
浏览量:72020
E60283d7-4822-3dfb-9de4-f2377e30189c
android手机的安全问...
浏览量:128931
社区版块
存档分类
最新评论

多线程学习系列 - 1 - Single Threaded Execution Pattern

阅读更多

目录

多线程学习系列 - 1 - Single Threaded Execution Pattern

多线程学习系列 - 2 - Immutable Pattern

 

在android里面多线程是非常普遍的

之前工作中并不涉及太多多线程的问题,所以也就一直没有系统学习过

 

这个系列的学习选的书为《java多线程设计模式》-结城浩 著,博硕文化 译

一提到设计模式,大家可能有种种想法,但是不管大家怎么想,我希望您能先简单读读这本书

我觉得它讲的通俗易懂,例子也比较有代表性,分析的很全面,还有课后习题及答案

最重要的一点,它确实很实用,废话不多说了

 

学习过程中笔记也肯定会有些不严谨的地方,希望大家在踩我的时候能加上几句指导的话语,在此笔者感激不尽

 

书中最开始讲了一些简单的线程知识,这里也就不做整理了

 

书中以例子为主,我觉得效果还不错,所以我的笔记也以书中例子为主

 

第一章:Single Threaded Execution Pattern

考虑这样一个问题:

去某些公司面试的时候进出需要安检,一个门一次只能允许一个人通过,通过的时候工作人员对你进行身份识别。

如果多个人一起,那么工作人员所掌握的信息很有可能变得混乱。下面我们来看看工作人员的抱怨

 

假设有三个Alice,Boddy,Chris,他们分别来自Alaska,Brazil,Canada

那么我们先以一种简单的方式来做一个简单的检查,如果名字的首字母=国家的首字母,那么我们就认为检查通过

 

public static class Gate{
	private int counter = 0;
	private String name;
	private String address;
	
	public void pass(String name, String address){
		this.counter++;
		this.name = name;
		this.address = address;
		check();
	}
	
	private void check(){
		if(this.name.charAt(0) != this.address.charAt(0)){
			System.out.println("broken:" + toString());
		}
	}
	
	public String toString(){
		return "No. " + this.counter + ":" + this.name + "," + this.address;
	}
}

  大门对通过的人员进行信息记录并检查,如果出错则会打印错误信息,注意这里的pass方法可以改变Gate一些属性的状态

 

 

下面的类是捣乱的人

public static class UserClass extends Thread{
	private final Gate gate;
	private final String name;
	private final String address;
	
	public UserClass(Gate gate, String name, String address){
		this.gate = gate;
		this.name = name;
		this.address = address;
	}
	
	public void run(){
		while(true){
			gate.pass(this.name, this.address);
		}
	}
}

 这个人会不断反反复复的经过大门,同时告诉大门自己的信息

 

 

最后来看看程序运作

public static void main(String[] args) {
	Gate gate = new Gate();
	new UserClass(gate, "Alice", "Alaska").start();
	new UserClass(gate, "Boddy", "Brazil").start();
	new UserClass(gate, "Chris", "Canada").start();
}

 执行,结果让人很不满意

基本上马上就会有错误信息提示,log我就不贴了。。。

 

 

原因其实很简单,我觉得书中的表格不错

所以还是借来

 

 

线程Alice 线程Bruce this.name的值 this.address的值

this.counter++;

 

this.name = name;

this.address = address;

 

check();

 

this.counter++;

this.name = name;

 

 

this.address = address;

check();

 

(之前的值)

"Boddy"

"Alice"

"Alice"

"Alice"

"Alice"

broken...

(之前的值)

(之前的值)

(之前的值)

"Alaska"

"Brazil"

"Brazil"

 

 

上面是其中一种情况

工作人员智商有些问题,它一次只能记住一个名字和地址

Alice和Boddy都来到了门前,Bruce告诉工作人员他的名字,

工作人员记录名字:boddy

Alice来凑热闹把自己的名字和地址告诉了工作人员,这时候工作人员记录名字:Alice,地址Alaska

Boddy呢,这人他给忘了。。。

然后Boddy又告诉他自己的地址,于是工作人员脑中是这样记录的:名字Alice,地址Brazil

然后Alice和Boddy都等着工作人员核对(check()),于是悲剧发生了,工作人员认为这两个人都在骗他,所以发出了警告

 

发生这种情况的原因是Alice和Boddy非要争抢过安检,如果安检门弄小点,让他们一次最多过来一个,那就不会发生这种情况了

在这个例子中,也就是说:gate的pass方法一次只让一个线程调用,不允许Alice正在pass中呢,别人再凑过来

 

很幸运,java中用synchronized关键字就可以保证一次只有一个线程来执行这个方法

修改后的代码

public static class Gate{
	private int counter = 0;
	private String name;
	private String address;
	
	public synchronized void pass(String name, String address){
		this.counter++;
		this.name = name;
		this.address = address;
		check();
	}
	
	private void check(){
		if(this.name.charAt(0) != this.address.charAt(0)){
			System.out.println("broken:" + toString());
		}
	}
	
	public synchronized String toString(){
		return "No. " + this.counter + ":" + this.name + "," + this.address;
	}
}

 这时候再运行,则不会再出差错了

上面的代码只是给pass方法和toString方法加上了synchronized关键字

上面pass前面说过了

那么toString为什么也加上了synchronized关键字,为什么check没有加(课后习题3)

首先解释check的问题

check是private的,所以只能gate自己访问的到,在外面无法被调用。

gate里面pass调用了它,因为pass已经加上了synchronized,所以没有必要再给check函数设置此关键字

 

关于toString,由于它是public的,所以在外面可以被访问。

如果现在Alice正在调用toString,this.name = Alice,此时Boddy执行pass方法,把address改掉了,这时候Alice继续执行toString,那么悲剧发生了

所以,toString加上synchronized是必要的,这样执行toString方法时就不会被pass函数干扰了

 

在这个例子原始状态中,打印到出错的log时候counter已经到1000+了,如何让程序更快的看的错误log呢

其实不难,延长pass方法即可

public void pass(String name, String address) {
    this.counter++;
    this.name = name;
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
    }
    this.address = address;
    check();
}

 pass方法执行时间变长了,出错的几率也就变高了(课后习题1)

 

 

关于Single Threaded Execution模式适用性,需要满足下面的条件:

1.多线程时

2.数据可被多个线程访问

3.状态可能改变

4.需要确保数据安全性

这些都很容易理解,不再加以解释

 

生命性与死锁

考虑下面的一种情况:

假设Alice和Boddy都是个吃货,他们打算享用意大利面,但是只有一套餐具:一把叉子一把汤匙

吃面的时候同时需要叉子和汤匙,Alice眼疾手快拿到了叉子,Boddy不甘示弱抢到了汤匙,这时候尴尬了

Alice等着Boddy放下汤匙,Boddy等着Alice放下叉子,然后他们就这么一直等下去,地老天荒,海枯石烂。。。

这时候就是所谓的死锁。

 

Single Threaded Execution满足那些条件时可能发生死锁呢?

1.具有多个参与者(SharedResource)

2.线程锁定一个SharedResource时,没有解除锁定就去锁定另一个SharedResource

3.获取SharedResource参与者的顺序不固定(和SharedResource参与者是对等的)

 

现在我们再看看吃货这个例子

1.多个参与者(SharedResource)相当于叉子和汤匙

2.Alice拿到了汤匙不放手就要去拿叉子

3.拿汤匙和叉子地位相同,不要求先拿谁

 

只要1,2,3其中一条被破坏了,那么就可以避免死锁的发生

下面用代码来描述这个例子(习题6)

餐具

public static class Tool{
	private final String name;
	
	public Tool(String name){
		this.name = name;
	}
	
	public String toString(){
		return "[" + name + "]";
	}
}

 

吃货

public static class EaterThread extends Thread{
	private String name;
	private Tool leftHand;
	private Tool rightHand;
	
	public EaterThread(String name, Tool leftHand, Tool rightHand){
		this.name = name;
		this.leftHand = leftHand;
		this.rightHand = rightHand;
	}
	
	public void run(){
		while(true){
			eat();
		}
	}
	
	public void eat(){
		synchronized (leftHand) {
			System.out.println(name + " takes up " + leftHand + "(left.)");
			synchronized (rightHand) {
				System.out.println(name + " takes up " + rightHand + "(right.)");
				System.out.println(name + " is eating now ,yam yam!");
				System.out.println(name + " put down " + rightHand + "(right.)");
			}
			System.out.println(name + " put down " + leftHand + "(left.)");
 		}
	}
}

 

public static void main(String[] args) {
    Tool spoon = new Tool("Spoon");
    Tool fork = new Tool("Fork");
    new EaterThread("Alice", spoon, fork).start();
    new EaterThread("Bobby", fork, spoon).start();
}

 看看运行情况,果然他们没吃几口,就停住了

Alice takes up [Spoon](left.)
Alice takes up [Fork](right.)
Alice is eating now ,yam yam!
Alice put down [Fork](right.)
Alice put down [Spoon](left.)
Alice takes up [Spoon](left.)
Bobby takes up [Fork](left.)

 

原因也很简单,一个人先拿起一个餐具不放(synchronized (leftHand))的同时去拿另一个餐具,在他还没拿起来下一个餐具的时候,另一个人也打算eat,执行到这里的时候synchronized (leftHand),因为两个人拿到的是不同的餐具,所以leftHand指向不同的对象,代码并不相互影响所以程序继续执行,然后他们就发现无法获得另一把餐具,因为在对方手里

 

之前提到了Single Threaded Execution发生死锁的3个要素

我们来挨个尝试一下

首先破坏

1.具有多个参与者(SharedResource)

那么我们只提供叉子好了,你们都用一只手,有个吃相

public static class EaterThread extends Thread{
	private String name;
	private Tool hand;
	
	public EaterThread(String name, Tool hand){
		this.name = name;
		this.hand = hand;
	}
	
	public void run(){
		while(true){
			eat();
		}
	}
	
	public void eat(){
		synchronized (hand) {
			System.out.println(name + " takes up " + hand);
			System.out.println(name + " is eating now ,yam yam!");
			System.out.println(name + " put down " + hand);
		}
	}
}

public static void main(String[] args) {
    Tool fork = new Tool("Fork");
    new EaterThread("Alice",  fork).start();
    new EaterThread("Bobby", fork).start();
}

 

如果破坏

2.线程锁定一个SharedResource时,没有解除锁定就去锁定另一个SharedResource

习题答案的方式实际和上面差不多,如果只有一个工具那就不会打架了

下面引入一个新的类Pair

public static class Pair{
	private Tool rightHand;
	private Tool leftHand;
	public Pair(Tool rightHand, Tool leftHand){
		this.rightHand = rightHand;
		this.leftHand = leftHand;
	}
	
	public String toString(){
		return "[" + leftHand + "," + rightHand + "]";
	}
}

 改造EaterThread

public static class EaterThread extends Thread{
	private String name;
	private Tool leftHand;
	private Tool rightHand;
	
	public EaterThread(String name, Pair pair){
		this.name = name;
		this.leftHand = pair.leftHand;
		this.rightHand = pair.rightHand;
	}
	
	public void run(){
		while(true){
			eat();
		}
	}
	
	public void eat(){
		synchronized (leftHand) {
			System.out.println(name + " takes up " + leftHand + "(left.)");
			synchronized (rightHand) {
				System.out.println(name + " takes up " + rightHand + "(right.)");
				System.out.println(name + " is eating now ,yam yam!");
				System.out.println(name + " put down " + rightHand + "(right.)");
			}
			System.out.println(name + " put down " + leftHand + "(left.)");
		}
	}
}

 

public static void main(String[] args) {
    Tool spoon = new Tool("Spoon");
    Tool fork = new Tool("Fork");
    Pair p = new Pair(spoon, fork);
    new EaterThread("Alice", p).start();
    new EaterThread("Bobby", p).start();
}
 

这样他们也会交替的你吃一会我吃一会了

 

最后来破坏

3.获取SharedResource参与者的顺序不固定(和SharedResource参与者是对等的)

这只需要让他们按着相同的顺序来拿餐具即可

public static void main(String[] args) {
    Tool spoon = new Tool("Spoon");
    Tool fork = new Tool("Fork");
    new EaterThread("Alice", spoon, fork).start();
    new EaterThread("Bobby", spoon, fork).start();
}

 

笔记截图


 

至此这章内容就基本结束了

在书中的进阶说明里有一些关于synchronized的讨论

关于synchronized,我们需要思考

1.synchronized在保护什么

确定下来要保护的内容之后,要思考一下,其他地方有没有需要保护的,为什么

2.该以什么单位保护

比如我需要同时获得叉子和汤匙,这是一个完整的动作不能分割(当然也不是不能分割,分割后就会出现大眼瞪小眼的情况),那么

public synchronized void setName(String name){
	this.name = name;
}

public synchronized void setAddress(String address){
	this.address = address;
}

 这种形式就没有意义,并不安全(要把它当做一整体,可以像Pair那样封装一下)

3.获取谁的锁定来保护

调用synchronized方法是获得实例this的锁定,如果实例不同,锁也就不同,所以不同实例可同时执行synchronized的相同的方法。使用synchronized块的时候也是需要获得对象的锁,所以需要考虑好获得谁的锁。书中给出一个比喻:获得错误的锁就好比想要保护自己的家,却锁上了邻居的门

 

 

转贴请保留以下链接

本人blog地址

http://su1216.iteye.com/

http://blog.csdn.net/su1216/

  • 大小: 86.7 KB
分享到:
评论

相关推荐

    SPD-Conv-main.zip

    SPD-Conv-main.zip

    Docker从零走向实战视频(上).zip

    目录: 1-1 虚拟化技术发展史 1-2 虚拟化技术是什么 1-3 虚拟化技术的分类 1-4 虚拟化技术的优缺点(1) 1-4 虚拟化技术的优缺点 1-5 容器技术的发展 1-6 Docker的发展历史 1-7 Docker是什么 1-8 容器和虚拟机的区别(1) 1-9 容器和虚拟机的区别(2) 1-10 为什么要使用Docker 2-1 Docker的版本 2-2 Docker的安装 2-3 Docker服务启动 2-4 Docker服务信息 2-5 Docker使用初体验-Docker的运行机制 2-6 Docker使用初体验-Docker镜像仓库 2-7 Docker使用初体验-Docker镜像下载 2-8 Docker使用初体验-Docker镜像启动运行 2-9 Docker使用初体验-访问容器中的Tomcat服务 2-10 Docker使用初体验-Docker的网络访问机制 2-11 Docker使用初体验-进入Docker容器内部 2-12 Docker使用初体验-补充说明 3-1 Docker的体系架构(1) 3-2 Docker的体系架构(2)r ..........

    《狼》教学设计.docx

    《狼》教学设计

    房屋租赁平台:提升租赁交易透明度的数字化路径

    对于在外工作或生活的人来说,寻找合适的住房是首要解决的问题。传统的租房方式包括直接联系房东、通过房屋租赁公司或在线搜索房源。直接找房东可能耗时且不便,尤其是需要提前看房的情况;通过中介虽然方便,但需支付额外费用;而在线租房则提供了随时随地的便利性,因此越来越受到青睐。 本房屋租赁平台使用Java语言配合Idea开发环境进行构建,后端数据库选用了Mysql。平台提供了在线预约看房的功能,包括浏览出租房源、在线预约看房、收藏心仪房屋以及留言咨询等。该系统不仅方便了租房者在线预订和管理看房计划,也为房东提供了房屋信息发布和预订管理的便利。

    四轮独立驱动横摆角速度控制,LQR 基于LQR算法的 基于二自由度动力学方程,通过主动转向afs和直接横摆力矩dyc实现的横摆角速度跟踪 ,模型包括期望横摆角速度,质心侧偏角,稳定性因素,lqr模块等

    四轮独立驱动横摆角速度控制,LQR 基于LQR算法的 基于二自由度动力学方程,通过主动转向afs和直接横摆力矩dyc实现的横摆角速度跟踪 ,模型包括期望横摆角速度,质心侧偏角,稳定性因素,lqr模块等模块,作为lqr入门强烈推荐。 还有详细的lqr资料说明,可以作为基本模板,和其他算法(mpc smc)做对比等

    ESP8266、ESP32网页配网 支持中文SSID

    ESP8266、ESP32平台支持AIRKISS自动配网,但是实际使用中,发现失败的次数挺高的,影响体验,因此另辟他法,偶然发现EPS 支持webserver,通过webserver进行配网可大大提高成功率。 webserver.c实现网页的显示,及获取用户配置的wifi名称和密码; wifi_config.c根据是否已经配过网,决定是否开启ap配网模式还是st连接wifi模式; data_persistence.c实现保存用户设置的wifi名称和密码,防止断电后丢失;

    Python圣诞节倒计时与节日活动管理系统

    圣诞节倒计时与节日活动管理系统是一个基于Python的桌面应用程序,旨在帮助用户庆祝和管理圣诞节期间的活动。随着圣诞节的临近,许多人希望能够清晰地了解距离节日还有多少时间,同时也希望能够有效地组织和安排各类活动,如家庭聚会、朋友聚会、圣诞晚会等。这个应用程序通过直观的用户界面和实用的功能,满足了这些需求。 该系统的核心功能包括一个实时更新的倒计时器,用户可以看到距离圣诞节还有多少天、小时、分钟和秒。倒计时器通过Python的datetime模块实现,确保准确性和实时性。用户可以自定义圣诞节的日期,以适应不同的庆祝习惯。 除了倒计时功能,用户还可以添加、编辑和删除节日活动。通过简单的输入框,用户可以记录活动的名称、时间和地点等信息。所有活动将以列表的形式展示,用户可以轻松查看即将到来的活动,并进行相应的管理。 在技术实现方面,该应用程序使用了Python的Tkinter库来构建图形用户界面。界面设计简洁明了,用户可以轻松地进行操作。程序还使用了matplotlib库来绘制活动的统计图表,帮助用户直观地了解活动安排情况。

    双目立体匹配三维重建点云C++ 本工程基于网上开源代码进行修改,内容如下: 1.修改为 VS2015 Debug win32 版本,支持利用特征点和 OpenCV 立体匹配算法进行进行三维重建及显示

    双目立体匹配三维重建点云C++ 本工程基于网上开源代码进行修改,内容如下: 1.修改为 VS2015 Debug win32 版本,支持利用特征点和 OpenCV 立体匹配算法进行进行三维重建及显示,相关代码需要自行修改,代码中添加了修改注释。 2.工程依赖库为 OpenCV2.4.8,内部已完成 OpenCV 相关配置。 无论电脑中是否配置Opencv 都可以运行。 并且增加了点云保存,可以用MATLAB 显示点云。 一、操作步骤 1.解压后将 Reconstuction3d bin 中的所有 dll 拷贝到C: windows sysWOW64 或者system32 根据电脑版本决定,64 位为 sysWOW64。 2.双击 Reconstuction3d.sln 打开工程,运行后出现结果。 二、程序详解 Reconstuction3d.cpp 为程序主函数 cvFuncs.cpp 为特征点三维重建。 包含SIFT、SURF、FAST 等算法。 cvFuncs2.cpp 为视差图三维重建.包含 BM、SGBM 等算法可以选择两者中的一个进行重建,推荐特征点。 特征点三维重建流程:

    course_s5_linux应用程序开发篇.pdf

    course_s5_linux应用程序开发篇.pdf

    ESP32+DS1302芯片【简单DIY制作时钟】

    ESP32+DS1302芯片【简单DIY制作时钟】

    扑克牌数字检测48-CreateML、Darknet、Paligemma数据集合集.rar

    扑克牌数字检测48-CreateML、Darknet、Paligemma数据集合集.rarPCC3.0 Yolov8-V1 2023-12-04 5:04 PM ============================= *与您的团队在计算机视觉项目上合作 *收集和组织图像 *了解和搜索非结构化图像数据 *注释,创建数据集 *导出,训练和部署计算机视觉模型 *使用主动学习随着时间的推移改善数据集 对于最先进的计算机视觉培训笔记本,您可以与此数据集一起使用 该数据集包括4471张图像。 播放卡分类以创建格式注释。 将以下预处理应用于每个图像: *像素数据的自动取向(带有Exif-Arientation剥离) *调整大小为640x640(拉伸) 应用以下扩展用于创建每个源图像的2个版本: * 0到6像素之间的随机高斯模糊

    政务大数据资源平台设计方案

    政务大数据资源平台设计方案

    基于SSM框架一个比赛裁判管理系统校园赛事管理系统,主要技术(SpringMVC + Spring + Mybatis+Hui+Jquery+Ueditor)全部资料+详细文档+高分项目.zip

    【资源说明】 基于SSM框架一个比赛裁判管理系统校园赛事管理系统,主要技术(SpringMVC + Spring + Mybatis+Hui+Jquery+Ueditor)全部资料+详细文档+高分项目.zip 【备注】 1、该项目是个人高分项目源码,已获导师指导认可通过,答辩评审分达到95分 2、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 3、本项目适合计算机相关专业(人工智能、通信工程、自动化、电子信息、物联网等)的在校学生、老师或者企业员工下载使用,也可作为毕业设计、课程设计、作业、项目初期立项演示等,当然也适合小白学习进阶。 4、如果基础还行,可以在此代码基础上进行修改,以实现其他功能,也可直接用于毕设、课设、作业等。 欢迎下载,沟通交流,互相学习,共同进步!

    (174549194)ANSYS Fluent Tutorial Guide

    ANSYS Fluent Tutorial Guide ANSYS Fluent是一种基于 Finite Element Method(有限元方法)的计算流体力学(CFD)软件,广泛应用于航空航天、汽车、能源、医药等领域。下面是ANSYS Fluent Tutorial Guide的知识点总结: 1. ANSYS Fluent简介 ANSYS Fluent是一个功能强大且灵活的CFD软件,能够模拟复杂的流体力学、热传导、质量传递等物理过程。该软件广泛应用于航空航天、汽车、能源、医药等领域,用于模拟、设计和优化各种流体力学系统。 2. ANSYS Fluent的主要特点 * 基于Finite Element Method(有限元方法),能够模拟复杂的几何形状和边界条件 * 支持多种物理模型,包括流体力学、热传导、质量传递、化学反应等 * 具有强大的后处理功能,能够输出丰富的结果数据 * 可以与其他ANSYS产品集成,实现多物理场耦合分析 3. ANSYS Fluent在航空航天领域的应用 * 飞机和导弹的气动设计 * 飞机发动机的热传导和燃烧模拟 * 航天器的热保护和气动设计 4. AN

    (173083656)河西学院网络工程javaweb期末大作业.zip

    JavaWeb教务系统是基于Java技术构建的网络应用程序,用于管理高校的教学事务。这个期末大作业可能涵盖了多个关键知识点,包括但不限于以下内容: 1. **Servlet与JSP**:JavaWeb开发的基础,Servlet用于处理服务器端逻辑,而JSP则用于生成动态网页。学生可能需要了解如何创建Servlet类,实现doGet或doPost方法,以及如何在JSP页面上使用EL(Expression Language)和JSTL(JavaServer Pages Standard Tag Library)标签。 2. **MVC模式**:Model-View-Controller模式是JavaWeb开发中常见的设计模式,用于分离业务逻辑、数据模型和用户界面。学生可能需要设计并实现一个MVC架构的教务系统,如Controller负责接收请求并调用Service,Service层处理业务逻辑,而Model层则封装数据。 3. **数据库操作**:项目可能涉及到MySQL或其他关系型数据库的使用,包括数据表的设计、SQL查询语句的编写以及JDBC(Java Database Connect

    Python之正则表达式基础知识

    Python之正则表达式基础知识

    《我的白鸽》教学设计.docx

    《我的白鸽》教学设计

    基于Spring、SpringMVC、Mybatis的校园二手交易平台全部资料+详细文档+高分项目.zip

    【资源说明】 基于Spring、SpringMVC、Mybatis的校园二手交易平台全部资料+详细文档+高分项目.zip 【备注】 1、该项目是个人高分项目源码,已获导师指导认可通过,答辩评审分达到95分 2、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 3、本项目适合计算机相关专业(人工智能、通信工程、自动化、电子信息、物联网等)的在校学生、老师或者企业员工下载使用,也可作为毕业设计、课程设计、作业、项目初期立项演示等,当然也适合小白学习进阶。 4、如果基础还行,可以在此代码基础上进行修改,以实现其他功能,也可直接用于毕设、课设、作业等。 欢迎下载,沟通交流,互相学习,共同进步!

    非常好的音视频会议系统项目全套技术资料.zip

    非常好的音视频会议系统项目全套技术资料.zip

    UR5 3D模型 urdf模型

    UR5 3D模型

Global site tag (gtag.js) - Google Analytics