这是有关Java常见并发问题的各种民意测验。一个例子可能是经典的死锁或竞争条件,或者是Swing中的EDT线程错误。我不仅对可能出现的问题的范围感兴趣,而且对最常见的问题感兴趣。因此,请在每个评论中留下一个Java并发错误的特定答案,如果看到遇到的评论,请投票。
这是有关Java常见并发问题的各种民意测验。一个例子可能是经典的死锁或竞争条件,或者是Swing中的EDT线程错误。我不仅对可能出现的问题的范围感兴趣,而且对最常见的问题感兴趣。因此,请在每个评论中留下一个Java并发错误的特定答案,如果看到遇到的评论,请投票。
Answers:
我所见过的最常见的并发问题是没有意识到不能保证一个线程写入的字段可以被另一个线程看到。常见的应用:
class MyThread extends Thread {
private boolean stop = false;
public void run() {
while(!stop) {
doSomeWork();
}
}
public void setStop() {
this.stop = true;
}
}
只要停止不挥发或setStop
和run
不同步的,这是不能保证的工作。这个错误特别令人讨厌,因为在99.999%的实践中这无关紧要,因为读者线程最终会看到更改-但是我们不知道他多久才能看到更改。
我的#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
(即使它们彼此完全不同地声明。)结果显然很糟糕。
一个经典的问题是在同步对象的同时进行更改:
synchronized(foo) {
foo = ...
}
然后,其他并发线程正在另一个对象上同步,并且此块不提供您期望的互斥。
双重检查锁定。总的来说。
我开始学习在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内存模型搞砸的复杂所有的时间。
在上返回的对象未正确同步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");
尽管可能不是您所要的,但我遇到的最常见的与并发相关的问题(可能是因为它是在普通的单线程代码中出现的)
java.util.ConcurrentModificationException
由以下原因引起:
List<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));
for (String string : list) { list.remove(string); }
可能很容易想到同步集合比实际授予的保护更多,而忘记了两次调用之间的锁定。我已经多次看到此错误:
List<String> l = Collections.synchronizedList(new ArrayList<String>());
String[] s = l.toArray(new String[l.size()]);
例如,在上面的第二行中,toArray()
和size()
方法本身都是线程安全的,但与和size()
分开评估toArray()
,并且在这两个调用之间不持有List的锁。
如果在另一个线程同时从列表中删除项目的情况下运行此代码,则迟早会得到一个新的String[]
返回值,该值大于保存列表中所有元素所需的值,并且尾部具有空值。容易想到,因为对List的两个方法调用发生在一行代码中,所以这在某种程度上是原子操作,但事实并非如此。
另一个常见的错误是不良的异常处理。当后台线程引发异常时,如果处理不当,则可能根本看不到堆栈跟踪。或您的后台任务停止运行,再也无法启动,因为您无法处理该异常。
直到我和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;
}
}
认为您正在编写单线程代码,但是使用可变的静态变量(包括单例)。显然,它们将在线程之间共享。这经常出乎意料地发生。
不平衡的同步,尤其是针对Maps的同步,似乎是一个相当普遍的问题。许多人认为,对地图的put进行同步(不是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 ...
}
}
共享数据结构中的可变类
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");
发生这种情况时,代码比此简化示例要复杂得多。复制,查找和修复该错误很难。如果我们可以将某些类标记为不可变,将某些数据结构标记为仅包含不可变对象,则可以避免。
在字符串文字或由字符串文字定义的常量上进行同步(潜在地)是一个问题,因为该字符串文字是内部的,并且将由JVM中的其他任何人使用相同的字符串文字共享。我知道在应用程序服务器和其他“容器”方案中会出现此问题。
例:
private static final String SOMETHING = "foo";
synchronized(SOMETHING) {
//
}
在这种情况下,使用字符串“ foo”进行锁定的任何人都将共享同一锁定。
我相信将来Java的主要问题将是(缺乏)构造函数的可见性保证。例如,如果您创建以下类
class MyClass {
public int a = 1;
}
然后只需从另一个线程读取MyClass的属性a,MyClass.a可以为0或1,具体取决于JavaVM的实现和心情。今天,“ a”为1的机会非常高。但是在将来的NUMA机器上,这可能会有所不同。许多人没有意识到这一点,并认为他们在初始化阶段不需要关心多线程。
我经常犯的最愚蠢的错误是忘记在对象上调用notify()或wait()之前进行同步。
使用本地“ new Object()”作为互斥量。
synchronized (new Object())
{
System.out.println("sdfs");
}
这没用。
不好意思?在出现之前java.util.concurrent
,我经常遇到的最常见的问题是我所谓的“线程颠簸”:使用线程进行并发但产生太多线程并最终导致颠簸的应用程序。