我一直在阅读Doug Lea的“Java并发编程”一书。您可能知道,Doug最初编写了Java Concurrency API。然而,某些事情让我有些困惑,我希望能就这个小难题获得一些我的看法!
从Doug Lea的排队示例中获取以下代码......
class LinkedQueue {
protected Node head = new Node(null);
protected Node last = head;
protected final Object pollLock = new Object();
protected final Object putLock = new Object();
public void put(Object x) {
Node node = new Node(x);
synchronized (putLock) { // insert at end of list
synchronized (last) {
last.next = node; // extend list
last = node;
}
}
}
public Object poll() { // returns null if empty
synchronized (pollLock) {
synchronized (head) {
Object x = null;
Node first = head.next; // get to first real node
if (first != null) {
x = first.object;
first.object = null; // forget old object
head = first; // first becomes new head
}
return x;
}
}
}
static class Node { // local node class for queue
Object object;
Node next = null;
Node(Object x) { object = x; }
}
}
这是一个非常好的队列。它使用两个监视器,因此Producer和Consumer可以同时访问Queue。太好了!但是,'last'和'head'的同步让我感到困惑。该书指出,对于Queue当前或即将有0个条目的情况,这是必需的。好吧,公平,这种做法很有道理。
然而,我查看了Java Concurrency LinkedBlockingQueue。队列的original版本不会在头部或尾部同步(我也想发布另一个链接到现代版本,这个版本也遇到同样的问题,但我不能这样做因为我是新手)。我想知道为什么不呢?我在这里错过了什么吗?是否缺少Java内存模型特殊性质的某些部分?为了可见性,我会考虑需要这种同步吗?我很欣赏一些专家意见!
答案 0 :(得分:2)
这里的细微之处在于synchronized(null)会抛出NullPointerException,因此head和last都不允许变为null。它们都被初始化为永远不会从任何列表中返回或删除的同一虚拟节点的值。
put()和poll()在两个不同的锁上同步。如果这些方法可以从不同的线程修改相同的值,那么这些方法需要在同一个锁上同步,以便相互之间是线程安全的。这是一个问题的唯一情况是当head == last时(即它们是同一个对象,通过不同的成员变量引用)。这就是代码在头部和最后同步的原因 - 大部分时间这些都是快速,无条件的锁定,但偶尔头部和最后一个将是同一个实例,其中一个线程将不得不阻塞另一个。
唯一一次可见性是一个问题是当队列几乎为空时,其余的时间put()和poll()在队列的不同端工作,并且不会相互干扰。
答案 1 :(得分:2)
在你为最新JRE中提供链接以及版本的版本中,Node类中的项是volatile,它强制读取和写入对所有其他线程可见,这里有一个更深入的解释{ {3}}