1501 - JUC高并发

须知少许凌云志,曾许人间第一流

看的是尚硅谷的视频做的学习总结,感恩老师,下面是视频的地址

传送门icon-default.png?t=N7T8https://www.bilibili.com/video/BV1Kw411Z7dF

0.思维导图

1.JUC简介

1.1 什么是JUC

JUC, java.util.concurrent工具包的简称,一个处理线程的工具包。

1.2 进程和线程的概念

1.2.1 进程与线程

  • 进程

指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程是资源分配的最小单位。

  • 线程

系统分配处理器时间资源的基本单元;进程之内独立执行的一个单元执行流;线程是程序执行的最小单位。

一个进程包含多个线程。

1.2.2 线程的状态

查看jdk源码

1.2.3 wait 和 sleep

区别

  1. sleep是Thread的静态方法;wait是Object的方法,任何对象实例都能调用。
  2. sleep不会释放锁,它也不需要占用锁;wait会释放锁。(wait会释放锁去睡觉,sleep会抓住锁去睡觉,在哪里谁就会在哪里醒)
  3. 它们都可以被interrupt方法中断。

1.2.4 并发和并行

  • 并发

同一时间间隔内多个线程交替执行,实际上是宏观上并行,微观上串行。(春运抢票,电商秒杀,抢同一个资源

  • 并行

同一时刻多个线程正在执行,多核并行。(一边看书,一边听音乐)

1.2.5 管程

叫 Monitor 监视器,就是锁。

是一种同步机制,保证同一个时间,只有一个线程访问被保护的数据或者代码。

1.2.6 用户线程和守护线程

  • 用户线程

自定义的线程,不随主线程的结束而结束。主线程结束了,用户线程还会运行,jvm还是存活状态。

  • 守护线程

随着主线程的结束而结束,如垃圾回收线程。主线程结束,jvm结束。

2.Lock接口

2.1 Synchronized

2.1.1 Synchronized作用范围

synchronized是Java的关键字,是一种同步锁。

synchronized的作用范围可以根据使用方式的不同而有所区别,主要有以下几种情况:

  • 同步方法(实例方法)
public class SynchronizedExample {public synchronized void syncMethod() {// 同步代码块}
}

synchronized修饰一个实例方法时,它作用于整个方法体。当一个线程进入一个对象的同步方法时,其他线程在该对象上调用同步方法时会被阻塞,直到第一个线程退出该方法。这种同步方式是基于对象的,也就是说,不同的对象实例的同步方法是互不干扰的。

  • 同步静态方法
public class SynchronizedExample {public static synchronized void syncStaticMethod() {// 同步代码块}
}

synchronized修饰一个静态方法时,它作用于整个静态方法体。由于静态方法是属于类的,而不是类的实例,因此这种同步是基于类的。当一个线程进入一个类的同步静态方法时,其他线程在该类上调用同步静态方法时会被阻塞。

  • 同步代码块
public class SynchronizedExample {private final Object lock = new Object();public void someMethod() {synchronized (lock) {// 同步代码块}}
}

synchronized也可以用来修饰一个代码块,此时需要指定一个对象作为锁对象。当线程进入同步代码块时,它会获取指定对象的锁,如果其他线程已经持有该对象的锁,则进入阻塞状态。这种同步方式允许更细粒度的控制,只同步需要同步的代码部分。 

2.1.2 多线程编程步骤

第一步:创建资源类,在资源类创建属性和操作方法。

第二步:创建多个线程,去调用资源类的操作方法。

2.1.3 Synchronized实现买票示例

需求:3个售票员卖一百张门票。

分析:资源是一百张门票,操作方法是买票,创建多个线程是3个售货员。

代码示例

// 第一步:定义资源类
class Ticket {// 第二步:定义资源public int number = 100;// 第三步:定义操作方法public synchronized void sale() {if (number > 0) {System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - number + 1) + "张票,剩余" + --number + "张票");}}
}public class SaleTicket {public static void main(String[] args) {Ticket ticket = new Ticket();// 使用匿名内部类创建线程Runnable runnable = () -> {for (int i = 0; i < 150; i++) {ticket.sale();}};// 创建多个线程进行卖票new Thread(runnable,"售票员1").start();new Thread(runnable,"售票员2").start();new Thread(runnable,"售票员3").start();}
}

输出结果

2.2 Lock

2.2.1 Lock接口的介绍

Lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。

2.2.2 使用Lock实现卖票例子

// 第一步:定义资源类
class Ticket {// 第二步:定义资源public int number = 100;// 创建可重入锁private final ReentrantLock lock = new ReentrantLock();// 第三步:定义操作方法public synchronized void sale() {// 手动上锁lock.lock();try{if (number > 0) {System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - number + 1) + "张票,剩余" + --number + "张票");}}finally {// 手动解锁lock.unlock();}}
}public class SaleTicket {public static void main(String[] args) {Ticket ticket = new Ticket();// 使用匿名内部类创建线程Runnable runnable = () -> {for (int i = 0; i < 150; i++) {ticket.sale();}};// 创建多个线程进行卖票new Thread(runnable,"售票员1").start();new Thread(runnable,"售票员2").start();new Thread(runnable,"售票员3").start();}
}

2.2.3 synchronized和Lock两者差异

  • synchronized是java内置关键字。Lock不是内置,可以实现同步访问且比 synchronized中的方法更加丰富。
  • synchronized自动释放锁,而lock需手动释放锁(不解锁会出现死锁,需要在 finally 块中释放锁)。
  • Lock 可以让等待锁的线程响应中断,而等待synchronized锁的线程不能响应中断,会一直等待。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  • Lock 可以提高多个线程进行读操作的效率(当多个线程竞争的时候,Lock 性能远远好于synchronized)。

2.2.4 创建线程的四种方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 使用Callable接口
  4. 使用线程池

3.线程间通信

3.1 多线程编程步骤

第一步:创建资源类,在资源类创建属性和操作方法。

第二步:在资源类操作方法,判断、干活、通知。

第三步:创建多个线程,去调用资源类的操作方法。

第四步:防止虚假唤醒。

3.2 synchronized 实现线程通信案例

关键字 synchronized 与 wait()/notify() 这两个方法一起使用可以实现等待/通知模式。

代码示例

// 第一步:创建资源类,定义属性和操作方法
class Share {private int number = 0;// +1的方法public synchronized void incr() throws InterruptedException {// 第二步:判断if (number != 0) {this.wait();}// 干活number++;System.out.println(Thread.currentThread().getName() + ":" + number);// 通知其他线程this.notifyAll();}// -1的方法public synchronized void decr() throws InterruptedException {// 判断if (number != 1) {this.wait();}// 干活number--;System.out.println(Thread.currentThread().getName() + ":" + number);// 通知其他线程this.notifyAll();}
}public class ThreadDemo {public static void main(String[] args) {// 第三步:创建多个线程,调用资源类中的操作方法Share share = new Share();new Thread(() -> {for (int i = 0; i < 3; i++) {try {share.incr();} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "生产").start();new Thread(() -> {for (int i = 0; i < 3; i++) {try {share.decr();} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "消费").start();}}

3.3 虚假唤醒问题(if改while)

当多个线程都处于等待集合中,一旦收到通知,可以直接操作而不再判断,这叫做虚假唤醒问题。 将this.wait()放在while循环中可以解决该问题。

代码示例

class Share {int number = 0;public synchronized void incr() throws InterruptedException {//判断if (number != 0) {this.wait();//这里会释放锁}//执行number++;System.out.print(Thread.currentThread().getName() + " : " + number + "-->");// 通知this.notifyAll();}public synchronized void decr() throws InterruptedException {//判断if (number != 1) {this.wait();}//执行number--;System.out.println(Thread.currentThread().getName() + " : " + number);//通知this.notifyAll();}
}public class ThreadDemo {public static void main(String[] args) {Share share = new Share();new Thread(() -> {for (int i = 1; i <= 100; i++) {try {share.incr();} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "A").start();new Thread(() -> {for (int i = 1; i <= 100; i++) {try {share.decr();} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "B").start();new Thread(() -> {for (int i = 1; i <= 100; i++) {try {share.incr();} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "C").start();new Thread(() -> {for (int i = 1; i <= 100; i++) {try {share.decr();} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "D").start();}
}

输出结果

原因

由于 wait() 方法使线程在哪里睡就在哪里醒,B和D在wait后被唤醒,执行操作时不会再通过 if 判断,从而导致出现异常结果。
为了保证线程“醒了”之后再次判断,需要将wait() 方法放入while循环中。 

class Share {int number = 0;public synchronized void incr() throws InterruptedException {//判断while (number != 0) {this.wait();}//执行number++;System.out.print(Thread.currentThread().getName() + " : " + number + "-->");// 通知this.notifyAll();}public synchronized void decr() throws InterruptedException {//判断while (number != 1) {this.wait();}//执行number--;System.out.println(Thread.currentThread().getName() + " : " + number);//通知this.notifyAll();}
}

3.4 Lock实现线程间通信案例

在 Lock 接口中,有一个 newCondition() 方法,返回一个新 Condition 绑定到该实例 Lock 实例。

Condition 类中有 await() signalAll() 等方法,和 synchronized 实现案例中的 wait() 和 notifyAll() 方法相同。

代码示例

class Share {private int number = 0;//创建Lockprivate final ReentrantLock lock = new ReentrantLock();private final Condition condition = lock.newCondition();public void incr() throws InterruptedException {lock.lock();try {while (number != 0) {condition.await();}number++;System.out.print(Thread.currentThread().getName() + " : " + number + "-->");condition.signalAll();} finally {lock.unlock();}}public void decr() throws InterruptedException {lock.lock();try {//判断while (number != 1) {condition.await();}//执行number--;System.out.println(Thread.currentThread().getName() + " : " + number);//通知condition.signalAll();} finally {lock.unlock();}}
}public class ThreadDemo {public static void main(String[] args) {Share share = new Share();new Thread(() -> {for (int i = 1; i <= 100; i++) {try {share.incr();} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "A").start();new Thread(() -> {for (int i = 1; i <= 100; i++) {try {share.decr();} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "B").start();new Thread(() -> {for (int i = 1; i <= 100; i++) {try {share.incr();} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "C").start();new Thread(() -> {for (int i = 1; i <= 100; i++) {try {share.decr();} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "D").start();}
}

4.线程间定制化通信

4.1 Condition 类选择性通知

案例: 启动三个线程,按照如下要求执行,AA打印5此,BB打印10次,CC打印15次,一共进行10轮
具体思路: 每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志位的线程。

代码示例

class ShareResource {// 标志位 1:AA 2:BB 3:CCprivate int flag = 1;private ReentrantLock lock = new ReentrantLock();// 创建三个Condition对象,实现定向唤醒Condition c1 = lock.newCondition();Condition c2 = lock.newCondition();Condition c3 = lock.newCondition();public void print5(int loop) throws InterruptedException {lock.lock();try {while (flag != 1) {c1.await();}for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + " :: " + i + ", loop=" + loop);}flag = 2;c2.signal();} finally {lock.unlock();}}public void print10(int loop) throws InterruptedException {lock.lock();try {while (flag != 2) {c2.await();}for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + " :: " + i + ", loop=" + loop);}flag = 3;c3.signal();} finally {lock.unlock();}}public void print15(int loop) throws InterruptedException {lock.lock();try {while (flag != 3) {c3.await();}for (int i = 0; i < 15; i++) {System.out.println(Thread.currentThread().getName() + " :: " + i + ", loop=" + loop);}flag = 1;c1.signal();} finally {lock.unlock();}}
}public class ThreadDemo {public static void main(String[] args) {ShareResource shareResource = new ShareResource();new Thread(() -> {for (int i = 0; i < 10; i++) {try {shareResource.print5(i);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "AA").start();new Thread(() -> {for (int i = 0; i < 10; i++) {try {shareResource.print10(i);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "BB").start();new Thread(() -> {for (int i = 0; i < 10; i++) {try {shareResource.print15(i);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "CC").start();}
}

上面的代码采用单标志法,设置一个公用整型变量flag,用于指示被允许进入临界区的进程编号。

若 flag =1,则允许 AA 进程进入临界区;

若 flag =2,则允许 BB 进程进入临界区;

若 flag =3,则允许 CC 进程进入临界区。

该算法可确保每次只允许一个进程进入临界区。但两个进程必须交替进入临界区,若某个进程不再进入临界区,则另一个进程也无法进入临界区。在线程的run()方法调用中设置不同的loop次数,在后期会有部分线程不能访问 Share 资源了,违背了"空闲让进"原则,让资源利用不充分。

4.2 进程/线程同步四个原则

  • 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
  • 忙则等待:当已经有进程进入临界区的时候,其他试图进入临界区的进程必须等待。
  • 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区。
  • 让权等待:当进程不能进入临界区的时候,应立即释放处理机,防止进程忙等待。

5.集合的线程安全

集合线程不安全,简单来说就是底层的方法没有使用同步安全锁。

5.1 ArrayList 不安全

jdk源码

没有使用synchronized关键字,所以在多线程并发时,会出现线程安全问题。

代码示例

public class ThreadDemo {public static void main(String[] args) {List<String> list = new ArrayList<>();for (int i = 0; i < 30; i++) {new Thread(() -> {list.add(UUID.randomUUID().toString().substring(0, 8));System.out.println(list);}, String.valueOf(i)).start();}}
}

运行报错

5.2 解决方案 Vector

jdk源码

Vector类中的方法加了synchronized关键字,因此可以保证线程安全。

代码改造

public class ThreadDemo {public static void main(String[] args) {List<String> list = new Vector<>();for (int i = 0; i < 30; i++) {new Thread(() -> {list.add(UUID.randomUUID().toString().substring(0, 8));System.out.println(list);}, String.valueOf(i)).start();}}
}

 运行结果

5.3 解决方案 synchronizedList(List list) 

Collections 接口中的 synchronizedList(List list) 方法,可以将传入的 List列表对象转为同步(线程安全的)列表并返回。

语法

List<String> list = Collections.synchronizedList(new ArrayList<>());

jdk源码

代码示例

public class ThreadDemo {public static void main(String[] args) {List<String> list = Collections.synchronizedList(new ArrayList<>());for (int i = 0; i < 30; i++) {new Thread(() -> {list.add(UUID.randomUUID().toString().substring(0, 8));System.out.println(list);}, String.valueOf(i)).start();}}
}

5.4 解决方案 CopyOnWriteArrayList

语法

List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList 采用读写分离的思想,读操作不加锁,写操作加锁。(redis也使用)

  • 读的时候并发读取旧数据(多个线程操作)
  • 写的时候独立,先复制一份比旧数据长 1 的数据出来,在最后添加数据,旧新合并,完成写操作,之后就可以读所有数据(每次加新内容都写到新区域,合并之前旧区域,读取新区域添加的内容)

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

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

相关文章

STM32-呼吸灯仿真

目录 前言: 一.呼吸灯 二.跑马灯 三. 总结 前言: 本篇的主要内容是关于STM32-呼吸灯的仿真,包括呼吸灯,跑马灯的实现与完整代码,欢迎大家的点赞,评论和关注. 接上http://t.csdnimg.cn/mvWR4 既然已经点亮了一盏灯,接下来就可以做更多实验了, 一.呼吸灯 在上一个的基础上…

【一】apollo 环境配置

域控制器配置 google输入法安装 安装输入google pinyin法 sudo apt install fcitx-bin sudo apt install fcitx-table sudo apt-get install fcitx fcitx-googlepinyin -y 最后需要reboot 系统环境 修改文件夹名称为英文 export LANGen_US xdg-user-dirs-gtk-update 挂载硬…

2559. 统计范围内的元音字符串数(前缀和) o(n)时间复杂度

给你一个下标从 0 开始的字符串数组 words 以及一个二维整数数组 queries 。 每个查询 queries[i] [li, ri] 会要求我们统计在 words 中下标在 li 到 ri 范围内&#xff08;包含 这两个值&#xff09;并且以元音开头和结尾的字符串的数目。 返回一个整数数组&#xff0c;其中…

微前端之旅:探索Qiankun的实践经验

theme: devui-blue 什么是微前端&#xff1f; 微前端是一种前端架构方法&#xff0c;它借鉴了微服务的架构理念&#xff0c;将一个庞大的前端应用拆分为多个独立灵活的小型应用&#xff0c;每个应用都可以独立开发、独立运行、独立部署&#xff0c;再将这些小型应用联合为一个完…

淘宝天猫商品详情API接口详解

一、淘宝天猫商品详情API接口概述 淘宝天猫商品详情API接口是淘宝天猫开放平台提供的一项重要服务&#xff0c;它允许开发者通过API接口获取淘宝天猫商品的详细信息。这些信息包括但不限于商品标题、价格、描述、图片、销量、评价等。通过使用淘宝天猫商品详情API接口&#xf…

国密算法SM2的优势、原理和应用场景

随着信息化时代的到来&#xff0c;数据安全和网络空间的安全成为了国家安全的重要组成部分。密码学作为保障信息安全的关键技术&#xff0c;其重要性日益凸显。在这样的背景下&#xff0c;中国国家密码管理局推出了一系列自主的密码学算法&#xff0c;即国密算法&#xff0c;其…

12.【Orangepi Zero2】基于orangepi_Zero_2 Linux的智能家居项目

基于orangPi Zero 2的智能家居项目 需求及项目准备 语音接入控制各类家电&#xff0c;如客厅灯、卧室灯、风扇回顾二阶段的Socket编程&#xff0c;实现Sockect发送指令远程控制各类家电烟雾警报监测&#xff0c; 实时检查是否存在煤气泄漏或者火灾警情&#xff0c;当存在警情时…

SkyWalking之P0业务场景输出调用链路应用

延伸扩展&#xff1a;XX业务场景 路由标签打标、传播、检索 链路标签染色与传播 SW: SkyWalking的简写 用户请求携带HTTP头信息X-sw8-correlation “X-sw8-correlation: key1value1,key2value2,key3value3” 网关侧读取解析HTTP头信息X-sw8-correlation&#xff0c;然后通过SW…

探索未来制造,BFT Robotics引领潮流

“买机器人&#xff0c;上BFT” 在这个快速变化的时代&#xff0c;创新和效率是企业发展的关键。BFT Robotics&#xff0c;作为您值得信赖的合作伙伴&#xff0c;专注于为您提供一站式的机器人采购和自动化解决方案。 产品系列&#xff1a; 协作机器人&#xff1a;安全、灵活、…

Linux C语言:指针和指针变量

一、指针的作用 使程序简洁、紧凑、高效有效地表示复杂的数据结构动态分配内存能直接访问硬件能够方便的处理字符串得到多于一个的函数返回值 二、内存、地址和变量 1、内存地址 2、变量和地址 1&#xff09;变量用来在程序中保存数据 比如: int k 58; //声明一个int变…

基于JSP技术的社区疫情防控管理信息系统

你好呀&#xff0c;我是计算机学长猫哥&#xff01;如果有相关需求&#xff0c;文末可以找到我的联系方式。 开发语言&#xff1a;JSP 数据库&#xff1a;MySQL 技术&#xff1a;JSPJavaBeans 工具&#xff1a;MyEclipse、Tomcat、Navicat 系统展示 首页 用户注册与登录界…

2024-5-7 石群电路-26

2024-6-7&#xff0c;星期五&#xff0c;15:00&#xff0c;天气&#xff1a;阴转小雨&#xff0c;心情&#xff1a;晴。今天虽然是阴雨天&#xff0c;但是心情不能差哦&#xff0c;离答辩越来越近了&#xff0c;今天学完习好好准备准备ppt&#xff0c;加油学习喽~ 今日观看了石…

Faster R-CNN:端到端的目标检测网络

本文回顾了由微软研究人员开发的 Faster R-CNN 模型。Faster R-CNN 是一种用于物体检测的深度卷积网络&#xff0c;在用户看来&#xff0c;它是一个单一的、端到端的统一网络。该网络可以准确快速地预测不同物体的位置。为了真正理解 Faster R-CNN&#xff0c;我们还必须快速概…

《接口自动化测试框架》代码片段 - 接口请求封装

抛砖引玉 requests模块是Python中发送HTTP请求的强大工具&#xff0c;它以其直观易用的API和人性化的设计赢得了广泛赞誉。 这个模块不仅提供了丰富的功能来定制HTTP请求&#xff0c;如设置请求头、传递URL参数等&#xff0c;还能够自动处理许多底层细节&#xff0c;如Cookie管…

重庆公司记账代理,打造专业财务管理解决方案的领先企业

重庆公司记账代理&#xff0c;作为专业的财务管理服务提供商&#xff0c;我们的目标是为公司的经营管理和决策提供科学、准确的财务数据支持&#xff0c;我们通过长期的专业经验和对市场的深入理解&#xff0c;为您提供一站式的记账服务和财务咨询。 专业团队 我们拥有一支由经…

如何移动 hiberfil.sys 文件来减少C盘空间

如何移动 hiberfil.sys 文件来减少C盘空间 hiberfil.sys 文件是什么&#xff1f; hiberfil.sys 文件是 Windows 系统在开启休眠功能后自动生成的内存镜像文件。这个文件保存了系统休眠时的内存内容&#xff0c;以便我们唤醒电脑之后可以快速恢复到休眠前的状态。这个文件通常…

基于JSP技术的师生交流平台

你好呀&#xff0c;我是计算机学长猫哥&#xff01;如果有相关需求&#xff0c;文末可以找到我的联系方式。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSP技术 工具&#xff1a;Myeclipse、B/S架构 系统展示 首页 管理员登录界面 学生信息管理…

Qwen2开源发布,各方位全面升级!

今天&#xff0c;通义千问团队带来了Qwen2系列模型&#xff0c;Qwen2系列模型是Qwen1.5系列模型的重大升级。包括了&#xff1a; 5个尺⼨的预训练和指令微调模型, 包括Qwen2-0.5B、Qwen2-1.5B、Qwen2-7B、Qwen2-57B-A14B以及Qwen2-72B&#xff1b; 在中⽂英语的基础上&#xf…

Thermal-BST自动化工具在Flotherm建模中的应用与优势

引言 随着科技的不断发展&#xff0c;电子领域的需求也越来越广泛和多样化。然而&#xff0c;PCB板及其上的器件建模问题一直是电子工程师在设计过程中面临的重要挑战之一。软件中原有的PCB建模工具&#xff0c;转换出来的模型复杂&#xff0c;影响后期的网格划分&#xff0c;…

选择云桌面必看:影响云桌面性能的一些重要因素

当云桌面发展的如火如荼并被越来越多的人所追捧的时候&#xff0c;偶然在网上看到有人评论说云桌面太坑&#xff0c;说好的免维护和节省成本好像与想象中的还是不一样。云桌面真的太坑&#xff1f;那是因为你没做好这几点。 选择云桌面你该关注哪些指标&#xff1f; 其实从云…