blob: 08c6c3cc7cb13f1a220be2176cc25026a2debef9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.openjpa.lib.util.concurrent;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Enumeration;
import java.util.Set;
import java.util.Collection;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.AbstractCollection;
import java.util.Random;
import java.util.HashSet;
import java.util.TreeSet;
import org.apache.commons.collections.set.MapBackedSet;
/**
* A subclass of {@link ConcurrentHashMap} that allows null keys and values.
* In exchange, it weakens the contract of {@link #putIfAbsent} and the other
* concurrent methods added in {@link #ConcurrentHashMap}.
*
* @since 1.1.0
*/
public class NullSafeConcurrentHashMap extends ConcurrentHashMap {
private enum Markers {
NULL,
MAP_BACKED_SET_DUMMY_VAL
}
// The second argument is used within MapBackedSet as the value for
// all the key-val pairs that are put into the underlying Map. This
// is required for our usage since ConcurrentHashMap does not allow
// null values.
private Set randomKeys = MapBackedSet.decorate(
new ConcurrentHashMap(), Markers.MAP_BACKED_SET_DUMMY_VAL);
private Random random = new Random();
public NullSafeConcurrentHashMap(int size, float load,
int concurrencyLevel) {
super(size, load, concurrencyLevel);
}
public NullSafeConcurrentHashMap() {
}
/**
* Returns internal representation for object.
*/
private static Object maskNull(Object o) {
return (o == null ? Markers.NULL : o);
}
/**
* Returns object represented by specified internal representation.
*/
private static Object unmaskNull(Object o) {
return (o == Markers.NULL ? null : o);
}
public Entry removeRandom() {
// this doesn't just use randomEntryIterator() because that iterator
// has weaker concurrency guarantees than this method. In particular,
// this method will continue to attempt to remove random entries even
// as other threads remove the same entries, whereas the random
// iterator may return values that have been removed.
for (Iterator iter = randomKeys.iterator(); iter.hasNext(); ) {
// randomKeys contains null-masked data
Object key = iter.next();
if (key != null && randomKeys.remove(key)) {
Object val = super.remove(key);
if (val != null)
return new EntryImpl(unmaskNull(key), unmaskNull(val));
}
}
// if randomKeys is empty, fall back to non-random behavior.
for (Iterator iter = super.keySet().iterator(); iter.hasNext(); ) {
Object key = iter.next();
if (key == null)
continue;
Object val = super.remove(key);
if (val != null)
return new EntryImpl(unmaskNull(key), unmaskNull(val));
}
return null;
}
/**
* The returned data structure should not be shared among multiple
* threads.
*/
public Iterator<Entry> randomEntryIterator() {
return new Iterator<Entry>() {
Iterator randomIter = randomKeys.iterator();
Iterator nonRandomIter = NullSafeConcurrentHashMap.super.keySet()
.iterator();
Set returned = new HashSet();
Entry next;
boolean nextSet = false;
public boolean hasNext() {
// we've set the next value and we haven't returned it yet
if (nextSet)
return true;
// compute the next value. If the computation returns null,
// return false. Else, store the next value and return true.
Object nextKey;
Object nextValue;
if (randomIter.hasNext()) {
nextKey = randomIter.next();
nextValue = NullSafeConcurrentHashMap.super.get(nextKey);
if (nextValue != null) {
returned.add(nextKey);
next = new EntryImpl(unmaskNull(nextKey),
unmaskNull(nextValue));
nextSet = true;
return true;
}
}
while (nonRandomIter.hasNext()) {
nextKey = nonRandomIter.next();
if (returned.contains(nextKey))
continue;
nextValue = NullSafeConcurrentHashMap.super.get(nextKey);
if (nextValue != null) {
returned.add(nextKey);
next = new EntryImpl(unmaskNull(nextKey),
unmaskNull(nextValue));
nextSet = true;
return true;
}
}
return false;
}
public Entry next() {
// hasNext() will initialize this.next
if (!nextSet && !hasNext())
return null;
// if we get here, then we're about to return a next value
nextSet = false;
if (containsKey(next.getKey()))
return next;
// something has changed since the last iteration (presumably
// due to multi-threaded access to the underlying data
// structure); recurse
return next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
public Object remove(Object key) {
Object maskedKey = maskNull(key);
Object val = unmaskNull(super.remove(maskedKey));
randomKeys.remove(maskedKey);
return val;
}
@Override
public boolean remove(Object key, Object value) {
Object maskedKey = maskNull(key);
boolean val = super.remove(maskedKey, maskNull(value));
randomKeys.remove(maskedKey);
return val;
}
@Override
public boolean replace(Object key, Object oldValue, Object newValue) {
return super.replace(maskNull(key), maskNull(oldValue),
maskNull(newValue));
}
@Override
public Object replace(Object key, Object value) {
return unmaskNull(super.replace(maskNull(key), maskNull(value)));
}
@Override
public Object putIfAbsent(Object key, Object value) {
Object maskedKey = maskNull(key);
Object superVal = super.putIfAbsent(maskedKey, maskNull(value));
addRandomKey(maskedKey);
return unmaskNull(superVal);
}
@Override
public Object put(Object key, Object value) {
Object maskedKey = maskNull(key);
Object superVal = super.put(maskedKey, maskNull(value));
addRandomKey(maskedKey);
return unmaskNull(superVal);
}
/**
* Potentially adds <code>maskedKey</ccode> to the set of random keys
* to be removed by {@link #removeRandom()}.
*
* @since 1.1.0
*/
private void addRandomKey(Object maskedKey) {
// Add one in every three keys to the set. Only do this when
// there are less than 16 elements in the random key set; this
// means that the algorithm will be pseudo-random for up to
// 16 removes (either via removeRandom() or normal remove()
// calls) that have no intervening put() calls.
if (random != null && randomKeys.size() < 16 && random.nextInt(10) < 3)
randomKeys.add(maskedKey);
}
@Override
public Object get(Object key) {
return unmaskNull(super.get(maskNull(key)));
}
@Override
public boolean containsKey(Object key) {
return super.containsKey(maskNull(key));
}
@Override
public boolean containsValue(Object value) {
return super.containsValue(maskNull(value));
}
@Override
public boolean contains(Object value) {
throw new UnsupportedOperationException();
}
@Override
public Enumeration elements() {
throw new UnsupportedOperationException();
}
@Override
public Set entrySet() {
return new TranslatingSet(super.entrySet()) {
protected Object unmask(Object internal) {
final Entry e = (Entry) internal;
return new Entry() {
public Object getKey() {
return unmaskNull(e.getKey());
}
public Object getValue() {
return unmaskNull(e.getValue());
}
public Object setValue(Object value) {
return unmaskNull(e.setValue(maskNull(value)));
}
@Override
public int hashCode() {
return e.hashCode();
}
};
}
};
}
@Override
public Enumeration keys() {
throw new UnsupportedOperationException();
}
@Override
public Set keySet() {
return new TranslatingSet(super.keySet()) {
protected Object unmask(Object internal) {
return unmaskNull(internal);
}
};
}
@Override
public Collection values() {
return new TranslatingCollection(super.values()) {
protected Object unmask(Object internal) {
return unmaskNull(internal);
}
};
}
private abstract class TranslatingSet extends AbstractSet {
private Set backingSet;
private TranslatingSet(Set backing) {
this.backingSet = backing;
}
protected abstract Object unmask(Object internal);
public Iterator iterator() {
final Iterator iterator = backingSet.iterator();
return new Iterator() {
public boolean hasNext() {
return iterator.hasNext();
}
public Object next() {
return unmask(iterator.next());
}
public void remove() {
iterator.remove();
}
};
}
public int size() {
return backingSet.size();
}
}
private abstract class TranslatingCollection extends AbstractCollection {
private Collection backingCollection;
private TranslatingCollection(Collection backing) {
this.backingCollection = backing;
}
protected abstract Object unmask(Object internal);
public Iterator iterator() {
final Iterator iterator = backingCollection.iterator();
return new Iterator() {
public boolean hasNext() {
return iterator.hasNext();
}
public Object next() {
return unmask(iterator.next());
}
public void remove() {
iterator.remove();
}
};
}
public int size() {
return backingCollection.size();
}
}
private class EntryImpl implements Entry {
final Object key;
final Object val;
private EntryImpl(Object key, Object val) {
this.key = key;
this.val = val;
}
public Object getKey() {
return key;
}
public Object getValue() {
return val;
}
public Object setValue(Object value) {
throw new UnsupportedOperationException();
}
}
public interface KeyFilter {
/**
* @param key may be null
* @return whether or not <code>key</code> shuold be excluded
*/
public boolean exclude(Object key);
}
}