您在Java中遇到的最常见的并发问题是什么?

时间:2009-01-20 15:58:39

标签: java multithreading concurrency

这是对Java中常见并发问题的一种调查。一个例子可能是经典的死锁或竞争条件,或者可能是Swing中的EDT线程错误。我对各种可能的问题既感兴趣,也对最常见的问题感兴趣。因此,请在每条评论中留下Java并发错误的一个特定答案,如果您看到一个已经遇到的错误,请立即投票。

49 个答案:

答案 0 :(得分:172)

两个不同的开源库做了类似这样的事情时,我发生了#1最痛苦的并发问题:

private static final String LOCK = "LOCK";  // use matching strings 
                                            // in two different libraries

public doSomestuff() {
   synchronized(LOCK) {
       this.work();
   }
}

乍一看,这看起来像一个非常简单的同步示例。然而;因为字符串在Java中是 interned ,所以文字字符串"LOCK"被证明是java.lang.String的同一个实例(即使它们彼此完全不同地宣布它们。)结果显然很糟糕。

答案 1 :(得分:121)

我见过的最常见的并发问题是没有意识到一个线程编写的字段不能保证被不同的线程看到。一个常见的应用:

class MyThread extends Thread {
  private boolean stop = false;

  public void run() {
    while(!stop) {
      doSomeWork();
    }
  }

  public void setStop() {
    this.stop = true;
  }
}

只要停止不是易失性setStoprun不是同步,就不能保证这不起作用。这个错误特别恶劣,因为99.999%在实践中无关紧要,因为读者线程最终会看到变化 - 但我们不知道他多久见到它。

答案 2 :(得分:64)

一个经典问题是在同步对象时更改正在同步的对象:

synchronized(foo) {
  foo = ...
}

其他并发线程然后在不同的对象上进行同步,并且此块不提供您期望的互斥。

答案 3 :(得分:49)

一个常见问题是使用来自多个线程的Calendar和SimpleDateFormat等类(通常通过在静态变量中缓存它们)而不进行同步。这些类不是线程安全的,因此多线程访问最终会导致状态不一致的奇怪问题。

答案 4 :(得分:46)

双重锁定。总的来说。

我开始学习BEA工作时遇到的问题的范例是人们会用以下方式检查单身人士:

public Class MySingleton {
  private static MySingleton s_instance;
  public static MySingleton getInstance() {
    if(s_instance == null) {
      synchronized(MySingleton.class) { s_instance = new MySingleton(); }
    }
    return s_instance;
  }
}

这永远不会起作用,因为另一个线程可能已经进入synchronized块并且s_instance不再为null。因此,自然的变化是:

  public static MySingleton getInstance() {
    if(s_instance == null) {
      synchronized(MySingleton.class) {
        if(s_instance == null) s_instance = new MySingleton();
      }
    }
    return s_instance;
  }

这也不起作用,因为Java内存模型不支持它。您需要将s_instance声明为volatile才能使其正常工作,即使这样它也只适用于Java 5.

不熟悉Java内存模型错综复杂的人们总是把这个搞得一团糟

答案 5 :(得分:44)

Collections.synchronizedXXX()返回的对象上没有正确同步,尤其是在迭代或多次操作期间:

Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());

...

if(!map.containsKey("foo"))
    map.put("foo", "bar");

那是错误。尽管单个操作为synchronized,但调用containsput之间的映射状态可以由另一个线程更改。它应该是:

synchronized(map) {
    if(!map.containsKey("foo"))
        map.put("foo", "bar");
}

ConcurrentMap实施:

map.putIfAbsent("foo", "bar");

答案 6 :(得分:36)

虽然可能不是您要求的,但我遇到的最常见的并发相关问题(可能是因为它出现在普通的单线程代码中)是

java.util.ConcurrentModificationException

由以下内容引起:

List<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));
for (String string : list) { list.remove(string); }

答案 7 :(得分:28)

我们看到的最常见的错误是程序员在EDT上执行长时间的操作,如服务器调用,锁定GUI几秒钟,并使应用程序无响应。

答案 8 :(得分:27)

可以很容易地认为同步集合可以为您提供比实际更多的保护,并且忘记在调用之间保持锁定。我已经看过几次这个错误了:

 List<String> l = Collections.synchronizedList(new ArrayList<String>());
 String[] s = l.toArray(new String[l.size()]);

例如,在上面的第二行中,toArray()size()方法本身都是线程安全的,但size()toArray()分开评估这两个调用之间没有保持List上的锁定。

如果您使用另一个并发线程从列表中删除项目来运行此代码,迟早会返回一个新的String[]返回,该值大于保留所有列表中的元素,尾部有空值。很容易想到,因为对List的两个方法调用发生在一行代码中,这在某种程度上是一个原子操作,但事实并非如此。

答案 9 :(得分:27)

在循环中忘记wait()(或Condition.await()),检查等待条件是否为真。如果没有这个,你会遇到虚假的wait()唤醒错误。规范用法应该是:

 synchronized (obj) {
     while (<condition does not hold>) {
         obj.wait();
     }
     // do stuff based on condition being true
 }

答案 10 :(得分:25)

另一个常见的错误是异常处理不当。当后台线程抛出异常时,如果您没有正确处理它,您可能根本看不到堆栈跟踪。或者,您的后台任务可能会停止运行,并且永远不会再次启动,因为您无法处理异常。

答案 11 :(得分:21)

在我和Brian Goetz一起上课之前,我没有意识到通过同步getter变异的私有字段的非同步setter永远不会保证返回更新的值。只有当变量受读取和写入上的同步块保护时,才能保证变量的最新值。

public class SomeClass{
    private Integer thing = 1;

    public synchronized void setThing(Integer thing)
        this.thing = thing;
    }

    /**
     * This may return 1 forever and ever no matter what is set
     * because the read is not synched
     */
    public Integer getThing(){
        return thing;  
    }
}

答案 12 :(得分:13)

不应在同步块内进行任意方法调用。

Dave Ray在他的第一个答案中触及了这一点,事实上我也遇到了一个死锁,也与在一个synchronized方法中调用侦听器上的方法有关。我认为更一般的教训是,方法调用不应该从同步块中“进入狂野” - 你不知道调用是否会长时间运行,导致死锁或其他什么。

在这种情况下,通常情况下,解决方案是减少同步块的范围,以保护关键的私有代码段。

此外,由于我们现在正在访问同步块之外的侦听器集合,因此我们将其更改为写入时复制集合。或者我们可以简单地制作一个防御性的收藏品。关键是,通常有安全访问未知对象集合的替代方案。

答案 13 :(得分:13)

认为你正在编写单线程代码,但使用可变静态(包括单例)。显然它们将在线程之间共享。这种情况经常发生。

答案 14 :(得分:11)

我遇到的最近与Concurrency相关的错误是一个对象,它在其构造函数中创建了一个ExecutorService,但是当该对象不再被引用时,它从未关闭ExecutorService。因此,在几周内,数千个线程泄露,最终导致系统崩溃。 (从技术上讲,它没有崩溃,但它确实停止了正常运行,同时继续运行。)

从技术上讲,我认为这不是并发问题,但这是与使用java.util.concurrency库有关的问题。

答案 15 :(得分:10)

当存在可由每个请求设置的可变字段时,我遇到了Servlet的并发问题。 但是对于所有请求只有一个servlet-instance,所以这在单个用户环境中完美地工作,但是当多个用户请求servlet发生不可预测的结果时。

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 ...
    }
}

答案 16 :(得分:9)

在类的构造函数中启动一个线程是有问题的。如果扩展了类,则可以在执行子类'constructor 之前启动线程。

答案 17 :(得分:9)

不完全是一个错误,但最糟糕的是提供了一个你打算让别人使用的库,但没有说明哪些类/方法是线程安全的,哪些只能从单个线程调用等。

更多人应该使用Goetz书中描述的并发注释(例如@ThreadSafe,@ GuardBy等)。

答案 18 :(得分:9)

不平衡的同步,特别是针对地图似乎是一个相当普遍的问题。许多人认为将put放入Map(不是ConcurrentMap,而是说HashMap)并且不同步get就足够了。然而,这可能在重新哈希期间导致无限循环。

然而,在具有读写共享状态的任何地方都可能发生同样的问题(部分同步)。

答案 19 :(得分:9)

我最大的问题一直是死锁,特别是由持有锁定的侦听器造成的。在这些情况下,在两个线程之间进行反向锁定非常容易。在我的例子中,在一个线程中运行的模拟和在UI线程中运行的模拟的可视化之间。

编辑:将第二部分移至单独答案。

答案 20 :(得分:8)

共享数据结构中的可变类

Thread1:
    Person p = new Person("John");
    sharedMap.put("Key", p);
    assert(p.getName().equals("John");  // sometimes passes, sometimes fails

Thread2:
    Person p = sharedMap.get("Key");
    p.setName("Alfonso");

当发生这种情况时,代码要比这个简化的例子复杂得多。复制,查找和修复错误很难。如果我们可以将某些类标记为不可变且某些数据结构只保存不可变对象,则可以避免这种情况。

答案 21 :(得分:8)

我相信Java的主要问题是构造函数的(缺乏)可见性保证。例如,如果您创建以下类

class MyClass {
    public int a = 1;
}

然后从另一个线程中读取MyClass的属性 a ,MyClass.a可以是0或1,具体取决于JavaVM的实现和情绪。今天'a'成为1的可能性非常高。但是在未来的NUMA机器上,这可能会有所不同。很多人都没有意识到这一点,并且认为在初始化阶段他们不需要关心多线程。

答案 22 :(得分:8)

对字符串文字或由字符串文字定义的常量进行同步(可能)是一个问题,因为字符串文字是实习的,并且将由JVM中的任何其他人使用相同的字符串文字共享。我知道应用服务器和其他“容器”场景中出现了这个问题。

示例:

private static final String SOMETHING = "foo";

synchronized(SOMETHING) {
   //
}

在这种情况下,任何使用字符串“foo”锁定的人都会共享同一个锁。

答案 23 :(得分:7)

使用本地“new Object()”作为互斥锁。

synchronized (new Object())
{
    System.out.println("sdfs");
}

这没用。

答案 24 :(得分:7)

另一个常见的“并发”问题是在根本不需要时使用同步代码。例如,我仍然看到程序员使用StringBuffer甚至java.util.Vector(作为方法局部变量)。

答案 25 :(得分:7)

我经常犯的最蠢的错误是在对象上调用notify()或wait()之前忘记同步。

答案 26 :(得分:6)

受锁保护但通常连续访问的多个对象。我们遇到过几种情况,其中锁是由不同的代码以不同的顺序获得的,导致死锁。

答案 27 :(得分:5)

没有意识到内部类中的this不是外部类的this。通常在实现Runnable的匿名内部类中。根本问题是因为同步是所有Object的一部分,所以实际上没有静态类型检查。我在usenet上至少看过两次,它也出现在Brian Goetz'z Java Concurrency in Practice中。

BGGA闭包不会受此影响,因为闭包没有thisthis引用外部类)。如果你使用非this对象作为锁,那么它可以解决这个问题和其他问题。

答案 28 :(得分:3)

使用静态变量等全局对象进行锁定。

由于争用,这会导致非常糟糕的性能。

答案 29 :(得分:3)

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

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

解决方案是使用-XX:+ DisableExplicitGC

关闭显式gc

答案 30 :(得分:3)

没有意识到java.awt.EventQueue.invokeAndWait acts as if it holds a lock(独家访问事件调度线程,EDT)。关于死锁的好处是,即使很少发生这种情况,你也可以使用jstack等来获取堆栈跟踪。我已经在一些广泛使用的程序中看到了这一点(我在Netbeans中看到过的一个问题的解决方案应该包含在下一个版本中)。

答案 31 :(得分:3)

Honesly?在java.util.concurrent出现之前,我经常遇到的最常见的问题是我称之为“线程颠簸”的问题:使用线程进行并发的应用程序,但会产生过多的并且最终会发生颠簸。

答案 32 :(得分:2)

一种将数据保存到实例变量的方法,以便“节省工作量”将其传递给辅助方法,而另一种可以同时调用的方法为了自己的目的使用相同的实例变量。

应该在同步调用的持续时间内将数据作为方法参数传递。这只是我最糟糕的记忆的一点点简化:

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());
    }

}

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

答案 33 :(得分:2)

使用带有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 lock对象的所有者。

答案 34 :(得分:2)

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

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

3)在同步块中投入太多耗时的操作通常会导致非常糟糕的性能。幸运的是,并发包中的Future模式可以安全地度过这一天。

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

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

答案 35 :(得分:2)

保持所有线程忙。

最常见的是必须修复其他人的代码中的问题,因为他们滥用了锁定结构。最近,我的同事似乎发现读者/作者锁定很有趣,而一点点思想完全消除了他们的需要。

在我自己的代码中,保持线程繁忙不那么明显但具有挑战性。它需要更深入地考虑算法,例如编写新的数据结构,或仔细设计系统以确保在使用锁定时它永远不会出现争议。

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

答案 36 :(得分:2)

试试这段代码..

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 ...
    }
}

答案 37 :(得分:2)

未能在管理长时间运行的线程的对象上提供明确定义的生命周期方法。我喜欢创建名为init()和destroy()的方法对。实际调用destroy()也很重要,这样你的应用就可以优雅地退出。

答案 38 :(得分:2)

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

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

答案 39 :(得分:1)

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

答案 40 :(得分:1)

由于Java 5存在Thread.getUncaughtExceptionHandler,但在使用ExecutorService / ThreadPool时从不调用此UncaughtExceptionHandler。
至少我无法使用ExcutorService工作来获取UncaughtExceptionHandler。

答案 41 :(得分:1)

我遇到了一个创建倒计时锁存器的I / O线程的伪死锁。这个问题的一个非常简化的版本就像:

public class MyReader implements Runnable {

  private final CountDownLatch done = new CountDownLatch(1);
  private volatile isOkToRun = true;

  public void run() {
    while (isOkToRun) {
       sendMessage(getMessaage());
    }
    done.countDown();
  }

  public void stop() {
    isOkToRun = false;
    done.await();
  }

}

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

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

答案 42 :(得分:0)

我在java中发现的令人讨厌的问题是有多个线程在没有同步的情况下访问HashMap。如果一个人正在阅读而另一个正在写作,那么读者很有可能以无限循环结束(存储桶节点列表结构被破坏成循环列表)。

显然你不应该首先这样做(使用ConcurrentHashMap或Collections.synch ...包装器),但它似乎总是通过网络导致正确的线程卡住,系统完全破坏,通常是由于包含这样一个地图的实用程序类在堆栈中的几个级别而没有人想到它。

答案 43 :(得分:0)

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

答案 44 :(得分:0)

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

  1. 编写代码时,始终知道您所在的主题
  2. 在设计要重用的类或API时,始终问自己代码是否必须是线程安全的。最好做出慎重的决定,并记录你的单位线程安全,而不是与潜在的死锁进行不明智的同步。
  3. new Thread()的调用是一种气味。使用专用的ExecutorServices,这会迫使您考虑应用程序的整体线程概念(参见1)并鼓励其他人遵循它。
  4. 了解和使用库类(例如AtomicBoolean ,同步集合等)。再次:有意识地决定线程安全在特定环境中是否重要,不要盲目地使用它们。

答案 45 :(得分:0)

while(true)
{
   if (...)
     break

   doStuff()
}

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

即如果该块没有退出,应用程序甚至系统都会锁定并死掉。仅仅因为一个简单的while(fantasy_land)...if(...) break

答案 46 :(得分:0)

协助在功能Java中实现Actors并对多核计算机上的数百万个线程进行基准测试。

答案 47 :(得分:0)

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;
    }
}

我试图在这里指出的问题(以及其他)是在设置值“Hallo”之前发生SharedObject obj的刷新。这意味着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;
    }
}

答案 48 :(得分:0)

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