Java - 我是否需要将共享的监听器成员变量声明为volatile?

5

我有一个简单的类,它在自己的线程中进行一些计算,并将结果报告给监听器。

class Calculator extends Thread {
    protected Listener listener;

    public void setListener(Listener l) {
        listener = l;
    }

    public void run() {
        while (running) {
            ... do something ...

            Listener l = listener;

            if (l != null) {
                l.onEvent(...);
            }
        }
    }
}

任何时候,用户都可以调用 setListener(null) 来暂停一段时间内的任何事件。因此,在 run() 函数中,我创建了一个监听器的副本,这样就不会遇到空指针异常,即使在 != null 条件检查成功后将监听器设置为 null。在我的情况下,我认为这是同步的正确替代方案。
我的问题是:我应该将此处的监听器成员变量声明为 volatile 吗?我已经阅读了很多关于 volatile 的文章,但所有的例子似乎都针对基本数据类型(布尔型、整数等),而不是对象。因此,我不确定对象是否也应该被声明为 volatile。我认为我必须将其声明为 volatile,以便线程始终具有成员变量的最新版本,但我不确定。
谢谢!

仅仅是一个建议——我承认并没有回答你的问题——但是,将一个“启用”类型的标志放在你的监听器上会更好吗?然后,客户端不再设置监听器为null或非null,而是设置setEnabled(true)或setEnabled(false)。这可以避免处理意外的NPE问题,并且还可以防止您不断地实例化新的监听器对象。 - Jim Kiley
@JimKiley 我同意你的观点。不过我在简化我的问题,所以看起来有些奇怪。对此请接受我的道歉。 - Japer D.
@Крысa 为什么?你能解释一下吗? - Japer D.
@JaperD.,与其允许另一个对象将另一个类的成员变量设置为“null”,您应该提供一个可查询的状态(即isEventAvailable())。如果评估为true,则执行onEvent,否则不执行任何操作。此外,如果您想要停止该线程,您必须将running声明为volatile - mre
1
@Крысa 我不同意。在某个时候没有监听器并没有什么问题,我认为这也是默认和初始情况。 - Japer D.
显示剩余3条评论
2个回答

5

是的。为了确保Calculator线程能看到另一个线程设置的新值,您必须将变量设置为volatile。

然而,volatile是一种相当低级的机制,在客户端代码中很少使用。我建议在这种情况下考虑使用java.util.concurrent.AtomicReference,它可以确保这些事情按预期工作。


好的,感谢确认。我会研究一下AtomicReference,我必须承认我忽略了新的并发包太久了。 - Japer D.
呵呵..我也是,但这是因为我尽可能地避免并发 :-) - aioobe
即使使用原子引用,事情仍可能按照某种顺序执行,以至于侦听器在注销后仍会收到事件调用。请参阅我的更新答案。 - Ted Hopp
1
我不是专家,但我没有看到在这个应用程序中使用AtomicReference(而不是volatile)的任何优势。我错过了什么?它在这个应用程序中使得什么“按预期工作”,而在其他情况下却不能“按预期工作”? - Ed Staub
@EdStaub - 我同意。在这里,我看不到使用AtomicReference比使用volatile有任何好处。 - Ted Hopp

3
如果您使用此方法,则无法保证侦听器在 setListener(null)返回后不再收到事件通知。 执行可能如下所示:
Listener l = listener; // listener != null at this point

// setListener(null) executes here

if (l != null) {
    l.onEvent(...);
}

如果你需要保证在listener被取消注册后不会再有事件被发送到它,那么你需要使用同步块。声明listenervolatile是没有帮助的。代码应该改为:

public synchronized void setListener(Listener l) {
    listener = l;
}

public void run() {
    while (running) {
        ... do something ...

        synchronized (this) {
            if (listener != null) {
                listener.onEvent(...);
            }
        }
    }
}

如果您想避免每次都使用synchronized的开销,可以尝试以下方法:

if (listener != null) {
    synchronized (this) {
        if (listener != null) {
            listener.onEvent(...);
        }
    }
}

这样做有一定风险,因为在设置非空监听器后,您可能会错过事件。将listener声明为volatile可能会修复该问题。

@TedHopp,不会,因为他将内容复制到本地变量中。 - aioobe
谢谢您的更新。现在我理解了您的顾虑。然而,在我的情况下,监听器注销后仍然接收事件(一段时间),这不是一个问题。但是,在一般情况下,这可能是一个问题。很好,您为了清晰度添加了这个说明。 - Japer D.
@JaperD. - 双重检查锁定问题与构建单例有关。在这里,问题仅涉及设置或取消对已完全构建的侦听器的引用,这些问题不会出现。 - Ted Hopp
1
监听器在取消设置后可能被解构。自JDK5起,如果监听器是易失性的,则双重检查已修复。 - Ed Staub
1
+1 volatile通常会引入一种错误的安全感,而不解决问题。然而,在这里volatile有一些用途,即便它可能无法保证线程安全,但监听器也不会被不安全地发布。 - Tom Hawtin - tackline
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接