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

195

这是一个关于Java常见并发问题的投票。例如,经典死锁或竞态条件,或者Swing中的EDT线程错误等。我对可能存在的问题广度和最常见的问题都很感兴趣。因此,请在每个评论中留下一个特定的Java并发错误的答案,并投票支持您遇到过的问题。


19
为什么这个被关闭了?这对于其他学习Java并发编程的程序员非常有用,同时可以让人们了解其他Java开发人员最常遇到的并发缺陷类别。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
@Longpoke 闭合信息解释了为什么被关闭。这不是一个具有特定“正确”答案的问题,而更像是一个投票/清单问题。Stack Overflow不打算主持这些类型的问题。如果您不同意该政策,可以在上进行讨论。 - Andrzej Doyle
8
我猜社区不同意,因为这篇文章每天都有100多次浏览!对我来说非常有用,因为我参与开发了一个专门设计用于修复并发问题的静态分析工具http://www.contemplateltd.com/threadsafe。拥有一个常见并发问题库对测试和改进ThreadSafe非常有帮助。 - Craig Manson
《Java并发编程代码审查清单》整理了本问题答案中提到的大多数陷阱,以一种方便日常代码审查的形式呈现。 - leventov
49个回答

182

我最痛苦的并发问题之一是由两个不同的开源库执行以下操作引起的:

#1 most painful表示这是最令人痛苦的问题之一。

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

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

乍一看,这似乎是一个相当琐碎的同步示例。然而,由于Java中的字符串被内部化,字面字符串"LOCK"实际上是java.lang.String的同一实例(尽管它们从根本上声明不同)。结果显然很糟糕。


63
这就是我喜欢使用“private static final Object LOCK = new Object();”的原因之一。 - Andrzej Doyle
17
我很喜欢它 - 哦,这太恶心了 :) - Thorbjørn Ravn Andersen
7
这是一个很适合收录在Java Puzzlers 2中的谜题。 - Dov Wasserman
12
实际上,这让我真的希望编译器拒绝允许您在字符串上进行同步。鉴于字符串池,不存在任何情况下这将是一个“好事情(tm)”。 - Jared
3
我看到过简单的synchronized("LOCK") {this.work();} :P - Peter Lawrey
显示剩余9条评论

127

我见过的最常见的并发问题是,没有意识到一个线程写入的字段不一定会被另一个线程看到。这种情况的常见应用:

class MyThread extends Thread {
  private boolean stop = false;
  
  public void run() {
    while(!stop) {
      doSomeWork();
    }
  }
  
  public void setStop() {
    this.stop = true;
  }
}
只要stop不是volatile,或者setStoprun没有synchronized,就不能保证它能正常工作。这个错误特别麻烦,因为在实践中99.999%情况下它不会产生影响,因为读取线程最终会看到更改,但我们不知道他看到更改的时间。

9
一个很好的解决方案是将“stop”实例变量声明为AtomicBoolean类型。这样做可以解决非volatile的所有问题,并保护你免受JMM(Java内存模型)的影响。 - Kirk Wylie
39
比“几分钟”更糟糕——你可能永远看不到它。根据内存模型,JVM允许优化 while(!stop) 为 while(true),然后你就完了。这可能仅会在某些虚拟机上发生,在服务器模式下才会发生,在循环的x次迭代之后JVM重新编译时才会发生等等。哎呀! - Cowan
2
为什么您想要在布尔值上使用AtomicBoolean而不是volatile boolean?我正在开发1.4+版本,所以仅声明volatile是否存在任何问题? - Pool
2
Nick,我认为这是因为原子CAS通常比volatile更快。 如果你正在开发1.4版本,我个人认为你唯一安全的选择是使用synchronized作为volatile。因为在Java 5中,volatile具有强大的内存屏障保证,而在1.4中则没有。 - Kutzi
5
@Thomas:这是因为Java内存模型。如果你想详细了解它,你应该阅读相关材料(例如Brian Goetz的《Java并发实战》可以很好地解释它)。 简而言之,除非你使用内存同步关键字/结构(如volatile、synchronized、AtomicXyz,以及线程完成时),否则一个线程没有任何保证能够看到由另一个线程对任何字段所做的更改。 - Kutzi
显示剩余11条评论

65

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

synchronized(foo) {
  foo = ...
}

其他并发线程正在同步不同的对象,而这个代码块并没有提供你所期望的互斥保护。


19
针对此问题,有一个被称为“非 final 字段上的同步可能不会有用语义”的 IDEA 检查。非常好。 - Jen S.
8
嗯...这描述有些拗口。"unlikely to have useful semantics" 更好的描述可能是 "很可能出了问题"。 :) - Alex Miller
你知道有哪些开源项目在某个版本中包含了这个漏洞吗?我正在寻找现实世界软件中这个漏洞的具体例子。 - reprogrammer
@reprogrammer - 没有。如果我知道,我会告诉他们去修复它。 :) - Alex Miller
我之前故意使用过这种结构。大致上是synchronized(locks){key=locks.get(key);}synchronized(key){objects.put(key,modify(objects.get(key)));},当然要在objects上适当同步。 - yingted
显示剩余5条评论

50

一种常见的问题是在没有同步的情况下从多个线程中使用CalendarSimpleDateFormat这样的类(通常通过缓存它们在静态变量中)。 这些类不是线程安全的,因此多线程访问最终会导致状态不一致的奇怪问题。


你知道有哪些开源项目在某个版本中包含了这个漏洞吗?我正在寻找现实世界软件中这个漏洞的具体例子。 - reprogrammer

48

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
或者更好的方式是使用ConcurrentHashMap和putIfAbsent方法。 - Tom Hawtin - tackline

47

双重检查锁定。总的来说。

这种编程范式,我在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;
  }
}

这种方法从来不起作用,因为另一个线程可能已经进入了同步块,并且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内存模型细节的人经常会搞砸这个。


7
枚举单例模式可以解决所有这些问题(请参阅Josh Bloch对此的评论)。它的存在应该在Java程序员中更广泛地传播。 - Robin
3
这是我用于懒加载单例类的方法。同时,由于这在Java中被隐式地保证,因此不需要进行同步。类Foo包含一个Holder静态内部类,该类拥有一个静态的Foo类实例foo。getInstance()方法返回Holder.foo,从而实现了只有在需要时才会初始化Foo的目的。 - Irfan Zulfiqar
伊尔凡,如果我没记错的话,那被称为普格方法。 - Chris R
@Robin,使用静态初始化器不是更简单吗?它们总是保证同步运行。 - matt b
@fhucho,这当然取决于您的使用情况。同步方法通常比其非同步变体慢几倍。有一些神话说同步方法调用比非同步方法调用慢多达50倍,但现代JVM将这个数字降低到大多数情况下约为2-10倍。如果你问我,仍然相当沉重。请查看http://www.ibm.com/developerworks/java/library/j-threads1/index.html以获取更多信息。 - Ibrahim Arief
显示剩余5条评论

37

虽然可能不完全符合您的要求,但我遇到的最常见的并发相关问题(可能是因为它会在正常的单线程代码中出现)是 java.util.ConcurrentModificationException,这通常是由以下情况引起的:

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

不,这正是我想要的。谢谢! - Alex Miller

31

很容易认为同步集合提供的保护比它们实际上提供的更多,并忘记在调用之间持有锁。我已经看到过这个错误几次:

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

例如,在上面的第二行中,toArray()size() 方法本身都是线程安全的,但是size()是与toArray()分开计算的,并且在这两个调用之间没有保持列表上的锁。
如果您以另一个线程同时从列表中删除项目运行此代码,迟早会返回一个新的String[],它比所需容纳列表中所有元素所需要的要大,并且尾部有空值。很容易认为,因为对列表的两个方法调用出现在单行代码中,所以这种情况在某种程度上是原子操作,但实际上并非如此。

5
好的例子。我认为可以更普遍地将其归类为“原子操作的组合不是原子操作”。(另一个简单的例子是volatile field ++) - Alex Miller

29

我工作中经常遇到的最常见的错误是程序员在事件分发线程(EDT)上执行长时间的操作,如服务器调用,锁定GUI几秒钟并使应用程序无响应。


其中一个答案,我真希望能够给多于一个点数。 - Epaga
3
EDT是指事件分派线程。 - mjj1409

28

在循环中忘记使用wait()(或Condition.await()),检查等待条件是否实际为true。如果没有这样做,你会遇到因虚假的wait()唤醒而导致的错误。标准用法应该是:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接