-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Refine Locking #4431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refine Locking #4431
Conversation
@@ -349,13 +356,15 @@ private synchronized Object resolve() { | |||
} | |||
return result; | |||
} | |||
lock.readLock().unlock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "read" lock should be released inside a finally { .. }
block here, particularly since there are interactions with a Logger
occurring just above it.
It seems rather benign, but if the LOGGER
was not properly initialized, or if the property
reference were null
when trace is enabled, or some other issues occurred during logging and threw an Exception, then the "read" lock would not be properly released, preventing any writes from occurring, thereby leading to possible Thread starvation.
@@ -39,7 +41,7 @@ | |||
*/ | |||
abstract class CursorReadingTask<T, R> implements Task { | |||
|
|||
private final Object lifecycleMonitor = new Object(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
synchronized
blocks within this class serve the purpose to ensure ordering according (happens before as of the JLS spec). We should keep these to avoid visibility issues of the state
field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using Locks
has the same visibility guarantees as using a synchronized
block. There are no issues so long as 1) the state
variable is guarded by the same Lock
every time the state
variable is accessed and 2) the same Lock
is consistently used everywhere the state
variable is accessed.
See the Javadoc for Lock
, and specifically the section on "Memory Synchronization" (concerns "visibility").
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, I might add that if CursorReadingTask
is being accessed by multiple, concurrent Threads, as is implied by the use of synchronization, now locking, that perhaps a ReentrantReadWriteLock
might be a better choice for accessing the state
variable (Read Lock on query, and Write Lock on mutation).
} | ||
lock.lock(); | ||
state = State.CANCELLED; | ||
lock.unlock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the 3rd time this sequence of statements has appeared now. Also visible on lines 91-93, 100-102, and then here (153-155).
Perhaps consider adding a setState(:State)
method:
private void setState(State newState) {
lock.lock();
try {
this.state = newState;
}
finally {
lock.unlock();
}
}
And, I'd still stick to the[ <lock>, <try to do something while lock is held>, <unlock in finally block> ]
pattern and idiom, for consistency sake.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, you could introduce an abstract Lock
utility class, for other similar lock, action, unlock scoped blocks, as are present throughout the code above (and below), for both LazyLoadingProxyFactory
and the CursorReadingTask
.
This utility would work for read and write Locks
returned by ReadWriteLock
, as well.
For instance:
abstract class LockUtils {
static void doWithLock(Lock lock, Runnable doInsideLock) {
lock.lock();
try {
doInsideLock.run();
}
finally {
lock.unlock();
}
}
static <T> T doWithLock(Lock lock, Supplier<T> doInsideLock) {
lock.lock();
try {
return doInsideLock.get();
}
finally {
lock.unlock();
}
}
}
Then, sequences such as:
lock.lock();
this.state = State.CANCELLED;
lock.unlock();
Could be simply replaced with:
LockUtils.doWithLock(this.lock, ()-> this.state = State.CANCELLED);
This could be used in other places here as well, not just the lines noted above.
This would even work for lines, 119-123, enforcing the pattern/idiom to which I refer, along with lines 129-144.
Food for thought.
|
||
if (State.RUNNING.equals(state) || State.STARTING.equals(state)) { | ||
this.state = State.CANCELLED; | ||
if (cursor != null) { | ||
cursor.close(); | ||
} | ||
} | ||
} finally { | ||
lock.unlock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For instance, the LockUtils
could be used here:
LockUtils.doWithLock(lock, () -> {
if (State.RUNNING.equals(state) || State.STARTING.equals(state)) {
this.state = State.CANCELLED;
if (cursor != null) {
cursor.close();
}
}
});
return state; | ||
} finally { | ||
lock.unlock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the overloaded Supplier
variant, using LockUtils
:
public State getState() {
return LockUtils.doWithLock(lock, () -> this.state);
}
return running; | ||
} finally { | ||
lifecycleMonitor.writeLock().unlock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example use of LockUtils
here:
return LockUtils.doWithLock(lifecycleMonitor.writeLock(), () -> running);
@@ -39,7 +41,7 @@ | |||
*/ | |||
abstract class CursorReadingTask<T, R> implements Task { | |||
|
|||
private final Object lifecycleMonitor = new Object(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using Locks
has the same visibility guarantees as using a synchronized
block. There are no issues so long as 1) the state
variable is guarded by the same Lock
every time the state
variable is accessed and 2) the same Lock
is consistently used everywhere the state
variable is accessed.
See the Javadoc for Lock
, and specifically the section on "Memory Synchronization" (concerns "visibility").
@@ -39,7 +41,7 @@ | |||
*/ | |||
abstract class CursorReadingTask<T, R> implements Task { | |||
|
|||
private final Object lifecycleMonitor = new Object(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, I might add that if CursorReadingTask
is being accessed by multiple, concurrent Threads, as is implied by the use of synchronization, now locking, that perhaps a ReentrantReadWriteLock
might be a better choice for accessing the state
variable (Read Lock on query, and Write Lock on mutation).
@@ -171,35 +180,42 @@ public <S, T> Subscription register(SubscriptionRequest<S, ? super T, ? extends | |||
@Override | |||
public Optional<Subscription> lookup(SubscriptionRequest<?, ?, ?> request) { | |||
|
|||
synchronized (lifecycleMonitor) { | |||
subscriptionMonitor.readLock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be subscriptionMonitor.readLock().lock();
561669c
to
db4a1be
Compare
rebased & updated. I would not want to introduce more public API but moved some code into methods with limited scope. |
Introduce Lock utility for easier lock handling.
Use Lock utility for easier lock handling. Original Pull Request: #4431 See also: spring-projects/spring-data-commmons#2944
Closes: #4429