Java多线程--线程间的通信
文章目录
- 一、线程间的通信
- (1)为什么要处理线程间的通信
- (2)等待唤醒机制
- 二、案例
- (1)案例
- 1、创建线程
- 2、解决线程安全问题
- 3、等待
- 4、唤醒
- 5、同步监视器
- (2)调用wait和notify需注意的细节
- 三、wait与sleep的区别
一、线程间的通信
(1)为什么要处理线程间的通信
当我们需要多个线程
来共同完成一件任务,并且我们希望他们有规律的执行
,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。(在同步的基础之上解决通信的问题)
比如:线程A用来生产包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,此时B线程必须等到A线程完成后才能执行,那么线程A与线程B之间就需要线程通信,即—— 等待唤醒机制。
(2)等待唤醒机制
这是多个线程间的一种协作机制
。
谈到线程我们经常想到的是线程间的竞争(race)
,比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。
在一个线程满足某个条件时,就进入等待状态(wait() / wait(time)
), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify()
);
或可以指定wait的时间,等时间到了自动唤醒;
在有多个线程进行等待时,如果需要,可以使用 notifyAll()
来唤醒所有的等待线程。wait/notify
就是线程间的一种协作机制。
- wait:线程不再活动,不再参与调度,进入
wait set
中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态是 WAITING 或 TIMED_WAITING。它还要等着别的线程执行一个特别的动作
,也即“通知(notify)
”或者等待时间到,在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue
)中。 - notify:则选取所通知对象的 wait set 中的一个线程释放。
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
🗳️注意:
被通知的线程被唤醒后也不一定能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;
- 否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态
二、案例
(1)案例
🌋案例描述:
使用两个线程打印 1-100。线程1, 线程2 交替打印。
🍰分析
1、创建线程
这里使用实现的方式创建线程。
class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {}
}
在run
方法里面写上逻辑代码:
class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){if(number<=100){System.out.println(Thread.currentThread().getName()+":"+number);number++;}else{break;}}}
}
在main
方法中创建两个线程,如下:
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}
关于number的问题,需要考虑同步。
在if中加一个sleep
,将问题放大。
目前的代码如下:
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;}else{break;}}}
}
输出:
可以看到打印出了错误信息。
2、解决线程安全问题
现在我们使用同步代码块
来解决线程安全问题。
将操作number的代码用synchronized
包裹起来,就是下面蓝色部分:
用synchronized
包裹的时候,不能包裹少了,那样会有安全问题。
若是包裹多了,串行的场景就会更多,导致效率变低。
所以,操作共享数据的代码,不能包裹多,也不能少。
🌱代码
package yuyi04.Communication;/*** ClassName: PrintNumberTest* Package: yuyi04.Communication* Description:* 使用两个线程打印 1-100。线程1, 线程2 交替打印。* @Author 雨翼轻尘* @Create 2024/2/2 0002 14:57*/
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){synchronized (this) { //当前this表示PrintNumber的实例,即p,是唯一的if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;}else{break;}}}}
}
🍺输出
可以看到现在的线程是安全的。
3、等待
紧接着需要考虑下一个问题,需要将两个线程交替打印。
🎲如何实现交替?
比如线程1进入同步代码块,然后打印结束,出了同步代码块。
此时线程1需要进入阻塞状态,让线程2进入同步代码块输出执行,才能保证与线程1交替执行。
如何让线程1处于阻塞,这里需要使用一个方法wait()
,让它处于等待状态。如下:
线程一旦执行wait()
方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用。
wait()
方法有一个异常需要处理一下:
注意,sleep()
不会释放同步监视器,这一点需要注意。
有时候面试会问,wait()
与sleep()
有什么区别?
这就是其中一个区别,sleep()
不会释放对同步监视器的调用,而wait()
会释放对同步监视器的调用。
🌱代码
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){synchronized (this) { //当前this表示PrintNumber的实例,即p,是唯一的if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出
☕注意
一旦线程1进入同步代码块执行到wait()
,就会释放对同步监视器的调用,然后开始等待操作。
线程2就可以拿着同步监视器进入同步代码块了,当线程2执行到wait()
,它也开始等待。
所以两个线程都处于等待状态,只能输出两个数字,然后进入了阻塞状态。
4、唤醒
🎲如何不一直等待下去呢?
Ctrl+P
可以看到wait()
方法还有带参的,可以设置等待时间,就是达到时间自动醒来。如下:
现在我们不使用这个了,因为想让它们交互输出,可以考虑唤醒线程。
线程1进入同步代码块执行到wait()
,然后释放同步监视器,线程2进入同步代码块。
所以我们需要在线程2wait()
之前把线程1给唤醒。
可以在这里写上notify()
,如下:
🍰分析一下现在的情况:
线程1进入同步代码块,碰到了notify()
,发现没有可以唤醒的线程,就继续往后执行。
然后线程1打印了“1”,接下来执行到wait()
,开始等待,并且释放同步监视器。
线程2拿到锁,进入同步代码块,碰到了notify()
,发现线程1在等待,就将线程1叫醒,虽然叫醒了,但是没有用,因为已经没有同步资源了。
线程2就拿着锁继续执行,即使sleep
也不影响自动睡醒之后继续往后执行(sleep不会导致锁被释放),然后输出“2”。
当线程2执行到wait()
,就开始等待了,同时也释放同步监视器。
此时线程1是醒着的状态,从被wait()
的地方继续往后执行,后边要是有代码的话还需要继续执行,然后拿着锁再次进入同步代码块,然后碰到notify()
,将线程2叫醒。
🌱代码
package yuyi04.Communication;/*** ClassName: PrintNumberTest* Package: yuyi04.Communication* Description:* 使用两个线程打印 1-100。线程1, 线程2 交替打印。* @Author 雨翼轻尘* @Create 2024/2/2 0002 14:57*/
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;@Overridepublic void run() {//打印数据while(true){synchronized (this) { //当前this表示PrintNumber的实例,即p,是唯一的notify();if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出(部分)
现在看到的就是交互的场景。
5、同步监视器
🗳️关于同步监视器
这里其实省略了this
,如下:
wait
同理:
notify(); //this.notify();
wait(); //this.wait();
凡是在方法当中,没有写是谁调用的,如果是非静态方法,那就少了this。
若是静态方法,那就是当前类。
这里的this
必须是一样的吗?如下:
我们先来自己定义一个同步监视器,然后将obj
放入:
🌱代码
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;Object obj=new Object();@Overridepublic void run() {//打印数据while(true){synchronized (obj) { //obj是唯一的,线程安全this.notify(); //notify();if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出
会报一个异常IllegalMonitorStateException
,这表示当前使用的同步监视器(即obj)和notify()
、wait()
方法的调用者不一致。
所以,它们的调用者,必须是同步监视器。(这里也可以看出,notify
和wait
方法必须要在同步代码块或同步方法中使用,只有这两个结构中才存在同步监视器,Lock里面没有同步监视器)
🚗处理
既然现在的同步监视器是obj,那么就用obj去调用notify
和wait
方法即可。
如下:
🌱代码
package yuyi04.Communication;/*** ClassName: PrintNumberTest* Package: yuyi04.Communication* Description:* 使用两个线程打印 1-100。线程1, 线程2 交替打印。* @Author 雨翼轻尘* @Create 2024/2/2 0002 14:57*/
public class PrintNumberTest {public static void main(String[] args) {//创建PrintNumber类的实例PrintNumber p=new PrintNumber();Thread t1=new Thread(p,"线程1");Thread t2=new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{//共享100private int number=1;Object obj=new Object();@Overridepublic void run() {//打印数据while(true){synchronized (obj) { //obj是唯一的,线程安全obj.notify(); //notify();if(number<=100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+number);number++;try {obj.wait(); //线程一旦执行此方法,就进入等待状态(阻塞),同时会释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else{break;}}}}
}
🍺输出(部分)
notify
和wait
方法的调用者必须是“同步监视器”。
(2)调用wait和notify需注意的细节
上述案例涉及到三个方法的使用:
wait()
:线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用。notify()
:一旦执行此方法,就会唤醒被wait()
的线程中优先级最高的那一个线程。(如果被wait()
的多个线程的优先级相同,则随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程。
🗳️注意点
- 此三个方法的使用,必须是在同步代码块或同步方法(即
synchronized
结构)中。(超纲:Lock
需要配合Condition
实现线程间的通信,方式更加灵活) - 此三个方法的调用者,必须是同步监视器。否则,会报
IllegalMonitorStateException
异常。 - 此三个方法声明在Object类中。(当初说同步监视器的时候,说到“任何一个对象都可以来充当同步监视器”,那么就意味着任何一个对象都应该有能力去调用这几个方法,这几个方法必定是定义在Object类里面的)
☕调用wait
和notify
需注意的细节总结
1、wait
方法与notify
方法必须要由同一个锁对象调用
。
因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
2、wait
方法与notify
方法是属于Object类的方法的。
因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
3、wait
方法与notify
方法必须要在同步代码块
或者是同步函数
中使用。
因为:必须要通过锁对象
调用这2个方法。否则会报java.lang.IllegalMonitorStateException
异常。
三、wait与sleep的区别
🎲wait()
和 sleep()
的区别?(常见面试题)
①相同点:一旦执行,都会使得当前线程结束执行状态,进入阻塞状态。
②不同点:
- 声明的位置(定义方法所属的类)
wait()
:声明在Object类中。sleep()
:声明在Thread类中,静态的。
- 使用的场景不同(使用范围不同)
wait()
:只能使用在同步代码块或同步方法中。sleep()
:可以在任何需要使用的场景。
- 都在同步结构(同步代码块或同步方法)中使用的时候,是否释放同步监视器的操作不同
wait()
:一旦执行,会释放同步监视器。sleep()
:一旦执行,不会释放同步监视器。
- 结束阻塞的方式(结束等待的方式不同)
wait()
:到达指定时间自动结束阻塞 或 无限等待直到被notify/notifyAll唤醒,结束阻塞。sleep()
:到达指定时间自动结束阻塞。
相关文章:

Java多线程--线程间的通信
文章目录 一、线程间的通信(1)为什么要处理线程间的通信(2)等待唤醒机制 二、案例(1)案例1、创建线程2、解决线程安全问题3、等待4、唤醒5、同步监视器 (2)调用wait和notify需注意的…...

vue + element 页面滚动计算百分比 + 节流函数
html: <el-progress :percentage"scrollValue"></el-progress> js: data() {return {scrollValue: 0,} }, mounted() {window.addEventListener(scroll, this.handleScroll) // 监听页面滚动 }, beforeDestroy() {window.remov…...

【笔记】React Native实战练习(仿网易云游戏网页移动端)
/** * 如果系统看一遍RN相关官方文档,可能很快就忘记了。一味看文档也很枯燥无味, * 于是大概看了关键文档后,想着直接开发一个Demo出来,边学边写,对往后工作 * 开发衔接上能够更顺。这期间肯定会遇到各种各样的问题&a…...

Android SystemUI 介绍
目录 一、什么是SystemUI 二、SystemUI应用源码 三、学习 SystemUI 的核心组件 四、修改状态与导航栏测试 本篇文章,主要科普的是Android SystemUI , 下一篇文章我们将介绍如何把Android SystemUI 应用转成Android Studio 工程项目。 一、什么是Syst…...

2024美赛数学建模A题思路分析 - 资源可用性和性别比例
1 赛题 问题A:资源可用性和性别比例 虽然一些动物物种存在于通常的雄性或雌性性别之外,但大多数物种实质上是雄性或雌性。虽然许多物种在出生时的性别比例为1:1,但其他物种的性别比例并不均匀。这被称为适应性性别比例的变化。例…...

2024年数学建模美赛C题(预测 Wordle)——思路、程序总结分享
1: 问题描述与要求 《纽约时报》要求您对本文件中的结果进行分析,以回答几个问题。 问题1:报告结果的数量每天都在变化。开发一个模型来解释这种变化,并使用您的模型为2023年3月1日报告的结果数量创建一个预测区间。这个词的任何属性是否会…...

TryHackMe-File Inclusion练习
本文相关的TryHackMe实验房间链接:TryHackMe | Why Subscribe 路径遍历(目录遍历) LocationDescription/etc/issue包含要在登录提示之前打印的消息或系统标识。/etc/profile控制系统范围的默认变量,例如导出(Export)变量、文件创…...

Leetcode 《面试经典150题》169. 多数元素
题目 给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的,并且给定的数组总是存在多数元素。 示例 1: 输入:nums [3,2,3] 输出:3示…...

百度输入法往选字框里强塞广告
关注卢松松,会经常给你分享一些我的经验和观点。 国内几乎100%的输入法都有广告,只是你们没发现而已!!! 百度输入法居然在输入法键盘上推送广告,近日,博主阑夕 表示,V2EX论坛上有…...

分享一个Qt使用的模块间通信类
需求: 不同线程,或者同一线程的不同类之间通信,按照Qt的机制,定义一个信号,一个槽,然后绑定。以两个类A,B为例,A触发一个信号,B执行一个槽,在定义好信号和槽之后&#x…...

工作七年,对消息推送使用的一些经验和总结
前言:不管是APP还是WEB端都离不开消息推送,尤其是APP端,push消息,小信箱消息;WEB端的代办消息等。因在项目中多次使用消息推送且也是很多项目必不可少的组成部分,故此总结下供自己参考。 一、什么是消息推…...

计网——应用层
应用层 应用层协议原理 网络应用的体系结构 客户-服务器(C/S)体系结构 对等体(P2P)体系结构 C/S和P2P体系结构的混合体 客户-服务器(C/S)体系结构 服务器 服务器是一台一直运行的主机,需…...

算法面试八股文『 基础知识篇 』
博客介绍 近期在准备算法面试,网上信息杂乱不规整,出于强迫症就自己整理了算法面试常出现的考题。独乐乐不如众乐乐,与其奖励自己,不如大家一起嗨。以下整理的内容可能有不足之处,欢迎大佬一起讨论。 PS:…...

docker-学习-4
docker学习第四天 docker学习第四天1. 回顾1.1. 容器的网络类型1.2. 容器的本质1.3. 数据的持久化1.4. 看有哪些卷1.5. 看卷的详细信息 2. 如何做多台宿主机里的多个容器之间的数据共享2.1. 概念2.2. 搭NFS服务器实现多个容器之间的数据共享的详细步骤2.3. 如果是多台机器&…...

el-upload子组件上传多张图片(上传为files或base64url)
场景: 在表单页,有图片需要上传,表单的操作行按钮中有上传按钮,点击上传按钮。 弹出el-dialog进行图片的上传,可以上传多张图片。 由于多个表单页都有上传多张图片的操作,因此将上传多图的el-upload定义…...

2024美赛数学建模C题思路源码——网球选手的动量
这题挺有意思,没具体看比赛情况,打过比赛的人应该都知道险胜局(第二局、第五局逆转局)最影响心态的,导致第3、5局输了 模型结果需要证明这样的现象 赛题目的 赛题目的:分析网球球员的表现 问题一.球员在比赛特定时间表现力 问题分析 excel数据:每个时间段有16场比赛,…...

金三银四_程序员怎么写简历_写简历网站
你们在制作简历时,是不是基本只关注两件事:简历模板,还有基本信息的填写。 当你再次坐下来更新你的简历时,可能会发现自己不自觉地选择了那个“看起来最好看的模板”,填写基本信息,却没有深入思考如何使简历更具吸引力。这其实是一个普遍现象:许多求职者仍停留在传统简历…...

echarts条形图添加滚动条
效果展示: 测试数据: taskList:[{majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {maj…...

Java 使用Soap方式调用WebService接口
pom文件依赖 <dependencies><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.13.0</version></dependency><!-- https://mvnrepository.com/artif…...

2024美赛数学建模所有题目思路分析
美赛思路已更新,关注后可以获取更多思路。并且领取资料 C题思路 首先,我们要理解势头是什么。简单来说,势头是一方在比赛中因一系列事件而获得的动力或优势。在网球中,这可能意味着连续赢得几个球,或是在比赛的某个关…...

Docker容器引擎(5)
目录 一.docker-compose docker-compose的三大概念: yaml文件格式: json文件格式: docker-compose 配置模板文件常用的字段: 二.Docker Compose 环境安装: 查看版本: 准备好nginx 的dockerfile的文…...

百分点科技:《数据科学技术: 文本分析和知识图谱》
科技进步带来的便利已经渗透到工作生活的方方面面,ChatGPT的出现更是掀起了新一波的智能化浪潮,推动更多智能应用的涌现。这背后离不开一个朴素的逻辑,即对数据的收集、治理、建模、分析和应用,这便是数据科学所重点研究的对象——…...

LabVIEW传感器通用实验平台
LabVIEW传感器通用实验平台 介绍了基于LabVIEW的传感器实验平台的开发。该平台利用LabVIEW图形化编程语言和多参量数据采集卡,提供了一个交互性好、可扩充性强、使用灵活方便的传感器技术实验环境。 系统由硬件和软件两部分组成。硬件部分主要包括多通道数据采集卡…...

向日葵企业“云策略”升级 支持Android 被控策略设置
此前,贝锐向日葵推出了适配PC企业客户端的云策略功能,这一功能支持管理平台统一修改设备设置,上万设备实时下发实时生效,很好的解决了当远程控制方案部署后,想要灵活调整配置需要逐台手工操作的痛点,大幅提…...

51单片机通过级联74HC595实现倒计时秒表Protues仿真设计
一、设计背景 近年来随着科技的飞速发展,单片机的应用正在不断的走向深入。本文阐述了51单片机通过级联74HC595实现倒计时秒表设计,倒计时精度达0.05s,解决了传统的由于倒计时精度不够造成的误差和不公平性,是各种体育竞赛的必备设…...

深信服技术认证“SCCA-C”划重点:深信服云计算关键技术
为帮助大家更加系统化地学习云计算知识,高效通过云计算工程师认证,深信服特推出“SCCA-C认证备考秘笈”,共十期内容。“考试重点”内容框架,帮助大家快速get重点知识。 划重点来啦 *点击图片放大展示 深信服云计算认证(…...

Redis stream特性了解
在发布订阅中我们了解到发布订阅模式存在的无法持久化保存消息和对于离线重连的客户端不能读取历史消息的缺陷,以下就来了解一下stream是如何解决这个问题的 steam是类似于仅添加log的数据结构,提供了以下基本命令 XADD: 添加新条目到stream # 语法xadd…...

苍穹外卖项目可以写的简历和如何优化简历
文章目录 重点写中规写添加自己个性的项目面试会问道的问题 我是一名双非大二计算机本科生,希望我的分享对你有帮助,点赞关注不迷路。 简历编写一直是很多人求职人的心病,我自己上学期有一门课程是去校内企业面试,当时我就感受出…...

C++:智能指针
C在用引用取缔掉指针的同时,模板的引入带给了指针新的发挥空间 智能指针简单的来说就是带有不同特性和内存管理的指针模板 unique_ptr 1.不能有多个对象指向一块内存 2.对象释放时内部指针指向地址也随之释放 3.对象内数据只能通过接口更改绑定 4.对象只能接收右值…...

用户界面(UI)、用户体验(UE)和用户体验(UX)的差异
对一个应用程序而言,UX/UE (user experience) 设计和 UI (user interface) 设计非常重要。UX设计包括可视化布局、信息结构、可用性、图形、互动等多个方面。UI设计也属于UX范畴。正是因为三者在一定程度上具有重叠的工作内容,很多从业多年的设计师都分不…...