Search code examples
javareadwritelockreentrantreadwritelock

Read lock while Writing


I need help in understanding the below code :

private Predicate composedPredicate = null;

public boolean evaluate(Task taskData) {
        boolean isReadLock = false;
        try{
            rwl.readLock().lock();
            isReadLock = true;
            if (composedPredicate == null) {
                rwl.readLock().unlock();
                isReadLock = false;
                rwl.writeLock().lock();
                if (composedPredicate == null) {
                    //write to the "composedPredicate" object
                }
            }
        }finally {
            if (isReadLock) {
                rwl.readLock().unlock();
            }else{
                rwl.writeLock().unlock();
            }
        }
        return composedPredicate.test(taskData);
    }

What will happen if we don't use Read Locks in the above code? Like :

public boolean evaluate(Task taskData) {
        //boolean isReadLock = false;
        try{
            //rwl.readLock().lock();
            //isReadLock = true;
            if (composedPredicate == null) {
                //rwl.readLock().unlock();
                //isReadLock = false;
                rwl.writeLock().lock();
                if (composedPredicate == null) {
                    //write to the "composedPredicate" object
                }
            }
        }finally {
            rwl.writeLock().unlock();
        }
        return composedPredicate.test(taskData);
    }
  1. Do we really need Read locks while we are only writing the data?
  2. What is the difference between the above two codes?
  3. Should we use Read locks even for accessing the object(composedPredicate) for null check?

Solution

  • The first code that you posted is a correct implementation of the double-checked locking approach in Java using a read/write lock.

    Your second implementation without a read-lock is broken. The memory model allows writes to be reordering from the perspective of another thread seeing the result of the writes to memory.

    What could happen is that you could be using a not-fully initialized instance of Predicate in the thread that is reading it.

    Example with your code:

    We have thread A and B both running evaluate and composedPredicate is null initially.

    1. A: sees composedPredicate is null
    2. A: write-locks
    3. A: creates an instance of an implementation of Predicate
    4. A: initializes this instance in the constructor
    5. A: assigns the instance to the the shared variable composedPredicate
    6. A: unlocks the write lock
    1. B: sees composedPredicate is not null
    2. B: runs composedPredicate.test(taskData);
    3. HOWEVER, the compiler, the JVM, or the hardware architecture of your system reordered steps 4 and 5 of thread A, and assigned the address of the Predicate instance of the shared field before it was initialized (this is allowed by the Java Memory model)
    4. composedPredicate.test(taskData); is run using a not-fully initialized instance and your code has random unexpected errors in production resulting in great losses to your company (potentially that happens .. depends on the system that you're building)

    Whether or not the reordering of step 4 and 5 happens depends on many factors. Maybe it only does under heavy system load. It may not happen at all on your OS, hardware, version of JVM, etc. (But on the next version of the JVM, your OS, or when you move your application to a different physical machine, it may suddenly start happening)

    Bad idea.