What will happen if two parts of a program try to use the same data at the same time? What if two threads try to change the same number at the same time? How can we make sure that only one thread uses the data at a time, so everything works correctly? This is where synchronization comes into. But what is the meaning of synchronization in Java? When should we use it to keep our program safe? Let’s find out how Java uses the synchronized keyword to control thread access and keep shared data safe.
In this article, we will further study what synchronization is in detail, with real-world examples of it.
Table of Contents:
What is Synchronization in Java?
Synchronization in Java is a method to control how multiple threads access shared resources. It prevents data inconsistency when 2 or more threads access the same data at the same time.
The synchronized keyword in Java is used to control access to a critical section by multiple threads. It allows only one thread to access a critical section at a time.
Note: A critical section is a piece of code that uses shared data and should only run in one thread at a time.
In the image above, we see four threads trying to use a synchronized method. At first, thread-1 and thread-4 enter the synchronized method, but thread-4 has to wait because thread-1 is in process. Once thread-1 finishes its work, then thread-4 gets a chance to enter the method and do its work.
Master Java Today - Accelerate Your Future
Enroll Now and Transform Your Future
Understanding the Problem without Synchronization
In Java, when multiple threads try to access and change the critical section at the same time, they may interfere in between with each other. This leads to a problem called a race condition.
For example, you have a balance of ₹1000 in your bank account. Two threads, t1 and t2, try to withdraw ₹700 from the account at the same time. Then, without synchronization:
- Both will see that the balance is ₹1000.
- And both will try to withdraw ₹700.
- And the result is that ₹1400 might be withdrawn from the account, due to which the balance will become negative or incorrect.
In the above example, the actual problem is that,
- No waiting: Threads t1 and t2 do not wait for each other.
- No order: You do not know which thread will run first.
- Shared data gets mixed: When both threads access the critical section at the same time.
Types of Synchronization
There are two main types of synchronization in Java. These are:
1. Process Synchronization
Process Synchronization deals with independent processes that run in parallel to each other which ensures that any two processes do not interfere with each other when accessing shared system resources.
It is handled by the Operating System, not directly by Java. Java programs do not deal with process synchronization directly, unless you are using tools like inter-process communication.
2. Thread Synchronization
Thread Synchronization is used to manage access to shared resources among multiple threads of the same process. Java provides built-in support for thread synchronization.
There are two types of thread synchronization:
1. Mutual exclusion: Mutual Exclusion ensures that only one thread can access the shared resource at any given time.
2. Cooperation or Inter-thread Communication: Cooperation allows threads to communicate and coordinate with each other.
Note: Java can use java.lang.Process, sockets, RMI, or external libraries for inter-process communication.
How to Use Synchronization in Java?
In Java, you can perform synchronization using the synchronized keyword in the following ways.
1. Synchronized Method
In this, you can declare an entire method as a synchronized method, which ensures that one thread at a time can execute this method on the same object.
Example:
Output:
Explanation:
In the above Java code, two threads increase the counter 1000 times. Because we used the synchronized keyword, they don’t interfere with each other, and the final count is 2000. The synchronized void increase() method ensures that only one thread can access the increase() method at a time. The join() method makes the main thread wait until t1 and t2 finish their execution.
2. Synchronized Block
In this, instead of locking the whole method, only a certain part of the code is synchronized that accesses shared data.
Example:
Output:
Explanation:
In the above Java code, two threads increase the counter 1000 times. Because we used the synchronized keyword, they don’t interfere with each other, and the final count is 2000. The synchronized(this) locks the current object so that only one thread at a time can run the count++ part, which ensures thread safety and avoids the race conditions.
3. Static Synchronized Method
When a method is declared both static and synchronized, it means only one thread can execute that method at a time, across all instances of the class. The lock is not on a particular object, but on the class itself.
Example:
Output:
Explanation:
In the above Java code, threads t1, t2, and t3 are created. The static synchronized locks are made on the class, not on an object. So, even if the threads come from different objects, only one thread at a time can enter the printMessage() method.
4. Thread Synchronization
This is the most common use of the synchronized keyword in Java, it makes sure that only one thread accesses the shared data at a time by acquiring a lock on the object or class.
Example:
Output:
Explanation:
In the above Java code, the add() method is made synchronized, so only one thread can run it at a time. Without using synchronization, both threads will update num at the same time, which will cause wrong results.
5. Process synchronization
Process synchronization means keeping two or more independent processes in sync when they are using shared resources like files, databases, or memory. In Java, this is usually done using Inter-Process Communication (IPC) or external mechanisms like files, sockets, or database locks.
Note: Java does not have built-in inter-process synchronization mechanisms
Example:
Before the execution of the code, the file “Intellipaat.txt” will be empty. After the execution of the code, the file will look as below.
Output:
Get 100% Hike!
Master Most in Demand Skills Now!
Inter-Thread Communication
Inter-thread communication means two or more threads are communicating with each other while sharing the data. It is also called Cooperative Synchronization, because the threads cooperate with each other instead of competing.
There are mainly 3 methods that are used inside the synchronized blocks or methods of the object class. These are
1. wait(): It tells the current thread to pause and release the lock. It can only be called from a synchronized context.
2. notify(): It wakes up one thread that is waiting.
3. notifyAll(): It wakes up all the waiting threads.
4. sleep(): It pauses the current thread for a specified time (in milliseconds), but does not release the lock.
Imagine there are two threads:
- Thread A is a Producer that creates data.
- Thread B is a Consumer that uses the data produced by A.
We don’t want that Thread B to consume the data before Thread A has produced it. So, Thread B should wait until Thread A finishes its job. After Thread A produces the data, it will notify Thread B to consume it.
This kind of communication between the threads is known as inter-thread communication.
For example, consider the following code.
Output:
The above Java code shows how one thread (producer) puts numbers into a buffer and another thread (consumer) takes them out. If the buffer is full, the producer waits. If the box is empty, the consumer waits. They do this turn by turn using wait() and notify(). Then, the synchronized keyword makes sure that only one thread uses the buffer at a time.
Alternatives to Synchronization in Java
Java has many other tools that do not use the synchronized keyword to manage thread safety in Java. These tools are more efficient. Some of them are:
1. Locks (ReentrantLock)
A Lock in Java is a tool that is used to control access to shared resources like variables, files, memory, etc., in a multithreaded program. It is part of the java.util.concurrent.locks package.
It is like using a door lock; only one person can go inside a room, and others have to wait.
ReentrantLock is a part of the java.util.concurrent.locks package, which can acquire a lock on more than once without getting blocked. Unlike the synchronized keyword, it gives explicit control over the process of locking and unlocking. It is mainly used when you need better control over locking logic timeouts.
Example:
Output:
After the execution of the code, the output.txt will look as below.
Explanation:
In the above Java code, we are using ReentrantLock to control the access of a thread to the critical section. The lock.lock() method is used to call a thread to enter the section, and lock.unlock() is used to allow other threads to enter once the first entered is done.
2. Atomic Variables
Atomic Variable is present in the java.util.concurrent.atomic package, which has classes like AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference, etc., which use the CPU-level atomic operations, like CAS (Compare And Swap) for lock-free synchronization.
It is very fast and efficient for small blocks of code, like counters. They are mainly used when you want to avoid locking completely.
Example:
Output:
Explanation:
In the above Java code, AtomicInteger is used to increment a number safely in a multithreaded environment without using any other lock.
3. Executor Service
Executor Service is a part of the java.util.concurrent package, which has a thread pool that can manage and schedule threads efficiently. It is easy, as it removes the need to create and manage each thread manually. But it does not remove the need for synchronization when accessing the shared resources.
It is used by the application where a large number of threads are used, as it can handle a large number of threads easily. It also promotes the parallel task execution.
Example:
Output:
Explanation:
In the above Java code, we are using ExecutorService to run a task using a thread pool, which will help the execution of a task in an organized way.
4. Semaphores
A semaphore is a value that is used to restrict the number of threads that can access a resource at a time. It is a part of the java.util.concurrent package.
A semaphore with a value of 1 is called a binary semaphore.
For example, 3 threads can access a pool of 3 printers, but if the 4th comes, it must wait.
Example:
Output:
Explanation:
In the above Java code, a Semaphore is used to allow only one thread to access the critical section at a time.
5. Read-Write Locks (ReadWriteLock)
Read-Write Lock is present in the java.util.concurrent.locks.ReadWriteLock class, and it uses the readLock() and writeLock() methods internally to allow multiple threads to read at the same time. But allow only one thread to write, and while the process of writing, other processes, like reading or writing, cannot be performed.
Example: It is used in memory caches, real-time analytics data.
Example:
Output:
Explanation:
In the above Java code, ReadWriteLock is used to allow multiple threads to read shared data at the same time.
6. Volatile Keyword
Volatile Keyword in Java is used to make changes to a variable that is visible to all the threads. Using this keyword, the latest value will be used from the main memory. It is used for simple flag variables.
Example:
Output:
Explanation:
In the above Java code, volatile boolean running = true; ensures that changes made by one thread are visible to other threads immediately.
Advantages of Synchronization
1. It is safe from errors, as only one thread can work on shared data at a time.
2. It keeps your data right, even if more than one thread is working together.
3. It is simple to use, as Java provides the synchronized keyword, which makes synchronization easy.
4. No extra tool is needed, as you can manage synchronization directly in Java code, without using any external libraries.
Disadvantages of Synchronization
1. It makes the program slow, as only one thread can access the block at a time.
2. Sometimes it creates a problem of deadlock, if two threads are waiting for each other to release locks.
3. It makes it hard to find the bugs in the program, because errors related to synchronization are difficult to debug.
4. You need to write the code carefully because a small mistake in using synchronization, like missing a lock, can cause serious problems like a race condition.
Real-World Examples of Synchronization
1. In online banking apps, two people might try to withdraw money at the same time. Without any control, both could withdraw more than the actual balance. This causes wrong account updates.
2. In offices, many people may send files to the same printer simultaneously. If all the pages are printed at once, pages can get mixed up, which can waste paper and confuse the users. Synchronization helps handle print jobs one by one.
3. On YouTube or Instagram, many people can like or view something at the same time. If it happens together, some likes or views might not be added. This can show the wrong total number.
4. In food delivery apps, one thread may add items while another places the order. If both of them act at once, the cart might show the wrong items. This can lead to wrong or missing orders. Synchronization keeps the cart updated in order.
Best Practices of Using Synchronization
1. Use synchronization only when it is needed, i.e., when shared data is accessed by multiple threads, because it slows down performance.
2. Synchronize only the critical section, not the whole method, because it avoids deadlocks and improves performance.
3. Use a synchronized Block instead of a synchronized method because it gives more flexibility and better performance
4. Add comments near the synchronized blocks to explain why it is used.
5. Try to reduce the use of shared variables between the threads, which will reduce the chances of a race condition.
Unlock Your Future in Java
Start Your Java Journey for Free Today
Conclusion
From the above article, we conclude that synchronization in Java helps us make sure that only one thread uses shared data at a time, which prevents problems like wrong results or data getting mixed up. We can use synchronized, wait(), notify(), and other tools for this. These tools help threads work in the right order and avoid errors. But using too much synchronization can slow things down. So, we should use it only when really needed.
If you want to learn more about this topic, you can refer to our Java course.
What is Synchronization in Java – FAQs
Q1. What is synchronization in Java?
Synchronization in Java means only one thread can do a task at a time to avoid conflicts.
Q2. What is a synchronized class?
synchronized(X.class) is used to make sure only one thread can run that block of code at a time.
Q3. What causes deadlocks?
A deadlock occurs when two or more tasks permanently block each other by each task having a lock on a resource that the other tasks are trying to lock
Q4. What is the difference between join and synchronization in Java?
The easiest way to synchronize a static variable is by using Java’s synchronized keyword, which locks the whole class since the variable is static.
Q5. How to avoid deadlock in Java?
To avoid deadlocks in Java, you can use things like thread joins.