确定同步范围?

时间:2010-09-30 14:05:32

标签: java multithreading concurrency theory

在尝试提高我对并发性问题的理解时,我正在考虑以下场景(编辑:我已将示例从List更改为Runtime,这更接近我正在尝试的内容) :

public class Example {
    private final Object lock = new Object();
    private final Runtime runtime = Runtime.getRuntime();
    public void add(Object o) { 
        synchronized (lock) { runtime.exec(program + " -add "+o); } 
    }
    public Object[] getAll() { 
        synchronized (lock) { return runtime.exec(program + " -list "); }
    }
    public void remove(Object o) { 
        synchronized (lock) { runtime.exec(program + " -remove "+o); } 
    }
}

就目前而言,每个方法在独立使用时都是线程安全的。现在,我想弄清楚的是如何处理调用类希望调用的位置:

for (Object o : example.getAll()) {
    // problems if multiple threads perform this operation concurrently
    example.remove(b); 
}

但是如上所述,无法保证状态在调用getAll()和调用remove()之间是一致的。如果多个线程调用它,我将遇到麻烦。所以我的问题是 - 我应该如何让开发人员以线程安全的方式执行操作?理想情况下,我希望以一种让开发人员难以避免/错过的方式强制执行线程安全,但同时并不复杂。到目前为止,我可以想到三个选项:

答:锁定'this',因此调用代码可以访问同步对象,然后可以包装代码块。缺点:难以在编译时强制执行:

synchronized (example) {
    for (Object o : example.getAll()) {
        example.remove(b);
    }
}

B:将组合代码放入Example类中 - 并且可以从优化实现中受益,如本例所示。缺点:添加扩展的痛苦,以及潜在的混合无关逻辑:

public class Example {
   ...
   public void removeAll() { 
       synchronized (lock) { Runtime.exec(program + " -clear"); } 
   }
}

C:提供一个Closure类。缺点:过多的代码,可能过于慷慨的同步块,实际上可以使死锁更容易:

public interface ExampleClosure {
    public void execute(Example example);
}
public Class Example {
    ...
    public void execute(ExampleClosure closure) { 
        synchronized (this) { closure.execute(this); } 
    }
}

example.execute(new ExampleClosure() {
        public void execute(Example example) { 
            for (Object o : example.getAll()) {
                example.remove(b);
            }
        }
    }
);

有什么我想念的吗?如何确定同步的范围,以确保代码是线程安全的?

6 个答案:

答案 0 :(得分:3)

使用通过API公开的ReentrantReadWriteLock。这样,如果有人需要同步多个API调用,他们就可以在方法调用之外获取锁定。

答案 1 :(得分:3)

通常,这是一个典型的多线程设计问题。通过同步数据结构而不是同步使用数据结构的概念,很难避免在没有锁定的情况下基本上对数据结构的引用这一事实。

我建议不要把锁做得那么靠近数据结构。但这是一个受欢迎的选择。

使这种风格有效的一种潜在技巧是使用编辑树助行器。实际上,您公开了一个对每个元素执行回调的函数。

// pointer to function:
//      - takes Object by reference and can be safely altered 
//      - if returns true, Object will be removed from list

typedef bool (*callback_function)(Object *o);

public void editAll(callback_function func) {  
    synchronized (lock) {
          for each element o { if (callback_function(o)) {remove o} } }  
} 

那么你的循环就变成了:

bool my_function(Object *o) {
 ...
     if (some condition) return true;
}

...
   editAll(my_function);
...

我工作的公司(corensic)具有从真实错误中提取的测试用例,以验证Jinx是否正确地发现了并发错误。这种类型的低级数据结构锁定没有更高级别的同步是非常常见的模式。树编辑回调似乎是这种竞争条件的流行修复。

答案 2 :(得分:2)

我认为j.u.c.CopyOnWriteArrayList是您尝试解决的类似问题的一个很好的例子。

JDK与Lists有类似的问题 - 有多种方法可以同步任意方法,但多次调用没有同步(这是可以理解的)。

所以CopyOnWriteArrayList实际上实现了相同的接口,但是有一个非常特殊的契约,无论谁调用它,都知道它。

与您的解决方案类似 - 您应该实现List(或任何接口),同时为现有/新方法定义特殊合同。例如,getAll的一致性无法保证,如果.remove为空,或者不在列表内,等等o的调用不会失败。如果用户希望两者合并和安全/一致的选项 - 你的这一类将提供一种特殊方法(例如safeDeleteAll),使其他方法尽可能接近原始合同。

所以回答你的问题 - 我会选择选项B,但也会实现原始对象正在实现的界面。

答案 3 :(得分:2)

我想每个人都错过了他真正的问题。迭代新的Object数组并尝试一次删除一个问题仍然在技术上不安全(虽然ArrayList植入不会爆炸,但它不会产生预期的结果)。

即使使用CopyOnWriteArrayList,当您尝试删除时,当前列表中也可能存在过时读取。

您提供的两条建议很好(A和B)。我的一般建议是B.使收集线程安全非常困难。一个好的方法是尽可能少地为客户提供功能(在合理范围内)。因此,提供removeAll方法并删除getAll方法就足够了。

现在你可以同时说'我想保持API的方式,让客户担心额外的线程安全'。如果是这样,请记录线程安全。记录“查找和修改”操作既非原子又非线程安全的事实。

今天的并发列表实现对于提供的单个函数都是线程安全的(get,remove add都是线程安全的)。复合函数不是,并且可以做的最好的事情就是记录如何使它们成为线程安全的。

答案 4 :(得分:1)

来自List.toArray()的Javadoc:

  

返回的数组将是“安全的”   没有引用它   由此列表维护。 (其他   单词,这个方法必须分配一个新的   数组,即使此列表由后备   数组)。因此呼叫者可以自由   修改返回的数组。

也许我不明白你想要完成什么。您是否希望Object[]数组始终与List的当前状态同步?为了实现这一点,我认为您必须在Example实例本身上进行同步并保持锁定,直到您的线程完成其方法调用AND它当前正在使用的任何Object[]数组。否则,您如何知道原始List是否已被另一个线程修改过?

答案 5 :(得分:1)

选择要锁定的内容时,必须使用适当的粒度。您在示例中抱怨的是粒度级别太低,其中锁定不包括必须一起发生的所有方法。您需要制作方法,将需要在同一个锁中共同发生的所有操作组合在一起。

锁是可重入的,因此高级方法可以毫无问题地调用低级同步方法。