前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >并发编程-用锁来保护状态

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

作者头像
ImportSource
发布2018-04-03 15:23:42
7150
发布2018-04-03 15:23:42
举报
文章被收录于专栏:ImportSource

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

如果你对共享的状态的访问是复合操作(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中已经看到了这些问题。

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

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

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

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

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