Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

Sponsored Links

Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs

Java 101: Understanding Java threads, Part 2: Thread synchronization

Use synchronization to serialize thread access to critical code sections

  • Print
  • Feedback

Last month I showed you how easy it is to create thread objects, start threads that associate with those objects by calling Thread's start() method, and perform simple thread operations by calling other Thread methods such as the three overloaded join() methods. This month we're taking on multithreaded Java programs, however, which are more complex.

Multithreaded programs often function erratically or produce erroneous values due to the lack of thread synchronization. Synchronization is the act of serializing (or ordering one at a time) thread access to those code sequences that let multiple threads manipulate class and instance field variables, and other shared resources. I call those code sequences critical code sections.. This month's column is all about using synchronization to serialize thread access to critical code sections in your programs.

I begin with an example that illustrates why some multithreaded programs must use synchronization. I next explore Java's synchronization mechanism in terms of monitors and locks, and the synchronized keyword. Because incorrectly using the synchronization mechanism negates its benefits, I conclude by investigating two problems that result from such misuse.

Tip: Unlike class and instance field variables, threads cannot share local variables and parameters. The reason: Local variables and parameters allocate on a thread's method-call stack. As a result, each thread receives its own copy of those variables. In contrast, threads can share class fields and instance fields because those variables do not allocate on a thread's method-call stack. Instead, they allocate in shared heap memory—as part of classes (class fields) or objects (instance fields).

The need for synchronization

Why do we need synchronization? For an answer, consider this example: You write a Java program that uses a pair of threads to simulate withdrawal/deposit of financial transactions. In that program, one thread performs deposits while the other performs withdrawals. Each thread manipulates a pair of shared variables, class and instance field variables, that identifies the financial transaction's name and amount. For a correct financial transaction, each thread must finish assigning values to the name and amount variables (and print those values, to simulate saving the transaction) before the other thread starts assigning values to name and amount (and also printing those values). After some work, you end up with source code that resembles Listing 1:

Listing 1. NeedForSynchronizationDemo.java

// NeedForSynchronizationDemo.java
class NeedForSynchronizationDemo
{
   public static void main (String [] args)
   {
      FinTrans ft = new FinTrans ();
      TransThread tt1 = new TransThread (ft, "Deposit Thread");
      TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
      tt1.start ();
      tt2.start ();
   }
}
class FinTrans
{
   public static String transName;
   public static double amount;
}
class TransThread extends Thread
{
   private FinTrans ft;
   TransThread (FinTrans ft, String name)
   {
      super (name); // Save thread's name
      this.ft = ft; // Save reference to financial transaction object
   }
   public void run ()
   {
      for (int i = 0; i < 100; i++)
      {
           if (getName ().equals ("Deposit Thread"))
           {
               // Start of deposit thread's critical code section
               ft.transName = "Deposit";
               try
               {
                  Thread.sleep ((int) (Math.random () * 1000));
               }
               catch (InterruptedException e)
               {
               }
               ft.amount = 2000.0;
               System.out.println (ft.transName + " " + ft.amount);
               // End of deposit thread's critical code section
           }
           else
           {
               // Start of withdrawal thread's critical code section
               ft.transName = "Withdrawal";
               try
               {
                  Thread.sleep ((int) (Math.random () * 1000));
               }
               catch (InterruptedException e)
               {
               }
               ft.amount = 250.0;
               System.out.println (ft.transName + " " + ft.amount);
               // End of withdrawal thread's critical code section
           }
      }
   }
}

NeedForSynchronizationDemo's source code has two critical code sections: one accessible to the deposit thread, and the other accessible to the withdrawal thread. Within the deposit thread's critical code section, that thread assigns the Deposit String object's reference to shared variable transName and assigns 2000.0 to shared variable amount. Similarly, within the withdrawal thread's critical code section, that thread assigns the Withdrawal String object's reference to transName and assigns 250.0 to amount. Following each thread's assignments, those variables' contents print. When you run NeedForSynchronizationDemo, you might expect output similar to a list of interspersed Withdrawal 250.0 and Deposit 2000.0 lines. Instead, you receive output resembling the following:

  • Print
  • Feedback

Resources
  • Learn more about Java: See the complete listing for Jeff Friesen's Java 101 series -- archived on JavaWorld.
  • Also see the Java Tips series: More than five years of compiled tips from JavaWorld's expert readers.