多线程:对象在使用时设置为 null

Multi-threading: Objects being set to null while using them

提问人: 提问时间:12/14/2008 更新时间:1/10/2009 访问量:3300

问:

我有一个带有渲染线程的小应用程序。这个线程所做的只是在它们的当前位置绘制我的对象。

我有一些代码,例如:

public void render()
{
    // ... rendering various objects

    if (mouseBall != null) mouseBall.draw()

}

然后,我还有一些鼠标处理程序,当用户单击鼠标时,它会创建mouseBall并将其设置为一个新球。然后,用户可以拖动鼠标,球将跟随鼠标所到之处。当用户释放球时,我有另一个鼠标事件,该事件设置 mouseBall = null。

问题是,我的渲染循环运行得足够快,以至于在随机时间条件 (mouseBall != null) 将返回 true,但是在该点之后的那一瞬间,用户将松开鼠标,并且我会得到一个 nullpointer 异常尝试 .draw() 在 null 对象上。

解决此类问题的方法是什么?

Java 多线程 NullPointerException

评论


答:

13赞 Greg Hewgill 12/14/2008 #1

问题在于您访问了两次,一次是检查它是否不是,另一次是调用函数。您可以通过使用临时方法避免此问题,如下所示:mouseBallnull

public void render()
{
    // ... rendering various objects
    tmpBall = mouseBall;
    if (tmpBall != null) tmpBall.draw();
}

评论

0赞 Dave L. 12/14/2008
完全。只要确保mouseBall是易失性的,如果你打算在没有同步块的情况下这样做。
9赞 Nathaniel Flath 12/14/2008 #2

您必须同步 if 和 draw 语句,以便保证它们作为一个原子序列运行。在 java 中,这将像这样完成:

    
public void render()
{
    // ... rendering various objects
    synchronized(this) {
        if (mouseBall != null) mouseBall .draw();
   }
}

评论

0赞 Greg Hewgill 12/15/2008
这最终会在 mouseBall.draw() 方法调用的整个持续时间内保持对“this”的锁定,这可能比保持锁定所需的时间更长。此外,只有当对 mouseBall 的所有其他访问(读取和写入)也被同步的 (this) { ... } 块包围时,这才是原子的。
2赞 Andrzej Doyle 1/10/2009 #3

我知道您已经接受了其他答案,但第三种选择是使用 java.util.concurrent.atomic 包的 AtomicReference 类。这提供了检索、更新和比较操作,这些操作以原子方式操作,而无需任何支持代码。所以在您的示例中:

public void render()
{
    AtomicReference<MouseBallClass> mouseBall = ...;

    // ... rendering various objects
    MouseBall tmpBall = mouseBall.get();
    if (tmpBall != null) tmpBall.draw();
}

这看起来与 Greg 的解决方案非常相似,从概念上讲,它们相似之处在于,它们在幕后都使用波动性来确保值的新鲜度,并在使用值之前获取临时副本以应用条件。

因此,这里使用的确切示例并不能很好地展示 AtomicReferences 的强大功能。相反,请考虑您的另一个线程仅在鼠标球 vairable 已经为 null 时才会更新 - 对于各种初始化风格的代码块来说,这是一个有用的习惯用语。在这种情况下,通常必须使用同步,以确保如果您检查并发现球为空,则在尝试设置它时它仍然是空的(否则您将回到原始问题的领域)。但是,使用 AtomicReference,您可以简单地说:

mouseBall.compareAndSet(null, possibleNewBall);

因为这是一个原子操作,所以如果一个线程“看到”该值为 null,它也会在任何其他线程有机会读取它之前将其设置为可能的 NewBall 引用。

原子引用的另一个不错的成语是,如果您无条件地设置某些内容,但需要使用旧值执行某种清理。在这种情况下,你可以说:

   MouseBall oldBall = mouseBall.getAndSet(newMouseBall);
   // Cleanup code using oldBall

AtomicIntegers 具有这些优点以及更多优点;getAndIncrement() 方法非常适合全局共享的计数器,因为您可以保证每次调用它都会返回一个不同的值,而不管线程的交错。丝线安全,最少大惊小怪。