diff --git a/src/main/java/org/springframework/data/util/DefaultLock.java b/src/main/java/org/springframework/data/util/DefaultLock.java new file mode 100644 index 0000000000..ad119578bb --- /dev/null +++ b/src/main/java/org/springframework/data/util/DefaultLock.java @@ -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(); + } +} diff --git a/src/main/java/org/springframework/data/util/DefaultReadWriteLock.java b/src/main/java/org/springframework/data/util/DefaultReadWriteLock.java new file mode 100644 index 0000000000..07bf4a5b5d --- /dev/null +++ b/src/main/java/org/springframework/data/util/DefaultReadWriteLock.java @@ -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 { + +} diff --git a/src/main/java/org/springframework/data/util/Lock.java b/src/main/java/org/springframework/data/util/Lock.java new file mode 100644 index 0000000000..f5b8d538ec --- /dev/null +++ b/src/main/java/org/springframework/data/util/Lock.java @@ -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. + *

+ * 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. + * + *

+ * ReentrantLock backend = new ReentrantLock();
+ *
+ * Lock lock = Lock.of(backend);
+ *
+ * lock.executeWithoutResult(() -> {
+ *   // callback without returning a result
+ * });
+ *
+ * lock.execute(() -> {
+ *   // callback returning a result
+ *   return …;
+ * });
+ * 
+ * + * @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. + *

+ * 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}. + *

+ * Acquires the lock if it is available and returns immediately. + *

+ * 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: + *

+ *

+ * If the current thread: + *

+ * 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 type of the result. + * @throws RuntimeException if thrown by the action + */ + default T execute(Supplier 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(); + } + +} diff --git a/src/main/java/org/springframework/data/util/ReadWriteLock.java b/src/main/java/org/springframework/data/util/ReadWriteLock.java new file mode 100644 index 0000000000..741da08e7c --- /dev/null +++ b/src/main/java/org/springframework/data/util/ReadWriteLock.java @@ -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(); + +} diff --git a/src/test/java/org/springframework/data/util/LockUnitTests.java b/src/test/java/org/springframework/data/util/LockUnitTests.java new file mode 100644 index 0000000000..04808727e4 --- /dev/null +++ b/src/test/java/org/springframework/data/util/LockUnitTests.java @@ -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"); + } +}