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

这是一个关于 Java 中常见并发问题的调查。一个例子可能是 Swing 中的典型死锁或竞态条件,或者 EDT 线程错误。我不仅对可能的问题的广度感兴趣,而且对最常见的问题感兴趣。因此,请在每条评论中留下一个关于 Java 并发 bug 的具体答案,如果您遇到了这样的问题,请投赞成票。

76583 次浏览

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

synchronized(foo) {
foo = ...
}

然后,其他并发线程在不同的对象上进行同步,而这个块并没有提供您所期望的互斥锁。

使用全局对象(如用于锁定的静态变量)。

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

我遇到的最大的问题一直是死锁,尤其是由于侦听器触发时保持了一个锁。在这些情况下,很容易在两个线程之间实现反向锁定。在我的例子中,在一个线程中运行的模拟和在 UI 线程中运行的模拟的可视化之间。

编辑: 移动第二部分到单独的答案。

不平衡的同步,特别是针对 Maps 的同步,似乎是一个相当常见的问题。许多人认为同步 put 到 Map (不是 ConcurrentMap,而是 HashMap)并且不同步 gets 就足够了。然而,这可能导致在重新散列期间出现无限循环。

但是,同样的问题(部分同步)可能发生在读写共享状态的任何地方。

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

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

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

java.util.ConcurrentModificationException

由以下原因引起的:

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

受锁保护但通常连续访问的多个对象。我们遇到过这样的情况: 不同的代码以不同的顺序获得锁,从而导致死锁。

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");

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

忘记在循环中等待()(或 Condition.wait ()) ,检查等待条件是否为真。如果不这样做,就会遇到来自虚假的 wait ()唤醒的 bug。规范的用法应该是:

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

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

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

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

认为您正在编写单线程代码,但使用可变的静态(包括单例)。显然,它们将在线程之间共享。这种情况出人意料地频繁发生。

没有实现 java.awt.EventQueue.invokeAndWait的行为就好像它持有一个锁(对事件分派线程,EDT 的独占访问)。死锁的好处是,即使这种情况很少发生,您也可以使用 jstack 或类似的方法获取堆栈跟踪。我在许多广泛使用的程序中看到过这种情况(我只在 Netbeans 看到过一次修复问题的方法,应该包含在下一个版本中)。

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

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

我工作的地方最常见的 bug 是程序员在 EDT 上执行长时间的操作,比如服务器调用,锁定 GUI 几秒钟,使应用程序无响应。

另一个常见的错误是差的异常处理。当后台线程抛出异常时,如果不正确处理,可能根本看不到堆栈跟踪。或者,由于未能处理异常,您的后台任务停止运行而不再重新启动。

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

我在 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 才能使其工作,即使这样,它也只能在 Java5上工作。

不熟悉 Java 内存模型复杂性的人会把 一直都是搞得一团糟。

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

我在使用 Servlet 时遇到了一个并发问题,每个请求都会设置一些可变字段。 但是对于所有请求只有一个 servlet 实例,所以这在单个用户环境中可以很好地工作,但是当多个用户请求时,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 ...
}
}

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

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

没用的。

从 Java 5开始,就有了 Thread.getUncaughtExceptionHandler,但是当使用 ExecutorService/ThreadPool 时,这个 UncaughtExceptionHandler 从来不会被调用。 < br/> 至少我不能让一个 ExcutorService 工作的 UncaughtExceptionHandler

共享数据结构中的可变类

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");

当这种情况发生时,代码要比这个简化的示例复杂得多。复制、查找和修复这个 bug 是很困难的。如果我们能够将某些类标记为不可变的,而将某些数据结构标记为只包含不可变对象,那么这种情况也许可以避免。

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 对象的刷新发生在设置值“ 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;
}
}

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

class MyThread extends Thread {
private boolean stop = false;


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


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

只要停止不是 反复无常setStoprun不是 同步这是不能保证工作。这个错误在99.999% 的情况下尤其恶劣,因为读者线程最终会看到这个变化,但我们不知道他是多久看到这个变化的。

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

Dave Ray 在他的第一个回答中提到了这个问题,事实上,我还遇到了一个死锁,这个死锁与从同步方法中调用侦听器的方法有关。我认为更普遍的教训是,方法调用不应该从同步块中“进入野外”——你不知道调用是否会长时间运行,导致死锁,或者其他什么。

在这种情况下(通常情况下) ,解决方案是减少同步块的范围,只保护关键的 二等兵代码段。

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

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

class MyClass {
public int a = 1;
}

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

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

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

例如:

private static final String SOMETHING = "foo";


synchronized(SOMETHING) {
//
}

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

我的 # 最痛苦的 # 并发问题曾经发生在 两个不同的人开放源代码库执行以下操作时:

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


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

乍一看,这似乎是一个非常简单的同步示例。但是,因为字符串在 Java 中是 被拘留,所以字符串 "LOCK"实际上是同一个 java.lang.String实例(即使它们彼此声明完全不同)结果显然很糟糕。

保持所有线程忙碌。

这种情况在修复其他人代码中的问题时最为常见,因为他们滥用了锁定结构。最近,我的同事们似乎发现阅读器/写器锁非常有趣,而一点点想法就能完全消除他们的需求。

在我自己的代码中,保持线程忙碌虽然不那么明显,但是很有挑战性。它需要对算法进行更深入的思考,比如编写新的数据结构,或者精心设计一个系统,以确保在使用锁定时不会出现竞争。

解决并发性错误很容易,但要想弄清楚如何避免锁争用就很难了。

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

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

3)在同步块中放入太多耗时的操作往往会导致非常糟糕的性能。幸运的是,并发包中的 Future 模式可以保证万无一失。

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

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

协助在 FunctionalJava 中实现 Actors 并在多核机器上对数百万个线程进行基准测试。

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


doStuff()
}
当开发人员写 while 循环时,他们总是错过“资源提交” 用他们自己的代码

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

我的两点建议是,从一开始就努力避免同步问题ーー注意以下问题/气味:

  1. 编写代码时,始终使用 知道自己的处境
  2. 当设计用于重用的类或 API 时,始终使用 问问自己代码是否必须是线程安全的。最好做一个深思熟虑的决定,并记录下您的单元是 没有线程安全的,而不要让不明智的同步带来潜在的死锁。
  3. new Thread()的调用是一种味道.而是使用专用的 ExecutorServices,它强制您考虑应用程序的整体线程概念(参见1) ,并鼓励其他人遵循它。
  4. 了解和使用库类(如 AtomicBoolean 等等、同步集合等)。再次强调: 在给定的上下文中,有意识地决定线程安全性是否重要,不要只是盲目地使用它们。

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

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

解决方案是使用-XX: + DisableExplicity GC 关闭显式 gc

这并不完全是一个 bug,但最糟糕的是提供了一个你希望其他人使用的库,但是没有说明哪些类/方法是线程安全的,哪些只能从单个线程调用等等。

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

人们很容易认为,同步收集给予您的保护比实际上要多,而且忘记了保持调用之间的锁。这个错误我已经见过好几次了:

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

例如,在上面的第二行中,toArray()size()方法本身都是线程安全的,但是 size()toArray()是分开计算的,List 上的锁不在这两个调用之间。

如果您使用另一个线程 同时从列表中删除项目来运行这段代码,那么您迟早会返回一个新的 String[],这个返回值大于保存列表中所有元素所需的值,并且在尾部具有 null 值。很容易认为,由于对 List 的两个方法调用发生在一行代码中,因此这是一个原子操作,但实际上并非如此。

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

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

我遇到了一个 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 线程的活动可能并不明显!

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

直到我和 Brian Goetz 一起上课,我才意识到通过同步 setter变异的私有字段的非同步 getter保证返回更新的值。只有当 读和写上的同步块保护变量时,才能保证变量的最新值。

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

启动类的线程 在构造函数中是有问题的。如果类被扩展,就会执行可以启动的线程 在子类的构造函数之前

将数据保存到实例变量的方法,以便“省力”地将数据传递给 helper 方法,而另一个可以并发调用的方法为了自己的目的使用相同的实例变量。

相反,应该在同步调用期间将数据作为方法参数传递。这只是我最糟糕记忆的一个小小简化:

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


}

从逻辑上讲,登录和注销方法不必同步。但是写成这样,你就能体验到各种各样有趣的客户服务电话。

使用带等待和通知的不同锁对象的并发性问题。

我尝试使用 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();
}

这将导致 IllegalMonitor 异常,因为调用 notifyAll ()的线程已经使用此对象调用了 notifyAll () ,但不是此锁对象的所有者。但是当前线程是 O2锁对象的所有者。

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

我在 java 中发现的一个棘手的问题是,多个线程在没有同步的情况下访问 HashMap。如果一个正在读取,另一个正在写入,那么读取器很有可能以无限循环结束(bucket 节点列表结构被破坏为循环列表)。

显然,你一开始就不应该这样做(使用 ConcurrentHashMap 或 Collections.synch... wrapper) ,但它似乎总是穿过网络,导致适当的线程卡住,系统完全崩溃,通常是由于包含这样一个映射的实用工具类在堆栈的几个层次上,没有人想到它。

试试这个密码。

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 中最常见的并发问题是目前为止看起来可以工作的代码,尽管它根本不是线程安全的。由于一个很小的错误,它变成了一个定时炸弹,在几乎所有的情况下,你都不能提前知道,因为它对你来说并不明显。虽然常规错误代码有可能在测试期间失败,但并发代码通常最终才会失败,而且不可重复。