Synchronization in Java: // Synchronized Code Block
Synchronization in Java: // Synchronized Code Block
Thread represents an independent path of execution within a program. When multiple threads
access shared resources concurrently, problems may arise due to unpredictable interleaving of
operations. Consider a scenario where two threads increment a shared variable concurrently:
class Counter {
private int count = 0;
public void increment() {
count++;
}
}
If two threads execute increment() simultaneously, they might read the current value of count,
increment it, and write it back concurrently. This can result in lost updates or incorrect final values
due to race conditions.
Introducing Synchronization
Synchronization in Java tackles these problems through the capacity of a single thread to have
exclusive access to either a synchronized block of code or a synchronized method associated with an
object in question at a time. There are two primary mechanisms for synchronization in Java:
synchronized methods and synchronized blocks.
Synchronized Methods
In Java, you can declare entire methods as synchronized which prevent multiple threads from
accessing the method simultaneously. With this, synchronization becomes a simpler process because
the mechanism is applied to all invocations of the synchronized method automatically.
class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
With this modification, concurrent calls to increment() or getCount() will be synchronized,
preventing race conditions.
Synchronized Blocks
Synchronized block provides exclusive access to shared resources, and only one thread is allowed to
execute it in the same time frame. It's structured as follows:
synchronized (object) {
// Synchronized code block
}
This monitor object or lock is the subject. While only one thread can be holding a lock on a monitor
object at one instance. Other threads that want to go into the synchronized blocks with this object
must wait till the lock becomes available.
Intrinsic Locks and Synchronization
In Java, every object automatically has an intrinsic lock (or monitor lock) associated to it. The
moment a thread enters the synchronized block or method, it gets the lock for the object, and then
no other thread is allowed to enter the synchronized block or method for that object until the lock is
released.
Deadlocks
On the one hand, synchronization ensures that race condition is ruled out, but on the other hand, it
may lead to deadlocks if not used critically. Stalemates could result as two or more threads are
ceaseless when they are waiting for resources from each other. Avoid deadlocks by ordering the
locks and releasing them in an opposite sequence of both.
Locking Granularity
The best locking granularity has to be selected and has to avoid contention and thus be good for
performance. Leaning too broadly can reduce concurrency, while leaning too finely can lead to
overhead increase. Identification of the only section of the code which needs to be the only one
having an exclusive access to the shared resources and synchronization of that section alone.
Concurrent Collections
Java contains a thread-safe versions of the common collections classes that found in the
java.util.concurrent package, including the ConcurrentHashMap and ConcurrentLinkedQueue. These
classes provide internal synchronization mechanisms that guarantee the thread safety without giving
up the synchronization control to the user.
Volatile Keyword
Furthermore, volatile keyword may be used to maintain the visibility of changes made to the
variables among the threads. For variables declared as volatile, their value will always be read
directly from memory and the writes to them will be visible to all the other threads immediately.
However, volatile does not ensure as such for complex instructions such as incrementing.
Atomic Classes
Java gives atomic classes in the java.util.concurrent.atomic package including Atomic Integer and
Atomic Long, which offer atomic operations of variables without using explicit synchronization.
These kind of classes use hardware's low level atomic operations to make thread safety.
1. Process Synchronization
2. Thread Synchronization
Here, we will discuss only thread synchronization.
Thread Synchronization
There are two types of thread synchronization in Java: mutual exclusive and inter-thread
communication.
1. Mutual Exclusive
1. Synchronized method.
2. Synchronized block.
3. Static synchronization.
2. Cooperation (Inter-thread communication in Java)
Mutual Exclusive
Mutual Exclusive helps keep threads from interfering with one another while sharing data. It can be
achieved by using the following three ways:
Synchronization is built around an internal entity known as the lock or monitor. Every object has a
lock associated with it. By convention, a thread that needs consistent access to an object's fields has
to acquire the object's lock before accessing them, and then release the lock when it's done with
them.
In this example, there is no synchronization, so output is inconsistent. Let's see the example:
Example
class Table {
// Method to print the table, not synchronized
void printTable(int n) {
for(int i = 1; i <= 5; i++) {
// Print the multiplication result
System.out.println(n * i);
try {
// Pause execution for 400 milliseconds
Thread.sleep(400);
} catch(Exception e) {
// Handle any exceptions
System.out.println(e);
}
}
}
}
class MyThread1 extends Thread {
Table t;
// Constructor to initialize Table object
MyThread1(Table t) {
this.t = t;
}
// Run method to execute thread
public void run() {
// Call printTable method with argument 5
t.printTable(5);
}
}
class MyThread2 extends Thread {
Table t;
// Constructor to initialize Table object
MyThread2(Table t) {
this.t = t;
}
// Run method to execute thread
public void run() {
// Call printTable method with argument 100
t.printTable(100);
}
}
public class Main {
public static void main(String args[]) {
// Create a Table object
Table obj = new Table();
// Create MyThread1 and MyThread2 objects with the same Table object
MyThread1 t1 = new MyThread1(obj);
MyThread2 t2 = new MyThread2(obj);
// Start both threads
t1.start();
t2.start();
}
}
Output:
100
10
200
15
300
20
400
25
500
When a thread invokes a synchronized method, it automatically acquires the lock for that object and
releases it when the thread completes its task.
Example
class Table {
// Synchronized method to print the table
synchronized void printTable(int n) {
for(int i = 1; i <= 5; i++) {
// Print the multiplication result
System.out.println(n * i);
try {
// Pause execution for 400 milliseconds
Thread.sleep(400);
} catch(Exception e) {
// Handle any exceptions
System.out.println(e);
}
}
}
}
class MyThread1 extends Thread {
Table t;
// Constructor to initialize Table object
MyThread1(Table t) {
this.t = t;
}
// Run method to execute thread
public void run() {
// Call synchronized method printTable with argument 5
t.printTable(5);
}
}
class MyThread2 extends Thread {
Table t;
// Constructor to initialize Table object
MyThread2(Table t) {
this.t = t;
}
// Run method to execute thread
public void run() {
// Call synchronized method printTable with argument 100
t.printTable(100);
}
}
public class Main {
public static void main(String args[]) {
// Create a Table object
Table obj = new Table();
// Create MyThread1 and MyThread2 objects with the same Table object
MyThread1 t1 = new MyThread1(obj);
MyThread2 t2 = new MyThread2(obj);
// Start both threads
t1.start();
t2.start();
}
}
Compile and Run
Output:
10
15
20
25
100
200
300
400
500
Example of Synchronized Method by Using Anonymous Class
In this program, we have created the two threads by using the anonymous class, so less coding is
required.
Example
10
15
20
25
100
200
300
400
500
Thread Safety: Synchronization ensures that shared resources are accessed by only one thread at a
time, preventing race conditions and maintaining data integrity. This makes it easier to write multi-
threaded programs without worrying about unpredictable behaviors caused by concurrent access.
Consistency: By using synchronization, you can ensure that concurrent operations on shared
resources are performed in a consistent and predictable manner. This is crucial for maintaining the
correctness of the program's logic and preventing unexpected outcomes.
Data Visibility: Synchronization mechanisms such as locks and memory barriers guarantee that
changes made by one thread to shared variables are visible to other threads. This ensures that
threads always see the most up-to-date values of shared data, preventing inconsistencies due to
stale data.
Compatibility with Legacy Code: Synchronization is a fundamental concept in Java concurrency that
has been widely adopted in libraries, frameworks, and existing codebases. By leveraging
synchronization, developers can ensure compatibility with legacy code and libraries that rely on
thread-safe programming practices.
Performance Overhead: Synchronization involves acquiring and releasing locks, which introduces
overhead due to context switching and contention for shared resources. This can degrade
performance, especially in highly concurrent applications where many threads contend for the same
locks.
Potential for Deadlocks: Incorrect use of synchronization primitives can lead to deadlocks, where
threads are blocked indefinitely, waiting for each other to release locks. Deadlocks are challenging to
debug and can cause the entire application to hang, impacting its availability and reliability.
Complexity and Maintenance: Synchronized code can be more complex and error-prone than
single-threaded or lock-free alternatives. Managing locks, ensuring proper lock acquisition and
release, and avoiding deadlocks require careful design and testing, increasing the complexity and
maintenance burden of the codebase.
Potential for Livelocks: Livelocks are similar to deadlocks but occur when threads continuously
change their states in response to each other, preventing any of them from making progress.
Livelocks can occur when threads repeatedly acquire and release locks in a specific pattern without
making progress toward resolving the contention.
Potential for Performance Degradation with I/O Operations: Synchronization may lead to
performance degradation when threads block on I/O operations while holding locks. This can cause
other threads waiting for the same locks to be blocked unnecessarily, reducing overall throughput
and responsiveness.