并发编程-用锁来保护状态

由于锁机制可以让他保护起来的代码片段始终被串行访问。也就是一个访问完了,再由下一个来访问。我们可以利用锁的这种特点,来约定一些协议,来对共享的状态进行独占访问。只要一直按照这些约定,就可以确保状态的一致性。

如果你对共享的状态的访问是复合操作(compound actions)的话,比如命中计数器的递增(读取-修改-写入)或者延迟初始化(先检查后执行:check then act),这种复合操作你就必须要保证原子性从而避免race condition的出现,也就是竞态条件的出现。在一个复合操作的整个过程中都hold着一把锁的话,那么我们就可以确保这个compound action(复合操作)是atomic的。然而,仅仅将一个复合操作封装到一个synchronized block是不够的。如果使用synchronization来协调对变量的访问,那么我们就要在所有的需要访问这个变量的地方加上同步。而且当我们使用锁来进行协调对变量的访问的时候,还要确保所有需要访问这个变量所在代码中都要使用同一把锁。(译者曰:这简直不可想象)。

For each mutable state variable that may be accessed by more than one thread, all accesses to that variable must be performed with the same lock held. In this case, we say that the variable is guarded by that lock. 每个可变的状态变量可能都要被多个线程访问,所有的对这些变量的访问都要使用同一把锁。在这种情况下,我们说这个变量是被这个锁保护了起来。 每个可变的状态变量可能都要被多个线程访问,所有的对这些变量的访问都要使用同一把锁。在这种情况下,我们说这个变量是被这个锁保护了起来。

社会上有一种错误的认识,认为只有在对共享变量进行写入的时候才需要用到同步(synchronization)。然而并非如此。我们将在后面的章节中说这个问题。

在程序清单2.6中SynchronizedFactorizer,lastNumber和lastFactors是被servlet对象的内置锁保护起来的。 @GuardedBy这个注解就说明了这点。

@ThreadSafe public class SynchronizedFactorizer implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; public synchronized void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber)) encodeIntoResponse(resp, lastFactors); else { } } }

程序清单2.6

对象的内置锁和该对象的状态是没有什么内在关联的。 尽管大多数情况下使用内置锁是一个不错的做法。但,对象的fields并不一定要通过内置锁来保护。当你获取一个与对象关联的锁的时候,并不能避免其它的threads去访问该对象。一个thread在获得对象的锁之后,只能够阻止其他的线程获得同一个锁。之所以每个对象都有一个内置锁,是因为jvm只是为了让你更方便,不用显式地去create锁对象。我们还是要通过自己去约定一个锁的协议或者一个同步策略来让我们安全的访问那些shared state(共享状态),并且在我们的program的整个过程中都坚持使用这些约定和策略。

Every shared, mutable variable should be guarded by exactly one lock. Make it clear to maintainers which lock that is. 每个共享的、可变的变量都应该只有一个锁来保护。从而让维护人员们清楚的知道是哪一个锁。

一种常见的加锁做法就是把所有的可变的状态都封装在一个对象里,然后通过使用对象的内置锁来同步对可变状态访问的所有的代码块,从而保护这个对象不会发生并发访问。这种做法被用在很多线程安全类上。比如Vector以及其它的同步集合类。在这种情况下,在一个对象状态下的所有变量都被该对象的内置锁包裹了起来。然而,这种做法也没有什么特别的。而且compiler以及runtime也不会强制这种做法的锁必须被执行。这种情况下,有一天你要新加一个方法或者代码块的时候,很可能就忘记使用synchronization机制了,这时候你的加锁约定或者协议就被破坏了。

并不是所有的数据都需要被锁来保护,只有那些可变的将会被多个线程并发访问的数据才需要加锁机制。在前面的文章中,我们讲过了如何添加一个简单的异步事件。比如TimerTask,整个程序都需要满足线程安全性需求,尤其是当程序状态的封装性比较糟糕时。想象一下,一个单线程的program来处理大量的数据。单线程的program是不需要synchronization机制的,因为不需要在线程之间共享数据。现在假设你要添加一个新的功能,就是定期地对数据处理进度生成快照,这样我们就可以保证因程序崩溃后重启后不用再从头开始处理数据。这种情况下,你也许就会选择用TimerTask来做这件事情,每十分钟触发一次,然后把program的state保存到一个file中。

由于TimerTask将会被另外一个线程调用(由Timer来管理的),这时候快照的数据就会被两个线程访问:一个是main program thread,另外一个是Timer thread。这就意味着不仅TimerTask的代码中要用synchronization,在访问program state的时候;而且其它的任何要访问这个state的代码块都要用synchronization。整个program中,以前不需要用synchronization的地方,现在都要用synchronization了。

当一个变量被锁保护起来的时候,意味着对这个变量的每次访问都要拥有同一把锁,从而确保在同一时间点(at a time)只有一个线程能够访问这个变量。

当一个类的不变性涉及到多个状态变量的时,就会有一个新的要求:在这个不变性中参与的每个变量都必须被同一个锁保护。这样可以让你在一个原子操作中访问或更新它们,确保不变性不被破坏。在SynchronizedFactorizer类中就说明了这条规则:缓存的数值和缓存的因数分解结果都是被servlet的对象内置锁来保护。

如果synchronization是治愈race condition(竞态条件)的良药,那么为什么不给每个方法都加synchronized呢?事实上如果不加区别的滥用synchronized,就会导致程序中出现过多的同步。给每个方法都加synchronized,像Vector那样,也不会确保在Vector上的复合操作(多个操作:compound actions)是原子的。

if (!vector.contains(element)) vector.add(element);

这种在put if absent 操作上的尝试是存在race condition(竞态条件)问题的,尽管contains和add都是原子的。虽然synchronized方法可以让单个操作是原子的,但如果要把多个操作合并为一个复合操作(compound action),还是需要一些额外的加锁机制的。(我们会在后面的章节中讲述如何在线程安全的对象中添加原子操作的方法)。而且,把每个方法都变成同步方法会导致活跃性(liveness)问题和性能问题,我们在SynchronizedFactorizer中已经看到了这些问题。

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

原文发表时间:2016-08-21

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Python攻城狮

MySQL与Python的交互1.交互类型2.增删改查(CRUD)3.封装

3342
来自专栏Python攻城狮

Python系统编程-进程1.进程1.多任务的引入2.多任务的概念

有很多的场景中的事情是同时进行的,比如开车的时候手和脚共同来驾驶汽车,再比如唱歌跳舞也是同时进行的;

923
来自专栏人工智能LeadAI

MySQL与Python的交互

1、交互类型 1、安装引入模块 安装mysql模块,在windows和ubuntu中 ? windows里安装mysql模块 ? Linux里安装mysql模块...

4209
来自专栏IT技术精选文摘

Shell入门指南

1563
来自专栏猿人谷

realloc invalid pointer错误解析

realloc invalid pointer错误 char* temp=(char*) realloc(src,sizeof(char)*100); 如上面这...

1995
来自专栏云计算教程系列

如何使用Grep

Grep是一个命令行实用程序,可以使用常见的正则表达式语法搜索和过滤文本。它无处不在,动词“to grep”已经成为“搜索”的同义词。它grep是一个有用的工具...

1393
来自专栏杂烩

mongodb拾遗

851
来自专栏Java编程技术

一个有关定时生产与消费的问题

按照上面的逻辑看的话,每个队列里面最多有一个元素。其实不然,因为在多线程模型中每个线程占用cpu执行的时间是按照时间片来划分的,每个线程执行完自己的时间片后会被...

821
来自专栏扎心了老铁

python使用上下文管理器实现sqlite3事务机制

如题,本文记录如何使用python上下文管理器的方式管理sqlite3的句柄创建和释放以及事务机制。 1、python上下文管理(with) python上下文...

48712
来自专栏古时的风筝

从实例出发,了解单例模式和静态块

什么是单例模式呢,单例模式(Singleton)又叫单态模式,它出现目的是为了保证一个类在系统中只有一个实例,并提供一个访问它的全局访问点。从这点可以看出,单例...

810

扫码关注云+社区

领取腾讯云代金券