Java locks often supersede synchronized blocks and methods by allowing to back out from the synchronization attempt. This post shows how to use the feature, and demonstrates some other basic concepts, like the lock fairness and reentrant locks, which should help you to get started with the lock-based synchronization.
The reentrant lock
The ReentrantLock
is the most often used implementation of the Lock
interface. Reentrant means that the same thread can acquire a lock multiple times, which could be extremely handy when you need to chain synchronized method calls. The only caveat here is that the lock must be released the same number of times it was requested by a thread:
public class ReentrantExample {
Lock lock = new ReentrantLock();
public void foo() {
lock.lock();
bar();
lock.unlock();
}
public void bar() {
lock.lock();
//do something
lock.unlock();
}
}
Code language: PHP (php)
As you can see, the ReentrantLock
implementation allows us to call the lock.lock()
two times from the one thread, and execute the bar from the locked block of the foo method.
Lock fairness
The second concept, we will have a look at, is the lock fairness. Lock fairness is quite easy to grasp, but when used incorrectly can lead to confusing, blocking-alike issues.
The expected behaviour for threads is to acquire a lock in the same order they ask for it. But, in case of the unfair lock this order is not guaranteed, and a thread can get a lock before the other thread that asked for the lock first. As you have already guessed, there is a risk that one of the threads will never acquire the lock.
So, usually the rule of thumb is to set the ordering policy to fair when instantiating a lock object:
public Lock lock = new ReentrantLock(true); // declaring the fair lock
Code language: PHP (php)
Try lock
The last concept and one of the main benefits of the Java locks is the try lock mechanism. The try lock method allows to back out of an attempt to acquire a lock if the lock is taken by another thread. Also, the method accepts the time
parameter, which can be especially useful when you need to limit the time a thread waits for a lock in order to speed up the application or to avoid deadlocks. For instance:
private class ThreadExample implements Runnable {
final Lock lock;
public ThreadExample(final Lock lock) {
this.lock = lock;
}
@Override
public void run() {
while (true) {
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("The lock is taken by " +
Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(2);
} finally {
lock.unlock();
System.out.println("The lock is released by" +
Thread.currentThread().getName());
}
break;
} else {
System.out.println("Thread" +
Thread.currentThread().getName() +
"unable to acquire the lock");
}
} catch (InterruptedException ignore) {
}
}
}
}
Code language: PHP (php)
final Lock lock = new ReentrantLock();
new Thread(new ThreadExample(lock), "thread_1").start();
new Thread(new ThreadExample(lock), "thread_2").start();
Code language: PHP (php)
Results in the following output:
The lock is taken by thread_1
Thread thread_2 unable to take the lock
The lock is released by thread_1
The lock is taken by thread_2
The lock is released by thread_2
Here, as the lock is not available and is already taken by the first thread, the tryLock
method called from the second thread backs out to the else block after one second of waiting.
Conclusion
Thread synchronization techniques are currently in a gradual decline, replaced by implementations of non-blocking asynchronous concepts, such as Actors or Agents. However, if you have a Java application that heavily relies on the synchronization, you can still get a sufficient flexibility with Java locks.