【问题标题】:Java store reflected Method statically in class: Safe?Java 存储类中静态反映的方法:安全吗?
【发布时间】:2021-06-24 16:57:30
【问题描述】:

在 Java 中是否存在类似以下“安全”的内容,为什么?

public final class Utility {

    private Utility() {}

    private static Method sFooMethod = null;

    public static void callFoo(SomeThing thing) {
        try {
            if(sFooMethod == null)
                sFooMethod = SomeThing.class.getMethod("foo");
            sFooMethod.invoke(thing);
        } catch(Exception e) {}  // Just for simplicity here
    }

}

我的理由是,即使另一个线程在后台写入 sFooMethod 并且当前线程在执行 callFoo() 期间突然在某处看到它,它仍然会导致 thing.foo() 的相同旧反射调用?

额外问题:以下方法与上述方法有何不同(正面/负面)?会更受欢迎吗?

public final class Utility {

    private Utility() {}

    private static final Method sFooMethod;
    static {
        try {
            sFooMethod = SomeThing.class.getMethod("foo");
        } catch(Exception e) {}
    }

    public static void callFoo(SomeThing thing) {
        try {
            if(sFooMethod != null)
                sFooMethod.invoke(thing);
        } catch(Exception e) {}
    }

}

评论的背景更新: 我正在编写一个 Android 应用程序,我需要调用一个在 API 29 之前是私有的方法,当它公开而不被更改时。在 AndroidX 核心库的 alpha 版本(还不能使用它)中,Google 提供了一个 HandlerCompat 方法,该方法使用反射来调用私有方法(如果它不是公共的)。所以我现在将谷歌的方法复制到我自己的 HandlerCompatP 类中,但我注意到如果我调用它 1000 次,那么反射查找将发生 1000 次(我看不到任何缓存)。所以这让我开始思考是否有一种好方法可以只执行一次反射,并且只在需要时执行。

“不要使用反射”在这里不是一个答案,因为在这种情况下它是必需的,谷歌自己打算在他们的兼容性库中发生它。我的问题也不是使用反射是否安全和/或良好的做法,我很清楚这通常不好,而是考虑到我是否 am 使用反射,哪种方法是安全的/更好。

【问题讨论】:

  • 为什么?你想达到什么目的?听起来像是一个 XY 问题...
  • 您是否尝试仅分配方法foo() 的参考?或者您希望其他线程存储对 SomeThng 类中其他方法的引用?
  • 第二种方法更好 - 但不要吃异常 - 将异常包装在一些 Error/RuntimeException 中,然后重新抛出它。不确定 android 是如何工作的,但是 OpenJDK 的 JIT 可以不断折叠 static final 字段。
  • 第一个被破坏了,因为没有线程安全构造,sFooMethod == null 可以看到由不同线程进行的写入,评估为false,然后是随后的sFooMethod.invoke(thing); not看到写入并导致NullPointerException。这并没有说明Method 的内部状态。您应该遵循 Johannes Kuhn 的建议并使用第二个,但不要吞下异常。如果您不确保始终且最多分配一次最终字段,则无论如何都不会编译。请记住,不能使用可变的setAccessible
  • @pallgeuer 这是两个没有线程安全的独立读取。他们每个人都可能感知到并发写入。所以是的,第一次读取可能会感知到写入,而第二次则不会。这不是关于“忘记”,因为阅读不记得以前的阅读。请参阅shipilev.net/blog/2014/safe-public-construction/…UnsafeDCLFactory 示例及其下方的第 2 点。

标签: java methods reflection static singleton


【解决方案1】:

避免内存一致性错误的关键是理解happens-before关系。这种关系只是保证一个特定语句的内存写入对另一个特定语句可见。
Java 语言规范声明如下:
17.4.5. Happens-before Order

两个动作可以通过happens-before关系排序。如果一个 动作发生在另一个之前,然后第一个对和可见 在第二个之前订购。

如果我们有两个动作 x 和 y,我们写 hb(x, y) 来表示 x 发生在 y 之前。

如果 x 和 y 是同一个线程的动作并且 x 在 y 之前 程序顺序,然后是 hb(x, y)。

在您的情况下,写入静态字段然后从静态字段读取是在同一个步骤中发生的。所以“发生在之前”的关系就成立了。所以读操作总是会看到写操作的效果。
此外,所有线程都将写入相同的数据。更糟糕的是,所有符合条件的线程都会同时写入变量。该变量将引用最后分配的对象,其余取消引用的对象将被垃圾收集。

您的应用程序中不会有很多线程同时进入同一个方法,这会由于创建大量对象而导致严重的性能损失。但是,如果您只想设置一次变量,那么第二种方法会更好。作为static blocks are thread safe

【讨论】:

  • “对静态字段的写入和读取都发生在同一个线程中”——仅当 sFooMethod == null 的计算结果为 true 时。
  • 看来@Holger 是对的。在执行getMethod() 时,sFooMethod 可能被设置为线程 A 中的部分构造对象。然后线程 A 继续尽可能快地构造 sFooMethod,但同时线程 B 可能会看到 sFooMethod 不为空,并尝试在对象完全构造之前或线程 B 看到之前调用它线程 A 进行的建设性字段写入。这似乎与为什么幼稚的双重检查锁定不起作用的问题类似:en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
【解决方案2】:

在 Java 中是否存在类似以下“安全”的内容,为什么?

  1. 不,我不建议使用反射,除非你必须这样做。
  2. 大多数时候,开发人员以某种方式设计他们的类,以便从不需要访问隐藏字段或方法。很可能会有更好的方法来访问隐藏的内容。
  3. 尤其是隐藏的字段和方法可以在更新它们所在的库时更改其名称。所以你的代码可能会突然停止工作,而你不知道为什么,因为编译器不会输出任何错误。
  4. 直接访问方法或字段也比通过反射更快,因为反射首先需要搜索它,而直接访问则不需要

所以如果没有必要,不要使用反射

【讨论】:

  • 这错过了我的问题。我不是在问反射是否从根本上说是一个好主意。假设我正在使用反射并且不想每次都执行反射查找,那么我的两个建议中哪一个更安全/更好?
  • 存储方法会更好(就像您在第二个示例中所做的那样)。这样调用会更快
【解决方案3】:

我不确定你的目标是什么——可能有更好的方法来做你想做的事情。

使用静态初始化器的第二种方法更可取,因为您的第一个实现具有竞争条件。

【讨论】:

  • 我已经用我的目标更新了这个问题。你能详细说明一下比赛条件可能是一个潜在的问题吗?
猜你喜欢
  • 1970-01-01
  • 2014-10-08
  • 1970-01-01
  • 2010-11-08
  • 1970-01-01
  • 1970-01-01
  • 2018-10-23
  • 1970-01-01
相关资源
最近更新 更多