After semaphores I decided to look at the Lock interface in Java. The first question that comes is "Why use locks when wait notify works well enough ?" I looked in the documentation for the class and the below lines stand out:
I have a cake that I would like to share with friends. Every friend takes a slice of cake eats it and updates the number of slices remaining. This ensures that no friend lifts the plate and finds it empty. (Like I would ever care , but lets assume I am a good host for the sake of the example! ) First the cake:
To test the above scenario:
The tryLock also has an overloaded version which waits for lock until a set period before returning.
There is also a lockInterruptibly method.It is a blocking method. If the lock is not available than the "the current thread becomes disabled for thread scheduling purposes and lies dormant". During this period if some other thread interrupts this thread than "if interruption of lock acquisition is supported, then InterruptedException is thrown and the current thread's interrupted status is cleared."
While the scoping mechanism for synchronized methods and statements makes it much easier
to program with monitor locks, and helps avoid many common programming errors involving
locks, there are occasions where you need to work with locks in a more flexible way. For example,
some algorithms for traversing concurrently accessed data structures require the use of
"hand-over-hand" or "chain locking": you acquire the lock of node A, then node B, then release A
and acquire C, then release B and acquire D and so on. Implementations of the Lock interface
enable the use of such techniques by allowing a lock to be acquired and released in different scopes,
and allowing multiple locks to be acquired and released in any order.
To do the above example of chain locking using synchronized blocks:
synchronized(A) { synchronized(B) { } } //now release A and acquire C synchronized(B) { synchronized(C) { } }As seen here block based scope requires that the lock A to be released only after lock on B is released. We have to freshly acquire the lock again in order to acquire lock on C. Of course we could enforce the requirement via code, For example hold locks on some other object and do this lock handling within it.
synchronized(super) { //do all the chain handling here //Only Threads with lock on super can acquire lock on A/B/C... }The code is complicated to say the least (And who knows how many more scenarios will come up once I actually implement it ?). This above behavior could be achieved much more cleanly and easily (?? Not sure if any kind of Threading is easy, but that's a different story). Lets try the above feature with a simple example.
I have a cake that I would like to share with friends. Every friend takes a slice of cake eats it and updates the number of slices remaining. This ensures that no friend lifts the plate and finds it empty. (Like I would ever care , but lets assume I am a good host for the sake of the example! ) First the cake:
class Cake { private int pieces; public Cake(int slices) { this.pieces = slices; } public void eat(String friend) { int pieces = this.pieces; System.out.println("A piece of cake eaten by " + friend + ", No of pieces that remain " + (pieces - 1)); this.pieces = pieces - 1; } public boolean remains() { return pieces > 0; } }As seen above, the class has two simple methods - an eat which allows us to.... eat the cake and a boolean method that tells us if any cake remains. Next is the Friend class:
class Friend implements Runnable { int maxCapacity = 0; int digestTime = 100; Friend(int maxCapacity, int digestTime) { this.maxCapacity = maxCapacity; this.digestTime = digestTime; } @Override public void run() { String threadName = Thread.currentThread().getName(); int piecesEaten = 0; while (piecesEaten < maxCapacity) { System.out.println(threadName + " trying to get a slice ..."); LockTest.cakeLock.lock();// like a wait if (LockTest.cake.remains()) { System.out.println("Cake is available for " + threadName + " - eating it. (Slize No. - " + (piecesEaten + 1) + " )"); LockTest.cake.eat(threadName); piecesEaten++; LockTest.cakeLock.unlock(); // like a notify } else { System.out.println("Cake is finished ! " + threadName + " is leaving. (Total slizes eaten - " + piecesEaten + " )"); LockTest.cakeLock.unlock(); // like a notify break; } try { Thread.sleep(digestTime);// take some time to digest ! } catch (InterruptedException e) { e.printStackTrace(); } if (piecesEaten == maxCapacity) { System.out.println(threadName + " Has had his full. Leaving." +
" Total slizes eaten - " + piecesEaten);
}
}
}
}
As seen here every Friend tries to eat the maximum amount of cakes he can. Also after eating a cake he rests for a certain time to allow digestion. Instead of the synchronized block that we would have used for the cake instance, I have used a lock class.To test the above scenario:
public class LockTest { static Lock cakeLock = new ReentrantLock();// the most basic lock // (works likes the synchronized keyword + additional capabilities) static Cake cake = new Cake(6); public static void main(String[] args) { new Thread(new Friend(2, 100), "Selene").start(); new Thread(new Friend(3, 50), "Marvin").start(); new Thread(new Friend(2, 40), "Steve").start(); new Thread(new Friend(2, 140), "Rhea").start(); } }If I run the code:
Selene trying to get a slice ... Rhea trying to get a slice ... Cake is available for Rhea - eating it. (Slize No. - 1 ) A piece of cake eaten by Rhea, No of pieces that remain 5 Cake is available for Selene - eating it. (Slize No. - 1 ) A piece of cake eaten by Selene, No of pieces that remain 4 Marvin trying to get a slice ... Cake is available for Marvin - eating it. (Slize No. - 1 ) A piece of cake eaten by Marvin, No of pieces that remain 3 Steve trying to get a slice ... Cake is available for Steve - eating it. (Slize No. - 1 ) A piece of cake eaten by Steve, No of pieces that remain 2 Steve trying to get a slice ... Cake is available for Steve - eating it. (Slize No. - 2 ) A piece of cake eaten by Steve, No of pieces that remain 1 Marvin trying to get a slice ... Cake is available for Marvin - eating it. (Slize No. - 2 ) A piece of cake eaten by Marvin, No of pieces that remain 0 Steve Has had his full. Leaving. Total slizes eaten - 2 Selene trying to get a slice ... Cake is finished ! Selene is leaving. (Total slizes eaten - 1 ) Marvin trying to get a slice ... Cake is finished ! Marvin is leaving. (Total slizes eaten - 2 ) Rhea trying to get a slice ... Cake is finished ! Rhea is leaving. (Total slizes eaten - 1 )As seen here, No thread could proceed until they acquired the lock. Also because of their varied capacity and digest time, different threads ended up eating different number of slices. While this showed how to use Locks, we still didn't actually try the "hand-over-hand" scenario described in the docs.
private static void eatCake(String friend) { // pre requisite - hold the necessary locks cakeLock.lock();// Reentrant Lock - so same thread can acquire several times if (cake.remains()) { cake.eat(friend); cakeLock.unlock(); } else { cakeLock.unlock(); throw new RuntimeException("No cake Available !!"); } } public static void main(String[] args) { String friend1 = "Selene"; Lock friend1Lock = new ReentrantLock(); String friend2 = "Marvin"; Lock friend2Lock = new ReentrantLock(); String friend3 = "Steve"; Lock friend3Lock = new ReentrantLock(); cakeLock.lock(); friend1Lock.lock(); friend2Lock.lock(); eatCake(friend1); friend1Lock.unlock(); friend3Lock.lock(); eatCake(friend2); friend2Lock.unlock(); eatCake(friend3); friend3Lock.unlock(); cakeLock.unlock(); }In our Friend class we acquired a lock by calling:
System.out.println(threadName + " trying to get a slice ..."); LockTest.cakeLock.lock();// like a waitThe lock method is a blocking method. It waits until a lock is available. Say we had a series of cakes to offer. Our thread instance would prefer to have a different cake than wait till this one is available.
{ List<Cake> cakes = new ArrayList<Cake>(3); // a list containing all cakes List<Lock> cakeLocks = new ArrayList<Lock>(3); boolean hadCake = false; while(!hadCake) { for (int i = 0; i < cakes.size(); i++) { if(cakeLocks.get(i).tryLock()) { hadCake = true; cakes.get(i).eat(friend); cakeLocks.get(i).unlock(); break; } } }Here we have used the tryLock method. This is the non-blocking version. If lock is not available it returns immediately with a false value.
The tryLock also has an overloaded version which waits for lock until a set period before returning.
There is also a lockInterruptibly method.It is a blocking method. If the lock is not available than the "the current thread becomes disabled for thread scheduling purposes and lies dormant". During this period if some other thread interrupts this thread than "if interruption of lock acquisition is supported, then InterruptedException is thrown and the current thread's interrupted status is cleared."
No comments:
Post a Comment