From: Erik Brakkee Date: Mon, 10 May 2010 21:56:50 +0000 (+0000) Subject: new utility for updated cached values in a multi-threaded environment. X-Git-Tag: wamblee-utils-0.7~469 X-Git-Url: http://wamblee.org/gitweb/?a=commitdiff_plain;ds=sidebyside;h=20b609f290912dd2a186ff7240a1c0be93731cca;hp=f4f8467b507b7bb401e4ad0749ea426208831846;p=utils new utility for updated cached values in a multi-threaded environment. --- diff --git a/support/general/src/main/java/org/wamblee/cache/ComputedValue.java b/support/general/src/main/java/org/wamblee/cache/ComputedValue.java new file mode 100644 index 00000000..24bbe5b0 --- /dev/null +++ b/support/general/src/main/java/org/wamblee/cache/ComputedValue.java @@ -0,0 +1,146 @@ +/* + * Copyright 2005-2010 the original author or authors. + * + * Licensed 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.wamblee.cache; + +/** + * Utility class to deal with recomputation of a certain value. The goal is to + * have only one thread at a time compute the value while other threads that + * simulateneously detect recomputation continue with the old value. + * + * + * @author Erik Brakkee + * + * @param + */ +public class ComputedValue { + + /** + * Computation + * + * @param + * Type of object to compute. + */ + public static interface Computation { + /** + * Checks whether the object is out of date. This will be invoked while + * holding the lock passed at construction of the compute guard. + * + * Any runtime exceptions thrown are passed back through the + * {@link ComputedValue#get()}. + * + * @return True iff recomputation is necessary. + */ + boolean isOutOfDate(); + + /** + * Computes the object. This will be invoked while not holding + * the lock passed at construction of the compute guard. It is + * guaranteed that per ComputeGuard, no concurrent calls to compute() + * are done. + * + * Any runtime exceptions thrown are passed back through the + * {@link ComputedValue#get()}. + * + * @return Computed object. + */ + T compute(); + } + + private Object lock; + private Computation computedValue; + + private Boolean busy; + private T value; + + /** + * Constructs the compute guard + * + * @param aLock + * Lock to use during computation and to guard the value. + * @param aComputation + * Computation to use. + */ + public ComputedValue(Object aLock, Computation aComputation) { + lock = aLock; + computedValue = aComputation; + busy = false; + value = null; + } + + /** + * Triggers computation of the value (if no other thread is currently + * computing the value). + */ + public void compute() { + synchronized (this) { + if (busy) { + return; // another thread is already taking care of it. + } + busy = true; + } + try { + T newvalue = computedValue.compute(); + set(newvalue); + } finally { + synchronized (this) { + busy = false; + } + } + } + + /** + * Gets the current value of the object, recomputing it if the object is out + * of date. This method ensures that only one thread at a time will do + * recomputations. + * + * @return Current value. + */ + public T get() { + boolean mustCompute = false; + synchronized (lock) { + if (computedValue.isOutOfDate()) { + mustCompute = true; + } + } + if (mustCompute) { + compute(); + } + synchronized (lock) { + return value; + } + } + + /** + * Gets the currently cached value. No recomputation is performed. + * @return Cached value. + */ + public T getCached() { + synchronized(lock) { + return value; + } + + } + + /** + * Sets the value explicitly. + * @param aValue value to set. + */ + public void set(T aValue) { + synchronized(lock) { + value = aValue; + } + } +} diff --git a/support/general/src/test/java/org/wamblee/cache/ComputedValueTest.java b/support/general/src/test/java/org/wamblee/cache/ComputedValueTest.java new file mode 100644 index 00000000..d13cd41c --- /dev/null +++ b/support/general/src/test/java/org/wamblee/cache/ComputedValueTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2005-2010 the original author or authors. + * + * Licensed 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.wamblee.cache; + +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.wamblee.cache.ComputedValue.Computation; + +public class ComputedValueTest extends TestCase { + + private Computation computation; + private ComputedValue guard; + + + @Override + protected void setUp() throws Exception { + computation = mock(Computation.class); + } + + public void testComputeOutOfDate() { + initGuard(); + } + + private void initGuard() { + guard = new ComputedValue(this, computation); + + assertNull(guard.getCached()); + + when(computation.isOutOfDate()).thenReturn(true); + when(computation.compute()).thenReturn(10); + + int value = guard.get(); + assertEquals(10, value); + verify(computation).compute(); + reset(computation); + } + + public void testGetCached() { + initGuard(); + assertEquals(10, (int)guard.getCached()); + verifyNoMoreInteractions(computation); + } + + public void testNoComputationWhenNotOutOfDate() { + initGuard(); + when(computation.isOutOfDate()).thenReturn(false); + assertEquals(10, (int) guard.get()); + verify(computation, never()).compute(); + } + + public void testOnlyOneConcurrentComputation() throws Exception { + initGuard(); + final int computeTime = 500; + when(computation.isOutOfDate()).thenReturn(true); + when(computation.compute()).thenAnswer(new Answer() { + @Override + public Integer answer(InvocationOnMock aInvocation) + throws Throwable { + Thread.sleep(computeTime); + return 100; + } + }); + + final List results = new ArrayList(); + + Runnable task = new Runnable() { + @Override + public void run() { + int res = guard.get(); + results.add(res); + } + }; + + // concurrent computation in two threads. + Thread t1 = new Thread(task); + Thread t2 = new Thread(task); + t1.start(); + Thread.sleep(computeTime / 2); + // second task will return old value 10 because first one is still busy. + t2.start(); + t1.join(); + t2.join(); + assertEquals(2, results.size()); + assertEquals(10, (int) results.get(0)); + assertEquals(100, (int) results.get(1)); + verify(computation, times(2)).isOutOfDate(); + } + + public void testExceptionWhileComputing() { + initGuard(); + when(computation.isOutOfDate()).thenReturn(true); + when(computation.compute()).thenThrow(new RuntimeException("xx")); + try { + guard.get(); + fail(); + } catch (RuntimeException e) { + assertEquals("xx", e.getMessage()); + } + } + + public void testExceptionWhileCheckingOutOfDate() { + initGuard(); + when(computation.isOutOfDate()).thenThrow(new RuntimeException("xx")); + try { + guard.get(); + fail(); + } catch (RuntimeException e) { + assertEquals("xx", e.getMessage()); + } + } +} diff --git a/support/general/src/test/java/org/wamblee/concurrency/ReadWriteLockTest.java b/support/general/src/test/java/org/wamblee/concurrency/ReadWriteLockTest.java index 273abd0e..4b4bd426 100644 --- a/support/general/src/test/java/org/wamblee/concurrency/ReadWriteLockTest.java +++ b/support/general/src/test/java/org/wamblee/concurrency/ReadWriteLockTest.java @@ -24,11 +24,11 @@ import junit.framework.TestCase; * @see ReadWriteLock */ public class ReadWriteLockTest extends TestCase { - private static final int HALF_SECOND = 500; + private static final int HALF_SECOND = 100; - private static final int ONE_SECOND = 1000; + private static final int ONE_SECOND = 200; - private static final int TWO_SECONDS = 2000; + private static final int TWO_SECONDS = 400; private ReadWriteLock lock; private int nReaders; private int nWriters;