Java 中的多线程计数问题

2024年9月10日 | 阅读 6 分钟

Java 支持单线程操作和多线程操作。单线程程序有一个入口点(main() 方法)和一个出口点。多线程程序有一个初始入口点(main() 方法),随后有许多与 main() 方法并发运行的入口点和出口点。并发(concurrency)这个术语指的是同时进行多个任务。

Java 通过在单个程序中并发运行多个线程,内置了对并发编程的支持。线程(thread),也称为轻量级进程(lightweight process),是编程操作的单一顺序流,有明确的开始和结束。在线程的生命周期中,只有一个执行点。线程本身不是一个程序,因为它无法独立运行。相反,它在一个程序内运行。

多任务处理(或多进程处理)

现代操作系统(如 Windows 和 UNIX)是多任务系统。多任务系统可以通过共享计算资源(如 CPU、主内存和 I/O 通道)来并发执行多个任务。在单 CPU 机器上,一次只能执行一个任务——通过 CPU 的时间分片(time-slicing)。在多 CPU 机器上,可以通过在 CPU 之间分配任务或对 CPU 进行时间分片来同时执行一些任务。

多任务处理对于当今的操作系统来说是必需的,因为它通过充分利用和优化计算资源的利用来提高性能。通常有两种类型的多任务操作系统:

  • 协作式多任务处理系统(Co-operative multitasking systems):每个任务必须自愿将控制权交出给其他任务。这有一个缺点,即一个失控或不合作的任务可能会导致整个系统挂起。
  • 抢占式多任务处理系统(Pre-emptive multitasking systems):任务被分配 CPU 的时间片,一旦其分配时间用完,就会被强制将控制权交出给其他任务。

多线程(在进程内)

在 UNIX 中,我们 fork 一个新进程。在 Windows 中,我们启动一个程序。进程或程序有自己的地址空间和控制块。它被称为重量级(heavyweight),因为它消耗大量系统资源。在一个进程或程序内,我们可以并发运行多个线程以提高性能。

线程与重量级进程不同,它们是轻量级的,并且在单个进程内运行——它们共享该进程的相同地址空间、分配的资源和环境。它之所以是轻量级的,是因为它在重量级进程的上下文中运行,并利用为该程序分配的资源和程序的上下文。线程必须在正在运行的进程内为自己分配资源。例如,一个线程有自己的堆栈、寄存器和程序计数器。在线程内运行的代码仅在该上下文中工作,因此,线程(顺序操作流)也被称为执行上下文(execution context)。

程序内的多线程通过优化系统资源的使用来提高程序的性能。例如,当一个线程被阻塞时(例如,等待 I/O 操作完成),另一个线程可以利用 CPU 时间执行计算,从而提高性能和整体吞吐量。

多线程对于提供更好的用户交互性也是必需的。例如,在文字处理器中,当一个线程正在打印或保存文件时,另一个线程可以用于继续键入。在 GUI 应用程序中,多线程对于提供响应式用户界面至关重要。

在本文中,我将假设我们理解 Swing 编程,因为 Swing 应用程序依赖于多线程(执行其特定功能、重绘和处理事件),并且最适合说明多线程。

一个典型的 Java 程序在一个进程中运行,并且不关心多个进程。然而,在进程内,它通常使用多个线程来并发运行多个任务。一个独立的 Java 应用程序从一个与 main() 方法关联的单个线程(称为主线程)开始。然后,这个主线程可以启动新的用户线程。

Counter.java

输出

Final Count: 6743

在此示例中,多个线程在一个循环中递增计数器。但是,由于 increment 方法未同步,存在竞态条件(race condition)的风险,最终计数可能不如预期。

SynchronizedCounter.java

输出

Final Count: 10000

计数信号量(Counting Semaphore)示例

二元信号量(binary semaphore)被称为具有一个许可的计数信号量,因为它只有两种状态:可用或许不可用。为了执行互斥(mutual exclusion)或关键区域(critical section),其中只允许一个线程执行,可以使用二元信号量。线程在 acquire() 上等待,直到线程在关键区域内通过调用信号量上的 release() 来允许 release。以下是 Java 信号量计数,其中使用二元信号量为关键代码部分提供共享独占访问。

SemaphoreTest.java

输出

Thread-0 inside mutual exclusive 
Thread-1 inside mutual exclusive 
Thread-0 outside of mutual exclusive 
Thread-1 outside of mutual exclusive