【问题标题】:How to deal with Concurrency before you start coding [closed]在开始编码之前如何处理并发[关闭]
【发布时间】:2011-01-11 01:38:16
【问题描述】:

我正在编写一个 Java 程序,目前我正在调试的并发问题比我想处理的要多得多。

我不得不问:你在精神上设置程序时如何处理并发问题?就我而言,这是一款相对简单的游戏,但线程问题不断出现——任何快速修复几乎肯定会导致新问题。

笼统地说,在决定我的应用程序应该如何“流动”而不让所有线程都打结时,我应该使用什么技术?

【问题讨论】:

  • 定义“打结”?我们是在谈论由于竞争条件导致的死锁或错误吗?
  • @meriton:没有那么多死锁,但列表不断抛出 ConcurrentModificationExceptions。我已经同步了它们的修饰函数 - 够了吗?
  • 可能不会,如果执行:“if (list.Count > 0) { a = list[0]; }”会怎样?如果另一个线程清除中间的列表会发生什么?线程化不容易,千万不要掉以轻心,没有速战速决,只有经验、知识、毅力和大量的调试,才能让你的代码正常工作。
  • @Lasse V. Karlsen:听到我不是唯一一个觉得这很困难的人,我很放心。

标签: java concurrency multithreading project-planning


【解决方案1】:

并发归结为管理共享状态。

“所有并发问题归结为 协调对可变状态的访问。 可变状态越少,越容易 是为了保证线程安全。” -- Java 并发实践

所以你必须问自己的问题是:

  • 我的应用程序需要的固有共享数据是什么?
  • 线程何时可以处理数据的快照,也就是说,它可以暂时处理共享数据的克隆?
  • 我能否识别已知的模式并使用更高级别的抽象而不是低级别的锁和线程协调,例如队列、执行器等?
  • 考虑一个全局锁定方案以避免死锁并获得一致的锁定

管理共享状态的最简单方法是序列化每个操作。然而,这种粗粒度的方法会导致高锁争用和较差的性能。可以将管理并发视为一种优化练习,您可以在其中尝试减少争用。所以后续的问题是:

  • 最简单的方法是什么?
  • 在不使解决方案过于复杂的情况下,我可以做出哪些简单的选择来减少争用(可能使用细粒度锁定)并提高性能?
  • 什么时候我会过于细粒度,也就是说,引入的复杂性不值得性能提升?

许多减少争用的方法依赖于某种形式的权衡,在强制执行正确行为所需的内容和减少争用的可行方法之间进行。

  • 我在哪里可以放松一些约束并接受有时东西不会 100% 正确(例如计数器)?
  • 我能否保持乐观并仅在发生并发修改时处理冲突(例如,使用时间戳和重试逻辑 - 这就是 TM 所做的)?

请注意,我从未开发过游戏,只开发过企业应用的服务器端部分。我可以想象它会完全不同。

【讨论】:

    【解决方案2】:

    我尽可能使用不可变的数据结构。大约我唯一一次使用可变结构是当我不得不使用一个可以节省大量工作的库时。即便如此,我还是尝试将该库封装在一个不可变的结构中。如果事情不能改变,那么就不用担心了。

    我应该补充一点,在您未来的工作中要记住的一些事情是 STM 和 Actor 模型。这两种并发方法都显示出非常好的进展。虽然每个都有一些开销,但取决于您的程序的性质,这可能不是问题。

    编辑:

    以下是一些您可以在下一个项目中使用的库的链接。 Deuce STM 顾名思义是 Java 的 STM 实现。然后是ActorFoundry,顾名思义,它是Java 的Actor 模型。但是,我忍不住用其内置的 Actor 模型为Scala 制作插件。

    【讨论】:

      【解决方案3】:

      你拥有的线程越少,它们共享的状态就越小,它们在这个共享状态上的交互模式越简单,你的生活就会越简单。

      您说列表正在抛出 ConcurrentModificationException。我认为您的列表是由单独的线程访问的。所以你应该问自己的第一件事是这是否有必要。第二个线程是不是不能对列表的副本进行操作?

      如果线程确实需要同时访问列表,则在整个遍历期间锁定列表可能是一种选择(如果列表通过任何其他方式修改,则迭代器无效迭代器)。当然,如果你在遍历链表的同时做其他事情,这个遍历可能会花费很长时间,并且锁定其他线程可能会威胁到系统的活力。

      还要记住,如果列表是共享状态,那么它的内容也是,所以如果你打算通过复制列表来绕过锁定,一定要执行深拷贝,或者证明列表中包含的对象是它们本身是线程安全的。

      【讨论】:

        【解决方案4】:

        对于您提到的 ConcurrentModificationExceptions,您的应用程序的多线程性质可能是一个红鲱鱼:还有其他方法可以获得不一定涉及多个线程的 ConcurrentModificationException。 考虑以下几点:

        List<Item> items = new ArrayList<Item>();
        
        //... some code adding items to the list
        
        for (Item item : items) {
            if(item.isTheOneIWantToRemove()) {
                items.remove(item); //This will result in a ConcurrentModificationException
            }
        }
        

        将 for 循环更改为带有迭代器的循环,或者增加索引值可以解决问题:

        for (Iterator<String> it = items.iterator(); it.hasNext();) {
            if(item.isTheOneIWantToRemove()) {
                it.remove(); //No exception thrown
            }
        }
        

        for (int i = 0; i < items.size(); i++) {
            if(item.isTheOneIWantToRemove()) {
                items.remove(items.get(i)); //No exception thrown
            }
        }
        

        【讨论】:

          【解决方案5】:

          从设计的角度来看,我发现绘制序列图很有用,其中每个线程的操作都用颜色编码(也就是说,每个线程都有自己的颜色)。以这种方式使用颜色可能是序列图的非标准用法,但它有助于概述线程交互的方式和位置。

          正如其他人所提到的,将设计中的线程数量减少到使其正常工作所需的绝对最小值也会有很大帮助。

          【讨论】:

            【解决方案6】:

            这取决于你的线程做什么。通常程序有一个执行思考的主线程和一个执行并行任务的工作线程(计时器,在 GUI 上处理长计算等)但是您的应用程序可能会有所不同 - 这取决于您的设计。你用线程做什么?你有什么锁来保护共享数据结构?如果您使用多个锁,您是否有一个锁定顺序以防止死锁?

            【讨论】:

              【解决方案7】:
              1. 尝试使用 java.util.concurrent 包中的集合,甚至是 Google Collections 中更好的不可变集合。
              2. 阅读有关使用同步块的信息

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2011-03-02
                • 1970-01-01
                • 2013-02-20
                • 1970-01-01
                • 2010-11-19
                • 2023-03-22
                • 1970-01-01
                相关资源
                最近更新 更多