Java多线程带来的风险问题讨论
java对线程的支持其实是一把双刃剑。虽然java提供了响应的语言和库,以及一种明确的跨平台内存模型(该内存模型实现了java中开发“编写一次,随处运行”的并发应用程序),这些工具简化了并发应用程序的开发,但同时也提高了对开发人员的技术要求,因为在更多的程序中会使用线程。当线程还是一项鲜为人知的技术时,并发性是一个“高深的”主题,但现在主流开发人员都必须了解线程方面的内容,同时也带来了一定的风险:
安全性问题
线程安全性可能是非常复杂的,在没有充足同步的情况下,多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的后果。看一下下面的程序:
public class UnsafeSequence{
private int value;
public int getNext(){
return value++;
}}
类中将产生一个整数值序列,该序列中的每个值都是唯一的。在这个类中简要地说明了多个线程之间的交替操作将如何导致不可预料的结果。在单线程环境中,这个类能正确工作,但在多线程环境中则不能。
问题在于,如果执行时机不对,那么两个线程在调用getNext方法时会得到相同的值。下图给出了这种错误情况。
打开百度APP,查看更多高清图片
虽然递增运算value++看上去是单个操作,但事实上它包含三个独立的操作:读取value,将value加1,并将计算结果写入value。由于运行时可能将多个线程之间的操作交替执行,因此这两个线程可能同时执行读操作,从而使他们得到相同的值,并都将这个值加1。结果就是,在不同线程的调用中返回了相同的数值。上图中,AB代表两个线程,执行时序按照从左到右的顺序递增,每行表示一个线程的动作。这些交替执行示意图给出的是最糟糕的执行情况,目的是为了说明如果错误地假设程序中的操作将按照某种特性顺序来执行,那么会存在各种各样的危险。
UnsafeSequence类中说明的是一种常见的并发安全问题,成为竞态条件,在多线程环境下,getNext是否会返回唯一的值,要取决于运行时对线程中操作的交替执行方式,这使我们无法控制的。
由于多个线程要共享相同的内存地址空间,并且是并发运行,因此他们可能会访问或修改其他线程正在使用的变量。当然,这是一种极大的便利,因为这种方式比其他线程间通信机制更容易以实现数据共享。但它同样也带来了巨大的风险:线程会由于无法预料的数据变化而发生错误。当多个线程同时访问和修改相同的变量时,将会在串行编程模型中引入非串行因素,而这种非串行性是很难分析的。要使多线程程序的行为可以预测,必须对共享变量的访问操作进行协同,这样才不会在线程之间发生彼此干扰。幸运的是,java提供了各种同步机制来协同这种访问。
通过将getNext修改为一个同步方法,可以修复UnsafeSequence中的错误,如下:
public class UnsafeSequence{
private int value;
public synchronized int getNext(){
return value++;
}}
synchronized意思就是同步,是一个锁,保证了多线程按顺序调用此方法。如果没有同步,那么,无论是编译器、硬件还是运行时,都可以随意安排操作的执行时间和顺序,例如对寄存器或者处理器中的变量进行缓存,而这些被缓存的变量对于其他线程来说暂时(甚至永久)不可见的。虽然这些技术有助于实现更高的性能,并且通常也是值得采用的方法,但它们也为开发人员带来负担,因为开发人员必须找出这些数据在哪些位置被多个线程共享,只有这样才能使这些优化措施不被破坏线程安全性。
活跃性问题
在开发并发代码时,一定要注意线程安全性是不可破坏的。安全性不仅对于多线程程序很重要,对于单线程程序同样重要。此外,线程还会导致一些在单线程程序中不会出现的问题,例如活跃性问题。
安全性的含义是“永远不发生糟糕的事情”,而活跃性则关注另一个目标,即“某件正确的事情最终会发生”。当某个操作无法继续执行下去时,就会发生活跃性问题。在串行程序中,活跃性问题的形势之一就是无意中造成的无限循环,从而使循环之后的代码无法得到执行。线程将带来其他一些活跃性问题。例如,如果线程A在等待线程B释放其持有的资源,而线程B永远都不释放该资源,那么A久永久地等待下去。与大多数并发性错误一样,导致活跃性问题的错误同样是难以分析的,因为他们依赖于不同线程的事件发生时序,因此在开发或者测试中不总是能够重现的,这是最让开发人员头疼的问题。
性能问题
与活跃性问题密切相关的是性能问题。活跃性意味着某件正确的事情最终会发生,但却不够好,因为我们通常希望正确的事情尽快发生。性能问题包括多个方面,例如服务时间过长,响应不灵敏,吞吐率过低,资源消耗过高,或者可伸缩性较低等。与安全性和活跃性一样,在多线程程序中不仅存在于单线程程序相同的性能问题,而且还存在由于使用线程而引入的其他性能问题。
在设计良好的并发应用程序中,线程能提升程序的性能,但无论如何,线程总会带来某种程度的运行时开销。在多线程层序中,当线程调度器临时挂起活跃线程并运转运行另一个线程时,就会频繁地出现上下文切换操作,这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不是线程的运行上。当线程共享数据时,必须使用同步机制,而这些同步机制往往会抑制某些编译器优化,使内存缓存区中的数据无效,以及增加共享内存总线的同步流量。所有这些因素都将带来额外的性能开销,java中如何减少频繁的上下文切换呢?
1. 无锁并发编程
多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用
锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
2. CAS
Java的Atomic包使用CAS算法来更新数据,而不需要加锁。使用最少线程。
3. 使用最少线程
避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
4. 协程
在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。--GO语言用得多,JAVA很少使用到。