组合线程安全类

来源:互联网 发布:mac免费视频编辑软件 编辑:程序博客网 时间:2024/05/17 04:36

本文是个人对如何使用线程安全的组件组合线程安全类的一些理解。

首先从设计线程安全类说起。怎么设计线程安全类,这跟我们平时解数学题一样,告诉我们变量的范围,根据给出的约束条件,选择合理的方案找出符合要求的答案。这里也是一样的,首先要明确对象的状态由那些变量构成,然后要确定限制状态变量的不变约束,最后制定出一个协调并发访问对象状态的策略。

其中对象的状态是由对象的域组成的,需要注意的是如果对象的域是引用类型的,那么对象的状态也包括了被引用对象的域。比如LinkedList的状态包含了存储于其中的对象的状态。所有可能的状态构成了对象的状态空间。而不变约束指明了状态的合法性。不同的状态还会存在转换关系,根据具体问题的不同状态转换可能会有相应的约束关系,我们称之为后验条件。后验条件约束了状态转换。

下面是一些组合线程安全类的方法。

1.实例限制

通过对对象进行封装(这个过程可以称为限制),并结合适当的锁策略,我们可以让不是线程安全的对象安全的用于多线程程序。而且由于封装过程是由我们自己设计的,这也方便了我们的分析。

eg:

public class PersonSet {@GuardedBy("this")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);}}
这里HashSet就是上面说的线程不安全的类。但是这里通过private final将其封装在PersonSet类中,确保了外部无法对其直接进行操作。然后通过两个同步方法(内部锁)提供操作接口,将其变为线程安全的。考虑一下如果这里再添加一个getPerson的方法返回mySet中一个Person引用,同时Person是可变的,那么在对返回的person进行操作时就要求Person也是线程安全的。

2.Java监视器模式

由对象内部锁(或者其他锁对象,但要保证一致)来保护可变状态。

eg:

public class PrivateLock {private final Object myLock = new Object();@GuardedBy("myLock") Widget widget;void someMethod() {synchronized(myLock) {// Access or modify the state of widget}}}

这里就是使用私有锁对象保护状态widget,使用私有锁对象避免了使用可公共访问的锁在活跃度上带来的影响和分析锁上的不便.

3.委托线程安全

就是把当前对象的线程安全要求委托给其他对象完成。通俗的说,就是借助其他对象线程安全方面的优势实现当前对象的线程安全。需要指出的是,由线程安全的类组件组成的组件不见得就是线程安全的。

eg:

public class CountingFactorizer implements Servlet {private final AtomicLong count = new AtomicLong(0);public long getCount() {return count.get();}public void service(ServletRequest req, ServletResponse resp) {BigInteger i = extractFromRequest(req);BigInteger[] factors = factor(i);count.incrementAndGet();encodeIntoResponse(resp, factors);}}
这里CountingFactorizer 把线程安全委托给了count。因为count是线程安全的,CountingFactorizer 的状态只有count,而且对count没有额外的约束。在这里只是委托给了一个状态变量,当委托给多个状态变量的时候,情况就相对复杂了。下面看一个例子:

public class NumberRange {//INVARIANT: lower <= upperprivate final AtomicInteger lower = new AtomicInteger(0);private final AtomicInteger upper = new AtomicInteger(0);public void setLower(int i) {// Warning -- unsafe check-then-actif (i > upper.get())throw new IllegalArgumentException("can’t set lower to " + i + " > upper");lower.set(i);}public void setUpper(int i) {// Warning -- unsafe check-then-actif (i < lower.get())throw new IllegalArgumentException("can’t set upper to " + i + " < lower");upper.set(i);}public boolean isInRange(int i) {return (i >= lower.get() && i <= upper.get());}}
这里由于约束条件lower<=upper,导致两个被委托的状态变量不不是独立的。因此NumberRange不是线程安全的。具体解释一下,比如一个线程将lower修改为5,另外一个线程将upper修改为3,这时不满足约束条件lower<=upper了。这里与使用volatile时的一条原则相似:当且仅当一个变量没有参与涉及其他状态变量的不变约束时,才适合声明为volatile类型。

现在假设已经将线程安全委托给了状态变量,那么这个状态变量什么时候可以公开化呢?既然是公开化,那么就有可能在外部对状态进行改变,如果这种改变打破了线程安全类的约束条件,那么公开化就是不允许的。所以,只有当没有任何不变约束限制,没有任何状态转换制约的情况下,才可以公开化被委托的状态变量。当然了,这个状态变量还必须是线程安全的。

4.向已有的线程安全类添加函数

直接看示例:

public class BetterVector<E> extends Vector<E> {public synchronized boolean putIfAbsent(E x) {boolean absent = !contains(x);if (absent)add(x);return absent;}}

这里putIfAbsent函数就是用来检查vector中是否有某个元素,没有就加入。需要注意的一点就是对于这种check-then-act的操作在多线程环境下要注意需要具备原子性。

4.1client-side lock 下面还需要说明一下特殊的情况:对于Collections.synchronizedList封装的ArrayList,向原始类加入方法或扩展类是行不通的,因为客户端代码不知道同步工厂方法返回的List对象的类型。这时可以采用扩展功能的方法。把扩展代码提取到一个助手类中。

eg:

@NotThreadSafepublic class ListHelper<E> {public List<E> list =Collections.synchronizedList(new ArrayList<E>());...public synchronized boolean putIfAbsent(E x) {boolean absent = !list.contains(x);if (absent)list.add(x);return absent;}}
这段代码很具有迷惑性。一般会认为他是线程安全的,但是事实上不是。原因在于Collections.synchronizedList(new ArrayList<E>()) 使用的锁与putIfAbsent使用的锁不是同一个锁,putIfAbsent与list的其他方法可以并发执行,因此不是线程安全的。

4.2Composition

事实上,使用组合的方式实现上面的功能会更加健壮。

eg:

@ThreadSafepublic class ImprovedList<T> implements List<T> {private final List<T> list;public ImprovedList(List<T> list){this.list = list;}public synchronized boolean putIfAbsent(T x) {boolean contains = list.contains(x);if (contains)list.add(x);return !contains;}public synchronized void clear() { list.clear(); }// ... similarly delegate other List methods}
下面分析一下上面的代码,这里把List封装进ImprovedList中,使客户无法直接操作List,然后使用同步机制对List操作进行封装,保证操作的原子性,实现了线程安全。这里使用的都是内部锁,不容易出错。

并发编程是easy to start,tough to master,之所以难是因为它比较麻烦,涉及到各个线程的协调,约束条件的维护等等。其实生活中类似多线程的情况多得是。比如打牌的过程就是这样一种过程。每个人都相当于一个单独的线程,如果不进行线程同步,可能会出现其中一个人出牌后,其他多个人同时出牌的情况,继续往下分析就会发现后面的问题变得更糟糕,因为在多个人并发出牌情况下,其他人可能看到的情况也不相同,每个人按照自己看到的出牌,这还能快乐的玩耍吗抓狂。所以平时玩牌的过程是有一个隐式的锁的,这里的锁按顺序进行传递,获得锁的人具有出牌权,而且这里的锁必须是同一个,不然有多个锁的话,大家就分组各打各的了。接下来还存在一个问题,我拿到锁后,我随便出牌可以吗,这叫耍赖,那又没法玩了,所以我必须按照特定牌的规则出牌,而且任何其他人相当于其他线程都必须遵守这个规则。这样大家可以愉快的玩耍了大笑。继续分析这个过程,首先这里的牌本身不是线程安全的,任何人都可以对其做任何操作,我可以拿来折飞机,可以拿来扔着玩,但是一旦几个人决定玩牌了,就相当于对牌进行了封装,谁敢再折飞机试试,呵呵。每个人本来也是线程不安全的,它可以对牌做任何事情,但是呢大家决定一起玩牌了,就相当于对人进行了封装,它有很多方法,这里只调用它摸牌、思考、出牌的方法,而且使用锁对其进行了同步,这样就ok了,不扯太多了。。。


0 0
原创粉丝点击