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:
+ *
+ * - The lock is acquired by the current thread; or
+ *
- Some other thread {@linkplain Thread#interrupt interrupts} the current thread, and interruption of lock
+ * acquisition is supported.
+ *
+ *
+ * If the current thread:
+ *
+ * - has its interrupted status set on entry to this method; or
+ *
- is {@linkplain Thread#interrupt interrupted} while acquiring the lock, and interruption of lock acquisition is
+ * supported,
+ *
+ * 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");
+ }
+}