We are already aware of basic concepts around thread synchronization and various mechanisms using synchronized keyword. Java provides another mechanism for the synchronization of blocks of code based on the Lock
interface and classes that implement it (such as ReentrantLock
). In this tutorial, we will see a basic usage of Lock
interface to solve printer queue problem.
Lock Interface
A java.util.concurrent.locks.Lock
is a thread synchronization mechanism just like synchronized blocks. A Lock
is, however, more flexible and more sophisticated than a synchronized block. Since Lock
is an interface, you need to use one of its implementations to use a Lock in your applications. ReentrantLock
is one such implementation of Lock interface.
Here is the simple use of Lock interface.
Lock lock = new ReentrantLock(); lock.lock(); //critical section lock.unlock();
First a Lock is created. Then it’s lock()
method is called. Now the Lock
instance is locked. Any other thread calling lock()
will be blocked until the thread that locked the lock calls unlock()
. Finally unlock()
is called, and the Lock
is now unlocked so other threads can lock it.
Difference between Lock Interface and synchronized keyword
The main differences between a Lock and a synchronized block are:
1) Having a timeout trying to get access to a synchronized
block is not possible. Using Lock.tryLock(long timeout, TimeUnit timeUnit), it is possible.
2) The synchronized
block must be fully contained within a single method. A Lock can have it’s calls to lock()
and unlock()
in separate methods.
Simulating Printer Queue using Locks
In this example, program will simulate the behavior of a printer. You can submit a number of print jobs to printer during varying time interval or simultaneously. Printer will take a job from printer queue and print it. Rest of jobs will wait there for their turn. Once printer is done with print job in hand, it will pick another job from queue and start printing. Keep this happening in a loop.
PrintingJob.java
This class represents an independent printing which could be submitted to printer. This class implements Runnable
interface, so that printer can execute it when it’s turn come.
class PrintingJob implements Runnable { private PrinterQueue printerQueue; public PrintingJob(PrinterQueue printerQueue) { this.printerQueue = printerQueue; } @Override public void run() { System.out.printf("%s: Going to print a document\n", Thread.currentThread().getName()); printerQueue.printJob(new Object()); } }
PrinterQueue.java
This class represent the printer queue/ printer. A lock is maintained by printer to start new print job as soon as current print job is finished.
class PrinterQueue { private final Lock queueLock = new ReentrantLock(); public void printJob(Object document) { queueLock.lock(); try { Long duration = (long) (Math.random() * 10000); System.out.println(Thread.currentThread().getName() + ": PrintQueue: Printing a Job during " + (duration / 1000) + " seconds :: Time - " + new Date()); Thread.sleep(duration); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName()); queueLock.unlock(); } } }
Let’s test our printer program:
public class LockExample { public static void main(String[] args) { PrinterQueue printerQueue = new PrinterQueue(); Thread thread[] = new Thread[10]; for (int i = 0; i < 10; i++) { thread[i] = new Thread(new PrintingJob(printerQueue), "Thread " + i); } for (int i = 0; i < 10; i++) { thread[i].start(); } } } Output: Thread 0: Going to print a document Thread 9: Going to print a document Thread 8: Going to print a document Thread 7: Going to print a document Thread 5: Going to print a document Thread 6: Going to print a document Thread 4: Going to print a document Thread 3: Going to print a document Thread 2: Going to print a document Thread 1: Going to print a document Thread 0: PrintQueue: Printing a Job during 8 seconds :: Time - Tue Jan 06 15:19:02 IST 2015 Thread 0: The document has been printed Thread 9: PrintQueue: Printing a Job during 1 seconds :: Time - Tue Jan 06 15:19:11 IST 2015 Thread 9: The document has been printed Thread 8: PrintQueue: Printing a Job during 8 seconds :: Time - Tue Jan 06 15:19:12 IST 2015 Thread 8: The document has been printed Thread 7: PrintQueue: Printing a Job during 9 seconds :: Time - Tue Jan 06 15:19:21 IST 2015 Thread 7: The document has been printed Thread 5: PrintQueue: Printing a Job during 7 seconds :: Time - Tue Jan 06 15:19:31 IST 2015 Thread 5: The document has been printed Thread 6: PrintQueue: Printing a Job during 5 seconds :: Time - Tue Jan 06 15:19:39 IST 2015 Thread 6: The document has been printed Thread 4: PrintQueue: Printing a Job during 2 seconds :: Time - Tue Jan 06 15:19:44 IST 2015 Thread 4: The document has been printed Thread 3: PrintQueue: Printing a Job during 2 seconds :: Time - Tue Jan 06 15:19:46 IST 2015 Thread 3: The document has been printed Thread 2: PrintQueue: Printing a Job during 5 seconds :: Time - Tue Jan 06 15:19:49 IST 2015 Thread 2: The document has been printed Thread 1: PrintQueue: Printing a Job during 5 seconds :: Time - Tue Jan 06 15:19:54 IST 2015 Thread 1: The document has been printed
The key to the example is in the printJob()
method of the PrinterQueue
class. When we want to implement a critical section using locks and guarantee that only one execution thread runs a block of code, we have to create a ReentrantLock
object. At the beginning of the critical section, we have to get the control of the lock using the lock()
method.
At the end of the critical section, we have to use the unlock()
method to free the control of the lock and allow the other threads to run this critical section. If you don’t call the unlock()
method at the end of the critical section, the other threads that are waiting for that block will be waiting forever, causing a deadlock situation. If you use try-catch blocks in your critical section, don’t forget to put the sentence containing the unlock()
method inside the finally section.
Read More: How to create deadlock and solve it in java
Happy Learning !!
Rahul
Few Changes Suggested:
1-) In PrinterQueue change to : ReentrantLock queueLock = new ReentrantLock();
2-) Reduce time : Long duration = (long) (Math.random() * 100);
System.out.println(Thread.currentThread().getName() + “: PrintQueue: Printing a Job during ” + (duration / 10) + ” seconds :: Time – ” + new Date());
Jeena
First of all thanks for the nice post . I am a beginner and I am very confused with lock().
What exactly is lock() locking. I mean a thread or an object or what??
So just to be clear about my understanding of locks.
queueLock.lock(); will lock queueLock
and this means only one Thread can run this block of code(Between try and catch) at a time.
is it correct?
This code is locking the block as there is only one instance of PrinterQueue. Right??
Please help me understand it.
Thanks
Himansu
Hi Lokesh,
If possible can you cover a comparitive study of ReentrantLock against ReadWriteLock() and ReentrantReadWriteLock()
Regards,
Himansu
tarun
Important Difference in Lock and synchronized blocks :
Lock implementations provide additional functionality over the use of synchronized methods and statements by providing a non-blocking attempt to acquire a lock (tryLock()), an attempt to acquire the lock that can be interrupted (lockInterruptibly, and an attempt to acquire the lock that can timeout (tryLock(long, TimeUnit)).
They allow more flexible structuring, may have quite different properties, and may support multiple associated Condition objects.
– from JavaDoc
Himansu Nayak
Hi Lokesh,
There is one more key difference is unlike synchronized mechanism which uses implict JVM CAS instruction, Lock uses explicit “object” locking (not sure implicitly it uses CAS). Other then lock and unlock api this “object” it self can also be locked using synchronized. Quoting the javadoc
“Note that Lock instances are just normal objects and can themselves be used as the target in a synchronized statement. Acquiring the monitor lock of a Lock instance has no specified relationship with invoking any of the lock() methods of that instance. It is recommended that to avoid confusion you never use Lock instances in this way, except within their own implementation.”
Lokesh Gupta
Yes, locks implicitly use CAS using sun.misc.Unsafe. Thanks for your valuable comment.