概述
Java对线程的支持其实是一把双刃剑。如果使用得当,线程可以有效的降低程序的开发和维护成本,同时提升复杂应用的性能。优点如下:
- 发挥多处理器的能力,提高系统吞吐率
- 简化建模(例如Servlet和RMI远程方法调用):
复杂工作简化为一组简单的并且同步的工作流,每个工作流在单独的线程中运行,并在特定的地方进行交互。 - 简化异步事件处理
- 更灵敏的用户响应界面
然而,线程也带来了一些额外的问题。
安全性问题
安全性的含义是“永远不发生糟糕的事情”。
看下面的代码:
在单线程的环境中,这个类可以正常工作,产生正确的结果,但是在多线程的则不能。
UnsafeSequence的问题在于,如果执行的时间不对,那么两个线程在调用getNext方法时会得到相同的值。看下图:
这种错误出现的情况是:虽然 value++ 看上去是一个原子操作,但事实上它包含了3个操作:读取value、将value的值+1、将结果写入value。由于运行的时候多个线程存在交替执行的情况,因此这两个线程可能同时执行读操作,从而使它们得到相同的值。结果也就是不同的线程的调用返回了相同的值,而这不是我们想要的结果。
这是一个并发安全问题,称为 竞态条件(Race Condition) 。在多线程环境下,getNext是否会返回唯一的数值,要取决于运行时对线程中操作的交替执行方式,这显然不正确。
由于多个线程要共享相同的内存地址空间,并且是并发操作,因此它们可能会访问或修改其他线程正在使用的变量。当然,这是一种极大的便利,因为这种方式比其他线程间通讯机制更容易实现数据共享。但是它同样带来了巨大的风险:线程由于无法预料数据变化而发生错误,当多个线程同时访问或修改相同的变量时,将会在串行编程模型中引入非串行因素,而非串行性是很难分析的。要使多线程程序的行为进行预测,必须对共享变量的访问操作进行协同,这样才不会在线程之间发生干扰。幸运的是,Java提供了各种同步机制来协同这种访问。
看下面代码:
我们将getNext修改为一个同步的方法,修复竟态条件问题。
在开发并发代码时,一定要注意 线程安全性是不可破坏的。
活跃性问题
活跃性的含义是“某件正确的事情一定会发生”。
当某个操作无法继续执行下去的时候,就会发生活跃性问题。在串行程序中,活跃性问题就是无意中造成的无限循环,从而使得循环之后的代码无法得到执行。线程将带来其他一些活跃性问题。
比如,线程A在等待线程B释放所持用的资源,而线程B永远都不释放持有的资源,那么A就会永久地等下去。就是一种 饥饿现象,除此之外还有死锁以及活锁。
与大多数并发性错误一样,导致活跃性问题的错误同样是难以分析的,因为它们依赖于不同线程时间发生的时序。
性能问题
性能问题的含义是“正确的事情能尽快发生”。
活跃性意味着某件正确的事情最终一定会发生,但却不够好,因为我们同城希望正确的事情尽快发生。性能问题包括多个方面,例如服务时间过长、响应不灵敏、吞吐率过低、资源消耗过高、可伸缩性较低等。
在设计良好的并发应用程序中,线程能提升程序的性能。但无论如何,线程总会带来某种程度的运行时开销。在多线程程序中,但线程调度器临时挂起并转而运行另一个线程时,就会频繁地出现 上下文切换(Context Switch),这种操作将带来极大的开销。当线程共享数据时,必须使用同步机制,而这些同步机制往往会压抑某些编译器优化,使内存缓冲区中的数据无效,以及增加共享内存总线的同步流量。所有这些操作都将带来额外开销。
参考文档
- 《Java并发编程实战》Brain Goetz等著 机械工业出版社