Home > Articles > Programming > Java

  • Print
  • + Share This
This chapter is from the book

Synchronization

In most practical multithreaded applications, two or more threads need to share access to the same objects. What happens if two threads have access to the same object and each calls a method that modifies the state of the object? As you might imagine, the threads step on each other's toes. Depending on the order in which the data was accessed, corrupted objects can result. Such a situation is often called a race condition.

Thread Communication Without Synchronization

To avoid simultaneous access of a shared object by multiple threads, you must learn how to synchronize the access. In this section, you'll see what happens if you do not use synchronization. In the next section, you'll see how to synchronize object access.

In the next test program, we simulate a bank with 10 accounts. We randomly generate transactions that move money between these accounts. There are 10 threads, one for each account. Each transaction moves a random amount of money from the account serviced by the thread to another random account.

The simulation code is straightforward. We have the class Bank with the method transfer. This method transfers some amount of money from one account to another. If the source account does not have enough money in it, then the call simply returns. Here is the code for the transfer method of the Bank class.

public void transfer(int from, int to, double amount)
  // CAUTION: unsafe when called from multiple threads
{ 
  if (accounts[from] < amount) return;
  accounts[from] -= amount;
  accounts[to] += amount;
  ntransacts++;
  if (ntransacts % NTEST == 0) test();
}

Here is the code for the TransferThread class. Its run method keeps moving money out of a fixed bank account. In each iteration, the run method picks a random target account and a random amount, calls transfer on the bank object, and then sleeps.

class TransferThread extends Thread
{ 
  public TransferThread(Bank b, int from, int max)
  { 
   bank = b;
   fromAccount = from;
   maxAmount = max;
  }
  
  public void run()
  { 
   try
   { 
     while (!interrupted())
     { 
      int toAccount = (int)(bank.size() * Math.random());
      int amount = (int)(maxAmount * Math.random());
      bank.transfer(fromAccount, toAccount, amount);
      sleep(1);
     }
   }
   catch(InterruptedException e) {}
  }

  private Bank bank;
  private int fromAccount;
  private int maxAmount;
}

When this simulation runs, we do not know how much money is in any one bank account at any time. But we do know that the total amount of money in all the accounts should remain unchanged since all we do is move money from one account to another.

Every 10,000 transactions, the transfer method calls a test method that recomputes the total and prints it out.

This program never finishes. Just press ctrl+c to kill the program.

Here is a typical printout:

  Transactions:10000 Sum: 100000
  Transactions:20000 Sum: 100000
  Transactions:30000 Sum: 100000
  Transactions:40000 Sum: 100000
  Transactions:50000 Sum: 100000
  Transactions:60000 Sum: 100000
  Transactions:70000 Sum: 100000
  Transactions:80000 Sum: 100000
  Transactions:90000 Sum: 100000
  Transactions:100000 Sum: 100000
  Transactions:110000 Sum: 100000
  Transactions:120000 Sum: 100000
  Transactions:130000 Sum: 94792
  Transactions:140000 Sum: 94792
  Transactions:150000 Sum: 94792
  . . .

As you can see, something is very wrong. For quite a few transactions, the bank balance remains at $100,000, which is the correct total for 10 accounts of $10,000 each. But after some time, the balance changes slightly. When you run this program, you may find that errors happen quickly, or it may take a very long time for the balance to become corrupted. This situation does not inspire confidence, and you would probably not want to deposit your hard-earned money into this bank.

Example 1–5 provides the complete source code. See if you can spot the problem with the code. We will unravel the mystery in the next section.

Example 1–5: UnsynchBankTest.java

	1.	public class UnsynchBankTest
	2.	{ 
	3.	  public static void main(String[] args)
	4.	  { 
	5.	   Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
	6.	   int i;
	7.	   for (i = 0; i < NACCOUNTS; i++)
	8.	   { 
	9.	     TransferThread t = new TransferThread(b, i,
	10.	      INITIAL_BALANCE);
	11.	     t.setPriority(Thread.NORM_PRIORITY + i % 2);
	12.	     t.start();
	13.	   }
	14.	  }
	15.	
	16.	  public static final int NACCOUNTS = 10;
	17.	  public static final int INITIAL_BALANCE = 10000;
	18.	}
	19.	
	20.	/**
	21.	  A bank with a number of bank accounts.
	22.	*/
	23.	class Bank
	24.	{ 
	25.	  /**
	26.	   Constructs the bank.
	27.	   @param n the number of accounts
	28.	   @param initialBalance the initial balance
	29.	   for each account
	30.	  */
	31.	  public Bank(int n, int initialBalance)
	32.	  { 
	33.	   accounts = new int[n];
	34.	   int i;
	35.	   for (i = 0; i < accounts.length; i++)
	36.	     accounts[i] = initialBalance;
	37.	   ntransacts = 0;
	38.	  }
	39.	
	40.	  /**
	41.	   Transfers money from one account to another.
	42.	   @param from the account to transfer from
	43.	   @param to the account to transfer to
	44.	   @param amount the amount to transfer
	45.	  */
	46.	  public void transfer(int from, int to, int amount)
	47.	   throws InterruptedException
	48.	  { 
	49.	   accounts[from] -= amount;
	50.	   accounts[to] += amount;
	51.	   ntransacts++;
	52.	   if (ntransacts % NTEST == 0) test();
	53.	  }
	54.	
	55.	  /**
	56.	   Prints a test message to check the integrity
	57.	   of this bank object.
	58.	  */
	59.	  public void test()
	60.	  { 
	61.	   int sum = 0;
	62.	
	63.	   for (int i = 0; i < accounts.length; i++)
	64.	     sum += accounts[i];
	65.	
	66.	   System.out.println("Transactions:" + ntransacts
	67.	     + " Sum: " + sum);
	68.	  }
	69.	
	70.	  /**
	71.	   Gets the number of accounts in the bank.
	72.	   @return the number of accounts
	73.	  */
	74.	  public int size()
	75.	  { 
	76.	   return accounts.length;
	77.	  }
	78.	
	79.	  public static final int NTEST = 10000;
	80.	  private final int[] accounts;
	81.	  private long ntransacts = 0;
	82.	}
	83.	
	84.	/**
	85.	  A thread that transfers money from an account to other
	86.	  accounts in a bank.
	87.	*/
	88.	class TransferThread extends Thread
	89.	{ 
	90.	  /**
	91.	   Constructs a transfer thread.
    92.	   @param b the bank between whose account money is 
               transferred
	93.	   @param from the account to transfer money from
	94.	   @param max the maximum amount of money in each transfer 
	95.	  */
	96.	  public TransferThread(Bank b, int from, int max)
	97.	  { 
	98.	   bank = b;
	99.	   fromAccount = from;
	100.	   maxAmount = max;
	101.	  }
	102.	
	103.	  public void run()
	104.	  { 
	105.	   try
	106.	   { 
	107.	     while (!interrupted())
	108.	     { 
	109.	      for (int i = 0; i < REPS; i++)
	110.	      {
	111.	        int toAccount = (int)(bank.size() * Math.random());
	112.	        int amount = (int)(maxAmount * Math.random() / REPS);
	113.	        bank.transfer(fromAccount, toAccount, amount);
	114.	        sleep(1);
	115.	      }
	116.	     }
	117.	   }
	118.	   catch(InterruptedException e) {}
	119.	  }
	120.	
	121.	  private Bank bank;
	122.	  private int fromAccount;
	123.	  private int maxAmount;
	124.	  private static final int REPS = 1000;
	125.	}

Synchronizing Access to Shared Resources

In the previous section, we ran a program in which several threads updated bank account balances. After a while, errors crept in and some amount of money was either lost or spontaneously created. This problem occurs when two threads are simultaneously trying to update an account. Suppose two threads simultaneously carry out the instruction:

accounts[to] += amount;

The problem is that these are not atomic operations. The instruction might be processed as follows:

  1. Load accounts[to] into a register.

  2. Add amount.

  3. Move the result back to accounts[to].

Now, suppose the first thread executes Steps 1 and 2, and then it is interrupted. Suppose the second thread awakens and updates the same entry in the account array. Then, the first thread awakens and completes its Step 3.

That action wipes out the modification of the other thread. As a result, the total is no longer correct. (See Figure 1–7.)

Our test program detects this corruption. (Of course, there is a slight chance of false alarms if the thread is interrupted as it is performing the tests!)

Figure 1–7: Simultaneous access by two threads

NOTE

You can actually peek at the virtual machine bytecodes that execute each statement in our class. Run the command

javap -c -v Bank

to decompile the Bank.class file. For example, the line

accounts[to] += amount;

is translated into the following bytecodes.

aload_0
getfield #16 <Field Bank.accounts [J>
iload_1
dup2
laload
iload_3
i2l
lsub
lastore

What these codes mean does not matter. The point is that the increment command is made up of several instructions, and the thread executing them can be interrupted at the point of any instruction.

What is the chance of this corruption occurring? It is quite low, because each thread does so little work before going to sleep again that it is unlikely that the scheduler will preempt it. We found by experimentation that we could boost the probability of corruption by various measures, depending on the target platform. On Windows 98, it helps to assign half of the transfer threads a higher priority than the other half.

for (i = 0; i < NACCOUNTS; i++)
{ 
  TransferThread t = new TransferThread(b, i,
   INITIAL_BALANCE);
  t.setPriority(Thread.NORM_PRIORITY + i % 2);
  t.start();
}

When a higher-priority transfer thread wakes up from its sleep, it will preempt a lower-priority transfer thread.

NOTE

This is exactly the kind of tinkering with priority levels that we tell you not to do in your own programs. We were in a bind—we wanted to show you a program that can demonstrate data corruption. In your own programs, you presumably will not want to increase the chance of corruption, so you should not imitate this approach.

On Linux, thread priorities are ignored, and instead, a slight change in the run method did the trick—to repeat the transfer multiple times before sleeping.

final int REPS = 1000; 
for (int i = 0; i < REPS; i++)
{
  int toAccount = (int)(bank.size() * Math.random());
  int amount = (int)(maxAmount * Math.random() / REPS);
  bank.transfer(fromAccount, toAccount, amount);
  sleep(1);
}

On all platforms, it helps if you load your machine heavily, by running a few bloatware programs in parallel with the test.

The real problem is that the work of the transfer method can be interrupted in the middle. If we could ensure that the method runs to completion before the thread loses control, then the state of the bank account object would not be corrupted.

Many thread libraries force the programmer to fuss with so-called semaphores and critical sections to gain uninterrupted access to a resource. This is sufficient for procedural programming, but it is hardly object-oriented. The Java programming language has a nicer mechanism, inspired by the monitors invented by Tony Hoare.

You simply tag any operation that should not be interrupted as synchronized, like this:

public synchronized void transfer(int from, int to, 
  int amount)
{ 
  if (accounts[from] < amount) return;
  accounts[from] -= amount;
  accounts[to] += amount;
  ntransacts++;
  if (ntransacts % NTEST == 0) test();
}

When one thread calls a synchronized method, it is guaranteed that the method will finish before another thread can execute any synchronized method on the same object (see Figure 1–8). When one thread calls transfer and then another thread also calls transfer, the second thread cannot continue. Instead, it is deactivated and must wait for the first thread to finish executing the transfer method.

Figure 1–8: Comparison of unsynchronized and synchronized threads

Try it out. Tag the transfer method as synchronized and run the program again. You can run it forever, and the bank balance will not get corrupted.

In general, you will want to tag those methods as synchronized that require multiple operations to update a data structure, as well as those that retrieve a value from a data structure. You are then assured that these operations run to completion before another thread can use the same object.

Of course, the synchronization mechanism isn't free. As you'll see in the next section, some bookkeeping code is executed every time a synchronized method is called. Thus, you will not want to synchronize every method of every class. If objects are not shared among threads, then there is no need to use synchronization. If a method always returns the same value for a given object, then you don't need to synchronize that method. For example, the size method of our Bank class need not be synchronized—the size of a bank object is fixed after the object is constructed.

Because synchronization is too expensive to use for every class, it is usually a good idea to custom-build classes for thread communication. For example, suppose a browser loads multiple images in parallel and wants to know when all images are loaded. You can define a ProgressTracker class with synchronized methods to update and query the loading progress.

Some programmers who have experience with other threading models complain about Java synchronization, finding it cumbersome and inefficient. For system level programming, these may be valid complaints. But for application programmers, the Java model works quite nicely. Just remember to use supporting classes for thread communication—don't try to hack existing code by sprinkling a few synchronized keywords over it.

NOTE

In some cases, programmers try to avoid the cost of synchronization in code that performs simple independent load or store operations. However, that can be dangerous, for two reasons. First, a load or store of a 64-bit value is not guaranteed to be atomic. That is, if you make an assignment to a double or long field, half of the assignment could happen, then the thread might be preempted. The next thread then sees the field in an inconsistent state. Moreover, in a multiprocessor machine, each processor can work on a separate cache of data from the main memory. The synchronized keyword ensures that local caches are made consistent with the main memory, but unsynchronized methods have no such guarantee. It can then happen that one thread doesn't see a modification to a shared variable made by another thread.

NOTE

The volatile keyword is designed to address these situations. Loads and stores of a 64-bit variable that is declared as volatile are guaranteed to be atomic. In a multiprocessor machine, loads and stores of volatile variables should work correctly even for data in processor caches.

In some situations, it might be possible to avoid synchronization and use volatile variables instead. However, not only is this issue fraught with complexity, there also have been reports of virtual machine implementations that don't handle volatile variables correctly. Our recommendation is to use synchronization, not volatile variables, to guarantee thread safety.

Object Locks

When a thread calls a synchronized method, the object becomes "locked." Think of each object as having a door with a lock on the inside. It is quite common among Java programmers to visualize object locks by using a "rest room" analogy. The object corresponds to a rest room stall that can hold only one person at a time. In the interest of good taste, we will use a "telephone booth" analogy instead. Imagine a traditional, enclosed telephone booth and suppose it has a latch on the inside. When a thread enters the synchronized method, it closes the door and locks it. When another thread tries to call a synchronized method on the same object, it can't open the door, so it stops running. Eventually, the first thread exits its synchronized method and unlocks the door.

Periodically, the thread scheduler activates the threads that are waiting for the lock to open, using its normal activation rules that we already discussed. Whenever one of the threads that wants to call a synchronized method on the object runs again, it checks to see if the object is still locked. If the object is now unlocked, the thread gets to be the next one to gain exclusive access to the object.

However, other threads are still free to call unsynchronized methods on a locked object. For example, the size method of the Bank class is not synchronized and it can be called on a locked object.

When a thread leaves a synchronized method by throwing an exception, it still relinquishes the object lock. That is a good thing—you wouldn't want a thread to hog the object after it has exited the synchronized method.

If a thread owns the lock of an object and it calls another synchronized method of the same object, then that method is automatically granted access. The thread only relinquishes the lock when it exits the last synchronized method.

NOTE

Technically, each object has a lock count that counts how many synchronized methods the lock owner has called. Each time a new synchronized method is called, the lock count is increased. Each time a synchronized method terminates (either because of a normal return or because of an uncaught exception), the lock count is decremented. When the lock count reaches zero, the thread gives up the lock.

Note that you can have two different objects of the same class, each of which is locked by a different thread. These threads may even execute the same synchronized method. It's the object that's locked, not the method. In the telephone booth analogy, you can have two people in two separate booths. Each of them may execute the same synchronized method, or they may execute different methods—it doesn't matter.

Of course, an object's lock can only be owned by one thread at any given point in time. But a thread can own the locks of multiple objects at the same time, simply by calling a synchronized method on an object while executing a synchronized method of another object. (Here, admittedly, the telephone booth analogy breaks down.)

The wait and notify methods

Let us refine our simulation of the bank. We do not want to transfer money out of an account that does not have the funds to cover the transfer. Note that we cannot use code like:

if (bank.getBalance(from) >= amount)
  bank.transfer(from, to, amount);

It is entirely possible that the current thread will be deactivated between the successful outcome of the test and the call to transfer.

if (bank.getBalance(from) >= amount)
   // thread might be deactivated at this point 
  bank.transfer(from, to, amount);

By the time the thread is running again, the account balance may have fallen below the withdrawal amount. You must make sure that the thread cannot be interrupted between the test and the insertion. You do so by putting both the test and the transfer action inside the same synchronized method:

public synchronized void transfer(int from, int to, 
  int amount)
{ 
  while (accounts[from] < amount)
  { 
   // wait
   . . . 
  }
  // transfer funds
  . . .
}

Now, what do we do when there is not enough money in the account? We wait until some other thread has added funds. But the transfer method is synchronized. This thread has just gained exclusive access to the bank object, so no other thread has a chance to make a deposit. A second feature of synchronized methods takes care of this situation. You use the wait method of the Object class if you need to wait inside a synchronized method.

When wait is called inside a synchronized method, the current thread is blocked and gives up the object lock. This lets in another thread that can, we hope, increase the account balance.

The wait method can throw an InterruptedException when the thread is interrupted while it is waiting. In that case, you can either turn on the "interrupted" flag or propagate the exception—after all, the calling thread, not the bank object, should decide what to do with an interruption. In our case, we simply propagate the exception and add a throws specifier to the transfer method.

public synchronized void transfer(int from, int to, 
  int amount) throws InterruptedException
{ 
  while (accounts[from] < amount)
   wait();
  // transfer funds
  . . .
}

Note that the wait method is a method of the class Object, not of the class Thread. When calling wait, the bank object unlocks itself and blocks the current thread. (Here, the wait method was called on the this reference.)

NOTE

If a thread holds the locks to multiple objects, then the call to wait unlocks only the object on which wait was called. That means that the blocked thread can hold locks to other objects, which won't get unlocked until the thread is unblocked again. That's a dangerous situation that you should avoid.

There is an essential difference between a thread that is waiting to get inside a synchronized method and a thread that has called wait. Once a thread calls the wait method, it enters a wait list for that object. The thread is now blocked. Until the thread is removed from the wait list, the scheduler ignores it and it does not have a chance to continue running.

To remove the thread from the wait list, some other thread must call the notifyAll or notify method on the same object. The notifyAll method removes all threads from the object's wait list. The notify method removes just one arbitrarily chosen thread. When the threads are removed from the wait list, then they are again runnable, and the scheduler will eventually activate them again. At that time, they will attempt to reenter the object. As soon as the object lock is available, one of them will lock the object and continue where it left off after the call to wait.

To understand the wait and notifyAll/notify calls, let's trot out the telephone booth analogy once again. Suppose a thread locks an object and then finds it can't proceed, say because the phone is broken. First of all, it would be pointless for the thread to fall asleep while locked inside the booth. Then a maintenance engineerwould be prevented from entering and fixing the equipment. By calling wait, the thread unlocks the object and waits outside. Eventually, a maintenance engineer enters the booth, locks it from inside, and does something. After the engineer leaves the booth, the waiting threads won't know that the situation has improved—maybe the engineer just emptied the coin reservoir. The waiting threads just keep waiting, still without hope. By calling notifyAll, the engineer tells all waiting threads that the object state may have changed to their advantage. By calling notify, the engineer picks one of the waiting threads at random and only tells that one, leaving the others in their despondent waiting state.

It is crucially important that some other thread calls the notify or notifyAll method periodically. When a thread calls wait, it has no way of unblocking itself. It puts its faith in the other threads. If none of them bother to unblock the waiting thread, it will never run again. This can lead to unpleasant deadlock situations. If all other threads are blocked and the last active thread calls wait without unblocking one of the others, then it also blocks. There is no thread left to unblock the others, and the program hangs. The waiting threads are not automatically reactivated when no other thread is working on the object. We will discuss deadlocks later in this chapter.

As a practical matter, it is dangerous to call notify because you have no control over which thread gets unblocked. If the wrong thread gets unblocked, that thread may not be able to proceed. We simply recommend that you use the notifyAll method and that all threads be unblocked.

When should you call notifyAll? The rule of thumb is to call notifyAll whenever the state of an object changes in a way that might be advantageous to waiting threads. For example, whenever an account balance changes, the waiting threads should be given another chance to inspect the balance. In our example, we will call notifyAll when we have finished with the funds transfer.

public synchronized void transfer(int from, int to, 
  int amount)
{ 
  . . .
  accounts[from] -= amount;
  accounts[to] += amount;
  ntransacts++;
  notifyAll();
 . . .
}

This notification gives the waiting threads the chance to run again. A thread that was waiting for a higher balance then gets a chance to check the balance again. If the balance is sufficient, the thread performs the transfer. If not, it calls wait again.

Note that the call to notifyAll does not immediately activate a waiting thread. It only unblocks the waiting threads so that they can compete for entry into the object after the current thread has exited the synchronized method.

TIP

If your multithreaded program gets stuck, double-check that every wait is matched by a notifyAll.

If you run the sample program with the synchronized version of the transfer method, you will notice that nothing ever goes wrong. The total balance stays at $100,000 forever. (Again, you need to press ctrl+c to terminate the program.)

You will also notice that the program in Example 1–6 runs a bit slower—this is the price you pay for the added bookkeeping involved in the synchronization mechanism.

Example 1–6: SynchBankTest.java

	1.	public class SynchBankTest
	2.	{ 
	3.	  public static void main(String[] args)
	4.	  { 
	5.	   Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
	6.	   int i;
	7.	   for (i = 0; i < NACCOUNTS; i++)
	8.	   { 
	9.	     TransferThread t = new TransferThread(b, i,
	10.	      INITIAL_BALANCE);
	11.	     t.setPriority(Thread.NORM_PRIORITY + i % 2);
	12.	     t.start();
	13.	   }
	14.	  }
	15.	
	16.	  public static final int NACCOUNTS = 10;
	17.	  public static final int INITIAL_BALANCE = 10000;
	18.	}
	19.	
	20.	/**
	21.	  A bank with a number of bank accounts.
	22.	*/
	23.	class Bank
	24.	{ 
	25.	  /**
	26.	   Constructs the bank.
	27.	   @param n the number of accounts
	28.	   @param initialBalance the initial balance
	29.	   for each account
	30.	  */
	31.	  public Bank(int n, int initialBalance)
	32.	  { 
	33.	   accounts = new int[n];
	34.	   int i;
	35.	   for (i = 0; i < accounts.length; i++)
	36.	     accounts[i] = initialBalance;
	37.	   ntransacts = 0;
	38.	  }
	39.	
	40.	  /**
	41.	   Transfers money from one account to another.
	42.	   @param from the account to transfer from
	43.	   @param to the account to transfer to
	44.	   @param amount the amount to transfer
	45.	  */
	46.	  public synchronized void transfer(int from, int to, int amount)
	47.	   throws InterruptedException
	48.	  { 
	49.	   while (accounts[from] < amount)
	50.	     wait();
	51.	   accounts[from] -= amount;
	52.	   accounts[to] += amount;
	53.	   ntransacts++;
	54.	   notifyAll();
	55.	   if (ntransacts % NTEST == 0) test();
	56.	  }
	57.	
	58.	  /**
	59.	   Prints a test message to check the integrity
	60.	   of this bank object.
	61.	  */
	62.	  public synchronized void test()
	63.	  { 
	64.	   int sum = 0;
	65.	
	66.	   for (int i = 0; i < accounts.length; i++)
	67.	     sum += accounts[i];
	68.	
	69.	   System.out.println("Transactions:" + ntransacts
	70.	     + " Sum: " + sum);
	71.	  }
	72.	
	73.	  /**
	74.	   Gets the number of accounts in the bank.
	75.	   @return the number of accounts
	76.	  */
	77.	  public int size()
	78.	  { 
	79.	   return accounts.length;
	80.	  }
	81.	
	82.	  public static final int NTEST = 10000;
	83.	  private final int[] accounts;
	84.	  private long ntransacts = 0;
	85.	}
	86.	
	87.	/**
	88.	  A thread that transfers money from an account to other
	89.	  accounts in a bank.
	90.	*/
	91.	class TransferThread extends Thread
	92.	{ 
	93.	  /**
	94.	   Constructs a transfer thread.
	95.	   @param b the bank between whose account money is transferred
	96.	   @param from the account to transfer money from
	97.	   @param max the maximum amount of money in each transfer 
	98.	  */
	99.	  public TransferThread(Bank b, int from, int max)
	100.	  { 
	101.	   bank = b;
	102.	   fromAccount = from;
	103.	   maxAmount = max;
	104.	  }
	105.	
	106.	  public void run()
	107.	  { 
	108.	   try
	109.	   { 
	110.	     while (!interrupted())
	111.	     { 
	112.	      int toAccount = (int)(bank.size() * Math.random());
	113.	      int amount = (int)(maxAmount * Math.random());
	114.	      bank.transfer(fromAccount, toAccount, amount);
	115.	      sleep(1);
	116.	     }
	117.	   }
	118.	   catch(InterruptedException e) {}
	119.	  }
	120.	
	121.	  private Bank bank;
	122.	  private int fromAccount;
	123.	  private int maxAmount;
	124.	}

Here is a summary of how the synchronization mechanism works.

  1. To call a synchronized method, the implicit parameter must not be locked. Calling the method locks the object. Returning from the call unlocks the implicit parameter object. Thus, only one thread at a time can execute synchronized methods on a particular object.

  2. When a thread executes a call to wait, it surrenders the object lock and enters a wait list for that object.

  3. To remove a thread from the wait list, some other thread must make a call to notifyAll or notify, on the same object.

The scheduling rules are undeniably complex, but it is actually quite simple to put them into practice. Just follow these five rules:

  1. If two or more threads modify an object, declare the methods that carry out the modifications as synchronized. Read-only methods that are affected by object modifications must also be synchronized.

  2. If a thread must wait for the state of an object to change, it should wait inside the object, not outside, by entering a synchronized method and calling wait.

  3. Don't spend any significant amount of time in a synchronized method. Most operations simply update a data structure and quickly return. If you can't complete a synchronized method immediately, call wait so that you give up the object lock while waiting.

  4. Whenever a method changes the state of an object, it should call notifyAll. That gives the waiting threads a chance to see if circumstances have changed.

  5. Remember that wait and notifyAll/notify are methods of the Object class, not the Thread class. Double-check that your calls to wait are matched up by a notification on the same object.

Synchronized Blocks

Occasionally, it is useful to lock an object and obtain exclusive access to it for just a few instructions without writing a new synchronized method. You use synchronized blocks to achieve this access. A synchronized block consists of a sequence of statements, enclosed in { . . . } and prefixed with synchronized (obj), where obj is the object to be locked. Here is an example of the syntax:

public void run()
{ 
  . . .
  synchronized (bank) // lock the bank object
  { 
   if (bank.getBalance(from) >= amount)
     bank.transfer(from, to, amount);
  }
  . . .
}

In this sample code segment, the synchronized block will run to completion before any other thread can call a synchronized method on the bank object.

Application programmers tend to avoid this feature. It is usually a better idea to take a step back, think about the mechanism on a higher level, come up with a class that describes it, and use synchronized methods of that class. System programmers who consider additional classes "high overhead" are more likely to use synchronized blocks.

Synchronized Static Methods

A singleton is a class with just one object. Singletons are commonly used for management objects that need to be globally unique, such as print spoolers, database connection managers, and so on.

Consider the typical implementation of a singleton.

public class Singleton
{
  public static Singleton getInstance()
  {
   if (instance == null)
     instance = new Singleton(. . .);
   return instance;
  }
  
  private Singleton(. . .) { . . . }
  . . .
  private static Singleton instance;
}

However, the getInstance method is not threadsafe. Suppose one thread calls getInstance and is preempted in the middle of the constructor, before the instance field has been set. Then another thread gains control and it calls getInstance. Since instance is still null, that thread constructs a second object. That's just what a singleton is supposed to prevent.

The remedy is simple: make the getInstance method synchronized:

  public static synchronized Singleton getInstance()
  {
   if (instance == null)
     instance = new Singleton(. . .);
   return instance;
  }

Now, the method runs to completion before another thread can call it.

If you paid close attention to the preceding sections, you may wonder how this works. When a thread calls a synchronized method, it acquires the lock of an object. But this method is static—what object does a thread lock when calling Singleton.getInstance()?

Calling a static method locks the class object Singleton.class. (Recall from Volume 1, Chapter 5, that there is a unique object of type Class that describes each class that the virtual machine has loaded.) Therefore, if one thread calls a synchronized static method of a class, all synchronized static methods of the class are blocked until the first call returns.

java.lang.Object

  • void notifyAll()
    unblocks the threads that called wait on this object. This method can only be called from within a synchronized method or block. The method throws an IllegalMonitorStateException if the current thread is not the owner of the object's lock.

  • void notify()
    unblocks one randomly selected thread among the threads that called wait on this object. This method can only be called from within a synchronized method or block. The method throws an IllegalMonitorStateException if the current thread is not the owner of the object's lock.

  • void wait()
    causes a thread to wait until it is notified. This method can only be called from within a synchronized method. It throws an IllegalMonitorStateException if the current thread is not the owner of the object's lock.

  • + Share This
  • 🔖 Save To Your Account