你必须要弄懂的多线程与多进程编程模型的对比与选择!

举报
bug菌 发表于 2025/02/20 11:59:34 2025/02/20
【摘要】 🏆本文收录于「滚雪球学SpringBoot」专栏,手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!@TOC环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8 🌟 前言  并发编程是现代软件开发中非常重要的一部分。在多核处理器和高负载...

🏆本文收录于「滚雪球学SpringBoot」专栏,手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

@TOC

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

🌟 前言

  并发编程是现代软件开发中非常重要的一部分。在多核处理器和高负载的系统环境下,如何合理选择多线程与多进程的编程模型来处理大量并发任务,成为了开发者面临的一个难题。在Java中,得益于其强大的线程和进程管理机制,开发者可以在这些模型之间做出灵活选择,以应对不同的性能需求和任务场景。

  这篇文章将会从多线程和多进程的基础概念出发,详细探讨它们各自的优缺点、适用场景以及实际应用中的选择标准。通过Java代码示例,帮助大家理解如何在实践中实现这些编程模型,并为你提供选择和优化方案的思路。

📝 摘要

  在并发编程中,多线程与多进程是两种常见的编程模型。多线程通过在同一个进程内创建多个线程来实现并发,而多进程通过在操作系统级别创建多个独立进程来实现并行。两者各有优势与劣势,选择合适的模型需要根据任务的特点、资源消耗以及系统架构来做出权衡。

  本文从多线程与多进程的基础概念开始,分析它们在不同应用场景下的优缺点,并通过具体的Java代码实例深入解释每种模型的实现方式及其适用场景。此外,文章还提供了性能测试案例,帮助开发者在实际开发中做出更有根据的技术选择。

📖 简介

多线程编程模型

多线程是一种在同一个进程内创建多个执行单元(线程)来并行执行任务的编程模型。线程是轻量级的进程,每个线程都有自己的栈内存和程序计数器,但多个线程共享进程的内存空间。

优点

  • 资源共享:线程之间可以共享进程的内存和资源,减少了数据传递的成本。
  • 轻量级:创建线程的开销比创建进程小,能够快速响应任务需求。
  • 并发性能好:适合I/O密集型任务,如网络请求、文件操作等。

缺点

  • 线程安全问题:多个线程访问共享资源时容易发生竞争条件,需要额外的同步机制。
  • 复杂性增加:当线程数量过多时,管理和调度复杂度增加,容易发生死锁或资源浪费。
  • 上下文切换:频繁的线程切换可能会带来较大的开销,影响性能。

多进程编程模型

  多进程模型是指在操作系统级别启动多个独立的进程,每个进程拥有独立的内存空间。多进程能有效地隔离任务和资源,避免线程竞争的问题。

优点

  • 内存隔离:每个进程拥有独立的内存空间,进程间的相互影响较小。
  • 稳定性高:如果一个进程崩溃,其他进程不会受到影响。
  • 并行处理能力强:可以更好地利用多核CPU,提升任务的并行性。

缺点

  • 内存消耗大:每个进程都有独立的内存空间,内存占用较大。
  • 创建开销高:创建和销毁进程的开销相对较大,启动速度较慢。
  • 进程间通信复杂:进程间的通信(IPC)比线程间的通信要复杂得多,通常需要使用管道、共享内存等机制。

🧩 概述

多线程与多进程的核心区别

特性 多线程模型 多进程模型
内存空间 所有线程共享同一内存空间 每个进程拥有独立内存
创建开销 创建和销毁线程的开销较小 创建进程的开销较大
资源竞争 线程之间会竞争CPU和内存 进程之间相对独立,竞争少
性能瓶颈 线程切换频繁时,性能可能下降 进程启动慢,通信复杂
应用场景 高并发I/O密集型任务 计算密集型任务,大数据处理

适用场景的选择

  • 多线程:适合那些需要高频繁操作且共享资源多的任务,如Web服务器、实时通信应用等。尤其是当任务大部分时间处于等待I/O的情况下,线程模型可以更高效地管理并发。
  • 多进程:适合需要高隔离性和独立内存空间的任务,如大规模计算、数据处理任务。多进程可以更好地避免线程死锁、数据竞争等问题,也能更充分地利用多核CPU。

💻 核心源码解读

多线程模型的实现(Java)

/**
 * @author bug菌
 * @Source 公众号:猿圈奇妙屋
 * @date: 2025-02-20 11:48
 */
public class MultiThreadExample {
    public static void main(String[] args) {
        // 创建线程1
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("Thread 1 - Task " + i);
                try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            }
        });

        // 创建线程2
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("Thread 2 - Task " + i);
                try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();
    }
}

  在这个例子中,两个线程并行执行,每个线程打印10条消息,并在每次打印后休眠100毫秒。通过这种方式,线程能够并行处理多个任务,提高了应用程序的响应速度。

多进程模型的实现(Java)

import java.io.*;

/**
 * @author bug菌
 * @Source 公众号:猿圈奇妙屋
 * @author: luoyong
 * @date: 2025-02-20 11:48
 * @desc:
 */
public class MultiProcessExample {
    public static void main(String[] args) throws IOException {
        // 使用ProcessBuilder启动多个进程
        ProcessBuilder processBuilder = new ProcessBuilder("java", "-cp", ".", "ChildProcess");
        Process process1 = processBuilder.start();
        Process process2 = processBuilder.start();

        try {
            process1.waitFor();  // 等待进程1完成
            process2.waitFor();  // 等待进程2完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  这里使用ProcessBuilder来启动两个独立的进程,每个进程都执行一个Java程序。在进程启动后,主线程等待两个进程完成工作。

🧑‍💻 案例分析

多线程与多进程在Web应用中的对比

假设我们正在构建一个高并发的Web应用,这个应用将同时处理成千上万的HTTP请求。

  • 使用多线程模型:对于这种场景,使用线程池管理并发任务是比较常见的做法。每个请求由一个线程处理,线程池会限制线程的最大数量,以避免过多线程消耗过多资源。线程池能够高效地调度线程,处理大量请求。
  • 使用多进程模型:如果请求处理非常复杂,

  且每个请求需要大量的计算资源,可能会使用多进程模型。每个请求由一个独立进程处理,能够更好地隔离不同任务,但也带来了更高的资源消耗和进程间通信的复杂性。

🎯 应用场景演示

  在实际的开发中,我们可以通过选择合适的并发模型来应对不同的场景。下面我们通过一个Web服务器的示例来演示多线程和多进程的应用:

1. 多线程Web服务器

  适用于大量并发请求,但每个请求处理时间较短(如API服务、静态文件服务等)。

2. 多进程Web服务器

  适用于计算密集型的任务,或需要任务隔离的场景(如大数据处理任务或计算服务)。

⚖️ 优缺点分析

  在选择并发模型时,我们需要权衡系统的资源消耗、并发处理能力以及开发复杂性等多个因素:

多线程模型

  • 优点:轻量级、响应速度快、资源共享便利。
  • 缺点:线程安全问题、调试复杂。

多进程模型

  • 优点:内存隔离、稳定性高、并行计算能力强。
  • 缺点:内存消耗大、进程间通信复杂。

🔧 类代码方法介绍及演示

  在并发编程中,无论是多线程还是多进程,都需要一定的类和方法来管理任务的执行、调度以及资源的分配。这里将分别通过多线程和多进程两个模型,展示如何实现和管理并发任务的核心类和方法。

1. 多线程模型的实现

  在Java中,线程可以通过实现 Runnable 接口或者继承 Thread 类来创建。在下面的代码中,我们将展示如何使用 Runnable 接口来实现多线程。

示例:使用 Runnable 接口实现多线程

public class MultiThreadRunnableExample {
    
    // 创建一个任务类实现Runnable接口
    static class Task implements Runnable {
        private String taskName;

        public Task(String taskName) {
            this.taskName = taskName;
        }

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " is performing " + taskName + " - Step " + i);
                try {
                    Thread.sleep(500);  // 模拟任务处理
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    public static void main(String[] args) {
        // 创建多个线程并启动它们
        Thread thread1 = new Thread(new Task("Task 1"));
        Thread thread2 = new Thread(new Task("Task 2"));

        thread1.start();
        thread2.start();
    }
}

代码解读:

  • Task类:实现了Runnable接口的Task类,每个任务打印5次信息,每次休眠500毫秒来模拟实际任务的处理。
  • 创建线程:在main方法中,我们通过创建Thread对象来启动多个线程,每个线程执行一个不同的任务。
  • 输出示例:两个线程并发执行,在控制台上交替打印信息,展示了多线程的基本使用。

扩展:

  我们可以使用 ExecutorService 来管理线程池,避免每次都手动创建新线程,这对于处理大量任务时非常有用。

示例:使用 ExecutorService 管理线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 提交任务给线程池
        for (int i = 1; i <= 5; i++) {
            executorService.submit(new Task("Task " + i));
        }

        // 关闭线程池
        executorService.shutdown();
    }

    // 创建任务类实现Runnable接口
    static class Task implements Runnable {
        private String taskName;

        public Task(String taskName) {
            this.taskName = taskName;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " is performing " + taskName);
            try {
                Thread.sleep(500);  // 模拟任务处理
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

代码解读:

  • ExecutorService:我们使用ExecutorService来管理线程池,通过newFixedThreadPool方法创建一个固定大小的线程池。
  • 提交任务:通过调用executorService.submit()方法将任务提交给线程池执行。
  • 关闭线程池:通过调用shutdown()方法来关闭线程池,确保在任务执行完毕后资源被释放。

2. 多进程模型的实现

  与多线程不同,多进程编程的管理和执行需要操作系统级别的支持。在Java中,我们通常通过 ProcessBuilderRuntime.exec() 方法来启动新进程。

示例:使用 ProcessBuilder 启动子进程

import java.io.*;

public class MultiProcessExample {

    public static void main(String[] args) throws IOException {
        // 使用ProcessBuilder启动外部Java程序作为子进程
        ProcessBuilder processBuilder = new ProcessBuilder("java", "-cp", ".", "ChildProcess");

        // 启动进程1
        Process process1 = processBuilder.start();
        // 启动进程2
        Process process2 = processBuilder.start();

        // 等待进程完成
        try {
            process1.waitFor();  // 等待进程1完成
            process2.waitFor();  // 等待进程2完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

代码解读:

  • ProcessBuilderProcessBuilder用于启动新的外部进程,传入的参数包括要执行的Java类和类路径。这里通过调用processBuilder.start()启动了两个子进程。
  • 等待进程:调用waitFor()方法确保主进程等待子进程执行完成后再继续。

子进程实现(ChildProcess.java)

public class ChildProcess {
    public static void main(String[] args) {
        System.out.println("Child process is running...");
        try {
            Thread.sleep(2000);  // 模拟处理时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Child process completed.");
    }
}
  • 子进程ChildProcess是我们要在子进程中执行的类,模拟一个长时间运行的任务。

3. 多进程与多线程结合的实践

  在一些复杂的应用场景下,可能需要结合多进程和多线程的优势。例如,某些计算密集型任务可以使用多进程,而I/O密集型任务则使用多线程。

示例:使用多进程处理计算密集型任务,同时利用线程池管理I/O任务

import java.util.concurrent.*;

public class HybridExample {

    public static void main(String[] args) throws IOException {
        // 创建一个固定大小的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 提交多线程I/O任务
        executorService.submit(new IOTask("I/O Task 1"));
        executorService.submit(new IOTask("I/O Task 2"));

        // 启动多进程计算任务
        ProcessBuilder processBuilder = new ProcessBuilder("java", "-cp", ".", "ComputeTask");
        Process process1 = processBuilder.start();
        Process process2 = processBuilder.start();

        // 等待进程完成
        try {
            process1.waitFor();
            process2.waitFor();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 关闭线程池
        executorService.shutdown();
    }

    // I/O任务类
    static class IOTask implements Runnable {
        private String taskName;

        public IOTask(String taskName) {
            this.taskName = taskName;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " is performing " + taskName);
            try {
                Thread.sleep(500);  // 模拟I/O操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

代码解读:

  • I/O任务:我们通过线程池来提交多个I/O任务,每个线程负责执行一个I/O任务。
  • 计算任务:计算任务通过子进程来执行,模拟复杂的计算任务。
  • 结合使用:通过多进程与多线程的结合,可以在处理不同类型任务时充分利用系统资源。

🧪 测试用例(Main函数写法)

import java.io.IOException;

/**
 * @author bug菌
 * @Source 公众号:猿圈奇妙屋
 * @date: 2025-02-20 11:26
 */
public class TestConcurrencyOptimization {
    public static void main(String[] args) throws IOException {
        MultiThreadExample.main(args);
        MultiProcessExample.main(args);
    }
}

🔍 测试结果展示

  通过启动不同数量的线程或进程,我们可以测量响应时间、CPU占用、内存消耗等指标。预期结果应当根据并发数和任务复杂度来设定。

🧐 测试代码分析

  在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。

  如上这段代码展示了如何在 Java 中实现并发和并行的两种方式:多线程多进程。同时还提供了一个测试类 TestConcurrencyOptimization 来同时执行这两种方式。

1. MultiThreadExample 类:

这个类演示了如何使用 Java 的多线程来并发执行任务。

代码解析:

Thread thread1 = new Thread(() -> {
    for (int i = 0; i < 10; i++) {
        System.out.println("Thread 1 - Task " + i);
        try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
});
  • 创建线程 1thread1 会执行一个循环,从 0 到 9,打印 "Thread 1 - Task i"
  • 每次输出后会休眠 100 毫秒,模拟任务处理的延迟。
  • Thread.sleep(100) 会让线程暂时挂起,释放 CPU 资源。
Thread thread2 = new Thread(() -> {
    for (int i = 0; i < 10; i++) {
        System.out.println("Thread 2 - Task " + i);
        try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
});
  • 创建线程 2thread2 的功能与 thread1 类似,执行相同的任务,只是输出的内容不同。
thread1.start();
thread2.start();
  • 启动线程start() 方法会启动线程并开始执行各自的任务,两个线程并发执行。
  • 输出会交替出现,由于线程调度不可预见,所以线程 1 和线程 2 的输出顺序会交替打印。

运行结果:

Thread 1 - Task 0
Thread 2 - Task 0
Thread 1 - Task 1
Thread 2 - Task 1
...

2. MultiProcessExample 类:

这个类演示了如何使用 Java 的 ProcessBuilder 启动多个外部进程,进程间并行执行。

代码解析:

ProcessBuilder processBuilder = new ProcessBuilder("java", "-cp", ".", "ChildProcess");
Process process1 = processBuilder.start();
Process process2 = processBuilder.start();
  • ProcessBuilder 用来创建和启动外部进程。
  • 这里启动了两个进程 process1process2,它们运行的是 ChildProcess 类。假设 ChildProcess 是一个已有的 Java 类。
    • -cp . 指定类路径为当前目录,即 ChildProcess 类位于当前目录。
    • start() 启动进程并返回 Process 对象。
process1.waitFor();  // 等待进程1完成
process2.waitFor();  // 等待进程2完成
  • waitFor() 方法会让当前线程阻塞,直到指定的进程结束。即主进程会等待 process1process2 执行完后再继续。

运行结果:

  • 启动两个独立的进程(process1process2),每个进程独立运行,不会共享内存。
  • 每个进程执行自己的任务,且互不干扰。进程的输出和行为是独立的。

3. TestConcurrencyOptimization 类:

该类用于测试并发和并行的优化,通过同时运行 MultiThreadExampleMultiProcessExample 中的 main 方法。

代码解析:

MultiThreadExample.main(args);
MultiProcessExample.main(args);
  • 该类的 main 方法首先调用 MultiThreadExample.main(args),然后调用 MultiProcessExample.main(args),这两个方法会在同一时刻执行。
    • 通过并发执行多线程和多进程,来观察它们的效果。

运行结果:

  • MultiThreadExample.main(args) 会启动两个线程并发执行任务。
  • MultiProcessExample.main(args) 会启动两个独立进程并行执行任务。
  • 因为两者的执行是独立的,所以它们会并行地执行,输出顺序会受到操作系统调度的影响。

总结:

  • 多线程(MultiThreadExample:多个线程共享同一进程的内存空间,可以并发执行。线程间的资源共享和同步是并发编程中的关键。
  • 多进程(MultiProcessExample:每个进程拥有独立的内存空间,进程间不共享数据。进程间的通信一般需要通过 IPC(进程间通信)机制。
  • 测试类(TestConcurrencyOptimization:同时测试了多线程和多进程的执行,演示了并发和并行优化的不同实现方式。

两者各有优劣:

  • 多线程 适用于资源共享、快速切换任务的场景,适合 CPU 密集型或 I/O 密集型任务。
  • 多进程 适用于隔离、独立执行的任务,能有效避免一个进程崩溃影响整个应用,但开销较大。

📝 小结

  多线程和多进程各有优势,选择合适的并发模型能有效提高系统性能。在实际开发中,我们需要根据应用的特性和需求来做出选择。在I/O密集型任务中,使用多线程通常能够带来更高的性能;而在计算密集型任务中,多进程可能会更合适。

💡 总结

  并发编程模型的选择并非一成不变,重要的是要结合实际的需求来判断最适合的方案。合理的选择和优化能显著提升系统的性能和响应速度。希望本文为大家提供了一个清晰的思路,帮助你在开发中做出更合理的决策。

🎉 寄语

  并发编程是一项极具挑战性的技术,希望你在学习和实践中逐步积累经验,掌握各种并发模型的应用技巧。无论选择多线程还是多进程,最重要的是理解它们的优缺点,并能根据不同场景灵活运用。

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。

✨️ Who am I?

我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主/价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。