2.4 用锁来保护状态

由于锁能使其保护的代码路径以串行形式[1]来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。只要始终遵循这些协议,就能确保状态的一致性。

访问共享状态的复合操作,例如命中计数器的递增操作(读取-修改-写入)或者延迟初始化(先检查后执行),都必须是原子操作以避免产生竞态条件。如果在复合操作的执行过程中持有一个锁,那么会使复合操作成为原子操作。然而,仅仅将复合操作封装到一个同步代码块中是不够的。如果用同步来协调对某个变量的访问,那么在访问这个变量的所有位置上都需要使用同步。而且,当使用锁来协调对某个变量的访问时,在访问变量的所有位置上都要使用同一个锁。

一种常见的错误是认为,只有在写入共享变量时才需要使用同步,然而事实并非如此(3.1节将进一步解释其中的原因)。

对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。

在程序清单2-6的SynchronizedFactorizer中,lastNumber和lastFactors这两个变量都是由Servlet对象的内置锁来保护的,在标注@GuardedBy中也已经说明了这一点。

对象的内置锁与其状态之间没有内在的关联。虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后,只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,只是为了免去显式地创建锁对象。[2]你需要自行构造加锁协议或者同步策略来实现对共享状态的安全访问,并且在程序中自始至终地使用它们。

每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。 ...

Get Java并发编程实战 now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.