/*
* JVSTM: a Java library for Software Transactional Memory
* Copyright (C) 2005 INESC-ID Software Engineering Group
* http://www.esw.inesc-id.pt
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* Author's contact:
* INESC-ID Software Engineering Group
* Rua Alves Redol 9
* 1000 - 029 Lisboa
* Portugal
*/
package jvstm;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.ReentrantLock;
import static jvstm.ReadWriteTransaction.NULL_VALUE;
/** Transactional array implementation for the JVSTM optimized for read-heavy workloads
*
* This implementation uses less memory and presents better read performance than other options.
* For more details, check the notes on the source-code.
*
* Note that:
* - Even on jvstm-lock-free, writing (committing) to a VArray uses locking; no locking is needed
* for reading
* - Under a workload with lots of writes, a simple array of VBoxes might present better performance,
* and solves the aforementioned locking issue
**/
/*
* Main design notes:
* - VArray keeps latest versions of values on VArray.values
* - Older versions are kept in a linked-list of VArrayLogNode, similar to VBoxBody, but
* only one VArrayLogNode keeps all of the old values that a transaction changed
* - VArrayEntry is used to represent the pair (array, index) inside read and write-sets
*
* How does it work (sketch)
* To read a value, we check the array version. If it's older than our tx number, we
* return the value from the latest version array.
* If, instead, we need an older value, we have to check the logs to find the value
* corresponding to our world version, and return that version, if found; otherwise,
* the value from the latest version array is returned -- it just means that the value
* was never changed, although other array positions were.
*
* To commit (see TopLevelTransaction), the writeset is iterated, and the entries are
* distributed into an array of entries for each VArray that is going to be changed.
* This array is then sorted, and used to generate the logEntryIndexes.
* After this operation, commit is called on the array, resulting in the log node
* being created, and the writeback done.
*/
final class VArrayLogNode<E> implements GarbageCollectable {
/* These fields are kept outside the VArrayLogNode to allow the jvstm GC algorithm
* to unlink the body.
*/
static final private class VArrayLogNodeBody<E> {
final VArrayLogNode<E> next;
final int[] logEntryIndexes;
final E[] logEntryValues;
VArrayLogNodeBody(VArrayLogNode<E> next, int[] logEntryIndexes, E[] logEntryValues) {
this.next = next;
this.logEntryIndexes = logEntryIndexes;
this.logEntryValues = logEntryValues;
}
}
final int version;
final VArrayLogNodeBody<E> body;
VArrayLogNode(int[] logEntryIndexes, E[] logEntryValues, int version, VArrayLogNode<E> next) {
this.version = version;
this.body = new VArrayLogNodeBody<E>(next, logEntryIndexes, logEntryValues);
}
/* Algorithm:
* - Recurse until we find the VArrayLogNode with the SMALLEST version still >= minVersion
* - Look for the index we want from that version forward towards the beginning of the list
* - Return the first value found; otherwise return null
*/
public E getLogValue(int index, int minVersion) {
if (version < minVersion) return null;
E value = null;
if (body.next != null) value = body.next.getLogValue(index, minVersion);
if (value == null) {
// If no value was found in previous nodes, try to find it on the current node
int pos = Arrays.binarySearch(body.logEntryIndexes, index);
if (pos >= 0) {
// A special case may occur here: we might read NULL_VALUE, meaning that the value
// was truly a null, or we might read null, meaning that we are on a partially-initialized
// log, and we ignore what we found.
value = body.logEntryValues[pos];
}
}
return value;
}
// this static field is used to change the non-static final field "body"
// see the comments on the clearPrevious method
private static final Field BODY_FIELD;
static {
try {
BODY_FIELD = VArrayLogNode.class.getDeclaredField("body");
BODY_FIELD.setAccessible(true);
} catch (NoSuchFieldException nsfe) {
throw new Error("JVSTM error: couldn't get access to the VArrayLogNode.body field");
}
}
public void clearPrevious() {
// Copied from VBoxBody.clearPrevious()
// we set the body field to null via reflection because it is
// a final field
// making the field final is crucial to ensure that the field
// is properly initialized (and visible to other threads)
// after an instance of VArrayLogNode is constructed, as
// per the new Java Memory Model (JSR133)
// also, according to the Java specification, we may change a
// final field only via reflection and in some specific cases
// (such as object reconstruction after deserialization)
// even though this use is not the case, the potential
// problems that may occur do not affect the correcteness of
// the system: we just want to set the field to null to allow
// the garbage collector to do its thing...
try {
BODY_FIELD.set(this, null);
} catch (IllegalAccessException iae) {
throw new Error("JVSTM error: cannot set the VArrayLogNode.body field to null");
}
}
}
final class VArrayEntry<E> implements Comparable<VArrayEntry<E>> {
final VArray<E> array;
final int index;
// This field is used for two purposes:
// 1) If this VArrayEntry is being used inside a read-set, it contains the read value
// 2) If this VarrayEntry is being used inside a write-set, it contains the value to be
// written during the Tx commit operation
private E object;
// This field is used for parallel nested writes in the array so that
// a nested tx may safely read VArrayEntries that were written in the nesting tree
// -- Only used when VArrayEntry is in the read-set of a parallel nested transaction
volatile int nestedVersion;
// Also used for nesting, represents the owner of the entry when it was read.
// -- Only used when VArrayEntry is in the read-set of a parallel nested transaction
ReadWriteTransaction owner;
VArrayEntry(VArray<E> array, int index) {
this.array = array;
this.index = index;
}
@Override
public int hashCode() {
return array.hashCode() + index;
}
@Override
public boolean equals(Object o) {
if (o instanceof VArrayEntry<?>) {
VArrayEntry<?> other = (VArrayEntry<?>) o;
return (array == other.array) && (index == other.index);
}
return false;
}
@Override
public int compareTo(VArrayEntry<E> other) {
if (array != other.array) {
throw new AssertionError("Cannot compare with a VArrayEntry belonging to different VArray");
}
return index - other.index;
}
public E getValue(int maxVersion) {
// Keep read value for later validation
object = getInternalValue(maxVersion);
return object;
}
private E getInternalValue(int maxVersion) {
// Read value from array (volatile read)
E value = array.values.get(index);
// Read array version
int version = array.version;
// If version <= maxVersion, array hasn't changed since we started the current transaction
if (version <= maxVersion) return value;
// Otherwise, check the log for the value
E logValue = array.log.getLogValue(index, maxVersion);
return logValue != null ?
(logValue == NULL_VALUE ? null : logValue)
: value;
}
// Only used when VArrayEntry is part of the read-set
public boolean validate() {
return object == array.values.get(index);
}
public void setReadOwner(ReadWriteTransaction owner) {
this.owner = owner;
}
// Only used when VArrayEntry is part of the write-set
public void setWriteValue(E value, int nestedVersion) {
object = value;
this.nestedVersion = nestedVersion;
}
// Only used when VArrayEntry is part of the write-set
public E getWriteValue() {
return object;
}
}
public class VArray<E> {
public final AtomicReferenceArray<E> values;
public int version;
public final int length;
public VArrayLogNode<E> log;
final ReentrantLock writebackLock = new ReentrantLock();
public VArray(int size) {
values = new AtomicReferenceArray<E>(size);
length = size;
}
@SuppressWarnings("static-access")
public E get(int index) {
rangeCheck(index);
// TODO: Apply the same optimization as in VBox.get()
Transaction tx = Transaction.current();
if (tx == null) {
tx = Transaction.begin(true);
E value = tx.getArrayValue(new VArrayEntry<E>(this, index));
tx.commit();
return value;
} else {
return tx.getArrayValue(new VArrayEntry<E>(this, index));
}
}
@SuppressWarnings("static-access")
public void put(int index, E newE) {
rangeCheck(index);
Transaction tx = Transaction.current();
if (tx == null) {
tx = Transaction.begin();
tx.setArrayValue(new VArrayEntry<E>(this, index), newE);
tx.commit();
} else {
tx.setArrayValue(new VArrayEntry<E>(this, index), newE);
}
}
private void rangeCheck(int index) {
if (index < 0 || index >= length) throw new IndexOutOfBoundsException();
}
@SuppressWarnings("unchecked")
public GarbageCollectable commit(int txNumber, VArrayEntry[] writesToCommit, int[] logEntryIndexes) {
// Prepare arrays for log
E[] logEntryValues = (E[]) new Object[writesToCommit.length];
// Create and place log node
log = new VArrayLogNode<E>(logEntryIndexes, logEntryValues, txNumber - 1, log);
// Bump array version
version = txNumber;
// Proceed with normal writeback and populating log values
int i = 0;
for (VArrayEntry<E> entry : writesToCommit) {
// Read old value from the array, and copy it to the log
E oldValue = values.get(entry.index);
if (oldValue == null) oldValue = (E) NULL_VALUE;
logEntryValues[i++] = oldValue;
// Write the new value
// Using a lazySet because we don't need the new value to be seen by other threads
// as soon as possible. In fact, it would be nice if only threads with transactions
// created after we finish our commit see the new value.
// For more details see the java.util.concurrent.atomic package description javadoc.
values.lazySet(entry.index, entry.getWriteValue()); // Volatile write!
}
return log;
}
}