1. 线程的优势
如果使用得当,线程可以有效降低程序开发和维护成本,同时提升复杂应用程序的性能。线程的作用主要表现在这几个方面:发挥多处理器的强大能力、建模的简单性、异步事件的简化处理和响应更加灵敏的用户界面。
发挥多处理器的强大能力:操作系统基本的调度单位是线程,因此如果在程序中只有一个线程,那么最多同时只能在一个处理器上运行,而现在多处理器系统非常常见,因此多线程才能发挥多处理器的强大能力。另一方面,即使是单处理器系统,如果仅有一个线程运行,那么当程序在等待I/O操作完成时,处理器将处于空闲状态。而在多线程程序中,如果一个线程在等待I/O操作完成,另一个线程可以继续运行。
建模的简单性:对于一个复杂软件来说,如果为每种类型的任务都分配一个专门的线程,那么可以形成一种串行执行的假象,并将程序的执行逻辑、调度细节、异步IO以及资源等待等问题分离开来。这样,通过线程就将复杂并且异步的工作流进一步分解为一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置进行交互。
一些现有的框架可以实现上述目标,例如Servlet和RMI,这些框架负责解决一些细节问题,例如请求管理、线程创建、负载平衡,并在正确的时刻将请求分发给正确的应用程序组件。
异步事件的简化处理:服务器应用程序在接受来自多个远程客户端的套接字连接请求时,如果为每个连接都分配其各自的线程并且使用同步I/O,那么就会降低这类程序的开发难度。如果某个应用程序执行读操作而此时还没有数据到来,那么这个读操作将一直阻塞,直到有数据到达。在单线程应用程序中,这不仅意味着在处理请求的过程中将停顿,而且还意味着在这个线程被阻塞期间,对所有请求的处理都将停顿。为避免这个问题,单线程服务器应用程序必须使用非阻塞I/O,这种I/O的复杂性要远远高于同步I/O,并且很容易出错。后者,如果每个请求都拥有自己的处理线程,那么在处理某个请求时发生的阻塞将不会影响其它请求的处理。
响应更灵敏的用户界面:略。
2. 线程带来的风险
Java对线程的支持其实是一把双刃剑。虽然Java提供了相应的语言和库,以及一种明确的跨平台内存模型,这些工具简化了并发应用程序的开发,但同时也提高了对开发人员的技术要求。线程带来的问题有:安全性问题、活跃性问题和性能问题。
2.1安全性问题
线程安全性可能是非常复杂的,在没有充分同步的情况下,多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果。
在下面这个程序中就有可能会出现这种问题:
@NotThreadSafe public class UnsafeSequence { private int value; //返回一个唯一的数值 public int getNext() { return value++; } }
这段代码的问题在于,如果执行时机不对,那么两个线程在调用getNext时会得到相同的值。原因在于,value++并不是单个操作,它包含3个独立的操作:读取value、将value加1、将计算结果写入value。由于运行时可能将多个线程之间的操作交替执行,因而可能两个线程同时进行读操作,从而使它们获取相同的值,经过加1之后又返回了相同的值。
在UnsafeSequence类中说明的是一种常见的并发安全问题,称为竞态条件(Race Condition)。在多线程环境下,getNext是否会返回唯一的值,要取决于运行时对线程中操作的交替执行方式,这并不是我们希望看到的。
由于多个线程要共享相同的内存地址空间,并且是并发运行,因此它们可能会访问或修改其它线程正在使用的变量。这虽然带来了极大的便利,因为这种方式比其它线程间通信机制更容易实现数据共享。但是它也带来了巨大的风险:线程会由于无法预料的数据变化而发生错误。当多个线程同时访问和修改相同变量时,将会在串行编程模型中引入非串行因素,而这种非串行性是很难分析的。要使多线程程序的行为可以预测,必须对共享变量的访问操作进行协同,这样才不会让线程之间彼此干扰。
Java提供了一些同步机制来协同这种访问:
@ThreadSafe public class Sequence { @GuardeBy("this") private int value; public synchronized int getNext() { return value++; } }
2.2 活跃性问题
并发还会导致一些在单线程程序中不会出现的问题,例如活跃性问题。安全性的含义是“永远不会发生糟糕的事”,而活跃性则关注另一目标,即“某件正确的事情最终会发生”。当操作无法继续执行下去时,就会发生活跃性问题。
在串行程序中,活跃性问题的形式之一就是无意中造成的无限循环,从而使循环之后的代码无法得到执行。线程带来其它一些活跃性问题,包括:死锁、饥饿以及活锁。与大多数并发行错误一样,导致活跃性问题的错误同样难以分析,因为它们依赖于不同线程的事件发生时序,因此在开发或测试中并不总是能重现。
2.3 性能问题
性能问题包括多个方面,例如服务时间过长、响应不灵敏、吞吐率过低、资源消耗过高、可伸缩性较低等。与安全性和活跃性一样,在多线程程序中不仅存在与单线程程序相同的性能问题,而且还存在由于使用线程而引入的其它性能问题。
在设计良好的并发应用程序中,线程能提升程序的性能,但无论如何,线程总会带来某种程度的运行时开销。在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁出现上下文切换操作,这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不是线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓冲区的数据无效,以及增加共享内存总线的同步流量。所有这些因素都将带来额外的性能开销。