当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《您在Java中遇到的最常见的并发问题是什么?》经验,为你挑选了28个好方法。

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



1> Jared..:

两个不同的开源库做到这样的事情时,我遇到了最痛苦的并发问题:

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

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

乍一看,这看起来像一个非常简单的同步示例.然而; 因为字符串被拘禁在Java中,文字串"LOCK"原来是同实例java.lang.String(即使它们被声明彼此完全全异.)结果显然是不好的.


这是我更喜欢私有静态最终Object LOCK = new Object()的原因之一;
我喜欢它 - 哦,这很讨厌:)
实际上......它真的让我希望编译器拒绝允许你在String上进行同步.给定字符串实习,没有任何情况下这将是"好事(tm)".
对于Java Puzzlers 2来说这是一个不错的选择.
@Jared:"直到字符串被实习"是没有意义的.字符串不会神奇地"成为"实习.String.intern()返回一个不同的对象,除非您已经拥有指定String的规范实例.此外,所有文字字符串和字符串值常量表达式都是实体.总是.请参阅JLS的String.intern()和§3.10.5的文档.
"但是;因为字符串是用Java实现的"应该真的是"但是;因为字符串文字是用Java实现的".(并非所有字符串都被实习,只是文字)当然,"LOCK"是实习的,这是这个问题的根源.
我见过简单的`synchronized("LOCK"){this.work(); `:P

2> Kutzi..:

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

class MyThread extends Thread {
  private boolean stop = false;

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

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

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


它比"几分钟"更糟糕 - 你可能永远不会看到它.在内存模型下,允许JVM优化while(!stop)into while(true)然后你就被软化了.这可能只发生在某些VM上,仅在服务器模式下,只有当JVM在循环的x次迭代后重新编译时才会发生等等.哎!
一个很好的解决方案是使stop实例变量成为AtomicBoolean.它解决了非易失性的所有问题,同时屏蔽了JMM问题.
@Thomas:那是因为Java内存模型.如果你想详细了解它,你应该阅读它(Brian Goetz的Java Concurrency in Practice解释得很好).简而言之:除非你使用内存同步关键字/结构(如volatile,synchronized,AtomicXyz,但是当一个Thread完成时),一个线程无法保证看到对不同线程完成的任何字段所做的更改
为什么要在易失性布尔值上使用AtomicBoolean?我正在为版本1.4+开发,所以是否有任何陷阱只是声明volatile?
尼克,我认为这是因为原子CAS通常比挥发性更快.如果你正在为1.4开发你唯一的安全选项IMHO是使用synchronized,因为1.4中的volatile没有得到像Java 5中那样强大的内存屏障保证.

3> Alex Miller..:

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

synchronized(foo) {
  foo = ...
}

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


对此进行IDEA检查称为"非最终字段上的同步不太可能具有有用的语义".非常好.
哈...现在这是一个折磨的描述."不太可能有有用的语义"可以更好地被描述为"最有可能被打破".:)

4> Alex Miller..:

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



5> Kirk Wylie..:

双重锁定.总的来说.

我开始学习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内存模型搞砸的复杂所有的时间.


枚举单例模式解决了所有这些问题(参见Josh Bloch对此的评论).Java程序员应该更广泛地了解它的存在.
这是我用于Singleton类的Lazy初始化.也不需要同步,因为这是由java隐式保证的.class Foo {static class Holder {static Foo foo = new Foo(); } static Foo getInstance(){return Holder.foo; }}

6> Dave Ray..:

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

Map map = Collections.synchronizedMap(new HashMap());

...

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


或者更好的是,使用ConcurrentHashMap和putIfAbsent.

7> Fabian Steeg..:

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

java.util.ConcurrentModificationException

由以下因素引起:

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



8> 小智..:

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

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

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

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


好例子.我认为我会更普遍地将其描述为"原子操作的组合不是原子的".(有关另一个简单示例,请参阅volatile field ++)

9> Eric Burke..:

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


EDT是什么意思?
EDT =事件派遣线程

10> Alex Miller..:

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

 synchronized (obj) {
     while () {
         obj.wait();
     }
     // do stuff based on condition being true
 }



11> Eric Burke..:

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


您是否可以发布更详细解释此内容的任何文章或参考文献的链接?

12> John Russell..:

直到我把一类布赖恩戈茨我没有意识到的是,非同步getter通过同步突变的私人领域setter从来没有保证返回更新后的值.只有当变量在读取和写入被synchronized块保护时,才能保证变量的最新值.

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


在后来的JVM(1.5和前进,我认为)中,使用volatile也会解决这个问题.
不必要.volatile为您提供最新值,因此它可以防止永久返回1,但它不提供锁定.它很接近,但并不完全相同.

13> Tom Hawtin -..:

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


确实是的!可变静态变量破坏线程限制。出乎意料的是,无论是在JCiP还是CPJ中,我从未发现有关此陷阱的任何信息。

14> Scott Bale..:

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

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

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

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



15> Eddie..:

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

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



16> Alex Miller..:

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

但是,在具有读写共享状态的任何位置都可能出现同样的问题(部分同步).



17> Ludwig Wensa..:

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



18> 小智..:

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

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



19> Dave Ray..:

我最大的问题一直是死锁,特别是由持有锁的解雇者造成的.在这些情况下,在两个线程之间进行反向锁定非常容易.就我而言,在一个线程中运行的模拟和在UI线程中运行的模拟的可视化之间.

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



20> grayger..:

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



21> Steve McLeod..:

共享数据结构中的可变类

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

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



22> Tim Jansen..:

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

class MyClass {
    public int a = 1;
}

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


MyClass.a表示静态访问,'a'不是MyClass的静态成员.除此之外,它就像'ReneS'所述,如果对未完成对象的引用被泄露,这只是一个问题,例如,将"this"添加到构造函数中的某个外部映射.
如果在构造函数返回/完成之前已经使用了新创建的实例的引用,那么这只是一个问题.例如,类在构造期间在公共池中注册自己,而其他线程开始访问它.

23> Alex Miller..:

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

例:

private static final String SOMETHING = "foo";

synchronized(SOMETHING) {
   //
}

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



24> Dave Ray..:

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


与大多数并发问题不同,这个不容易找到吗?至少你在这里得到一个IllegalMonitorStateException ......

25> Ludwig Wensa..:

使用本地"new Object()"作为互斥锁.

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

这没用.


这没用.这是没有锁的记忆障碍.
这可能是*无用的,但同步的行为完全做了一些有趣的事情......当然每次创建一个新的对象都是完全浪费.

26> Kutzi..:

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



27> Brendan Cash..:

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



28> Tom Hawtin -..:

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

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

推荐阅读
郑谊099_448
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有