Wednesday, March 23, 2022

Synchronization in Java, Part 2: The synchronized keyword

Java Synchronization, Synchronized Keyword, Core Java, Oracle Java Tutorial and Materials, Oracle Java Career, Java Skills, Java Job

This second article in a series on thread synchronization addresses intrinsic locks, the synchronized keyword, synchronized blocks, and ad hoc locks.

The first article in this series about thread synchronization covered the fundamentals of race conditions, lock objects, condition objects, and the await, signal, and signalAll methods.

This second article addresses intrinsic locks, the synchronized keyword, synchronized blocks, ad hoc locks, and the concept of monitors.

The synchronized keyword

In the preceding article, you saw how to use lock and condition objects. To summarize, the following are the key points about locks and conditions:

◉ A lock protects sections of code, allowing only one thread to execute the code at a time.

◉ A lock manages threads that are trying to enter a protected code segment.

◉ A lock can have one or more associated condition objects.

◉ Each condition object manages threads that have entered a protected code section but that cannot proceed.

The Lock and Condition interfaces give you a high degree of control over locking. However, in most situations, you don’t need that control, because you can use a mechanism that is built into the Java language. Ever since version 1.0, every object in Java has an intrinsic lock. If a method is declared with the synchronized keyword, the object’s lock protects the entire method. That is, to call the method, a thread must acquire the intrinsic object lock. In other words,

public synchronized void method()

{

   method body

}

is the equivalent of

public void method()

{

   this.intrinsicLock.lock();

   try

   {

      method body

   }

   finally

   {

      this.intrinsicLock.unlock();

   }

}

For example, instead of using an explicit lock in the banking application introduced in the first article in this series, you could simply declare the transfer method of the Bank class as synchronized.

The intrinsic object lock has a single associated condition. The wait method adds a thread to the wait set, and the notifyAll and notify methods unblock waiting threads. In other words, calling wait or notifyAll is the equivalent of

intrinsicCondition.await();

intrinsicCondition.signalAll();

Note that the wait, notifyAll, and notify methods are final methods of the Object class. The Condition methods had to be named await, signalAll, and signal so that they didn’t conflict with those methods.

For example, you can implement the Bank class in Java like this.

class Bank

{

   private double[] accounts;

   public synchronized void transfer(int from, int to, int amount)

      throws InterruptedException

   {

      while (accounts[from] < amount)

         wait(); // wait on intrinsic object lock's single condition

      accounts[from] -= amount;

      accounts[to] += amount;

      notifyAll(); // notify all threads waiting on the condition

   }

   public synchronized double getTotalBalance()

   {

      ...

   }

}

As you can see, using the synchronized keyword yields code that is much more concise. Of course, to understand this code, you must know that each object has an intrinsic lock and that the lock has an intrinsic condition. The lock manages the threads that try to enter a synchronized method. The condition manages the threads that have called wait.

It is also legal to declare static methods as synchronized. If such a method is called, it acquires the intrinsic lock of the associated class object. For example, if the Bank class has a static synchronized method, the lock of the Bank.class object is locked when it is called. As a result, no other thread can call this or any other synchronized static method of the same class.

The intrinsic locks and conditions have some limitations. The following are among them:

◉ You cannot interrupt a thread that is trying to acquire a lock.

◉ You cannot specify a timeout when trying to acquire a lock.

◉ Having a single condition per lock can be inefficient.

What should you use in your code: lock and condition objects or synchronized methods? Here are my recommendations.

◉ It is best to not use Lock, Condition, or the synchronized keyword. In many situations, you can use one of the mechanisms of the java.util.concurrent package that do all the locking for you.

◉ If the synchronized keyword works for your situation, by all means, use it. You’ll write less code and have less room for error. Listing 1 shows the bank example from the first article implemented with synchronized methods.

◉ Use Lock and Condition if you really need the additional power that these constructs give you.

Listing 1. synch2/Bank.java

package synch2;

import java.util.*;

/**

 * A bank with a number of bank accounts that uses synchronization primitives.

 */

public class Bank

{

   private final double[] accounts;

   /**

    * Constructs the bank.

    * @param n the number of accounts

    * @param initialBalance the initial balance for each account

    */

   public Bank(int n, double initialBalance)

   {

      accounts = new double[n];

      Arrays.fill(accounts, initialBalance);

   }

   /**

    * Transfers money from one account to another.

    * @param from the account to transfer from

    * @param to the account to transfer to

    * @param amount the amount to transfer

    */

   public synchronized void transfer(int from, int to, double amount)

      throws InterruptedException

   {

      while (accounts[from] < amount)

         wait();

      System.out.print(Thread.currentThread());

      accounts[from] -= amount;

      System.out.printf(" %10.2f from %d to %d", amount, from, to);

      accounts[to] += amount;

      System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());

      notifyAll();

   }

   /**

     * Gets the sum of all account balances.

     * @return the total balance

     */

   public synchronized double getTotalBalance()

   {

      double sum = 0;

      for (double a : accounts) sum += a;

      return sum;

   }

   /**

   * Gets the number of accounts in the bank.

   * @return the number of accounts

   */

   public int size()

   {

      return accounts.length;

   }

}

Synchronized blocks

As you just saw, every Java object has a lock. A thread can acquire the lock by calling a synchronized method. There is a second mechanism for acquiring the lock, which is by entering a synchronized block. When a thread enters a block of the form

synchronized (obj) // this is the syntax for a synchronized block

{

   Critical section

}

then it acquires the lock for obj.

You will sometimes find ad hoc locks, such as

public class Bank

{

   private double[] accounts;

   private Lock lock = new Object();

   ...

   public void transfer(int from, int to, int amount)

   {

      synchronized (lock) // an ad-hoc lock

      {

         accounts[from] -= amount;

         accounts[to] += amount;

      }

      System.out.println(. . .);

   }

}

Here, the lock object is created only to use the lock that every Java object possesses.

With synchronized blocks, be careful about the lock object. For example, the following will not work:

private final String lock = "LOCK";

...

synchronized (lock) { . . . } // Don't lock on string literal!

If this occurs twice in the same program, the locks are the same object since string literals are shared. This can lead to a deadlock. Also, stay away from using primitive type wrappers as locks.

private final Integer lock = new Integer(42); // Don't lock on wrappers

The constructor call new Integer(0) is deprecated, and you don’t want a maintenance programmer to change the call to Integer.valueOf(42). If done twice with the same magic number, the lock will be accidentally shared.

If you need to modify a static field, lock on the specific class, not on the value returned by getClass().

synchronized (MyClass.class) { staticCounter++; } // OK

synchronized (getClass()) { staticCounter++; } // Don't

If the method containing this code is called from a subclass, getClass() returns a different Class object! You are no longer guaranteed mutual exclusion! In general, if you must use synchronized blocks, know your lock object! You must use the same lock for all protected access paths—and nobody else must use your lock.

Sometimes, programmers use the lock of an object to implement additional atomic operations. This practice is known as client-side locking. Consider, for example, the Vector class, which is a list whose methods are synchronized. Now suppose you stored the bank balances in a Vector<Double>. Here is a naïve implementation of a transfer method.

public void transfer(Vector<Double> accounts, int from, int to, int amount) // ERROR

{

   accounts.set(from, accounts.get(from) - amount);

   accounts.set(to, accounts.get(to) + amount);

   System.out.println(. . .);

}

The get and set methods of the Vector class are synchronized, but that doesn’t help. It is entirely possible for a thread to be pre-empted in the transfer method after the first call to get has been completed. Another thread may then store a different value in the same position. However, you can hijack the lock, as follows:

public void transfer(Vector<Double> accounts, int from, int to, int amount)

{

   synchronized (accounts)

   {

      accounts.set(from, accounts.get(from) - amount);

      accounts.set(to, accounts.get(to) + amount);

   }

System.out.println(. . .);

}

This approach works, but it is entirely dependent on the fact that the Vector class uses the intrinsic lock for all of its mutator methods. However, is this really a fact? The documentation of the Vector class makes no such promise. You have to carefully study the source code and hope that future versions do not introduce unsynchronized mutators. As you can see, client-side locking is very fragile and not generally recommended.

Note that the JVM has built-in support for synchronized methods. However, synchronized blocks are compiled into a lengthy sequence of bytecodes to manage the intrinsic lock.

The monitor concept

Locks and conditions are powerful tools for thread synchronization, but they are not very object-oriented. For many years, researchers have looked for ways to make multithreading safe without forcing programmers to think about explicit locks. One of the most successful solutions is the monitor concept that was pioneered by computer scientists Per Brinch Hansen and Tony Hoare in the 1970s. In the terminology of Java, a monitor has the following properties:

◉ A monitor is a class with only private fields.

◉ Each object of that class has an associated lock.

◉ All methods are locked by that lock. In other words, if a client calls obj.method(), the lock for obj is automatically acquired at the beginning of the method call and relinquished when the method returns. Since all fields are private, this arrangement ensures that no thread can access the fields while another thread manipulates them.

◉ The lock can have any number of associated conditions.

◉ Earlier versions of monitors had a single condition, with a rather elegant syntax. You can simply call await accounts[from] >= amount without using an explicit condition variable. However, research showed that indiscriminate retesting of conditions can be inefficient. This problem is solved with explicit condition variables, each managing a separate set of threads.

◉ The Java designers loosely adapted the monitor concept. Every object in Java has an intrinsic lock and an intrinsic condition. If a method is declared with the synchronized keyword, it acts like a monitor method. The condition variable is accessed by calling wait, notifyAll, or notify.

However, a Java object differs from a monitor in three important ways that can compromise thread safety.

◉ Fields are not required to be private.

◉ Methods are not required to be synchronized.

◉ The intrinsic lock is available to clients.

This disrespect for security enraged Dr. Hansen. In a scathing 1999 review of the multithreading primitives in Java, he wrote: “It is astounding to me that Java’s insecure parallelism is taken seriously by the programming community, a quarter of a century after the invention of monitors and Concurrent Pascal. It has no merit.” (“Java’s Insecure Parallelism,” ACM SIGPLAN Notices 34:38–45, April 1999.)

Source: oracle.com

Related Posts

0 comments:

Post a Comment