【问题标题】:Is the following utility class thread-safe?以下实用程序类是线程安全的吗?
【发布时间】:2009-11-11 19:51:49
【问题描述】:

首先让我们看一下实用程序类(大多数javadoc已被删除以简化示例):

public class ApplicationContextUtils {

    /**
     * The application context; care should be taken to ensure that 1) this
     * variable is assigned exactly once (in the
     * {@link #setContext(ApplicationContext)} method, 2) the context is never
     * reassigned to {@code null}, 3) access to the field is thread-safe (no race
     * conditions can occur)
     */
    private static ApplicationContext context = null;

    public static ApplicationContext getContext() {

    if (!isInitialized()) {
        throw new IllegalStateException(
            "Context not initialized yet! (Has the "
                + "ApplicationContextProviderBean definition been configured "
                + "properly and has the web application finished "
                + "loading before you invoked this method?)");
    }

    return context;
    }

    public static boolean isInitialized() {
    return context == null;
    }

    @SuppressWarnings("unchecked")
    public static <T> T getBean(final String name, final Class<T> requiredType) {
    if (requiredType == null) {
        throw new IllegalArgumentException("requiredType is null");
    }
    return (T) getContext().getBean(name, requiredType);
    }

    static synchronized void setContext(final ApplicationContext theContext) {

    if (theContext == null) {
        throw new IllegalArgumentException("theContext is null");
    }

    if (context != null) {
        throw new IllegalStateException(
            "ApplicationContext already initialized: it cannot be done twice!");
    }

    context = theContext;
    }

    private ApplicationContextUtils() {
    throw new AssertionError(); // NON-INSTANTIABLE UTILITY CLASS
    }
}

最后,有以下帮助 Spring 托管 bean 实际调用了 'setContext' 方法:

public final class ApplicationContextProviderBean implements
    ApplicationContextAware {

    public void setApplicationContext(
        final ApplicationContext applicationContext) throws BeansException {
    ApplicationContextUtils.setContext(applicationContext);
    }
}

Spring 会在应用启动后调用一次 setApplicationContext 方法。假设 nincompoop 之前没有调用 ApplicationContextUtils.setContext(),它应该锁定对实用程序类中上下文的引用,允许对 getContext() 的调用成功(意味着 isInitialized() 返回 true)。

我只是想知道这个类是否违反了良好编码实践的任何原则,特别是在线程安全方面(但欢迎发现其他愚蠢行为)。

感谢 StackOverflow 帮助我成为更好的程序员!

问候, LES

附:我没有进入为什么我需要这个实用程序类 - 让我确实有合法需要从应用程序中任何位置的静态上下文访问它就足够了(在加载弹簧上下文之后,当然)。

【问题讨论】:

  • "大多数 javadoc 已被删除以简化示例" -> 大声笑

标签: java multithreading spring static thread-safety


【解决方案1】:

没有。它不是线程安全的。

不保证写入context 类变量对通过getContext() 读取该变量的线程可见。

至少,将context 声明为volatile。理想情况下,将context 重新定义为AtomicReference,通过这样的调用设置:

if(!context.compareAndSet(null, theContext))
  throw new IllegalStateException("The context is already set.");

这里有一个更完整的例子:

public class ApplicationContextUtils {

  private static final AtomicReference<ApplicationContext> context = 
    new AtomicReference<ApplicationContext>();

  public static ApplicationContext getContext() {
    ApplicationContext ctx = context.get();
    if (ctx == null)
      throw new IllegalStateException();
    return ctx;
  }

  public static boolean isInitialized() {
    return context.get() == null;
  }

  static void setContext(final ApplicationContext ctx) {
    if (ctx == null) 
      throw new IllegalArgumentException();
    if (!context.compareAndSet(null, ctx))
      throw new IllegalStateException();
  }

  public static <T> T getBean(final String name, final Class<T> type) {
    if (type == null) 
      throw new IllegalArgumentException();
    return type.cast(getContext().getBean(name, type));
  }

  private ApplicationContextUtils() {
    throw new AssertionError();
  }

}

请注意,除了线程安全之外,这还提供了类型安全,利用了传递给 getBean() 方法的 Class 实例。

我不确定您打算如何使用isInitialized() 方法;它对我来说似乎不是很有用,因为一旦你调用它,条件可能会改变,而且你没有很好的通知方式。

【讨论】:

  • 确实如此,但为什么它在 this 场景中很重要? setContext() 只会被调用一次,getContext() 将在 OP 预期之前(或期间)失败。
  • getContext() 也可能导致对setContext() 的调用“之后”失败。如果没有内存屏障,您就无法说出一个线程何时看到另一个线程的操作。换句话说,如果没有规范中定义的 happens-before 关系,“之前”、“期间”和“之后”之类的词是没有意义的。
  • 很好,让我将我在第一条评论中提到的“期间”定义为“直到 getter 线程看到更改为 context 之前的时间段”。是的,理论上这可能永远不会发生,因为 getter 没有在同一个锁上同步并且context 不是易失性的。即使在这种情况下,getContext() 也会简单地抛出一个异常 - 就像从未调用过 setContext() 一样。再说一次,在 this 场景中,这些实际上有什么影响?
  • 所以如果一个线程正在循环,说:while(!ApplicationContextUtils.isInitialized()) { } Service myService = ApplicationContextUtils.getBean("service",Service.class); JVM 可以将其优化为:while(true) { ... 无限循环 ... } ?
  • @LES2:正确,完全正确。如果 JVM 愿意,它完全有权进行此类观察。它可能永远不会发生,但这正是当您从客户端切换到服务器 VM,或从 Hotspot 切换到 JRockit,或者您的方法被第 1,000,001 次调用触发某种 JIT 重新编译时可能停止工作的事情,管他呢。至少应该是不稳定的。
【解决方案2】:

Spring 已经有一个名为ContextSingletonBeanFactoryLocator 的类,它为您连接到ApplicationContext 的静态访问。至少,使用这个类可能会让您不必担心您的自定义方法是否是线程安全的。

但是,一开始使用这个类有点令人困惑,因为有一点间接性。您可以查看this blog post,了解有关此调用如何工作的更多信息。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多