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 <[email protected]>:
> 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