首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

2.4  用锁来保护状态

因为锁使得线程能够串行地(serialized8)访问它所保护的代码路径,所以我们可以用锁来创建相关的协议,以保证线程对共享状态的独占访问。只要始终如一地遵循这些协议,就能够确保状态的一致性。

操作共享状态的复合操作必须是原子的,以避免竞争条件,比如递增命中计数器(读-改-写)或者惰性初始化(检查再运行)。复合操作会在完整的运行期间占有锁,以确保其行为是原子的。然而,仅仅用synchronized块包装复合操作是不够的;如果用同步来协调访问变量,每次访问变量时都需要同步。进一步讲,用锁来协调访问变量时,每次访问变量都需要用同一个锁。

一种常见的错误观念认为只有写入共享变量时才需要同步;其实并非如此(其中的原因会在3.1节说明)。

对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的。

清单2.6的SynchronizedFactorizer中的lastNumber和lastFactors是由Servlet对象的内部锁保护的。这一点已由@GuardedBy Annotation进行了文档化。

对象的内部锁与它的状态之间没有内在的关系。尽管大多数类普遍使用这样一种非常有效的锁机制:用对象的内部锁来保护所有的域,然而这并不是必需的。即使获得了与对象关联的锁也不能阻止其他线程访问这个对象——获得对象的锁后,唯一可以做的事情是阻止其他线程再获得相同的锁。作为一种便利,每个对象都有一个内部锁,所以你不需要显式地创建锁对象9。你可以构造自己的锁协议或同步策略,使你可以安全地访问共享状态,并且贯穿程序都始终如一地使用它们。

每个共享的可变变量都需要由唯一一个确定的锁保护。而维护者应该清楚这个锁。

一种常见的锁规则是在对象内部封装所有的可变状态,通过对象的内部锁来同步任何访问可变状态的代码路径,保护它在并发访问中的安全。很多的线程安全类都是这个模式,例如Vector和其他同步的容器(collection)类。这种情况下,对象状态中的一切变量都被对象的内部锁保护。然而这种锁机制并没有什么特殊的,编译器或运行时都不会强制要求这种(或者其他任何一种)锁模式10。如果添加新的方法或者代码路径而忘记使用锁,这种锁协议也很容易被破坏。

并不是所有数据都需要锁的保护——只有那些被多个线程访问的可变数据。在第1章我们看到,添加一个简单的异步事件(比如TimerTask)后,尤其是当你的程序并未良好封装时,整个程序是如何引入线程安全的需求的。考虑一个处理大量数据的单线程化的程序,由于没有跨线程共享的数据,所以不需要同步。现在想象你打算添加新特性,周期性地为进度创建快照,这样当程序崩溃或者必须停止时,不必从起点重新启动。你选择TimerTask,设置每十分钟触发一次,将程序状态保存在文件中,从而完成这个功能。然

而另一个线程(Timer的管理线程)会调用TimerTask,因此现在有两个线程会访问存储在快照中的任意数据:程序主线程与Timer线程。这意味着访问程序的状态时,不仅TimerTask的代码要使用同步,程序中其余的访问相同数据的代码路径也要同步。一个原本不需要同步的程序,现在要在整个程序中贯穿使用同步。

锁保护的变量,意味着每一次访问变量时都要获得该锁,确保在同一时刻只有一个线程可以访问这个变量。若类的不变约束涉及多个状态变量,那么另外还需要一个附加需求:每个参与到不变约束的变量由同一个锁守护。这样你可以在一个单一的原子操作中访问或者更改它们,从而保证了不变约束。SynchronizedFactorizer示范了这条规则:servlet对象的内部锁保护缓存的number与缓存的factors。

对于每一个涉及多个变量的不变约束,需要同一个锁保护其所有的变量。

既然同步可以避免竞争条件,为什么不将每个方法都声明为synchronized类型?如此武断地使用同步,可能导致程序中使用的同步过多或过少。如同Vector这样,仅仅同步它每个方法,并不足以确保在Vector上执行的复合操作是原子的:

if (!vector.contains(element))

     vector.add(element);

虽然contains和add都是原子的,但在尝试“缺少即加入(put-if-absent)”操作的过程中仍然存在竞争条件。虽然同步方法确保了不可分割操作的原子性,但是把多个操作整合到一个复合操作时,还是需要额外的锁。(关于向线程安全对象中安全地添加额外的原子操作的技术,参见4.4节。)同时,同步每个方法还会导致活跃度(liveness)或性能(performance)的问题,正如我们在SynchronizedFactorizer中看到的那样。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论