Introduce Lock utility.
The Lock utility is an adapter for Java's Lock and ReadWrite Lock types providing an easier to consume programming model (callback-style and try-with-resources). Closes: #2944
This commit is contained in:
committed by
Christoph Strobl
parent
e51c419b6d
commit
cbcb848fab
50
src/main/java/org/springframework/data/util/DefaultLock.java
Normal file
50
src/main/java/org/springframework/data/util/DefaultLock.java
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.data.util;
|
||||
|
||||
import org.springframework.data.util.Lock.AcquiredLock;
|
||||
|
||||
/**
|
||||
* Default {@link Lock} implementation.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.2
|
||||
*/
|
||||
class DefaultLock implements Lock, AcquiredLock {
|
||||
|
||||
private final java.util.concurrent.locks.Lock delegate;
|
||||
|
||||
DefaultLock(java.util.concurrent.locks.Lock delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AcquiredLock lock() {
|
||||
delegate.lock();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AcquiredLock lockInterruptibly() throws InterruptedException {
|
||||
delegate.lockInterruptibly();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
delegate.unlock();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.data.util;
|
||||
|
||||
/**
|
||||
* Default holder for read and write locks.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.2
|
||||
*/
|
||||
record DefaultReadWriteLock(Lock readLock, Lock writeLock) implements ReadWriteLock {
|
||||
|
||||
}
|
||||
137
src/main/java/org/springframework/data/util/Lock.java
Normal file
137
src/main/java/org/springframework/data/util/Lock.java
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.data.util;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@code Lock} provides more extensive locking operations than can be obtained using {@code synchronized} methods and
|
||||
* {@link java.util.concurrent.locks.Lock}. It allows more flexible structuring and an improved usage model.
|
||||
* <p>
|
||||
* This Lock abstraction is an extension to the {@link java.util.concurrent.locks.Lock lock utilities} and intended for
|
||||
* easier functional and try-with-resources usage.
|
||||
*
|
||||
* <pre class="code">
|
||||
* ReentrantLock backend = new ReentrantLock();
|
||||
*
|
||||
* Lock lock = Lock.of(backend);
|
||||
*
|
||||
* lock.executeWithoutResult(() -> {
|
||||
* // callback without returning a result
|
||||
* });
|
||||
*
|
||||
* lock.execute(() -> {
|
||||
* // callback returning a result
|
||||
* return …;
|
||||
* });
|
||||
* </pre>
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.2
|
||||
*/
|
||||
public interface Lock {
|
||||
|
||||
/**
|
||||
* Create a new {@link Lock} adapter for the given {@link java.util.concurrent.locks.Lock delegate}.
|
||||
*
|
||||
* @param delegate must not be {@literal null}.
|
||||
* @return a new {@link Lock} adapter.
|
||||
*/
|
||||
static Lock of(java.util.concurrent.locks.Lock delegate) {
|
||||
|
||||
Assert.notNull(delegate, "Lock delegate must not be null");
|
||||
|
||||
return new DefaultLock(delegate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires the lock.
|
||||
* <p>
|
||||
* If the lock is not available then the current thread becomes disabled for thread scheduling purposes and lies
|
||||
* dormant until the lock has been acquired.
|
||||
*
|
||||
* @see java.util.concurrent.locks.Lock#lock()
|
||||
*/
|
||||
AcquiredLock lock();
|
||||
|
||||
/**
|
||||
* Acquires the lock unless the current thread is {@linkplain Thread#interrupt interrupted}.
|
||||
* <p>
|
||||
* Acquires the lock if it is available and returns immediately.
|
||||
* <p>
|
||||
* If the lock is not available then the current thread becomes disabled for thread scheduling purposes and lies
|
||||
* dormant until one of two things happens:
|
||||
* <ul>
|
||||
* <li>The lock is acquired by the current thread; or
|
||||
* <li>Some other thread {@linkplain Thread#interrupt interrupts} the current thread, and interruption of lock
|
||||
* acquisition is supported.
|
||||
* </ul>
|
||||
* <p>
|
||||
* If the current thread:
|
||||
* <ul>
|
||||
* <li>has its interrupted status set on entry to this method; or
|
||||
* <li>is {@linkplain Thread#interrupt interrupted} while acquiring the lock, and interruption of lock acquisition is
|
||||
* supported,
|
||||
* </ul>
|
||||
* then {@link InterruptedException} is thrown and the current thread's interrupted status is cleared.
|
||||
*/
|
||||
AcquiredLock lockInterruptibly() throws InterruptedException;
|
||||
|
||||
/**
|
||||
* Execute the action specified by the given callback object guarded by a lock and return its result. The
|
||||
* {@code action} is only executed once the lock has been acquired.
|
||||
*
|
||||
* @param action the action to run.
|
||||
* @return the result of the action.
|
||||
* @param <T> type of the result.
|
||||
* @throws RuntimeException if thrown by the action
|
||||
*/
|
||||
default <T> T execute(Supplier<T> action) {
|
||||
try (AcquiredLock l = lock()) {
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the action specified by the given callback object guarded by a lock. The {@code action} is only executed
|
||||
* once the lock has been acquired.
|
||||
*
|
||||
* @param action the action to run.
|
||||
* @throws RuntimeException if thrown by the action.
|
||||
*/
|
||||
default void executeWithoutResult(Runnable action) {
|
||||
try (AcquiredLock l = lock()) {
|
||||
action.run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An acquired lock can be used with try-with-resources for easier releasing.
|
||||
*/
|
||||
interface AcquiredLock extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* Releases the lock.
|
||||
*
|
||||
* @see java.util.concurrent.locks.Lock#unlock()
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.data.util;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@code ReadWriteLock} maintains a pair of associated {@link Lock locks}, one for read-only operations and one for
|
||||
* writing. The {@link #readLock read lock} may be held simultaneously by multiple reader threads, so long as there are
|
||||
* no writers. The {@link #writeLock write lock} is exclusive.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.2
|
||||
* @see Lock
|
||||
*/
|
||||
public interface ReadWriteLock {
|
||||
|
||||
/**
|
||||
* Create a new {@link ReadWriteLock} adapter for the given {@link java.util.concurrent.locks.ReadWriteLock delegate}.
|
||||
*
|
||||
* @param delegate must not be {@literal null}.
|
||||
* @return a new {@link ReadWriteLock} adapter.
|
||||
*/
|
||||
static ReadWriteLock of(java.util.concurrent.locks.ReadWriteLock delegate) {
|
||||
|
||||
Assert.notNull(delegate, "Lock delegate must not be null");
|
||||
|
||||
return new DefaultReadWriteLock(Lock.of(delegate.readLock()), Lock.of(delegate.writeLock()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lock used for reading.
|
||||
*
|
||||
* @return the lock used for reading
|
||||
* @see java.util.concurrent.locks.ReadWriteLock#readLock()
|
||||
*/
|
||||
Lock readLock();
|
||||
|
||||
/**
|
||||
* Returns the lock used for reading.
|
||||
*
|
||||
* @return the lock used for writing.
|
||||
* @see java.util.concurrent.locks.ReadWriteLock#writeLock() ()
|
||||
*/
|
||||
Lock writeLock();
|
||||
|
||||
}
|
||||
155
src/test/java/org/springframework/data/util/LockUnitTests.java
Normal file
155
src/test/java/org/springframework/data/util/LockUnitTests.java
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.springframework.data.util;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.util.Lock.AcquiredLock;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Lock}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class LockUnitTests {
|
||||
|
||||
@Test // GH-2944
|
||||
void shouldDelegateLock() {
|
||||
|
||||
ReentrantLock backend = new ReentrantLock();
|
||||
|
||||
Lock lock = Lock.of(backend);
|
||||
|
||||
lock.executeWithoutResult(() -> {
|
||||
|
||||
assertThat(backend.isLocked()).isTrue();
|
||||
assertThat(backend.isHeldByCurrentThread()).isTrue();
|
||||
});
|
||||
|
||||
lock.execute(() -> {
|
||||
|
||||
assertThat(backend.isLocked()).isTrue();
|
||||
assertThat(backend.isHeldByCurrentThread()).isTrue();
|
||||
return null;
|
||||
});
|
||||
|
||||
assertThat(backend.isLocked()).isFalse();
|
||||
}
|
||||
|
||||
@Test // GH-2944
|
||||
void shouldIncrementLockCount() {
|
||||
|
||||
ReentrantLock backend = new ReentrantLock();
|
||||
backend.lock();
|
||||
|
||||
Lock lock = Lock.of(backend);
|
||||
|
||||
lock.executeWithoutResult(() -> {
|
||||
|
||||
assertThat(backend.getHoldCount()).isEqualTo(2);
|
||||
assertThat(backend.isLocked()).isTrue();
|
||||
assertThat(backend.isHeldByCurrentThread()).isTrue();
|
||||
});
|
||||
|
||||
assertThat(backend.getHoldCount()).isEqualTo(1);
|
||||
assertThat(backend.isLocked()).isTrue();
|
||||
}
|
||||
|
||||
@Test // GH-2944
|
||||
void exceptionShouldCleanupLock() {
|
||||
|
||||
ReentrantLock backend = new ReentrantLock();
|
||||
|
||||
Lock lock = Lock.of(backend);
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(() -> lock.executeWithoutResult(() -> {
|
||||
throw new IllegalStateException();
|
||||
}));
|
||||
|
||||
assertThat(backend.isLocked()).isFalse();
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(() -> lock.execute(() -> {
|
||||
throw new IllegalStateException();
|
||||
}));
|
||||
|
||||
assertThat(backend.isLocked()).isFalse();
|
||||
}
|
||||
|
||||
@Test // GH-2944
|
||||
void shouldDelegateReadWriteLock() {
|
||||
|
||||
ReentrantReadWriteLock backend = new ReentrantReadWriteLock();
|
||||
|
||||
ReadWriteLock lock = ReadWriteLock.of(backend);
|
||||
|
||||
lock.readLock().executeWithoutResult(() -> {
|
||||
assertThat(backend.getReadLockCount()).isEqualTo(1);
|
||||
});
|
||||
|
||||
lock.writeLock().executeWithoutResult(() -> {
|
||||
assertThat(backend.isWriteLocked()).isTrue();
|
||||
});
|
||||
|
||||
assertThat(backend.getReadLockCount()).isEqualTo(0);
|
||||
assertThat(backend.isWriteLocked()).isFalse();
|
||||
}
|
||||
|
||||
@Test // GH-2944
|
||||
void lockTryWithResources() {
|
||||
|
||||
ReentrantLock backend = new ReentrantLock();
|
||||
Lock lock = Lock.of(backend);
|
||||
|
||||
try (AcquiredLock l = lock.lock()) {
|
||||
assertThat(backend.isLocked()).isTrue();
|
||||
assertThat(backend.isHeldByCurrentThread()).isTrue();
|
||||
}
|
||||
|
||||
assertThat(backend.isLocked()).isFalse();
|
||||
}
|
||||
|
||||
@Test // GH-2944
|
||||
void lockInterruptiblyTryWithResources() {
|
||||
|
||||
ReentrantLock backend = new ReentrantLock();
|
||||
Lock lock = Lock.of(backend);
|
||||
|
||||
try (AcquiredLock l = lock.lockInterruptibly()) {
|
||||
assertThat(backend.isLocked()).isTrue();
|
||||
assertThat(backend.isHeldByCurrentThread()).isTrue();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
assertThat(backend.isLocked()).isFalse();
|
||||
}
|
||||
|
||||
@Test // GH-2944
|
||||
void shouldReturnResult() {
|
||||
|
||||
ReentrantLock backend = new ReentrantLock();
|
||||
|
||||
Lock lock = Lock.of(backend);
|
||||
|
||||
String result = lock.execute(() -> "foo");
|
||||
|
||||
assertThat(result).isEqualTo("foo");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user