并行和并发,如何区分?

你好,我是猿java

在计算机科学中,“并发”和“并行性”是两个经常被混淆但实际上具有不同含义的概念。这篇文章,我们将深入探讨这两个概念,并通过 Java代码演示它们的实现。

关于并行和并发, 先看一张很形象的 gif图片(图片来自网络):

img

1. 并发

1.1 定义

并发性(Concurrency)是指系统能够处理多个任务,但不一定是同时执行。关键在于任务的管理,使得多个任务在时间上交错进行,以提高资源利用率和响应能力。

如下图,在一个 CPU上,交替执行多个task:

img

1.2 特点

  • 任务切换:在单核或多核系统上,通过快速切换任务,让用户感觉任务是同时进行的。
  • 资源共享:多个任务共享系统资源,如CPU、内存等。
  • 异步处理:任务可以在等待某些操作完成(如I/O)时,切换到其他任务。

1.3 实际应用示例

  • 用户界面:在图形用户界面(GUI)中,主线程负责响应用户输入,而后台线程处理耗时操作,使界面保持响应。
  • 服务器处理:Web服务器同时处理多个客户端请求,通过线程池或异步IO管理并发连接。

1.4 Java实现示例

以下是一个简单的Java并发示例,模拟多个任务交替执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConcurrencyExample {
public static void main(String[] args) {
Runnable task1 = () -> {
for(int i=1; i<=5; i++) {
System.out.println("Task1 - Count: " + i);
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
};

Runnable task2 = () -> {
for(int i=1; i<=5; i++) {
System.out.println("Task2 - Count: " + i);
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
};

Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);

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

输出示例(任务交错执行):

1
2
3
4
5
Task1 - Count: 1
Task2 - Count: 1
Task1 - Count: 2
Task2 - Count: 2
...

在这个例子中,两个任务在同一个处理器上交替执行,实现了并发性。

2. 并行性

2.1 定义

并行性(Parallelism)是指利用多核或多处理器系统同时执行多个任务或任务的多个部分,以加快总体处理速度。

如下图,多个CPU,每个CPU上分别执行一个 task:

img

2.2 特点

  • 真实的同时执行:在多核处理器上,多个任务可以在不同的核心上同时运行。
  • 任务分解:大的任务可以分解为多个子任务,并行处理后合并结果。
  • 性能提升:通过并行执行,能够显著缩短处理时间,尤其适合计算密集型任务。

2.3 实际应用示例

  1. 机器学习训练:训练深度学习模型涉及将数据集划分为较小的批次。每个批次都跨多个 GPU 或 CPU 内核同时处理,从而显著加快了训练过程。
  2. 视频渲染:视频帧是独立渲染的,因此可以同时处理多个帧。 例如,当使用多个内核并行处理不同的帧时,渲染 3D 动画的速度会快得多。
  3. 网络爬虫:像 Googlebot 这样的网络爬虫将 URL 列表分解成更小的块,并并行处理它们。这允许爬虫同时从多个网站获取数据,从而减少收集信息的时间。
  4. 数据处理:Apache Spark 等大数据框架利用并行性来处理海量数据集。分析来自数百万用户的日志等任务分布在集群中,从而实现同步处理和更快的洞察。
  5. 科学模拟:天气建模或分子相互作用等模拟需要大量的计算。这些计算在多个内核之间分配,允许同时执行并更快地获得结果。

2.4 Java实现示例

以下是一个使用Java并行流实现并行计算的示例,计算1到1000000的平方和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.stream.LongStream;

public class ParallelismExample {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();

long sum = LongStream.rangeClosed(1, 1_000_000)
.parallel()
.map(x -> x * x)
.sum();

long endTime = System.currentTimeMillis();
System.out.println("Sum: " + sum);
System.out.println("Time taken: " + (endTime - startTime) + " ms");
}
}

输出示例

1
2
Sum: 333333833333500000
Time taken: 50 ms

在多核处理器上,.parallel()方法使得流操作并行执行,从而加快计算速度。

3. 并发与并行性的对比

3.1 目标

  • 并发性:使多个任务有条不紊地进行,增强系统响应能力和资源利用率。
  • 并行性:通过同时执行多个任务,提升总体处理速度和吞吐量。

3.2 示例对比

  • 并发:单核处理器上,通过时间片轮转执行多个任务,使用户感觉多个任务同时进行。
  • 并行:多核处理器上,多个任务或任务的部分在不同核心上同时执行。

3.3 性能考虑

  • 并发性适用于I/O密集型应用,通过管理任务等待时间提高系统效率。
  • 并行性适用于CPU密集型应用,通过利用多核资源加快计算速度。

3.4 资源利用

  • 并发更关注任务的调度和资源的共享。
  • 并行更关注如何划分任务以充分利用多核资源。

4. 通过并发和并行实现的Java框架

在 Java中,提供了丰富的工具和库来实现并发和并行操作,下面分别举一个例子来展示并发和并行。

4.1 线程和Executor框架

线程是实现并发的基本单元,Java通过Thread类和Runnable接口提供了对线程的支持。但直接使用Thread可能导致资源管理困难,因此Java引入了Executor框架,简化线程管理。

示例:使用ExecutorService实现并发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorConcurrencyExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);

Runnable task1 = () -> {
for(int i=1; i<=5; i++) {
System.out.println("Task1 - Count: " + i);
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
};

Runnable task2 = () -> {
for(int i=1; i<=5; i++) {
System.out.println("Task2 - Count: " + i);
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
};

executor.submit(task1);
executor.submit(task2);

executor.shutdown();
}
}

输出示例

1
2
3
4
5
Task1 - Count: 1
Task2 - Count: 1
Task1 - Count: 2
Task2 - Count: 2
...

4.2 并行流(Parallel Streams)

Java 8引入了Streams API,它支持顺序和并行操作,极大简化了并行处理的编程复杂度。通过调用.parallel(),可以轻松将流操作并行化。

示例:并行处理列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.Arrays;
import java.util.List;

public class ParallelStreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

numbers.parallelStream()
.map(n -> {
System.out.println("Processing " + n + " in " + Thread.currentThread().getName());
return n * n;
})
.forEach(result -> System.out.println("Result: " + result));
}
}

输出示例(线程顺序可能不同):

1
2
3
4
5
Processing 2 in ForkJoinPool.commonPool-worker-1
Result: 4
Processing 1 in ForkJoinPool.commonPool-worker-3
Result: 1
...

5. 实践建议

  1. 选择合适的并发工具:对于简单的线程管理,可以使用ExecutorService;对于复杂的任务调度,考虑使用ForkJoinPool
  2. 避免共享可变状态:共享状态可能导致竞态条件(Race Conditions),使用线程安全的数据结构或同步机制。
  3. 理解任务的性质:I/O密集型任务适合并发处理,CPU密集型任务适合并行处理。
  4. 合理划分任务:避免过度划分导致线程切换开销过大,或任务粒度过粗导致资源浪费。
  5. 使用高层次抽象:如Java 8的CompletableFuture,简化异步编程模型。

6. 总结

本文从多个维度对比了并发和并行,虽然在处理多任务方面它们有共同之处,但它们的目标和实现方式不同。并发性侧重于任务的管理和调度,以提高系统的响应能力和资源利用率;而并行性则侧重于通过同时执行多个任务或任务的多个部分,以提升处理速度和吞吐量。

  • 并发性关注的是如何结构化程序以处理多个任务的进展,不一定同时执行。
  • 并行性关注的是如何同时执行多个任务,以加快总体的处理速度。

7. 学习交流

如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。

drawing