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


191

这是有关Java常见并发问题的各种民意测验。一个例子可能是经典的死锁或竞争条件,或者是Swing中的EDT线程错误。我不仅对可能出现的问题的范围感兴趣,而且对最常见的问题感兴趣。因此,请在每个评论中留下一个Java并发错误的特定答案,如果看到遇到的评论,请投票。


16
为什么关闭?这对于其他要求Java并发的程序员都非常有用,并且可以让其他Java开发人员了解哪些类的并发缺陷最多。
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

@Longpoke关闭消息说明了为什么关闭它。这不是一个带有特定“正确”答案的问题,它更多是一个民意测验/清单问题。而且Stack Overflow并不打算托管此类问题。如果您不同意该政策,则可能需要在meta上进行讨论。
Andrzej Doyle

7
我猜社区对本文的看法不一样,因为本文每天的访问量超过100!我参与静态分析工具的开发非常有用,该工具专门用于解决并发问题contemplateltd.com/threadsafe。拥有一堆经常遇到的并发问题对于测试和改进ThreadSafe很有帮助。
Craig Manson 2014年

Java Concurrency的代码审查清单以便于日常代码审查的形式消化了该问题的答案中提到的大多数陷阱。
leventov '19

Answers:


125

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

class MyThread extends Thread {
  private boolean stop = false;

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

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

只要停止不挥发setStoprun同步的,这是不能保证的工作。这个错误特别令人讨厌,因为在99.999%的实践中这无关紧要,因为读者线程最终会看到更改-但是我们不知道他多久才能看到更改。


9
一个很好的解决方案是使stop实例变量成为AtomicBoolean。它解决了非易失性的所有问题,同时使您免受JMM问题的困扰。
Kirk Wylie

39
这比“几分钟”还要糟糕-您可能永远也看不到它。在内存模型下,允许JVM将while(!stop)优化为while(true),然后进行处理。仅在某些VM上,仅在服务器模式下,仅在JVM循环x次迭代后重新编译JVM等时,才可能发生这种情况。
科恩

2
为什么要在可变布尔值上使用AtomicBoolean?我正在为1.4+版本开发,因此仅声明volatile是否有任何陷阱?
游泳池

2
尼克,我认为这是因为原子CAS通常比挥发性更快。如果您为1.4开发,恕我直言,唯一安全的选择是使用1.4,因为volatile在1.4中没有像Java 5那样具有强大的内存屏障保证
。– Kutzi

5
@Thomas:那是因为Java内存模型。如果您想详细了解它,则应该阅读它(Brian Goetz的Java Concurrency in Practice很好地解释了这一点)。简而言之:除非您使用内存同步关键字/构造(例如volatile,synchronized,AtomicXyz,而且还需要在线程完成时),否则一个线程不能保证看到由另一个线程对任何字段所做的更改
Kutzi

178

我的#1最痛苦的并发问题是在两个不同的开源库执行如下操作时发生的:

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

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

乍一看,这看起来像一个简单的同步示例。然而; 因为字符串是用Java 嵌入的,所以文字字符串实际上"LOCK"是的相同实例java.lang.String(即使它们彼此完全不同地声明。)结果显然很糟糕。


62
这就是为什么我更喜欢私有静态最终Object LOCK = new Object();的原因之一。
Andrzej Doyle,

17
我爱它-哦,这是讨厌的:)
托尔比约恩Ravn的安徒生

7
这是一个很好的一个Java的谜题2
多夫沃瑟曼

12
实际上...这确实让我希望编译器拒绝允许您在String上进行同步。在给定String实习生的情况下,在任何情况下都不会出现“好事(tm)”的情况。
杰瑞德(Jared)2009年

3
@Jared:“直到字符串被拘禁”才是没有道理的。字符串不会神奇地“成为”实习生。除非您已经具有指定String的规范实例,否则String.intern()返回一个不同的对象。此外,所有文字字符串和字符串值常量表达式都将被插入。总是。请参阅有关String.intern()和JLS的§3.10.5的文档。
劳伦斯·贡萨尔维斯

65

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

synchronized(foo) {
  foo = ...
}

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


19
为此有一个IDEA检查,称为“不太可能具有有用语义的非最终字段同步”。非常好。
Jen S.

8
哈...现在这是一个折磨的描述。最好将“不太可能具有有用的语义”描述为“最有可能破坏”。:)
亚历克斯·米勒

我认为是Bitter Java在其ReadWriteLock中具有此功能。幸运的是,我们现在有了java.util.concurrency.locks,而Doug的作用还更多。
Tom Hawtin-定位线

我也经常看到这个问题。就此而言,仅同步最终对象。FindBugs等。帮助,是的。
gimpf

这是分配期间的唯一问题吗?(请参见下面的@Alex Miller的地图示例)该地图示例也会遇到同样的问题吗?
亚历克斯比尔兹利

50

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


您是否知道某个开源项目的某个版本包含此错误?我正在寻找实际软件中此错误的具体示例。
重编程器2010年

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程序员中更广泛地传播。
罗宾

我还没有遇到过实际情况,即单例的惰性初始化实际上是合适的。如果是这样,只需声明该方法已同步即可。
Dov Wasserman,

3
这就是我用于Singleton类的延迟初始化的方法。也不需要同步,因为java隐式保证了同步。class Foo {静态类Holder {静态Foo foo = new Foo(); }静态Foo getInstance(){return Holder.foo; }
Irfan Zulfiqar,2009年

伊尔凡,这就是所谓Pugh的方法,从我记得
克里斯- [R

@Robin,仅使用静态初始化器难道不是很简单吗?这些总是保证同步运行。
马特b

47

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

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

...

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

这是错误的。尽管正在执行单个操作synchronized,但调用contains和之间的映射状态put可以由另一个线程更改。它应该是:

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

ConcurrentMap实施:

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

5
或者更好的方法是使用ConcurrentHashMap和putIfAbsent。
汤姆·哈特芬

37

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

java.util.ConcurrentModificationException

由以下原因引起:

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

不,那完全是我要找的东西。谢谢!
亚历克斯·米勒

30

可能很容易想到同步集合比实际授予的保护更多,而忘记了两次调用之间的锁定。我已经多次看到此错误:

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

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

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


5
好的例子。我认为我会更笼统地说“原子操作的组成不是原子”。(有关另一个简单示例,请参见volatile field ++)
Alex Miller

29

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


这些答案之一我希望我能为您提供不止一个观点
Epaga

2
EDT =事件调度线程
mjj1409'1

28

忘记循环地等待()(或Condition.await()),检查等待条件是否真实。否则,您会因虚假的wait()唤醒而遇到错误。规范用法应为:

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

26

另一个常见的错误是不良的异常处理。当后台线程引发异常时,如果处理不当,则可能根本看不到堆栈跟踪。或您的后台任务停止运行,再也无法启动,因为您无法处理该异常。


是的,现在有很好的工具可以通过处理程序来处理。
亚历克斯·米勒

3
您能否发布指向任何对此进行更详细说明的文章或参考的链接?
Abhijeet Kashnia

22

直到我和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;  
    }
}

5
在更高版本的JVM(我认为1.5及更高版本)中,使用volatile也可以解决该问题。
James Schek 09年

2
不必要。volatile为您提供最新值,因此它可以防止永远返回1,但不提供锁定。其接近,但不完全相同。
约翰·罗素,2009年

1
@JohnRussell我认为volatile可以保证事前发生关系。那不是“锁定”吗?“对volatile变量(第8.3.1.4节)的写操作将与任何线程对v的所有后续读取进行同步(其中,后续操作是根据同步顺序定义的)。”
肖恩2012年

15

认为您正在编写单线程代码,但是使用可变的静态变量(包括单例)。显然,它们将在线程之间共享。这经常出乎意料地发生。


3
确实是的!可变静态变量破坏线程限制。出乎意料的是,在JCiP或CPJ中,我从未发现有关此陷阱的任何信息。
朱利安·查斯唐

我希望这对于进行并发编程的人们是显而易见的。全局状态应该是检查线程安全性的第一位。
gtrak,2011年

1
@Gary Thing是,他们不认为自己在进行并发编程。
汤姆·哈特芬

15

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

戴夫·雷(Dave Ray)在他的第一个答案中谈到了这一点,实际上,我也遇到了一个僵局,这也与从同步方法中调用侦听器的方法有关。我认为,更普遍的教训是,不应从同步块内“疯狂”进行方法调用-您不知道该调用是否会长时间运行,导致死锁或其他原因。

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

另外,由于我们现在正在访问同步块之外的侦听器的集合,因此将其更改为写时复制集合。或者,我们可以只是简单地制作了防御系列的副本。关键是,通常存在替代方法来安全访问未知对象的集合。


13

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

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


11

不平衡的同步,尤其是针对Maps的同步,似乎是一个相当普遍的问题。许多人认为,对地图的put进行同步(不是ConcurrentMap,而是HashMap),而对get进行同步就足够了。但是,这可能导致在重新哈希过程中出现无限循环。

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


11

当每个请求都将设置可变字段时,我遇到了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 ...
    }
}

10

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

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


9

我最大的问题一直是死锁,尤其是由持有锁的用户触发的死锁。在这些情况下,两个线程之间的反向锁定确实很容易。就我而言,是在一个线程中运行的模拟与在UI线程中运行的模拟的可视化之间。

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


您可以将最后一个分开吗?让我们保持每个帖子1个。这是两个非常好的。
亚历克斯·米勒

9

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


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

发生这种情况时,代码比此简化示例要复杂得多。复制,查找和修复该错误很难。如果我们可以将某些类标记为不可变,将某些数据结构标记为仅包含不可变对象,则可以避免。


8

在字符串文字或由字符串文字定义的常量上进行同步(潜在地)是一个问题,因为该字符串文字是内部的,并且将由JVM中的其他任何人使用相同的字符串文字共享。我知道在应用程序服务器和其他“容器”方案中会出现此问题。

例:

private static final String SOMETHING = "foo";

synchronized(SOMETHING) {
   //
}

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


可能已被锁定。问题在于,何时插入字符串时的语义是不确定的(或IMNSHO,未定义)。会编译“ foo”的编译时常数,只有在这样做的情况下,才可以屏蔽从网络接口传入的“ foo”。
Kirk Wylie

是的,这就是为什么我专门使用文字字符串常量,该常量可以被保留的原因。
亚历克斯·米勒

8

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

class MyClass {
    public int a = 1;
}

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


我觉得这有点令人惊讶,但我知道您是个聪明的蒂姆,所以我会参考一下。:)但是,如果a是最终的,就不用担心了,对吗?然后,您在构建过程中会受到最终冻结语义的约束吗?
亚历克斯·米勒

我仍然在JMM中找到令我惊讶的东西,所以我不会相信我,但是对此我很确定。另请参见cs.umd.edu/~pugh/java/memoryModel/…。如果该字段是最终字段,那将不成问题,那么在初始化阶段之后它将可见。
Tim Jansen

2
如果刚创建的实例的引用在构造函数返回/完成之前已经在使用中,则这只是一个问题。例如,该类在构造期间在公共池中注册自己,而其他线程开始访问它。
ReneS

3
MyClass.a指示静态访问,而“ a”不是MyClass的静态成员。除此之外,这是“ ReneS”状态,这仅在泄漏对未完成对象的引用时才是问题,例如,将“ this”添加到构造函数中的某些外部映射。
Markus Jevring'1

7

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


8
与大多数并发问题不同,这不是很容易找到吗?至少您在这里收到IllegalMonitorStateException ...
Outlaw程序员,

值得庆幸的是,它很容易找到...但是它仍然是一个愚蠢的错误,浪费了我很多的时间:)
Dave Ray 2009年

7

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

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

这没用。


2
可能是没有用的,但是完全同步的行为却做了一些有趣的事情……当然每次创建一个新的Object都是完全浪费的。
TREE

4
这不是没有用的。它是无锁的内存屏障。
David Roussel

1
@大卫:唯一的问题- JVM可以通过在所有除去这些锁对其进行优化
yetanothercoder

@insighter我看到您的意见是共享的ibm.com/developerworks/java/library/j-jtp10185/index.html我同意这样做是很愚蠢的事情,因为您不知道记事栏何时同步,我只是指出那无所不能。
David Roussel

7

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


1
这不是问题,而是不必要的,因为它告诉JVM将数据同步到全局内存,因此即使在多CPU上也可能运行不佳,因此没有人以并发方式使用同步块。
ReneS

6

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


5

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

BGGA闭包不受此困扰,因为没有this闭包(this引用外部类)。如果将非this对象用作锁,则可以解决此问题和其他问题。


3

使用全局对象(例如静态变量)进行锁定。

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


好吧,有时候,有时候没有。如果那会那么简单...
gimpf

假设线程完全有助于提高所给问题的性能,则只要有多个线程访问受锁保护的代码,线程就会降低性能。
kohlerm

3

不好意思?在出现之前java.util.concurrent,我经常遇到的最常见的问题是我所谓的“线程颠簸”:使用线程进行并发但产生太多线程并最终导致颠簸的应用程序。


您是否暗示由于java.util.concurrent可用而遇到更多问题?
Andrzej Doyle
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.