Java 并发之基础知识

并发 (Concurrency) 和并行 (Parallelism)

并发指的是操作系统同时处理多个任务的能力,在时间上,这些任务是交替执行(或是部分顺序执行)的,操作系统通过快速切换时间片,让多个任务交替执行,看起来就像是多个任务同时执行一样。

并行指的是操作系统在多个处理器核心上同时执行不同的任务。

关键区别:
1. 执行方式的不同:
- 并发是在时间上交替执行,通过任务切换和调度实现。
- 并行是在空间上同时执行,任务各自运行在不同的处理器核心上。
2. 任务间的关系:
- 并发中任务可能会有共享资源的情况,需要考虑同步和竞争问题。
- 并行中任务彼此独立,无需考虑资源共享的问题。

进程(Thread)和线程(Process)

  • 进程(Process):进程是操作系统进行资源分配和调度的基本单位。它是一个程序在某个数据集上的一次运行活动,拥有独立的内存空间和系统资源。
  • 线程(Thread):线程是进程内的一个执行单元,是CPU调度和分派的基本单位。线程在进程的上下文中运行,共享进程的资源。

通信方式:

  • 进程间通信(IPC):管道,消息队列,信号量,共享内存。
  • 线程通过读写共享内存中的变量进行通信。

资源拥有:

  • 每个进程都有自己独立的内存空间,包括代码、数据和堆栈等。进程之间的资源是隔离的,一个线程的崩溃通常不会影响其他进程。
  • 线程共享进程的内存空间和资源。

线程安全

上文提到,线程共享进程的内存空间和资源。当多个线程同时访问和修改共享资源时,就会发生竞争问题,线程安全是指在多个线程同时访问共享资源的情况下,保持一致性。

线程不安全的原因

  • 原子性问题

    对于数据的读取和修改操作,需要原子性的保证,即在读取和修改的过程中,不能被其他线程中断。
    如果被中断,操作没有被原子性的执行,可能会出现数据结果不一致或计算错误。

  • 可见性问题

    在共享变量中,当一个线程对其访问修改时,对其他线程不可见,就无法保证数据的一致性。

  • 有序性问题

    在并发中,线程的执行顺序时不确定的,不同的线程会以不同的顺序访问修改共享数据。

    如何线程依赖于特定的执行顺序,但是没有保证线程的执行顺序,也会出现不一致的问题。

死锁

死锁发生在并发过程中,多个线程相互持有对方所需要的资源,同时无法释放,导致线程相互等待。

死锁产生条件(4点缺一不可)

  • 互斥:至少有一个资源时排他性的,即同一时间只能被一个线程或进程占有。
  • 持有并等待:一个线程因持有一个资源而继续请求另一个被其他线程占有的资源。
  • 非抢占:资源只能通过持有它的线程来释放,不能被抢占。
  • 循环等待:若干线程之间形成一个循环链,每个线程都在等待下一个线程所持有的资源。

如何避免死锁

对于上述四个产生条件,破坏之一即可。

线程的生命周期

graph LR;
    A[创建] --> B[就绪];
    B --> C[运行];
    C --> D[阻塞或等待];
    D --> B;
    C --> E[终止];

Java 中的线程生命周期

stateDiagram-v2
    direction LR
    新建 --> 可运行: start()
    可运行 --> 阻塞: synchronized block/method
    阻塞 --> 可运行: lock released
    可运行 --> 等待: wait()
    等待 --> 可运行: notify()/notifyAll()
    可运行 --> 计时等待: wait(long), sleep(long), join(long)
    计时等待 --> 可运行: time out, notify()/notifyAll()
    可运行 --> 终止: run() completes
    阻塞 --> 终止: Exception
    等待 --> 终止: Exception
    计时等待 --> 终止: Exception

CPU 调度算法

对象是进程

先来先服务(First-Come, First-Served, FCFS)

最基本的进程调度算法,按照进程到达就绪队列的顺序进行调度。

特点:

  • 非抢占:一旦一个线程开始执行,它将一直运行直到完成或阻塞,中间不会被其他线程抢占。
  • 平均等待时间较长:如果先到达长作业,那么后续到达的短作业可能会等待时间过长。
  • 可能导致饥饿:短作业可能会被长作业阻塞,导致饥饿,即短作业长时间得不到服务。
  • 适用于 I/O 密集型作业:CPU 在等待 I/O 操作完成时,可以执行其他进程。

短作业优先(Shortest Job First, SJF)

优先执行预计执行时间最短的进程。

实现方式:

  1. 非抢占式短作业优先(Non-Preemptive SJF):一旦一个线程开始执行,中间不会被抢占,它将一直运行到完成或阻塞状态。操作系统只有在阻塞或者完成状态下才能进行调度。
  2. 抢占式短作业优先(Preemptive SJF):允许操作系统在任何时候中断当前正在执行的进程,运行一个预计剩余执行时间更短的进程(如果就绪队列中有这个进程)。

优点是可以减少进程的平均等待时间和平均周转时间。

缺点是可能导致饥饿现象,即长时间运行的进程可能永远不会被调度。

还有就是进程的执行时间可能会预计不准确,达不到算法的预计效果。

优先级调度

基于进程优先级的调度方法。每个进程被赋予一个优先级值,操作系统根据这个值来决定哪个进程应该获得 CPU 时间。

特点:

  • 优先级的定义:进程的优先级可以基于多种因素,如进程类型、预计执行时间、资源需求等。
  • 优先级调度可以是抢占式的,也可以是非抢占式的。

时间片轮转(Round Robin, RR)

为每个进程分配一个固定的时间单元,称为时间片。在这个时间片内,进程可以连续执行。当时间片用完时,如果进程还没有完成,它将被放回就绪队列的末尾,等待下一个轮转周期再次被调度执行。

特点:

  • 公平性:所有进程都有机会在固定的时间间隔内执行,这有助于避免某些进程长时间占用 CPU。
  • 响应性:由于时间片的存在,即使是长作业也能在较短的时间内得到响应。
  • 抢占式:当前进程在时间片用尽时会被中断。
  • 适用于分时系统:可以保证多个用户或者进程公平的共享 CPU 资源。