【问题标题】:Do Guice singletons honor thread-confinement?Guice 单身人士是否尊重线程限制?
【发布时间】:2013-09-03 19:20:29
【问题描述】:

我担心 Guice 以及它的单例是否会遵守我可能会尝试设置的线程限制:

public class CacheModule extends AbstractModule {
    @Override
    protected void configure() {
        // WidgetCache.class is located inside a 3rd party JAR that I
        // don't have the ability to modify.
        WidgetCache widgetCache = new WidgetCache(...lots of params);

        // Guice will reuse the same WidgetCache instance over and over across
        // multiple calls to Injector#getInstance(WidgetCache.class);
        bind(WidgetCache.class).toInstance(widgetCache);
    }
}

// CacheAdaptor is the "root" of my dependency tree. All other objects
// are created from it.
public class CacheAdaptor {
    private CacheModule bootstrapper = new CacheModule();

    private WidgetCache widgetCache;

    public CacheAdaptor() {
        super();

        Injector injector = Guice.createInjector(bootstrapper);

        setWidgetCache(injector.getInstance(WidgetCache.class));
    }

    // ...etc.
}

如您所见,每当我们创建CacheAdaptor 的新实例时,CacheModule 将用于引导它下面的整个依赖关系树。

如果从多个线程内部调用 new CacheAdaptor(); 会发生什么?

例如:线程#1 通过它的无参数构造函数创建一个新的CacheAdaptor,线程#2 做同样的事情。 Guice 会为每个线程的 CacheAdaptor 提供完全相同的 WidgetCache 实例,还是 Guice 会为每个线程提供 2 个不同的实例? 即使 toInstance(...) 应该返回相同的单例实例,我我希望 - 因为模块是在 2 个不同的线程中创建的 - 每个 CacheAdaptor 将收到不同的 WidgetCache 实例。

提前致谢!

【问题讨论】:

  • 你说的是单例,但我没看到
  • @PhilippSander - bind(WidgetCache.class).toInstance(widgetCache) 创建一个单例 WidgetCache 实例,然后无论客户端请求多少次都重用它(通过 Injector#getInstance(WidgetCache.class)
  • 你刚刚回答了你的问题......
  • 再次感谢@PhilippSander - 但是您是否阅读过我的问题? 这个问题与 Guice 跨多个线程的行为有关。我知道,对于单线程应用程序,bind(WidgetCache.class).toInstance(widgetCache) 创建了一个单例 - 但是该单例是否受线程限制? WidgetCache 非常可行线程#1中的模块获得的实例将不同于线程#2中获得的实例。
  • 是的,我做到了——它实际上出现在上面的 bold 中,如下所示:“如果从多个线程内部调用 new CacheAdaptor(); 会发生什么? b>”。我一直很感谢@PhilippSander 的所有意见,但如果你不知道这个问题的答案,我会请你继续回答另一个问题。

标签: java dependency-injection singleton guice


【解决方案1】:

不仅 Guice 为同一个注入器跨线程提供相同的单例,而且如果您使用 toInstance,Guice 只能跨线程提供相同的单例.每个注入器对模块进行一次评估,您给 Guice 一个实例,却无法生成第二个。

Guice 不是魔法。当试图提供一个对象的实例时,它要么需要(1)一个 Guice 友好的 no-arg 或 @Inject-annotated 构造函数; (2) 一个Provider@Provides 方法,让你自己创建实例;或者 (3) 一个您已经创建并与toInstance 绑定的实例,Guice 重复使用该实例,因为它不知道如何创建另一个。请注意,带有Provider 的选项2 不需要保证它每次都创建一个新实例,我们可以利用它来编写一个具有ThreadLocal 缓存的Provider。它看起来像这样:

public class CacheModule extends AbstractModule {
    /** This isn't needed anymore; the @Provides method below is sufficient. */
    @Override protected void configure() {}

    /** This keeps a WidgetCache per thread and knows how to create a new one. */
    private ThreadLocal<WidgetCache> threadWidgetCache = new ThreadLocal<>() {
        @Override protected WidgetCache initialValue() {
            return new WidgetCache(...lots of params);
        }
    };

    /** Provide a single separate WidgetCache for each thread. */
    @Provides WidgetCache provideWidgetCache() {
        return threadWidgetCache.get();
    }
}

当然,如果您想为多个对象执行此操作,则必须为要缓存的每个键编写一个 ThreadLocal,然后为每个键创建一个提供程序。这似乎有点过分,这就是自定义范围的用武之地。

创建自己的 ThreadLocal 作用域

查看Scope唯一有意义的方法:

/**
 * Scopes a provider. The returned provider returns objects from this scope.
 * If an object does not exist in this scope, the provider can use the given
 * unscoped provider to retrieve one.
 *
 * <p>Scope implementations are strongly encouraged to override
 * {@link Object#toString} in the returned provider and include the backing
 * provider's {@code toString()} output.
 *
 * @param key binding key
 * @param unscoped locates an instance when one doesn't already exist in this
 *  scope.
 * @return a new provider which only delegates to the given unscoped provider
 *  when an instance of the requested object doesn't already exist in this
 *  scope
 */
public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped);

正如您从Scope interface 中看到的,作用域只是Provider 的装饰器,将某些线程本地化的范围等同于返回ThreadLocal-cached 副本(如果存在)或缓存并从如果没有,则通过 Provider。所以我们可以很容易地编写一个与上面手动执行相同逻辑的作用域。

事实上,需要为每个线程(对于 FooObject 的任何值)创建一个新的 FooObject 是一个常见的请求——too much of an "advanced feature" for the base library,但对于它是example about how to write a custom scope 来说已经足够常见了。要根据您的需要调整 SimpleScope 示例,您可以省略 scope.enter()scope.exit() 调用,但保留 ThreadLocal&lt;Map&lt;Key&lt;?&gt;, Object&gt;&gt; 作为对象的线程本地缓存。

此时,假设您已经使用自己编写的 ThreadScope 实现创建了自己的 @ThreadScoped 注释,您可以将模块调整为如下所示:

public class CacheModule extends AbstractModule {
    @Override
    protected void configure() {
        bindScope(ThreadScoped.class, new ThreadScope());
    }

    /** Provide a single separate WidgetCache for each thread. */
    @Provides @ThreadScoped WidgetCache provideWidgetCache() {
        return new WidgetCache(...lots of params);
    }
}

请记住,单例行为并不取决于您在哪个线程中创建模块,而是取决于您询问的是哪个注入器。如果您创建了五个不相关的 Injector 实例,它们每个都有自己的单例。如果您只是尝试以多线程方式运行一个小算法,您可以为每个线程创建自己的注入器,但那样您将失去创建跨线程的单例对象的机会。

【讨论】:

  • 感谢@Jeff Bowman (+1) - 我认为我开始透过树林看到森林了。我有一个后续问题要问你:你的第二句话说“每个注射器对模块进行一次评估,你给 Guice 一个实例,没有办法产生第二个实例。你能详细说明一下吗?你的意思是什么?我只给了Guice一个what的实例?我将如何制作它的“第二个”?如果你能向我解释这一点,我应该能够连接所有的点。再次感谢!
  • @IAmYourFaja 我最终重构了我的答案,包括在 Provider 方法中手动执行它的方法。抱歉,它变得更长了,但我认为它会更好地解释 Guice 对象是如何创建和限定范围的。
猜你喜欢
  • 2010-10-15
  • 1970-01-01
  • 1970-01-01
  • 2011-08-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多