前言

在Java并发编程中线程的使用尤为重要。了解线程的由来,使用场景及注意事项是作为一个合格的Java程序员必备的技能。本文章中会对线程的由来、进程与线程的区别、及线程的使用场景进行简单介绍。希望通过该文章,小伙伴们能对线程有一个更深的了解。

从操作系统发展了解线程

线程的出现,离不开进程。而进程的出现又离不开操作系统。操作系统的发展促进了线程与进程的技术崛起。所以了解操作系统的发展,对我们理解线程尤为重要。整个操作系统大致分为如下几个阶段:

  • 手工操作
  • 单道批处理系统
  • 多道批处理系统
  • 分时操作系统
  • 等等等

在前三个阶段中,对进程与线程的理解尤为重要,故文章会着重介绍前三个阶段。

手工操作

最早的计算机并位出现真正意义上的操作系统,这时的计算机智能解决简单的数学问题,比如正铉,余铉等。其运行方式也特别简单,程序员将对应于程序和数据的已穿孔的纸带(或卡片)装入输入机,然后启动输入机把程序和数据输入计算机内存,接着通过控制台开关启动程序针对数据运。计算完毕后,输出机输出计算结果;用户取走结果并卸下纸带(或卡片)后,才让下一个用户上机。举个简单的列子,假设我们需要向计算机发送吃饭、洗澡、睡觉这三个指令,那么在传统计算机中,我们可以得到下图:

 

java多线程Stream closed Java多线程演进史_批处理系统

 

 

从上图中我们可以明显看出,手工操作方式严重损害了系统的利用率。在等待用户输入指令时,计算机一直处于闲置状态。

单道批处理系统

为了摆脱手动操作带来的耗时性,实现作业(程序、数据、指令)的自动过渡。接着又出现了单道批处理系统。单道批处理系统在原来手动操作主要的区别是在输入机与主机之间增加了一个存储设备磁带(盘)(下图,红色虚线部分),并在主机系统上配上监督程序,其具体运行方式通常是把一批作业以输入到磁带上,然后由监督程序将磁带上的第一个作业装入内存,并把运行控制权交给该作业。当该作业处理完成时,又把控制权交还给监督程序,再由监督程序把磁带(盘)上的第二个作业调入内存。计算机系统就这样自动地一个作业一个作业地进行处理,直至磁带上的所有作业全部完成。还是以上文吃饭、洗澡、睡觉这三个指令为例子,我们可以得到下图:

 

java多线程Stream closed Java多线程演进史_数据_02

 

 

需要注意的是,虽然单道批处理操作系统能够解决手动操作时需要人工切换作业导致的系统利用率低的问题,但是又因为单道批处理系统是将作业一个一个加入内存的,那么某一个作业因为等待磁带(盘)或者其他I/O操作而暂停时,那计算机就只能一直阻塞,直到该I/O完成。对于CPU操作密集型的程序,I/O操作相对较少,因此浪费的时间也很少。但是对于I/O操作较多的场景来说,CPU的资源是属于严重浪费的。

多道批处理系统

为了解决单道批处理系统因为输入/输出(I/O)请求后,导致计算机等待I/O完成而造成的计算机的资源的浪费。接下来又出现了多道批处理系统。多道批处理系统与单道批处理系统的主要区别是在内存中允许一个或多个作业,当一个作业在等待I/O处理时,多批处理系统会通过相应调度算法调度另外一个作业让计算机执行。从而使CPU的利用率得到更大的提高。如下图所示:

 

java多线程Stream closed Java多线程演进史_Android_03

 

 

进程的由来

在多道批处理系统中引申出了一个非常重要的模式,即允许多个作业进入内存并运行。由于在内存中存储了多个作业,那么多个作业如何进行区分?当某个作业因为等待I/O暂停时,怎么恢复到之前的运行状态呢?

所以这个时候,聪明的人们就发明了进程这一概念,用进程来保存每个作业的数据与运行状态,同时对每个进程划分对应的内存地址空间(代码、数据、进程空间、打开的文件),并且要求进程只能使用它自己的内存空间。那么就可以达到作业的区分及恢复。

线程的由来

多道批处理系统所引入的进程的概念,确实提高了计算机的运行效率,但是单单使用进程来处理程序的并发的弊端也越来与突出,因为一个进程在一个时间段内只能做一件事情。如果某个程序有多个任务,只能逐个执行这些任务。

并发:宏观上看起来同一个时间段有多个任务在计算机中执行。

还是以上文提到的吃饭为例子。假设在吃饭进程中包含两个任务,一个看电视任务,一个吃饭任务。现在我们希望计算机一边执行吃饭任务,一边执行看电视任务。那么根据多道批处理系统的运行规则,计算机实际调度是以进程为调度单位的,那么我们想一边吃饭,一边看电视的行为,只能拆分为两个进程。但是我们知道进程中存储了大量信息(数据,进程运行状态信息等)。那么当计算机进行进程切换的时候,必然存在着很大的时间与空间消耗(因为每个进程对应不同内存地址空间,进程的切换,实际是处理器处理不同的地址空间)。如果不拆分为两个进程,吃饭这两个任务在同一个进程中,那么这两个任务必然是依次执行的,这又违背了我们一边看电视一边吃饭的初衷。

那么问题来了,我们是否可以实现进程中任务的切换,又可以避免进程切换内存地址空间呢?聪明的人们又进一步的发明了线程这一概念。用线程表示进程中的不同任务,同时又将计算机实际调度的单元转到线程。这样就避免了进程的内存地址空间的切换,也达到了多任务的并发执行。具体如下图所示:

 

java多线程Stream closed Java多线程演进史_Android_04

 

 

进程与线程的区别

前面讲解了进程与线程的由来,有可能大家现在还有一丝疑惑,下面就让我们一起来总结一下进程与线程的关系。

  • 进程是计算机分配资源的单元,而线程是计算机调度的基本单元。
  • 一个进程由一个或多个线程组成。线程代表着进程中不同的任务。
  • 进程之间相互独立,同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)
  • 进程的切换由时空开销。

进程与线程的关系如下图所示:

java多线程Stream closed Java多线程演进史_批处理系统_05

 

线程的使用场景

线程的出现确实提高了程序运行的性能,那么线程的使用场景有哪些呢?

  • 执行后台任务,在很多场景中,可能会有一些定时的批量任务,比如定时发送短信、定时生成批量文件。在这些场景中可以通过多线程的来执行。
  • 异步处理,比如在用户注册成功以后给用户发送优惠券或者短信,可以通过异步的方式来执行,一方面提升主程序的执行性能;另一方面可以解耦核心功能,防止非核心功能对核心功能造成影响。
  • 分布式处理,比如fork/join,将一个任务拆分成多个子任务分别执行。
  • BIO模型中的线程任务分发,也是一种比较常见的使用场景,一个请求对应一个线程。

线程使用注意事项

虽然在程序中我们可以创建多个线程来提高程序的运行效率与吞吐率,但是也会出现以下问题:

  • 由于多个线程共同占有所属进程的资源和地址空间,那么多个线程要同时访问某个资源,这个时候该怎么处理?
  • 线程既然能提高运行效率,那么是否在程序中创建越多线程越好?

这些问题其实是关于线程同步与合理的创建线程数的问题。这里就不做过多的介绍,如果大家对这部分感兴趣,可以关注后续文章或查阅相关资料。