本文节选自 Effective Java by Joshua Bloch 和 Concurrent Programming in Java by Doug Lea.
1.6 避免过多的同步
1.6.1是否需要同步
过多的同步可能会导致性能降低、死锁,甚至不确定行为。通常,在同步区域内应该做尽可能少的工作。同步区域之外被调用的外来方法被称为“开放调用(open call)”。除了可以避免死锁之外,开放调用还可以极大地增加并发性。
考虑StringBuffer类和BufferedInputStream类,这些类都是线程安全(thread-safe)的,但是它们往往被用于单个线程中,所以它们所做的锁操作往往是不必要的,虽然同步的开销自java平台早期开始就一直在下降,但是它永远也不会消失。一个给定的类是否应该执行内部同步并不总是很清楚,下面是一些指导性的原则。
如果你正在编写的类主要被用于同步环境中,同时也被用于不要求同步的环境中,那么一个合理的方法是,同时提供同步版本和和未同步版本。这也正是Collections Framework采用的方法。还有,java.util.Random也是采用这一种做法是提供一个包装类(wrapper class),它实现一个描述该类的接口,同时在将方法调用转发给内部对象中对应的方法之前执行适当的同步操作。种方法。第二种方法适用于那些不是被设计用来扩展或者重新实现的类,它提供一个未同步的类和一个子类,在子类中包含一些被同步的方法,它们依次调用到超类中对应的方法上。
关于是否对一个用于存取成员变量的方法进行同步,需要考虑两点:合法性和陈旧性。如果成员变量不总是合法的,那么可以的选择是:
- 同步所有存取方法
- 确保用户在得到非法值的时候能得到通知
- 省略存取方法。在并发程序中,对象的属性可以被异步修改,客户通过某行代码得到的值可能在下一行代码中就改变了。因此需要仔细评估存取方法存在的必要性。
如果成员变量的值总是合法的,但是不能是陈旧数据,那么可以的选择是:
- 把成员变量定义为volatile,并去掉存取方法的同步。
1.6.2 分解同步和分解锁
另外一种增加程序并发性的方法是分解同步,如果一个类的行为可以分解为互相独立、互不干扰或者不冲突的子部分,那么就值得用细粒度的辅助对象来重新构造类。普遍的原则是,把类的内部同步操作分得越细,在大多数情况下,它的活性就越高。但是这一点是以更加复杂和潜在的错误为代价的。例如:
public class Shape
{
public synchronized vodi adjustLocation(){ /*Long time operation*/ }
public synchronized vodi adjustDimensions(){ /*Long time operation*/ }
}
我们假设adjustLocation不处理维度信息,adjustDimensions不处理位置信息,那么可以考虑把维度和位置信息分解到两个类中, 例如:
public class Shape
{
private final Location location = new Location();
private final Dimensions dimensions = new Dimensions();
public void adjustLocation(){ location.adjustLocation(); }
public void adjustDimensions(){ dimensions.adjustDimensions(); }
}
public class Location
{
public synchronized void adjustLocation(){ /*Long time operation*/ }
}
public class Dimensions
{
public synchronized void adjustDimensions(){ /*Long time operation*/ }
}
如果你不能或者不想分解类,则可以分解每个子功能相关的同步锁。例如
public class Shape
{
private final Object locationLock = new Object();
private final Object dimensionsLock = new Object();
public void adjustLocation()
{
synchronized(locationLock)
{
/*Long time operation*/
}
}
public void adjustDimensions()
{
synchronized(dimensionsLock)
{
/*Long time operation*/
}
}
}
1.6.3 冲突集合
设想有一个Inventory类,它有store和retrieve方法来存取对象。以下的例子中使用了Hashtable来演示,虽然这种完全同步的Hashtable允许Inventory类的实现无需考虑底层的实现细节。但是,我们仍然想store和retrieve方法添加一些语义上的约束,如下:
- retrieve操作不应该和store操作并发执行。
- 两个或者两个以上的retrieve方法不应该同时执行。
- 两个或者两个以上的store方法可以同时执行。
以下的非正规符号描述了冲突集合,即不能并发的方法对的集合.
{(store, retrieve), (retrieve, retrieve)}
基于冲突集合的类可以使用before/after这种模式,即基本操作被那些维护者独占关系的代码所环绕。首先,对于每个方法,定义一个计数变量,用以表示该方法是否在执行中。其次,把每个基本操作都隔离入非公共方法中。最后,编写那些基本操作的公共版本,即在那些基本操作的前后添加上before/after的控制。以下是个示例代码:
public class Inventory
{
protected final Hashtable items = new Hashtable();
protected final Hashtable suppliers = new Hashtable();
protected int storing = 0;
protected int retrieving = 0;
public void store(String desc, Object item, String supplier)
throws InterruptedException
{
synchronized(this)
{
while(retrieving != 0)
{
wait();
++storing;
}
}
try
{
doStore(desc, item, supplier);
}
finally
{
synchronized(this)
{
if(--storing == 0)
{
notifyAll();
}
}
}
}
public Object retrieve(String desc)
throws InterruptedException
{
synchronized(this)
{
while(storing != 0 || retrieving != 0)
{
wait();
++retrieving;
}
}
try
{
return doRetrieve(desc);
}
finally
{
synchronized(this)
{
if(--retrieving == 0)
{
notifyAll();
}
}
}
}
protected void doStore(String desc, Object item, String supplier)
{
items.put(desc, item);
suppliers.put(supplier, desc);
}
protected Object doRetrieve(String desc)
{
Object x = items.get(desc);
if(x != null)
{
items.remove(desc);
}
return x;
}
}
接下来考虑一个更复杂的例子,一个读出者和写入者模型,与Inventroy不同,读出者和写入者策略不仅应用于特定方法,而是控制所有具有读出和写入语义的方法。假设我们需要进行有目的的锁定(intention lock),比如,要求按照write,read,write,read,write的顺序等。这时候我们需要考虑的有以下几点:
- 如果当前已经存在一个或者多个活动(执行中)的读出者,而且有一个写入者正在等待的时候,一个新的读出者是否能否立即加入?如果答案是肯定的话,那么不断增加的读出者将会使写入者无法执行;如果答案为否,那么读出者的吞吐量就会下降。
- 如果某些读出者与写入者同时在等待一个活动的写入者完成操作,那么你的处理策略会偏向读出者还是写入者?先到者优先?随意?轮流?
虽然以上策略没有明确的答案,但是一些标准的解决方案和相关的实现还是存在的,以下一个通用的实现,使用了模板类和before/after这种模式,其子类版本不需要做过多的修改。而且可以通过让allowReader和allowWriter方法中的谓词依赖与这个值,来调整控制策略。以下是示例代码:
public abstract class ReadWrite
{
protected int activeReaders = 0;
protected int activeWriters = 0;
protected int waitingReaders = 0;
protected int waitingWriters = 0;
protected abstract void doRead();
protected abstract void doWrite();
public void read() throws InterruptedException
{
beforeRead();
try { doRead(); }
finally { afterRead(); }
}
public void write() throws InterruptedException
{
beforeWrite();
try { doWrite(); }
finally { afterWrite(); }
}
protected boolean allowReader()
{
return waitingWriters == 0 && activeWriters == 0;
}
protected boolean allowWriter()
{
return activeReaders == 0 && activeWriters == 0;
}
protected synchronized void beforeRead() throws InterruptedException
{
++waitingReaders;
while(!allowReader())
{
try { wait(); }
catch(InterruptedException ie)
{
--waitingReaders;
throw ie;
}
}
--waitingReaders;
++activeReaders;
}
protected synchronized void afterRead()
{
--activeReaders;
notifyAll();
}
protected synchronized void beforeWrite() throws InterruptedException
{
++waitingWriters;
while(!allowWriter())
{
try { wait(); }
catch(InterruptedException ie)
{
--waitingWriters;
throw ie;
}
}
--waitingWriters;
++activeWriters;
}
protected synchronized void afterWrite()
{
--activeWriters;
notifyAll();
}
}