1.6 多线程编程的优势和风险
多线程编程具有以下优势。
• 提高系统的吞吐率(Throughput)。多线程编程使得一个进程中可以有多个并发(Concurrent,即同时进行的)的操作。例如,当一个线程因为I/O操作而处于等待状态时,其他线程仍然可以执行其操作。
• 提高响应性(Responsiveness)。在使用多线程编程的情况下,对于GUI软件(如桌面应用程序)而言,一个操作慢(比如从服务器上下载一个大文件)并不会导致软件界面出现被“冻住”的现象而无法响应用户的其他操作;对于Web应用程序而言,一个请求的处理速度慢并不会影响对其他请求的处理[11]。
• 充分利用多核(Multicore)CPU资源。如今多核CPU的设备越来越普及,就算是手机这样的消费类设备也普遍使用多核CPU。实施恰当的多线程编程有助于我们充分利用设备的多核CPU资源,从而避免了资源浪费。
• 尽量少地使用系统资源。一个进程中的多个线程可以共享其所在进程申请的资源(如内存空间),因此使用多个线程相比于使用多个进程进行编程来说,节约了系统资源。
• 简化程序的结构。线程可以简化复杂应用程序的结构。
多线程编程也有自身的问题与风险,包括:
• 线程安全(Thread Safe)问题。当多个线程共享数据的时候,如果没有采取相应的并发访问控制措施,那么就可能产生数据一致性问题,如读取脏数据(过期的数据)、丢失更新(某些线程所做的更新被其他线程所做的更新覆盖)等。使用锁(包括synchronized关键字和ReentrantLock等)能够保证线程安全是大家所熟知的,本书后续章节则会提供一些不借助锁来实现线程安全的方案。
• 线程的生命特征(Thread Liveness)问题。一个线程从创建到运行结束的整个生命周期会经历若干个状态。从单个线程的角度来看,RUNNABLE状态是我们所期望的状态。但实际上,代码编写不适当可能导致某些线程一直处于等待其他线程释放锁的状态(BLOCKED状态),即产生了死锁(Dead Lock)。例如,线程T1拥有锁L1,并试图获取锁L2,而此时线程T2拥有锁L2并试图获取锁L1,这就导致线程T1和T2一直处于等待对方释放锁且又得不到锁的状态。当然,一直忙碌的线程也可能会出现问题,它可能面临活锁(Live Lock)问题,即一个线程一直在尝试某个操作但就是没有进展,这就好比室内窗户上的苍蝇向着阳光飞却一直没能飞出去一样。另外,线程是一种稀缺的计算资源,一个系统所拥有的CPU数量相比于该系统中存在的线程数量而言总是少之又少的。某些情况下可能出现线程饥饿(Starvation)的问题,即某些线程永远无法获取被CPU执行的机会而永远处于RUNNABLE状态的READY子状态。
• 上下文切换。由于CPU资源的稀缺性,上下文切换可以看作多线程编程的必然副产物,它增加了系统的消耗,不利于提高系统的吞吐率。
• 可靠性。多线程编程一方面有利于可靠性,例如,某个线程意外提前停止了,但这并不影响其他线程继续执行各自的处理操作。另一方面,线程是进程的一个组件,它总是存在于特定的进程中,如果这个进程由于某种原因意外提前停止了,比如,某个Java进程由于内存泄漏导致Java虚拟机崩溃而意外停止了,那么该进程中所有的线程也就随之无法继续运行。因此,从提高软件可靠性的角度来看,在某些情况下可能要考虑多进程、多线程的编程方式[12],而非简单的单进程多线程的编程方式。