The following is a code from one of my projects which implements a loading cache with timeout and expiry. The associated tests are [here][1] (mixed with a good number of argument sanity checks tests). I seek comments on the thread safety aspects in particular. I think it is, but I may miss something.

Also, do you see ways it could be improved?


    /**
     * A caching, on-demand loading message source provider with configurable expiry
     *
     * <p>This class uses a {@link MessageSourceLoader} internally to look up
     * message sources. As is the case for {@link StaticMessageSourceProvider}, you
     * can also set a default source if the loader fails to grab a source.</p>
     *
     * <p>Apart from the loader, you can customize two aspects of the provider:</p>
     *
     * <ul>
     *     <li>its load timeout (5 seconds by default);</li>
     *     <li>its expiry time (10 minutes by default).</li>
     * </ul>
     *
     * <p>Note that the expiry time is periodic only, and not per source. The
     * loading result (success or failure) is recorded permanently until the expiry
     * time kicks in, <b>except</b> when the timeout kicks in; in this case, loading
     * will be retried.</p>
     *
     * <p>You cannot instantiate that class directly; use {@link #newBuilder()} to
     * obtain a builder class and set up your provider.</p>
     */
    @ThreadSafe
    public final class LoadingMessageSourceProvider
        implements MessageSourceProvider
    {
        /*
         * Use daemon threads. We don't give control to the user about the
         * ExecutorService, and we don't have a reliable way to shut it down (a JVM
         * shutdown hook does not get involved on a webapp shutdown, so we cannot
         * use that...).
         */
        private static final ThreadFactory THREAD_FACTORY = new ThreadFactory()
        {
            private final ThreadFactory factory = Executors.defaultThreadFactory();
    
            @Override
            public Thread newThread(final Runnable r)
            {
                final Thread ret = factory.newThread(r);
                ret.setDaemon(true);
                return ret;
            }
        };
    
        private static final InternalBundle BUNDLE
            = InternalBundle.getInstance();
    
        private static final int NTHREADS = 3;
    
        /*
         * Executor service for loading tasks
         */
        private final ExecutorService service
            = Executors.newFixedThreadPool(NTHREADS, THREAD_FACTORY);
    
        /*
         * Loader and default source
         */
        private final MessageSourceLoader loader;
        private final MessageSource defaultSource;
    
        /*
         * Timeout
         */
        private final long timeoutDuration;
        private final TimeUnit timeoutUnit;
    
        /*
         * List of sources
         */
        private final Map<Locale, FutureTask<MessageSource>> sources
            = new HashMap<Locale, FutureTask<MessageSource>>();
    
        private LoadingMessageSourceProvider(final Builder builder)
        {
            loader = builder.loader;
            defaultSource = builder.defaultSource;
            timeoutDuration = builder.timeoutDuration;
            timeoutUnit = builder.timeoutUnit;
            if (builder.expiryDuration > 0L)
                setupExpiry(builder.expiryDuration, builder.expiryUnit);
        }
    
        /**
         * Create a new builder
         *
         * @return an empty builder
         */
        public static Builder newBuilder()
        {
            return new Builder();
        }
    
        @Override
        public MessageSource getMessageSource(final Locale locale)
        {
            FutureTask<MessageSource> task;
    
            /*
             * The algorithm is as follows:
             *
             * - access the sources map in a synchronous manner (the expiry task
             *   also does this);
             * - grab the FutureTask matching the required locale:
             *     - if no task exists, create it;
             *     - if it exists but has been cancelled (in the event of a timeout,
             *       see below), create a new task;
             * - always within the synchronized access to sources, submit the task
             *   for immediate execution to our ExecutorService;
             * - to be followed...
             */
            synchronized (sources) {
                task = sources.get(locale);
                if (task == null || task.isCancelled()) {
                    task = loadingTask(locale);
                    sources.put(locale, task);
                    service.execute(task);
                }
            }
    
            /*
             * - try and get the result of the task, with a timeout;
             * - if we get a result in time, return it, or the default source (if
             *   any) if the result is null;
             * - in the event of an error, return the default source; in addition,
             *   if this is a timeout, cancel the task.
             */
            try {
                final MessageSource source = task.get(timeoutDuration, timeoutUnit);
                return source == null ? defaultSource : source;
            } catch (InterruptedException ignored) {
                return defaultSource;
            } catch (ExecutionException ignored) {
                return defaultSource;
            } catch (TimeoutException ignored) {
                task.cancel(true);
                return defaultSource;
            }
        }
    
        private FutureTask<MessageSource> loadingTask(final Locale locale)
        {
            return new FutureTask<MessageSource>(new Callable<MessageSource>()
            {
                @Override
                public MessageSource call()
                    throws IOException
                {
                    return loader.load(locale);
                }
            });
        }
    
        private void setupExpiry(final long duration, final TimeUnit unit)
        {
            final Runnable runnable = new Runnable()
            {
                @Override
                public void run()
                {
                    /*
                     * We need to walk the list of current tasks and cancel them if
                     * they are still running.
                     */
                    synchronized (sources) {
                        /*
                         * This MUST be done from within this block. If we don't do
                         * this here, a task can "leak" from getMessageSource(), and
                         * if the caller .get()s, it will be greeted with a
                         * CancellationException. Not what we want!
                         */
                        for (final FutureTask<MessageSource> task: sources.values())
                            task.cancel(true);
                        sources.clear();
                    }
                }
            };
            // Overkill?
            final ScheduledExecutorService scheduled
                = Executors.newScheduledThreadPool(1, THREAD_FACTORY);
            final long initialDelay = unit.toMillis(duration);
            scheduled.scheduleAtFixedRate(runnable, initialDelay, duration, unit);
        }
    
        /**
         * Builder class for a {@link LoadingMessageSourceProvider}
         */
        public static final class Builder
        {
            private MessageSourceLoader loader;
            private MessageSource defaultSource;
            private long timeoutDuration = 5L;
            private TimeUnit timeoutUnit = TimeUnit.SECONDS;
            private long expiryDuration = 10L;
            private TimeUnit expiryUnit = TimeUnit.MINUTES;
    
            private Builder()
            {
            }
    
            /**
             * Set the message source loader
             *
             * @param loader the loader
             * @throws NullPointerException loader is null
             * @return this
             */
            public Builder setLoader(final MessageSourceLoader loader)
            {
                BUNDLE.checkNotNull(loader, "cfg.nullLoader");
                this.loader = loader;
                return this;
            }
    
            /**
             * Set the default message source if the loader fails to load
             *
             * @param defaultSource the default source
             * @throws NullPointerException source is null
             * @return this
             */
            public Builder setDefaultSource(final MessageSource defaultSource)
            {
                BUNDLE.checkNotNull(defaultSource, "cfg.nullDefaultSource");
                this.defaultSource = defaultSource;
                return this;
            }
    
            /**
             * Set the load timeout
             *
             * @param duration number of units
             * @param unit the time unit
             * @throws IllegalArgumentException {@code duration} is negative or zero
             * @throws NullPointerException {@code unit} is null
             * @return this
             *
             * @deprecated use {@link #setLoadTimeout(long, TimeUnit)} instead.
             * Will be removed in 0.6.
             */
            @Deprecated
            public Builder setTimeout(final long duration, final TimeUnit unit)
            {
                BUNDLE.checkArgument(duration > 0L, "cfg.nonPositiveDuration");
                BUNDLE.checkNotNull(unit, "cfg.nullTimeUnit");
                timeoutDuration = duration;
                timeoutUnit = unit;
                return this;
            }
    
            /**
             * Set the load timeout (5 seconds by default)
             *
             * <p>If the loader passed as an argument fails to load a message
             * source after the specified timeout is elapsed, then the default
             * messagesource will be returned (if any).</p>
             *
             * @param duration number of units
             * @param unit the time unit
             * @throws IllegalArgumentException {@code duration} is negative or zero
             * @throws NullPointerException {@code unit} is null
             * @return this
             *
             * @see {@link #setLoader(MessageSourceLoader)}
             * @see {@link #setDefaultSource(MessageSource)}
             */
            public Builder setLoadTimeout(final long duration, final TimeUnit unit)
            {
                BUNDLE.checkArgument(duration > 0L, "cfg.nonPositiveDuration");
                BUNDLE.checkNotNull(unit, "cfg.nullTimeUnit");
                timeoutDuration = duration;
                timeoutUnit = unit;
                return this;
            }
    
            /**
             * Set the source expiry time (10 minutes by default)
             *
             * <p>Do <b>not</b> use this method if you want no expiry at all; use
             * {@link #neverExpires()} instead.</p>
             *
             * @since 0.5
             *
             * @param duration number of units
             * @param unit the time unit
             * @throws IllegalArgumentException {@code duration} is negative or zero
             * @throws NullPointerException {@code unit} is null
             * @return this
             */
            public Builder setExpiryTime(final long duration, final TimeUnit unit)
            {
                BUNDLE.checkArgument(duration > 0L, "cfg.nonPositiveDuration");
                BUNDLE.checkNotNull(unit, "cfg.nullTimeUnit");
                expiryDuration = duration;
                expiryUnit = unit;
                return this;
            }
    
            /**
             * Set this loading provider so that entries never expire
             *
             * <p>Note that, as noted in the description, apart from loading
             * timeouts, successes and failures are recorded permanently (see
             * {@link FutureTask}).</p>
             *
             * @since 0.5
             *
             * @return this
             */
            public Builder neverExpires()
            {
                expiryDuration = 0L;
                return this;
            }
    
            /**
             * Build the provider
             *
             * @return a {@link LoadingMessageSourceProvider}
             * @throws IllegalArgumentException no loader has been provided
             */
            public MessageSourceProvider build()
            {
                BUNDLE.checkArgument(loader != null, "cfg.noLoader");
                return new LoadingMessageSourceProvider(this);
            }
        }
    }

  [1]: https://raw.github.com/fge/msg-simple/master/src/test/java/com/github/fge/msgsimple/provider/LoadingMessageSourceProviderTest.java