Oliver Heger has raised concerns about this commit in JIRA [1]:

> This is a strong change in the behavior of this class. The main property
of atomic initializers was that they are non
> blocking. Now a blocking wait has been introduced. When there is so much
contention that the busy wait is
> actually a problem, wouln't it then be better to directly use a blocking
variant like lazy initializer?

I've looked through the JavaDoc of AtomicInitializer once more. It says:
"Because {@code AtomicSafeInitializer} does not use synchronization at all
it probably outruns {@link LazyInitializer}, at least under low or moderate
concurrent access."

This is the only thing I can find regarding concurrency properties of
AtomicInitializer. I think this still holds, doesn't it?

Benedikt

[1] https://issues.apache.org/jira/browse/LANG-1086

2015-02-23 21:15 GMT+01:00 <brit...@apache.org>:

> Author: britter
> Date: Mon Feb 23 20:15:49 2015
> New Revision: 1661762
>
> URL: http://svn.apache.org/r1661762
> Log:
> LANG-1086: Remove busy wait from AtomicSafeInitializer.get(). This also
> fixes #46 from github. Thanks to github user artnaseef.
>
> Modified:
>     commons/proper/lang/trunk/src/changes/changes.xml
>
> commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java
>
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java
>
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java
>
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java
>
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerTest.java
>
> Modified: commons/proper/lang/trunk/src/changes/changes.xml
> URL:
> http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/changes/changes.xml?rev=1661762&r1=1661761&r2=1661762&view=diff
>
> ==============================================================================
> --- commons/proper/lang/trunk/src/changes/changes.xml [utf-8] (original)
> +++ commons/proper/lang/trunk/src/changes/changes.xml [utf-8] Mon Feb 23
> 20:15:49 2015
> @@ -22,6 +22,7 @@
>    <body>
>
>    <release version="3.4" date="tba" description="tba">
> +    <action issue="LANG-1086" type="update" dev="britter">Remove busy
> wait from AtomicSafeInitializer.get()</action>
>      <action issue="LANG-1081" type="fix" dev="britter" due-to="Jonathan
> Baker">DiffBuilder.append(String, Object left, Object right) does not do a
> left.equals(right) check</action>
>      <action issue="LANG-1055" type="fix" dev="britter" due-to="Jonathan
> Baker">StrSubstitutor.replaceSystemProperties does not work
> consistently</action>
>      <action issue="LANG-1082" type="add" dev="britter" due-to="Jonathan
> Baker">Add option to disable the "objectsTriviallyEqual" test in
> DiffBuilder</action>
>
> Modified:
> commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java
> URL:
> http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java?rev=1661762&r1=1661761&r2=1661762&view=diff
>
> ==============================================================================
> ---
> commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java
> (original)
> +++
> commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java
> Mon Feb 23 20:15:49 2015
> @@ -16,6 +16,7 @@
>   */
>  package org.apache.commons.lang3.concurrent;
>
> +import java.util.concurrent.CountDownLatch;
>  import java.util.concurrent.atomic.AtomicReference;
>
>  /**
> @@ -62,20 +63,44 @@ public abstract class AtomicSafeInitiali
>      /** Holds the reference to the managed object. */
>      private final AtomicReference<T> reference = new AtomicReference<T>();
>
> +    /** Holds the exception that terminated the initialize() method, if
> an exception was thrown */
> +    private final AtomicReference<ConcurrentException> referenceExc = new
> AtomicReference<ConcurrentException>();
> +
> +    /** Latch for those threads waiting for initialization to complete. */
> +    private final CountDownLatch latch = new CountDownLatch(1);
> +
>      /**
>       * Get (and initialize, if not initialized yet) the required object
>       *
>       * @return lazily initialized object
>       * @throws ConcurrentException if the initialization of the object
> causes an
> -     * exception
> +     * exception or the thread is interrupted waiting for another thread
> to
> +     * complete the initialization
>       */
>      @Override
>      public final T get() throws ConcurrentException {
>          T result;
>
> -        while ((result = reference.get()) == null) {
> +        if ((result = reference.get()) == null) {
>              if (factory.compareAndSet(null, this)) {
> -                reference.set(initialize());
> +                try {
> +                    reference.set(result = initialize());
> +                } catch ( ConcurrentException exc ) {
> +                    referenceExc.set(exc);
> +                    throw exc;
> +                } finally {
> +                    latch.countDown();
> +                }
> +            } else {
> +                try {
> +                    latch.await();
> +                    if ( referenceExc.get() != null ) {
> +                        throw new
> ConcurrentException(referenceExc.get().getMessage(),
> referenceExc.get().getCause());
> +                    }
> +                    result = reference.get();
> +                } catch (InterruptedException intExc) {
> +                    throw new ConcurrentException("interrupted waiting
> for initialization to complete", intExc);
> +                }
>              }
>          }
>
>
> Modified:
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java
> URL:
> http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java?rev=1661762&r1=1661761&r2=1661762&view=diff
>
> ==============================================================================
> ---
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java
> (original)
> +++
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java
> Mon Feb 23 20:15:49 2015
> @@ -18,6 +18,8 @@ package org.apache.commons.lang3.concurr
>
>  import static org.junit.Assert.assertEquals;
>  import static org.junit.Assert.assertNotNull;
> +import static org.junit.Assert.assertSame;
> +import static org.junit.Assert.assertTrue;
>
>  import java.util.concurrent.CountDownLatch;
>
> @@ -72,7 +74,41 @@ public abstract class AbstractConcurrent
>      @Test
>      public void testGetConcurrent() throws ConcurrentException,
>              InterruptedException {
> -        final ConcurrentInitializer<Object> initializer =
> createInitializer();
> +
> +        this.testGetConcurrentOptionallyWithException(false, null, null);
> +    }
> +
> +    /**
> +     * Tests the handling of exceptions thrown on the initialized when
> multiple threads execute concurrently.
> +     * Always an exception with the same message and cause should be
> thrown.
> +     *
> +     * @throws org.apache.commons.lang3.concurrent.ConcurrentException
> because the object under test may throw it
> +     * @throws java.lang.InterruptedException because the threading API
> my throw it
> +     */
> +    public void testGetConcurrentWithException(String expectedMessage,
> +                                               Exception expectedCause)
> +            throws ConcurrentException, InterruptedException {
> +
> +        this.testGetConcurrentOptionallyWithException(true,
> expectedMessage, expectedCause);
> +    }
> +
> +    /**
> +     * Tests whether get() can be invoked from multiple threads
> concurrently.  Supports the exception-handling case
> +     * and the normal, non-exception case.
> +     *
> +     * Always the same object should be returned, or an exception with
> the same message and cause should be thrown.
> +     *
> +     * @throws org.apache.commons.lang3.concurrent.ConcurrentException
> because the object under test may throw it
> +     * @throws java.lang.InterruptedException because the threading API
> my throw it
> +     */
> +    protected void testGetConcurrentOptionallyWithException(boolean
> expectExceptions, String expectedMessage,
> +                                                            Exception
> expectedCause)
> +            throws ConcurrentException, InterruptedException {
> +
> +        final ConcurrentInitializer<Object> initializer =
> expectExceptions ?
> +                createExceptionThrowingInitializer() :
> +                createInitializer();
> +
>          final int threadCount = 20;
>          final CountDownLatch startLatch = new CountDownLatch(1);
>          class GetThread extends Thread {
> @@ -106,9 +142,18 @@ public abstract class AbstractConcurrent
>          }
>
>          // check results
> -        final Object managedObject = initializer.get();
> -        for (final GetThread t : threads) {
> -            assertEquals("Wrong object", managedObject, t.object);
> +        if ( expectExceptions ) {
> +            for (GetThread t : threads) {
> +                assertTrue(t.object instanceof Exception);
> +                Exception exc = (Exception) t.object;
> +                assertEquals(expectedMessage, exc.getMessage());
> +                assertSame(expectedCause, exc.getCause());
> +            }
> +        } else {
> +            final Object managedObject = initializer.get();
> +            for (final GetThread t : threads) {
> +                assertEquals("Wrong object", managedObject, t.object);
> +            }
>          }
>      }
>
> @@ -119,4 +164,12 @@ public abstract class AbstractConcurrent
>       * @return the initializer object to be tested
>       */
>      protected abstract ConcurrentInitializer<Object> createInitializer();
> +
> +    /**
> +     * Creates a {@link ConcurrentInitializer} object that always throws
> +     * exceptions.
> +     *
> +     * @return
> +     */
> +    protected abstract ConcurrentInitializer<Object>
> createExceptionThrowingInitializer();
>  }
>
> Modified:
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java
> URL:
> http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java?rev=1661762&r1=1661761&r2=1661762&view=diff
>
> ==============================================================================
> ---
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java
> (original)
> +++
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java
> Mon Feb 23 20:15:49 2015
> @@ -16,12 +16,29 @@
>   */
>  package org.apache.commons.lang3.concurrent;
>
> +import org.junit.Test;
> +
>  /**
>   * Test class for {@code AtomicInitializer}.
>   *
>   * @version $Id$
>   */
>  public class AtomicInitializerTest extends
> AbstractConcurrentInitializerTest {
> +    private Exception testCauseException;
> +    private String testExceptionMessage;
> +
> +    public AtomicInitializerTest() {
> +        testExceptionMessage = "x-test-exception-message-x";
> +        testCauseException = new Exception(testExceptionMessage);
> +    }
> +
> +    @Test
> +    public void testGetConcurrentWithException ()
> +            throws ConcurrentException, InterruptedException {
> +
> +        super.testGetConcurrentWithException(testExceptionMessage,
> testCauseException);
> +    }
> +
>      /**
>       * Returns the initializer to be tested.
>       *
> @@ -36,4 +53,20 @@ public class AtomicInitializerTest exten
>              }
>          };
>      }
> +
> +    @Override
> +    protected ConcurrentInitializer<Object>
> createExceptionThrowingInitializer() {
> +        return new ExceptionThrowingAtomicSafeInitializerTestImpl();
> +    }
> +
> +    /**
> +     * A concrete test implementation of {@code AtomicSafeInitializer}.
> This
> +     * implementation always throws an exception.
> +     */
> +    private class ExceptionThrowingAtomicSafeInitializerTestImpl extends
> AtomicSafeInitializer<Object> {
> +        @Override
> +        protected Object initialize() throws ConcurrentException {
> +            throw new ConcurrentException(testExceptionMessage,
> testCauseException);
> +        }
> +    }
>  }
>
> Modified:
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java
> URL:
> http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java?rev=1661762&r1=1661761&r2=1661762&view=diff
>
> ==============================================================================
> ---
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java
> (original)
> +++
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java
> Mon Feb 23 20:15:49 2015
> @@ -17,7 +17,11 @@
>  package org.apache.commons.lang3.concurrent;
>
>  import static org.junit.Assert.assertEquals;
> +import static org.junit.Assert.assertFalse;
> +import static org.junit.Assert.assertSame;
> +import static org.junit.Assert.assertTrue;
>
> +import java.util.concurrent.CountDownLatch;
>  import java.util.concurrent.atomic.AtomicInteger;
>
>  import org.junit.Before;
> @@ -30,12 +34,19 @@ import org.junit.Test;
>   */
>  public class AtomicSafeInitializerTest extends
>          AbstractConcurrentInitializerTest {
> +
>      /** The instance to be tested. */
>      private AtomicSafeInitializerTestImpl initializer;
> +    private ExceptionThrowingAtomicSafeInitializerTestImpl
> exceptionThrowingInitializer;
> +    private Exception testCauseException;
> +    private String testExceptionMessage;
>
>      @Before
>      public void setUp() throws Exception {
>          initializer = new AtomicSafeInitializerTestImpl();
> +        exceptionThrowingInitializer = new
> ExceptionThrowingAtomicSafeInitializerTestImpl();
> +        testExceptionMessage = "x-test-exception-message-x";
> +        testCauseException = new Exception(testExceptionMessage);
>      }
>
>      /**
> @@ -49,6 +60,17 @@ public class AtomicSafeInitializerTest e
>      }
>
>      /**
> +     * Returns the exception-throwing initializer to be tested.
> +     *
> +     * @return the {@code AtomicSafeInitializer} under test when
> validating
> +     * exception handling
> +     */
> +    @Override
> +    protected ConcurrentInitializer<Object>
> createExceptionThrowingInitializer() {
> +        return exceptionThrowingInitializer;
> +    }
> +
> +    /**
>       * Tests that initialize() is called only once.
>       *
>       * @throws org.apache.commons.lang3.concurrent.ConcurrentException
> because {@link #testGetConcurrent()} may throw it
> @@ -62,6 +84,92 @@ public class AtomicSafeInitializerTest e
>                  initializer.initCounter.get());
>      }
>
> +    @Test
> +    public void testExceptionOnInitialize() throws ConcurrentException,
> +            InterruptedException {
> +
> +        testGetConcurrentWithException(testExceptionMessage,
> testCauseException);
> +    }
> +
> +    /**
> +     * Validate the handling of an interrupted exception on a thread
> waiting for another thread to finish calling the
> +     * initialize() method.
> +     *
> +     * @throws Exception
> +     */
> +    @Test(timeout = 3000)
> +    public void testInterruptedWaitingOnInitialize() throws Exception {
> +        this.execTestWithWaitingOnInitialize(true);
> +    }
> +
> +    /**
> +     * Test the success path of two threads reaching the initialization
> point at the same time.
> +     */
> +    @Test(timeout = 3000)
> +    public void testOneThreadWaitingForAnotherToInitialize () throws
> Exception {
> +        execTestWithWaitingOnInitialize(false);
> +    }
> +
> +
> +    /**
> +     * Execute a test that requires one thread to be waiting on the
> initialize() method of another thread.  This test
> +     * uses latches to guarantee the code path being tested.
> +     *
> +     * @throws Exception
> +     */
> +    protected void execTestWithWaitingOnInitialize(boolean interruptInd)
> throws Exception {
> +        final CountDownLatch startLatch = new CountDownLatch(1);
> +        final CountDownLatch finishLatch = new CountDownLatch(1);
> +        final WaitingInitializerTestImpl initializer = new
> WaitingInitializerTestImpl(startLatch, finishLatch);
> +
> +        InitializerTestThread execThread1 = new
> InitializerTestThread(initializer);
> +        InitializerTestThread execThread2 = new
> InitializerTestThread(initializer);
> +
> +        // Start the first thread and wait for it to get into the
> initialize method so we are sure it is the thread
> +        //  executing initialize().
> +        execThread1.start();
> +        startLatch.await();
> +
> +        // Start the second thread and interrupt it to force the
> InterruptedException.  There is no need to make sure
> +        //  the thread reaches the await() call before interrupting it.
> +        execThread2.start();
> +
> +        if ( interruptInd ) {
> +            // Interrupt the second thread now and wait for it to
> complete to ensure it reaches the wait inside the
> +            //  get() method.
> +            execThread2.interrupt();
> +            execThread2.join();
> +        }
> +
> +        // Signal the completion of the initialize method now.
> +        finishLatch.countDown();
> +
> +        // Wait for the initialize() to finish.
> +        execThread1.join();
> +
> +        // Wait for thread2 to finish, if it was not already done
> +        if ( ! interruptInd ) {
> +            execThread2.join();
> +        }
> +
> +        //
> +        // Validate: thread1 should have the valid result; thread2 should
> have caught an interrupted exception, if
> +        //  interrupted, or should have the same result otherwise.
> +        //
> +        assertFalse(execThread1.isCaughtException());
> +        assertSame(initializer.getAnswer(), execThread1.getResult());
> +
> +        if ( interruptInd ) {
> +            assertTrue(execThread2.isCaughtException());
> +            Exception exc = (Exception) execThread2.getResult();
> +            assertTrue(exc.getCause() instanceof InterruptedException);
> +            assertEquals("interrupted waiting for initialization to
> complete", exc.getMessage());
> +        } else {
> +            assertFalse(execThread2.isCaughtException());
> +            assertSame(initializer.getAnswer(), execThread2.getResult());
> +        }
> +    }
> +
>      /**
>       * A concrete test implementation of {@code AtomicSafeInitializer}.
> This
>       * implementation also counts the number of invocations of the
> initialize()
> @@ -78,4 +186,90 @@ public class AtomicSafeInitializerTest e
>              return new Object();
>          }
>      }
> +
> +    /**
> +     * A concrete test implementation of {@code AtomicSafeInitializer}.
> This
> +     * implementation always throws an exception.
> +     */
> +    private class ExceptionThrowingAtomicSafeInitializerTestImpl extends
> AtomicSafeInitializer<Object> {
> +        @Override
> +        protected Object initialize() throws ConcurrentException {
> +            throw new ConcurrentException(testExceptionMessage,
> testCauseException);
> +        }
> +    }
> +
> +    /**
> +     * Initializer that signals it has started and waits to complete
> until signalled in order to enable a guaranteed
> +     * order-of-operations.  This allows the test code to peg one thread
> to the initialize method for a period of time
> +     * that the test can dictate.
> +     */
> +    private class WaitingInitializerTestImpl extends
> AtomicSafeInitializer<Object> {
> +        private final CountDownLatch startedLatch;
> +        private final CountDownLatch finishLatch;
> +        private final Object answer = new Object();
> +
> +        public WaitingInitializerTestImpl(CountDownLatch startedLatch,
> CountDownLatch finishLatch) {
> +            this.startedLatch = startedLatch;
> +            this.finishLatch = finishLatch;
> +        }
> +
> +        @Override
> +        protected Object initialize() throws ConcurrentException {
> +            this.startedLatch.countDown();
> +            try {
> +                this.finishLatch.await();
> +            } catch (InterruptedException intExc) {
> +                throw new ConcurrentException(intExc);
> +            }
> +
> +            return  answer;
> +        }
> +
> +        public Object getAnswer () {
> +            return answer;
> +        }
> +    }
> +
> +    /**
> +     * Test executor of the initializer get() operation that captures the
> result.
> +     */
> +    private class InitializerTestThread extends Thread {
> +        private AtomicSafeInitializer<Object>   initializer;
> +        private Object result;
> +        private boolean caughtException;
> +
> +        public InitializerTestThread(AtomicSafeInitializer<Object>
> initializer) {
> +            super("AtomicSafeInitializer test thread");
> +            this.initializer = initializer;
> +        }
> +
> +        @Override
> +        public void run() {
> +            try {
> +                this.result = initializer.get();
> +            } catch ( ConcurrentException concurrentExc ) {
> +                this.caughtException = true;
> +                this.result = concurrentExc;
> +            }
> +        }
> +
> +        /**
> +         * Resulting object, if the get() method returned successfully,
> or exception if an exception was thrown.
> +         *
> +         * @return resulting object or exception from the get() method
> call.
> +         */
> +        public Object getResult () {
> +            return  this.result;
> +        }
> +
> +        /**
> +         * Determine whether an exception was caught on the get() call.
> Does not guarantee that the get() method was
> +         * called or completed.
> +         *
> +         * @return true => exception was caught; false => exception was
> not caught.
> +         */
> +        public boolean  isCaughtException () {
> +            return  this.caughtException;
> +        }
> +    }
>  }
>
> Modified:
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerTest.java
> URL:
> http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerTest.java?rev=1661762&r1=1661761&r2=1661762&view=diff
>
> ==============================================================================
> ---
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerTest.java
> (original)
> +++
> commons/proper/lang/trunk/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerTest.java
> Mon Feb 23 20:15:49 2015
> @@ -17,6 +17,7 @@
>  package org.apache.commons.lang3.concurrent;
>
>  import org.junit.Before;
> +import org.junit.Test;
>
>  /**
>   * Test class for {@code LazyInitializer}.
> @@ -26,10 +27,16 @@ import org.junit.Before;
>  public class LazyInitializerTest extends
> AbstractConcurrentInitializerTest {
>      /** The initializer to be tested. */
>      private LazyInitializerTestImpl initializer;
> +    private ExceptionThrowingLazyInitializerTestImpl
> exceptionThrowingInitializer;
> +    private Exception testCauseException;
> +    private String testExceptionMessage;
>
>      @Before
>      public void setUp() throws Exception {
>          initializer = new LazyInitializerTestImpl();
> +        exceptionThrowingInitializer = new
> ExceptionThrowingLazyInitializerTestImpl();
> +        testExceptionMessage = "x-test-exception-message-x";
> +        testCauseException = new Exception(testExceptionMessage);
>      }
>
>      /**
> @@ -43,6 +50,18 @@ public class LazyInitializerTest extends
>          return initializer;
>      }
>
> +    @Override
> +    protected ConcurrentInitializer<Object>
> createExceptionThrowingInitializer() {
> +        return exceptionThrowingInitializer;
> +    }
> +
> +    @Test
> +    public void testGetConcurrentWithException ()
> +            throws ConcurrentException, InterruptedException {
> +
> +        super.testGetConcurrentWithException(testExceptionMessage,
> testCauseException);
> +    }
> +
>      /**
>       * A test implementation of LazyInitializer. This class creates a
> plain
>       * Object. As Object does not provide a specific equals() method, it
> is easy
> @@ -55,4 +74,16 @@ public class LazyInitializerTest extends
>              return new Object();
>          }
>      }
> +
> +
> +    /**
> +     * A concrete test implementation of {@code AtomicSafeInitializer}.
> This
> +     * implementation always throws an exception.
> +     */
> +    private class ExceptionThrowingLazyInitializerTestImpl extends
> LazyInitializer<Object> {
> +        @Override
> +        protected Object initialize() throws ConcurrentException {
> +            throw new ConcurrentException(testExceptionMessage,
> testCauseException);
> +        }
> +    }
>  }
>
>
>


-- 
http://people.apache.org/~britter/
http://www.systemoutprintln.de/
http://twitter.com/BenediktRitter
http://github.com/britter

Reply via email to