【并发编程】线程的基础概念

一、基础概念

1.1 进程与线程A

什么是进程?

进程是指运行中的程序。 比如我们使用钉钉,浏览器,需要启动这个程序,操作系统会给这个程序分配一定的资源(占用内存资源)。

什么线程?

线程是CPU调度的基本单位,每个线程执行的都是某一个进程的代码的某个片段。

举个栗子:房子与人

比如现在有一个100平的房子,这个方式可以看做是一个进程

房子里有人,人就可以看做成一个线程。

人在房子中做一个事情,比如吃饭,学习,睡觉。这个就好像线程在执行某个功能的代码。

所谓进程就是线程的容器,需要线程利用进程中的一些资源,处理一个代码、指令。最终实现进程锁
预期的结果。

进程和线程的区别:

  • 根本不同:进程是操作系统分配的资源,而线程是CPU调度的基本单位。
  • 资源方面:同一个进程下的线程共享进程中的一些资源。线程同时拥有自身的独立存储空间。进程之间的资源通常是独立的。
  • 数量不同:进程一般指的就是一个进程。而线程是依附于某个进程的,而且一个进程中至少会有一个或多个线程。
  • 开销不同:毕竟进程和线程不是一个级别的内容,线程的创建和终止的时间是比较短的。而且线程之间的切换比进程之间的切换速度要快很多。而且进程之间的通讯很麻烦,一般要借助内核才可以实现,而线程之间通讯,相当方便。
1.2 多线程

什么是多线程?

多线程是指:单个进程中同时运行多个线程。

多线程的目的是为了提高CPU的利用率。

可以通过避免一些网络IO或者磁盘IO等需要等待的操作,让CPU去调度其他线程。

这样可以大幅度的提升程序的效率,提高用户的体验。

比如Tomcat可以做并行处理,提升处理的效率,而不是一个一个排队。

比如要处理一个网络等待的操作,开启一个线程去处理需要网络等待的任务,让当前业务线程可以继续往下执行逻辑,效率是可以得到大幅度提升的。

多线程的局限

  • 如果线程数量特别多,CPU在切换线程上下文时,会额外造成很大的消耗。
  • 任务的拆分需要依赖业务场景,有一些异构化的任务,很难对任务拆分,还有很多业务并不是多线程处理更好。
  • 线程安全问题:虽然多线程带来了一定的性能提升,但是再做一些操作时,多线程如果操作临界资源,可能会发生一些数据不一致的安全问题,甚至涉及到锁操作时,会造成死锁问题。
1.3 串行、并行、并发

什么是串行: 串行就是一个一个排队,第一个做完,第二个才能上。

什么是并行: 并行就是同时处理。(一起上!!!)

什么是并发: 这里的并发并不是三高中的高并发问题,这里是多线程中的并发概念(CPU调度线程的概念)。

CPU在极短的时间内,反复切换执行不同的线程,看似好像是并行,但是只是CPU高速的切换。

并行囊括并发。

并行就是多核CPU同时调度多个线程,是真正的多个线程同时执行。

单核CPU无法实现并行效果,单核CPU是并发。

1.4 同步异步、阻塞非阻塞

同步与异步:执行某个功能后,被调用者是否会主动反馈信息

阻塞和非阻塞:执行某个功能后,调用者是否需要一直等待结果的反馈。

两个概念看似相似,但是侧重点是完全不一样的。

  • 同步阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,需要一直等待水烧开。
  • 同步非阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能,但是需要时不时的查看水开了没。
  • 异步阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,需要一直等待水烧开。
  • 异步非阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能。

异步非阻塞这个效果是最好的,平时开发时,提升效率最好的方式就是采用异步非阻塞的方式处理一些多线程的任务。

二、线程的创建

线程的创建分为三种方式:

2.1 继承Thread类 重写run方法

启动线程是调用 start方法,这样会创建一个新的线程,并执行线程的任务。

如果直接调用run方法,这样会让当前线程执行run方法中的业务逻辑。

public class MyTest {public static void main(String[] args) {MyJob t1 = new MyJob();t1.start();for (int i = 0; i < 100; i++) {System.out.println("main:" + i);}}
}class MyJob extends Thread {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("MyJob:" + i);}}
}
2.2 实现Runnable接口 重写run方法
public class MyTest {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread t1 = new Thread(myRunnable);t1.start();for (int i = 0; i < 1000; i++) {System.out.println("main:" + i);}}
}class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println("MyRunnable:" + i);}}
}

最常用的方式:

  • 匿名内部类方式:
Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("匿名内部类:" + i);}}
});
  • Lambda方式:
Thread t2 = new Thread(() -> {for (int i = 0; i < 100; i++) {System.out.println("lambda:" + i);}
});
2.3 实现Callable 重写call方法,配合FutureTask

Callable一般用于有返回结果的非阻塞的执行方法

同步非阻塞

public class MyTest {public static void main(String[] args) throws ExecutionException, InterruptedException {//1. 创建MyCallableMyCallable myCallable = new MyCallable();//2. 创建FutureTask,传入CallableFutureTask futureTask = new FutureTask(myCallable);//3. 创建Thread线程Thread t1 = new Thread(futureTask);//4. 启动线程t1.start();//5. 做一些操作//6. 要结果Object count = futureTask.get();System.out.println("总和为:" + count);}
}class MyCallable implements Callable{ @Override public Object call() throws Exception {int count = 0;for (int i = 0; i < 100; i++) {count += i;}return count;}
}
2.4 基于线程池构建线程

追其底层,其实只有一种,实现Runnable

二、线程的使用

2.1 线程的状态

网上对线程状态的描述很多,有5种,6种,7种,都可以接受

5种状态一般是针对传统的线程状态来说(操作系统层面)
在这里插入图片描述

Java中给线程准备的6种状态
在这里插入图片描述

  • NEW:Thread对象被创建出来了,但是还没有执行start方法。
  • RUNNABLE:Thread对象调用了start方法,就为RUNNABLE状态(CPU调度/没有调度)
  • BLOCKEDWAITINGTIME_WAITING:都可以理解为是阻塞、等待状态,因为处在这三种状态下,CPU不会调度当前线程
  • BLOCKED:synchronized没有拿到同步锁,被阻塞的情况
  • WAITING:调用wait方法就会处于WAITING状态,需要被手动唤醒
  • TIME_WAITING:调用sleep方法或者join方法,会被自动唤醒,无需手动唤醒
  • TERMINATED:run方法执行完毕,线程生命周期到头了

在Java代码中验证一下效果

  • NEW
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {});System.out.println(t1.getState());
}
  • RUNNABLE
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while(true){}});t1.start();Thread.sleep(500);System.out.println(t1.getState());
}
  • BLOCKED
public static void main(String[] args) throws InterruptedException {Object obj = new Object();Thread t1 = new Thread(() -> {// t1线程拿不到锁资源,导致变为BLOCKED状态synchronized (obj){}});// main线程拿到obj的锁资源synchronized (obj) {t1.start();Thread.sleep(500);System.out.println(t1.getState());}
}
  • WAITING
public static void main(String[] args) throws InterruptedException {Object obj = new Object();Thread t1 = new Thread(() -> {synchronized (obj){try {  obj.wait();} catch (InterruptedException e){e.printStackTrace();}}});t1.start();Thread.sleep(500);System.out.println(t1.getState());
}
  • TIME_WAITING
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});t1.start();Thread.sleep(500);System.out.println(t1.getState());
}
  • TERMINATED
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}});t1.start();Thread.sleep(1000);System.out.println(t1.getState());
}
2.2 线程的常用方法
2.2.1 获取当前线程

Thread的静态方法获取当前线程对象

public static void main(String[] args) throws ExecutionException, InterruptedException {// 获取当前线程的方法Thread main = Thread.currentThread();System.out.println(main);// "Thread[" + getName() + "," + getPriority() + "," + group.getName() + "]";// Thread[main,5,main]
}
2.2.2 线程的名字

在构建Thread对象完毕后,一定要设置一个有意义的名称,方面后期排查错误

public static void main(String[] args) throws ExecutionException, InterruptedException {Thread t1 = new Thread(() -> {System.out.println(Thread.currentThread().getName());});t1.setName("模块-功能-计数器");t1.start();
}
2.2.3 线程的优先级

其实就是CPU调度线程的优先级、 java中给线程设置的优先级别有10个级别,从1~10任取一个整数。 如果超出这个范围,会排除参数异常的错误

public static void main(String[] args) throws ExecutionException, InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {System.out.println("t1:" + i);}});Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {System.out.println("t2:" + i);}});t1.setPriority(1);t2.setPriority(10);t2.start();t1.start();
}
2.2.4 线程的让步

可以通过Thread的静态方法yield,让当前线程从运行状态转变为就绪状态。

public static void main(String[] args) throws ExecutionException, InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 100; i++) {if(i == 50){Thread.yield();}System.out.println("t1:" + i);}});Thread t2 = new Thread(() -> {for (int i = 0; i < 100; i++) {System.out.println("t2:" + i);}});t2.start();t1.start();
}
2.2.5 线程的休眠

Thread的静态方法,让线程从运行状态转变为等待状态

sleep有两个方法重载:

  • 第一个就是native修饰的,让线程转为等待状态的效果
  • 第二个是可以传入毫秒和一个纳秒的方法(如果纳秒值大于等于0.5毫秒,就给休眠的毫秒值+1。如果传入的毫秒值是0,纳秒值不为0,就休眠1毫秒)

sleep会抛出一个InterruptedException

public static void main(String[] args) throws InterruptedException {System.out.println(System.currentTimeMillis());Thread.sleep(1000);System.out.println(System.currentTimeMillis());
}
2.2.6 线程的强占

Thread的非静态方法join方法

需要在某一个线程下去调用这个方法

如果在main线程中调用了t1.join(),那么main线程会进入到等待状态,需要等待t1线程全部执行完毕,在恢复到就绪状态等待CPU调度。

如果在main线程中调用了t1.join(2000),那么main线程会进入到等待状态,需要等待t1执行2s后,在恢复到就绪状态等待CPU调度。如果在等待期间,t1已经结束了,那么main线程自动变为就绪状态等待CPU调度。

public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println("t1:" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();for (int i = 0; i < 10; i++) {System.out.println("main:" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (i == 1){try {t1.join(2000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
2.2.7 守护线程

默认情况下,线程都是非守护线程

JVM会在程序中没有非守护线程时,结束掉当前JVM主线程默认是非守护线程,如果主线程执行结束,需要查看当前JVM内是否还有非守护线程,如果没有JVM直接停止

public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println("t1:" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t1.setDaemon(true);t1.start();
}
2.2.8 线程的等待和唤醒

可以让获取synchronized锁资源的线程通过wait方法进去到锁的等待池,并且会释放锁资源

可以让获取synchronized锁资源的线程,通过notify或者notifyAll方法,将等待池中的线程唤醒, 添加到锁池

notify随机的唤醒等待池中的一个线程到锁池

notifyAll将等待池中的全部线程都唤醒,并且添加到锁池

在调用wait方法和notify以及norifyAll方法时,必须在synchronized修饰的代码块或者方法内部才可以,因为要操作基于某个对象的锁的信息维护。

public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {sync();}, "t1");Thread t2 = new Thread(() -> {sync();},"t2");t1.start();t2.start();Thread.sleep(12000);synchronized (MyTest.class) {MyTest.class.notifyAll();}
}public static synchronized void sync() {try {for (int i = 0; i < 10; i++) {if(i == 5) {MyTest.class.wait();}Thread.sleep(1000);System.out.println(Thread.currentThread().getName());}} catch (InterruptedException e) {e.printStackTrace();}
}
2.3 线程的结束方式

线程结束方式很多,最常用就是让线程的run方法结束,无论是return结束,还是抛出异常结束,都可以

2.3.1 stop方法(不用)

强制让线程结束,无论你在干嘛,不推荐使用当然当然方式,但是,他确实可以把线程干掉

public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}});t1.start();Thread.sleep(500);t1.stop();System.out.println(t1.getState());
}
2.3.2 使用共享变量(很少会用)

这种方式用的也不多,有的线程可能会通过死循环来保证一直运行。

咱们可以通过修改共享变量在破坏死循环,让线程退出循环,结束run方法

static volatile boolean flag = true;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while(flag){// 处理任务}System.out.println("任务结束");});t1.start();Thread.sleep(500);flag = false;
}
2.3.3 interrupt方式

共享变量方式

public static void main(String[] args) throws InterruptedException {// 线程默认情况下, interrupt标记位:falseSystem.out.println(Thread.currentThread().isInterrupted());// 执行interrupt之后,再次查看打断信息Thread.currentThread().interrupt();// interrupt标记位:tureSystem.out.println(Thread.currentThread().isInterrupted());// 返回当前线程,并归位为false interrupt标记位:tureSystem.out.println(Thread.interrupted());// 已经归位了System.out.println(Thread.interrupted());// =====================================================Thread t1 = new Thread(() -> {while(!Thread.currentThread().isInterrupted()) {// 处理业务}System.out.println("t1结束");});t1.start();Thread.sleep(500);t1.interrupt();
}

通过打断WAITING或者TIMED_WAITING状态的线程,从而抛出异常自行处理

这种停止线程方式是最常用的一种,在框架和JUC中也是最常见的

public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while(true){// 获取任务// 拿到任务,执行任务// 没有任务了,让线程休眠try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();System.out.println("基于打断形式结束当前线程");return;}}});t1.start();Thread.sleep(500);t1.interrupt();
}

wait和sleep的区别?

  • sleep属于Thread类中的static方法、wait属于Object类的方法
  • sleep属于TIMED_WAITING,自动被唤醒、wait属于WAITING,需要手动唤醒。
  • sleep方法在持有锁时,执行,不会释放锁资源、wait在执行后,会释放锁资源。
  • sleep可以在持有锁或者不持有锁时,执行。 wait方法必须在只有锁时才可以执行。

wait方法会将持有锁的线程从owner扔到WaitSet集合中,这个操作是在修改ObjectMonitor对象,如果没有持有synchronized锁的话,是无法操作ObjectMonitor对象的。

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

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

相关文章

如何批量给照片添加圆角?为什么要给照片添加圆角?

公司在对外宣传过程中&#xff0c;往往要要求图片修成圆角。比如在网上公司&#xff0c;就下达过这样的任务&#xff0c;在短时间内将公司所有的物品图片都修成圆角。遇到这种情况哪怕是用专业的PS工具&#xff0c;解决这个问题也是非常麻烦的。那么图片怎么修成圆角又快又高效…

linux离线安装maven

一、下载maven 地址&#xff1a;Maven – Download Apache Maven 使用root权限用户登录服务器 cd /opt sudo mkdir maven cd maven 二、上传maven 使用Xftp工具 三、解压并配置环境变量 tar -zxvf tar -zxvf apache-maven-3.9.6-bin.tar.gz cd apache-maven-3.9.6/ 看到解压…

【Hadoop大数据技术】——Hive数据仓库(学习笔记)

&#x1f4d6; 前言&#xff1a; Hive起源于Facebook&#xff0c;Facebook公司有着大量的日志数据&#xff0c;而Hadoop是实现了MapReduce模式开源的分布式并行计算的框架&#xff0c;可轻松处理大规模数据。然而MapReduce程序对熟悉Java语言的工程师来说容易开发&#xff0c;但…

【CKA模拟题】综合案例演示如何创建pv和pvc

Useful Resources: Persistent Volumes & Claim 题干 For this question, please set this context (In exam, diff cluster name) kubectl config use-context kubernetes-adminkubernetesCreate a PersistentVolume (PV) and a PersistentVolumeClaim (PVC) using an e…

半导体实验用耐氢氟酸含氟塑料镊子金属杂质含量低

PFA镊子用于夹取小型片状、薄状、块状样品&#xff0c;广泛应用在半导体、新材料、新能源、原子能、石油化工、无线电、电力机械等行业。 具有耐高低温性&#xff08;可使用温度-200℃&#xff5e;&#xff0b;260℃&#xff09;、耐腐蚀、表面不粘性等特点&#xff0c;用于苛…

STM32嵌套中断向量控制器NVIC

一、嵌套终端向量控制器NVIC 1.1NVIC介绍 NVIC&#xff08;Nest Vector Interrupt Controller&#xff09;&#xff0c;嵌套中断向量控制器&#xff0c;作用是管理中断嵌套 先级。 核心任务是管理中断优 管理中断嵌套&#xff1a;我们在处理某个中断的过程中还没处理完这个中…

c++类和对象———拷贝构造和赋值运算符重载

衔接上一篇博客构造函数和析构函数c类和对象————构造函数和析构函数 目录 ​编辑 一、拷贝构造是什么&#xff1f; 二、拷贝构造 1.特点 2.代码解释拷贝构造参数类型&#xff08;重点&#xff09; 3.代码解释编译器默认拷贝构造 &#xff08;重点&#xff09; 4.构造函数、…

vite vue3 import.meta.glob动态路由

在Vite中使用Vue 3&#xff0c;你可以使用import.meta.glob来导入目录下的多个Vue组件&#xff0c;并自动生成路由。以下是一个简单的例子&#xff1a; router/index.js // router/index.js import { createRouter, createWebHistory } from vue-router;// 自动导入views目录下…

基于Spring Boot的在线学习系统的设计与实现

基于Spring Boot的在线学习系统的设计与实现 摘 要 在线学习系统是以大学传统线下教学方式不适应信息技术的迅速发展为背景&#xff0c;提高学习效率&#xff0c;解决传统教学问题&#xff0c;并且高效的实现教学信息化的一款软件系统。为了更好的实现对于教学和学生的管理&a…

词令外卖节红包天天神券每天领取直达入口

词令外卖节红包天天领直达入口 1、打开「词令」关键词口令直达微信小程序&#xff1b; 2、输入词令「外卖红包88」关键词直达口令&#xff1b; 3、搜索直达进入外卖红包天天领入口&#xff0c;即可成功领取外卖节红包和天天神券点外卖可享受券后价优惠&#xff1b; *温馨提醒&…

HTML5通过api实现拖放效果 dataTransfer对象

dataTransfer对象 说明&#xff1a;dataTransfer对象用于从被拖动元素向放置目标传递字符串数据。因为这个对象是 event 的属性&#xff0c;所以在拖放事件的事件处理程序外部无法访问 dataTransfer。在事件处理程序内部&#xff0c;可以使用这个对象的属性和方法实现拖放功能…

无药可医还能怎么办?越没本事的人,越喜欢从别人身上找原因!——早读(逆天打工人爬取热门微信文章解读)

无药可医的病该怎么办呢&#xff1f; 引言Python 代码第一篇 洞见 《骆驼祥子》&#xff1a;越没本事的人&#xff0c;越喜欢从别人身上找原因第二篇 人民日报 来啦 新闻早班车要闻社会政策 结尾 “吾日三省吾身&#xff0c;而后深知自助者天助之。” 在人生的迷宫中 遭遇困境时…

uniapp-打包IOS的APP流程

打包前所需配置 在manifest文件内配置 1. APP图标 2. 启动界面 有三种启动界面配置 第一种是 HBuilderX 官方给的通用启动界面&#xff0c;页面单一&#xff0c;屏幕中间就一个圆框图标 第二种是自定义的启动图&#xff0c;无法通过AppStore的审核 第三种是自定义storyboard启动…

论文研读:Transformers Make Strong Encoders for Medical Image Segmentation

论文&#xff1a;TransUNet&#xff1a;Transformers Make Strong Encoders for Medical Image Segmentation 目录 Abstract Introduction Related Works 各种研究试图将自注意机制集成到CNN中。 Transformer Method Transformer as Encoder 图像序列化 Patch Embed…

特殊数据类型

目录 记录类型 定义一个记录类型 myrecord_type&#xff0c;用于存储 emp 数据表中的员工姓名和职务 %TYPE 类型 定义一个变量&#xff0c;存储数据表 emp 中编号为 7369 的员工姓名&#xff0c;并且显示出结果 %ROWTYPE 类型 声明一个用于存储 emp 数据表中每行记录的变…

【力扣hot100】160.相交链表

相交链表 给你两个单链表的头节点 headA和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 题目数据 保证 整个链式结构中不存在环。 注意&#xff0c;函数返回…

牛客练习赛123 A~C

A.炸鸡块哥哥的粉丝题 输出字符串的前 ⌈ n 2 ⌉ \lceil \frac{n}{2} \rceil ⌈2n​⌉ 个字符 void solve() {int n;string s;cin >> n >> s;cout << s.substr(0, (n 1) / 2); }B.智乃想考一道鸽巢原理 当小球总个数为奇数时&#xff0c;贪心的留下 1 个…

天梯算法Day3整理

浮点数解析 炸鱼题掠过 冲突值 题面 解析 方法一 —— 并查集 按照边值排序&#xff0c;然后按边值从大到小遍历&#xff0c;通过并查集判断能否将所有点无冲突地归于两个集合。在判断时&#xff0c;若有两个点不得不产生冲突&#xff0c;则输出这两个点之间的边值并结束。…

LeetCode Python - 81. 搜索旋转排序数组 II

目录 题目描述解法运行结果 题目描述 已知存在一个按非降序排列的整数数组 nums &#xff0c;数组中的值不必互不相同。 在传递给函数之前&#xff0c;nums 在预先未知的某个下标 k&#xff08;0 < k < nums.length&#xff09;上进行了 旋转 &#xff0c;使数组变为 […

好用还平价的挂耳式耳机有哪些?五款超平价品牌测评推荐

在数字化时代&#xff0c;耳机已经成为我们日常生活中不可或缺的一部分。而开放式耳机作为一种新兴趋势&#xff0c;以其独有的开放性设计和卓越的音质表现&#xff0c;正在悄然改变着人们的听音习惯。不同于传统的耳机产品&#xff0c;开放式耳机让音乐与外界环境相得益彰&…