深入理解并发和并行

打印 上一主题 下一主题

主题 844|帖子 844|积分 2532

1 并发与并行

为什么操纵系统上可以同时运行多个程序而用户感觉不出来?
因为操纵系统营造出了可以同时运行多个程序的假象,通过调治进程以及快速切换CPU上下文,每个进程执行一会就停下来,切换到下个被调治到的进程上,这种切换速率非常快,人无法感知到,从而产生了多个任务同时运行的错觉。
并发(concurrent) 是指的在宏观上多个程序或任务在同时运行,而在微观上这些程序交替执行,可以提高系统的资源使用率和吞吐量。
通常一个CPU内核在一个时间片只能执行一个线程(某些CPU采用超线程技术,物理核心数和逻辑核心数形成一个 1:2 的关系,比如4核CPU,逻辑处理器会有8个,可以同时跑8个线程),假如N个内核同时执行N个线程,就叫做并行(parallel)。我们编写的多线程代码具备并发特性,而不愿定会并行。因为可否并行取决于操纵系统的调治,程序员无法控制,但是调治算法会尽量让差别线程使用差别的CPU核心,所以在实际使用中几乎总是会并行。假如多个任务在一个内核中顺序执行,就是串行(Serial),如下图所示:



并发是多个程序在一段时间内同时执行的现象,而并行是多个任务在同一时刻同时执行,也是多核CPU的紧张特性。
这里有一个疑问:并发肯定并行吗?
并发并不愿定并行。并发是逻辑上的同时发生,而并行是物理上的同时发生。并发可以跑在一个处理器上通过期间片进行切换,而并行须要两个或两个以上的线程跑在差别的处理器上。假如同一个任务的多个线程始终运行在不变的CPU核心上,那就不是并行。
举一个生活中的例子:

  • 你用饭吃到一半,电话来了,你不停到吃完了以后才去接,这就说明你不支持并发也不支持并行。
  • 你用饭吃到一半,电话来了,你停了下来接了电话,接完后继续用饭,这说明你支持并发。
  • 你用饭吃到一半,电话来了,你一边打电话一边用饭,这说明你支持并行。
2 多核调治算法

在多核CPU系统中,调治算法的主要目的是有效地使用全部可用的CPU核心,以提高系统的整体性能和资源使用率。下面是一些常见的多核CPU调治算法:

  • 抢占式调治(Preemptive Scheduling):这种调治算法允许操纵系统随时停止当前正在执行的任务,并将处理器分配给其他任务。在多核系统中,抢占式调治器可以将任务迁移到其他核心上,以充分使用系统资源。
  • 公平调治(Fair Scheduling):公平调治算法旨在公平地分配CPU时间给系统中的全部任务,以确保每个任务都偶尔机在肯定的时间内执行。在多核系统中,公平调治器通常会尝试平衡各个核心上的负载,以避免出现某些核心过载而其他核心处于空闲状态的环境。
  • 负载平衡调治(Load Balancing):负载平衡调治算法用于在多核系统中平衡各个核心上的任务负载,以确保全部核心都能够充分使用。这可以通过将任务从负载较重的核心迁移到负载较轻的核心来实现,或者通过动态地将新任务分配给负载较轻的核心来实现。
  • 优先级调治(Priority Scheduling):优先级调治算法允许为每个任务分配一个优先级,并根据优先级来决定任务的执行顺序。在多核系统中,可以根据任务的优先级将其分配给差别的核心,以确保高优先级任务优先得到执行。
  • 混合调治(Hybrid Scheduling):混合调治算法结合了多种调治策略的优点,以适应差别的应用场景和系统配置。例如,可以将公平调治算法和负载平衡调治算法结合起来,以在系统中实现公平且高效的任务调治。
这些调治算法可以根据系统的需求进行组合和调整,以实现对多核CPU系统资源的有效管理和使用。
抢占式调治(Preemptive Scheduling)的使用最为广泛,它允许操纵系统在任何时候停止当前正在执行的任务,并将处理器分配给其他任务。这种调治策略使得操纵系统能够实时响应各种事件和哀求,从而提高系统的响应性和实时性。
在抢占式调治中,每个任务都被赋予一个优先级,操纵系统会根据任务的优先级来决定哪个任务应该在当前时间片执行。假如某个高优先级任务准备停当而且当前正在执行的任务的优先级低于它,操纵系统会停止当前任务的执行,并将处理器分配给高优先级任务,从而实现抢占。抢占式调治的主要优点包括:

  • 实时性:抢占式调治允许操纵系统实时地响应外部事件和哀求,从而满足实时性要求。
  • 机动性:操纵系统可以根据任务的优先级动态地调整任务的执行顺序,以适应差别的系统负载和需求。
  • 公平性:抢占式调治可以确保高优先级任务得到实时执行,而不会被低优先级任务长时间占用处理器。
  • 多任务并发:通过在任务之间进行快速的切换,抢占式调治可以实现多任务并发执行,从而提高系统的吞吐量和效率。
抢占式调治也存在一些挑战和限制:

  • 上下文切换开销:频仍的任务切换会导致上下文切换的开销增加,可能会影响系统的性能。
  • 优先级反转:假如低优先级任务持有某些资源而高优先级任务须要访问这些资源,可能会导致优先级反转题目,从而影响系统的实时性。
  • 饥饿题目:假如某个任务的优先级始终较低,而且总是被更高优先级的任务抢占,可能会导致该任务长时间无法执行,出现饥饿题目。
抢占式调治在许多操纵系统中得到了广泛应用,包括Windows、Linux、MacOS等,它为实时系统和响应式系统提供了一种高效的任务调治机制。
3 Java并行编程

在编码层面上看,采用Java语言创建多线程代码,不须要程序员打上并行的标记,因为为了充分使用计算资源,操纵系统肯定会尽可能调治多线程到差别的CPU核心上。并发的任务通常有多线程竞争资源和频仍的CPU上下文切换,这些都会降低执行效率。
在实际的业务场景里,许多计算任务实在互不干扰,最后汇总效果就可以了,比如统计差别用户的每日运动次数。它们不存在竞争资源,并行处理的效率非常高,Java语言提供了多线程并行执行的 API。
3.1 Future

在Java并发编程中,Future是一种用于表现异步计算效果的接口。它允许你提交一个任务而且在未来的某个时候获取任务的效果。Future的原理是通过一个占位符来表现异步操纵的效果,在任务完成之前,可以通过Future对象获取占位符,而且在须要的时候等待任务的完成并获取效果。Future接口界说了异步计算效果的标准,具体的异步计算由实现了Future接口的类来执行,比如ExecutorService的submit方法会返回一个Future对象,用于跟踪任务的执行状态和效果。
Future提供了以下主要方法:

  • isDone():判断任务是否已经完成。
  • cancel(boolean mayInterruptIfRunning):取消任务的执行。
  • get():获取任务的执行效果,在任务完成之前会壅闭当火线程。
  • get(long timeout, TimeUnit unit):获取任务的执行效果,但最多等待指定的时间,超时后会抛出TimeoutException。
看看下面这个代码示例:
  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. import java.util.concurrent.Future;
  5. public class FutureParallelExample {
  6.     public static void main(String[] args) throws Exception {
  7.         ExecutorService executor = Executors.newFixedThreadPool(2);
  8.         Callable<Integer> task1 = () -> {
  9.             // 模拟耗时计算
  10.             Thread.sleep(2000);
  11.             return 10;
  12.         };
  13.         Callable<Integer> task2 = () -> {
  14.             // 模拟耗时计算
  15.             Thread.sleep(3000);
  16.             return 20;
  17.         };
  18.         Future<Integer> future1 = executor.submit(task1);
  19.         Future<Integer> future2 = executor.submit(task2);
  20.         // 异步执行,继续执行下面的代码
  21.         System.out.println("Asynchronous computation is executing.");
  22.         // 获取第一个任务的结果
  23.         Integer result1 = future1.get(); // 这将会阻塞直到任务1完成
  24.         System.out.println("Task 1 result: " + result1);
  25.         // 获取第二个任务的结果
  26.         Integer result2 = future2.get(); // 这将会阻塞直到任务2完成
  27.         System.out.println("Task 2 result: " + result2);
  28.         // 关闭ExecutorService
  29.         executor.shutdown();
  30.     }
  31. }
复制代码
在这个例子中,启动了两个异步任务,并分别获取了它们的 Future 对象。通过 Future.get() 方法,我们可以等待任务完成并获取效果。ExecutorService 使用了一个固定的线程池,大小为2。这意味着两个任务将会并行执行。
3.2 Fork / Join

Fork / Join 框架是Java 7中新增的并发编程工具,主要有两个步骤,第一是fork:将一个大任务分成许多个小任务;第二是 join:将第一个任务的效果 join 起来,生成最后的效果。假如第一步中并没有任何返回值,join将会等到全部的小任务都结束。
斐波那契数列由意大利数学家斐波那契初次提出,这个数列从第三项开始,每一项都等于前两项之和,通常以递归方式界说,即F(0)=1,F(1)=1,对于n>=2的任何正整数n,F(n)=F(n-1)+F(n-2),数列的前几个数字是1,1,2,3,5,8,13,21,34。我们尝试使用递归计算第n项的数值,代码如下:
  1. /**
  2. * 递归实现斐波那契数列
  3. **/
  4. public class FibonacciRecursion {
  5.     public static int fibonacciRecursive(int n) {
  6.         if (n <= 1)
  7.             return n;
  8.         return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
  9.     }
  10.     public static void main(String[] args) {
  11.         int n = 10;
  12.         System.out.println("Fibonacci of " + n + " is " + fibonacciRecursive(n));
  13.     }
  14. }
复制代码
上面代码调用了 parallel() 后,reduce()方法内部逻辑发生了变革,它会根据当火线程池资源分配任务,并行地在差别的工作线程上执行累加操纵,而不是串行执行的。
Java 并行流是基于 Fork/Join 框架实现的,它使用了多线程来处理流操纵。在多核环境中,Fork/Join 框架会根据系统资源自动调整任务分配,尽可能多地使用空闲核心,更充分地发挥硬件潜力。比如,当CPU具有8个内核时,并行计算的耗时远小于串行计算耗时的8倍,但是由于线程创建、销毁以及上下文切换等开销,实际性能提升并非线性。
并行计算并不总是适用于全部场景,特别是在数据集较小或者任务分解后产生的子任务粒度较小时,线程管理的开销可能凌驾并行计算带来的上风。假如硬件只有单核或少核,则并行计算效果有限乃至可能会因线程切换而降低效率。综合考虑以下因素:

  • 数据量:对于大规模数据集,尤其是须要复杂运算的任务,采用并行计算可以显著提高执行速率。
  • 硬件配置:确保运行环境为多核处理器,不适用于 IO 密集型操纵,仅适用于 CPU 密集型操纵。
  • 任务性质:若任务可以轻松拆分为独立的子任务,而且效果合并相对简单,更得当应用并行计算。
  • 系统负载:在高负载系统中,要避免过度增加并发,以免引发资源竞争和瓶颈题目。
3.4 CompletableFuture

CompletableFuture是一个实现了Future接口的类,它提供了一种更加机动和强大的方式来进行异步编程。CompletableFuture可以用来表现一个异步计算的效果,而且提供了丰富的方法来处理异步操纵的完成、组合多个异步操纵、处理非常等。CompletableFuture相比于传统的Future接口,具有以下上风:

  • 更加机动的方法链:CompletableFuture提供了一系列的方法,可以链式地进行异步操纵,比如thenApply、thenAccept、thenCompose等,使得代码更加简洁清楚。
  • 组合多个异步操纵:CompletableFuture允许你组合多个异步操纵,可以按照顺序执行、并行执行,或者根据肯定的条件来执行。
  • 非常处理:CompletableFuture提供了exceptionally和handle等方法来处理异步操纵中的非常环境,使得非常处理变得更加机动。
  • 支持回调函数:你可以通过thenApply、thenAccept等方法设置回调函数,以便在异步操纵完成时执行特定的操纵。
  • 可编程式地完成异步操纵:CompletableFuture提供了complete、completeExceptionally等方法,可以手动地完成异步操纵,从而更加机动地控制异步任务的执行过程。
我们看看简单的代码示例:

  • 简单的异步任务
  1. import java.util.concurrent.ForkJoinPool;
  2. import java.util.concurrent.RecursiveTask;
  3. /**
  4. * fork/join实现斐波那契数列
  5. **/
  6. public class FibonacciFork extends RecursiveTask<Integer> {
  7.     final int n;
  8.     public FibonacciFork(int n) {
  9.         this.n = n;
  10.     }
  11.     @Override
  12.     protected Integer compute() {
  13.         if (n <= 1)
  14.             return n;
  15.         FibonacciFork f1 = new FibonacciFork(n - 1);
  16.         FibonacciFork f2 = new FibonacciFork(n - 2);
  17.         f1.fork();
  18.         return f2.compute() + f1.join();
  19.     }
  20.     public static void main(String[] args) {
  21.         ForkJoinPool pool = new ForkJoinPool();
  22.         FibonacciFork fib = new FibonacciFork(10);
  23.         Integer result = pool.invoke(fib);
  24.         System.out.println(result);
  25.     }
  26. }
复制代码

  • 组合多个CompletableFuture
  1. public class StreamDemo {
  2.     public static void main(String[] args) {
  3.         Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
  4.               .reduce((a, b) -> a + b)
  5.               .ifPresent(System.out::println); // 输出结果:45
  6.     }
  7. }
复制代码

  • 处理非常环境
  1. public class StreamParallelDemo {
  2.     public static void main(String[] args) {
  3.         Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
  4.               .parallel()
  5.               .reduce((a, b) -> a + b)
  6.               .ifPresent(System.out::println);
  7.     }
  8. }
复制代码

  • 自界说线程池
  1. import java.util.concurrent.CompletableFuture;
  2. public class CompletableFutureExample {
  3.     public static void main(String[] args) {
  4.         CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
  5.             // 异步任务,返回结果为100
  6.             return 100;
  7.         });
  8.         // 在任务完成后输出结果
  9.         future.thenAccept(result -> System.out.println("异步任务结果为:" + result));
  10.     }
  11. }
复制代码
这些示例展示了使用CompletableFuture进行异步编程的一些常见用法,包括简单的异步任务、组合多个CompletableFuture、处理非常环境等。总的来说,CompletableFuture是Java并发编程中一个强大而机动的工具,它使得异步编程变得更加简单、清楚和可控。
4 总结

要更好地掌握Java并发编程技能,可以采取以下几个步骤:

  • 学习基础知识: 对Java并发编程的基本概念和术语有清楚的理解,比如线程、锁、同步、并发题目等。可以通过阅读干系的书籍、教程或者在线课程来学习。
  • 熟悉并发工具类: Java提供了丰富的并发工具类,比如 Thread、Runnable、Executor、ThreadPoolExecutor、Semaphore、CountDownLatch等。深入了解这些工具类的使用方法和特性,以及在差别场景下的应用。
  • 掌握多线程编程: 多线程编程是Java并发编程的核心,要纯熟掌握怎样创建线程、管理线程生命周期、线程同步和通信等技术。了解线程的状态、优先级、调治方式等概念,以及怎样避免常见的多线程题目,比如死锁、竞态条件等。
  • 深入理解并发模子: 了解并发模子,比如共享内存模子和消息通报模子,以及它们的优缺点。掌握在这些模子下怎样设计和实现并发程序。
  • 学习并发设计模式: 掌握常见的并发设计模式,比如生产者-消费者模式、读写锁模式、工作窃取模式等。了解这些模式的原理和实现方式,以及在实际项目中的应用。
  • 实践项目经验: 通过实际项目来锻炼并发编程技能,尝试在项目中应用所学的知识解决实际的并发题目。可以选择一些开源项目或者自己构建小型项目来练习。
参考

https://zhuanlan.zhihu.com/p/622768247
https://www.cnblogs.com/badaoliumangqizhi/p/17021500.html
https://blog.csdn.net/weixin_44073836/article/details/123346035
https://blog.csdn.net/m0_71149992/article/details/125327370
https://www.php.cn/faq/530720.html
https://zhuanlan.zhihu.com/p/424501870
https://zhuanlan.zhihu.com/p/339472446
https://zhuanlan.zhihu.com/p/622218563

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

写过一篇

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表