Untangling Java concurrency

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

Use synchronization to serialize thread access to critical code sections

1 2 3 Page 2
Page 2 of 3

From a source code perspective, a thread attempts to enter the critical code section that the synchronized statement guards. Internally, the JVM checks if some other thread holds the lock associated with the "sync object" object. (Yes, "sync object" is an object. You will understand why in a future article.) If no other thread holds the lock, the JVM gives the lock to the requesting thread and allows that thread to enter the critical code section between the brace characters. However, if some other thread holds the lock, the JVM forces the requesting thread to wait in a private waiting area until the thread currently within the critical code section finishes executing the final statement and transitions past the final brace character.

You can use the synchronized statement to eliminate NeedForSynchronizationDemo's race condition. To see how, examine Listing 2:

Listing 2. SynchronizationDemo1.java

// SynchronizationDemo1.java
class SynchronizationDemo1
{
   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"))
           {
               synchronized (ft)
               {
                  ft.transName = "Deposit";
                  try
                  {
                     Thread.sleep ((int) (Math.random () * 1000));
                  }
                  catch (InterruptedException e)
                  {
                  }
                  ft.amount = 2000.0;
                  System.out.println (ft.transName + " " + ft.amount);
               }
           }
           else
           {
               synchronized (ft)
               {
                  ft.transName = "Withdrawal";
                  try
                  {
                     Thread.sleep ((int) (Math.random () * 1000));
                  }
                  catch (InterruptedException e)
                  {
                  }
                  ft.amount = 250.0;
                  System.out.println (ft.transName + " " + ft.amount);
               }
           }
      }
   }
}

Look carefully at SynchronizationDemo1; the run() method contains two critical code sections sandwiched between synchronized (ft) { and }. Each of the deposit and withdrawal threads must acquire the lock that associates with the FinTrans object that ft references before either thread can enter its critical code section. If, for example, the deposit thread is in its critical code section and the withdrawal thread wants to enter its own critical code section, the withdrawal thread attempts to acquire the lock. Because the deposit thread holds that lock while it executes within its critical code section, the JVM forces the withdrawal thread to wait until the deposit thread executes that critical code section and releases the lock. (When execution leaves the critical code section, the lock releases automatically.)

Tip: When you need to determine if a thread holds a given object's associated lock, call Thread's static boolean holdsLock(Object o) method. That method returns a Boolean true value if the thread calling that method holds the lock associated with the object that o references; otherwise, false returns. For example, if you were to place System.out.println (Thread.holdsLock (ft)); at the end of SynchronizationDemo1's main() method, holdsLock() would return false. False would return because the main thread executing the main() method does not use the synchronization mechanism to acquire any lock. However, if you were to place System.out.println (Thread.holdsLock (ft)); in either of run()'s synchronized (ft) statements, holdsLock() would return true because either the deposit thread or the withdrawal thread had to acquire the lock associated with the FinTrans object that ft references before that thread could enter its critical code section.

Synchronized methods

You can employ synchronized statements throughout your program's source code. However, you might run into situations where excessive use of such statements leads to inefficient code. For example, suppose your program contains a method with two successive synchronized statements that each attempt to acquire the same common object's associated lock. Because acquiring and releasing the object's lock eats up time, repeated calls (in a loop) to that method can degrade the program's performance. Each time a call is made to that method, it must acquire and release two locks. The greater the number of lock acquisitions and releases, the more time the program spends acquiring and releasing the locks. To get around that problem, you might consider using a synchronized method.

A synchronized method is either an instance or class method whose header includes the synchronized keyword. For example: synchronized void print (String s). When you synchronize an entire instance method, a thread must acquire the lock associated with the object on which the method call occurs. For example, given an ft.update("Deposit", 2000.0); instance method call, and assuming that update() is synchronized, a thread must acquire the lock associated with the object that ft references. To see a synchronized method version of the SynchronizationDemo1 source code, check out Listing 3:

Listing 3. SynchronizationDemo2.java

// SynchronizationDemo2.java
class SynchronizationDemo2
{
   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
{
   private String transName;
   private double amount;
   synchronized void update (String transName, double amount)
   {
      this.transName = transName;
      this.amount = amount;
      System.out.println (this.transName + " " + this.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"))
             ft.update ("Deposit", 2000.0);
          else
             ft.update ("Withdrawal", 250.0);
   }
}

Though slightly more compact than Listing 2, Listing 3 accomplishes the same purpose. If the deposit thread calls the update() method, the JVM checks to see if the withdrawal thread has acquired the lock associated with the object that ft references. If so, the deposit thread waits. Otherwise, that thread enters the critical code section.

SynchronizationDemo2 demonstrates a synchronized instance method. However, you can also synchronize class methods. For example, the java.util.Calendar class declares a public static synchronized Locale [] getAvailableLocales() method. Because class methods have no concept of a this reference, from where does the class method acquire its lock? Class methods acquire their locks from class objects—each loaded class associates with a Class object, from which the loaded class's class methods obtain their locks. I refer to such locks as class locks.

Caution: Don't synchronize a thread object's run() method because situations arise where multiple threads need to execute run(). Because those threads attempt to synchronize on the same object, only one thread at a time can execute run(). As a result, each thread must wait for the previous thread to terminate before it can access run().

Some programs intermix synchronized instance methods and synchronized class methods. To help you understand what happens in programs where synchronized class methods call synchronized instance methods and vice-versa (via object references), keep the following two points in mind:

  1. Object locks and class locks do not relate to each other. They are different entities. You acquire and release each lock independently. A synchronized instance method calling a synchronized class method acquires both locks. First, the synchronized instance method acquires its object's object lock. Second, that method acquires the synchronized class method's class lock.
  2. Synchronized class methods can call an object's synchronized methods or use the object to lock a synchronized block. In that scenario, a thread initially acquires the synchronized class method's class lock and subsequently acquires the object's object lock. Hence, a synchronized class method calling a synchronized instance method also acquires two locks.

The following code fragment illustrates the second point:

class LockTypes
{
   // Object lock acquired just before execution passes into instanceMethod()
   synchronized void instanceMethod ()
   {
      // Object lock released as thread exits instanceMethod()
   }
   // Class lock acquired just before execution passes into classMethod()
   synchronized static void classMethod (LockTypes lt)
   {
      lt.instanceMethod ();
      // Object lock acquired just before critical code section executes
 
      synchronized (lt)
      {
         // Critical code section
         // Object lock released as thread exits critical code section
      }
      // Class lock released as thread exits classMethod() 
   }
}

The code fragment demonstrates synchronized class method classMethod() calling synchronized instance method instanceMethod(). By reading the comments, you see that classMethod() first acquires its class lock and then acquires the object lock associated with the LockTypes object that lt references.

Two problems with the synchronization mechanism

Despite its simplicity, developers often misuse Java's synchronization mechanism, which causes problems ranging from no synchronization to deadlock. This section examines these problems and provides a pair of recommendations for avoiding them.

Note: A third problem related to the synchronization mechanism is the time cost associated with lock acquisition and release. In other words, it takes time for a thread to acquire or release a lock. When acquiring/releasing a lock in a loop, individual time costs add up, which can degrade performance. For older JVMs, the lock-acquisition time cost often results in significant performance penalties. Fortunately, Sun Microsystems' HotSpot JVM (which ships with Sun's Java 2 Platform, Standard Edition (J2SE) SDK) offers fast lock acquisition and release, greatly reducing this problem's impact.

No synchronization

After a thread voluntarily or involuntarily (through an exception) exits a critical code section, it releases a lock so another thread can gain entry. Suppose two threads want to enter the same critical code section. To prevent both threads from entering that critical code section simultaneously, each thread must attempt to acquire the same lock. If each thread attempts to acquire a different lock and succeeds, both threads enter the critical code section; neither thread has to wait for the other thread to release its lock because the other thread acquires a different lock. The end result: no synchronization, as demonstrated in Listing 4:

Listing 4. NoSynchronizationDemo.java

// NoSynchronizationDemo.java
class NoSynchronizationDemo
{
   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"))
           {
               synchronized (this)
               {
                  ft.transName = "Deposit";
                  try
                  {
                     Thread.sleep ((int) (Math.random () * 1000));
                  }
                  catch (InterruptedException e)
                  {
                  }
                  ft.amount = 2000.0;
                  System.out.println (ft.transName + " " + ft.amount);
               }
           }
           else
           {
               synchronized (this)
               {
                  ft.transName = "Withdrawal";
                  try
                  {
                     Thread.sleep ((int) (Math.random () * 1000));
                  }
                  catch (InterruptedException e)
                  {
                  }
                  ft.amount = 250.0;
                  System.out.println (ft.transName + " " + ft.amount);
               }
           }
      }
   }
}

When you run NoSynchronizationDemo, you will see output resembling the following excerpt:

Withdrawal 250.0
Withdrawal 2000.0
Deposit 250.0
Withdrawal 2000.0
Deposit 2000.0
1 2 3 Page 2
Page 2 of 3