Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
252 views
in Technique[技术] by (71.8m points)

java - Do Guice singletons honor thread-confinement?

I have a concern regarding Guice and whether or not its singletons will obey thread confinement that I may try to set up:

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.
}

So as you can see, any time we create a new instance of CacheAdaptor, a CacheModule will be used to bootstrap the entire dependency tree underneath it.

What happens if new CacheAdaptor(); is called from inside multiple threads?

For example: Thread #1 creates a new CacheAdaptor via its no-arg constructor, and Thread #2 does the same thing. Will Guice provide the same exact WidgetCache instance to each thread's CacheAdaptor, or will Guice provide 2 different instances to each thread? Even though toInstance(...) is supposed to return the same singleton instance, I'm hoping - since the modules are created inside 2 distinct threads - that each CacheAdaptor will receive a different WidgetCache instance.

Thanks in advance!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...