It's not only that Guice will provide the same singleton across threads for the same injector, but Guice can only provide the same singleton across threads if you use toInstance
. Modules are evaluated once per injector, and you gave Guice one instance and no way to produce a second one.
Guice isn't magic. When trying to provide an instance of an Object, it either needs (1) a Guice-friendly no-arg or @Inject
-annotated constructor; (2) a Provider
or @Provides
method, letting you create the instance yourself; or (3) an instance that you've already created and bound with toInstance
, which Guice reuses because it doesn't know how to create another. Note that option 2, with a Provider
, doesn't need to guarantee that it creates a new instance every single time, and we can exploit that to write a Provider
that has a ThreadLocal cache. It would look something like this:
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();
}
}
Of course, if you want to do that for more than one object, you'd have to write a ThreadLocal for every single key you want to cache, and then create a provider for each. That seems a little excessive, and that's where custom scopes come in.
Creating your own ThreadLocal scope
Check out Scope's only meaningful method:
/**
* 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);
As you can see from the Scope interface, a scope is just a decorator for a Provider
, and scoping something thread-local is tantamount to returning a ThreadLocal
-cached copy if it exists or caching and returning from the passed Provider
if it doesn't. So we can easily write a scope that does the same logic we did manually above.
In fact, the need to create a new FooObject per-thread (for any value of FooObject) is a common request—too much of an "advanced feature" for the base library, but common enough for it to be the example about how to write a custom scope. To adjust that SimpleScope example to your needs, you can leave out the scope.enter()
and scope.exit()
calls, but keep the ThreadLocal<Map<Key<?>, Object>>
to act as your thread-local cache of objects.
At that point, assuming you've created your own @ThreadScoped
annotation with a ThreadScope
implementation you've written, you can adjust your module to look like this:
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);
}
}
Remember, singleton behavior doesn't depend on which threads you create the modules in, but rather which injector you're asking. If you created five unrelated Injector
instances, they would each have their own singleton. If you're merely trying to run a small algorithm in a multi-threaded way, you could create your own injector per thread, but that way you'd lose the chance to make singleton objects that do span threads.
bind(WidgetCache.class).toInstance(widgetCache)
creates a singletonWidgetCache
instance and then reuses it no matter how many times a client requests it (viaInjector#getInstance(WidgetCache.class)
. – Christynabind(WidgetCache.class).toInstance(widgetCache)
creates a singleton - but is that singleton thread-confined or not? It's very feasible that theWidgetCache
instance obtained by the module in Thread #1 will be different than the one obtained in Thread #2. – Christyna