T
- the type of element that is being accessed by readers and writerspublic class DoubleInstanceLock<T> extends Object
Readers never block, even when a write operation is in progress. Essentially, two instances of the data are kept (hence the name). Reads all safely happen against one instance while writes happen to the other. The two versions are atomically swapped to make changes visible to readers. Writers must make the same change to both halves: once to the instance not being read, then again to the other side after the instances are swapped.
All write operations must be deterministic, such that applying it to two equal objects produces two equal results. Otherwise, when writes are applied to both halves, the state may be corrupted and the two instances could get out of sync.
It is important that read and write operations neither return references to the object they
read/write nor allow such a reference to otherwise "escape" (e.g. storing it in another object
that ends up publishing the value to other threads). This is because any thread that tries to
inspect or interact with that value will encounter concurrency issues as the object is not
thread-safe. The only thread-safe interaction is through this
DoubleInstanceLock
. If an operation needs to provide access to the object, use
snapshot()
instead of the read or write methods. Note that, since actual copies of the
instance are made lazily (to defer the cost of copying to the next write operation and to allow
multiple readers to share the same snapshot), mutations should never be made to a
snapshot.
The object must be cloneable so that stable snapshots of the object can be captured. (Also, it
is cloned initially to generate the two instances from the one object.) It is very
important that no operations be performed directly against the object after the
DoubleInstanceLock
is created. All subsequent operations must use read and write methods
on the DoubleInstanceLock
to access the data. Failing to comply can result in corruption
of the data and undefined behavior.
This object can safely be used in a ForkJoinPool
. Blocking operations (for writes)
will use a ForkJoinPool.ManagedBlocker
if invoked from such a pool.
Modifier and Type | Method and Description |
---|---|
static <T extends Cloneable> |
newLock(T t)
Constructs a new double instance lock around the given object.
|
static <T> DoubleInstanceLock<T> |
newLock(T t,
Cloner<T> cloner)
Constructs a new double instance lock around the given object that uses the given cloner to
create additional instances.
|
<U> U |
read(Function<? super T,? extends U> action)
Performs a read operation that produces a result value.
|
void |
readWith(Consumer<? super T> action)
Performs a read operation that does not produce a result value.
|
T |
snapshot()
Returns a snapshot of the data, like for performing expensive work that requires a strongly
consistent view.
|
<U> U |
write(Function<? super T,? extends U> action)
Performs a write operation that produces a result value.
|
void |
writeWith(Consumer<? super T> action)
Performs a write operation that does not produce a value.
|
public static <T extends Cloneable> DoubleInstanceLock<T> newLock(T t)
Object.clone()
method is used to create additional instances.t
- an objectpublic static <T> DoubleInstanceLock<T> newLock(T t, Cloner<T> cloner)
Note: If the supplied cloner does not properly adhere to the contract, (e.g. does not produce a different instance that is otherwise identical to the input object) then the two instances of data used by the lock can get out of sync. This means data corruption and that subsequent operations will be undefined.
t
- an objectcloner
- a clonerpublic void readWith(Consumer<? super T> action)
Note: if the action performs any mutations on the object passed to it, this data structure will become corrupted and further operations will produce undefined results.
action
- the read operation to performpublic <U> U read(Function<? super T,? extends U> action)
Note: if the action performs any mutations on the object passed to it, this data structure will become corrupted and further operations will produce undefined results.
action
- the read operation to performpublic void writeWith(Consumer<? super T> action)
write(Function)
or writeWith(Consumer)
then a ReentranceException
is thrown.
Note: if the action is not deterministic, then applying the action could cause divergence of the two sides of this structure, thus causing corruption and making the results of subsequent operations undefined.
action
- the write operation to performReentranceException
- if the given action is reentrantpublic <U> U write(Function<? super T,? extends U> action)
write(Function)
or writeWith(Consumer)
then a ReentranceException
is thrown.
Note: if the action is not deterministic, then applying the action could cause divergence of the two sides of this structure, thus causing corruption and making the results of subsequent operations undefined.
action
- the write operation to performReentranceException
- if the given action is reentrantpublic T snapshot()
Note: the returned object should not be modified in anyway. For efficiency, creation of the snapshot is deferred to when a writer needs to modify the object. This allows multiple readers to share snapshots. Mutating the returned object will corrupt the data for other readers and result in undefined behavior for subsequent operations.