了解多线程

news2024/11/14 11:32:00

1.线程与并发

1.1 理解进程和线程的区别

进程:是指一个内存中运行的应用程序(程序的一次运行就产生一个进程),每个进程都有自己独立的一块内存空间,比如在Windows的任务管理器中,一个运行的xx.exe就是一个进程。

多进程操作系统

假定 A,B,C 三个程序开始运行,ABC 产生进程 PaPbPcPaPbPc 排队轮流使用 CPU。假

 Pa 先抢占到 CPUPa 执行,执行过程中 Pa 需要等待数据输入(I/O 操作(例如用户输入)、请求网络资源), 此时 CPU 空转,为了提高 CPU 利用率,Pa 被切换出去,Pa 保存当前执行状态,Pa 挂起。Pb 抢占 CPUPb 开始执行,Pb 如果没有数据输入,Pb 也可能被切换出去(CPU 时间片到了)Pb 挂起;Pc 抢占到 CPU,开始执行,如果 Pc 有数据输入,Pc 保存当前状态并挂起。此时 3 个进程都挂起,CPU 空闲。CPU 挑选一个进程运行,根据 CPU 执行原则,选中 PbPb 继续执行。

CPU 通过时间片实现多任务,这样的操作系统称为多任务操作系统,但同一时刻还是只有一个进程执行。

并行和并发

并行:同一时间点执行多个任务

并发:同一时间段中执行多个任务

线程:是指进程中的一个执行任务(控制单元),一个进程中可以运行多个线程,多个线程可共享进程的数据。

线程的出现为了解决实时性问题。

线程是进程的细分,通常,在实时性操作系统中,进程会被划分为多个可以独立运行的子任务, 这些子任务被称为线程,多个线程配合完成一个进程的任务。

假设 P 进程抢占 CPU 后开始执行,此时如果 P 进行正在进行获取网络资源的操作时,用户进行 UI 操作,此时 P 进程不会响应 UI 操作。可以把 P 进程可以分为 TaTb 两个线程。Ta 用于获取网络资源, Tb 用于响应 UI 操作。此时如果 Ta 正在执行获取网络资源时、用户进行 UI 操作, 为了做到实时性,Ta线程暂时挂起,Tb 抢占 CPU 资源,执行 UI 操作,UI 操作执行完成后让出CPUTa 抢占 CPU 资源继续执行请求网络资源。

多线程:在同一个进程中并发运行的多个子任务。

一个进程至少有一个线程,为了提高CPU的效率,可以在一个进程中开启多个控制单元,这就是多线程。

1.2. 主线程 main

在运行一个简单的Java程序的时候,就已经存在了两个线程,一个是主线程,一个是后台线程——维护的垃圾回收。主线程很特殊,在启动JVM的时候自动启动的。

如果一个进程没有任何线程,我们成为单线程应用程序;如果一个进程有多个线程存在, 我们成为多线程应用程序。进程执行时一定会有一个主线程(main 线程)存在,主线程有能力创建其他线程。

多个线程抢占CPU,导致程序的运行轨迹不确定。多线程的运行结果也不确定,多线程的程序复杂度提高很多。

1.3. 线程的创建和启动

方式一,继承Thread类:

 自定义类继承Thread

 覆写run方法

 创建自定义类对象

 自定义类对象调用start方法

class MyThread extends Thread {

public void run() {

//线程体,线程启动时,会自动调用本方法,所有这里是我们写代码的主体部分

}

}

public class ExceptionDemo {

public static void main(String[] args) {

MyThread t = new MyThread();

t.start();// 调用Threadstart方法,JVM会自动调用run方法。

}

}

方式二,实现Runnable 接口

 自定义类实现Runnable接口

 覆写run方法

 创建自定义类对象

public void run() {

//线程体,线程启动时,会自动调用本方法,所有这里是我们写代码的主体部分

}

}

public class ThreadDemo2 {

public static void main(String[] args) {

MyRunnable target = new MyRunnable();

Thread t = new Thread(target);

t.start();

}

}

第一种使用起来方便,启动一个线程也方便,很多功能都在Thread类中定义好了;

第二种方式启动得依赖于Thread,因为本身Runnable中只有run方法,请看Thread的构造方法。后期的功能拓展有优势

 线程体-run方法

不管哪种方式创建的线程,都得覆写run 方法,因为这是线程体方法,该方法在线程启动之后会自动被调用

public void run() {

//线程体,线程启动时,会自动调用本方法,所有这里是我们写代码的主体部分

}

线程的执行随机性:

一旦一个线程启动之后就是一个独立的线程,等待CPU的调度分配资源,不会因为启动它的外部线程结束而结束。

class MyThread extends Thread {

public void run() {

//自定义线程中的for循环打印i,打印顺序是完全随机的。

for (int i = 0; i < 10; i++) {

System.out.println("MyThread ==> " + i);

}

}

}

public class Demo {

public static void main(String[] args) {

MyThread mt = new MyThread();

mt.start();

//主线程中的for循环打印i

for (int i = 0; i < 10; i++) {

System.out.println("main ==> " + i);

}

}

}

多次运行该程序,观察每次运行的结果。

 线程的启动

启动线程必须调用线程类Thread中的start方法,该方法应该由Thread类的一个实例来调用,下面是方法签名:

public void start()

底层会调用该线程的 run 方法。

只有调用了线程对象的start方法才会开启一个新的线程,如果是直接调用对象的run方法不会开启新的线程,只是一个单线程。

注意:启动一个新线程,不能使用run()方法,只能使用start方法。

继承方式VS实现方式

当多线程并发访问同一个资源时,会导致线程出现安全性的原因,看案例。

案例:现有50张票,现在有三个窗口(ABC)卖这50张票。

因为ABC三个窗口可以同时卖票,此时得使用多线程技术来实现这个案例。

分析: 可以定义三个线程对象,并启动线程.

第一步:每一个窗口买票的时候:展示自己买出一张票,

第二步:还剩xx张票

使用继承方式

public class TicketWindow extends Thread{

private int count = 50;

public TicketThread(String name) {

super(name);

}

@Override

public void run() {

  • 模拟10个人买票

for( int i = 0;i < 10;i++ ){

if( TicketThread.count > 0 ){

count--;

System.out.println(super.getName() + "卖出一张票,还剩" + count +

"");

}

}

}

}

public class Test01Ticket {

public static void main(String[] args) {

  • 模拟买票过程。共有 5 张票,多线程模拟卖票的过程。

TicketWindow ta = new TicketWindow("窗口A"); 

TicketWindow tb = new TicketWindow("窗口B"); 

TicketWindow tc = new TicketWindow("窗口C");

ta.start();

tb.start();

tc.start();

}

}

使用继承方式完成该案例的时候,会发现ABC都各自卖了50张票,为何?

使用实现方式

public class Ticket implements Runnable{

private int count = 5;

@Override

public void run() {

  • 模拟10个人买票

for( int i = 0;i < 10;i++ ){

if( this.count > 0 ){

count--;

System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + count + "")

}

}

}

}

-----------------------------------------------

public class Test01Ticket {

public static void main(String[] args) {

  • 1>创建一个Runnable实现类 Ticket ticket = new Ticket();
  • 2> myRun对象的线程体(run方法)跑在4个线程中

Thread ta = new Thread(ticket,"窗口A"); 

Thread tb = new Thread(ticket,"窗口B"); 

Thread tc = new Thread(ticket,"窗口C");

ta.start();

tb.start();

tc.start();

}

}

在使用实现方式的时候,我们发现ABC一共卖了50张票,为何?

通过买票案例,分析继承方式和实现方式的区别:

继承方式:

  Java中类是单继承的,如果继承了Thread了,该类就不能再有其他的直接父类了。

 从操作上分析,继承方式更简单,获取线程名字也简单。

 从多线程共享同一个资源上分析,继承方式不能多个线程共享同一个资源。

实现方式:

  Java中类可以实现多接口,此时该类还可以继承其他类,并且还可以实现其他接口(设计上,更优

雅)。

 从操作上分析,获取线程名字也比较复杂,得使用Thread.currentThread()来获取当前线程的引用。

 从多线程共享同一个资源上分析,实现方式可以多线程共享同一个资源。

1.4. 线程生命周期和状态

 新生状态:用 new 关键字建立一个线程后,该线程对象就处于新生状态。

处于新生状态的线程有自己的内存空间,通过调用 start()方法进入就绪状态。

 就绪状态

处于就绪状态线程具备了运行条件,但还没分配到 CPU,处于线程就绪队列,等待系统为其分配CPU。当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称为“CPU 调度

 运行状态

在运行状态的线程执行自己的run 方法中代码,直到等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态(就绪)。

 阻塞状态

处于运行状态的线程在某些情况下,如执行了 sleep(睡眠)方法,或等待 I/O 设备等资源,将让出 CPU 并暂时停止自己运行,进入阻塞状态。

在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的 I/O 设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续执行。

 死亡状态

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个,一个是正常运行的线程完成了它的全部工作;二是线程抛出未捕获的ExceptionError,三是线程被强制性地终止,如通过 stop 方法来终止一个线程【易导致死锁,不推荐】

1.5. 操作线程的方法

 join方法

join方法的主要作用就是同步,它可以使得线程之间的并发执行变为串行执行。

比如在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。

public class JoinThread extends Thread{

public JoinThread() {}

public JoinThread(String name) {

super(name);

}

@Override

public void run() {

for(int i = 0;i < 10;i++){

System.out.println(super.getName() + i);

}

}

}

测试join方法

线程Ajoin方法表示线程A的强制执行,其他线程都阻塞,直到线程A执行完成,其他线程才会被执行。

sleep方法

sleep方法让正在执行的线程暂停一段时间,进入阻塞状态,常常用来模拟网络延迟,或者耗时操作等。

sleep(long milllis) throws InterruptedException:毫秒为单位

调用sleep()后,在指定时间段之内,该线程不会获得执行的机会

public class SleepThread extends Thread{

public SleepThread() {}

public SleepThread(String name) {

super(name);

}

@Override

public void run() {

System.out.println("线程即将执行");

try {

Thread.sleep(10000);

} catch (InterruptedException e) { System.out.println("我被中断了");

}

System.out.println("线程完成");

}

}

测试 sleep 方法

public class SleepDemo {

public static void main(String[] args) throws InterruptedException{

SleepThread ta = new SleepThread("线程A");

ta.start();

System.out.println("主线程进入休眠5s");

Thread.sleep(5000);

System.out.println("5s后主线程自动唤醒");

  • 企图去中断ta线程
  • ta.interrupt();

}

}

 线程的优先级

每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关。并不是说优先级高的就一定先执行,哪个线程的先运行取决于CPU的调度;

Thread对象的setPriority(int x)getPriority()用来设置和获得优先级。

 后台线程

所谓后台线程,一般用于为其他线程提供服务。也称为守护线程。JVM的垃圾回收就是典型的后台线程。

特点:若所有的前台线程都死亡,后台线程自动死亡。

Thread对象setDaemon(true)用来设置后台线程。

setDaemon(true)必须在start()调用前,否则抛IllegalThreadStateException异常。

2.线程安全性

2.1. 线程同步

当多线程并发访问同一个资源对象(共享资源)的时候,可能出现线程不安全的问题。

但是,分析打印的结果,有时候发现没有问题:

意识:看不到问题,不代表没有问题,可能是我们经验不够,或者说问题出现的不够明显。

那么可以使用线程休眠来模拟网络延迟,让问题来得更明显一些:

Thread.sleep(10);//当前线程睡10毫秒,当前线程休息着,让其他线程去抢资源.

在程序中并不是使用Thread.sleep(10)之后程序才出现问题,而是使用之后,问题更明显,休眠的时间越久问题越明显,一般用10100即可,具体根据情况而定。

public class MyRun implements Runnable {

private int count = 50;

@Override

public void run() {

  • 模拟10个人买票

for (int i = 0; i < 10; i++) {

  • 模拟询问过程(耗时操作) try {

Thread.sleep(100);

} catch (InterruptedException e) { e.printStackTrace();

}

if (this.count > 0) {

count--;

System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + count + "");

}

}

}

在这里,总数减1操作和打印输出剩余操作,应该是一个原子操作,也就说是一个不能分割的操作,两个步骤之间不能被其他线程插一脚。

对于原子性操作,要么都不执行,要么都执行完成,0% / 100%

第一步:count--;

第二步:System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + count + "");

解决方案:保证票总数减1操作和打印输出剩余操作,必须同步完成。

解决思路:A线程获得同步锁进入操作的时候,BC线程只能在外等着,A操作结束,释放同步锁。ABC才有机会去抢同步锁(谁获得同步锁,谁才能执行代码)。

通俗例子:ABC三个人去抢厕所的雅间,为了保证安全规定谁抢到了必须上锁,把其他人排除外雅间外面。若A抢到了,进入后应该立马上锁,BC只能在外等着,当A释放锁出来的时候,ABC又开始尝试抢资源。

 方式1:同步代码块

 方式2:同步方法

同步代码块

同步代码块语法:

synchronized(同步锁){

  • 需要同步操作的代码

}

同步锁,又称之为同步监听对象/同步锁/同步监听器/互斥锁:

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。

Java程序允许使用任何对象作为同步监听对象,一般的,我们把当前并发访问的共享资源作为同步监听对象,比如此时三个线程的共同资源Ticket对象。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就执行,其他的线程只能在代码块外等着。

public class Ticket implements Runnable {

private int count = 50;

@Override

public void run() {

  • 模拟10个人买票

for (int i = 0; i < 10; i++) {

synchronized (this) {

  • 业务上不可分割的逻辑单元 try {

Thread.sleep(1000);

} catch (InterruptedException e) { e.printStackTrace();

}

if (this.count > 0) {

count--;

System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + count + "");

}

}

}

}

}

此时的同步锁this表示Ticket对象,而程序中Ticket对象只有一份,故可以作为同步锁。

同步方法

使用synchronized修饰的方法,就叫做同步方法。保证A线程执行该方法的时候,其他线程只能在方法外等着。

public synchronized void doWork(){

// TODO

}

此时同步锁是谁——其实就是,调用当前同步方法的对象:

 对于非static方法,同步锁就是this

 对于static方法,同步锁就是当前方法所在类的字节码对象。

class Ticket implements Runnable {

private int count = 50;

public void run() {

for (int i = 0; i < 50; i++) {

this.saleTicket();

}

}

public synchronized void saleTicket(){

  • 业务上不可分割的逻辑单元 try {

Thread.sleep(1000);

} catch (InterruptedException e) { e.printStackTrace();

}

if (this.count > 0) {

count--;

System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + count + "");

}

}

}

2.2 synchronized的优劣

好处:保证了多线程并发访问时的同步操作,避免线程的安全性问题。

缺点:使用synchronized的方法/代码块的性能要低一些。

建议:尽量减小synchronized的作用域。

面试题:

  • StringBuilderStringBuffer的区别
  • 说说ArrayListVector的区别
  • HashMapHashtable的区别

通过源代码会发现,主要就是方法有没有使用synchronized的区别,比如StringBuilderStringBuffer

因此得出结论:使用synchronized修饰的方法性能较低,但是安全性较高,反之则反。

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

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

相关文章

一个热门的源码整站数据打包完整代码(开箱即用),集成了最新有效数据和完美wordpress主题。

分享一个资源价值几千元的好代码资源网整站打包代码&#xff0c;这个wordpress网站基于集成了ripro9.1完全明文无加密后门版本定制开发&#xff0c;无需独立服务器&#xff0c;虚拟主机也可以完美运营&#xff0c;只要主机支持php和mysql即可。整合了微信登录和几款第三方的主题…

AGP7+ 适配 plugin 动态引入第三方插件

AGP4 适配前 def hwPlugin com.huawei.agconnectmProject.getPlugins().apply(hwPlugin)AGP7 适配后 在 AGP4 如果仍然使用上述代码&#xff0c;那么编译期会报错&#xff0c;升级版本之后使用下面的pluginManager 即可。 def hwPlugin com.huawei.agconnectmProject.plug…

Studio One安装教程+软件安装包下载

Studio One6全新版本上线 记录、生产、混合、掌握和执行所有操作。从工作室到舞台&#xff0c;Studio One6以易用为核心&#xff0c;是您的创意合作伙伴。 当你准备好登上舞台时&#xff0c;Studio One就在那里。只有Studio One从最初的灵感到完整的制作&#xff0c;最终混音…

3D感知视觉表示与模型分析:深入探究视觉基础模型的三维意识

在深度学习与大规模预训练的推动下&#xff0c;视觉基础模型展现出了令人印象深刻的泛化能力。这些模型不仅能够对任意图像进行分类、分割和生成&#xff0c;而且它们的中间表示对于其他视觉任务&#xff0c;如检测和分割&#xff0c;同样具有强大的零样本能力。然而&#xff0…

(三十八)Vue之插槽Slots

文章目录 插槽介绍插槽分类默认插槽具名插槽条件插槽动态插槽名 作用域插槽默认作用域插槽具名作用域插槽 上一篇&#xff1a;&#xff08;三十七&#xff09;vue 项目中常用的2个Ajax库 插槽介绍 在之前的文章中&#xff0c;我们已经了解到组件能够接收任意类型的值作为 prop…

【品质】如何培养幽默感,如何幽默的沟通与应对生活(自卑vs自信,悲观vs乐观)

【品质】如何培养幽默感&#xff0c;如何幽默和正能量的沟通与应对生活&#xff08;自卑vs自信&#xff0c;悲观vs乐观&#xff09; 文章目录 一、性格底色&#xff08;自我认知&#xff0c;世界观&#xff09;1、从悲观的底色开始2、用摆烂、自嘲的方式与世界和解 二、沟通方法…

2024050802-重学 Java 设计模式《实战模板模式》

重学 Java 设计模式&#xff1a;实战模版模式「模拟爬虫各类电商商品&#xff0c;生成营销推广海报场景」 一、前言 黎明前的坚守&#xff0c;的住吗&#xff1f; 有人举过这样一个例子&#xff0c;先给你张北大的录取通知书&#xff0c;但要求你每天5点起床&#xff0c;12点…

mysql和redis备份和恢复数据的笔记

一、mysql的备份及恢复方法&#xff1a; 1.完全备份与恢复 1.1物理备份与恢复 物理备份又叫冷备份&#xff0c;需停止数据库服务&#xff0c;适合线下服务器 备份数据流程&#xff1a; 第一步:制作备份文件 systemctl stop mysqld #创建存放备份文件的目录 mkdir /bakdir …

医疗行业携手用友BIP收入云,开启高效收入管理新时代

在医疗行业&#xff0c;收入管理是实现可持续发展的重要环节。随着医疗改革的深入和市场竞争的加剧&#xff0c;医疗机构需要寻找有效的收入管理破局方法。用友BIP收入云作为一款强大的收入管理工具&#xff0c;为医疗行业提供了有力的支持。 一、医疗行业收入管理破局方法 精细…

多视图变换矩阵与SLAM位姿估计中的地图点投影的几何约束

定义 Homography & projective transform M ( 3 4 ) [ f s x c ′ 0 a f y c ′ 0 0 1 ] [ 1 0 0 0 0 1 0 0 0 0 1 0 ] [ R 3 3 0 3 1 0 1 3 1 ] [ I 3 3 T 3 1 0 1 3 1 ] \underset{(3 \times 4)}{\mathbf{M}}\left[\begin{array}{ccc} f & s & x_c^{\pr…

前端已学习内容

一、HTMLCSS 1、黑马B站视频-27小时 地址&#xff1a;基础班导学-精讲与实战_哔哩哔哩_bilibili 说明&#xff1a;讲义已下载。两个小项目还没学没练。 2、菜鸟教程 地址&#xff1a;HTML 简介 | 菜鸟教程 二、JavaScript 1、菜鸟教程 网址&#xff1a;JavaScript 教程 …

【点击收藏】鸿蒙HarmonyOS实战开发—如何实现应用悬浮窗

前言 鸿蒙登场&#xff01;它的征途是万物互联 备受瞩目的华为HarmonyOS 2&#xff08;即鸿蒙系统&#xff09;正式发布。同时&#xff0c;华为发布了多款搭载鸿蒙系统的新产品&#xff0c;包括Mate 40系列新版本、Mate X2新版本、华为WATCH 3系列、华为MatePad Pro等手机、智能…

vue-editor设置字体font-family

背景&#xff1a;Vue项目中需要用到富文本编辑器&#xff0c;所以选择了vue-editor这个富文本编辑器&#xff0c;发现字体font-family只有三种Sans Serif、Serif、MonoSpace可以选择&#xff0c;满足不了产品的需求&#xff0c;所以用想要定义成常用字体&#xff0c;主要是需要…

AGI时代的奠基石:Agent+算力+大模型是构建AI未来的三驾马车吗

★AI Agent&#xff1b;人工智能体&#xff0c;RPA&#xff1b;大语言模型&#xff1b;prompt&#xff1b;Copilot&#xff1b;AGI&#xff1b;ChatGPT&#xff1b;LLM&#xff1b;AIGC&#xff1b;CoT&#xff1b;Cortex&#xff1b;Genius&#xff1b;MetaGPT&#xff1b;大模…

借助ChatGPT撰写学术论文,如何设定有效的角色提示词指

大家好&#xff0c;感谢关注。这个给大家提供关于论文写作方面专业的讲解&#xff0c;以及借助ChatGPT等AI工具如何有效辅助的攻略技巧。有兴趣的朋友可以添加我&#xff08;yida985&#xff09;交流学术写作或ChatGPT等AI领域相关问题&#xff0c;多多交流&#xff0c;相互成就…

段页式管理

缝合怪&#xff01;&#xff01;&#xff01; 分页、分段的对比 分段分页段页式管理 先将进程按逻辑模块分段&#xff0c;再将各段分页 段页式管理的逻辑地址结构 段号页号页内偏移量 段号的位数&#xff1a;决定了每个进程最多可以分为几个段。 页号的位数&#xff1a;决定…

马斯克怒了,禁止员工使用苹果设备,抨击库克出卖数据给OpenA

昨晚&#xff0c;苹果发布会正式宣布了一系列重磅AI升级&#xff0c;甚至创造了一个新的概念——苹果智能&#xff08;Apple Intelligence&#xff09;。 这次升级在操作系统的交互层面上进行了智能化改进&#xff0c;使得更多自然语音和语言理解的控制成为可能&#xff0c;将…

常见数据编码方式

数据编码方式&#xff1a; 二进制数字信息在传输过程中可以采用不同的代码&#xff0c;各种代码的抗噪声特征和定时功能各不相同&#xff0c;实现费用也不一样。下面介绍几种常用的编码方式。 1、单极性码 在这种编码方案中&#xff0c;只用正的&#xff08;或负的&#xff09;…

服务器如何远程桌面连接不上,服务器远程桌面连接不上解决办法

服务器远程桌面连接不上&#xff0c;是IT运维中常见的挑战之一。针对这一问题&#xff0c;专业的解决方法通常涉及以下几个方面的排查与操作&#xff1a; 首先&#xff0c;我们需要检查网络连接是否正常。远程桌面连接依赖于稳定的网络连接&#xff0c;因此&#xff0c;确认服务…

第十九节:暴力递归到动态规划

一 动画规划的概念 优化出现重复解的递归 一旦写出递归来&#xff0c;改动态规划就很快 尝试策略和状态转移方程是一码事 学会尝试是攻克动态规划最本质的能力 如果你发现你有重复调用的过程&#xff0c;动态规划在算过一次之后把答案记下来&#xff0c;下回在越到重复调用过程…