The Java synchronized keyword

The Java synchronized keyword is fundamental for programs that use multiple threads. Ordinarily, if you have multiple threads all accessing the same data concurrently, there is risk of a so-called race condition. For example, one thread could see data in an inconsistent or out-of-date state because another thread is in the process of updating it. If the two threads are attempting to perform competing operations on the data, they could even leave it in a corrupt state.

In Java, synchronized provides a simple way to solve many such problems, associated with multiple threads attempting to access shared data or perform competing operations at the same time.

For example, if we have two methods or blocks of code that are synchronized on a particular list obejct, we can ensure that two threads don't attempt to add or remove items from the list at the same time and leave it in a corrupt state. Because of its simplicity, using synchronized also requires care and attention. For example, if a synchronized block of code is unable to complete, then any other thread that attempts to enter that block of code to work on the same object will also get "stuck" before it even has the opportunity to execute the cpde in question. In later sections of this tutorial, we will explore alternatives such as as explicit timed locks. Nonetheless, synchronized remains a fundamental part of multithreaded programming in Java.

Example: using a synchronized block to ensure consistent updates to a counter

As we described above, the essential idea is that if a method or block is marked as synchronized in Java, then this tells the JVM to "only let one thread in here at a time".

As an example of when synchronization is necessary, imagine that we have a counter that needs to be incremented at unpredictable points in time by different threads (for example, it might be counting visits to a web page from different users, accesses to cache objects by different background processes, etc). Ordinarily, there would be a risk that two threads could simultaneously try and update the counter at the same time, and in so doing currpt the value of the counter (or at least, miss an increment, because one thread reads the present value unaware that another thread is just about to write a new, incremented value). If we mark the code that accesses the counter as synchronized, then we can avoid this risk. One way of writing the code would be as follows:

public class Counter {
  private int count = 0;
  
  public int getCount() {
    synchronized (this) {
      return count;
    }
  }
  
  public void increment() {
    synchronized (this) {
      count++;
    }
  }
}

Here, there are two blocks of code that access the counter. The simplest of these is the get method, which simply reads the value. The increment method superficially contains a single line of code. But remember that the increment operation must read the current value, add one to it, and the write the new value back to memory: in other words, there are actually three sub-operations that we want to execute without interruptions from other threads (put another way, we want the increment operation to be atomic).

We prefix the two blocks of code with the synchronized keyword, but notice crucially that we also mark them as being synchronized on a particular objectthis in our example. That means that if we have multiple Counter objects, then different threads can update those different counters concurrently. But two threads cannot run the synchronized blocks on the same instance of Counter simultaneously.

How synchronized works: synchronization locks

As mentioned, any use of synchronized marks a block or method as synchronizing on a specific object. Inside the JVM, every Java object created, including every Class loaded, has an associated lock or monitor. Putting code inside a synchronized means that:

Between acquiring the lock and releasing it, a thread is said to "own" the lock. At the point of Thread A wanting to acquire the lock, if Thread B already owns the it, then Thread A must wait for Thread B to release it.

So this is specifically why simultaneous calls to increment() and getCount() will always behave as expected, and a read operation could not "step in" while another thread was in the middle of incrementing the count.

Synchronized methods

If we want to synchronize on this for all of the code inside a method, then we can simply mark that method as synchronized:

public class Counter {
  public synchronized int getCount() {
    return count;
  }
  
  public synchronized void increment() {
    count++;
  }
}

We mentioned above that a static method can also be marked as synchronized. In this case, the object being synchronized on is that class itself, and so in effect, only one thread at a time may execute the static method in question.

The advantage of marking entire method as synchronized is that the code is a little less verbose. (There is a marginal efficiency in terms of the class size and bytecode decoding, but this is less of a concern on today's JVMs.)

Synchronization and data visibility

Synchronizing also performs another important– and often overlooked– function. Unless told otherwise— using a synchronized block or via the Java volatile keyword&mdash, threads may work on locally cached copies of variables such as count, updating the "main" copy when it suits them. For the reasons discussed in our overview of processor architecture, they may also re-order reads and writes, meaning that updating a variable may not mean that it is updated when otherwise expected. However, on entry to and exit from blocks synchronizeded on a particular object, the entering/exiting thread also effectively synchronizes copies of all variables with main memory1.

The next page deals with some of the gorier details of this synchronization of variables with main memory. Or you may prefer to skip it and go on to the following page which detals with methods declared as synchronized.

1. We'll discuss in a moment what that actually means, but fundamentally, locally cached copes if variables must be 'flushed' to main memory on exit, and accesses to variables accessed inside the synchronized block cannot be re-ordered to occur outisde the synchronized block.


If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.

Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.