This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 5d146cd06 Use LazyInitializer without subclassing. (#1123)
5d146cd06 is described below

commit 5d146cd06965432812c11ea8a6b458b3d2f56f65
Author: Gary Gregory <garydgreg...@users.noreply.github.com>
AuthorDate: Fri Oct 20 13:53:08 2023 -0400

    Use LazyInitializer without subclassing. (#1123)
    
    * Use LazyInitializer without subclassing.
    
    - Allow a Supplier for initialized object
    - Allow a Consumer to close the managed object
    
    * Allow any checked exception in the argument to supplier and consumer
    
    * Make all impls of AbstractConcurrentInitializer concrete with builders 
and tests
    
    * add tests for closer
    
    * Use ConcurrentException as the wrapper for close
    
    * Changes requested in code review
    
    ---------
    
    Co-authored-by: Gary Gregory <gardgreg...@gmail.com>
    Co-authored-by: Benjamin Confino <benja...@uk.ibm.com>
---
 .../commons/lang3/builder/AbstractSupplier.java    |  42 ++++
 .../concurrent/AbstractConcurrentInitializer.java  | 152 +++++++++++-
 .../lang3/concurrent/AtomicInitializer.java        |  58 ++++-
 .../lang3/concurrent/AtomicSafeInitializer.java    |  58 ++++-
 .../lang3/concurrent/BackgroundInitializer.java    |  72 +++++-
 .../concurrent/CallableBackgroundInitializer.java  |   9 +
 .../commons/lang3/concurrent/LazyInitializer.java  | 106 ++++++---
 .../concurrent/MultiBackgroundInitializer.java     |  33 +++
 ...oncurrentInitializerCloseAndExceptionsTest.java | 184 +++++++++++++++
 .../concurrent/AtomicInitializerSupplierTest.java  |  43 ++++
 .../AtomicSafeInitializerSupplierTest.java         |  97 ++++++++
 .../BackgroundInitializerSupplierTest.java         | 148 ++++++++++++
 .../concurrent/BackgroundInitializerTest.java      | 111 ++++++---
 .../concurrent/LazyInitializerCloserTest.java      |  54 +++++
 .../LazyInitializerFailableCloserTest.java         |  72 ++++++
 .../concurrent/LazyInitializerSupplierTest.java    |  43 ++++
 .../MultiBackgroundInitializerSupplierTest.java    | 261 +++++++++++++++++++++
 .../concurrent/MultiBackgroundInitializerTest.java | 121 +++++++---
 18 files changed, 1574 insertions(+), 90 deletions(-)

diff --git 
a/src/main/java/org/apache/commons/lang3/builder/AbstractSupplier.java 
b/src/main/java/org/apache/commons/lang3/builder/AbstractSupplier.java
new file mode 100644
index 000000000..29b94b7aa
--- /dev/null
+++ b/src/main/java/org/apache/commons/lang3/builder/AbstractSupplier.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.lang3.builder;
+
+import org.apache.commons.lang3.function.FailableSupplier;
+
+/**
+ * Abstracts supplying an instance of {@code T}. Use to implement the builder 
pattern.
+ *
+ * @param <T> the type of instances to build.
+ * @param <B> the type of builder.
+ * @param <E> The kind of thrown exception or error.
+ * @since 3.14.0
+ */
+public abstract class AbstractSupplier<T, B extends AbstractSupplier<T, B, E>, 
E extends Throwable> implements FailableSupplier<T, E> {
+
+    /**
+     * Returns this instance typed as the proper subclass type.
+     *
+     * @return this instance typed as the proper subclass type.
+     */
+    @SuppressWarnings("unchecked")
+    protected B asThis() {
+        return (B) this;
+    }
+
+}
diff --git 
a/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java
 
b/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java
index 7786c8057..fb61e778f 100644
--- 
a/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java
+++ 
b/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java
@@ -17,6 +17,13 @@
 
 package org.apache.commons.lang3.concurrent;
 
+import java.util.Objects;
+
+import org.apache.commons.lang3.builder.AbstractSupplier;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+
 /**
  * Abstracts and defines operations for ConcurrentInitializer implementations.
  *
@@ -26,15 +33,150 @@ package org.apache.commons.lang3.concurrent;
  */
 public abstract class AbstractConcurrentInitializer<T, E extends Exception> 
implements ConcurrentInitializer<T> {
 
+    /**
+     * Builds a new instance for subclasses.
+     *
+     * @param <T> the type of the object managed by the initializer class.
+     * @param <I> the type of the initializer class.
+     * @param <B> the type of builder.
+     * @param <E> The exception type thrown by {@link #initialize()}.
+     */
+    public abstract static class AbstractBuilder<I extends 
AbstractConcurrentInitializer<T, E>, T, B extends AbstractBuilder<I, T, B, E>, 
E extends Exception>
+            extends AbstractSupplier<I, B, E> {
+
+        /**
+         * Closer consumer called by {@link #close()}.
+         */
+        private FailableConsumer<T, ? extends Exception> closer = 
FailableConsumer.nop();
+
+        /**
+         * Initializer supplier called by {@link #initialize()}.
+         */
+        private FailableSupplier<T, ? extends Exception> initializer = 
FailableSupplier.nul();
+
+        /**
+         * Gets the closer consumer called by {@link #close()}.
+         *
+         * @return the closer consumer called by {@link #close()}.
+         */
+        public FailableConsumer<T, ? extends Exception> getCloser() {
+            return closer;
+        }
+
+        /**
+         * Gets the initializer supplier called by {@link #initialize()}.
+         *
+         * @return the initializer supplier called by {@link #initialize()}.
+         */
+        public FailableSupplier<T, ? extends Exception> getInitializer() {
+            return initializer;
+        }
+
+        /**
+         * Sets the closer consumer called by {@link #close()}.
+         *
+         * @param closer the consumer called by {@link #close()}.
+         * @return this
+         */
+        public B setCloser(final FailableConsumer<T, ? extends Exception> 
closer) {
+            this.closer = closer != null ? closer : FailableConsumer.nop();
+            return asThis();
+        }
+
+        /**
+         * Sets the initializer supplier called by {@link #initialize()}.
+         *
+         * @param initializer the supplier called by {@link #initialize()}.
+         * @return this
+         */
+        public B setInitializer(final FailableSupplier<T, ? extends Exception> 
initializer) {
+            this.initializer = initializer != null ? initializer : 
FailableSupplier.nul();
+            return asThis();
+        }
+
+    }
+
+    /**
+     * Closer consumer called by {@link #close()}.
+     */
+    private final FailableConsumer<? super T, ? extends Exception> closer;
+
+    /**
+     * Initializer supplier called by {@link #initialize()}.
+     */
+    private final FailableSupplier<? extends T, ? extends Exception> 
initializer;
+
+    /**
+     * Constructs a new instance.
+     */
+    public AbstractConcurrentInitializer() {
+        this(FailableSupplier.nul(), FailableConsumer.nop());
+    }
+
+    /**
+     * Constructs a new instance.
+     *
+     * @param initializer the initializer supplier called by {@link 
#initialize()}.
+     * @param closer the closer consumer called by {@link #close()}.
+     */
+    AbstractConcurrentInitializer(final FailableSupplier<? extends T, ? 
extends Exception> initializer, final FailableConsumer<? super T, ? extends 
Exception> closer) {
+        this.closer = Objects.requireNonNull(closer, "closer");
+        this.initializer = Objects.requireNonNull(initializer, "initializer");
+    }
+
+    /**
+     * Calls the closer with the manager object.
+     *
+     * @throws ConcurrentException Thrown by the closer.
+     * @since 3.14.0
+     */
+    public void close() throws ConcurrentException {
+        if (isInitialized()) {
+            try {
+                closer.accept(get());
+            } catch (final Exception e) {
+                // This intentionally does not duplicate the logic in 
initialize
+                // or care about the generic type E.
+                //
+                // initialize may run inside a Future and it does not make 
sense
+                // to wrap an exception stored inside a Future. However close()
+                // always runs on the current thread so it always wraps in a
+                // ConcurrentException
+                throw new 
ConcurrentException(ExceptionUtils.throwUnchecked(e));
+            }
+        }
+    }
+
     /**
      * Creates and initializes the object managed by this {@code
      * ConcurrentInitializer}. This method is called by {@link #get()} when 
the object is accessed for the first time. An implementation can focus on the
      * creation of the object. No synchronization is needed, as this is 
already handled by {@code get()}.
+     * <p>
+     * Subclasses and clients that do not provide an initializer are expected 
to implement this method.
+     * </p>
      *
      * @return the managed data object
      * @throws E if an error occurs during object creation
      */
-    protected abstract T initialize() throws E;
+    @SuppressWarnings("unchecked")
+    protected T initialize() throws E {
+        try {
+            return initializer.get();
+        } catch (final Exception e) {
+            // Do this first so we don't pass a RuntimeException or Error into 
an exception constructor
+            ExceptionUtils.throwUnchecked(e);
+
+            // Depending on the subclass of AbstractConcurrentInitializer E 
can be Exception or ConcurrentException
+            // if E is Exception the if statement below will always be true, 
and the new Exception object created
+            // in getTypedException will never be thrown. If E is 
ConcurrentException and the if statement is false
+            // we throw the ConcurrentException returned from 
getTypedException, which wraps the original exception.
+            final E typedException = getTypedException(e);
+            if (typedException.getClass().isAssignableFrom(e.getClass())) {
+                throw (E) e;
+            }
+            throw typedException;
+        }
+    }
 
     /**
      * Returns true if initialization has been completed. If initialization 
threw an exception this will return false, but it will return true if a 
subsequent
@@ -45,4 +187,12 @@ public abstract class AbstractConcurrentInitializer<T, E 
extends Exception> impl
      */
     protected abstract boolean isInitialized();
 
+    /**
+     * Gets an Exception with a type of E as defined by a concrete subclass of 
this class.
+     *
+     * @param e The actual exception that was thrown
+     * @return a new exception with the actual type of E, that wraps e.
+     */
+    protected abstract E getTypedException(Exception e);
+
 }
diff --git 
a/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java 
b/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java
index 42706f79c..affd8c81d 100644
--- a/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java
+++ b/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java
@@ -18,6 +18,9 @@ package org.apache.commons.lang3.concurrent;
 
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+
 /**
  * A specialized implementation of the {@link ConcurrentInitializer} interface
  * based on an {@link AtomicReference} variable.
@@ -62,13 +65,58 @@ import java.util.concurrent.atomic.AtomicReference;
  * @since 3.0
  * @param <T> the type of the object managed by this initializer class
  */
-public abstract class AtomicInitializer<T> extends 
AbstractConcurrentInitializer<T, RuntimeException> {
+public class AtomicInitializer<T> extends AbstractConcurrentInitializer<T, 
ConcurrentException> {
+
+    /**
+     * Builds a new instance.
+     *
+     * @param <T> the type of the object managed by the initializer.
+     * @param <I> the type of the initializer managed by this builder.
+     * @since 3.14.0
+     */
+    public static class Builder<I extends AtomicInitializer<T>, T> extends 
AbstractBuilder<I, T, Builder<I, T>, ConcurrentException> {
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public I get() {
+            return (I) new AtomicInitializer(getInitializer(), getCloser());
+        }
+
+    }
 
     private static final Object NO_INIT = new Object();
 
     /** Holds the reference to the managed object. */
     private final AtomicReference<T> reference = new 
AtomicReference<>(getNoInit());
 
+    /**
+     * Creates a new builder.
+     *
+     * @param <T> the type of object to build.
+     * @return a new builder.
+     * @since 3.14.0
+     */
+    public static <T> Builder<AtomicInitializer<T>, T> builder() {
+        return new Builder<>();
+    }
+
+    /**
+     * Constructs a new instance.
+     */
+    public AtomicInitializer() {
+        // empty
+    }
+
+    /**
+     * Constructs a new instance.
+     *
+     * @param initializer the initializer supplier called by {@link 
#initialize()}.
+     * @param closer the closer consumer called by {@link #close()}.
+     */
+    private AtomicInitializer(final FailableSupplier<T, ConcurrentException> 
initializer, final FailableConsumer<T, ConcurrentException> closer) {
+        super(initializer, closer);
+    }
+
     /**
      * Returns the object managed by this initializer. The object is created if
      * it is not available yet and stored internally. This method always 
returns
@@ -109,4 +157,12 @@ public abstract class AtomicInitializer<T> extends 
AbstractConcurrentInitializer
     public boolean isInitialized() {
         return reference.get() != NO_INIT;
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected ConcurrentException getTypedException(Exception e) {
+        return new ConcurrentException(e);
+    }
 }
diff --git 
a/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java 
b/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java
index 5e2911b50..7015c53f0 100644
--- 
a/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java
+++ 
b/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java
@@ -18,6 +18,9 @@ package org.apache.commons.lang3.concurrent;
 
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+
 /**
  * A specialized {@link ConcurrentInitializer} implementation which is similar
  * to {@link AtomicInitializer}, but ensures that the {@link #initialize()}
@@ -51,7 +54,24 @@ import java.util.concurrent.atomic.AtomicReference;
  * @since 3.0
  * @param <T> the type of the object managed by this initializer class
  */
-public abstract class AtomicSafeInitializer<T> extends 
AbstractConcurrentInitializer<T, RuntimeException> {
+public class AtomicSafeInitializer<T> extends AbstractConcurrentInitializer<T, 
ConcurrentException> {
+
+    /**
+     * Builds a new instance.
+     *
+     * @param <T> the type of the object managed by the initializer.
+     * @param <I> the type of the initializer managed by this builder.
+     * @since 3.14.0
+     */
+    public static class Builder<I extends AtomicSafeInitializer<T>, T> extends 
AbstractBuilder<I, T, Builder<I, T>, ConcurrentException> {
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public I get() {
+            return (I) new AtomicSafeInitializer(getInitializer(), 
getCloser());
+        }
+
+    }
 
     private static final Object NO_INIT = new Object();
 
@@ -61,6 +81,34 @@ public abstract class AtomicSafeInitializer<T> extends 
AbstractConcurrentInitial
     /** Holds the reference to the managed object. */
     private final AtomicReference<T> reference = new 
AtomicReference<>(getNoInit());
 
+    /**
+     * Creates a new builder.
+     *
+     * @param <T> the type of object to build.
+     * @return a new builder.
+     * @since 3.14.0
+     */
+    public static <T> Builder<AtomicSafeInitializer<T>, T> builder() {
+        return new Builder<>();
+    }
+
+    /**
+     * Constructs a new instance.
+     */
+    public AtomicSafeInitializer() {
+        // empty
+    }
+
+    /**
+     * Constructs a new instance.
+     *
+     * @param initializer the initializer supplier called by {@link 
#initialize()}.
+     * @param closer the closer consumer called by {@link #close()}.
+     */
+    private AtomicSafeInitializer(final FailableSupplier<T, 
ConcurrentException> initializer, final FailableConsumer<T, 
ConcurrentException> closer) {
+        super(initializer, closer);
+    }
+
     /**
      * Gets (and initialize, if not initialized yet) the required object
      *
@@ -97,4 +145,12 @@ public abstract class AtomicSafeInitializer<T> extends 
AbstractConcurrentInitial
     public boolean isInitialized() {
         return reference.get() != NO_INIT;
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected ConcurrentException getTypedException(Exception e) {
+        return new ConcurrentException(e);
+    }
 }
diff --git 
a/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java 
b/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java
index 91ee1c015..dd95ed551 100644
--- 
a/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java
+++ 
b/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java
@@ -23,6 +23,9 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+
 /**
  * A class that allows complex initialization operations in a background task.
  *
@@ -82,7 +85,42 @@ import java.util.concurrent.Future;
  * @since 3.0
  * @param <T> the type of the object managed by this initializer class
  */
-public abstract class BackgroundInitializer<T> extends 
AbstractConcurrentInitializer<T, Exception> {
+public class BackgroundInitializer<T> extends AbstractConcurrentInitializer<T, 
Exception> {
+
+    /**
+     * Builds a new instance.
+     *
+     * @param <T> the type of the object managed by the initializer.
+     * @param <I> the type of the initializer managed by this builder.
+     * @since 3.14.0
+     */
+    public static class Builder<I extends BackgroundInitializer<T>, T> extends 
AbstractBuilder<I, T, Builder<I, T>, Exception> {
+
+        /**
+         * The external executor service for executing tasks. null is an 
permitted value.
+         */
+        private ExecutorService externalExecutor;
+
+        /**
+         * Sets the external executor service for executing tasks. null is an 
permitted value.
+         *
+         * @see 
org.apache.commons.lang3.concurrent.BackgroundInitializer#setExternalExecutor(ExecutorService)
+         *
+         * @param externalExecutor the {@link ExecutorService} to be used.
+         * @return this
+         */
+        public Builder<I, T> setExternalExecutor(final ExecutorService 
externalExecutor) {
+            this.externalExecutor = externalExecutor;
+            return asThis();
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public I get() {
+            return (I) new BackgroundInitializer(getInitializer(), 
getCloser(), externalExecutor);
+        }
+
+    }
 
     /** The external executor service for executing tasks. */
     private ExecutorService externalExecutor; // @GuardedBy("this")
@@ -115,6 +153,29 @@ public abstract class BackgroundInitializer<T> extends 
AbstractConcurrentInitial
         setExternalExecutor(exec);
     }
 
+    /**
+     * Creates a new builder.
+     *
+     * @param <T> the type of object to build.
+     * @return a new builder.
+     * @since 3.14.0
+     */
+    public static <T> Builder<BackgroundInitializer<T>, T> builder() {
+        return new Builder<>();
+    }
+
+    /**
+     * Constructs a new instance.
+     *
+     * @param initializer the initializer supplier called by {@link 
#initialize()}.
+     * @param closer the closer consumer called by {@link #close()}.
+     * @param exec the {@link ExecutorService} to be used @see 
#setExternalExecutor(ExecutorService)
+     */
+    private BackgroundInitializer(final FailableSupplier<T, 
ConcurrentException> initializer, final FailableConsumer<T, 
ConcurrentException> closer, final ExecutorService exec) {
+        super(initializer, closer);
+        setExternalExecutor(exec);
+    }
+
     /**
      * Returns the external {@link ExecutorService} to be used by this class.
      *
@@ -341,4 +402,13 @@ public abstract class BackgroundInitializer<T> extends 
AbstractConcurrentInitial
             }
         }
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Exception getTypedException(Exception e) {
+        //This Exception object will be used for type comparison in 
AbstractConcurrentInitializer.initialize but not thrown
+        return new Exception(e);
+    }
 }
diff --git 
a/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java
 
b/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java
index cccb43d92..81d6efab0 100644
--- 
a/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java
+++ 
b/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java
@@ -119,4 +119,13 @@ public class CallableBackgroundInitializer<T> extends 
BackgroundInitializer<T> {
     private void checkCallable(final Callable<T> callable) {
         Objects.requireNonNull(callable, "callable");
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Exception getTypedException(Exception e) {
+        //This Exception object will be used for type comparison in 
AbstractConcurrentInitializer.initialize but not thrown
+        return new Exception(e);
+    }
 }
diff --git 
a/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java 
b/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java
index 7a69bfd42..acdfead21 100644
--- a/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java
+++ b/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java
@@ -16,29 +16,25 @@
  */
 package org.apache.commons.lang3.concurrent;
 
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+
 /**
- * This class provides a generic implementation of the lazy initialization
- * pattern.
+ * This class provides a generic implementation of the lazy initialization 
pattern.
  *
  * <p>
- * Sometimes an application has to deal with an object only under certain
- * circumstances, e.g. when the user selects a specific menu item or if a
- * special event is received. If the creation of the object is costly or the
- * consumption of memory or other system resources is significant, it may make
- * sense to defer the creation of this object until it is really needed. This 
is
- * a use case for the lazy initialization pattern.
+ * Sometimes an application has to deal with an object only under certain 
circumstances, e.g. when the user selects a specific menu item or if a special 
event
+ * is received. If the creation of the object is costly or the consumption of 
memory or other system resources is significant, it may make sense to defer the
+ * creation of this object until it is really needed. This is a use case for 
the lazy initialization pattern.
  * </p>
  * <p>
- * This abstract base class provides an implementation of the double-check 
idiom
- * for an instance field as discussed in Joshua Bloch's "Effective Java", 2nd
- * edition, item 71. The class already implements all necessary 
synchronization.
- * A concrete subclass has to implement the {@code initialize()} method, which
+ * This abstract base class provides an implementation of the double-check 
idiom for an instance field as discussed in Joshua Bloch's "Effective Java", 2nd
+ * edition, item 71. The class already implements all necessary 
synchronization. A concrete subclass has to implement the {@code initialize()} 
method, which
  * actually creates the wrapped data object.
  * </p>
  * <p>
- * As an usage example consider that we have a class {@code ComplexObject} 
whose
- * instantiation is a complex operation. In order to apply lazy initialization
- * to this class, a subclass of {@link LazyInitializer} has to be created:
+ * As an usage example consider that we have a class {@code ComplexObject} 
whose instantiation is a complex operation. In order to apply lazy 
initialization to
+ * this class, a subclass of {@link LazyInitializer} has to be created:
  * </p>
  *
  * <pre>
@@ -51,9 +47,8 @@ package org.apache.commons.lang3.concurrent;
  * </pre>
  *
  * <p>
- * Access to the data object is provided through the {@code get()} method. So,
- * code that wants to obtain the {@code ComplexObject} instance would simply
- * look like this:
+ * Access to the data object is provided through the {@code get()} method. So, 
code that wants to obtain the {@code ComplexObject} instance would simply look
+ * like this:
  * </p>
  *
  * <pre>
@@ -65,32 +60,75 @@ package org.apache.commons.lang3.concurrent;
  * </pre>
  *
  * <p>
- * If multiple threads call the {@code get()} method when the object has not 
yet
- * been created, they are blocked until initialization completes. The algorithm
- * guarantees that only a single instance of the wrapped object class is
- * created, which is passed to all callers. Once initialized, calls to the
- * {@code get()} method are pretty fast because no synchronization is needed
- * (only an access to a <b>volatile</b> member field).
+ * If multiple threads call the {@code get()} method when the object has not 
yet been created, they are blocked until initialization completes. The algorithm
+ * guarantees that only a single instance of the wrapped object class is 
created, which is passed to all callers. Once initialized, calls to the {@code 
get()}
+ * method are pretty fast because no synchronization is needed (only an access 
to a <b>volatile</b> member field).
  * </p>
  *
  * @since 3.0
- * @param <T> the type of the object managed by this initializer class
+ * @param <T> the type of the object managed by the initializer.
  */
-public abstract class LazyInitializer<T> extends 
AbstractConcurrentInitializer<T, ConcurrentException> {
+public class LazyInitializer<T> extends AbstractConcurrentInitializer<T, 
ConcurrentException> {
+
+    /**
+     * Builds a new instance.
+     *
+     * @param <T> the type of the object managed by the initializer.
+     * @param <I> the type of the initializer managed by this builder.
+     * @since 3.14.0
+     */
+    public static class Builder<I extends LazyInitializer<T>, T> extends 
AbstractBuilder<I, T, Builder<I, T>, ConcurrentException> {
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public I get() {
+            return (I) new LazyInitializer(getInitializer(), getCloser());
+        }
 
+    }
+
+    /**
+     * A unique value indicating an un-initialzed instance.
+     */
     private static final Object NO_INIT = new Object();
 
+    /**
+     * Creates a new builder.
+     *
+     * @param <T> the type of object to build.
+     * @return a new builder.
+     * @since 3.14.0
+     */
+    public static <T> Builder<LazyInitializer<T>, T> builder() {
+        return new Builder<>();
+    }
+
     /** Stores the managed object. */
     @SuppressWarnings("unchecked")
     private volatile T object = (T) NO_INIT;
 
     /**
-     * Returns the object wrapped by this instance. On first access the object
-     * is created. After that it is cached and can be accessed pretty fast.
+     * Constructs a new instance.
+     */
+    public LazyInitializer() {
+        // empty
+    }
+
+    /**
+     * Constructs a new instance.
+     *
+     * @param initializer the initializer supplier called by {@link 
#initialize()}.
+     * @param closer the closer consumer called by {@link #close()}.
+     */
+    private LazyInitializer(final FailableSupplier<T, ConcurrentException> 
initializer, final FailableConsumer<T, ConcurrentException> closer) {
+        super(initializer, closer);
+    }
+
+    /**
+     * Returns the object wrapped by this instance. On first access the object 
is created. After that it is cached and can be accessed pretty fast.
      *
      * @return the object initialized by this {@link LazyInitializer}
-     * @throws ConcurrentException if an error occurred during initialization 
of
-     * the object
+     * @throws ConcurrentException if an error occurred during initialization 
of the object
      */
     @Override
     public T get() throws ConcurrentException {
@@ -121,4 +159,12 @@ public abstract class LazyInitializer<T> extends 
AbstractConcurrentInitializer<T
         return object != NO_INIT;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected ConcurrentException getTypedException(Exception e) {
+        return new ConcurrentException(e);
+    }
+
 }
diff --git 
a/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java
 
b/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java
index f294ef407..1357576d5 100644
--- 
a/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java
+++ 
b/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java
@@ -215,6 +215,39 @@ public class MultiBackgroundInitializer
         return 
childInitializers.values().stream().allMatch(BackgroundInitializer::isInitialized);
     }
 
+    /**
+     * Calls the closer of all child {@code BackgroundInitializer} objects
+     *
+     * @throws ConcurrentException throws an ConcurrentException that will 
have all other exceptions as suppressed exceptions. ConcurrentException thrown 
by children will be unwrapped.
+     * @since 3.14.0
+     */
+    @Override
+    public void close() throws ConcurrentException {
+        ConcurrentException exception = null;
+
+        for (BackgroundInitializer<?> child : childInitializers.values()) {
+            try {
+                child.close();
+            } catch (Exception e) {
+                if (exception == null) {
+                    exception = new ConcurrentException();
+                }
+
+                if (e instanceof ConcurrentException) {
+                    // Because ConcurrentException is only created by classes 
in this package
+                    // we can safely unwrap it.
+                    exception.addSuppressed(e.getCause());
+                } else {
+                    exception.addSuppressed(e);
+                }
+            }
+        }
+
+        if (exception != null) {
+            throw exception;
+        }
+    }
+
     /**
      * A data class for storing the results of the background initialization
      * performed by {@link MultiBackgroundInitializer}. Objects of this inner
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerCloseAndExceptionsTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerCloseAndExceptionsTest.java
new file mode 100644
index 000000000..af77f3c61
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerCloseAndExceptionsTest.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.lang3.concurrent;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.sql.SQLException;
+
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+import org.junit.jupiter.api.Test;
+
+/**
+ * An abstract base class for tests of exceptions thrown during initialize and 
close methods
+ * on concrete {@code ConcurrentInitializer} implementations.
+ *
+ * This class provides some basic tests for initializer implementations. 
Derived
+ * class have to create a {@link ConcurrentInitializer} object on which the
+ * tests are executed.
+ */
+public abstract class AbstractConcurrentInitializerCloseAndExceptionsTest 
extends AbstractConcurrentInitializerTest {
+
+    /**
+     * This method tests that if a AbstractConcurrentInitializer.initialize 
method catches a
+     * ConcurrentException it will rethrow it without wrapping it.
+     */
+    @Test
+    public void testSupplierThrowsConcurrentException() {
+        final ConcurrentException concurrentException = new 
ConcurrentException();
+
+        @SuppressWarnings("unchecked")
+        final ConcurrentInitializer<CloseableObject> initializer = 
createInitializerThatThrowsException(
+                () -> {
+                    if ("test".equals("test")) {
+                        throw concurrentException;
+                    }
+                    return new CloseableObject();
+                },
+                FailableConsumer.NOP);
+        try {
+            initializer.get();
+            fail();
+        } catch (ConcurrentException e) {
+            assertEquals(concurrentException, e);
+        }
+    }
+
+    /**
+     * This method tests that if AbstractConcurrentInitializer.initialize 
catches a checked
+     * exception it will rethrow it wrapped in a ConcurrentException
+     */
+    @SuppressWarnings("unchecked") //for NOP
+    @Test
+    public void testSupplierThrowsCheckedException() {
+        final ConcurrentInitializer<CloseableObject> initializer = 
createInitializerThatThrowsException(
+                () -> methodThatThrowsException(ExceptionToThrow.IOException),
+                FailableConsumer.NOP);
+        assertThrows(ConcurrentException.class, () -> initializer.get());
+    }
+
+    /**
+     * This method tests that if AbstractConcurrentInitializer.initialize 
catches a runtime exception
+     * it will not be wrapped in a ConcurrentException
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testSupplierThrowsRuntimeException() {
+        final ConcurrentInitializer<CloseableObject> initializer = 
createInitializerThatThrowsException(
+                () -> 
methodThatThrowsException(ExceptionToThrow.NullPointerException),
+                FailableConsumer.NOP);
+        assertThrows(NullPointerException.class, () -> initializer.get());
+    }
+
+    /**
+     * This method tests that if AbstractConcurrentInitializer.close catches a
+     * ConcurrentException it will rethrow it wrapped in a ConcurrentException
+     */
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testCloserThrowsCheckedException() throws ConcurrentException {
+        final ConcurrentInitializer<CloseableObject> initializer = 
createInitializerThatThrowsException(
+                CloseableObject::new,
+                (CloseableObject) -> 
methodThatThrowsException(ExceptionToThrow.IOException));
+        try {
+            initializer.get();
+            ((AbstractConcurrentInitializer) initializer).close();
+            fail();
+        } catch (Exception e) {
+            assertThat(e, instanceOf(ConcurrentException.class));
+            assertThat(e.getCause(), instanceOf(IOException.class));
+        }
+    }
+
+    /**
+     * This method tests that if AbstractConcurrentInitializer.close catches a
+     * RuntimeException it will throw it withuot wrapping it in a 
ConcurrentException
+     */
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testCloserThrowsRuntimeException() throws ConcurrentException {
+        final ConcurrentInitializer<CloseableObject> initializer = 
createInitializerThatThrowsException(
+                CloseableObject::new,
+                (CloseableObject) -> 
methodThatThrowsException(ExceptionToThrow.NullPointerException));
+
+        initializer.get();
+        assertThrows(NullPointerException.class, () -> {
+            ((AbstractConcurrentInitializer) initializer).close();
+            });
+    }
+
+    /**
+     * This method tests that if AbstractConcurrentInitializer.close actually 
closes the wrapped object
+     */
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testWorkingCloser() throws Exception {
+        final ConcurrentInitializer<CloseableObject> initializer = 
createInitializerThatThrowsException(
+                CloseableObject::new,
+                CloseableObject::close);
+
+        CloseableObject cloesableObject = initializer.get();
+        assertFalse(cloesableObject.isClosed());
+        ((AbstractConcurrentInitializer) initializer).close();
+        assertTrue(cloesableObject.isClosed());
+    }
+
+    protected enum ExceptionToThrow {
+        IOException,
+        SQLException,
+        NullPointerException
+    }
+
+    // The use of enums rather than accepting an Exception as the input means 
we can have
+    // multiple exception types on the method signature.
+    protected static CloseableObject 
methodThatThrowsException(ExceptionToThrow input) throws IOException, 
SQLException, ConcurrentException {
+        switch (input) {
+        case IOException:
+            throw new IOException();
+        case SQLException:
+            throw new SQLException();
+        case NullPointerException:
+            throw new NullPointerException();
+        default:
+            fail();
+            return new CloseableObject();
+        }
+    }
+
+    protected abstract ConcurrentInitializer<CloseableObject> 
createInitializerThatThrowsException(
+            FailableSupplier<CloseableObject, ? extends Exception> supplier, 
FailableConsumer<CloseableObject, ? extends Exception> closer);
+
+    protected static final class CloseableObject {
+        boolean closed;
+
+        public boolean isClosed() {
+            return closed;
+        }
+
+        public void close() {
+            closed = true;
+        }
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerSupplierTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerSupplierTest.java
new file mode 100644
index 000000000..41413ff84
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerSupplierTest.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.lang3.concurrent;
+
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+
+/**
+ * Test class for {@code AtomicInitializer}.
+ */
+public class AtomicInitializerSupplierTest extends 
AbstractConcurrentInitializerCloseAndExceptionsTest {
+    /**
+     * Returns the initializer to be tested.
+     *
+     * @return the {@code AtomicInitializer}
+     */
+    @Override
+    protected ConcurrentInitializer<Object> createInitializer() {
+        return AtomicInitializer.<Object>builder().setInitializer(() -> new 
Object()).get();
+    }
+
+    @Override
+    protected ConcurrentInitializer<CloseableObject> 
createInitializerThatThrowsException(
+            final FailableSupplier<CloseableObject, ? extends Exception> 
supplier,
+            final FailableConsumer<CloseableObject, ? extends Exception> 
closer) {
+        return 
AtomicInitializer.<CloseableObject>builder().setInitializer(supplier).setCloser(closer).get();
+    }
+
+}
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerSupplierTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerSupplierTest.java
new file mode 100644
index 000000000..9bbfde19b
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerSupplierTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import 
org.apache.commons.lang3.concurrent.AbstractConcurrentInitializerCloseAndExceptionsTest.CloseableObject;
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@code AtomicSafeInitializer} which also serves as a simple 
example.
+ */
+public class AtomicSafeInitializerSupplierTest extends 
AbstractConcurrentInitializerCloseAndExceptionsTest {
+
+    /** An initCounter used in testing. Reset before each test */
+    private AtomicInteger initCounter = new AtomicInteger();
+
+    /** A supplier method used in testing */
+    private Object incAndMakeObject() {
+        initCounter.incrementAndGet();
+        return new Object();
+    }
+
+    @Override
+    protected ConcurrentInitializer<CloseableObject> 
createInitializerThatThrowsException(
+            final FailableSupplier<CloseableObject, ? extends Exception> 
supplier,
+            final FailableConsumer<CloseableObject, ? extends Exception> 
closer) {
+        return 
AtomicSafeInitializer.<CloseableObject>builder().setInitializer(supplier).setCloser(closer).get();
+    }
+
+    @BeforeEach
+    public void setUp() {
+        initCounter = new AtomicInteger();
+    }
+
+    /**
+     * Creates the initializer to be tested.
+     *
+     * @return the {@code AtomicSafeInitializer} under test
+     */
+    @Override
+    protected ConcurrentInitializer<Object> createInitializer() {
+        return 
AtomicSafeInitializer.<Object>builder().setInitializer(this::incAndMakeObject).get();
+    }
+
+    /**
+     * Tests that initialize() is called only once.
+     *
+     * @throws org.apache.commons.lang3.concurrent.ConcurrentException because 
{@link #testGetConcurrent()} may throw it
+     * @throws InterruptedException because {@link #testGetConcurrent()} may 
throw it
+     */
+    @Test
+    public void testNumberOfInitializeInvocations() throws 
ConcurrentException, InterruptedException {
+        testGetConcurrent();
+        assertEquals(1, initCounter.get(), "Wrong number of invocations");
+    }
+
+    @Test
+    public void testGetThatReturnsNullFirstTime() throws ConcurrentException {
+        final AtomicSafeInitializer<Object> initializer = new 
AtomicSafeInitializer<Object>() {
+            final AtomicBoolean firstRun = new AtomicBoolean(true);
+
+            @Override
+            protected Object initialize() {
+                if (firstRun.getAndSet(false)) {
+                    return null;
+                } else {
+                    return new Object();
+                }
+            }
+        };
+
+        assertNull(initializer.get());
+        assertNull(initializer.get());
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerSupplierTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerSupplierTest.java
new file mode 100644
index 000000000..3a6b1da1a
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerSupplierTest.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.lang3.concurrent;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+import org.junit.jupiter.api.Test;
+
+public class BackgroundInitializerSupplierTest extends 
BackgroundInitializerTest {
+
+    /**
+     * Tests that close() method closes the wrapped object
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testClose() throws Exception {
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
+        assertFalse(init.getCloseableCounter().isClosed(), "closed without 
close() call");
+        init.close();
+        assertFalse(init.getCloseableCounter().isClosed(), "closed() succeeded 
before start()");
+        init.start();
+        init.get(); //ensure the Future has completed.
+        assertFalse(init.getCloseableCounter().isClosed(), "closed() succeeded 
after start() but before close()");
+        init.close();
+        assertTrue(init.getCloseableCounter().isClosed(), "closed() did not 
succeed");
+    }
+
+    /**
+     * Tests that close() wraps a checked exception in a ConcurrentException
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testCloseWithCheckedException() throws Exception {
+
+        final IOException ioException = new IOException();
+        final FailableConsumer<?, ?> IOExceptionConsumer = (CloseableCounter 
cc) -> {
+            throw ioException;
+        };
+
+        final AbstractBackgroundInitializerTestImpl init = new 
SupplierBackgroundInitializerTestImpl(IOExceptionConsumer);
+        init.start();
+        init.get(); //ensure the Future has completed.
+        try {
+            init.close();
+            fail();
+        } catch (Exception e) {
+            assertThat(e, instanceOf(ConcurrentException.class));
+            assertSame(ioException, e.getCause());
+        }
+    }
+
+    /**
+     * Tests that close() throws a runtime exception
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testCloseWithRuntimeException() throws Exception {
+
+        final NullPointerException npe = new NullPointerException();
+        final FailableConsumer<?, ?> NullPointerExceptionConsumer = 
(CloseableCounter cc) -> {
+            throw npe;
+        };
+
+        final AbstractBackgroundInitializerTestImpl init = new 
SupplierBackgroundInitializerTestImpl(NullPointerExceptionConsumer);
+        init.start();
+        init.get(); //ensure the Future has completed.
+        try {
+            init.close();
+            fail();
+        } catch (Exception e) {
+            assertSame(npe, e);
+        }
+    }
+
+    /**
+     * A concrete implementation of BackgroundInitializer. It is designed as a 
warpper so the test can
+     * use the same builder pattern that real code will.
+     */
+    protected static final class SupplierBackgroundInitializerTestImpl extends 
AbstractBackgroundInitializerTestImpl {
+
+        SupplierBackgroundInitializerTestImpl() {
+            super();
+            setSupplierAndCloser((CloseableCounter cc) -> cc.close());
+        }
+
+        SupplierBackgroundInitializerTestImpl(FailableConsumer<?, ?> consumer) 
{
+            super();
+            setSupplierAndCloser(consumer);
+        }
+
+        SupplierBackgroundInitializerTestImpl(final ExecutorService exec) {
+            super(exec);
+            setSupplierAndCloser((CloseableCounter cc) -> cc.close());
+        }
+
+        private void setSupplierAndCloser(FailableConsumer<?, ?> consumer) {
+            try {
+                // Use reflection here because the constructors we need are 
private
+                FailableSupplier<?, ?> supplier = () -> initializeInternal();
+                Field initializer = 
AbstractConcurrentInitializer.class.getDeclaredField("initializer");
+                initializer.setAccessible(true);
+                initializer.set(this, supplier);
+
+                Field closer = 
AbstractConcurrentInitializer.class.getDeclaredField("closer");
+                closer.setAccessible(true);
+                closer.set(this, consumer);
+            } catch (NoSuchFieldException | SecurityException | 
IllegalArgumentException | IllegalAccessException e) {
+                fail();
+            }
+        }
+    }
+
+    protected AbstractBackgroundInitializerTestImpl 
getBackgroundInitializerTestImpl() {
+        return new SupplierBackgroundInitializerTestImpl();
+    }
+
+    protected SupplierBackgroundInitializerTestImpl 
getBackgroundInitializerTestImpl(final ExecutorService exec) {
+        return new SupplierBackgroundInitializerTestImpl(exec);
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java
index 90193f2af..8714beaaf 100644
--- 
a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java
@@ -29,6 +29,8 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.commons.lang3.AbstractLangTest;
@@ -42,10 +44,10 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      *
      * @param init the initializer to test
      */
-    private void checkInitialize(final BackgroundInitializerTestImpl init) 
throws ConcurrentException {
-        final Integer result = init.get();
+    private void checkInitialize(final AbstractBackgroundInitializerTestImpl 
init) throws ConcurrentException {
+        final Integer result = init.get().getInitializeCalls();
         assertEquals(1, result.intValue(), "Wrong result");
-        assertEquals(1, init.initializeCalls, "Wrong number of invocations");
+        assertEquals(1, init.getCloseableCounter().getInitializeCalls(), 
"Wrong number of invocations");
         assertNotNull(init.getFuture(), "No future");
     }
 
@@ -54,7 +56,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testInitialize() throws ConcurrentException {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         init.start();
         checkInitialize(init);
     }
@@ -65,7 +67,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testGetActiveExecutorBeforeStart() {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         assertNull(init.getActiveExecutor(), "Got an executor");
     }
 
@@ -76,7 +78,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
     public void testGetActiveExecutorExternal() throws InterruptedException, 
ConcurrentException {
         final ExecutorService exec = Executors.newSingleThreadExecutor();
         try {
-            final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl(
+            final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl(
                     exec);
             init.start();
             assertSame(exec, init.getActiveExecutor(), "Wrong executor");
@@ -92,7 +94,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testGetActiveExecutorTemp() throws ConcurrentException {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         init.start();
         assertNotNull(init.getActiveExecutor(), "No active executor");
         checkInitialize(init);
@@ -104,7 +106,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testInitializeTempExecutor() throws ConcurrentException {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         assertTrue(init.start(), "Wrong result of start()");
         checkInitialize(init);
         assertTrue(init.getActiveExecutor().isShutdown(), "Executor not 
shutdown");
@@ -118,7 +120,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
     public void testSetExternalExecutor() throws ConcurrentException {
         final ExecutorService exec = Executors.newCachedThreadPool();
         try {
-            final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+            final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
             init.setExternalExecutor(exec);
             assertEquals(exec, init.getExternalExecutor(), "Wrong executor 
service");
             assertTrue(init.start(), "Wrong result of start()");
@@ -137,7 +139,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testSetExternalExecutorAfterStart() throws 
ConcurrentException, InterruptedException {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         init.start();
         final ExecutorService exec = Executors.newSingleThreadExecutor();
         try {
@@ -155,7 +157,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testStartMultipleTimes() throws ConcurrentException {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         assertTrue(init.start(), "Wrong result for start()");
         for (int i = 0; i < 10; i++) {
             assertFalse(init.start(), "Could start again");
@@ -168,7 +170,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testGetBeforeStart() {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         assertThrows(IllegalStateException.class, init::get);
     }
 
@@ -178,7 +180,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testGetRuntimeException() {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         final RuntimeException rex = new RuntimeException();
         init.ex = rex;
         init.start();
@@ -192,7 +194,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testGetCheckedException() {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         final Exception ex = new Exception();
         init.ex = ex;
         init.start();
@@ -208,7 +210,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
     @Test
     public void testGetInterruptedException() throws InterruptedException {
         final ExecutorService exec = Executors.newSingleThreadExecutor();
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl(
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl(
                 exec);
         final CountDownLatch latch1 = new CountDownLatch(1);
         init.shouldSleep = true;
@@ -242,7 +244,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testIsStartedFalse() {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         assertFalse(init.isStarted(), "Already started");
     }
 
@@ -251,7 +253,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testIsStartedTrue() {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         init.start();
         assertTrue(init.isStarted(), "Not started");
     }
@@ -261,7 +263,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testIsStartedAfterGet() throws ConcurrentException {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         init.start();
         checkInitialize(init);
         assertTrue(init.isStarted(), "Not started");
@@ -272,7 +274,7 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testIsInitialized() throws ConcurrentException {
-        final BackgroundInitializerTestImpl init = new 
BackgroundInitializerTestImpl();
+        final AbstractBackgroundInitializerTestImpl init = 
getBackgroundInitializerTestImpl();
         init.enableLatch();
         init.start();
         assertTrue(init.isStarted(), "Not started"); //Started and Initialized 
should return opposite values
@@ -282,29 +284,37 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
         assertTrue(init.isInitialized(), "Not initalized after releasing 
latch");
     }
 
+    protected AbstractBackgroundInitializerTestImpl 
getBackgroundInitializerTestImpl() {
+        return new MethodBackgroundInitializerTestImpl();
+    }
+
+    protected AbstractBackgroundInitializerTestImpl 
getBackgroundInitializerTestImpl(final ExecutorService exec) {
+        return new MethodBackgroundInitializerTestImpl(exec);
+    }
+
     /**
      * A concrete implementation of BackgroundInitializer. It also overloads
      * some methods that simplify testing.
      */
-    private static final class BackgroundInitializerTestImpl extends
-            BackgroundInitializer<Integer> {
+    protected static class AbstractBackgroundInitializerTestImpl extends
+            BackgroundInitializer<CloseableCounter> {
         /** An exception to be thrown by initialize(). */
         Exception ex;
 
         /** A flag whether the background task should sleep a while. */
         boolean shouldSleep;
 
-        /** The number of invocations of initialize(). */
-        volatile int initializeCalls;
-
         /** A latch tests can use to control when initialize completes. */
         final CountDownLatch latch = new CountDownLatch(1);
         boolean waitForLatch = false;
 
-        BackgroundInitializerTestImpl() {
+        /** An object containing the state we are testing */
+        CloseableCounter counter = new CloseableCounter();
+
+        AbstractBackgroundInitializerTestImpl() {
         }
 
-        BackgroundInitializerTestImpl(final ExecutorService exec) {
+        AbstractBackgroundInitializerTestImpl(final ExecutorService exec) {
             super(exec);
         }
 
@@ -316,14 +326,17 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
             latch.countDown();
         }
 
+        public CloseableCounter getCloseableCounter() {
+            return counter;
+        }
+
         /**
          * Records this invocation. Optionally throws an exception or sleeps a
          * while.
          *
          * @throws Exception in case of an error
          */
-        @Override
-        protected Integer initialize() throws Exception {
+        protected CloseableCounter initializeInternal() throws Exception {
             if (ex != null) {
                 throw ex;
             }
@@ -333,7 +346,47 @@ public class BackgroundInitializerTest extends 
AbstractLangTest {
             if (waitForLatch) {
                 latch.await();
             }
-            return Integer.valueOf(++initializeCalls);
+            return counter.increment();
+        }
+    }
+
+    protected static class MethodBackgroundInitializerTestImpl extends 
AbstractBackgroundInitializerTestImpl {
+
+        MethodBackgroundInitializerTestImpl() {
+        }
+
+        MethodBackgroundInitializerTestImpl(final ExecutorService exec) {
+            super(exec);
+        }
+
+        @Override
+        protected CloseableCounter initialize() throws Exception {
+            return initializeInternal();
+        }
+    }
+
+    protected static class CloseableCounter {
+        /** The number of invocations of initialize(). */
+        AtomicInteger initializeCalls = new AtomicInteger();
+
+        /** Has the close consumer successfully reached this object. */
+        AtomicBoolean closed = new AtomicBoolean();
+
+        public CloseableCounter increment() {
+            initializeCalls.incrementAndGet();
+            return this;
+        }
+
+        public int getInitializeCalls() {
+            return initializeCalls.get();
+        }
+
+        public void close() {
+            closed.set(true);
+        }
+
+        public boolean isClosed() {
+            return closed.get();
         }
     }
 }
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerCloserTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerCloserTest.java
new file mode 100644
index 000000000..3bdaf0c77
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerCloserTest.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@code LazyInitializer}.
+ */
+public class LazyInitializerCloserTest extends 
AbstractConcurrentInitializerTest {
+
+    private final AtomicBoolean closed = new AtomicBoolean();
+
+    /**
+     * Creates the initializer to be tested. This implementation returns the 
{@code LazyInitializer} created in the {@code setUp()} method.
+     *
+     * @return the initializer to be tested
+     */
+    @Override
+    protected LazyInitializer<Object> createInitializer() {
+        return 
LazyInitializer.builder().setInitializer(Object::new).setCloser(e -> 
closed.set(true)).get();
+    }
+
+    @Test
+    public void testIsInitialized() throws ConcurrentException {
+        final LazyInitializer<Object> initializer = createInitializer();
+        assertFalse(initializer.isInitialized());
+        initializer.get();
+        assertTrue(initializer.isInitialized());
+        assertFalse(closed.get());
+        initializer.close();
+        assertTrue(closed.get());
+    }
+
+}
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerFailableCloserTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerFailableCloserTest.java
new file mode 100644
index 000000000..c1e6137fc
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerFailableCloserTest.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.lang3.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@code LazyInitializer}.
+ */
+public class LazyInitializerFailableCloserTest extends 
AbstractConcurrentInitializerTest {
+
+    private final AtomicBoolean closed = new AtomicBoolean();
+
+    /**
+     * Creates the initializer to be tested. This implementation returns the 
{@code LazyInitializer} created in the {@code setUp()} method.
+     *
+     * @return the initializer to be tested
+     */
+    @Override
+    protected LazyInitializer<Object> createInitializer() {
+        return 
LazyInitializer.builder().setInitializer(this::makeObject).setCloser(e -> 
throwingCloser()).get();
+    }
+
+    private Object makeObject() throws ConcurrentException {
+        if (closed.get()) {
+            // Doesn't actually throw, just for the method sig without unused 
warning.
+            throw new ConcurrentException("test", new IOException());
+        }
+        return new Object();
+    }
+
+    @Test
+    public void testIsInitialized() throws ConcurrentException {
+        final LazyInitializer<Object> initializer = createInitializer();
+        assertFalse(initializer.isInitialized());
+        initializer.get();
+        assertTrue(initializer.isInitialized());
+        assertFalse(closed.get());
+        initializer.close();
+        assertTrue(closed.get());
+    }
+
+    private void throwingCloser() throws ConcurrentException {
+        closed.set(true);
+        // always false:
+        if (!closed.get()) {
+            // Doesn't actually throw, just for the method sig without unused 
warning.
+            throw new ConcurrentException("test", new IOException());
+        }
+    }
+
+}
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerSupplierTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerSupplierTest.java
new file mode 100644
index 000000000..0ac26a4bf
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerSupplierTest.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.lang3.concurrent;
+
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+
+/**
+ * Tests {@code LazyInitializer}.
+ */
+public class LazyInitializerSupplierTest extends 
AbstractConcurrentInitializerCloseAndExceptionsTest {
+
+    /**
+     * Creates the initializer to be tested.
+     *
+     * @return the initializer to be tested
+     */
+    @Override
+    protected ConcurrentInitializer<Object> createInitializer() {
+        return LazyInitializer.<Object>builder().setInitializer(() -> new 
Object()).get();
+    }
+
+    @Override
+    protected ConcurrentInitializer<CloseableObject> 
createInitializerThatThrowsException(
+            final FailableSupplier<CloseableObject, ? extends Exception> 
supplier,
+            final FailableConsumer<CloseableObject, ? extends Exception> 
closer) {
+        return 
LazyInitializer.<CloseableObject>builder().setInitializer(supplier).setCloser(closer).get();
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerSupplierTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerSupplierTest.java
new file mode 100644
index 000000000..beca37ed5
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerSupplierTest.java
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.lang3.concurrent;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@link MultiBackgroundInitializer}.
+ */
+public class MultiBackgroundInitializerSupplierTest extends 
MultiBackgroundInitializerTest {
+
+    private NullPointerException npe;
+    private IOException ioException;
+    private FailableConsumer<?, ?> ioExceptionConsumer;
+    private FailableConsumer<?, ?> nullPointerExceptionConsumer;
+
+    @BeforeEach
+    public void setUpException() throws Exception {
+        npe = new NullPointerException();
+        ioException = new IOException();
+        ioExceptionConsumer = (CloseableCounter cc) -> {
+            throw ioException;
+        };
+        nullPointerExceptionConsumer = (CloseableCounter cc) -> {
+            throw npe;
+        };
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected AbstractChildBackgroundInitializer 
createChildBackgroundInitializer() {
+        return new SupplierChildBackgroundInitializer();
+    }
+
+    /**
+     * Tests that close() method closes the wrapped object
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testClose()
+            throws ConcurrentException, InterruptedException {
+        final AbstractChildBackgroundInitializer childOne = 
createChildBackgroundInitializer();
+        final AbstractChildBackgroundInitializer childTwo = 
createChildBackgroundInitializer();
+
+        assertFalse(initializer.isInitialized(), "Initalized without having 
anything to initalize");
+
+        initializer.addInitializer("child one", childOne);
+        initializer.addInitializer("child two", childTwo);
+
+        assertFalse(childOne.getCloseableCounter().isClosed(), "child one 
closed() succeeded before start()");
+        assertFalse(childTwo.getCloseableCounter().isClosed(), "child two 
closed() succeeded before start()");
+
+        initializer.start();
+
+        long startTime = System.currentTimeMillis();
+        long waitTime = 3000;
+        long endTime = startTime + waitTime;
+        //wait for the children to start
+        while (!childOne.isStarted() || !childTwo.isStarted()) {
+            if (System.currentTimeMillis() > endTime) {
+                fail("children never started");
+                Thread.sleep(PERIOD_MILLIS);
+            }
+        }
+
+        assertFalse(childOne.getCloseableCounter().isClosed(), "child one 
close() succeeded after start() but before close()");
+        assertFalse(childTwo.getCloseableCounter().isClosed(), "child two 
close() succeeded after start() but before close()");
+
+        childOne.get(); // ensure this child finishes initializing
+        childTwo.get(); // ensure this child finishes initializing
+
+        assertFalse(childOne.getCloseableCounter().isClosed(), "child one 
initializing succeeded after start() but before close()");
+        assertFalse(childTwo.getCloseableCounter().isClosed(), "child two 
initializing succeeded after start() but before close()");
+
+        try {
+            initializer.close();
+        } catch (Exception e) {
+            fail();
+        }
+
+        assertTrue(childOne.getCloseableCounter().isClosed(), "child one 
close() did not succeed");
+        assertTrue(childOne.getCloseableCounter().isClosed(), "child two 
close() did not succeed");
+    }
+
+    /**
+     * Tests that close() wraps a checked exception from a child initializer 
in an ConcurrentException as the first suppressed under in an 
ConcurrentException
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testCloseWithCheckedException() throws Exception {
+        final AbstractChildBackgroundInitializer childOne = new 
SupplierChildBackgroundInitializer(ioExceptionConsumer);
+
+        initializer.addInitializer("child one", childOne);
+        initializer.start();
+
+        long startTime = System.currentTimeMillis();
+        long waitTime = 3000;
+        long endTime = startTime + waitTime;
+        //wait for the children to start
+        while (! childOne.isStarted()) {
+            if (System.currentTimeMillis() > endTime) {
+                fail("children never started");
+                Thread.sleep(PERIOD_MILLIS);
+            }
+        }
+
+        childOne.get(); // ensure the Future has completed.
+        try {
+            initializer.close();
+            fail();
+        } catch (Exception e) {
+            assertThat(e, instanceOf(ConcurrentException.class));
+            assertSame(ioException, e.getSuppressed()[0]);
+        }
+    }
+
+    /**
+     * Tests that close() wraps a runtime exception from a child initializer 
as the first suppressed under in an ConcurrentException
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testCloseWithRuntimeException() throws Exception {
+        final AbstractChildBackgroundInitializer childOne = new 
SupplierChildBackgroundInitializer(nullPointerExceptionConsumer);
+
+        initializer.addInitializer("child one", childOne);
+        initializer.start();
+
+        long startTime = System.currentTimeMillis();
+        long waitTime = 3000;
+        long endTime = startTime + waitTime;
+        //wait for the children to start
+        while (! childOne.isStarted()) {
+            if (System.currentTimeMillis() > endTime) {
+                fail("children never started");
+                Thread.sleep(PERIOD_MILLIS);
+            }
+        }
+
+        childOne.get(); // ensure the Future has completed.
+        try {
+            initializer.close();
+            fail();
+        } catch (Exception e) {
+            assertThat(e, instanceOf(ConcurrentException.class));
+            assertSame(npe, e.getSuppressed()[0]);
+        }
+    }
+
+    /**
+     * Tests that calling close() on a MultiBackgroundInitializer with two 
children that both throw exceptions throws
+     * an ConcurrentException and both the child exceptions are present
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testCloseWithTwoExceptions()
+            throws ConcurrentException, InterruptedException {
+
+        final AbstractChildBackgroundInitializer childOne = new 
SupplierChildBackgroundInitializer(ioExceptionConsumer);
+        final AbstractChildBackgroundInitializer childTwo = new 
SupplierChildBackgroundInitializer(nullPointerExceptionConsumer);
+
+        initializer.addInitializer("child one", childOne);
+        initializer.addInitializer("child two", childTwo);
+
+        initializer.start();
+
+        final long startTime = System.currentTimeMillis();
+        final long waitTime = 3000;
+        final long endTime = startTime + waitTime;
+        //wait for the children to start
+        while (! childOne.isStarted() || ! childTwo.isStarted()) {
+            if (System.currentTimeMillis() > endTime) {
+                fail("children never started");
+                Thread.sleep(PERIOD_MILLIS);
+            }
+        }
+
+        childOne.get(); // ensure this child finishes initializing
+        childTwo.get(); // ensure this child finishes initializing
+
+        try {
+            initializer.close();
+            fail();
+        } catch (Exception e) {
+            // We don't actually know which order the children will be closed 
in
+            boolean foundChildOneException = false;
+            boolean foundChildTwoException = false;
+
+            for (Throwable t : e.getSuppressed()) {
+                if (t.equals(ioException)) {
+                    foundChildOneException = true;
+                }
+                if (t.equals(npe)) {
+                    foundChildTwoException = true;
+                }
+            }
+
+            assertTrue(foundChildOneException);
+            assertTrue(foundChildTwoException);
+        }
+    }
+
+    /**
+     * A concrete implementation of {@code BackgroundInitializer} used for
+     * defining background tasks for {@code MultiBackgroundInitializer}.
+     */
+    private static final class SupplierChildBackgroundInitializer extends 
AbstractChildBackgroundInitializer {
+
+        SupplierChildBackgroundInitializer() {
+            this((CloseableCounter cc) -> cc.close());
+        }
+
+        SupplierChildBackgroundInitializer(FailableConsumer<?, ?> consumer) {
+            try {
+                // Use reflection here because the constructors we need are 
private
+                final FailableSupplier<?, ?> supplier = () -> 
initializeInternal();
+                final Field initializer = 
AbstractConcurrentInitializer.class.getDeclaredField("initializer");
+                initializer.setAccessible(true);
+                initializer.set(this, supplier);
+
+                final Field closer = 
AbstractConcurrentInitializer.class.getDeclaredField("closer");
+                closer.setAccessible(true);
+                closer.set(this, consumer);
+            } catch (NoSuchFieldException | SecurityException | 
IllegalArgumentException | IllegalAccessException e) {
+                fail();
+            }
+        }
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java
 
b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java
index f861e0302..4260f9b61 100644
--- 
a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java
+++ 
b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java
@@ -42,10 +42,10 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
     private static final String CHILD_INIT = "childInitializer";
 
     /** The initializer to be tested. */
-    private MultiBackgroundInitializer initializer;
+    protected MultiBackgroundInitializer initializer;
 
     /** A short time to wait for background threads to run. */
-    private static final long PERIOD_MILLIS = 50;
+    protected static final long PERIOD_MILLIS = 50;
 
     @BeforeEach
     public void setUp() {
@@ -63,8 +63,8 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
      */
     private void checkChild(final BackgroundInitializer<?> child,
             final ExecutorService expExec) throws ConcurrentException {
-        final ChildBackgroundInitializer cinit = (ChildBackgroundInitializer) 
child;
-        final Integer result = cinit.get();
+        final AbstractChildBackgroundInitializer cinit = 
(AbstractChildBackgroundInitializer) child;
+        final Integer result = cinit.get().getInitializeCalls();
         assertEquals(1, result.intValue(), "Wrong result");
         assertEquals(1, cinit.initializeCalls, "Wrong number of executions");
         if (expExec != null) {
@@ -78,7 +78,7 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testAddInitializerNullName() {
-        assertThrows(NullPointerException.class, () -> 
initializer.addInitializer(null, new ChildBackgroundInitializer()));
+        assertThrows(NullPointerException.class, () -> 
initializer.addInitializer(null, createChildBackgroundInitializer()));
     }
 
     /**
@@ -117,7 +117,7 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
         final int count = 5;
         for (int i = 0; i < count; i++) {
             initializer.addInitializer(CHILD_INIT + i,
-                    new ChildBackgroundInitializer());
+                    createChildBackgroundInitializer());
         }
         initializer.start();
         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res 
= initializer
@@ -126,7 +126,7 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
         for (int i = 0; i < count; i++) {
             final String key = CHILD_INIT + i;
             assertTrue(res.initializerNames().contains(key), "Name not found: 
" + key);
-            assertEquals(Integer.valueOf(1), res.getResultObject(key), "Wrong 
result object");
+            assertEquals(CloseableCounter.wrapInteger(1), 
res.getResultObject(key), "Wrong result object");
             assertFalse(res.isException(key), "Exception flag");
             assertNull(res.getException(key), "Got an exception");
             checkChild(res.getInitializer(key), 
initializer.getActiveExecutor());
@@ -175,8 +175,8 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
         final String initExec = "childInitializerWithExecutor";
         final ExecutorService exec = Executors.newSingleThreadExecutor();
         try {
-            final ChildBackgroundInitializer c1 = new 
ChildBackgroundInitializer();
-            final ChildBackgroundInitializer c2 = new 
ChildBackgroundInitializer();
+            final AbstractChildBackgroundInitializer c1 = 
createChildBackgroundInitializer();
+            final AbstractChildBackgroundInitializer c2 = 
createChildBackgroundInitializer();
             c2.setExternalExecutor(exec);
             initializer.addInitializer(CHILD_INIT, c1);
             initializer.addInitializer(initExec, c2);
@@ -201,7 +201,7 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
         initializer.start();
         assertThrows(
                 IllegalStateException.class,
-                () -> initializer.addInitializer(CHILD_INIT, new 
ChildBackgroundInitializer()),
+                () -> initializer.addInitializer(CHILD_INIT, 
createChildBackgroundInitializer()),
                 "Could add initializer after start()!");
         initializer.get();
     }
@@ -275,7 +275,7 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testInitializeRuntimeEx() {
-        final ChildBackgroundInitializer child = new 
ChildBackgroundInitializer();
+        final AbstractChildBackgroundInitializer child = 
createChildBackgroundInitializer();
         child.ex = new RuntimeException();
         initializer.addInitializer(CHILD_INIT, child);
         initializer.start();
@@ -291,7 +291,7 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
      */
     @Test
     public void testInitializeEx() throws ConcurrentException {
-        final ChildBackgroundInitializer child = new 
ChildBackgroundInitializer();
+        final AbstractChildBackgroundInitializer child = 
createChildBackgroundInitializer();
         child.ex = new Exception();
         initializer.addInitializer(CHILD_INIT, child);
         initializer.start();
@@ -312,7 +312,7 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
     @Test
     public void testInitializeResultsIsSuccessfulTrue()
             throws ConcurrentException {
-        final ChildBackgroundInitializer child = new 
ChildBackgroundInitializer();
+        final AbstractChildBackgroundInitializer child = 
createChildBackgroundInitializer();
         initializer.addInitializer(CHILD_INIT, child);
         initializer.start();
         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res 
= initializer
@@ -329,7 +329,7 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
     @Test
     public void testInitializeResultsIsSuccessfulFalse()
             throws ConcurrentException {
-        final ChildBackgroundInitializer child = new 
ChildBackgroundInitializer();
+        final AbstractChildBackgroundInitializer child = 
createChildBackgroundInitializer();
         child.ex = new Exception();
         initializer.addInitializer(CHILD_INIT, child);
         initializer.start();
@@ -348,13 +348,13 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
     public void testInitializeNested() throws ConcurrentException {
         final String nameMulti = "multiChildInitializer";
         initializer
-                .addInitializer(CHILD_INIT, new ChildBackgroundInitializer());
+                .addInitializer(CHILD_INIT, 
createChildBackgroundInitializer());
         final MultiBackgroundInitializer mi2 = new 
MultiBackgroundInitializer();
         final int count = 3;
         for (int i = 0; i < count; i++) {
             mi2
                     .addInitializer(CHILD_INIT + i,
-                            new ChildBackgroundInitializer());
+                            createChildBackgroundInitializer());
         }
         initializer.addInitializer(nameMulti, mi2);
         initializer.start();
@@ -374,8 +374,8 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
     @Test
     public void testIsInitialized()
             throws ConcurrentException, InterruptedException {
-        final ChildBackgroundInitializer childOne = new 
ChildBackgroundInitializer();
-        final ChildBackgroundInitializer childTwo = new 
ChildBackgroundInitializer();
+        final AbstractChildBackgroundInitializer childOne = 
createChildBackgroundInitializer();
+        final AbstractChildBackgroundInitializer childTwo = 
createChildBackgroundInitializer();
 
         childOne.enableLatch();
         childTwo.enableLatch();
@@ -409,14 +409,28 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
     }
 
     /**
-     * A concrete implementation of {@code BackgroundInitializer} used for
+     * An overrideable method to create concrete implementations of
+     * {@code BackgroundInitializer} used for defining background tasks
+     * for {@code MultiBackgroundInitializer}.
+     */
+    protected AbstractChildBackgroundInitializer 
createChildBackgroundInitializer() {
+        return new MethodChildBackgroundInitializer();
+    }
+
+    /**
+     * A mostly complete implementation of {@code BackgroundInitializer} used 
for
      * defining background tasks for {@code MultiBackgroundInitializer}.
+     *
+     * Subclasses will contain the initializer, either as an method 
implementation
+     * or by using a supplier.
      */
-    private static final class ChildBackgroundInitializer extends
-            BackgroundInitializer<Integer> {
+    protected static class AbstractChildBackgroundInitializer extends 
BackgroundInitializer<CloseableCounter> {
         /** Stores the current executor service. */
         volatile ExecutorService currentExecutor;
 
+        /** An object containing the state we are testing */
+        CloseableCounter counter = new CloseableCounter();
+
         /** A counter for the invocations of initialize(). */
         volatile int initializeCalls;
 
@@ -435,23 +449,76 @@ public class MultiBackgroundInitializerTest extends 
AbstractLangTest {
             latch.countDown();
         }
 
+        public CloseableCounter getCloseableCounter() {
+            return counter;
+        }
+
         /**
          * Records this invocation. Optionally throws an exception.
          */
-        @Override
-        protected Integer initialize() throws Exception {
-            currentExecutor = getActiveExecutor();
+        protected CloseableCounter initializeInternal() throws Exception {
             initializeCalls++;
+            currentExecutor = getActiveExecutor();
+
+            if (waitForLatch) {
+                latch.await();
+            }
 
             if (ex != null) {
                 throw ex;
             }
 
-            if (waitForLatch) {
-                latch.await();
+            return counter.increment();
+        }
+    }
+
+    protected static class MethodChildBackgroundInitializer extends 
AbstractChildBackgroundInitializer {
+        @Override
+        protected CloseableCounter initialize() throws Exception {
+            return initializeInternal();
+        }
+    }
+
+    protected static class CloseableCounter {
+        /** The number of invocations of initialize(). */
+        volatile int initializeCalls;
+
+        /** Has the close consumer successfully reached this object. */
+        volatile boolean closed;
+
+        public CloseableCounter increment() {
+            initializeCalls++;
+            return this;
+        }
+
+        public int getInitializeCalls() {
+            return initializeCalls;
+        }
+
+        public CloseableCounter setInitializeCalls(int i) {
+            initializeCalls = i;
+            return this;
+        }
+
+        public void close() {
+            closed = true;
+        }
+
+        public boolean isClosed() {
+            return closed;
+        }
+
+        @Override
+        public boolean equals(final Object other) {
+            if (other instanceof CloseableCounter) {
+                return initializeCalls == ((CloseableCounter) 
other).getInitializeCalls();
             }
+            return false;
+        }
 
-            return Integer.valueOf(initializeCalls);
+        // A convenience for testing that a CloseableCounter typed as Object 
has a specific initializeCalls value
+        public static CloseableCounter wrapInteger(int i) {
+            return new CloseableCounter().setInitializeCalls(i);
         }
     }
 }


Reply via email to