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 @@ |
|||||||
|
/* |
||||||
|
* 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 { |
||||||
|
|
||||||
|
} |
||||||
@ -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(); |
||||||
|
|
||||||
|
} |
||||||
@ -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