C++学习第三十一课:C++ 线程与多线程编程的应用

news2024/9/29 1:29:26

在这里插入图片描述

一、线程与多线程编程的基本概念

线程的定义

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

多线程编程的优势与挑战

优势:

  • 提高CPU的利用率
  • 提高程序的响应速度
  • 简化程序的结构

挑战:

  • 数据同步问题:多个线程访问共享数据时可能产生数据不一致的问题。
  • 线程安全问题:需要确保线程间的数据访问和操作是安全的。
  • 线程管理问题:创建、销毁和调度线程都需要消耗资源,过多线程可能导致性能下降。
C++11中线程编程的引入

C++11标准引入了<thread>库,为C++程序员提供了创建和管理线程的能力。使用<thread>库,程序员可以更加容易地编写出多线程的C++程序。

二、C++线程编程基础

引入<thread>头文件

在C++11中,要使用线程编程,首先需要包含<thread>头文件。

#include <thread>
std::thread类的使用

std::thread是C++11中用于表示和管理线程的类。通过创建一个std::thread对象并传入一个可调用的对象(如函数、函数对象、Lambda表达式等),可以启动一个新的线程。

线程的创建与启动

以下是一个简单的示例,展示如何创建并启动一个线程:

#include <iostream>
#include <thread>

void print_hello() {
    std::cout << "Hello from thread!\n";
}

int main() {
    std::thread t(print_hello); // 创建并启动线程
    t.join(); // 等待线程完成
    return 0;
}

在这个例子中,print_hello函数被一个新的线程t执行。主线程通过调用t.join()来等待新线程执行完毕。如果不调用join()detach(),当main函数结束时,新线程可能还未执行完毕,这会导致程序崩溃。

join()方法:等待线程完成

join()方法用于阻塞当前线程(通常是主线程),直到调用该方法的线程对象所代表的线程执行结束。在上述示例中,主线程通过调用t.join()来等待新线程执行完毕。

detach()方法:使线程在后台运行

detach()方法用于分离线程,使得新线程在后台运行,而当前线程(通常是主线程)可以继续执行。一旦线程被分离,就不能再调用join()方法,且新线程结束时会自动释放其占用的资源。

joinable()方法:检查线程是否可连接或分离

joinable()方法用于检查线程对象是否代表一个可连接的线程。如果线程对象是可连接的(即它代表一个活跃的线程,且该线程未被分离),则joinable()返回true;否则返回false

C++线程编程基础

一、C++标准库中的线程库

在C++11及以后的版本中,标准库引入了<thread>头文件,它包含了std::thread类和其他与线程相关的类和函数,使得C++程序员可以方便地进行多线程编程。

二、C++线程的基本操作

1. 创建线程

使用std::thread类的构造函数可以创建一个线程。构造函数接受一个可调用的对象(如函数、函数对象、Lambda表达式等)作为参数,这个可调用的对象将在新的线程中执行。

示例代码:

#include <iostream>
#include <thread>

void print_hello() {
    std::cout << "Hello from thread!\n";
}

int main() {
    std::thread t(print_hello); // 创建一个线程t,并执行print_hello函数

    // 等待线程执行完毕
    t.join();

    return 0;
}

在这个例子中,我们定义了一个名为print_hello的函数,然后在main函数中创建了一个std::thread对象t,并将print_hello函数作为参数传递给t的构造函数。这将在新的线程中执行print_hello函数。主线程通过调用t.join()来等待新线程执行完毕。

2. 分离线程

使用std::thread::detach成员函数可以将线程与创建它的线程分离,使它在后台运行。一旦线程被分离,就不能再调用joindetach,并且线程结束时会自动释放其占用的资源。

示例代码:

#include <iostream>
#include <thread>

void print_hello() {
    std::cout << "Hello from detached thread!\n";
}

int main() {
    std::thread t(print_hello);
    t.detach(); // 分离线程,使它在后台运行

    // 主线程可以继续执行其他任务,不需要等待分离后的线程
    std::cout << "Main thread continuing...\n";

    return 0;
}

在这个例子中,我们创建了一个线程t并执行print_hello函数,然后立即调用t.detach()将线程分离。主线程可以继续执行其他任务,而不需要等待分离后的线程完成。

3. 检查线程是否可连接或分离

使用std::thread::joinable成员函数可以检查线程对象是否代表一个可连接的线程。如果线程对象是可连接的(即它代表一个活跃的线程,且该线程未被分离),则joinable返回true;否则返回false

示例代码:

#include <iostream>
#include <thread>

void print_hello() {
    std::cout << "Hello from thread!\n";
}

int main() {
    std::thread t(print_hello);

    if (t.joinable()) {
        std::cout << "Thread is joinable, calling join...\n";
        t.join();
    } else {
        std::cout << "Thread is not joinable\n";
    }

    return 0;
}

在这个例子中,我们创建了一个线程t并执行print_hello函数。然后检查线程是否可连接,如果可连接则调用join等待其完成。由于我们还没有调用detach,所以线程是可连接的。如果在线程被分离后调用joinable,它将返回false

线程同步与互斥

一、线程同步与互斥的基本概念

在多线程编程中,线程同步与互斥是两个非常重要的概念。由于多个线程可能同时访问共享资源(如内存中的变量、文件、数据库等),如果没有适当的同步机制,就可能导致数据不一致、脏读、脏写等问题。线程同步与互斥就是为了解决这些问题而提出的。

线程同步

线程同步是指多个线程按照某种顺序或规则来访问共享资源,以确保数据的完整性和一致性。线程同步通常通过同步原语(如互斥锁、条件变量、信号量等)来实现。

线程互斥

线程互斥是指同一时刻只允许一个线程访问共享资源,其他线程必须等待当前线程释放资源后才能访问。线程互斥是线程同步的一种特殊情况,它通过互斥锁(如std::mutex)来实现。

二、C++中的互斥锁

C++11标准库提供了std::mutex类来实现互斥锁。std::mutex类提供了对共享资源的互斥访问,确保同一时刻只有一个线程可以访问共享资源。

1. std::mutex的基本使用

示例代码:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx; // 全局互斥锁
int counter = 0; // 共享资源

void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // 自动加锁和解锁
        ++counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Final counter value: " << counter << std::endl;

    return 0;
}

在这个例子中,我们定义了一个全局的互斥锁mtx和一个共享资源counter。两个线程t1t2都调用increment函数来增加counter的值。在increment函数中,我们使用std::lock_guard来自动管理互斥锁的加锁和解锁。这样可以确保在修改counter时,只有一个线程能够访问它。

2. std::lock_guard的使用

std::lock_guard是一个简单的互斥锁封装类,它会在构造时自动加锁,在析构时自动解锁。这样可以确保在lock_guard对象存在期间,互斥锁始终处于锁定状态。使用lock_guard可以避免因忘记解锁而导致的问题。

三、C++中的条件变量

除了互斥锁之外,C++还提供了条件变量(std::condition_variable)来实现更复杂的线程同步。条件变量允许线程在满足某个条件之前等待,当条件满足时,它可以唤醒一个或多个等待的线程。

1. std::condition_variable的基本使用

示例代码:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false; // 共享条件

void print_id(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{return ready;}); // 等待条件满足
    std::cout << "thread " << id << '\n';
}

void go() {
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟延迟
    std::lock_guard<std::mutex> lock(mtx);
    ready = true; // 设置条件为真
    cv.notify_all(); // 唤醒所有等待的线程
}

int main() {
    std::thread threads[10];
    for (int i = 0; i < 10; ++i) {
        threads[i] = std::thread(print_id, i);
    }
    std::cout << "10 threads ready to race...\n";
    go(); // go!

    for (auto &th : threads) {
        th.join();
    }

    return 0;
}

线程高级话题

一、线程池

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在完成任务时终止了,线程池会控制其他线程来补足该线程。

1. 线程池的优势
  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2. C++中的线程池实现(简化版)

由于C++标准库并没有直接提供线程池的实现,我们通常需要自己实现或者使用第三方库。下面是一个简化版的线程池实现示例:

示例代码:

#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>

class ThreadPool {
private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;

    void WorkerThread() {
        while (!stop) {
            std::function<void()> task;

            {
                std::unique_lock<std::mutex> lock(queue_mutex);
                condition.wait(lock, [this]{ return !tasks.empty() || stop; });
                if (!tasks.empty()) {
                    task = std::move(tasks.front());
                    tasks.pop();
                }
            }

            if (task) {
                task();
            }
        }
    }

public:
    ThreadPool(size_t threads) : stop(false) {
        for (size_t i = 0; i < threads; ++i) {
            workers.emplace_back(&ThreadPool::WorkerThread, this);
        }
    }

    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread &worker : workers) {
            worker.join();
        }
    }

    template<class F>
    void enqueue(F f) {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);

            // don't allow enqueueing after stopping the pool
            if (stop) {
                throw std::runtime_error("enqueue on stopped ThreadPool");
            }

            tasks.emplace(std::move(f));
        }
        condition.notify_one();
    }
};

// 使用示例
int main() {
    ThreadPool pool(4); // 创建一个包含4个线程的线程池

    // 提交任务到线程池
    for (int i = 0; i < 10; ++i) {
        pool.enqueue([&](){
            std::cout << "Task " << i << " is running on thread " << std::this_thread::get_id() << std::endl;
        });
    }

    // 等待所有任务完成(在这里,主线程可以执行其他任务或等待线程池销毁)
    // ...

    return 0;
}

二、线程局部存储(Thread-Local Storage, TLS)

线程局部存储允许你创建只能由单个线程访问的全局变量。这对于每个线程需要自己的副本,但又不想使用线程间通信的情况非常有用。

1. C++中的线程局部存储

C++11引入了thread_local关键字来实现线程局部存储。

示例代码:

#include <iostream>
#include <thread>

thread_local int tls_counter = 0; // 线程局部存储的变量

void print_and_increment() {
    tls_counter++;
    std::cout << "Thread " << std::this_thread::get_id() << " tls_counter: " << tls_counter << std::endl;
}

并发编程的高级话题

一、死锁与避免死锁

死锁 是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法向前推进。

1. 死锁产生的条件
  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
2. 避免死锁的策略
  • 预防策略:通过破坏死锁产生的四个必要条件中的一个或多个来预防死锁。
  • 避免策略:在资源分配过程中,使用某种方法去防止系统进入不安全状态,从而避免死锁。
  • 检测与恢复策略:允许死锁的发生,但是通过系统的检测机构及时检测出死锁的发生,然后采取某种措施将进程从死锁状态中解脱出来。
3. 示例代码(使用锁避免死锁)

在C++中,可以使用std::mutexstd::lock_guard等同步原语来避免死锁。下面是一个简单的示例,展示了如何使用std::lock来避免死锁,它使用了一种称为锁顺序的技术。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtxA, mtxB;

void print_block_A_then_B() {
    std::lock(mtxA, mtxB); // 尝试同时锁定两个互斥量
    std::lock_guard<std::mutex> lockA(mtxA, std::adopt_lock), lockB(mtxB, std::adopt_lock);
    // ... 执行临界区操作 ...
    std::cout << "Thread " << std::this_thread::get_id() << " has both A and B\n";
    // ... 临界区结束 ...
}

void print_block_B_then_A() {
    std::lock(mtxB, mtxA); // 尝试以相反的顺序锁定两个互斥量
    std::lock_guard<std::mutex> lockB(mtxB, std::adopt_lock), lockA(mtxA, std::adopt_lock);
    // ... 执行临界区操作 ...
    std::cout << "Thread " << std::this_thread::get_id() << " has both B and A\n";
    // ... 临界区结束 ...
}

int main() {
    std::thread th1(print_block_A_then_B);
    std::thread th2(print_block_B_then_A);

    th1.join();
    th2.join();

    return 0;
}

在上面的示例中,虽然两个线程试图以不同的顺序锁定互斥量,但std::lock函数会确保以相同的顺序锁定它们(如果它们没有被其他线程持有),因此避免了死锁。

二、条件变量(Condition Variables)

条件变量通常用于实现生产者-消费者模型,其中一个或多个线程(生产者)将数据添加到队列中,而一个或多个线程(消费者)从队列中移除并处理这些数据。

示例代码(使用条件变量)
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::queue<int> data_queue;
std::mutex mtx;
std::condition_variable cond_var;
bool data_ready = false; // 标志位,表示数据是否准备好
bool stop_flag = false;  // 停止标志位

void producer() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        data_queue.push(i);
        data_ready = true; // 通知消费者数据已准备好
        cond_var.notify_one(); // 唤醒等待的线程
        lock.unlock(); // 注意:在 C++11 中,std::unique_lock 在析构时会自动解锁
    }
    
    // 通知所有等待的线程,数据已全部生产完成
    {
        std::lock_guard<std::mutex> lock(mtx);
        stop_flag = true;
    }
    cond_var.notify_all();
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cond_var.wait(lock, []{ return data_ready || stop_flag; }); // 等待数据准备好或停止信号

        if (stop_flag && data_queue.empty()) {
            break; // 所有数据已处理,且收到停止信号,退出循环
        }

        int value = data_queue.front();
        data_queue.pop();
        data_ready = false; // 重置数据准备标志位

        // 处理数据...
        std::cout << "Consumed: " << value << std::endl;

        lock.unlock(); // 注意:虽然在这里调用了 unlock,但在 C++11 中,std::unique_lock 会在析构时自动解锁
    }
}

int main() {
    std::thread producer_thread(producer);
    std::thread consumer_thread(consumer);

    producer_thread.join();
    consumer_thread.join();

    return 0;
}

在这个示例中,我们使用了std::condition_variable来实现生产者-消费者模型。生产者线程在添加数据到队列后会设置data_ready标志,并通过notify_one唤醒一个等待的消费者线程。消费者线程在检查到数据准备好或收到停止信号后,会从队列中取出数据并处理。注意,我们在等待条件变量时使用了std::unique_lock来确保互斥量在条件变量等待期间被锁定,并在条件满足后自动解锁。此外,我们还添加了一个stop_flag来通知消费者线程何时停止处理数据。

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

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

相关文章

《机器学习by周志华》学习笔记-决策树-01

本书中的「决策树」有时指学习方法,有时指学得的树。 1、基本流程 1.1、概念 基本流程,亦称「判定树」 决策树(decision tree),是一种常见的机器学习方法。以二分类任务为例,我们希望从给定训练数据集学得一个模型,用以对新样例进行分离。 以二分类任务为例,可看作对…

揭秘微服务架构:十大设计模式助力企业数字化转型

微服务架构中10个常用的设计模式 微服务是一种架构风格&#xff0c;它将一个复杂的应用拆分成多个独立自治的服务&#xff0c;每个服务负责应用程序中的一小部分功能。这些服务通过定义良好的API进行通信&#xff0c;通常是HTTP RESTful API或事件流。微服务架构的主要特点包括…

深入学习指针3

目录 前言 1.二级指针 2.指针数组 3.指针数组模拟二维数组 前言 Hello,小伙伴们我又来了&#xff0c;上期我们讲到了数组名的理解&#xff0c;指针与数组的关系等知识&#xff0c;那今天我们就继续深入到学习指针域数组的练联系&#xff0c;如果喜欢作者菌生产的内容还望不…

攻略:大学生三下乡投稿媒体网站和快速方法

作为当代大学生,不仅需要学习和掌握知识,更需要将所学知识运用到实践中,参与各种社会实践活动。其中,“三下乡”活动就是一个非常有意义的社会实践活动。三下乡社会实践活动新闻稿投稿网站有哪些?有哪些方式可以快速投稿呢&#xff1f;今天小编给大家一次讲个明白。 三下乡新…

C 语言中怎么产生真正的随机数?

在C语言中&#xff0c;要产生真正的随机数&#xff0c;我们通常使用标准库中的 <stdlib.h> 头文件中提供的随机数生成函数。 这些函数可以生成伪随机数&#xff0c;但它们在一定程度上是随机的&#xff0c;足以满足大多数应用程序的需求。 1. 伪随机数生成函数 C标准库…

【从零开始学架构 架构基础】架构设计的本质、历史背景和目的

本文是《从零开始学架构》的第一篇学习笔记&#xff0c;主要理解架构的设计的本质定义、历史背景以及目的。 架构设计的本质 分别从三组概念的区别来理解架构设计。 系统与子系统 什么是系统&#xff0c;系统泛指由一群有关联的个体组成&#xff0c;根据某种规则运作&#…

Java实现的网上书店系统(附带完整源码)

作者声明:文章仅供学习交流与参考!严禁用于任何商业与非法用途!否则由此产生的一切后果均与作者 实现技术:JSP技术;javaBean;servlet;MySql数据库。 系统功能结构图 该系统为MVC结构,它的运行环境分客户端、应用服务器端和数据库服务器端三部分 书店系统需求分析: 通过…

Git泄露(续)

接上一篇补充 git config --global user.name " " git config --global user.email 邮箱地址 配置用户名和邮箱 git commit 使其处于交互区&#xff0c;没有使用 -m&#xff0c;默认用vim 来编辑和提交信息 输入要提交的内容&#xff0c;然后按ESC建回到命令…

Colab/PyTorch - 002 Pre Trained Models for Image Classification

Colab/PyTorch - 002 Pre Trained Models for Image Classification 1. 源由2. 图像分类的预训练模型3. 示例 - AlexNet/ResNet1013.1 模型推断过程3.2 使用TorchVision加载预训练网络3.3 使用AlexNet进行图像分类3.3.1 Step1&#xff1a;加载预训练模型3.3.2 Step2&#xff1a…

【系统架构师】-选择题(十二)计算机网络

1、网闸的作用&#xff1a;实现内网与互联网通信&#xff0c;但内网与互联网不是直连的 2、管理距离是指一种路由协议的路由可信度。15表示该路由信息比较可靠 管理距离越小&#xff0c;它的优先级就越高&#xff0c;也就是可信度越高。 0是最可信赖的&#xff0c;而255则意味…

减瘦误区、雷点、陷阱和挑战怎么应对

在减瘦过程中&#xff0c;很多肥胖人群都容易踩到坑。比如陷入误区&#xff0c;认为只有短期快速的减调方式方法&#xff0c;才值得尝试&#xff0c;而忽视身体健康&#xff1b;或是踩到雷点&#xff0c;轻信强速方剂或方法&#xff0c;结果身体产生了排斥或根本没效用白花钱&a…

RabbitMQ的用途

RabbitMQ主要有四个用途&#xff0c;分别是应用解耦、异步提速、削峰填谷、消息分发。详情讲解如下&#xff1a; RabbitMQ介绍、解耦、提速、削峰、分发 详解、RabbitMQ安装 可视化界面讲解 1.应用解耦&#xff1a;提高系统容错性和可维护性 2.异步提速&#xff1a;提升用户体验…

LabVIEW学习记录4-局部变量、全局变量、共享变量

【LabVIEW】局部变量、全局变量、共享变量 一、变量定义二、内存分配三、竞争状态四、变量创建及简单使用示例4.1 局部变量4.1.1 局部变量的创建4.1.2 局部变量的编程实例 4.2 全局变量4.2.1 创建4.2.2 调用4.2.3 编程实例 4.3 共享变量 一、变量定义 LabVIEW&#xff08;Labor…

怎么把图片改成1920*1080的?一键修改图片尺寸小技巧

一张合适尺寸的图片&#xff0c;不仅可以适应不同设备的屏幕尺寸&#xff0c;保证视觉效果的舒适和协调&#xff0c;还可以有效降低图片的存储空间占用&#xff0c;比如我们有时候想要把一张图片改成1920*1080尺寸的&#xff0c;该如何修改图片尺寸呢&#xff1f;其实可以使用图…

【ZZULI数据结构实验】压缩与解码的钥匙:赫夫曼编码应用

&#x1f4c3;博客主页&#xff1a; 小镇敲码人 &#x1f49a;代码仓库&#xff0c;欢迎访问 &#x1f680; 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1f442;&#x1f3fd;留言 &#x1f60d;收藏 &#x1f30f; 任尔江湖满血骨&#xff0c;我自踏雪寻梅香。 万千浮云遮碧…

虚拟化数据恢复—误还原虚拟机快照怎么办?怎么恢复最新虚拟机数据?

虚拟化技术原理是将硬件虚拟化给不同的虚拟机使用&#xff0c;利用虚拟化技术可以在一台物理机上安装多台虚拟机。误操作或者物理机器出现故障都会导致虚拟机不可用&#xff0c;虚拟机中的数据丢失。 虚拟化数据恢复环境&#xff1a; 有一台虚拟机是由物理机迁移到ESXI上面的&a…

继电器测试负载箱的常见故障和解决方法有哪些?

继电器测试负载箱是用于模拟各种电气负载的设备&#xff0c;广泛应用于继电器、接触器等电气元件的测试和校验。在使用过程中&#xff0c;可能会出现一些故障&#xff0c;影响测试的准确性和效率。以下是一些常见的故障及其解决方法&#xff1a; 电源问题&#xff1a;如果电源电…

FMEA助力医疗设备研发制造:领跑未来,实现弯道超车!

医疗设备作为保障人类健康的重要工具&#xff0c;其研发与制造水平直接关系到医疗技术的进步。然而&#xff0c;在激烈的市场竞争中&#xff0c;如何能够让自家医疗设备研发制造实现弯道超车&#xff0c;成为行业佼佼者&#xff1f;答案就在于——FMEA&#xff08;失效模式与影…

C语言中数组与指针的区别

一. 简介 本文学习了 C语言中数组与指针的区别。这样的话&#xff0c;可以在编写C代码时规避掉出错的问题。 二. C语言中数组与指针的区别 1. 数组 定义字符串数组时&#xff0c;必须让编译器知道需要多少空间。 一种方法是用足够空间的数组存储字符串。例如如下&#xf…

Spring MVC(三) 参数传递

1 Controller到View的参数传递 在Spring MVC中&#xff0c;把值从Controller传递到View共有5中操作方法&#xff0c;分别是。 使用HttpServletRequest或HttpSession。使用ModelAndView。使用Map集合使用Model使用ModelMap 使用HttpServletRequest或HttpSession传值 使用HttpSe…