0

Sometime, I find in some Java code, they use nested lock to accomplish the syncronization method. code as below

// lock for appending state management
final Lock appendLock = new ReentrantLock();
// global lock for array read and write management
final ReadWriteLock arrayReadWritelock = new ReentrantReadWriteLock();
final Lock arrayReadLock = arrayReadWritelock.readLock();
final Lock arrayWriteLock = arrayReadWritelock.writeLock(); 

private MapEntry acquireNew(int length) throws IOException {
    MapEntry mapEntry = null;
    try {
        arrayReadLock.lock(); //THE FIRST LOCK
        IMappedPage toAppendIndexPage = null;
        long toAppendIndexPageIndex = -1L;
        long toAppendDataPageIndex = -1L;
        long toAppendArrayIndex = -1L;
        try {
            appendLock.lock(); //THE SECOND LOCK
            if (this.isFull()) {
                throw new IOException("ring space of java long type used up, the end of the world!!!");
            }
            if (this.headDataItemOffset + length > DATA_PAGE_SIZE) {
                if (this.headDataPageIndex == Long.MAX_VALUE) {
                    this.headDataPageIndex = 0L;
                } else {
                    this.headDataPageIndex++;
                }
                this.headDataItemOffset = 0;
            }
            toAppendDataPageIndex = this.headDataPageIndex;
            ..........
            ..........
            mapEntry = new MapEntry(toAppendArrayIndex, length, toAppendIndexItemOffset, toAppendIndexPage, this.dataPageFactory);
            mapEntry.MarkAllocated();
            this.totalEntryCount.incrementAndGet();
            this.totalSlotSize.addAndGet(length);
            this.arrayHeadIndex.incrementAndGet();
            IMappedPage metaDataPage = this.metaPageFactory.acquirePage(META_DATA_PAGE_INDEX);
            ByteBuffer metaDataBuf = metaDataPage.getLocal(0);
            metaDataPage.setDirty(true);
        } finally {
            appendLock.unlock();
        }
    } finally {
        arrayReadLock.unlock();
    }
    return mapEntry;
}

This puzzles me because the first lock has been used, why the author use another lock again?

lawrence
  • 353
  • 2
  • 17

3 Answers3

1

First lock is "read" lock, second lock is some kind of "write" lock. So this may be good if you want to avoid locking read operation when writing, and vice versa. This may improve performances. But there is always a risk with this kind of thing, and it may cause tricky bugs. If you can afford only one lock for both reading and writing probably that is simpler and more robust.

  • Of course there's `ReadWriteLock` which could (and should) be used instead of having separate lock objects for read/write. – Kayaman Jan 20 '17 at 08:33
  • Thanks @Kayaman @ developer33, I update the code. final Lock appendLock = new ReentrantLock(); // global lock for array read and write management final ReadWriteLock arrayReadWritelock = new ReentrantReadWriteLock(); – lawrence Jan 20 '17 at 08:37
  • I would be interrested to see if a deadlock will occure if a seccond Thread tries to aqquire the writeLock after the the readLock is aquired by a first Thread, but before the writeLock is aquired. (In a scenario where locks are using fair policy) – n247s Jan 20 '17 at 08:45
  • @n247s It won't. If the first thread has acquired the `readLock`, then the second thread will block on acquiring the `writeLock`. The second thread could acquire `readLock`, but not the *exclusive* `writeLock`. – Kayaman Jan 20 '17 at 08:55
  • @Kayaman True, but the second Thread is on the queue first of the writeLock. so if the writeLock is fair use, will the first Thread wait (or deadlock), or will it be able to aquire the lock instead eventhough it came second? If that is the case, pre-read-locking might become interesting, otherwise another thing to look out for. – n247s Jan 20 '17 at 14:08
  • @n247s If the read lock is being held by the current thread, acquiring the write lock won't care about any other waiters in queue, it will just upgrade the lock. The fairness doesn't affect that, so that kind of situation can't happen. – Kayaman Jan 20 '17 at 14:34
  • @n247s The fairness is used to control starvation, so that you don't end up in a situation where a writer is constantly acquiring the lock and the readers don't get a chance to run. – Kayaman Jan 20 '17 at 14:48
0

Based on the names I would suggest that the outer lock is a readLock, and the inner lock a WriteLock. Both have different behavior. As there may be as many readLocks as possible, but a writeLock can only be the single lock compaired to both read- and writeLocks.

So in this example he locks for reading so no writing can be done without him releasing (Although in this case it might be a bit overkill). Usually there are some checks done to see if writing should be performed (not in this case) after which a writingLock is aqquired to perform the write actions Threadsafe.

The ReentrantLock is a default implementation of such mechanism, which you could use to observe/test these mechanisms

n247s
  • 1,898
  • 1
  • 12
  • 30
0

It increases locking granularity.

For example, suppose you want to protect some variables a and b with a lock. You could use one big lock:

public class BigLockDemo {

    private final Lock bigLock = new ReentrantLock();
    private int a;
    private int b;

    public int getA() {
        bigLock.lock();
        try {
            return a;
        } finally {
            bigLock.unlock();
        }
    }

    public void setA(int a) {
        bigLock.lock();
        try {
            this.a = a;
        } finally {
            bigLock.unlock();
        }
    }

    public int getB() {
        bigLock.lock();
        try {
            return b;
        } finally {
            bigLock.unlock();
        }
    }

    public void setB(int b) {
        bigLock.lock();
        try {
            this.b = b;
        } finally {
            bigLock.unlock();
        }
    }

    public void setAB(int a, int b) {
        bigLock.lock();
        try {
            // nobody holding the lock may see simultaneously
            // the new value of a and the old value of b
            this.a = a;
            this.b = b;
        } finally {
            bigLock.unlock();
        }
    }
}

However, if one threads reads/write only a and another reads/writes only B, they have to synchronize unnecessarily (for example, getA() will block while setB() holds the lock).

With two locks, you could avoid this problem:

public class ABLockDemo {

    private final Lock aLock = new ReentrantLock();
    private final Lock bLock = new ReentrantLock();
    private int a;
    private int b;

    public int getA() {
        aLock.lock();
        try {
            return a;
        } finally {
            aLock.unlock();
        }
    }

    public void setA(int a) {
        aLock.lock();
        try {
            this.a = a;
        } finally {
            aLock.unlock();
        }
    }

    public int getB() {
        bLock.lock();
        try {
            return b;
        } finally {
            bLock.unlock();
        }
    }

    public void setB(int b) {
        bLock.lock();
        try {
            this.b = b;
        } finally {
            bLock.unlock();
        }
    }

    public void setAB(int a, int b) {
        aLock.lock();
        bLock.lock();
        try {
            // nobody holding the locks may see simultaneously
            // the new value of a and the old value of b
            this.a = a;
            this.b = b;
        } finally {
            aLock.unlock();
            bLock.unlock();
        }
    }
}

In the case where you want to protect both a and b, you need to hold both locks.

rom1v
  • 2,752
  • 3
  • 21
  • 47