Browse Source
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: #2944pull/2958/head
5 changed files with 428 additions and 0 deletions
@ -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(); |
||||
} |
||||
} |
||||
@ -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 { |
||||
|
||||
} |
||||
@ -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(); |
||||
} |
||||
|
||||
} |
||||
@ -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(); |
||||
|
||||
} |
||||
@ -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…
Reference in new issue