混用同步块和同步方法时的问题

  在Thinking in Java (Fourth Edition)的Concurrency一章的Critical Sections 一节中有一个例子,用于展示同步整个方法和手动给代码块加锁两种同步方式的差异。

  例子中定义了Pair具有xy两个字段,要求xy必须始终保持相同。checkState()会检查xy是否相同,如不相同则抛出异常。

  Pair本身没有采用任何同步措施,不保证线程安全。PairManager通过持有Pair并控制对Pair的访问,确保线程安全。increment()Pairxy同时自增,由具体子类实现。checkCounter用于计数其他任务访问成功的次数。

  PairManipulator会不停的调用PairManagerincrement(),使得PairManager持有的Pair对象的xy不断自增。

  PairChecker不断地检查PairManager持有的Pair是否满足xy相等。每进行一次成功的访问,就令PairManagercheckCounter自增。

  CriticalSection对两个PairManager进行测试,对每个PairManager,在不同的线程上创建PairManipulatorPairChecker,同时对PairManager进行自增和检查。

  书中然后给出了同步整个increment()方法的ExplicitPairManager1,和手动锁定increment()方法中部分代码块的ExplicitPairManager2

  以上程序在我的电脑上(Windows 10 x64 + JDK 8)会抛出异常:

显然出现了xy不等的情况。通过分别注释掉CriticalSection.testApproaches()中两个PairManager的相关任务,定位问题出在ExplicitPairManager2

  ExplicitPairManager2increment()使用ReentrantLock进行手动加锁,确保xy的自增不会被打断。问题应该出在读取xy的时机。ExplicitPairManager2getPair()直接继承自父类PairManager

getPair()使用了synchronized,也是同步的,但却没有与ExplicitPairManager2increment()同步,在increment()运行期间执行了getPair(),得到了不同的xy

  这里的问题应该是,使用synchronized同步的方法是同步于对象自己的锁,而ExplicitPairManager2increment()是同步于显式创建的ReentrantLockgetPair()increment()没有同步于同一个锁,导致二者实际上没有同步。

  通过在ExplicitPairManager2重写getPair(),使其同步于increment()的锁,即可解决此问题。

运行结果如下:

可见手动为代码块加锁能让对象更多地处于解锁状态,使共享资源能更充分地被其他任务使用。