001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.lang3.concurrent; 018 019import java.util.concurrent.CancellationException; 020import java.util.concurrent.ConcurrentHashMap; 021import java.util.concurrent.ConcurrentMap; 022import java.util.concurrent.ExecutionException; 023import java.util.concurrent.Future; 024import java.util.concurrent.FutureTask; 025import java.util.function.Function; 026 027import org.apache.commons.lang3.exception.ExceptionUtils; 028 029/** 030 * Definition of an interface for a wrapper around a calculation that takes a single parameter and returns a result. The 031 * results for the calculation will be cached for future requests. 032 * 033 * <p> 034 * This is not a fully functional cache: it is unbounded, and there is no way of limiting or removing results once they 035 * have been generated. In particular, note the exception-caching default: unless the {@code recalculate} constructor 036 * option is set to {@code true}, the <em>first</em> exception thrown by a calculation for a given parameter is cached 037 * and rethrown for every future call with that parameter for the lifetime of this instance - a single transient 038 * failure permanently poisons that key. Set {@code recalculate} to {@code true} to retry failed calculations on 039 * subsequent calls instead. 040 * </p> 041 * <p> 042 * Thanks go to Brian Goetz, Tim Peierls and the members of JCP JSR-166 Expert Group for coming up with the 043 * original implementation of the class. It was also published within Java Concurrency in Practice as a sample. 044 * </p> 045 * 046 * @param <I> The type of the input to the calculation 047 * @param <O> The type of the output of the calculation 048 * @since 3.6 049 */ 050public class Memoizer<I, O> implements Computable<I, O> { 051 052 private final ConcurrentMap<I, Future<O>> cache = new ConcurrentHashMap<>(); 053 private final Function<? super I, FutureTask<O>> mappingFunction; 054 private final boolean recalculate; 055 056 /** 057 * Constructs a Memoizer for the provided Computable calculation. 058 * 059 * <p> 060 * If a calculation throws an exception for any reason, this exception will be cached and returned for all future 061 * calls with the provided parameter. 062 * </p> 063 * 064 * @param computable The computation whose results should be memorized 065 */ 066 public Memoizer(final Computable<I, O> computable) { 067 this(computable, false); 068 } 069 070 /** 071 * Constructs a Memoizer for the provided Computable calculation, with the option of whether a Computation that 072 * experiences an error should recalculate on subsequent calls or return the same cached exception. 073 * 074 * @param computable The computation whose results should be memorized 075 * @param recalculate determines whether the computation should be recalculated on subsequent calls if the previous call 076 * failed 077 */ 078 public Memoizer(final Computable<I, O> computable, final boolean recalculate) { 079 this.recalculate = recalculate; 080 this.mappingFunction = k -> new FutureTask<>(() -> computable.compute(k)); 081 } 082 083 /** 084 * Constructs a Memoizer for the provided Function calculation. 085 * 086 * <p> 087 * If a calculation throws an exception for any reason, this exception will be cached and returned for all future 088 * calls with the provided parameter. 089 * </p> 090 * 091 * @param function The function whose results should be memorized 092 * @since 2.13.0 093 */ 094 public Memoizer(final Function<I, O> function) { 095 this(function, false); 096 } 097 098 /** 099 * Constructs a Memoizer for the provided Function calculation, with the option of whether a Function that 100 * experiences an error should recalculate on subsequent calls or return the same cached exception. 101 * 102 * @param function The computation whose results should be memorized 103 * @param recalculate determines whether the computation should be recalculated on subsequent calls if the previous call 104 * failed 105 * @since 2.13.0 106 */ 107 public Memoizer(final Function<I, O> function, final boolean recalculate) { 108 this.recalculate = recalculate; 109 this.mappingFunction = k -> new FutureTask<>(() -> function.apply(k)); 110 } 111 112 /** 113 * This method will return the result of the calculation and cache it, if it has not previously been calculated. 114 * 115 * <p> 116 * This cache will also cache exceptions that occur during the computation if the {@code recalculate} parameter in the 117 * constructor was set to {@code false}, or not set: the first exception thrown for a given argument is rethrown for 118 * every future call with that argument. Otherwise, if an exception happened on the previous calculation, 119 * the method will attempt again to generate a value. 120 * </p> 121 * <p> 122 * The calculation for a given argument runs at most once per cached entry and executes <em>outside</em> any internal 123 * lock of the backing map (the pattern published in <em>Java Concurrency in Practice</em>): a slow calculation for 124 * one key does not block calls for unrelated keys, and a calculation may itself use this Memoizer without 125 * deadlocking. Concurrent callers for the same argument wait on the same {@link Future}. 126 * </p> 127 * 128 * @param arg The argument for the calculation 129 * @return The result of the calculation 130 * @throws InterruptedException Thrown if the calculation is interrupted. 131 */ 132 @Override 133 public O compute(final I arg) throws InterruptedException { 134 while (true) { 135 Future<O> future = cache.get(arg); 136 if (future == null) { 137 final FutureTask<O> futureTask = mappingFunction.apply(arg); 138 future = cache.putIfAbsent(arg, futureTask); 139 if (future == null) { 140 // This thread won the race to install the task: run the user computation here, 141 // outside the ConcurrentHashMap's internal locks. Losing threads (and later 142 // callers) block on futureTask.get() instead of on a map bin lock. 143 future = futureTask; 144 futureTask.run(); 145 } 146 } 147 try { 148 return future.get(); 149 } catch (final CancellationException e) { 150 cache.remove(arg, future); 151 } catch (final ExecutionException e) { 152 if (recalculate) { 153 cache.remove(arg, future); 154 } 155 throw launderException(e.getCause()); 156 } 157 } 158 } 159 160 /** 161 * Always throws an unchecked exception or error, rethrowing a {@link RuntimeException} or {@link Error} unchanged 162 * and wrapping any other throwable in an {@link IllegalStateException}. 163 * 164 * @param throwable The throwable to rethrow or wrap. 165 * @return Never returns normally. 166 */ 167 private RuntimeException launderException(final Throwable throwable) { 168 throw new IllegalStateException("Unchecked exception", ExceptionUtils.throwUnchecked(throwable)); 169 } 170}