这是对Java中常见并发问题的一种调查.一个例子可能是经典的死锁或竞争条件,或者可能是Swing中的EDT线程错误.我对各种可能的问题既感兴趣,也对最常见的问题感兴趣.因此,请在每条评论中留下Java并发错误的一个特定答案,如果您看到自己遇到的错误,请立即投票.
当两个不同的开源库做到这样的事情时,我遇到了最痛苦的并发问题:
private static final String LOCK = "LOCK"; // use matching strings // in two different libraries public doSomestuff() { synchronized(LOCK) { this.work(); } }
乍一看,这看起来像一个非常简单的同步示例.然而; 因为字符串被拘禁在Java中,文字串"LOCK"
原来是同实例java.lang.String
(即使它们被声明彼此完全全异.)结果显然是不好的.
我见过的最常见的并发问题是没有意识到一个线程写的字段不能保证被不同的线程看到.这个的常见应用:
class MyThread extends Thread { private boolean stop = false; public void run() { while(!stop) { doSomeWork(); } } public void setStop() { this.stop = true; } }
只要停止不挥发或setStop
和run
不同步的,这是不能保证工作.这个错误特别恶劣,因为99.999%它在实践中无关紧要,因为读者线程最终会看到变化 - 但我们不知道他多久见到它.
一个经典问题是在同步时更改正在同步的对象:
synchronized(foo) { foo = ... }
然后,其他并发线程在不同对象上进行同步,并且此块不提供您期望的互斥.
一个常见的问题是使用来自多个线程的Calendar和SimpleDateFormat等类(通常通过在静态变量中缓存它们)而不进行同步.这些类不是线程安全的,因此多线程访问最终会导致状态不一致的奇怪问题.
双重锁定.总的来说.
我开始学习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内存模型搞砸的复杂所有的时间.
未正确同步返回的对象Collections.synchronizedXXX()
,尤其是在迭代或多个操作期间:
Mapmap = 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");
虽然可能不是您要求的,但我遇到的最常见的并发相关问题(可能是因为它出现在普通的单线程代码中)是
java.util.ConcurrentModificationException
由以下因素引起:
Listlist = new ArrayList (Arrays.asList("a", "b", "c")); for (String string : list) { list.remove(string); }
可以很容易地认为同步集合可以为您提供比实际更多的保护,并且忘记在调用之间保持锁定.我已经看过几次这个错误了:
Listl = Collections.synchronizedList(new ArrayList ()); String[] s = l.toArray(new String[l.size()]);
例如,在上面的第二行中,toArray()
和size()
方法本身都是线程安全的,但是与size()
它分开评估toArray()
,并且在这两个调用之间不保持List上的锁定.
如果您使用另一个线程同时从列表中删除项目来运行此代码,迟早会String[]
返回一个新的返回值,该值大于保存列表中所有元素所需的值,并且尾部具有空值.很容易想到,因为对List的两个方法调用发生在一行代码中,这在某种程度上是一个原子操作,但事实并非如此.
我们看到的最常见的错误是程序员在EDT上执行长时间的操作,比如服务器调用,将GUI锁定几秒钟并使应用程序无响应.
在循环中忘记wait()(或Condition.await()),检查等待条件是否为真.如果没有这个,你会遇到虚假的wait()唤醒错误.规范用法应该是:
synchronized (obj) { while () { obj.wait(); } // do stuff based on condition being true }
另一个常见错误是异常处理不当.当后台线程抛出异常时,如果您没有正确处理它,您可能根本看不到堆栈跟踪.或者,您的后台任务可能会停止运行,并且永远不会再次启动,因为您无法处理异常.
直到我把一类布赖恩戈茨我没有意识到的是,非同步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; } }
认为你正在编写单线程代码,但使用可变静态(包括单例).显然它们将在线程之间共享.这经常出人意料地发生.
不应在同步块内进行任意方法调用.
Dave Ray在他的第一个答案中触及了这一点,事实上我也遇到了一个死锁,也与在一个synchronized方法中调用侦听器上的方法有关.我认为更普遍的教训是方法调用不应该从同步块中"进入狂野" - 你不知道调用是否会长时间运行,导致死锁或其他什么.
在这种情况下,通常一般来说,解决方案是减少同步块的范围,以保护关键的私有代码段.
此外,由于我们现在正在访问同步块之外的侦听器集合,因此我们将其更改为写入时复制集合.或者我们可以简单地制作一个防御性的收藏品.关键是,通常有替代方法可以安全地访问未知对象的集合.
我遇到的最新的与并发相关的错误是一个对象,它在其构造函数中创建了一个ExecutorService,但是当该对象不再被引用时,它从未关闭ExecutorService.因此,在几周的时间内,数千个线程泄露,最终导致系统崩溃.(从技术上讲,它没有崩溃,但它确实停止正常运行,同时继续运行.)
从技术上讲,我认为这不是一个并发问题,但它是一个与使用java.util.concurrency库有关的问题.
不平衡的同步,特别是针对地图似乎是一个相当普遍的问题.很多人认为将put放到Map(不是ConcurrentMap,而是说HashMap)而不是同步get就足够了.然而,这可能在重新哈希期间导致无限循环.
但是,在具有读写共享状态的任何位置都可能出现同样的问题(部分同步).
当存在可由每个请求设置的可变字段时,我遇到了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 ... } }
不完全是一个错误但是,最糟糕的罪恶是提供一个你打算让别人使用的库,但不说明哪些类/方法是线程安全的,哪些只能从单个线程调用等等.
更多人应该使用Goetz的书中描述的并发注释(例如@ThreadSafe,@ GuardBy等).
我最大的问题一直是死锁,特别是由持有锁的解雇者造成的.在这些情况下,在两个线程之间进行反向锁定非常容易.就我而言,在一个线程中运行的模拟和在UI线程中运行的模拟的可视化之间.
编辑:将第二部分移动到单独的答案.
在类的构造函数中启动一个线程是有问题的.如果扩展了类,则可以在执行子类的构造函数之前启动该线程.
共享数据结构中的可变类
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");
当发生这种情况时,代码远比这个简化示例复杂得多.复制,查找和修复错误很难.如果我们可以将某些类标记为不可变且某些数据结构仅保存不可变对象,则可能可以避免它.
我相信Java的主要问题是构造函数的(缺乏)可见性保证.例如,如果您创建以下类
class MyClass { public int a = 1; }
然后从另一个线程中读取MyClass的属性a,MyClass.a可以是0或1,具体取决于JavaVM的实现和情绪.今天'a'成为1的可能性非常高.但是在未来的NUMA机器上,这可能会有所不同.很多人都没有意识到这一点,并且认为在初始化阶段他们不需要关心多线程.
对字符串文字或由字符串文字定义的常量进行同步(可能)是一个问题,因为字符串文字是实例化的,并且将由JVM中的任何其他人使用相同的字符串文字共享.我知道应用服务器和其他"容器"场景中出现了这个问题.
例:
private static final String SOMETHING = "foo"; synchronized(SOMETHING) { // }
在这种情况下,使用字符串"foo"锁定的任何人都共享相同的锁.
我经常犯的最蠢的错误是在对象上调用notify()或wait()之前忘记同步.
使用本地"new Object()"作为互斥锁.
synchronized (new Object()) { System.out.println("sdfs"); }
这没用.
另一个常见的"并发"问题是在根本不需要时使用同步代码.例如,我仍然看到程序员使用StringBuffer
甚至java.util.Vector
(作为方法局部变量).
多个对象受锁保护但通常连续访问.我们遇到过几种情况,其中锁是由不同的代码以不同的顺序获得的,导致死锁.
没有意识到this
内部阶级不this
属于外部阶级.通常在实现的匿名内部类中Runnable
.根本问题是因为同步是所有Object
s的一部分,实际上没有静态类型检查.我在usenet上至少看过两次,它也出现在Brian Goetz'z Java Concurrency in Practice中.
BGGA闭包不会受此影响,因为闭包没有this
(this
参考外类).如果你使用非this
对象作为锁,那么它可以解决这个问题和其他问题.