【问题标题】:What is the most frequent concurrency issue you've encountered in Java? [closed]您在 Java 中遇到的最常见的并发问题是什么? [关闭]
【发布时间】:2010-10-02 11:47:57
【问题描述】:

这是一个关于 Java 中常见并发问题的投票。一个例子可能是经典的死锁或竞争条件,或者可能是 Swing 中的 EDT 线程错误。我对广泛的可能问题感兴趣,也对最常见的问题感兴趣。因此,请在每条评论中留下一个关于 Java 并发错误的具体答案,如果您遇到遇到的问题,请投票支持。

【问题讨论】:

  • 为什么关闭?这对于寻求 Java 并发的其他程序员以及了解其他 Java 开发人员观察到的并发缺陷类别最多。
  • @Longpoke 关闭消息解释了它关闭的原因。这不是一个具有特定“正确”答案的问题,它更像是一个民意调查/列表问题。 Stack Overflow 不打算主持这类问题。如果您不同意该政策,您可以通过meta 进行讨论。
  • 我猜社区不同意这篇文章每天的浏览量超过 100 次!我发现它非常有用,因为我参与了专门用于解决并发问题的静态分析工具的开发contemplateltd.com/threadsafe。拥有一组常见的并发问题对于测试和改进 ThreadSafe 非常有用。
  • Code review checklist for Java Concurrency 以一种便于日常代码审查的形式消化了这个问题的答案中提到的大部分陷阱。

标签: java multithreading concurrency


【解决方案1】:

1) 我遇到的一个常见错误涉及迭代同步的 Collection 类。在获取迭代器之前和迭代过程中需要手动同步。

2) 另一个错误是大多数教科书给人的印象是使类线程安全只是在每个方法上添加同步的问题。这本身并不能保证 - 它只会保护特定类的完整性,但结果仍然可能是不确定的。

3) 在同步块中放置太多耗时的操作通常会导致非常糟糕的性能。幸运的是,并发包中的 Future 模式可以保证安全。

4) 缓存可变对象以提高性能通常也会导致多线程问题(有时很难跟踪,因为您假设您是唯一的用户)。

5) 必须小心处理使用多个同步对象。

【讨论】:

    【解决方案2】:

    启动 Java RMI 会导致后台任务运行,强制垃圾收集器每 60 秒运行一次。就其本身而言,这可能是一件好事,但是 RMI 服务器可能不是由您直接启动的,而是由您使用的框架/工具(例如 JRun)启动的。而且,RMI 可能实际上并没有被用于任何事情。

    最终结果是每分钟调用一次 System.gc()。在负载较重的系统上,您将在日志中看到以下输出 - 60 秒的活动,然后是长时间的 gc 暂停,然后是 60 秒的活动,然后是长时间的 gc 暂停。这对吞吐量来说是致命的。

    解决方法是使用-XX:+DisableExplicitGC关闭显式gc

    【讨论】:

      【解决方案3】:

      试试这个代码..

      public class MyServlet implements Servlet{
          private Object something;
      
          public void service(ServletRequest request, ServletResponse response)
              throws ServletException, IOException{
              this.something = request.getAttribute("something");
              doSomething();
          }
      
          private void doSomething(){
              this.something ...
          }
      }
      

      【讨论】:

      • 这是 Java Web 应用程序中最常见的问题。我只是好奇这段代码随着时间的推移产生问题的可能性有多大,具体取决于并发请求的平均数量(和时间段)。对于某些应用是否值得修复可能非常有用。
      【解决方案4】:

      没有意识到java.awt.EventQueue.invokeAndWait 的行为就像它持有一个锁(对事件调度线程,EDT 的独占访问)。死锁的伟大之处在于,即使这种情况很少发生,您也可以使用 jstack 等获取堆栈跟踪。我已经在许多广泛使用的程序中看到了这一点(对我在 Netbeans 中只见过一次的问题的修复应该包含在下一个版本中)。

      【讨论】:

        【解决方案5】:

        对象的 finalize/release/shutdown/destructor 方法和正常调用期间的竞争条件。

        在 Java 中,我对需要关闭的资源进行了大量集成,例如 COM 对象或 Flash 播放器。开发人员总是忘记正确执行此操作,最终导致线程调用已关闭的对象。

        【讨论】:

          【解决方案6】:

          未能为管理长时间运行的线程的对象提供明确定义的生命周期方法。我喜欢创建一对名为 init() 和 destroy() 的方法。实际调用 destroy() 也很重要,这样您的应用程序才能正常退出。

          【讨论】:

          • 这周我咬了一口。我有 destroy() 但没有很好地定义 init() 端导致一些比赛。
          【解决方案7】:

          保持所有线程忙碌。

          这在不得不去修复其他人的代码中的问题时最常见,因为他们滥用了锁定结构。最近,我的同事们似乎发现阅读器/编写器锁很有趣,但稍加思考就完全消除了他们的需求。

          在我自己的代码中,让线程保持忙碌并不那么明显,但具有挑战性。它需要对算法进行更深入的思考,例如编写新的数据结构,或者仔细设计一个系统以确保在使用锁定时它永远不会被争用。

          解决并发错误很容易 - 试图弄清楚如何避免锁争用可能很困难。

          【讨论】:

            【解决方案8】:

            当另一个可以同时调用的方法出于自己的目的使用相同的实例变量时,将数据保存到实例变量以“省力”将其传递给辅助方法的方法。

            数据应该在同步调用期间作为方法参数传递。这只是对我最糟糕的记忆的轻微简化:

            public class UserService {
            
                private String userName;
            
                public String getUserName() {
                    return userName;
                }
            
                public void login(String name) {
                    this.userName = name; 
                    doLogin();
                }
            
                private void doLogin() {
                    userDao.login(getUserName());
                }
            
                public void delete(String name) {
                    this.userName = name; 
                    doDelete();
                }
            
                private void doDelete() {
                    userDao.delete(getUserName());
                }
            
            }
            

            从逻辑上讲,登录和注销方法不必同步。但按原样编写,您可以体验各种有趣的客户服务电话。

            【讨论】:

              【解决方案9】:

              wait和notify使用不同锁对象的并发问题。

              我试图使用 wait() 和 notifyAll() 方法,这就是我如何使用并陷入地狱。

              线程1

              Object o1 = new Object();
              
              synchronized(o1) {
                  o1.wait();
              }
              

              在其他线程中。 线程 - 2

              Object o2 = new Object();
              
              synchronized(o2) {
                  o2.notifyAll();
              }
              

              Thread1 将等待 o1,而应该调用 o1.notifyAll() 的 Thread2 正在调用 o2.notifyAll()。线程 1 永远不会唤醒。

              当然了,在同步块中不调用 wait() 或 notifyAll() 以及不使用用于同步块的同一对象调用它们的常见问题。

              Object o2 = new Object();
              
              synchronized(o2) {
                  notifyAll();
              }
              

              这将导致 IllegalMonitorStateException,因为调用 notifyAll() 的线程已使用此对象调用 notifyAll(),但不是此锁对象的所有者。但是当前线程是o2锁对象的所有者。

              【讨论】:

              • 建议在带有布尔标志的 while 条件下使用 o1.wait() 以防止任何虚假唤醒。
              【解决方案10】:

              我遇到的最大问题是开发人员事后才添加多线程支持。

              【讨论】:

              • 虽然这肯定是个问题,但它不是原始问题中要求的并发错误。
              【解决方案11】:

              从 Java 5 开始,有了 Thread.getUncaughtExceptionHandler,但是当使用 ExecutorService/ThreadPool 时,这个 UncaughtExceptionHandler 永远不会被调用。
              至少我无法让 UncaughtExceptionHandler 与 ExcutorService 一起工作。

              【讨论】:

              • 您可以将 ThreadFactory 传递给 ExecutorService/ThreadPool,在 newThread 方法中,您可以将 UncaughtExceptionHandler 附加到每个新线程。
              【解决方案12】:

              我的两分钱从一开始就试图避免同步问题 - 注意以下问题/气味:

              1. 编写代码时,始终知道您在哪个线程中
              2. 在设计可重用的类或 API 时,请始终问问自己代码是否必须是线程安全的。最好做出深思熟虑的决定,并证明您的单元不是线程安全的,而不是不明智地进行可能死锁的同步。
              3. new Thread() 的调用是一种气味。改用专用的 ExecutorServices,这会迫使您考虑应用程序的整体线程概念(参见 1)并鼓励其他人遵循它。
              4. 了解和使用库类(如AtomicBoolean、同步集合等)。再次重申:对线程安全在给定上下文中是否重要做出有意识的决定,不要盲目地使用它们。

              【讨论】:

              • 这确实是个好建议,但没有列出 Java 并发错误。
              【解决方案13】:

              我在创建倒计时锁存器的 I/O 线程中遇到了伪死锁。这个问题的一个大大简化的版本是这样的:

              公共类 MyReader 实现 Runnable { 私人最终 CountDownLatch 完成 = new CountDownLatch(1); 私有易失性 isOkToRun = true; 公共无效运行(){ 而(isOkToRun){ 发送消息(获取消息()); } done.countDown(); } 公共无效停止(){ isOkToRun = 假; done.await(); } }

              stop() 的想法是在线程退出之前它不会返回,因此当它返回时系统处于已知状态。这没关系,除非 sendMessage() 导致调用 stop(),它将永远等待。只要永远不会从 Runnable 调用 stop(),一切都会按您的预期工作。然而,在大型应用程序中,Runnable 线程的活动可能并不明显!

              解决方案是调用 await() 并超时几秒钟,并在超时发生时记录堆栈转储和投诉。这在可能的情况下保留了所需的行为,并在遇到编码问题时暴露了它们。

              【讨论】:

              • 或者你可以得到looper的线程并且当从同一个线程调用stop时不要调用await
              【解决方案14】:

              在工作线程中而不是在 Swing 线程中更新 Swing UI 组件(通常是进度条)(当然应该使用 SwingUtilities.invokeLater(Runnable),但如果您忘记这样做,那么该错误可能需要很长时间表面。)

              【讨论】:

                【解决方案15】:

                协助在函数式 Java 中实现 Actor,并在多核机器上对数百万个线程进行基准测试。

                【讨论】:

                  【解决方案16】:
                  public class ThreadA implements Runnable {
                      private volatile SharedObject obj;
                  
                      public void run() {
                          while (true) {
                              obj = new SharedObject();
                              obj.setValue("Hallo");
                          }
                      }
                  
                      public SharedObject getObj() {
                          return obj;
                      }
                  }
                  

                  我在这里(尤其是)要指出的问题是 SharedObject obj 的刷新发生在设置值“Hallo”之前。这意味着 getObj() 的使用者可能会检索到 getValue() 返回 null 的实例。

                  public class ThreadB implements Runnable {
                      ThreadA a = null;
                  
                      public ThreadB(ThreadA a) {
                          this.a = a;
                      }
                  
                      public void run() {
                          while (true) {
                              try {
                                  System.out.println("SharedObject: " + a.getObj().getVal());
                                  Thread.sleep(50);
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }
                          }
                      }
                  }
                  
                  public class SharedObject {
                      private String val = null;
                  
                      public SharedObject() {
                      }
                  
                      public String getVal() {
                          return val;
                      }
                  
                      public void setVal(String val) {
                          this.val = val;
                      }
                  }
                  

                  【讨论】:

                  • 在这里也有一个简短的解释会很糟糕......这里似乎不止一个问题。
                  【解决方案17】:

                  我在 java 中发现的一个令人讨厌的问题是让多个线程在没有同步的情况下访问 HashMap。如果一个人在读,一个人在写,那么阅读器很有可能陷入无限循环(桶节点列表结构被破坏为循环列表)。

                  显然你一开始就不应该这样做(使用 ConcurrentHashMap 或 Collections.synch... 包装器),但它似乎总是通过网络并导致正确的线程卡住,系统完全崩溃,通常是由于包含此类地图的实用程序类位于堆栈的下几层,而没有人想到它。

                  【讨论】:

                    【解决方案18】:
                    while(true)
                    {
                       if (...)
                         break
                    
                       doStuff()
                    }
                    

                    当开发人员编写 while 循环时,他们总是会错过“资源提交” 在他们自己的代码中。

                    也就是说,如果该块不退出,应用程序甚至系统可能会被锁定并死掉。仅仅因为一个简单的while(fantasy_land)...if(...) break

                    【讨论】:

                    • 这是个问题,但我不认为这是一个并发错误?
                    【解决方案19】:

                    我认为 Java 中最常见的并发问题是目前看来实际上可以工作的代码,尽管它根本不是真正的线程安全的。由于一个微小的错误,它变成了一个定时炸弹,而且在几乎所有情况下,你都不会事先知道这一点,因为它对你来说并不明显。虽然常规错误代码在测试期间可能会失败,但并发代码通常只会最终失败且不可重现。

                    【讨论】:

                      猜你喜欢
                      • 2010-09-11
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2010-11-22
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多