出于学习目的,我试图实现一个线程安全的队列数据结构+消费者/生产者链,出于学习目的,我还没有使用通知/等待机制:
SyncQueue:
package syncpc;
/**
* Created by Administrator on 01/07/2009.
*/
public class SyncQueue {
private int val = 0;
private boolean set = false;
boolean isSet() {
return set;
}
synchronized public void enqueue(int val) {
this.val = val;
set = true;
}
synchronized public int dequeue() {
set = false;
return val;
}
}
消费者:
package syncpc;
/**
* Created by Administrator on 01/07/2009.
*/
public class Consumer implements Runnable {
SyncQueue queue;
public Consumer(SyncQueue queue, String name) {
this.queue = queue;
new Thread(this, name).start();
}
public void run() {
while(true) {
if(queue.isSet()) {
System.out.println(queue.dequeue());
}
}
}
}
制片人:
package syncpc;
import java.util.Random;
/**
* Created by Administrator on 01/07/2009.
*/
public class Producer implements Runnable {
SyncQueue queue;
public Producer(SyncQueue queue, String name) {
this.queue = queue;
new Thread(this, name).start();
}
public void run() {
Random r = new Random();
while(true) {
if(!queue.isSet()) {
queue.enqueue(r.nextInt() % 100);
}
}
}
}
主要:
import syncpcwn.*;
/**
* Created by Administrator on 27/07/2015.
*/
public class Program {
public static void main(String[] args) {
SyncQueue queue = new SyncQueue();
new Producer(queue, "PROCUDER");
new Consumer(queue, "CONSUMER");
}
}
这里的问题是,如果 isSet 方法未同步,我得到了这样的输出:
97,
55
并且程序只是继续运行而不输出任何值。如果 isSet 方法已同步,则程序正常工作。
我不明白为什么,没有死锁, isSet 方法只是查询 set 实例变量而不设置它,所以没有竞争条件。
答案 0 :(得分:4)
set
需要volatile
:
private boolean volatile set = false;
这可确保所有读者在写入完成时看到更新值。否则他们最终会看到缓存的值。这在this关于并发的文章中有更详细的讨论,并且还提供了使用volatile
的不同模式的示例。
现在您的代码与synchronized
一起使用的原因最好用一个例子来解释。 synchronized
方法可以写成如下(即,它们等同于以下表示):
public class SyncQueue {
private int val = 0;
private boolean set = false;
boolean isSet() {
synchronized(this) {
return set;
}
}
public void enqueue(int val) {
synchronized(this) {
this.val = val;
set = true;
}
}
public int dequeue() {
synchronized(this) {
set = false;
return val;
}
}
}
这里,实例是本身用作锁。这意味着只有线程可以保存该锁。这意味着任何线程总是获取更新的值,因为只有一个线程可以写入值,并且想要读取set
的线程赢了能够执行isSet
直到其他线程释放锁定this
,此时set
的值将会更新。
如果你想要正确理解Java中的并发性,你应该真正阅读Java: Concurrency In Practice(我认为在某个地方也有一个免费的PDF版本)。我还在阅读这本书,因为还有许多我不理解或错误的事情。
如matt forsythe所述,当您有多个消费者时,您会遇到问题。这是因为他们都可以检查isSet()
并发现有一个值出队,这意味着他们都会尝试将相同的值出列。它归结为这样一个事实,即真正想要的是“检查并且如果设置已经出列”操作是有效的原子,但它不是你编码它的方式。这是因为最初调用isSet
的同一个线程可能不一定是调用dequeue
的相同线程。因此整个操作不是原子操作,这意味着您必须同步整个操作。
答案 1 :(得分:3)
你遇到的问题是可见性(或者说缺乏可见性)。
没有任何相反的指令,JVM将假定分配给一个线程中的变量的值不需要对其他线程可见。它有时可以在以后(当它方便的时候)可见,或者可能永远不可见。管理什么应该可见的规则以及何时由Java内存模型定义,并且它们总结为here。 (起初他们可能有点干和可怕,但理解它们绝对是至关重要的。)
因此,即使生产者将set
设置为true
,消费者也会继续将其视为错误。你怎么能发布新的价值?
volatile
。这适用于像boolean
这样的原始值,引用时你必须要小心一些。synchronized
不仅提供互斥,还保证任何进入使用同一对象的synchronized
块的人都可以看到其中设置的任何值。 (这就是为什么一切都有效,如果你声明isSet()
方法synchronized
。)Atomic*
java.util.concurrent
类
醇>
在您的情况下,volatile
可能是最佳解决方案,因为您只更新boolean
,因此默认情况下保证更新的原子性。
正如@matt forsythe所指出的那样,您的代码也存在TOCTTOU问题,因为您的帖子可能被isSet()
和enqueue()/dequeue()
之间的其他人中断。
答案 2 :(得分:0)
我认为当我们陷入线程问题时,第一步是确保两个线程都运行良好。 (我知道他们会因为没有锁来创建死锁)
为此你也可以在enqueue函数中添加一个printf语句。这将确保排队和出队线程运行良好。
然后第二步应该是“set”是共享资源,因此值足够切换以便代码可以以所需的方式运行。
我认为如果你可以推理并充分记录日志,你就可以意识到问题所在。