net.openhft.chronicle.bytes.util.AbstractInterner Maven / Gradle / Ivy
/*
* Copyright (c) 2016-2022 chronicle.software
*
* https://chronicle.software
*
* 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 net.openhft.chronicle.bytes.util;
import net.openhft.chronicle.bytes.Bytes;
import net.openhft.chronicle.bytes.BytesStore;
import net.openhft.chronicle.core.Maths;
import net.openhft.chronicle.core.annotation.NonNegative;
import net.openhft.chronicle.core.io.ClosedIllegalStateException;
import net.openhft.chronicle.core.io.IORuntimeException;
import net.openhft.chronicle.core.io.IOTools;
import net.openhft.chronicle.core.io.ThreadingIllegalStateException;
import org.jetbrains.annotations.NotNull;
import java.nio.BufferUnderflowException;
import java.util.Objects;
import java.util.stream.Stream;
import static net.openhft.chronicle.core.Jvm.uncheckedCast;
/**
* This class provides a caching mechanism that returns a value which matches the decoded bytes. It does not
* guarantee the return of the same object across different invocations or from different threads, but it
* guarantees that the contents will be the same. Although not strictly thread-safe, it behaves correctly
* under concurrent access.
*
* The main usage is to reduce the amount of memory used by creating new objects when the same byte sequence is
* repeatedly decoded into an object.
*
* This cache only guarantees it will provide a String which matches the decoded bytes.
*
* It doesn't guarantee it will always return the same object,
* nor that different threads will return the same object,
* though the contents should always be the same.
*
* While not technically thread safe, it should still behave correctly.
*
* Abstract base class for implementing an interning mechanism, which helps
* in reusing instances of immutable objects. This class is designed to store objects
* and return previously stored instances that are equal to the required instance.
*
* Note: The interning cache may not always return the same object instance, but
* the contents of the instances will be equal.
* *
*
* @param the type of the object being interned
* @author peter.lawrey
*/
@SuppressWarnings("rawtypes")
public abstract class AbstractInterner {
protected final InternerEntry[] entries;
protected final int mask;
protected final int shift;
protected boolean toggle = false;
/**
* Constructor for creating an intern cache with the given capacity. The capacity will be adjusted to the next
* power of 2 if it is not already a power of 2, to a limit of {@code 1 << 30}.
*
* @param capacity the desired capacity for the intern cache
*/
protected AbstractInterner(@NonNegative int capacity){
int n = Maths.nextPower2(capacity, 128);
shift = Maths.intLog2(n);
entries = uncheckedCast(new InternerEntry[n]);
mask = n - 1;
}
/**
* Returns the 32-bit hash code of the given bytes store and length.
*
* @param bs the bytes store
* @param length the length
* @return the 32-bit hash code
* @throws BufferUnderflowException If there is not enough data in the buffer
* @throws ClosedIllegalStateException If the resource has been released or closed.
* @throws ThreadingIllegalStateException If this resource was accessed by multiple threads in an unsafe way
*/
private static int hash32(@NotNull BytesStore, ?> bs, @NonNegative int length) throws IllegalStateException, BufferUnderflowException {
return bs.fastHash(bs.readPosition(), length);
}
/**
* Interns the specified Bytes object. If the Bytes object is already in the cache,
* this method returns the cached instance; otherwise, it adds the Bytes object to the cache
* and returns the newly cached instance. The length of Bytes object for interning is determined
* by the remaining readable bytes.
*
* @param cs the Bytes object to intern
* @return the interned instance
* @throws IORuntimeException If an I/O error occurs
* @throws BufferUnderflowException If there is not enough data in the buffer
* @throws ClosedIllegalStateException If the resource has been released or closed.
* @throws ThreadingIllegalStateException If this resource was accessed by multiple threads in an unsafe way
*/
public T intern(@NotNull Bytes> cs)
throws IORuntimeException, BufferUnderflowException, IllegalStateException {
return intern((BytesStore) cs, (int) cs.readRemaining());
}
/**
* Interns the specified BytesStore object. If the BytesStore object is already in the cache,
* this method returns the cached instance; otherwise, it adds the BytesStore object to the cache
* and returns the newly cached instance. The length of BytesStore object for interning is determined
* by the remaining readable bytes.
*
* @param cs the BytesStore object to intern
* @return the interned instance
* @throws IORuntimeException If an I/O error occurs
* @throws BufferUnderflowException If there is not enough data in the buffer
* @throws ClosedIllegalStateException If the resource has been released or closed.
* @throws ThreadingIllegalStateException If this resource was accessed by multiple threads in an unsafe way
*/
public T intern(@NotNull BytesStore, ?> cs)
throws IORuntimeException, BufferUnderflowException, IllegalStateException {
return intern(cs, (int) cs.readRemaining());
}
/**
* Interns the specified Bytes object of a given length. If the Bytes object is already in the cache,
* this method returns the cached instance; otherwise, it adds the Bytes object to the cache
* and returns the newly cached instance.
*
* @param cs the Bytes object to intern
* @param length the length of the Bytes object to intern
* @return the interned instance
* @throws IORuntimeException If an I/O error occurs
* @throws BufferUnderflowException If there is not enough data in the buffer
* @throws ClosedIllegalStateException If the resource has been released or closed.
* @throws ThreadingIllegalStateException If this resource was accessed by multiple threads in an unsafe way
*/
public T intern(@NotNull Bytes> cs, @NonNegative int length)
throws IORuntimeException, BufferUnderflowException, IllegalStateException {
return intern((BytesStore) cs, length);
}
/**
* Interns the specified Bytes. If the Bytes are already in the cache, this method returns the cached instance;
* otherwise, it adds the Bytes to the cache and returns the newly cached instance.
*
* @param cs the Bytes to intern
* @param length of bytes to read
* @return the interned instance
* @throws IORuntimeException If an I/O error occurs
* @throws BufferUnderflowException If there is not enough data in the buffer
* @throws ClosedIllegalStateException If the resource has been released or closed.
* @throws ThreadingIllegalStateException If this resource was accessed by multiple threads in an unsafe way
*/
public T intern(@NotNull BytesStore, ?> cs, @NonNegative int length)
throws IORuntimeException, BufferUnderflowException, IllegalStateException {
if (length > entries.length)
return getValue(cs, length);
// Todo: This needs to be reviewed: UnsafeMemory UNSAFE loadFence
int hash = hash32(cs, length);
int h = hash & mask;
InternerEntry s = entries[h];
if (s != null && s.bytes.length() == length && s.bytes.equalBytes(cs, length))
return s.t;
int h2 = (hash >> shift) & mask;
InternerEntry s2 = entries[h2];
if (s2 != null && s2.bytes.length() == length && s2.bytes.equalBytes(cs, length))
return s2.t;
@NotNull T t = getValue(cs, length);
final byte[] bytes = new byte[length];
@NotNull BytesStore, ?> bs = BytesStore.wrap(bytes);
IOTools.unmonitor(bs);
cs.read(cs.readPosition(), bytes, 0, length);
entries[s == null || (s2 != null && toggle()) ? h : h2] = new InternerEntry<>(bs, t);
// UnsafeMemory UNSAFE storeFence
return t;
}
/**
* Retrieves the value corresponding to the bytes store and length.
* This method must be implemented by subclasses.
*
* @param bs the bytes store
* @param length the length of the data in the bytes store
* @return the value corresponding to the given bytes store and length
* @throws IORuntimeException If an IO error occurs
* @throws BufferUnderflowException If there is not enough data in the buffer
* @throws ClosedIllegalStateException If the resource has been released or closed.
* @throws ThreadingIllegalStateException If this resource was accessed by multiple threads in an unsafe way
*/
@NotNull
protected abstract T getValue(BytesStore, ?> bs, @NonNegative int length)
throws IORuntimeException, IllegalStateException, BufferUnderflowException;
/**
* Toggles the internal toggle state and returns its new value.
*
* @return the new state of the toggle
*/
protected boolean toggle() {
toggle = !toggle;
return toggle;
}
/**
* Returns the number of non-null values in the interner entries.
*
* @return the count of non-null values
*/
public int valueCount() {
return (int) Stream.of(entries).filter(Objects::nonNull).count();
}
/**
* Represents an entry in the interner.
*
* @param the type of the object being interned
*/
private static final class InternerEntry {
final BytesStore, ?> bytes;
final T t;
/**
* Constructs an InternerEntry with the given bytes store and value.
*
* @param bytes the bytes store
* @param t the value
*/
InternerEntry(BytesStore, ?> bytes, T t) {
this.bytes = bytes;
this.t = t;
}
}
}