目录

JUC - 概念

# 什么是JUC

JUC是 java.util.concurrent 的简写,在并发编程中使用的工具类。在 JDK 官方手册中可以看到 JUC 相关的 jar 包有三个。

用中文概括一下,JUC 的意思就是 Java 并发编程工具包

实现多线程有三种方式:Thread、Runnable、Callable,其中 Callable 就位于 concurrent 包下。

# 进程与线程

# 进程

进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。

当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。 进程就可以视为程序的一个实例。

大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

# 线程

通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程可以利用进程所有拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 Windows 中进程是不活动的,只是作为线程的容器。

# 二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信称为 IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

# 并行与并发

# 概念

做并发编程之前,必须首先理解什么是并发,什么是并行。

并发和并行是两个非常容易混淆的概念。它们都可以表示两个或多个任务一起执行,但是偏重点有点不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。然而并行的偏重点在于「同时执行」。

单核 CPU 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 CPU 的时间片(Windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 CPU 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。总结为一句话就是:微观串行,宏观并行

严格意义上来说,并行的多个任务是真实的同时执行,而对于并发来说,这个过程只是交替的,一会运行任务一,一会儿又运行任务二,系统会不停地在两者间切换。但对于外部观察者来说,即使多个任务是串行并发的,也会造成是多个任务并行执行的错觉。

实际上,如果系统内只有一个 CPU,而现在而使用多线程或者多线程任务,那么真实环境中这些任务不可能真实并行的,毕竟一个 CPU 一次只能执行一条指令,这种情况下多线程或者多线程任务就是并发的,而不是并行,操作系统会不停的切换任务。真正的并发也只能够出现在拥有多个 CPU 的系统中(多核 CPU)。

并行:用更多的 CPU 核心更快的完成任务。就像一个团队,一个脑袋不够用了,一个团队来一起处理一个任务。

并发:在计算能力恒定的情况下处理更多的任务,就像我们的大脑,计算能力相对恒定,要在一天中处理更多的问题,我们就必须具备多任务的能力。现实工作中有很多事情可能会中断你的当前任务,处理这种多任务的能力就是你的并发能力。

一般会将 线程轮流使用 CPU 的做法称为并发(concurrent)。

# 总结

  • 并发(concurrent)是同一时间段应对(dealing with)多件事情的能力
  • 并行(parallel)是同一时间动手做(doing)多件事情的能力

# 例子

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
  • 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)
  • 雇了 3 个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行

# 应用

  • 同步:需要等待结果返回,才能继续运行就是同步
  • 异步:不需要等待结果返回,就能继续运行就是异步

Java 实现异步的方法是多线程,可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 CPU 什么都做不了,其它代码都得暂停。

  • 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程

  • Tomcat 的异步 Servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 Tomcat 的工作线程

  • UI 程序中,开线程进行其他操作,避免阻塞 UI 线程

代码测试:

创建一个一亿的数组,初始化内容为 1。

@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
public class MyBenchmark {
    static int[] ARRAY = new int[1000_000_00];
    static {
        Arrays.fill(ARRAY, 1);
    }
    @Benchmark
    public int c() throws Exception {
        int[] array = ARRAY;
        FutureTask<Integer> t1 = new FutureTask<>(()->{
            int sum = 0;
            for(int i = 0; i < 250_000_00;i++) {
                sum += array[0+i];
            }
            return sum;
        });
        FutureTask<Integer> t2 = new FutureTask<>(()->{
            int sum = 0;
            for(int i = 0; i < 250_000_00;i++) {
                sum += array[250_000_00+i];
            }
            return sum;
        });
        FutureTask<Integer> t3 = new FutureTask<>(()->{
            int sum = 0;
            for(int i = 0; i < 250_000_00;i++) {
                sum += array[500_000_00+i];
            }
            return sum;
        });
        FutureTask<Integer> t4 = new FutureTask<>(()->{
            int sum = 0;
            for(int i = 0; i < 250_000_00;i++) {
                sum += array[750_000_00+i];
            }
            return sum;
        });
        new Thread(t1).start();
        new Thread(t2).start();
        new Thread(t3).start();
        new Thread(t4).start();
        return t1.get() + t2.get() + t3.get()+ t4.get();
    }
    @Benchmark
    public int d() throws Exception {
        int[] array = ARRAY;
        FutureTask<Integer> t1 = new FutureTask<>(()->{
            int sum = 0;
            for(int i = 0; i < 1000_000_00;i++) {
                sum += array[0+i];
            }
            return sum;
        });
        new Thread(t1).start();
        return t1.get();
    }
}
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

双核 CPU

在双核 CPU,即 4 个逻辑 CPU 下,上面的代码的效果如下:

Benchmark          Mode    Samples    Score    Score error    Units
o.s.MyBenchmark.c  avgt       5       0.020     0.001          s/op
o.s.MyBenchmark.d  avgt       5       0.043     0.003          s/op
1
2
3

看 Score 的时间,可以看到多核下,处理代码的效率提升还是很明显的,快了一倍左右

单核 CPU

在单核 CPU 下,上面的代码的效果如下:

Benchmark          Mode    Samples    Score    Score error    Units
o.s.MyBenchmark.c  avgt       5       0.061     0.060          s/op
o.s.MyBenchmark.d  avgt       5       0.064     0.071          s/op
1
2
3

可以看到,单核 CPU 下性能几乎是一样的。

结论

单核 CPU 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 CPU,不至于一个线程总占用 CPU,别的线程没法干活。

多核 CPU 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的:

  • 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(后面的阿姆达尔定律)
  • 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义

IO 操作不占用 CPU,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 CPU,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化。

更新时间: 2024/01/17, 05:48:13
最近更新
01
JVM调优
12-10
02
jenkins
12-10
03
Arthas
12-10
更多文章>