混用同步块和同步方法时的问题
在Thinking in Java (Fourth Edition)的Concurrency一章的Critical Sections 一节中有一个例子,用于展示同步整个方法和手动给代码块加锁两种同步方式的差异。
例子中定义了Pair
具有x
和y
两个字段,要求x
和y
必须始终保持相同。checkState()
会检查x
和y
是否相同,如不相同则抛出异常。
class Pair { // Not thread-safe private int x, y; public Pair(int x, int y) { this.x = x; this.y = y; } public Pair() { this(0, 0); } public int getX() { return x; } public int getY() { return y; } public void incrementX() { x++; } public void incrementY() { y++; } public String toString() { return "x: " + x + ", y: " + y; } public class PairValuesNotEqualException extends RuntimeException { public PairValuesNotEqualException() { super("Pair values not equal: " + Pair.this); } } // Arbitrary invariant -- both variables must be equal: public void checkState() { if(x != y) throw new PairValuesNotEqualException(); } }
Pair
本身没有采用任何同步措施,不保证线程安全。PairManager
通过持有Pair
并控制对Pair
的访问,确保线程安全。increment()
令Pair
的x
和y
同时自增,由具体子类实现。checkCounter
用于计数其他任务访问成功的次数。
// Protect a Pair inside a thread-safe class: abstract class PairManager { AtomicInteger checkCounter = new AtomicInteger(0); protected Pair p = new Pair(); private List<Pair> storage = Collections.synchronizedList(new ArrayList<Pair>()); public synchronized Pair getPair() { // Make a copy to keep the original safe: return new Pair(p.getX(), p.getY()); } // Assume this is a time consuming operation protected void store(Pair p) { storage.add(p); try { TimeUnit.MILLISECONDS.sleep(50); } catch(InterruptedException ignore) {} } public abstract void increment(); }
PairManipulator
会不停的调用PairManager
的increment()
,使得PairManager
持有的Pair
对象的x
和y
不断自增。
class PairManipulator implements Runnable { private PairManager pm; public PairManipulator(PairManager pm) { this.pm = pm; } public void run() { while(true) pm.increment(); } public String toString() { return "Pair: " + pm.getPair() + " checkCounter = " + pm.checkCounter.get(); } }
PairChecker
不断地检查PairManager
持有的Pair
是否满足x
和y
相等。每进行一次成功的访问,就令PairManager
的checkCounter
自增。
class PairChecker implements Runnable { private PairManager pm; public PairChecker(PairManager pm) { this.pm = pm; } public void run() { while(true) { pm.checkCounter.incrementAndGet(); pm.getPair().checkState(); } } }
CriticalSection
对两个PairManager
进行测试,对每个PairManager
,在不同的线程上创建PairManipulator
和PairChecker
,同时对PairManager
进行自增和检查。
public class CriticalSection { // Test the two different approaches: static void testApproaches(PairManager pman1, PairManager pman2) { ExecutorService exec = Executors.newCachedThreadPool(); PairManipulator pm1 = new PairManipulator(pman1), pm2 = new PairManipulator(pman2); PairChecker pcheck1 = new PairChecker(pman1), pcheck2 = new PairChecker(pman2); exec.execute(pm1); exec.execute(pm2); exec.execute(pcheck1); exec.execute(pcheck2); try { TimeUnit.MILLISECONDS.sleep(500); } catch(InterruptedException e) { System.out.println("Sleep interrupted"); } System.out.println("pm1: " + pm1 + "\npm2: " + pm2); System.exit(0); } }
书中然后给出了同步整个increment()
方法的ExplicitPairManager1
,和手动锁定increment()
方法中部分代码块的ExplicitPairManager2
:
import java.util.concurrent.locks.*; // Synchronize the entire method: class ExplicitPairManager1 extends PairManager { private Lock lock = new ReentrantLock(); public synchronized void increment() { lock.lock(); try { p.incrementX(); p.incrementY(); store(getPair()); } finally { lock.unlock(); } } } // Use a critical section: class ExplicitPairManager2 extends PairManager { private Lock lock = new ReentrantLock(); public void increment() { Pair temp; lock.lock(); try { p.incrementX(); p.incrementY(); temp = getPair(); } finally { lock.unlock(); } store(temp); } } public class ExplicitCriticalSection { public static void main(String[] args) throws Exception { PairManager pman1 = new ExplicitPairManager1(), pman2 = new ExplicitPairManager2(); CriticalSection.testApproaches(pman1, pman2); } }
以上程序在我的电脑上(Windows 10 x64 + JDK 8)会抛出异常:
Exception in thread "pool-1-thread-4" com.sharingresources.Pair$PairValuesNotEqualException: Pair values not equal: x: 2, y: 1
显然出现了x
和y
不等的情况。通过分别注释掉CriticalSection.testApproaches()
中两个PairManager
的相关任务,定位问题出在ExplicitPairManager2
。
ExplicitPairManager2
的increment()
使用ReentrantLock
进行手动加锁,确保x
和y
的自增不会被打断。问题应该出在读取x
和y
的时机。ExplicitPairManager2
的getPair()
直接继承自父类PairManager
:
public synchronized Pair getPair() { // Make a copy to keep the original safe: return new Pair(p.getX(), p.getY()); }
getPair()
使用了synchronized
,也是同步的,但却没有与ExplicitPairManager2
的increment()
同步,在increment()
运行期间执行了getPair()
,得到了不同的x
和y
。
这里的问题应该是,使用synchronized
同步的方法是同步于对象自己的锁,而ExplicitPairManager2
的increment()
是同步于显式创建的ReentrantLock
,getPair()
和increment()
没有同步于同一个锁,导致二者实际上没有同步。
通过在ExplicitPairManager2
重写getPair()
,使其同步于increment()
的锁,即可解决此问题。
class ExplicitPairManager2 extends PairManager { private Lock lock = new ReentrantLock(); public void increment() { Pair temp; lock.lock(); try { p.incrementX(); p.incrementY(); temp = getPair(); } finally { lock.unlock(); } store(temp); } @Override public Pair getPair() { lock.lock(); try { return new Pair(p.getX(), p.getY()); } finally { lock.unlock(); } } }
运行结果如下:
pm1: Pair: x: 76, y: 76 checkCounter = 23 pm2: Pair: x: 77, y: 77 checkCounter = 153991947
可见手动为代码块加锁能让对象更多地处于解锁状态,使共享资源能更充分地被其他任务使用。