blob: d472b574c5ca7f24d1ca2fbd886aa1f2000597c7 [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.
*/
/* $Id$ */
package org.apache.fop.fo.properties;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
/**
* Dedicated cache, meant for storing canonical instances
* of property-related classes.
* The public access points are overloaded <code>fetch()</code> methods
* that each correspond to a cached type.
* It is designed especially to be used concurrently by multiple threads,
* drawing heavily upon the principles behind Java 1.5's
* <code>ConcurrentHashMap</code>.
*/
public final class PropertyCache {
private static final int SEGMENT_COUNT = 32; //0x20
private static final int INITIAL_BUCKET_COUNT = SEGMENT_COUNT;
/** bitmask to apply to the hash to get to the
* corresponding cache segment */
private static final int SEGMENT_MASK = SEGMENT_COUNT - 1; //0x1F
/**
* Indicates whether the cache should be used at all
* Can be controlled by the system property:
* org.apache.fop.fo.properties.use-cache
*/
private final boolean useCache;
/** the segments array (length = 32) */
private CacheSegment[] segments = new CacheSegment[SEGMENT_COUNT];
/** the table of hash-buckets */
private CacheEntry[] table = new CacheEntry[INITIAL_BUCKET_COUNT];
private Class runtimeType;
private boolean[] votesForRehash = new boolean[SEGMENT_COUNT];
/* same hash function as used by java.util.HashMap */
private static int hash(Object x) {
return hash(x.hashCode());
}
private static int hash(int hashCode) {
int h = hashCode;
h += ~(h << 9);
h ^= (h >>> 14);
h += (h << 4);
h ^= (h >>> 10);
return h;
}
/* shortcut function */
private static boolean eq(Object p, Object q) {
return (p == q || (p != null && p.equals(q)));
}
/* Class modeling a cached entry */
private static class CacheEntry extends WeakReference {
private volatile CacheEntry nextEntry;
private final int hash;
/* main constructor */
public CacheEntry(Object p, CacheEntry nextEntry, ReferenceQueue refQueue) {
super(p, refQueue);
this.nextEntry = nextEntry;
this.hash = hash(p);
}
/* main constructor */
public CacheEntry(Object p, CacheEntry nextEntry) {
super(p);
this.nextEntry = nextEntry;
this.hash = hash(p);
}
}
/* Wrapper objects to synchronize on */
private static class CacheSegment {
private int count = 0;
}
private void cleanSegment(int segmentIndex) {
CacheSegment segment = segments[segmentIndex];
int oldCount = segment.count;
/* clean all buckets in this segment */
for (int bucketIndex = segmentIndex;
bucketIndex < table.length;
bucketIndex += SEGMENT_COUNT) {
CacheEntry prev = null;
CacheEntry entry = table[bucketIndex];
if (entry == null) {
continue;
}
do {
if (entry.get() == null) {
if (prev == null) {
table[bucketIndex] = entry.nextEntry;
} else {
prev.nextEntry = entry.nextEntry;
}
segment.count--;
assert segment.count >= 0;
} else {
prev = entry;
}
entry = entry.nextEntry;
} while (entry != null);
}
synchronized (votesForRehash) {
if (oldCount > segment.count) {
votesForRehash[segmentIndex] = false;
return;
}
/* cleanup had no effect */
if (!votesForRehash[segmentIndex]) {
/* first time for this segment */
votesForRehash[segmentIndex] = true;
int voteCount = 0;
for (int i = SEGMENT_MASK + 1; --i >= 0;) {
if (votesForRehash[i]) {
voteCount++;
}
}
if (voteCount > SEGMENT_MASK / 4) {
rehash(SEGMENT_MASK);
/* reset votes */
for (int i = SEGMENT_MASK + 1; --i >= 0;) {
votesForRehash[i] = false;
}
}
}
}
}
/*
* Puts a new instance in the cache.
* If the total number of entries for the corresponding
* segment exceeds twice the amount of hash-buckets, a
* cleanup will be performed to try and remove obsolete
* entries.
*/
private void put(Object o) {
int hash = hash(o);
int segmentIndex = hash & SEGMENT_MASK;
CacheSegment segment = segments[segmentIndex];
synchronized (segment) {
int index = hash & (table.length - 1);
CacheEntry entry = table[index];
if (entry == null) {
entry = new CacheEntry(o, null);
table[index] = entry;
segment.count++;
} else {
Object p = entry.get();
if (eq(p, o)) {
return;
} else {
CacheEntry newEntry = new CacheEntry(o, entry);
table[index] = newEntry;
segment.count++;
}
}
if (segment.count > (2 * table.length)) {
cleanSegment(segmentIndex);
}
}
}
/* Gets a cached instance. Returns null if not found */
private Object get(Object o) {
int hash = hash(o);
int index = hash & (table.length - 1);
CacheEntry entry = table[index];
Object q;
/* try non-synched first */
for (CacheEntry e = entry; e != null; e = e.nextEntry) {
if (e.hash == hash
&& (q = e.get()) != null
&& eq(q, o)) {
return q;
}
}
/* retry synched, only if the above attempt did not succeed,
* as another thread may, in the meantime, have added a
* corresponding entry */
CacheSegment segment = segments[hash & SEGMENT_MASK];
synchronized (segment) {
entry = table[index];
for (CacheEntry e = entry; e != null; e = e.nextEntry) {
if (e.hash == hash
&& (q = e.get()) != null
&& eq(q, o)) {
return q;
}
}
}
return null;
}
/*
* Recursively acquires locks on all 32 segments,
* extends the cache and redistributes the entries.
*
*/
private void rehash(int index) {
CacheSegment seg = segments[index];
synchronized (seg) {
if (index > 0) {
/* need to recursively acquire locks on all segments */
rehash(index - 1);
} else {
/* double the amount of buckets */
int newLength = table.length << 1;
if (newLength > 0) { //no overflow?
/* reset segment counts */
for (int i = segments.length; --i >= 0;) {
segments[i].count = 0;
}
CacheEntry[] newTable = new CacheEntry[newLength];
int hash, idx;
Object o;
newLength--;
for (int i = table.length; --i >= 0;) {
for (CacheEntry c = table[i]; c != null; c = c.nextEntry) {
if ((o = c.get()) != null) {
hash = c.hash;
idx = hash & newLength;
newTable[idx] = new CacheEntry(o, newTable[idx]);
segments[hash & SEGMENT_MASK].count++;
}
}
}
table = newTable;
}
}
}
}
/**
* Default constructor.
*
* @param c Runtime type of the objects that will be stored in the cache
*/
public PropertyCache(Class c) {
this.useCache = Boolean.valueOf(System.getProperty(
"org.apache.fop.fo.properties.use-cache", "true")
).booleanValue();
if (useCache) {
for (int i = SEGMENT_MASK + 1; --i >= 0;) {
segments[i] = new CacheSegment();
}
}
this.runtimeType = c;
}
/**
* Generic fetch() method.
* Checks if the given <code>Object</code> is present in the cache -
* if so, returns a reference to the cached instance.
* Otherwise the given object is added to the cache and returned.
*
* @param obj the Object to check for
* @return the cached instance
*/
private Object fetch(Object obj) {
if (!this.useCache) {
return obj;
}
if (obj == null) {
return null;
}
Object cacheEntry = get(obj);
if (cacheEntry != null) {
return cacheEntry;
}
put(obj);
return obj;
}
/**
* Checks if the given {@link Property} is present in the cache -
* if so, returns a reference to the cached instance.
* Otherwise the given object is added to the cache and returned.
*
* @param prop the Property instance to check for
* @return the cached instance
*/
public Property fetch(Property prop) {
return (Property) fetch((Object) prop);
}
/**
* Checks if the given {@link CommonHyphenation} is present in the cache -
* if so, returns a reference to the cached instance.
* Otherwise the given object is added to the cache and returned.
*
* @param chy the CommonHyphenation instance to check for
* @return the cached instance
*/
public CommonHyphenation fetch(CommonHyphenation chy) {
return (CommonHyphenation) fetch((Object) chy);
}
/**
* Checks if the given {@link CommonFont} is present in the cache -
* if so, returns a reference to the cached instance.
* Otherwise the given object is added to the cache and returned.
*
* @param cf the CommonFont instance to check for
* @return the cached instance
*/
public CommonFont fetch(CommonFont cf) {
return (CommonFont) fetch((Object) cf);
}
/**
* Checks if the given {@link CommonBorderPaddingBackground} is present in the cache -
* if so, returns a reference to the cached instance.
* Otherwise the given object is added to the cache and returned.
*
* @param cbpb the CommonBorderPaddingBackground instance to check for
* @return the cached instance
*/
public CommonBorderPaddingBackground fetch(CommonBorderPaddingBackground cbpb) {
return (CommonBorderPaddingBackground) fetch((Object) cbpb);
}
/**
* Checks if the given {@link CommonBorderPaddingBackground.BorderInfo} is present
* in the cache - if so, returns a reference to the cached instance.
* Otherwise the given object is added to the cache and returned.
*
* @param bi the BorderInfo instance to check for
* @return the cached instance
*/
public CommonBorderPaddingBackground.BorderInfo fetch(
CommonBorderPaddingBackground.BorderInfo bi) {
return (CommonBorderPaddingBackground.BorderInfo) fetch((Object) bi);
}
/** {@inheritDoc} */
public String toString() {
return super.toString() + "[runtimeType=" + this.runtimeType + "]";
}
}