我有一个Output类,它只打印要打印的所有内容。
public class Output {
private static List<String> textList = new ArrayList<>();
private static Output output = null;
private Output() {
Runnable task = () -> {
int lastIndex = 0;
while (true) {
while (lastIndex < textList.size()) {
System.out.println(lastIndex + " - " + textList.size() + ": " + textList.get(lastIndex));
outputText(textList.get(lastIndex));
lastIndex ++;
}
}
};
new Thread(task).start();
}
private static void outputText(String text) {
synchronized (System.out) {
System.out.println(text);
}
}
public static void say(String text) {
if (output == null) {
output = new Output();
}
textList.add(text);
}
}
当我添加要打印的内容时,一切正常:
for (int i = 0; i < 10; i++) {
Output.say("" + i);
}
但是当我在循环中添加Thread.sleep
时,它会在第一个输出上停止:
for (int i = 0; i < 10; i++) {
Output.say("" + i);
Thread.sleep(100);
}
我该如何预防呢?我的意思是,我只是在主线程而不是单独的线程中停止睡眠。
答案 0 :(得分:2)
如果没有正确同步线程,则不保证线程会看到其他线程所做的更新。它们可能完全错过更新或仅查看其中的一部分,从而产生完全不一致的结果。有时他们甚至可能出现来做正确的事情。如果没有适当的同步(在指定为线程安全的任何有效构造的意义上),这是完全不可预测的。
有时,看到特定行为的可能性更高,就像在您的示例中一样。在大多数运行中,没有sleep
的循环将在另一个线程开始工作之前完成,而插入sleep
会增加在第二个线程看到值之后丢失更新的机会。一旦第二个线程看到textList.size()
的值,它就可以永久重用该值,将lastIndex < textList.size()
评估为false
并执行等效的while(true) { }
。
有趣的是,您为线程安全插入构造的唯一位置是仅由单个线程调用的方法outputText
(并且在大多数环境中打印到System.out
在内部同步)。
此外,不清楚为什么要创建一个Output
类型的对象,这里没有相关性,因为所有字段和方法都是static
。
您的代码可以更正并简化为
public static void main(String[] args) throws InterruptedException {
List<String> textList = new ArrayList<>();
new Thread( () -> {
int index=0;
while(true) synchronized(textList) {
for(; index<textList.size(); index++)
System.out.println(textList.get(index));
}
}).start();
for (int i = 0; i < 10; i++) {
synchronized(textList) {
textList.add(""+i);
}
Thread.sleep(100);
}
}
虽然它仍然包含由于无限的第二个线程而无法终止的原始代码的问题,并且还使用轮询循环来烧录CPU。您应该让第二个线程等待以获取新项目并添加终止条件:
public static void main(String[] args) throws InterruptedException {
List<String> textList = new ArrayList<>();
new Thread( () -> {
synchronized(textList) {
for(int index=0; ; index++) {
while(index>=textList.size()) try {
textList.wait();
} catch(InterruptedException ex) { return; }
final String item = textList.get(index);
if(item==null) break;
System.out.println(item);
}
}
}).start();
for (int i = 0; i < 10; i++) {
synchronized(textList) {
textList.add(""+i);
textList.notify();
}
Thread.sleep(100);
}
synchronized(textList) {
textList.add(null);
textList.notify();
}
}
这仍然只是一个你不应该在现实代码中使用的学术范例。 Java API提供了用于线程安全数据交换的类,消除了自己实现这些事情的负担。
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
String endMarker = "END-OF-QUEUE"; // the queue does not allow null
new Thread( () -> {
for(;;) try {
String item = queue.take();
if(item == endMarker) break;// don't use == for ordinary strings
System.out.println(item);
} catch(InterruptedException ex) { return; }
}).start();
for (int i = 0; i < 10; i++) {
queue.put(""+i);
Thread.sleep(100);
}
queue.put(endMarker);
}