【问题标题】:Will the use of a final parameter preclude a class from being thread safe?使用 final 参数会阻止类成为线程安全的吗?
【发布时间】:2011-05-26 22:38:23
【问题描述】:

我的测试表明以下代码不是线程安全的,即使该类是无状态的并且方法之间共享的所有状态都作为参数从方法传递到方法。多个线程正在调用以下类的单个实例。

public class ThingFinder {

    Token findFoo(TokenIterator<? extends Token> iterator, 
                  Token start, final Token limit) {

        Token partOfFoo = searchForward(iterator, start, new TokenSearcher() {
            int maxTokens = 5;

            @Override
            public SearchAction assessToken(Token aToken) {
                if (limit != null && (aToken.getStart() >= limit.getStart())) {
                    return SearchAction.STOP;
                }
                if (maxTokens-- == 0) {
                    return SearchAction.STOP;
                }
                if (isAThing(aToken)) {
                    return SearchAction.MATCH;
                } else {
                    return SearchAction.IGNORE;
                }
            }
        });
        return partOfFoo;
    }

    public Token extractAThing(TokenIterator<? extends Token> iterator) {
        Token start = findStart(iterator);
        Token limit = findLimit(iterator, start);

        return findFoo(iterator, start, limit);
    }
}

这个类的意图是线程安全的,因为它是无状态的,所有需要在方法之间共享的状态都作为参数从一个方法传递到另一个方法。然而,测试表明有时我们会在这一行得到一个空指针异常:

     if (limit != null && (aToken.getStart() >= limit.getStart())) {

似乎在检查空值和调用 getStart 之间的某个时间,参数限制变为空。

请注意,findFoo 方法将 limit 参数声明为 final:

    Token findFoo(TokenIterator<? extends Token> iterator, Token start, 
                  final Token limit) {

是否最终方法参数不在堆栈帧上,而是在方法的所有调用之间共享一个实例?如果所有调用之间共享一个实例是真的,那么这是否意味着使用最终参数会使一个类天生线程不安全?

【问题讨论】:

  • 您确定限制为空吗?如果 aToken 为空,你会得到同样的异常。
  • 你有多确定 limit 是罪魁祸首,而不是 aToken
  • aToken 保证不为空。从执行回调的框架中可以清楚地看出,但很难在帖子中显示。此外,我在测试期间添加了对 aToken 的空指针检查。
  • getStart() 的返回类型是什么?是原始的吗?
  • Token 是不可变的类吗?如果不是,并且您在线程之间共享 Token 实例,这将不是线程安全的

标签: java multithreading thread-safety anonymous-function


【解决方案1】:

是否最终方法参数不在堆栈帧上,而是在方法的所有调用之间共享一个实例?如果确实有一个实例在所有调用之间共享,那么这是否意味着使用最终参数会使一个类天生线程不安全?

不,我认为你在这里混合了一些东西。

这里的final 修饰符的作用是允许将局部变量limit 复制到匿名TokenSearcher 实例的合成变量中。此复制发生在此实例的构造过程中,然后将由assessToken 方法使用。这个合成变量仍然是最终的(或者至少没有修改),因此如果您的框架不执行反射魔法(并且仍然是并行的),那么这里应该没有问题。

findFoo 的每个调用仍然会有自己的参数。

正如 Reed 所说,getStart 结果的拆箱更有可能是罪魁祸首。

【讨论】:

  • getStart 返回一个 int。我已经阅读了 jvm 中的“方法区”及其与最终变量的关系。匿名内部类可以活过方法调用吗?如果是这样,它将访问什么限制变量?
  • 我理解你的解释。如果 limit 是 final ,那么它可以被复制,这就是为什么它必须是 final 才能被匿名内部类使用。很好的解释,我将把它作为答案,因为它让我不必担心匿名内部类如何访问最终参数限制
  • 是的,这些方法本地类(包括匿名类)是闭包的一个非常有限的版本,它们只对最终变量起作用,因为它们永远不会改变,从而绕过了问题。
【解决方案2】:

如果 getStart() 返回 Long 或 Integer 类型,但值为 null,也可能产生相同的异常。该异常是由为 >= 比较取消装箱为整数或长整数引起的,但本机类型不能为空。

【讨论】:

  • 鉴于您在此处添加了足够多的响应以消除通常的嫌疑人,我将继续将两个 getLimit 调用的结果放入局部变量中(当然受到空检查的保护,尽管保证框架),因此 NPE 将在没有其他操作的情况下发生并查明罪魁祸首。如果在此之后仍然摸不着头脑,请将 getStart 标记为已同步,然后看看会发生什么。
  • 超出我的专业知识...也许 Jeremy Manson link 可以提供一些启示。他致力于 Java 内存模型 JSR 133。
【解决方案3】:

似乎在检查空值和调用 getStart 之间的某个时间,参数限制变为空。

这是不可能的。在您测试和使用limit 时,它(实际上)是线程受限对象的私有最终实例变量。它是最终的事实意味着它不会改变。如果一开始是非空的,它将保持这种状态。对象是线程受限的这一事实意味着除了当前线程之外没有其他线程可以访问它。

(如果您的searchForward 方法将TokenSearcher 对象传递给不同的线程,我能想到的唯一潜在的线程安全问题可能会出现;即对象不是 线程受限。可能会发生这种情况吗?)

我认为真正的问题是别的:

  • aStart 参数可以是null
  • 如果声明 getStart() 中的一个或其他方法返回一个装箱类型,它们可能返回 null

其中任何一个都可能导致该行出现 NPE。


匿名内部类可以在方法调用之后存活吗?

它可以做到。这完全取决于searchForward 方法调用的作用。

如果是这样,它将访问什么限制变量?

匿名内部类的代码访问作为TokenSearcher 对象的实例变量保存的limit 的副本。对象创建时初始化,为final

【讨论】:

  • getStart 返回一个 int。我已经阅读了 jvm 中的“方法区”及其与最终变量的关系。匿名内部类可以活过方法调用吗?如果是这样,它将访问什么限制变量? – ditkin 5 分钟前添加评论
【解决方案4】:

一般而言,关于您的问题,每个方法调用都会获得自己的最终参数实例。 Final 不会创建静态变量。它只是允许变量只设置一次。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-09-29
    • 2016-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-19
    • 1970-01-01
    相关资源
    最近更新 更多