对数据同步访问封装的策略
我们经常操作一些本身不是线程安全的对象时,在多线程的环境下就会考虑到要采取一些措施来处理。一些典型比如说用synchronized来同步,有的如果情况允许的话使用ThreadLocal变量,还有的会将对象变成immutable的方式。当然,每种方法都需要根据具体问题来分析,不能在保证高并发性和线程安全的情况下完全通用。
同步的数据结构
有一个比较常见的用法就是当我们要访问一些集合类的时候。大多数的集合类比如ArrayList, HashMap之类的都不是线程安全的。为了保证线程安全,我们可能需要用到前面提到的一些同步策略。常见的一种如下:
public class PersonSet {
private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
}
这种方法用到一个比较好的思路,就是将一些我们需要多线程访问的数据封装起来。然后对他们的访问操作进行线程同步。这样我们要访问修改的地方就在这个被封装的地方,也方便维护。
从这种用法我们看到的思路就是封装线程不安全的数据结构,通过这个封装对象添加一些同步的机制来保证最终访问的线程安全。那么,这种思路还有没有其他应用的示例呢?在java中间有一些典型的类,如Collections.synchronizedList, Collections.synchronizedSet等。他们就是用到了这样的思路。
设计思路
一个典型的synchronizedList用法如下:
List<Person> persons = new ArrayList<Person>();
List<Person> securePersons = Collections.synchronizedList(persons);
通过将我们线程不安全的数据传入synchronizedList方法,返回的还是一个list,不过对它的访问就变成线程安全的了。比较有意思吧?通过前面提到的将数据封装起来,然后提供同步机制,我们也就可以猜到,这种方式也是通过同样的手法。
现在,如果我们足够好奇的话,再进一步想想。最前面那个示例对数据进行了封装,再次访问这个arrayList的时候,实际上是通过访问封装对象提供的方法来使用的。而我们这边进行了封装之后居然和可以把它当成一个List来用。那么,这说明了什么呢?说明我们这个封装它的类肯定提供了和List一样的接口。如果我们对某些设计模式比较熟悉的话,再回想这么一句,实现相同的接口,封装了另外一个对象,对这么一个对象增加了某些功能,但是还能当成同样接口的对象用。
呵呵,像什么呢?好像很熟悉的感觉,有点decorator pattern的感觉,也有点像adapter。
如果我们深入代码的细节去探索的话,会发现它实际的实现方式是用到了decorator pattern。代码本身并不负责,具体的代码细节就不去赘述了。他们的类结构关系如下图:
这是一个稍微复杂一点的decorator pattern实现,每个进行封装的类如SynchronizedList, SynchronizedSet都继承自SynchronizedCollection,其他的需要被封装的类如ArrayList, LinkedList和SynchronizedCollection实现同样的接口。我们再对照一下典型的decorator pattern类图结构:
这里头,decorator就相当于一个二道贩子。它把一些现成的需要封装的对象拿过来,然后按照同样的规格(接口)在折腾出一个具有一点新功能的对象。想到这里,一种山寨的感觉油然而生。
总结
我们为了提供一个线程安全的数据结构而采用了一种封装对象。这样,用户就可以很方便的使用一个封装调用而得到所期望的同步效果。这样比直接去加synchronized要方便多了,更加不容易出错。在这么一个简单易用的外表下面,还是隐藏着一些封装的复杂手法。唉,扮靓是需要代价的。