net.diversionmc.async.mutex.Mutex Maven / Gradle / Ivy
Show all versions of promise Show documentation
package net.diversionmc.async.mutex;
import net.diversionmc.async.Blocking;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
/**
* {@link Mutex} is a thread-safe wrapper for a value with per-thread mutual exclusion. This
* means only one thread may be accessing the stored value at a time, where others will have to
* wait.
*
*
* For this API/Implementation, {@link Mutex} associates {@link ReentrantLock} with a value, and
* lets users modify the value when an {@link Mutex.Access} is held. This allows for a simple
* {@link AutoCloseable} try-with-resources usage pattern via {@link #access()}, while also allowing
* for the thread to access the value in a reentrant fashion without blocking itself by accident. A
* functional variant of this API is also available via {@link #access(Consumer)}.
*
*
* This class is not based around {@link Optional}, and having a {@link Mutex} with a {@code null}
* value is a common occurrence. If you want to store {@link Optional} you have to do it explicitly.
* See {@link Mutex.Access#uninitialized()}.
*
*
* Caution
*
*
* Be careful when pulling a value out of the {@link Mutex} - you might be mutating underlying value
* without synchronization. The exceptions are immutable values (like {@link Number}s,
* {@link String}s, {@link Record record}s of immutable values, etc.) and other stored
* {@link Mutex}es and similar structures, since they have their own synchronization.
*
*
* Examples
*
*
* Following example shows how to use {@link Mutex} to synchronize an {@link ArrayList} of
* {@link String}s.
*
{@code
* import java.util.ArrayList;
* import net.diversionmc.async.mutex.Mutex;
*
* // creating a `Mutex`
* var mutex = Mutex.of(new ArrayList());
*
* // accessing the `Mutex` (from any thread)
* try (var access = mutex.access()) { // blocking
* // lock happens here
* var list = access.get();
* list.add("hello");
* // unlock happens here
* }
*
* // taking a value from a Mutex
* var list = mutex.take(); // replaces stored value with `null`
* assert list.equals(List.of("hello"));
* }
*
*
* This example shows how to create a simple critical section using a {@link Mutex}.
*
{@code
* import net.diversionmc.async.mutex.Mutex;
*
* // global
* static Mutex> criticalSection = Mutex.of();
* static int counter = 0;
*
* // in all threads
* try (var ignored = criticalSection.access()) {
* // critical section here
* // atomic read + write is guaranteed
* var current = counter;
* counter = current + 1;
* }
* }
*
* @param Stored value type. Nullable ({@link Mutex.Access#uninitialized() uninitialized}).
*/
public final class Mutex {
private final ReentrantLock lock = new ReentrantLock();
private T value;
/**
* Creates an {@link Mutex.Access#uninitialized() uninitialized} {@link Mutex}. It is highly
* recommended to give out this {@link Mutex} to other users only after filling it with a value.
*
*
* Examples
*
*
{@code
* import net.diversionmc.async.mutex.Mutex;
*
* var mutex = new Mutex();
* assert mutex.uninitialized();
* }
*/
public Mutex() {
}
/**
* A static method alias of {@link #Mutex()} that creates an
* {@link Mutex.Access#uninitialized() uninitialized} {@link Mutex}.
*
* @param Stored value type.
* @return An {@link Mutex.Access#uninitialized() uninitialized} {@link Mutex}.
*/
public static Mutex of() {
return new Mutex<>();
}
/**
* Creates a {@link Mutex} initialized to some value.
*
*
* Examples
*
*
{@code
* import net.diversionmc.async.mutex.Mutex;
*
* var mutex = new Mutex<>("hello");
* assert !mutex.uninitialized();
* }
*
* @param value Initial value.
* @throws NullPointerException If {@code value} is {@code null}. Use {@link #Mutex()} instead
* if you want an {@link Mutex.Access#uninitialized()
* uninitialized} {@link Mutex}.
*/
public Mutex(T value) throws NullPointerException {
Objects.requireNonNull(value, "Could not create an initialized mutex from a `null` value");
this.value = value;
}
/**
* A static method alias of {@link #Mutex(Object)} that creates a {@link Mutex} initialized to
* some value.
*
* @param value Initial value.
* @param Stored value type.
* @return A {@link Mutex} initialized to {@code value}.
* @throws NullPointerException If {@code value} is {@code null}. Use {@link #Mutex()} instead
* if you want an {@link Mutex.Access#uninitialized()
* uninitialized} {@link Mutex}.
*/
public static Mutex of(T value) throws NullPointerException {
return new Mutex<>(value);
}
/**
* Access the value stored in this {@link Mutex}. Only using this method you can be certain that
* the value will be accessed in a thread-safe way (so that no other thread receives a value
* other than the one stored in this {@link Mutex}).
*
*
* Note that the {@link Mutex.Access} is {@link AutoCloseable}. This means the idiomatic way to
* access the stored value is by using a try-with-resources block, in which the current thread
* holds the access to the mutex. See examples below.
*
*
* Examples
*
*
{@code
* import net.diversionmc.async.mutex.Mutex;
*
* var mutex = new Mutex<>("hello");
* try (var access = mutex.access()) {
* var value = access.get();
* assert value.equals("hello");
* }
* }
*
* @return An {@link Mutex.Access} to the value stored in this {@link Mutex}.
*/
@Blocking
public Access access() {
return new Access();
}
/**
* Perform an action on the value stored in this {@link Mutex}. This is a functional variant of
* {@link #access()}.
*
*
* Note that this is a terminal access. This method returns {@code void} and there is no
* way (besides {@link AtomicReference} or {@link Mutex}) to get a value out of the {@code
* action} function.
*
*
* Examples
*
*
* This examples shows how to use {@link AtomicReference} to pull a value out when using this
* method.
*
{@code
* import net.diversionmc.async.mutex.Mutex;
* import java.util.concurrent.atomic.AtomicReference;
*
* var mutex = Mutex.of("hello");
* var atomicValue = new AtomicReference<>();
* mutex.access(access -> {
* var value = access.get();
* atomicValue.set(value);
* });
* assert atomicValue.get().equals("hello");
* }
*
* @param action Action to perform upon the value stored in this {@link Mutex}.
*/
@Blocking
public void access(Consumer action) {
try (var access = access()) {
action.accept(access);
}
}
/**
* Checks if this {@link Mutex} stores a {@code null}. This indicates that the {@link Mutex} has
* not yet been filled in, someone has {@link #take() took} a value from it, or the value has
* been set to {@code null}.
*
*
* Using {@link Mutex.Access#uninitialized()} is recommended instead.
*
*
* Examples
*
*
{@code
* import net.diversionmc.async.mutex.Mutex;
*
* var mutex = new Mutex<>();
* assert mutex.uninitialized();
* }
*
* @return {@code true} if this {@link Mutex} has no stored value.
* @see Mutex.Access#uninitialized()
*/
@Blocking
public boolean uninitialized() {
try (var access = access()) {
return access.uninitialized();
}
}
/**
* Take a value from this {@link Mutex}. This {@link Mutex} will be {@link #uninitialized()
* uninitialized} after this operation. See {@link Mutex.Access#uninitialized()} for more
* details.
*
* @return The value stored in this {@link Mutex}.
* @see Mutex.Access#uninitialized()
*/
@Blocking
public T take() {
return set(null);
}
/**
* Sets the value in this {@link Mutex}, returning the previously stored value.
*
* @param newValue The new value.
* @return Previous value. May be {@code null} if {@link Mutex.Access#uninitialized()}.
*/
@Blocking
public T set(T newValue) {
try (var access = access()) {
var oldValue = access.get();
access.set(newValue);
return oldValue;
}
}
/**
* Checks if {@code obj} is a {@link Mutex} with the same value. Note that it does not return
* {@code true} if {@code obj} is not a {@link Mutex}, even if the inner value is equal to
* {@code obj}.
*
* @param obj The object to check.
* @return {@link Object#equals(Object)} for the stored value.
*/
@Blocking
public boolean equals(Object obj) {
if (!(obj instanceof Mutex> mutex2))
return false;
try (var access1 = access()) {
try (var access2 = mutex2.access()) {
return access1.get().equals(access2.get());
}
}
}
/**
* Gets a {@link Object#hashCode()} for the stored value.
*
* @return {@link Object#hashCode()} for the stored value.
*/
@Blocking
public int hashCode() {
try (var access = access()) {
return access.get().hashCode();
}
}
/**
* Represents access to the {@link Mutex}'s inner value.
*
*
* Note that the {@link Mutex.Access} is {@link AutoCloseable}. This means the idiomatic way to
* access the stored value is by using a try-with-resources block, in which the current thread
* holds the access to the mutex. See examples for {@link Mutex#access()}.
*
* @see Mutex#access()
*/
public final class Access implements AutoCloseable {
private boolean currentlyLocked;
private Access() {
lock.lock();
currentlyLocked = true;
}
/**
* Checks if this {@link Mutex} stores a {@code null}. This indicates that the {@link Mutex}
* has not yet been filled in, someone has {@link #take() took} a value from it, or the
* value has been set to {@code null}.
*
*
* Examples
*
*
{@code
* import net.diversionmc.async.mutex.Mutex;
*
* var mutex = new Mutex<>();
* try (var access = mutex.access()) {
* assert access.uninitialized();
* }
* }
*
* @return {@code true} if this {@link Mutex} has no stored value.
* @throws IllegalStateException If this {@link Mutex.Access} no longer holds the lock. See
* {@link #close()}.
* @see Access#uninitialized()
*/
public boolean uninitialized() throws IllegalStateException {
if (!currentlyLocked)
throw new IllegalStateException("This access is invalid (not holding the lock)");
return value == null;
}
/**
* Gets the value currently stored in this {@link Mutex}.
*
*
* Caution
*
*
* Be careful when pulling a value out of the {@link Mutex} - you might be mutating
* underlying value without synchronization. The exceptions are immutable values (like
* {@link Number}s, {@link String}s, {@link Record record}s of immutable values, etc.) and
* other stored {@link Mutex}es and similar structures, since they have their own
* synchronization.
*
*
* Examples
*
*
{@code
* import net.diversionmc.async.mutex.Mutex;
*
* var mutex = Mutex.of("hello");
* try (var access = mutex.access()) {
* var value = access.get();
* assert value.equals("hello");
* }
* }
*
* @return Stored value.
* @throws IllegalStateException If this {@link Mutex.Access} no longer holds the lock. See
* {@link #close()}.
*/
public T get() throws IllegalStateException {
if (!currentlyLocked)
throw new IllegalStateException("This access is invalid (not holding the lock)");
return value;
}
/**
* Stores a new value in the associated {@link Mutex}.
*
*
* Examples
*
*
{@code
* import net.diversionmc.async.mutex.Mutex;
*
* var mutex = Mutex.of();
* try (var access = mutex.access()) {
* access.set("hello");
* }
*
* assert mutex.equals(Mutex.of("hello"));
* }
*
* @param value New value to store.
* @return Previous value. May be {@code null} if {@link Mutex.Access#uninitialized()}.
* @throws IllegalStateException If this {@link Mutex.Access} no longer holds the lock. See
* {@link #close()}.
*/
public T set(T value) throws IllegalStateException {
if (!currentlyLocked)
throw new IllegalStateException("This access is invalid (not holding the lock)");
var oldValue = Mutex.this.value;
Mutex.this.value = value;
return oldValue;
}
/**
* Closes this {@link Mutex.Access} and releases the lock on the associated {@link Mutex}.
* After calling this method, this {@link Mutex.Access} can no longer be used.
*
*
* Examples
*
*
{@code
* import net.diversionmc.async.mutex.Mutex;
*
* var mutex = Mutex.of();
* var access = mutex.access();
* access.close();
* access.get(); // throws IllegalStateException
* }
*/
public void close() {
currentlyLocked = false;
try {
lock.unlock();
} catch (IllegalMonitorStateException ignored) {
// safe `AutoClosable.close()` without unwanted exceptions
}
}
}
}