0

While reading the source code of java.util.concurrent.locks.ReentrantLock I found that the tryLock() method is implemented as below:

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

We try to "own" the lock or check if we have already owned the lock, depending on the state maintained in AbstractQueuedSynchronizer. But I wonder why the variable state is declared as volatile but the variable exclusiveOwnerThread is not?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
yangty89
  • 87
  • 5

1 Answers1

2

To understand why exclusiveOwnerThread does not need to be volatile it helps to look at both the acquire and release methods together.

Acquire method1:

/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

Release method:

@ReservedStackAccess
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

It's also important to realize that exclusiveOwnerThread does not reference some arbitrary object with no relation to the threads involved. It specifically holds a reference to a Thread instance and is compared strictly to the calling thread. In other words, what matters is if:

Thread.currentThread() == getExclusiveOwnerThread()

Which will be true if, and only if, the calling thread has previously invoked #setExclusiveOwnerThread(Thread), with itself as the argument, due to the combined natures of #nonfairTryAcquire(int) and #tryRelease(int). Actions in one thread always happen-before subsequent actions in the same thread.

So if c != 0 then there are two scenarios:

  1. The calling thread owns the synchronizer.

    • Since actions in one thread always happen-before subsequent actions in the same thread it is guaranteed getExclusiveOwnerThread() will return a reference to the calling thread.
  2. The calling thread does not own the synchronizer.

    • It no longer matters what reference is returned by getExclusiveOwnerThread() because it is impossible for that method to return a reference to the calling thread.

      The calling thread can never see a stale reference to itself because of the setExclusiveOwnerThread(null) call in #tryRelease(int). This means getExclusiveOwnerThread() can either return null or some other Thread reference (stale or not), but never a reference to the calling thread.

The reason state must be volatile is because it is shared between threads in a manner where it's paramount each thread sees the most recent value.


1. The implementation of FairSync#tryAcquire(int) has nearly the same implementation except it takes the order of calling threads into account.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Thanks a lot for the detailed answer :) I agree that it works well when a thread compares itself to the `exclusiveOwnerThread` value, because of the _happens-before_ rule within a single thread. If the thread has owned the lock, it will definitely get the right `exclusiveOwnerThread` (which refers to itself) when reentering or unlocking. And it's sufficient to guarantee that the thread doesn't see a stale value that refers to itself if the thread doesn't own the lock (done by `setExclusiveOwnerThread(null)`), as you said. – yangty89 Mar 24 '20 at 14:37
  • But what also confuse me is that there is a `getOwner()` method defined in `ReentrantLock`, which returns the `exclusiveOwnerThread` in case `c !=0`. While in the `c == 0` branch of `nonfairTryAcquire()`, the `exclusiveOwnerThread` variable is set **after** a volatile-write (which acts like unlocking, for memory consistency) to the state. So I think the `getOwner()` method may return a stale value of `exclusiveOwnerThread` even though it always trigger a volatile-read (which acts like locking, for memory consistency) on the state before the read of `exclusiveOwnerThread`... – yangty89 Mar 24 '20 at 14:52
  • The documentation of [`ReentrantLock#getOwner()`](https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html#getOwner()) says, "_When this method is called by a thread that is not the owner, the return value reflects a best-effort approximation of current lock status_". I interpret that to mean it there's explicitly no guarantee you'll see the correct value if the calling thread does not own the lock. – Slaw Mar 24 '20 at 19:50
  • sorry that I forgot to read the documentation of that method... Thanks a lot. – yangty89 Mar 25 '20 at 13:20