Skip to Content
Course content

258: StampedLock for Optimistic Reads

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're in a busy office and there's a whiteboard that displays the current project deadline. Most of the time, you just need to glance at it to make sure you're on track. Now, you could be really cautious: every time you look at the board, you put a "Do Not Erase" sticky note over the date. That's a traditional read lock. It works, but it's annoying because if the project manager needs to change the date, they have to wait for every single person to finish their glance and remove their sticky notes.

Instead, imagine you just glance at the board. While you're reading the date, you notice a small version number in the corner—let's say it's "v4". You read the date, and then you check the version number again. If it's still "v4", you're golden. But if it's now "v5", it means someone changed the date while you were looking at it, and your data might be a weird mix of the old and new dates. In that case, you don't panic; you just put the sticky note on the board and read it properly this time.

That's exactly how StampedLock works with optimistic reads. It's designed for those scenarios where reads are incredibly frequent and writes are rare, and you want to avoid the overhead of a full-blown lock whenever possible.

The Overhead of Traditional Read-Write Locks

You've already seen ReentrantReadWriteLock. It's great, but it has a cost. Even a read lock has to update the lock's internal state to keep track of how many readers are active. In a high-concurrency environment, that state update becomes a bottleneck because multiple threads are fighting to update the same counter. I've seen this kill performance in low-latency trading apps where the read-to-write ratio is 1000:1.

StampedLock solves this by providing a "stamp"—a long value—that represents a version of the lock state. An optimistic read doesn't actually lock anything; it just asks for the current stamp and hopes for the best.

The 'Hope for the Best' Implementation

Let's look at a real example. Suppose we're tracking a player's position in a game world. We have X and Y coordinates that always need to be updated together.

public class PlayerPosition {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    public void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock(); // This is a pessimistic write lock
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    public double[] getPosition() {
        // 1. Try an optimistic read
        long stamp = sl.tryOptimisticRead();
        
        // 2. Copy the shared state into local variables
        double currentX = x; 
        double currentY = y;

        // 3. Validate the stamp
        if (!sl.validate(stamp)) {
            // The state changed! Fall back to a pessimistic read lock
            stamp = sl.readLock(); 
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        
        return new double[]{currentX, currentY};
    }
}

Mapping the Logic to the Code

Notice the specific sequence in getPosition(). This is the pattern you'll use every time you implement an optimistic read:

  • The Glance: tryOptimisticRead() doesn't block anything. It just grabs the current version of the lock. If a writer is currently holding the lock, this will actually return 0 immediately.
  • The Local Copy: I read x and y into local variables. This is crucial. If you read the variables directly into a return statement or a complex calculation, you're exposing yourself to "torn reads" (where x is from the old version and y is from the new version).
  • The Validation: sl.validate(stamp) checks if a write lock was acquired since we got our stamp. If it returns false, our local copies are potentially corrupted.
  • The Fallback: If validation fails, we don't just give up. We switch to a readLock(). This blocks any writers until we've successfully copied the data, ensuring we get a consistent snapshot.

One thing to keep in mind: StampedLock is not reentrant. If you call a method that acquires a write lock from within another method that already holds a write lock on the same StampedLock instance, you'll deadlock yourself. It's a bit more "primitive" than ReentrantReadWriteLock, but that's why it's so much faster.




📋 Practical Task

Build a Thread-Safe High-Score Tracker with StampedLock

You are building a leaderboard for a competitive game. You need to create a class called HighScoreTracker that tracks the topScore (an integer) and the topPlayerName (a String). Since thousands of players will be checking the high score every second, but the score only changes occasionally, this is a perfect use case for StampedLock.

Requirements:

  • Implement a method updateScore(String name, int score) that updates both the name and the score only if the new score is higher than the current topScore. Use a pessimistic write lock for this.
  • Implement a method getHighScoreDetails() that returns a record or a small POJO containing both the name and the score.
  • In getHighScoreDetails(), you must implement the optimistic read pattern:
    1. Attempt an optimistic read.
    2. Copy the name and score to local variables.
    3. Validate the stamp.
    4. Fall back to a pessimistic read lock if validation fails.

Testing your implementation: Create a main method where one thread constantly updates the score with random values, while five other threads constantly read the high score. Verify that the name and score always match (i.e., you never see a new score paired with an old player's name).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.