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

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)上的同步流量。总之,所有的这些因素都会导致额外的性能开销。在后面的译文中我们会详细的分析这些问题,并介绍如何减少这些开销。

原文发布于微信公众号 - ImportSource(importsource)

原文发表时间:2016-07-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏王清培的专栏

.NET应用架构设计—面向查询服务的参数化查询设计(分解业务点,单独配置各自的数据查询契约)

阅读目录: 1.背景介绍 2.对业务功能点进行逻辑划分(如:A、B、C分别三个业务点) 2.1.配置映射关系,对业务点配置查询契约(构造VS插件方便生成查询契...

2338
来自专栏Java架构沉思录

单线程的Redis为什么这么快?

https://blog.csdn.net/xlgen157387/article/details/79470556

1953
来自专栏数据之美

玩转 SHELL 脚本之:Shell 命令 Buffer 知多少?

1、问题: 下午有同学问了这么一个问题: tail -n +$(tail -n1 /root/tmp/n) -F /root/tmp/ip.txt 2>...

4266
来自专栏Ceph对象存储方案

RGW 的GC深入解析与调优

什么是GC Garbage Collection缩写GC,简称垃圾回收。在RGW中GC一般都是指一些异步的磁盘空间回收操作,一般下面三种情况会发生GC。 1. ...

6878
来自专栏JackieZheng

十分钟带你了解服务化框架

在此之前 在此之前,你需要知道中间件的概念,可能在过往的从业生涯这个名词无数次的从你的眼前、耳畔都留下了足记,但是它的样子依然很模糊。 今天要说的服务化框架其...

18710
来自专栏FreeBuf

一名代码审计新手的实战经历与感悟

blueCMS介绍 个人认为,作为一个要入门代码审计的人,审计流程应该从简单到困难,逐步提升。因此我建议大家的审计流程为——DVWA——blueCMS——其他小...

4176
来自专栏java达人

多线程设计模式解读6-single threaded Execution模式(附分布式环境下的操作)

Single Threaded Execution模式主要是用于确保同一时间内只能让一个线程执行处理,说通俗点就是对synchronized的标准化使用方式,这...

1184
来自专栏Crossin的编程教室

【Python 第74课】多线程

很多人使用 python 编写“爬虫”程序,抓取网上的数据。 举个例子,通过豆瓣的 API 抓取 30 部影片的信息: import urllib, time...

2895
来自专栏CSDN技术头条

缓存那些事

导语:在网络分层应用服务中,缓存的使用已比较普及,本文将结合作者实际工作经验总结,讲述在不同的场景下如何选择和使用适用的缓存框架,以达到提升服务质量,优化系统...

2577
来自专栏企鹅号快讯

Golang 中的微服务-第一部分

介绍 Golang 中的微服务系列总计十部分,预计每周更新。本系列的解决方案采用了 protobuf 和 gRPC 作为底层传输协议。为什么采用这两个技术呢?我...

5529

扫码关注云+社区

领取腾讯云代金券