jforex78 jforex78 - 26 days ago 7
Java Question

tryLock() in a loop?

tryLock()
can fail to acquire lock. So if we use the return value to perform work we may not do the work at all.

Lock lock = new ReentrantLock();
boolean isLocked = lock.tryLock();

if (isLocked) {
try {
doWork();
} finally {
lock.unlock();
}
}


A
synchronized
would block until a lock is acquired, and so we know the
doWork()
will be done eventually.

So, is it right that we should
tryLock()
inside a loop until lock is acquired?

boolean isLocked = false;

while (!isLocked) {
isLocked = lock.tryLock();
Thread.sleep(100);
}

if (isLocked) {
try {
doWork();
} finally {
lock.unlock();
}
}

Answer Source

Almost never, no.

If you need to get the lock, you simply use the lock() call. This will obtain it immediately it if it available, or wait if not. Repeatedly looping on tryLock() also has the effect of waiting for the lock if it is not available, but without allowing proper OS-controlled blocking and waking of the waiting thread. Instead the waiting thread will continue to occupy a CPU, burning CPU time and power, while waiting for the lock to be released.

In one of the worst cases2, the thread which owns the lock may be context switched out and waiting for an available CPU to continue, but the CPU is not available since it is being used by a thread busy looping on tryLock. It's almost form of temporary deadlock - temporary because eventually the tryLock thread will use its quantum and be scheduled off, but this whole process could take many orders of magnitude longer than the usual approach of using lock().

All that to say that tryLock() has very limited uses. If you are using tryLock() in a tight loop, you are almost certainly doing something wrong. It is best used when you don't really need to get the lock and have other work you could do, and performance is important.

An example might be if you have to perform the same operation on several independent objects, each with their own lock. The default approach would simply be to lock() and unlock() each object in turn. An approach that might be faster would be to first tryLock() each object, updating any for which you get the lock (perhaps you do this more than once), and the returning to lock() any objects you missed the first time. By not initially waiting for any lock, and just continuing with the rest of the objects, you can probably achieve a higher update rate in a contended system.

Another case where tryLock() could be useful is when you can achieve the same effect in more than once way. Imagine for example some kind of "distributed counter"1, where you have one logical counter which is implemented as N separate equivalent counters each with their own lock. The value of the logical counter is simply the sum of the N underlying counters. To increment the counter (for example), you just need to increment any of the N counters. In this case, someone trying to update the counter can just tryLock() the underlying counters until he finds one that is "available" and modify it. This is likely to scale better than a single counter with a single lock.


1 Of course, for a simple counter you would likely use atomic integers, such as AtomicInteger or, better, LongAdder - but for more complex structures this might be realistic.

2 A worse scenario occurs if the tryLock() thread has a higher priority than the thread owning the lock, and the scheduling policy treats priorities strictly (i.e., a lower priority thread never preempts a higher priority one) like SCHED_FIFO or SCHED_RR. In that case, the higher-priority thread may spin indefinitely on tryLock(), since the lower priority thread which owns it will never wake due to the presence of a runnable higher priority thread. So you can even deadlock.