这个java代码的线程安全性有什么问题吗?线程1-10通过sample.add()添加数字,而线程11-20调用removeAndDouble()并将结果打印到stdout。我记得在我的脑海里有人说过,以同样的方式在removeAndDouble()中使用它来分配项目可能不是线程安全的。编译器可以优化指令,使它们不按顺序发生。这是这种情况吗?我的removeAndDouble()方法不安全吗?
使用此代码从并发角度看是否还有其他问题?我试图用java(1.6向上)更好地理解并发性和内存模型。
import java.util.*;
import java.util.concurrent.*;
public class Sample {
private final List<Integer> list = new ArrayList<Integer>();
public void add(Integer o) {
synchronized (list) {
list.add(o);
list.notify();
}
}
public void waitUntilEmpty() {
synchronized (list) {
while (!list.isEmpty()) {
try {
list.wait(10000);
} catch (InterruptedException ex) { }
}
}
}
public void waitUntilNotEmpty() {
synchronized (list) {
while (list.isEmpty()) {
try {
list.wait(10000);
} catch (InterruptedException ex) { }
}
}
}
public Integer removeAndDouble() {
// item declared outside synchronized block
Integer item;
synchronized (list) {
waitUntilNotEmpty();
item = list.remove(0);
}
// Would this ever be anything but that from list.remove(0)?
return Integer.valueOf(item.intValue() * 2);
}
public static void main(String[] args) {
final Sample sample = new Sample();
for (int i = 0; i < 10; i++) {
Thread t = new Thread() {
public void run() {
while (true) {
System.out.println(getName()+" Found: " + sample.removeAndDouble());
}
}
};
t.setName("Consumer-"+i);
t.setDaemon(true);
t.start();
}
final ExecutorService producers = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
final int j = i * 10000;
Thread t = new Thread() {
public void run() {
for (int c = 0; c < 1000; c++) {
sample.add(j + c);
}
}
};
t.setName("Producer-"+i);
t.setDaemon(false);
producers.execute(t);
}
producers.shutdown();
try {
producers.awaitTermination(600, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
sample.waitUntilEmpty();
System.out.println("Done.");
}
}
答案 0 :(得分:8)
它看起来对我来说是安全的。这是我的推理。
每次访问list
时,都会同步。这很棒。即使您在list
中提取了item
的一部分,但多个线程也无法访问item
。
只要您在同步时只访问list
,就应该很好(在您当前的设计中)。
答案 1 :(得分:6)
您的同步很好,不会导致任何无序执行问题。
但是,我注意到一些问题。
首先,如果您在waitUntilEmpty
list.notifyAll()
之后添加list.remove(0)
,则removeAndDouble
方法将更加及时。这将消除wait(10000)
中最多10秒的延迟。
其次,list.notify
中的add(Integer)
应该是notifyAll
,因为notify
只唤醒一个主题,并且可能会唤醒一个主题正在waitUntilEmpty
而不是waitUntilNotEmpty
内等待。
第三,以上都不是你的应用程序的活跃终结,因为你使用了有限的等待,但是如果你做了上面两个更改,你的应用程序将有更好的线程性能(waitUntilEmpty
)和有限的等待成为不必要的,可以变成普通的老式等待。
答案 2 :(得分:3)
您的代码实际上是线程安全的。这背后的原因是两部分。
首先是相互排斥。您的同步正确确保一次只有一个线程将修改集合。
第二个问题与您对编译器重新排序的关注有关。你担心编译实际上可以重新命令它不是线程安全的分配。在这种情况下你不必担心它。在列表上进行同步会创建一个先发生过的关系。在写入Integer item
之前,所有从列表中删除的内容都会发生。这告诉编译器它无法重新命令写入该方法中的项目。
答案 3 :(得分:1)
您的代码是线程安全的,但不是并发的(并行)。由于在单个互斥锁下访问所有内容,您将序列化所有访问权限,实际上对结构的访问是单线程的。
如果您需要生产代码中描述的功能,java.util.concurrent
包已经提供了BlockingQueue
具有(固定大小)数组和(可增长)链接列表的实现。至少研究实施思路非常有趣。