Search code examples
javavolatilereentrantlock

Why AbstractOwnableSynchronizer.exclusiveOwnerThread is not declared as volatile?


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?


Solution

  • 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.