1.2 线程
在早期的计算机操作系统中,拥有资源且独立运行的基本单位是进程。随着计算机技术的发展,进程出现了很多弊端。例如,由于进程是资源拥有者,创建、撤销与切换存在较大的时空开销,需要引入轻量型进程;由于对称多处理机(Symmetric Multi-Processor, SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
因此,在20世纪80年代,出现了能独立运行的基本单位——线程(Thread)。
1.2.1 什么是线程
线程是程序执行流的最小单元。一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程只拥有少量在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间相互制约,线程在运行中呈现出间断性。
线程有三种基本状态:就绪、阻塞和运行。线程的状态图如图1-3所示。
图1-3 线程的状态图
就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机,正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程,是进程内一个相对独立的、可调度的执行单元。在单个程序中同时运行多个线程完成不同的工作,称为多线程。在大多数情况下,多线程能提升程序的性能。
1.2.2 进程和线程
进程和线程是并发编程的两个基本的执行单元。在大多数编程语言中,并发编程主要涉及线程。
一个计算机系统通常有许多活动的进程和线程。在给定的时间内,每个处理器中只能有一个线程得到真正的运行。对于单核处理器来说,处理时间是通过时间切片在进程和线程之间进行共享的。
进程有一个独立的执行环境。进程通常有一个完整的、私人的基本运行时资源。每个进程都有自己的内存空间。操作系统的进程表(Process Table)存储了CPU寄存器值、内存映像、打开的文件、统计信息和特权信息等。进程一般定义为执行中的程序,也就是当前操作系统的某个虚拟处理器上运行的一个程序。多个进程并发共享同一个CPU和其他硬件资源,操作系统支持进程之间的隔离。这种并发透明性需要付出相对较高的代价。
进程往往被等同为程序或应用程序。然而,用户看到的一个单独的应用程序可能实际上是一组合作的进程。大多数操作系统都支持进程间通信(Inter Process Communication, IPC),如管道和socket。IPC不仅用于同一个系统的进程之间的通信,也用于不同系统的进程之间的通信。
线程,有时被称为轻量级进程(Lightweight Process, LWP)。进程和线程都提供了一个执行环境,但创建一个新的线程比创建一个新的进程需要更少的资源。线程系统一般只维护用来让多个线程共享CPU所必需的最少量信息,特别是线程上下文(Thread Context)中一般只包含CPU上下文及某些其他线程管理信息。通常忽略那些对于多线程管理而言不必要的信息。这样,单个进程中防止数据遭到某些线程不合法的访问的任务就完全落在了应用程序开发人员的肩上。线程不像进程那样彼此隔离,以及受到操作系统的自动保护,所以在多线程程序开发过程中需要开发人员做更多的努力。
线程存在于进程中,每个进程至少有一个线程。线程共享进程的资源,包括内存和打开的文件,尽管这使得工作变得高效,但也存在一个潜在的问题——通信。关于通信的内容,会在后面的章节中讲述。
现在多核处理器或多进程的计算机系统越来越流行,这大大增强了系统的进程和线程的并发执行能力。即便在没有多处理器或多进程的系统中,并发仍然是可能的。关于并发的内容,会在后面章节中讲述。
1.2.3 线程和纤程
为了提高并发量,某些编程语言中提供了“纤程”(Fiber)的概念,比如Golang的goroutine、Erlang风格的actor。Java语言虽然没有定义纤程,但仍有一些第三方库供选择,比如Quasar。纤程可以理解为比线程颗粒度更细的并发单元。
纤程是以用户方式代码来实现的,并不受操作系统内核管理,所以内核并不知道纤程,也就无法对纤程实现调度。纤程是根据用户定义的算法来调度的。因此,就内核而言,纤程采用了非抢占式调度方式,而线程是抢占式调度的。
一个线程可以包含一个或多个纤程。线程每次执行哪一个纤程的代码,是由用户来决定的。
所以,对于开发人员来说,使用纤程可以获得更大的并发量,但同时要面临自己实现调度纤程的复杂度。
1.2.4 编程语言中的线程对象
在面向对象语言开发中,每个线程都与Thread类的一个实例相关联。由于Java语言较流行,下面将用Java来实现并使用线程对象,作为并发应用程序的基本原型。
1.定义和启动一个线程
Java中有两种创建Thread实例的方式。
● 提供Runnable对象。Runnable接口定义了一个方法run,用来包含线程要执行的代码。HelloRunnable示例如下。
public class HelloRunnable implements Runnable { @Override public void run() { System.out.println("Hello from a runnable! "); } public static void main(String[] args) { (new Thread(new HelloRunnable())).start(); } }
● 继承Thread类。Thread类本身是Runnable的实现,只是它的run方法什么都没干。
HelloThread示例如下。
public class HelloThread extends Thread { @Override public void run() { System.out.println("Hello from a thread! "); } public static void main(String[] args) { (new HelloThread()).start(); } }
请注意,这两个例子调用start来启动线程。
第一种方式使用Runnable对象,在实际应用中更普遍,因为Runnable对象可以继承Thread以外的类。第二种方式在简单的应用程序中更容易使用,但受理的任务类必须是一个Thread类的后代。本书推荐使用第一种方式,将Runnable任务从Thread对象中分离来执行任务。这样不仅更灵活,而且适用于高级线程管理API。
Thread类还定义了大量的方法用于线程管理。
2.使用sleep来暂停执行
Thread.sleep可以让当前线程执行暂停一个时间段,这样处理器的时间就可以给其他线程使用。
sleep有两种重载形式:一种是指定睡眠时间为毫秒级,另一种是指定睡眠时间为纳秒级。然而,这些睡眠时间不能保证是精确的,因为它们是由操作系统提供的并受其限制。此外,睡眠周期也可以通过中断来终止,我们将在后面的章节中看到。
SleepMessages示例——使用sleep每隔4秒打印一次消息:
public class SleepMessages { public static void main(String[] args) throws InterruptedException { String importantInfo[] = { "Mares eat oats", "Does eat oats", "Little lambs eat ivy", "A kid will eat ivy too" }; for (int i = 0; i < importantInfo.length; i++) { // 暂停4秒 Thread.sleep(4000); // 打印消息 System.out.println(importantInfo[i]); } } }
请注意,main声明抛出InterruptedException。如果sleep是激活的,若有另一个线程中断当前线程,则sleep抛出异常。因为该应用程序还没有定义另一个线程来引起中断,所以考虑捕捉InterruptedException。
3.中断(interrupt)
中断表明一个线程应该停止它正在做和将要做的事。线程通过Thread对象调用interrupt实现中断。为了使中断机制正常工作,被中断的线程必须支持自己的中断。
支持中断
如何实现线程支持自己的中断?这要看它目前正在做什么。如果线程调用方法频繁抛出InterruptedException异常,那么它只要在run方法捕获异常之后返回即可。例如:
for (int i = 0; i < importantInfo.length; i++) { // 暂停4秒 try { Thread.sleep(4000); } catch (InterruptedException e) { // 已经中断,不需要更多信息 return; } // 打印消息 System.out.println(importantInfo[i]); }
很多方法都会抛出InterruptedException,如sleep被设计成在收到中断时立即取消它们当前的操作并返回。
若线程长时间没有调用方法抛出InterruptedException,那么它必须定期调用Thread.interrupted,该方法在收到中断后返回true。
for (int i = 0; i < inputs.length; i++) { heavyCrunch(inputs[i]); if (Thread.interrupted()) { // 已经中断,不需要更多信息 return; } }
在这个简单的例子中,代码简单地测试该中断,如果已接收中断线程就退出。在更复杂的应用程序中,它可能会更有意义地抛出一个InterruptedException:
if (Thread.interrupted()) { throw new InterruptedException(); }
中断状态标志
中断机制是使用被称为中断状态的内部标志实现的。调用Thread.interrupt可以设置该标志。当一个线程通过调用静态方法Thread.interrupted来检查中断时,中断状态被清除。非静态isInterrupted方法用于线程查询另一个线程的中断状态,而不会改变中断状态标志。
按照惯例,任何方法因抛出一个InterruptedException而退出都会清除中断状态。当然,它可能因为另一个线程调用interrupt而让那个中断状态立即被重新设置。
4.join方法
join方法允许一个线程等待另一个线程完成。假设t是一个正在执行的Thread对象,那么
t.join();
会导致当前线程暂停执行直到t线程终止。join方法允许程序员指定一个等待周期,与sleep一样,等待时间依赖于操作系统的时间,同时不能假设join方法的等待时间是精确的。
像sleep一样,join方法通过InterruptedException退出来响应中断。
1.2.5 SimpleThreads示例
SimpleThreads示例有两个线程。第一个线程是每个Java应用程序都有的主线程。主线程创建Runnable对象的MessageLoop,并等待它完成。如果MessageLoop需要很长时间才能完成,主线程就中断它。
该MessageLoop线程打印出一系列消息。如果中断之前就已经打印了所有消息,则MessageLoop线程打印一条消息并退出。
public class SimpleThreads { // 显示当前执行线程的名称、信息 static void threadMessage(String message) { String threadName = Thread.currentThread().getName(); System.out.format("%s: %s%n", threadName, message); } private static class MessageLoop implements Runnable { public void run() { String importantInfo[] = { "Mares eat oats", "Does eat oats", "Little lambs eat ivy", "A kid will eat ivy too" }; try { for (int i = 0; i < importantInfo.length; i++) { // 暂停4秒 Thread.sleep(4000); // 打印消息 threadMessage(importantInfo[i]); } } catch (InterruptedException e) { threadMessage("I wasn't done! "); } } } public static void main(String args[]) throws InterruptedException { // 在中断MessageLoop线程(默认为1小时)前延迟一段时间(单位是毫秒) long patience = 1000 * 60 * 60; // 如果命令行参数出现 // 设置patience的时间值 // 单位是秒 if (args.length > 0) { try { patience = Long.parseLong(args[0]) * 1000; } catch (NumberFormatException e) { System.err.println("Argument must be an integer."); System.exit(1); } } threadMessage("Starting MessageLoop thread"); long startTime = System.currentTimeMillis(); Thread t = new Thread(new MessageLoop()); t.start(); threadMessage("Waiting for MessageLoop thread to finish"); // 循环,直到MessageLoop线程退出 while (t.isAlive()) { threadMessage("Still waiting..."); // 最长等待1秒 // 交给MessageLoop线程来完成 t.join(1000); if (((System.currentTimeMillis() - startTime) > patience) && t.isAlive()) { threadMessage("Tired of waiting! "); t.interrupt(); // 等待 t.join(); } } threadMessage("Finally! "); } }
对Java的Thread类感兴趣的读者可以查看其在线API:https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html。
上面例子的代码可以在https://github.com/waylau/distributed-systems-technologies-and-cases-analysis的distributed-systems-java-demos程序的com.waylau.essentialjava.thread包下找到。