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.
- by declaring an instance method as synchronized, we can ensure that only one thread at a time can invoke
that method on the given object;
- by declaring a static method as synchronized, we ensure that only one thread at a time can invoke
that method;
- we can also use the synchronized keyword to ensure that only one thread at a time may enter specific
blocks of code that are associated with particular objects or classes (see below).
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 object— this 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:
- the thread executing that code must acquire the lock on the specified
object before executing the code;
- it must then release the lock afterwards (either because
the code finishes normally or abnormally, or— in some cases— because the thread is temporarily suspended).
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.