Browse Source

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
pull/2958/head
Mark Paluch 2 years ago committed by Christoph Strobl
parent
commit
cbcb848fab
No known key found for this signature in database
GPG Key ID: 8CC1AB53391458C8
  1. 50
      src/main/java/org/springframework/data/util/DefaultLock.java
  2. 26
      src/main/java/org/springframework/data/util/DefaultReadWriteLock.java
  3. 137
      src/main/java/org/springframework/data/util/Lock.java
  4. 60
      src/main/java/org/springframework/data/util/ReadWriteLock.java
  5. 155
      src/test/java/org/springframework/data/util/LockUnitTests.java

50
src/main/java/org/springframework/data/util/DefaultLock.java

@ -0,0 +1,50 @@ @@ -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();
}
}

26
src/main/java/org/springframework/data/util/DefaultReadWriteLock.java

@ -0,0 +1,26 @@ @@ -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

@ -0,0 +1,137 @@ @@ -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();
}
}

60
src/main/java/org/springframework/data/util/ReadWriteLock.java

@ -0,0 +1,60 @@ @@ -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

@ -0,0 +1,155 @@ @@ -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");
}
}
Loading…
Cancel
Save