前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >并发编程-多线程带来的风险

并发编程-多线程带来的风险

作者头像
ImportSource
发布2018-04-03 16:38:09
1.2K0
发布2018-04-03 16:38:09
举报
文章被收录于专栏:ImportSource

Java 对于线程的支持是一把双刃剑。 当它通过提供语言以及库的支持简化了并发应用程序的开发的同时,也提高了开发人员的门槛,因为要有更多的program使用到线程。当线程还比较难懂的时候,并发性是一个高深的topic;现在的话,主流的开发人员必须要了解线程安全性的问题。

1.3.1.Safety Hazards 安全性风险

线程的安全性是不可以预期的微妙,是非常复杂的,因为在没有充分的同步机制的情况下,多个线程的操作的顺序是不可预测的,有时候甚至给你带来surprising。在列表1.1中的UnsafeSequence类,这个类主要就是用来生成一个唯一的整型值的序列,这个类简要的说明了在多个线程交互操作的情况下将导致的不可预料的后果。在单线程环境下,这段代码是没有什么问题的,但在多线程的环境下就会有问题了。

列表1.1.非线程安全的序列号生成器.

UnsafeSequence的问题就是如果时机不对,那么两个线程可能会调用getNext然后得到相同的值。图1.1显示了这个问题是怎么发生的。虽然递增运算nextValue++看起来是一个操作,但其实是三个操作:读取value,对value加1,然后把新的value写入。由于运行时(runtime)可能会将多个线程的操作交互执行,所以极有可能就是两个线程同时读取一个值,最后两个线程读取到了相同的值,然后两个线程都基于这个值加1。结果自然是不同的线程的调用最后返回了同样的序列号。

图1.1.getNext的错误执行情况

如图1.1就描绘了在不同的线程间的可能的操作的交替。在这些图中,时序是从左往右运行,每一行表示一个线程的活动。上面的这些图描述了一个可能出现的最糟糕的情况,目的是想告诉你,如果错误的假想程序会按照特定的顺序来执行的话,那么将会存在很多危险。

UnsafeSequence使用了一个非标准的annotation:@NotThreadSafe。这是我们自己定义的几个annotation之一,用来说明类以及类的成员的并发属性。使用Annotation来标注线程安全是非常有用的。如果一个类标注了@ThreadSafe,那么用户可以心无旁骛的在多线程的环境下使用这个类,维护者也知道这个可以确保线程安全,软件分析工具也能分辨出可能的代码错误。

在UnsafeSequence类说的就是一个常见的并发问题,叫做:竞态条件(race condition)。当来自多个线程对nextValue的调用是否会返回一个唯一的值,要取决于运行时如何进行交替操作,这不是我们希望看到的状况。

因为线程共享同一个内存地址空间并且并发的运行,他们可以访问或修改其它线程也许正在使用的变量。这是一个便利,因为它让数据共享更加容易,而不是使用其它内部线程通信的机制。但这也是一个典型的风险:那就是线程可能会被不可预期的数据更改所迷惑。允许多个线程访问以及修改同一个变量,这就把非串行的元素引入到了一个其它串行编程模型中了,就是在串行中引入了并行,这个就增加了很大的迷惑性,而且如果出现问题,找原因也比较难。要使多线程的program的行为可预测,必须要对访问共享变量这个动作进行协调控制,从而让线程们不要在别人访问的时候去插手。就是要有同步机制。幸运的是,Java提供了同步机制来协调这样的访问。

列表1.2.线程安全的序列号生成器

@ThreadSafe public class Sequence { @GuardedBy("this") private int nextValue; public synchronized int getNext() { return nextValue++; } }

如果没有同步机制,那么无论是编译器,硬件以及runtime,都可以随便安排操作的执行时间和顺序,比如对寄存器(registers)或处理器的变量进行缓存,而这些被缓存的变量对其它线程是暂时(甚至永久)不可见的。

虽然这些技术有助于改善性能,而且通常是值得采用的方法,但它们也给开发人员带来了负担,开发人员需要分辨出哪些数据被多个线程共享,从而使得这些优化不会破坏安全性。

1.3.2.Liveness Hazards 活跃性问题

开发并发代码的时候,关注线程安全性问题是一个非常重要的事情:安全性一定不能妥协。安全性不仅仅是多线程要关注,即使是单线程程序也同样重要。而且线程的使用还会导致在单线程中不会出现的附加的安全性问题。比如,线程的使用会导致活跃性问题。

安全性的意思就是:“nothing bad ever happens”(永远不要发生不好的事情),活跃性则关注的是另外一个目标,那就是:“something good eventually happens”(某个正确的事情最终会发生)。当一个活动进入一种永久的不能前进的状态的时候,那么就意味着发生了活跃性问题,因为那个正确的结果将不会发生了,卡了!在串行程序中的活跃性问题的形式之一就是死循环,从而使得循环之后的代码再也无法执行了。使用了线程以后,变并行了,这时候又会出现其它的一些活跃性问题。比如说,如果线程 A正在等待一个被线程B hold住的资源,这时候如果线程B一直hold住这个资源不放,那么A将会永远的傻等着。后面的译文中我们会慢慢谈到有关活跃性问题的各种形式,以及如何避免这些问题,比如包括死锁(deadlock)、饥饿(starvation)以及活锁(livelock)这些都会提到。

当然了,就像大部分的那些并发bug一样,导致活跃性问题的那些bug是很难捕捉的,因为它们都是在一个一个的线程中,它们依赖于在不同的线程中的活动的相对时间。所以呢,在开发或测试的时候,这些bug并不是总能再现。debug比较费劲。

1.3.3. Performance Hazards 性能问题

与活跃性相关的一个问题就是性能问题。活跃性说的是正确的事情最终一定能够发生。事实上我们说一定发生还不够,而且还要很快的发生。涉及到快字,就是性能的事情了。当然“性能”是很宽泛的,包括响应时间过长(poor service time)、响应不够灵敏(responsiveness)、吞吐率(throughput)、资源消耗(resource consumption)或者是扩展性(scalability)。和前面说的安全性问题以及活跃性问题一样,多线程程序也会面临单线程中的所有的那些性能问题,同时,多线程程序也面临引入多线程后它所带来的额外的问题。

一个设计不错的并发程序,多线程是能够改善性能的。但无论如何,多线程都会带来一定的程序运行时开销。当调度器(scheduler)暂时悬挂当前活跃的线程转而运行其它的线程,这时候就涉及到Context切换,在一个应用程序中多个线程会有频繁的Context切换,这就是一笔很大的开销:保存以及恢复执行的context,局部内容的丢失(loss of locality)以及CPU 把一部分精力花费在了调度线程上,而不是花费在线程的运行上。当线程们共享一个数据,那么它们就必须使用同步机制,这个机制就会造成抑制某些编译器优化,让内存中的缓存数据flush或者失效,以及增加共享内存总线(shared memory bus)上的同步流量。总之,所有的这些因素都会导致额外的性能开销。在后面的译文中我们会详细的分析这些问题,并介绍如何减少这些开销。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2016-07-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 ImportSource 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档