Certification Objective Synchronizing Code (Objective 4. 3)


Certification Objective —Synchronizing Code (Objective 4. 3)

4.3 Given a scenario, write code that makes appropriate use of object locking to protect static or instance variables from concurrent access problems.

Can you imagine the havoc that can occur when two different threads have access to a single instance of a class, and both threads invoke methods on that object and those methods modify the state of the object? In other words, what might happen if two different threads call, say, a setter method on a single object? A scenario like that might corrupt an object's state (by changing its instance variable values in an inconsistent way), and if that object's state is data shared by other parts of the program, well, it's too scary to even visualize.

But just because we enjoy horror, let's look at an example of what might happen. The following code demonstrates what happens when two different threads are accessing the same account data. Imagine that two people each have a checkbook for a single checking account (or two people each have ATM cards, but both cards are linked to only one account).

In this example, we have a class called Account that represents a bank account. To keep the code short, this account starts with a balance of 50, and can be used only for withdrawals. The withdrawal will be accepted even if there isn't enough money in the account to cover it. The account simply reduces the balance by the amount you want to withdraw:

 class Account {    private int balance = 50;    public int getBalance() {       return balance;    }    public void withdraw(int amount) {       balance = balance - amount;    } } 

Now here's where it starts to get fun. Imagine a couple, Fred and Lucy, who both have access to the account and want to make withdrawals. But they don't want the account to ever be overdrawn, so just before one of them makes a withdrawal, he or she will first check the balance to be certain there's enough to cover the withdrawal. Also, withdrawals are always limited to an amount of 10, so there must be at least 10 in the account balance in order to make a withdrawal. Sounds reasonable. But that's a two'Step process:

  1. Check the balance.

  2. If there's enough in the account (in this example, at least 10), make the withdrawal.

What happens if something separates step 1 from step 2? For example, imagine what would happen if Lucy checks the balance and sees that there's just exactly enough in the account, 10. But before she makes the withdrawal, Fred checks the balance and also sees that there's enough for his withdrawal. Since Lucy has verified the balance, but not yet made her withdrawal, Fred is seeing "bad data. " He is seeing the account balance before Lucy actually debits the account, but at this point that debit is certain to occur. Now both Lucy and Fred believe there's enough to make their withdrawals. So now imagine that Lucy makes her withdrawal, and now there isn't enough in the account for Fred's withdrawal, but he thinks there is since when he checked, there was enough! Yikes. In a minute we'll see the actual banking code, with Fred and Lucy, represented by two threads, each acting on the same Runnable, and that Runnable holds a reference to the one and only account instance—so, two threads, one account.

The logic in our code example is as follows:

  1. The Runnable object holds a reference to a single account.

  2. Two threads arc started, representing Lucy and Fred, and each thread is given a reference to the same Runnable (which holds a reference to the actual account)

  3. The initial balance on the account is 50, and each withdrawal is exactly 10.

  4. In the run() method, we loop 5 times, and in each loop we

    • Make a withdrawal (if there's enough in the account).

    • Print a statement if the account is overdrawn (which it should never be, since we check the balance before making a withdrawal).

  5. The makeWithdrawal() method in the test class (representing the behavior of Fred or Lucy) will do the following:

    • Check the balance to see if there's enough for the withdrawal.

    • If there is enough, print out the name of the one making the withdrawal.

    • Go to sleep for 500 milliseconds—just long enough to give the other partner a chance to get in before you actually make the withdrawal.

    • Upon waking up, complete the withdrawal and print that fact.

    • If there wasn't enough in the first place, print a statement showing who you are and the fact that: there wasn't enough.

So what we're really trying to discover is if the following is possible: for one partner to check the account and see that there's enough, but before making the actual withdrawal, the other partner checks the account and also sees that there's enough. When the account balance gets to 10, if both partners check it before making the withdrawal, both will think it's OK to withdraw, and the account will overdraw by 10!

Here's the code:

 public class AccountDanger implements Runnable {    private Account acct = new Account();    public static void main (String [] args) {       AccountDanger r = new AccountDanger();       Thread one = new Thread(r);       Thread two = new Thread(r);       one.setName("Fred");       two.setName("Lucy");       one.start();       two.start();     }   public void run() {    for (int x = 0; x < 5; x++) {       makeWithdrawal(10);       if (acct.getBalance() < 0) {         System.out.printIn("account is overdrawn!");       }     } }  private void makeWithdrawal(int amt) {     if (acct.getBalance() >= amt) {        System.out.println(Thread.currentThread().getName()                   + " is going to withdraw");        try {          Thread.sleep(500);         } catch(InterruptedException ex)  { }         acct.withdraw(amt);         System.out.println(Thread.currentThread( ).getName( )                    + " completes the withdrawal");      } else {         System.out.println("Not enough in account for "                    + Thread.currentThread().getName()                    + " to withdraw " + acct.getBalance());      }    } } 

So what happened? Is it possible that, say, Lucy checked the balance, fell asleep, Fred checked the balance, Lucy woke up and completed her withdrawal, then Fred completes his withdrawal, and in the end they overdraw the account? Look at the (numbered) output:

 % java AccountDanger  1. Fred is going to withdraw  2. Lucy is going to withdraw  3. Fred completes the withdrawal  4. Fred is going to withdraw  5. Lucy completes the withdrawal  6. Lucy is going to withdraw  7. Fred completes the withdrawal  8. Fred is going to withdraw  9. Lucy completes the withdrawal 10. Lucy is going to withdraw 11. Fred completes the withdrawal 12. Not enough in account for Fred to withdraw 0 13. Not enough in account for Fred to withdraw 0 14. Lucy completes the withdrawal 15. account is overdrawn! 16. Not enough in account for Lucy to withdraw -10 17. account is overdrawn! 18. Not enough in account for Lucy to withdraw -10 19. account is overdrawn! 

Although each time you run this code the output might be a little different, let's walk through this particular example using the numbered lines of output. For the first four attempts, everything is fine. Fred checks the balance on line 1, and finds it's OK. At line 2, Lucy checks the balance and finds it OK. At line 3, Fred makes his withdrawal. At this point, the balance Lucy checked for (and believes is still accurate) has actually changed since she last checked. And now Fred checks the balance again, before Lucy even completes her first withdrawal. By this point, even Fred is seeing a potentially inaccurate balance, because we know Lucy is going to complete her withdrawal. It is possible, of course, that Fred will complete his before Lucy does, but that's not what happens here.

On line 5, Lucy completes her withdrawal and then before Fred completes his, Lucy does another check on the account on line 6. And so it continues until we get to line 8, where Fred checks the balance and sees that it's 20. On line 9, Lucy completes a withdrawal (that she had checked for earlier), and this takes the balance to 10. On line 10, Lucy checks again, sees that the balance is 10, so she knows she can do a withdrawal. But she didn't know that Fred, too, has already checked the balance on line 8 so he thinks it's safe to do the withdrawal! On line 11, Fred completes the withdrawal he approved on line 8. This takes the balance to zero. But Lucy still has a pending withdrawal that she got approval for on line 10! You know what's coming.

On lines 12 and 13, Fred checks the balance and finds that there's not enough in the account. But on line 14, Lucy completes her withdrawal and BOOM! The account is now overdrawn by 10—something we thought we were preventing by doing a balance check prior to a withdrawal.

Figure 9-4 shows the timeline of what can happen when two threads concurrently access the same object.

image from book
Figure 9-4: Problems with concurrent access

This problem is known as a "race condition," where multiple threads can access the same resource (typically an object's instance variables), and can produce corrupted data if one thread "races in" too quickly before an operation that should be "atomic" has completed.

Preventing the Account Overdraw So what can be done? The solution is actually quite simple. We must guarantee that the two steps of the withdrawal—checking the balance and making the withdrawal—are never split apart. We need them to always be performed as one operation, even when the thread falls asleep in between step 1 and step 2! We call this an "atomic operation" (although the physics is a little outdated, in this case "atomic" means "indivisible") because the operation, regardless of the number of actual statements (or underlying byte code instructions), is completed before any other thread code that acts on the same data.

You can't guarantee that a single thread will stay running throughout the entire atomic operation. But you can guarantee that even if the thread running the atomic operation moves in and out of the running state, no other running thread will be able to act on the same data. In other words, If Lucy falls asleep after checking the balance, we can stop Fred from checking the balance until after Lucy wakes up and completes her withdrawal.

So how do you protect the data? You must do two things:

  • Mark the variables private.

  • Synchronize the code that modifies the variables.

Remember, you protect the variables in the normal way—using an access control modifier. It's the method code that you must protect, so that only one thread at a time can be executing that code. You do this with the synchronized keyword.

We can solve all of Fred and Lucy's problems by adding one word to the code. We mark the makeWithdrawal () method synchronized as follows:

 private synchronized void makeWithdrawal(int amt) {   if (acct.getBalance() >= amt) {     System.out.println(Thread.currentThread().getName() +                          " is going to withdraw");     try {       Thread.sleep(500);     } catch(InterruptedException ex) { }     acct.withdraw(amt);     System.out.println(Thread.currentThread().getName() +                          " completes   the  withdrawal");   } else {     System.out.println("Not enough in account for "                        + Thread.currentThread().getName()                        + " to withdraw " + acct.getBalance ( ));   } } 

Now we've guaranteed that once a thread (Lucy or Fred) starts the withdrawal process (by invoking makeWithdrawal()), the other thread cannot enter that method until the first one completes the process by exiting the method. The new output shows the benefit of synchronizing the makeWithdrawal() method:

 % Java AccountDanger Fred is going to withdraw Fred completes the withdrawal Lucy is going to withdraw Lucy completes the withdrawal Fred is going to withdraw Fred completes the withdrawal Lucy is going to withdraw Lucy completes the withdrawal Fred is going to withdraw Fred completes the withdrawal Not enough in account for Lucy to withdraw 0 Not enough in account for Fred to withdraw 0 Not enough in account for Lucy to withdraw 0 Not enough in account for Fred to withdraw 0 Not enough in account for Lucy to withdraw 0 

Notice that now both threads, Lucy and Fred, always check the account balance and complete the withdrawal before the other thread can check the balance.

Synchronization and Locks

How does synchronization work? With locks. Every object in Java has a built-in lock that only comes into play when the object has synchronized method code. When we enter a synchronized non-static method, we automatically acquire the lock associated with the current instance of the class whose code we're executing (the this instance). Acquiring a lock for an object: is also known as getting the lock, or locking the object, locking on the object, or synchronizing on the object. We may also use the term monitor to refer to the object whose lock we're acquiring. Technically the lock and the monitor are two different things, but most people talk about the two interchangeably, and we will too.

Since there is only one lock per object, if one thread has picked up the lock, no other thread can pick up the lock until the first thread releases (or returns) the lock. This means no other thread can enter the synchronized code (which means it can't enter any synchronized method of that object) until the lock has been released. Typically, releasing a lock means the thread holding the lock (in other words, the thread currently in the synchronized method) exits the synchronized method. At that point, the lock is free until some other thread enters a synchronized method on that object. Remember the following key points about locking and synchronization:

  • Only methods (or blocks) can be synchronized, not variables or classes.

  • Each object has just one lock.

  • Not all methods in a class need to be synchronized. A class can have both synchronized and non-synchronized methods.

  • If two threads are about to execute a synchronized method in a class, and both threads are using the same instance of the class to invoke the method, only one thread at a time will be able to execute the method. The other thread will need to wait until the first one finishes its method call. In other words, once a thread acquires the lock on an object, no other thread can enter any of the synchronized methods in that class (for that object).

  • If a class has both synchronized and non-synchronized methods, multiple threads can still access the class's non-synchronized methods! If you have methods that don't access the data you're trying to protect, then you don't need to synchronize them. Synchronization can cause a hit in some cases (or even deadlock if used incorrectly), so you should be careful not to overuse it.

  • If a thread goes to sleep, it holds any locks it has—it doesn't release them.

  • A thread can acquire more than one lock. For example, a thread can enter a synchronized method, thus acquiring a lock, and then immediately invoke a synchronized method on a different object, thus acquiring that lock as well. As the stack unwinds, locks are released again. Also, if a thread acquires a lock and then attempts to call a synchronized method on that same object, no problem. The JVM knows that this thread already has the lock for this object, so the thread is free to call other synchronized methods on the same object, using the lock the thread already has.

  • You can synchronize a block of code rather than a method.

Because synchronization does hurt concurrency, you don't want to synchronize any more code than is necessary to protect your data. So if the scope of a method is more than needed, you can reduce the scope of the synchronized part to something less than a full method—to just a block. We call this, strangely, a synchronized block, and it looks like this:

 class SyncTest {   public void doStuff() {     System.out.println("not synchronized");     synchronized(this) {       System.out.println("synchronized");     }   } } 

When a thread is executing code from within a synchronized block, including any method code invoked from that synchronized block, the code is said to be executing in a synchronized context. The real question is, synchronized on what? Or, synchronized on which object's lock?

When you synchronize a method, the object used to invoke the method is the object whose lock must be acquired. But when you synchronize a block of code, you specify which object's lock you want to use as the lock, so you could, for example, use some third-party object as the lock for this piece of code. That gives you the ability to have more than one lock for code synchronization within a single object.

Or you can synchronize on the current instance (this) as in the code above. Since that's the same instance that synchronized methods lock on, it means that you could always replace a synchronized method with a non-synchronized method containing a synchronized block. In other words, this:

 public synchronized void doStuff () {     System.out.println("synchronized"); } 

is equivalent to this:

 public void doStuff() {     synchronized(this) {         System.out.println("synchronized");     } } 

These methods both have the exact same effect, in practical terms. The compiled bytecodes may not be exactly the same for the two methods, but they could be—and any differences are not really important. The first form is shorter and more familiar to most people, but the second can be more flexible.

So What About Static Methods? Can They Be Synchronized?

static methods can be synchronized. There is only one copy of the static data you're trying to protect, so you only need one lock per class to synchronize static methods—a lock for the whole class. There is such a lock; every class loaded in Java has a corresponding instance of java.lang.Class representing that class. It's that java.lang.Class instance whose lock is used to protect the static methods of the class (if they're synchronized). There's nothing special you have to do to synchronize a static method:

 public static synchronized int getCount() {     return count; } 

Again, this could be replaced with code that uses a synchronized block. If the method is defined in a class called MyClass, the equivalent code is as follows:

 public static int getCount() {     synchronized(MyClass.class) {         return count;     } } 

Wait—what's that MyClass.class thing? That's called a class literal. It's a special feature in the Java language that tells the compiler (who tells the JVM): go and find me the instance of Class that represents the class called MyClass. You can also do this with the following code:

 public static void classMethod() {     Class c1 = Class.forName("MyClass");     synchronized (cl) {         // do stuff     } } 

However that's longer, ickier, and most important, not on the SCJP exam. But it's quick and easy to use a class literal—just write the name of the class, and add .class at the end. No quotation marks needed. Now you've got an expression for the Class object you need to synchronize on.

Exercise 9-2: Synchronizing a Block of Code

image from book

In this exercise we will attempt to synchronize a block of code. Within that block of code we will get the lock on an object, so that other threads cannot modify it while the block of code is executing. We will be creating three threads that will all at tempt to manipulate the same object. Each thread will output a single letter 100 times, and then increment that letter by one. The object we will be using is StringBuffer.

We could synchronize on a String object, but strings cannot be modified once they are created, so we would not be able to increment the letter without generating a new String object. The final output should have 100 As, 100 Bs, and 100 Cs all in unbroken lines.

1. 

  1. Create a class and extend the Thread class.

  2. Override the run() method of Thread. This is where the synchronized block of code will go.

  3. For our three thread objects to share the same object, we will need to create a constructor that accepts a StringBuffer object in the argument.

  4. The synchronized block of code will obtain a lock on the StringBuffer object from step 3.

  5. Within the block, output the StringBuffer 100 times and then increment the letter in the StringBuffer. You can check Chapter 5 for StringBuffer methods that will help with this.

  6. Finally, in the main() method, create a single StringBuffer object using the letter A, then create three instances of our class and start all three of them.

image from book

Answers

1. 

Your code might look something like this when completed:

 class InSync extends Thread {   StringBuffer letter;   public InSync(StringBuffer letter) { this.letter = letter; }   public void run() {     synchronized(letter) {      // #1       for(int i = 1;i<=100;++i) System.out.print(letter);       System.out.println();       char temp = letter.charAt(0);       ++temp;         // Increment the letter in StringBuffer:       letter.setCharAt(0, temp);     }     // #2   }   public static void main(String [] args) {     StringBuffer sb = new StringBuffer("A");     new InSync(sb).start();  new InSync(sb).start();     new InSync(sb).start();   } } 

Just for fun, try removing lines I and 2 then run the program again. It will be unsynchronized—watch what happens.

image from book

What Happens If a Thread can't Get the Lock?

If a thread tries to enter a synchronized method and the lock is already taken, the thread is said to be blocked on the object's lock. Essentially, the thread goes into a kind of pool for that particular object and has to sit there until the lock is released and the thread can again become runnable/running. Just because a lock is released doesn't mean any particular thread will get it. There might be three threads waiting for a single lock, for example, and there's no guarantee that the thread that has waited the longest will get the lock first.

When thinking about blocking, it's important to pay attention to which objects are being used for locking.

  • Threads calling non-static synchronized methods in the same class will only block each other if they're invoked using the same instance. That's because they each lock on this instance, and if they're called using two different instances, they get two locks, which do not interfere with each other.

  • Threads calling static synchronized methods in the same class will always block each other—they all lock on the same Class instance.

  • A static synchronized method and a non-static synchronized method will not block each other, ever. The static method locks on a Class instance while the non-static method locks on the this instance—these actions do not interfere with each other at all.

  • For synchronized blocks, you have to look at exactly what object has been used for locking. (What's inside the parentheses after the word synchronized?) Threads that synchronize on the same object will block each other. Threads that synchronize on different objects will not.

Table 9-1 lists the thread-related methods and whether the thread gives up its lock as a result of the call.

Table 9-1: Methods and Lock Status

Give Up Locks

Keep Locks

Class Defining the Method

wait ()

notify() (Although the thread will probably exit the synchronized code shortly after this call, and thus give up its locks.)

java.lang.Object

 

join()

java.lang.Thread

 

sleep()

java.lang.Thread

 

yield()

java.lang.Thread

So When Do I need To Synchronize?

Synchronization can get pretty complicated, and you may be wondering why you would want to do this at all if you can help it. But remember the earlier "race conditions" example with Lucy and Fred making withdrawals from their account. When we use threads, we usually need to use some synchronization somewhere to make sure our methods don't interrupt each other at the wrong time and mess up our data. Generally, any time more than one thread is accessing mutable (changeable) data, you synchronize to protect that data, to make sure two threads aren't changing it at the same time (or that one isn't changing it at the same time the other is reading it, which is also confusing). You don't need to worry about local variables—each thread gets its own copy of a local variable. Two threads executing the same method at the same time will use different copies of the local variables, and they won't bother each other. However, you do need to worry about static and non-static fields, if they contain data that can be changed.

For changeable data in a non-static field, you usually use a non-static method to access it. By synchronizing that method, you will ensure that any threads trying to run that method using the same instance will be prevented from simultaneous access. But a thread working with a different instance will not be affected, because it's acquiring a lock on the other instance. That's what we want—threads working with the same data need to go one at a time, but threads working with different data can just ignore each other and run whenever they want to; it doesn't matter.

For changeable data in a static field, you usually use a static method to access it. And again, by synchronizing the method you ensure that any two threads trying to access the data will be prevented from simultaneous access, because both threads will have to acquire locks on the Class object for the class the static method's defined in. Again, that's what we want.

However—what if you have a non-static method that accesses a static field? Or a static method that accesses a non-static field (using an instance)? In these cases things start to get messy quickly, and there's a very good chance that things will not work the way you want. If you've got a static method accessing a non-static field, and you synchronize the method, you acquire a lock on the Class object. But what if there's another method that also accesses the non-static field, this time using a non-static method? It probably synchronizes on the current instance (this) instead. Remember that a static synchronized method and a non-static synchronized method will not block each other—they can run at the same time. Similarly, if you access a static field using a non-static method, two threads might invoke that method using two different this instances. Which means they won't block each other, because they use different locks. Which means two threads are simultaneously accessing the same static field—exactly the sort of thing we're trying to prevent.

It gets very confusing trying to imagine all the weird things that can happen here. To keep things simple: in order to make a class thread-safe, methods that access changeable fields need to be synchronized.

Access to static fields should be done from static synchronized methods. Access to non-static fields should be done from non-static synchronized methods. For example:

 public class Thing {     private static int staticField;     private int nonstaticField;     public static synchronized int getStaticField() {         return staticField;     }     public static synchronized void setStaticField(                                            int staticField) {         Thing.staticField = staticField;     }     public synchronized int getNonstaticField() {         return nonstaticField;     }     public synchronized void setNonstaticField(                                           int nonstaticField) {         this.nonstaticField = nonstaticField;     } } 

What if you need to access both static and non-static fields in a method? Well, there are ways to do that, but it's beyond what you need for the exam. You will live a longer, happier life if you JUST DON'T DO IT. Really. Would we lie?

Thread-Safe Classes

When a class has been carefully synchronized to protect its data (using the rules just given, or using more complicated alternatives), we say the class is "thread-safe." Many classes in the Java APIs already use synchronization internally in order to make the class "thread-safe." For example, StringBuffer and StringBuilder are nearly identical classes, except that all the methods in StringBuffer are synchronized when necessary, while those in StringBuilder are not. Generally, this makes StringBuffer safe to use in a multithreaded environment, while StringBuilder is not. (In return, StringBuilder is a little bit faster because it doesn't bother synchronizing.) However, even when a class is "thread-safe," it is often dangerous to rely on these classes to provide the thread protection you need. (C'mon, the repeated quotes used around "thread-safe" had to be a clue, right?) You still need to think carefully about how you use these classes, As an example, consider the following class.

 import java.util.*; public class NameList {     private List names = Collections.synchronizedList(                                            new LinkedList());     public void add(String name) {         names.add(name);     }     public String removeFirst() {         if (names.size() > 0)             return (String) names.remove(0);         else             return null;     } } 

The method collections.synchronizedList() returns a List whose methods are all synchronized and "thread-safe" according to the documentation (like a Vector—but since this is the 21st century, we're not going to use a Vector here). The question is, can the NameList class be used safely from multiple threads? It's tempting to think that yes, since the data in names is in a synchronized collection, the NameList class is "safe" too. However that's not the case—the removeFirst() may sometimes throw a NoSuchElementException. What's the problem? Doesn't it correctly check the size() of names before removing anything, to make sure there's something there? How could this code fail? Let's try to use NameList like this:

 public static void main(String[] args) {     final NameList nl = new NameList();     nl.add("Ozymandias");     class NameDropper extends Thread {         public void run() {             String name = n1.removeFirst();             Systein.out.println(name);         }     }     Thread t1 = new NameDropper();     Thread t2 = new NameDropper();     t1.start();     t2.start(); } 

What might happen here is that one of the threads will remove the one name and print it, then the other will try to remove a name and get null. If we think just about the calls to names.size() and names.get(0), they occur in this order:

  • Thread t1 executes names.size(), which returns 1.

  • Thread t1 executes names.remove(0), which returns Ozymandias.

  • Thread t2 executes names.size(), which returns 0.

  • Thread t2 does not call remove(0).

The output here is

 Ozymandias null 

However, if we run the program again something different might happen:

  • Thread t1 executes names.size(), which returns 1.

  • Thread t2 executes names.size(), which returns 1.

  • Thread t1 executes names.remove(0), which returns Ozymandias.

  • Thread t2 executes names.remove(0), which throws an exception because the list is now empty.

The thing to realize here is that in a "thread-safe" class like the one returned by synchronizedList(), each individual method is synchronized. So names.size() is synchronized, and names.remove(0) is synchronized. But nothing prevents another thread from doing something else to the list in between those two calls. And that's where problems can happen.

There's a solution here: don't rely on Collections.synchronizedList(). Instead, synchronize the code yourself:

 import java.util.*; public class NameList {     private List names = new LinkedList();     public synchronized void add(String name) {         names.add(name);     }     public synchronized String removeFirst() {         if (names.size() > 0)             return (String) names.remove(0);         else             return null;     } } 

Now the entire removeFirst() method is synchronized, and once one thread starts it and calls names.size(), there's no way the other thread can cut in and steal the last name. The other thread will just have to wait until the first thread completes the removeFirst() method.

The moral here is that just because a class is described as "thread-safe" doesn't mean it is always thread-safe. If individual methods are synchronized, that may not be enough—you may be better off putting in synchronization at a higher level (i.e., put it in the block or method that calls the other methods). Once you do that, the original synchronization (in this case, the synchronization inside the object returned by Collections.synchronizedList()) may well become redundant.

Thread Deadlock

Perhaps the scariest thing that can happen to a Java program is deadlock. Deadlock occurs when two threads are blocked, with each waiting for the other's lock. Neither can run until the other gives up its lock, so they'll sit there forever.

This can happen, for example, when thread A hits synchronized code, acquires a lock B, and then enters another method (still within the synchronized code it has the lock on) that's also synchronized. But thread A can't get the lock to enter this synchronized code—block C—because another thread D has the lock already. So thread A goes off to the waiting-for-the-C-lock pool, hoping that thread D will hurry up and release the lock (by completing the synchronized method). But thread A will wait a very long time indeed, because while thread D picked up lock C, it then entered a method synchronized on lock B. Obviously, thread D can't get the lock B because thread A has it. And thread A won't release it until thread D releases lock C. But thread D won't release lock C until after it can get lock B and continue. And there they sit. The following example demonstrates deadlock:

  1. public class DeadlockRisk {  2.   private static class Resource {  3.     public int value;  4.   }  5.   private Resource resourceA = new Resource();  6.   private Resource resourceB = new Resource();  7.   public int read() {  8.     synchronized(resourceA) { // May deadlock here  9.       synchronized(resourceB) 10.         return resourceB.value + resourceA.value; 11.       } 12.     } 13.   } 14. 15.   public void write(int a, int b) { 16.     synchronized(resourceB) { // May deadlock here 17.       synchronized(resourceA) { 18.         resourceA.value = a; 19.         resourceB.value = b; 20.       } 21.     } 22.   } 23. } 

Assume that read() is started by one thread and write() is started by another. If there are two different threads that may read and write independently, there is a risk of deadlock at line 8 or 16. The reader thread will have resourceA, the writer thread will have resourceB, and both will get stuck waiting for the other.

Code like this almost never results in deadlock because the CPU has to switch from the reader thread to the writer thread at a particular point in the code, and the chances of deadlock occurring are very small. The application may work fine 99.9 percent of the time.

The preceding simple example is easy to fix; just swap the order of locking for either the reader or the writer at lines 16 and 17 (or lines 8 and 9). More complex deadlock situations can take a long time to figure out.

Regardless of how little chance there is for your code to deadlock, the bottom line is, if you deadlock, you're dead. There are design approaches that can help avoid deadlock, including strategies for always acquiring locks in a predetermined order.

But that's for you to study and is beyond the scope of this book. We're just trying to get you through the exam. If you learn everything in this chapter, though, you'll still know more about threads than most experienced Java programmers.




SCJP Sun Certified Programmer for Java 5 Study Guide Exam 310-055
SCJP Sun Certified Programmer for Java 5 Study Guide (Exam 310-055) (Certification Press)
ISBN: 0072253606
EAN: 2147483647
Year: 2006
Pages: 131

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net