Skip to content
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

Closed
wants to merge 5 commits into from
Closed

Refine Locking #4431

wants to merge 5 commits into from

Conversation

christophstrobl
Copy link
Member

Closes: #4429

@@ -349,13 +356,15 @@ private synchronized Object resolve() {
}
return result;
}
lock.readLock().unlock();
Copy link
Contributor

@jxblum jxblum Jul 12, 2023

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();
Copy link
Member

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.

Copy link
Contributor

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").

Copy link
Contributor

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();
Copy link
Contributor

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.

Copy link
Contributor

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();
Copy link
Contributor

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();
Copy link
Contributor

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();
Copy link
Contributor

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();
Copy link
Contributor

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();
Copy link
Contributor

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();
Copy link
Contributor

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();

@christophstrobl
Copy link
Member Author

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.
christophstrobl added a commit that referenced this pull request Oct 5, 2023
Closes: #4429
Original Pull Request: #4431
christophstrobl pushed a commit that referenced this pull request Oct 5, 2023
Use Lock utility for easier lock handling.

Original Pull Request: #4431
See also: spring-projects/spring-data-commmons#2944
@christophstrobl christophstrobl deleted the issue/4429 branch October 5, 2023 08:54
@mp911de mp911de added this to the 4.2 RC1 (2023.1.0) milestone Mar 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Refine locking behaviour of core components.
3 participants