8145164: Default implementation of ConcurrentMap::compute can throw NPE
Reviewed-by: martin, psandoz, chegar
This commit is contained in:
parent
47e7f9dcdc
commit
7f6a9e3180
@ -649,7 +649,7 @@ public interface Map<K, V> {
|
|||||||
try {
|
try {
|
||||||
k = entry.getKey();
|
k = entry.getKey();
|
||||||
v = entry.getValue();
|
v = entry.getValue();
|
||||||
} catch(IllegalStateException ise) {
|
} catch (IllegalStateException ise) {
|
||||||
// this usually means the entry is no longer in the map.
|
// this usually means the entry is no longer in the map.
|
||||||
throw new ConcurrentModificationException(ise);
|
throw new ConcurrentModificationException(ise);
|
||||||
}
|
}
|
||||||
@ -704,7 +704,7 @@ public interface Map<K, V> {
|
|||||||
try {
|
try {
|
||||||
k = entry.getKey();
|
k = entry.getKey();
|
||||||
v = entry.getValue();
|
v = entry.getValue();
|
||||||
} catch(IllegalStateException ise) {
|
} catch (IllegalStateException ise) {
|
||||||
// this usually means the entry is no longer in the map.
|
// this usually means the entry is no longer in the map.
|
||||||
throw new ConcurrentModificationException(ise);
|
throw new ConcurrentModificationException(ise);
|
||||||
}
|
}
|
||||||
@ -714,7 +714,7 @@ public interface Map<K, V> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
entry.setValue(v);
|
entry.setValue(v);
|
||||||
} catch(IllegalStateException ise) {
|
} catch (IllegalStateException ise) {
|
||||||
// this usually means the entry is no longer in the map.
|
// this usually means the entry is no longer in the map.
|
||||||
throw new ConcurrentModificationException(ise);
|
throw new ConcurrentModificationException(ise);
|
||||||
}
|
}
|
||||||
@ -887,7 +887,7 @@ public interface Map<K, V> {
|
|||||||
* or atomicity properties of this method. Any implementation providing
|
* or atomicity properties of this method. Any implementation providing
|
||||||
* atomicity guarantees must override this method and document its
|
* atomicity guarantees must override this method and document its
|
||||||
* concurrency properties.
|
* concurrency properties.
|
||||||
*
|
*
|
||||||
* @param key key with which the specified value is associated
|
* @param key key with which the specified value is associated
|
||||||
* @param value value to be associated with the specified key
|
* @param value value to be associated with the specified key
|
||||||
* @return the previous value associated with the specified key, or
|
* @return the previous value associated with the specified key, or
|
||||||
@ -984,6 +984,9 @@ public interface Map<K, V> {
|
|||||||
* @throws ClassCastException if the class of the specified key or value
|
* @throws ClassCastException if the class of the specified key or value
|
||||||
* prevents it from being stored in this map
|
* prevents it from being stored in this map
|
||||||
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
||||||
|
* @throws IllegalArgumentException if some property of the specified key
|
||||||
|
* or value prevents it from being stored in this map
|
||||||
|
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
||||||
* @since 1.8
|
* @since 1.8
|
||||||
*/
|
*/
|
||||||
default V computeIfAbsent(K key,
|
default V computeIfAbsent(K key,
|
||||||
@ -1058,6 +1061,9 @@ public interface Map<K, V> {
|
|||||||
* @throws ClassCastException if the class of the specified key or value
|
* @throws ClassCastException if the class of the specified key or value
|
||||||
* prevents it from being stored in this map
|
* prevents it from being stored in this map
|
||||||
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
||||||
|
* @throws IllegalArgumentException if some property of the specified key
|
||||||
|
* or value prevents it from being stored in this map
|
||||||
|
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
||||||
* @since 1.8
|
* @since 1.8
|
||||||
*/
|
*/
|
||||||
default V computeIfPresent(K key,
|
default V computeIfPresent(K key,
|
||||||
@ -1103,7 +1109,7 @@ public interface Map<K, V> {
|
|||||||
* <pre> {@code
|
* <pre> {@code
|
||||||
* V oldValue = map.get(key);
|
* V oldValue = map.get(key);
|
||||||
* V newValue = remappingFunction.apply(key, oldValue);
|
* V newValue = remappingFunction.apply(key, oldValue);
|
||||||
* if (oldValue != null ) {
|
* if (oldValue != null) {
|
||||||
* if (newValue != null)
|
* if (newValue != null)
|
||||||
* map.put(key, newValue);
|
* map.put(key, newValue);
|
||||||
* else
|
* else
|
||||||
@ -1147,6 +1153,9 @@ public interface Map<K, V> {
|
|||||||
* @throws ClassCastException if the class of the specified key or value
|
* @throws ClassCastException if the class of the specified key or value
|
||||||
* prevents it from being stored in this map
|
* prevents it from being stored in this map
|
||||||
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
||||||
|
* @throws IllegalArgumentException if some property of the specified key
|
||||||
|
* or value prevents it from being stored in this map
|
||||||
|
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
||||||
* @since 1.8
|
* @since 1.8
|
||||||
*/
|
*/
|
||||||
default V compute(K key,
|
default V compute(K key,
|
||||||
@ -1239,6 +1248,9 @@ public interface Map<K, V> {
|
|||||||
* @throws ClassCastException if the class of the specified key or value
|
* @throws ClassCastException if the class of the specified key or value
|
||||||
* prevents it from being stored in this map
|
* prevents it from being stored in this map
|
||||||
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
||||||
|
* @throws IllegalArgumentException if some property of the specified key
|
||||||
|
* or value prevents it from being stored in this map
|
||||||
|
* (<a href="{@docRoot}/java/util/Collection.html#optional-restrictions">optional</a>)
|
||||||
* @throws NullPointerException if the specified key is null and this map
|
* @throws NullPointerException if the specified key is null and this map
|
||||||
* does not support null keys or the value or remappingFunction is
|
* does not support null keys or the value or remappingFunction is
|
||||||
* null
|
* null
|
||||||
@ -1251,7 +1263,7 @@ public interface Map<K, V> {
|
|||||||
V oldValue = get(key);
|
V oldValue = get(key);
|
||||||
V newValue = (oldValue == null) ? value :
|
V newValue = (oldValue == null) ? value :
|
||||||
remappingFunction.apply(oldValue, value);
|
remappingFunction.apply(oldValue, value);
|
||||||
if(newValue == null) {
|
if (newValue == null) {
|
||||||
remove(key);
|
remove(key);
|
||||||
} else {
|
} else {
|
||||||
put(key, newValue);
|
put(key, newValue);
|
||||||
|
@ -301,19 +301,15 @@ public interface ConcurrentMap<K,V> extends Map<K,V> {
|
|||||||
*
|
*
|
||||||
* @implSpec
|
* @implSpec
|
||||||
* The default implementation is equivalent to the following steps for this
|
* The default implementation is equivalent to the following steps for this
|
||||||
* {@code map}, then returning the current value or {@code null} if now
|
* {@code map}:
|
||||||
* absent:
|
|
||||||
*
|
*
|
||||||
* <pre> {@code
|
* <pre> {@code
|
||||||
* if (map.get(key) == null) {
|
* V oldValue, newValue;
|
||||||
* V newValue = mappingFunction.apply(key);
|
* return ((oldValue = map.get(key)) == null
|
||||||
* if (newValue != null)
|
* && (newValue = mappingFunction.apply(key)) != null
|
||||||
* return map.putIfAbsent(key, newValue);
|
* && (oldValue = map.putIfAbsent(key, newValue)) == null)
|
||||||
* }}</pre>
|
* ? newValue
|
||||||
*
|
* : oldValue;}</pre>
|
||||||
* The default implementation may retry these steps when multiple
|
|
||||||
* threads attempt updates including potentially calling the mapping
|
|
||||||
* function multiple times.
|
|
||||||
*
|
*
|
||||||
* <p>This implementation assumes that the ConcurrentMap cannot contain null
|
* <p>This implementation assumes that the ConcurrentMap cannot contain null
|
||||||
* values and {@code get()} returning null unambiguously means the key is
|
* values and {@code get()} returning null unambiguously means the key is
|
||||||
@ -323,16 +319,19 @@ public interface ConcurrentMap<K,V> extends Map<K,V> {
|
|||||||
* @throws UnsupportedOperationException {@inheritDoc}
|
* @throws UnsupportedOperationException {@inheritDoc}
|
||||||
* @throws ClassCastException {@inheritDoc}
|
* @throws ClassCastException {@inheritDoc}
|
||||||
* @throws NullPointerException {@inheritDoc}
|
* @throws NullPointerException {@inheritDoc}
|
||||||
|
* @throws IllegalArgumentException {@inheritDoc}
|
||||||
* @since 1.8
|
* @since 1.8
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
default V computeIfAbsent(K key,
|
default V computeIfAbsent(K key,
|
||||||
Function<? super K, ? extends V> mappingFunction) {
|
Function<? super K, ? extends V> mappingFunction) {
|
||||||
Objects.requireNonNull(mappingFunction);
|
Objects.requireNonNull(mappingFunction);
|
||||||
V v, newValue;
|
V oldValue, newValue;
|
||||||
return ((v = get(key)) == null &&
|
return ((oldValue = get(key)) == null
|
||||||
(newValue = mappingFunction.apply(key)) != null &&
|
&& (newValue = mappingFunction.apply(key)) != null
|
||||||
(v = putIfAbsent(key, newValue)) == null) ? newValue : v;
|
&& (oldValue = putIfAbsent(key, newValue)) == null)
|
||||||
|
? newValue
|
||||||
|
: oldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -340,22 +339,19 @@ public interface ConcurrentMap<K,V> extends Map<K,V> {
|
|||||||
*
|
*
|
||||||
* @implSpec
|
* @implSpec
|
||||||
* The default implementation is equivalent to performing the following
|
* The default implementation is equivalent to performing the following
|
||||||
* steps for this {@code map}, then returning the current value or
|
* steps for this {@code map}:
|
||||||
* {@code null} if now absent:
|
|
||||||
*
|
*
|
||||||
* <pre> {@code
|
* <pre> {@code
|
||||||
* if (map.get(key) != null) {
|
* for (V oldValue; (oldValue = map.get(key)) != null; ) {
|
||||||
* V oldValue = map.get(key);
|
|
||||||
* V newValue = remappingFunction.apply(key, oldValue);
|
* V newValue = remappingFunction.apply(key, oldValue);
|
||||||
* if (newValue != null)
|
* if ((newValue == null)
|
||||||
* map.replace(key, oldValue, newValue);
|
* ? map.remove(key, oldValue)
|
||||||
* else
|
* : map.replace(key, oldValue, newValue))
|
||||||
* map.remove(key, oldValue);
|
* return newValue;
|
||||||
* }}</pre>
|
* }
|
||||||
*
|
* return null;}</pre>
|
||||||
* The default implementation may retry these steps when multiple threads
|
* When multiple threads attempt updates, map operations and the
|
||||||
* attempt updates including potentially calling the remapping function
|
* remapping function may be called multiple times.
|
||||||
* multiple times.
|
|
||||||
*
|
*
|
||||||
* <p>This implementation assumes that the ConcurrentMap cannot contain null
|
* <p>This implementation assumes that the ConcurrentMap cannot contain null
|
||||||
* values and {@code get()} returning null unambiguously means the key is
|
* values and {@code get()} returning null unambiguously means the key is
|
||||||
@ -365,22 +361,21 @@ public interface ConcurrentMap<K,V> extends Map<K,V> {
|
|||||||
* @throws UnsupportedOperationException {@inheritDoc}
|
* @throws UnsupportedOperationException {@inheritDoc}
|
||||||
* @throws ClassCastException {@inheritDoc}
|
* @throws ClassCastException {@inheritDoc}
|
||||||
* @throws NullPointerException {@inheritDoc}
|
* @throws NullPointerException {@inheritDoc}
|
||||||
|
* @throws IllegalArgumentException {@inheritDoc}
|
||||||
* @since 1.8
|
* @since 1.8
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
default V computeIfPresent(K key,
|
default V computeIfPresent(K key,
|
||||||
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
|
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
|
||||||
Objects.requireNonNull(remappingFunction);
|
Objects.requireNonNull(remappingFunction);
|
||||||
V oldValue;
|
for (V oldValue; (oldValue = get(key)) != null; ) {
|
||||||
while ((oldValue = get(key)) != null) {
|
|
||||||
V newValue = remappingFunction.apply(key, oldValue);
|
V newValue = remappingFunction.apply(key, oldValue);
|
||||||
if (newValue != null) {
|
if ((newValue == null)
|
||||||
if (replace(key, oldValue, newValue))
|
? remove(key, oldValue)
|
||||||
return newValue;
|
: replace(key, oldValue, newValue))
|
||||||
} else if (remove(key, oldValue))
|
return newValue;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return oldValue;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -388,27 +383,23 @@ public interface ConcurrentMap<K,V> extends Map<K,V> {
|
|||||||
*
|
*
|
||||||
* @implSpec
|
* @implSpec
|
||||||
* The default implementation is equivalent to performing the following
|
* The default implementation is equivalent to performing the following
|
||||||
* steps for this {@code map}, then returning the current value or
|
* steps for this {@code map}:
|
||||||
* {@code null} if absent:
|
|
||||||
*
|
*
|
||||||
* <pre> {@code
|
* <pre> {@code
|
||||||
* V oldValue = map.get(key);
|
* for (;;) {
|
||||||
* V newValue = remappingFunction.apply(key, oldValue);
|
* V oldValue = map.get(key);
|
||||||
* if (oldValue != null ) {
|
* V newValue = remappingFunction.apply(key, oldValue);
|
||||||
* if (newValue != null)
|
* if (newValue != null) {
|
||||||
* map.replace(key, oldValue, newValue);
|
* if ((oldValue != null)
|
||||||
* else
|
* ? map.replace(key, oldValue, newValue)
|
||||||
* map.remove(key, oldValue);
|
* : map.putIfAbsent(key, newValue) == null)
|
||||||
* } else {
|
* return newValue;
|
||||||
* if (newValue != null)
|
* } else if (oldValue == null || map.remove(key, oldValue)) {
|
||||||
* map.putIfAbsent(key, newValue);
|
|
||||||
* else
|
|
||||||
* return null;
|
* return null;
|
||||||
|
* }
|
||||||
* }}</pre>
|
* }}</pre>
|
||||||
*
|
* When multiple threads attempt updates, map operations and the
|
||||||
* The default implementation may retry these steps when multiple
|
* remapping function may be called multiple times.
|
||||||
* threads attempt updates including potentially calling the remapping
|
|
||||||
* function multiple times.
|
|
||||||
*
|
*
|
||||||
* <p>This implementation assumes that the ConcurrentMap cannot contain null
|
* <p>This implementation assumes that the ConcurrentMap cannot contain null
|
||||||
* values and {@code get()} returning null unambiguously means the key is
|
* values and {@code get()} returning null unambiguously means the key is
|
||||||
@ -418,50 +409,29 @@ public interface ConcurrentMap<K,V> extends Map<K,V> {
|
|||||||
* @throws UnsupportedOperationException {@inheritDoc}
|
* @throws UnsupportedOperationException {@inheritDoc}
|
||||||
* @throws ClassCastException {@inheritDoc}
|
* @throws ClassCastException {@inheritDoc}
|
||||||
* @throws NullPointerException {@inheritDoc}
|
* @throws NullPointerException {@inheritDoc}
|
||||||
|
* @throws IllegalArgumentException {@inheritDoc}
|
||||||
* @since 1.8
|
* @since 1.8
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
default V compute(K key,
|
default V compute(K key,
|
||||||
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
|
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
|
||||||
Objects.requireNonNull(remappingFunction);
|
retry: for (;;) {
|
||||||
V oldValue = get(key);
|
V oldValue = get(key);
|
||||||
for (;;) {
|
// if putIfAbsent fails, opportunistically use its return value
|
||||||
V newValue = remappingFunction.apply(key, oldValue);
|
haveOldValue: for (;;) {
|
||||||
if (newValue == null) {
|
V newValue = remappingFunction.apply(key, oldValue);
|
||||||
// delete mapping
|
if (newValue != null) {
|
||||||
if (oldValue != null || containsKey(key)) {
|
if (oldValue != null) {
|
||||||
// something to remove
|
if (replace(key, oldValue, newValue))
|
||||||
if (remove(key, oldValue)) {
|
return newValue;
|
||||||
// removed the old value as expected
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
else if ((oldValue = putIfAbsent(key, newValue)) == null)
|
||||||
// some other value replaced old value. try again.
|
return newValue;
|
||||||
oldValue = get(key);
|
else continue haveOldValue;
|
||||||
} else {
|
} else if (oldValue == null || remove(key, oldValue)) {
|
||||||
// nothing to do. Leave things as they were.
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
continue retry;
|
||||||
// add or replace old mapping
|
|
||||||
if (oldValue != null) {
|
|
||||||
// replace
|
|
||||||
if (replace(key, oldValue, newValue)) {
|
|
||||||
// replaced as expected.
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// some other value replaced old value. try again.
|
|
||||||
oldValue = get(key);
|
|
||||||
} else {
|
|
||||||
// add (replace if oldValue was null)
|
|
||||||
if ((oldValue = putIfAbsent(key, newValue)) == null) {
|
|
||||||
// replaced
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// some other value replaced old value. try again.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -471,21 +441,25 @@ public interface ConcurrentMap<K,V> extends Map<K,V> {
|
|||||||
*
|
*
|
||||||
* @implSpec
|
* @implSpec
|
||||||
* The default implementation is equivalent to performing the following
|
* The default implementation is equivalent to performing the following
|
||||||
* steps for this {@code map}, then returning the current value or
|
* steps for this {@code map}:
|
||||||
* {@code null} if absent:
|
|
||||||
*
|
*
|
||||||
* <pre> {@code
|
* <pre> {@code
|
||||||
* V oldValue = map.get(key);
|
* for (;;) {
|
||||||
* V newValue = (oldValue == null) ? value :
|
* V oldValue = map.get(key);
|
||||||
* remappingFunction.apply(oldValue, value);
|
* if (oldValue != null) {
|
||||||
* if (newValue == null)
|
* V newValue = remappingFunction.apply(oldValue, value);
|
||||||
* map.remove(key);
|
* if (newValue != null) {
|
||||||
* else
|
* if (map.replace(key, oldValue, newValue))
|
||||||
* map.put(key, newValue);}</pre>
|
* return newValue;
|
||||||
*
|
* } else if (map.remove(key, oldValue)) {
|
||||||
* <p>The default implementation may retry these steps when multiple
|
* return null;
|
||||||
* threads attempt updates including potentially calling the remapping
|
* }
|
||||||
* function multiple times.
|
* } else if (map.putIfAbsent(key, value) == null) {
|
||||||
|
* return value;
|
||||||
|
* }
|
||||||
|
* }}</pre>
|
||||||
|
* When multiple threads attempt updates, map operations and the
|
||||||
|
* remapping function may be called multiple times.
|
||||||
*
|
*
|
||||||
* <p>This implementation assumes that the ConcurrentMap cannot contain null
|
* <p>This implementation assumes that the ConcurrentMap cannot contain null
|
||||||
* values and {@code get()} returning null unambiguously means the key is
|
* values and {@code get()} returning null unambiguously means the key is
|
||||||
@ -495,6 +469,7 @@ public interface ConcurrentMap<K,V> extends Map<K,V> {
|
|||||||
* @throws UnsupportedOperationException {@inheritDoc}
|
* @throws UnsupportedOperationException {@inheritDoc}
|
||||||
* @throws ClassCastException {@inheritDoc}
|
* @throws ClassCastException {@inheritDoc}
|
||||||
* @throws NullPointerException {@inheritDoc}
|
* @throws NullPointerException {@inheritDoc}
|
||||||
|
* @throws IllegalArgumentException {@inheritDoc}
|
||||||
* @since 1.8
|
* @since 1.8
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@ -502,20 +477,23 @@ public interface ConcurrentMap<K,V> extends Map<K,V> {
|
|||||||
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
|
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
|
||||||
Objects.requireNonNull(remappingFunction);
|
Objects.requireNonNull(remappingFunction);
|
||||||
Objects.requireNonNull(value);
|
Objects.requireNonNull(value);
|
||||||
V oldValue = get(key);
|
retry: for (;;) {
|
||||||
for (;;) {
|
V oldValue = get(key);
|
||||||
if (oldValue != null) {
|
// if putIfAbsent fails, opportunistically use its return value
|
||||||
V newValue = remappingFunction.apply(oldValue, value);
|
haveOldValue: for (;;) {
|
||||||
if (newValue != null) {
|
if (oldValue != null) {
|
||||||
if (replace(key, oldValue, newValue))
|
V newValue = remappingFunction.apply(oldValue, value);
|
||||||
return newValue;
|
if (newValue != null) {
|
||||||
} else if (remove(key, oldValue)) {
|
if (replace(key, oldValue, newValue))
|
||||||
return null;
|
return newValue;
|
||||||
}
|
} else if (remove(key, oldValue)) {
|
||||||
oldValue = get(key);
|
return null;
|
||||||
} else {
|
}
|
||||||
if ((oldValue = putIfAbsent(key, value)) == null) {
|
continue retry;
|
||||||
return value;
|
} else {
|
||||||
|
if ((oldValue = putIfAbsent(key, value)) == null)
|
||||||
|
return value;
|
||||||
|
continue haveOldValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,11 +48,14 @@ import java.util.WeakHashMap;
|
|||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentSkipListMap;
|
import java.util.concurrent.ConcurrentSkipListMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.testng.annotations.Test;
|
import org.testng.annotations.Test;
|
||||||
import org.testng.annotations.DataProvider;
|
import org.testng.annotations.DataProvider;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
import static org.testng.Assert.fail;
|
import static org.testng.Assert.fail;
|
||||||
import static org.testng.Assert.assertEquals;
|
import static org.testng.Assert.assertEquals;
|
||||||
import static org.testng.Assert.assertTrue;
|
import static org.testng.Assert.assertTrue;
|
||||||
@ -533,6 +536,191 @@ public class Defaults {
|
|||||||
"Should throw NPE");
|
"Should throw NPE");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A function that flipflops between running two other functions. */
|
||||||
|
static <T,U,V> BiFunction<T,U,V> twoStep(AtomicBoolean b,
|
||||||
|
BiFunction<T,U,V> first,
|
||||||
|
BiFunction<T,U,V> second) {
|
||||||
|
return (t, u) -> {
|
||||||
|
boolean bb = b.get();
|
||||||
|
try {
|
||||||
|
return (b.get() ? first : second).apply(t, u);
|
||||||
|
} finally {
|
||||||
|
b.set(!bb);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulates races by modifying the map within the mapping function.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testConcurrentMap_computeIfAbsent_racy() {
|
||||||
|
final ConcurrentMap<Long,Long> map = new ImplementsConcurrentMap<>();
|
||||||
|
final Long two = 2L;
|
||||||
|
Function<Long,Long> f, g;
|
||||||
|
|
||||||
|
// race not detected if function returns null
|
||||||
|
f = (k) -> { map.put(two, 42L); return null; };
|
||||||
|
assertNull(map.computeIfAbsent(two, f));
|
||||||
|
assertEquals(42L, (long)map.get(two));
|
||||||
|
|
||||||
|
map.clear();
|
||||||
|
f = (k) -> { map.put(two, 42L); return 86L; };
|
||||||
|
assertEquals(42L, (long)map.computeIfAbsent(two, f));
|
||||||
|
assertEquals(42L, (long)map.get(two));
|
||||||
|
|
||||||
|
// mapping function ignored if value already exists
|
||||||
|
map.put(two, 99L);
|
||||||
|
assertEquals(99L, (long)map.computeIfAbsent(two, f));
|
||||||
|
assertEquals(99L, (long)map.get(two));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulates races by modifying the map within the remapping function.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testConcurrentMap_computeIfPresent_racy() {
|
||||||
|
final AtomicBoolean b = new AtomicBoolean(true);
|
||||||
|
final ConcurrentMap<Long,Long> map = new ImplementsConcurrentMap<>();
|
||||||
|
final Long two = 2L;
|
||||||
|
BiFunction<Long,Long,Long> f, g;
|
||||||
|
|
||||||
|
for (Long val : new Long[] { null, 86L }) {
|
||||||
|
map.clear();
|
||||||
|
|
||||||
|
// Function not invoked if no mapping exists
|
||||||
|
f = (k, v) -> { map.put(two, 42L); return val; };
|
||||||
|
assertNull(map.computeIfPresent(two, f));
|
||||||
|
assertNull(map.get(two));
|
||||||
|
|
||||||
|
map.put(two, 42L);
|
||||||
|
f = (k, v) -> { map.put(two, 86L); return val; };
|
||||||
|
g = (k, v) -> {
|
||||||
|
assertSame(two, k);
|
||||||
|
assertEquals(86L, (long)v);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
assertNull(map.computeIfPresent(two, twoStep(b, f, g)));
|
||||||
|
assertFalse(map.containsKey(two));
|
||||||
|
assertTrue(b.get());
|
||||||
|
|
||||||
|
map.put(two, 42L);
|
||||||
|
f = (k, v) -> { map.put(two, 86L); return val; };
|
||||||
|
g = (k, v) -> {
|
||||||
|
assertSame(two, k);
|
||||||
|
assertEquals(86L, (long)v);
|
||||||
|
return 99L;
|
||||||
|
};
|
||||||
|
assertEquals(99L, (long)map.computeIfPresent(two, twoStep(b, f, g)));
|
||||||
|
assertTrue(map.containsKey(two));
|
||||||
|
assertTrue(b.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConcurrentMap_compute_simple() {
|
||||||
|
final ConcurrentMap<Long,Long> map = new ImplementsConcurrentMap<>();
|
||||||
|
BiFunction<Long,Long,Long> fun = (k, v) -> ((v == null) ? 0L : k + v);
|
||||||
|
assertEquals(Long.valueOf(0L), map.compute(3L, fun));
|
||||||
|
assertEquals(Long.valueOf(3L), map.compute(3L, fun));
|
||||||
|
assertEquals(Long.valueOf(6L), map.compute(3L, fun));
|
||||||
|
assertNull(map.compute(3L, (k, v) -> null));
|
||||||
|
assertTrue(map.isEmpty());
|
||||||
|
|
||||||
|
assertEquals(Long.valueOf(0L), map.compute(new Long(3L), fun));
|
||||||
|
assertEquals(Long.valueOf(3L), map.compute(new Long(3L), fun));
|
||||||
|
assertEquals(Long.valueOf(6L), map.compute(new Long(3L), fun));
|
||||||
|
assertNull(map.compute(3L, (k, v) -> null));
|
||||||
|
assertTrue(map.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulates races by modifying the map within the remapping function.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testConcurrentMap_compute_racy() {
|
||||||
|
final AtomicBoolean b = new AtomicBoolean(true);
|
||||||
|
final ConcurrentMap<Long,Long> map = new ImplementsConcurrentMap<>();
|
||||||
|
final Long two = 2L;
|
||||||
|
BiFunction<Long,Long,Long> f, g;
|
||||||
|
|
||||||
|
// null -> null is a no-op; race not detected
|
||||||
|
f = (k, v) -> { map.put(two, 42L); return null; };
|
||||||
|
assertNull(map.compute(two, f));
|
||||||
|
assertEquals(42L, (long)map.get(two));
|
||||||
|
|
||||||
|
for (Long val : new Long[] { null, 86L }) {
|
||||||
|
map.clear();
|
||||||
|
|
||||||
|
f = (k, v) -> { map.put(two, 42L); return 86L; };
|
||||||
|
g = (k, v) -> {
|
||||||
|
assertSame(two, k);
|
||||||
|
assertEquals(42L, (long)v);
|
||||||
|
return k + v;
|
||||||
|
};
|
||||||
|
assertEquals(44L, (long)map.compute(two, twoStep(b, f, g)));
|
||||||
|
assertEquals(44L, (long)map.get(two));
|
||||||
|
assertTrue(b.get());
|
||||||
|
|
||||||
|
f = (k, v) -> { map.remove(two); return val; };
|
||||||
|
g = (k, v) -> {
|
||||||
|
assertSame(two, k);
|
||||||
|
assertNull(v);
|
||||||
|
return 44L;
|
||||||
|
};
|
||||||
|
assertEquals(44L, (long)map.compute(two, twoStep(b, f, g)));
|
||||||
|
assertEquals(44L, (long)map.get(two));
|
||||||
|
assertTrue(map.containsKey(two));
|
||||||
|
assertTrue(b.get());
|
||||||
|
|
||||||
|
f = (k, v) -> { map.remove(two); return val; };
|
||||||
|
g = (k, v) -> {
|
||||||
|
assertSame(two, k);
|
||||||
|
assertNull(v);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
assertNull(map.compute(two, twoStep(b, f, g)));
|
||||||
|
assertNull(map.get(two));
|
||||||
|
assertFalse(map.containsKey(two));
|
||||||
|
assertTrue(b.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulates races by modifying the map within the remapping function.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testConcurrentMap_merge_racy() {
|
||||||
|
final AtomicBoolean b = new AtomicBoolean(true);
|
||||||
|
final ConcurrentMap<Long,Long> map = new ImplementsConcurrentMap<>();
|
||||||
|
final Long two = 2L;
|
||||||
|
BiFunction<Long,Long,Long> f, g;
|
||||||
|
|
||||||
|
for (Long val : new Long[] { null, 86L }) {
|
||||||
|
map.clear();
|
||||||
|
|
||||||
|
f = (v, w) -> { throw new AssertionError(); };
|
||||||
|
assertEquals(99L, (long)map.merge(two, 99L, f));
|
||||||
|
assertEquals(99L, (long)map.get(two));
|
||||||
|
|
||||||
|
f = (v, w) -> { map.put(two, 42L); return val; };
|
||||||
|
g = (v, w) -> {
|
||||||
|
assertEquals(42L, (long)v);
|
||||||
|
assertEquals(3L, (long)w);
|
||||||
|
return v + w;
|
||||||
|
};
|
||||||
|
assertEquals(45L, (long)map.merge(two, 3L, twoStep(b, f, g)));
|
||||||
|
assertEquals(45L, (long)map.get(two));
|
||||||
|
assertTrue(b.get());
|
||||||
|
|
||||||
|
f = (v, w) -> { map.remove(two); return val; };
|
||||||
|
g = (k, v) -> { throw new AssertionError(); };
|
||||||
|
assertEquals(55L, (long)map.merge(two, 55L, twoStep(b, f, g)));
|
||||||
|
assertEquals(55L, (long)map.get(two));
|
||||||
|
assertTrue(map.containsKey(two));
|
||||||
|
assertFalse(b.get()); b.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum IntegerEnum {
|
public enum IntegerEnum {
|
||||||
|
|
||||||
e0, e1, e2, e3, e4, e5, e6, e7, e8, e9,
|
e0, e1, e2, e3, e4, e5, e6, e7, e8, e9,
|
||||||
@ -838,13 +1026,13 @@ public class Defaults {
|
|||||||
|
|
||||||
protected ExtendsAbstractMap(M map) { this.map = map; }
|
protected ExtendsAbstractMap(M map) { this.map = map; }
|
||||||
|
|
||||||
public Set<Map.Entry<K,V>> entrySet() {
|
@Override public Set<Map.Entry<K,V>> entrySet() {
|
||||||
return new AbstractSet<Map.Entry<K,V>>() {
|
return new AbstractSet<Map.Entry<K,V>>() {
|
||||||
public int size() {
|
@Override public int size() {
|
||||||
return map.size();
|
return map.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Iterator<Map.Entry<K,V>> iterator() {
|
@Override public Iterator<Map.Entry<K,V>> iterator() {
|
||||||
final Iterator<Map.Entry<K,V>> source = map.entrySet().iterator();
|
final Iterator<Map.Entry<K,V>> source = map.entrySet().iterator();
|
||||||
return new Iterator<Map.Entry<K,V>>() {
|
return new Iterator<Map.Entry<K,V>>() {
|
||||||
public boolean hasNext() { return source.hasNext(); }
|
public boolean hasNext() { return source.hasNext(); }
|
||||||
@ -853,20 +1041,20 @@ public class Defaults {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean add(Map.Entry<K,V> e) {
|
@Override public boolean add(Map.Entry<K,V> e) {
|
||||||
return map.entrySet().add(e);
|
return map.entrySet().add(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public V put(K key, V value) {
|
@Override public V put(K key, V value) {
|
||||||
return map.put(key, value);
|
return map.put(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple mutable concurrent map implementation that provides only default
|
* A simple mutable concurrent map implementation that provides only default
|
||||||
* implementations of all methods. ie. none of the ConcurrentMap interface
|
* implementations of all methods, i.e. none of the ConcurrentMap interface
|
||||||
* default methods have overridden implementations.
|
* default methods have overridden implementations.
|
||||||
*
|
*
|
||||||
* @param <K> Type of keys
|
* @param <K> Type of keys
|
||||||
@ -875,14 +1063,26 @@ public class Defaults {
|
|||||||
public static class ImplementsConcurrentMap<K,V> extends ExtendsAbstractMap<ConcurrentMap<K,V>, K, V> implements ConcurrentMap<K,V> {
|
public static class ImplementsConcurrentMap<K,V> extends ExtendsAbstractMap<ConcurrentMap<K,V>, K, V> implements ConcurrentMap<K,V> {
|
||||||
public ImplementsConcurrentMap() { super(new ConcurrentHashMap<K,V>()); }
|
public ImplementsConcurrentMap() { super(new ConcurrentHashMap<K,V>()); }
|
||||||
|
|
||||||
// ConcurrentMap reabstracts these methods
|
// ConcurrentMap reabstracts these methods.
|
||||||
|
//
|
||||||
|
// Unlike ConcurrentHashMap, we have zero tolerance for null values.
|
||||||
|
|
||||||
public V replace(K k, V v) { return map.replace(k, v); };
|
@Override public V replace(K k, V v) {
|
||||||
|
return map.replace(requireNonNull(k), requireNonNull(v));
|
||||||
|
}
|
||||||
|
|
||||||
public boolean replace(K k, V v, V vv) { return map.replace(k, v, vv); };
|
@Override public boolean replace(K k, V v, V vv) {
|
||||||
|
return map.replace(requireNonNull(k),
|
||||||
|
requireNonNull(v),
|
||||||
|
requireNonNull(vv));
|
||||||
|
}
|
||||||
|
|
||||||
public boolean remove(Object k, Object v) { return map.remove(k, v); }
|
@Override public boolean remove(Object k, Object v) {
|
||||||
|
return map.remove(requireNonNull(k), requireNonNull(v));
|
||||||
|
}
|
||||||
|
|
||||||
public V putIfAbsent(K k, V v) { return map.putIfAbsent(k, v); }
|
@Override public V putIfAbsent(K k, V v) {
|
||||||
|
return map.putIfAbsent(requireNonNull(k), requireNonNull(v));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user