Thread Synchronization in Java: synchronized Keyword Explained
-
Last Updated: September 14, 2026
-
By: javahandson
-
Series

Learn synchronization in Java the easy way. See how the synchronized keyword stops race conditions using methods, blocks, monitors, and reentrant locks.
Two people can try to take money out of the same bank account at the same time. They both check the balance and see enough money available. Then, they each withdraw cash, which can cause the account to go into a negative balance. This situation shows why synchronization in Java is important. It helps prevent these issues and ensures that money transactions happen correctly.
When multiple threads share the same data, problems can arise quickly. One thread reads a value while another thread changes it just a moment later. Now, the first thread is using outdated data, and your program may produce strange results.
Java gives you a simple tool for this problem. It is the synchronized keyword. You use it to make sure only one thread runs a certain piece of code at a time. Everyone else waits their turn.
In this guide, we will keep things plain and hands-on. You will see a small race condition first, so the problem feels real. Then we will fix it, step by step, with synchronized methods and blocks.
Here is the path we will walk:
You only need to know what a thread is and how to write a basic Java class. If you have run a program with more than one thread, you are ready.

A race condition occurs when two threads access the same data at the same time, and the result depends on which one acts first. This leads to different outcomes with each run, making it very difficult to debug.
This is an example of a simple counter that increments by one with each operation. When a single thread performs this addition ten thousand times, the expected result is ten thousand. However, when two threads undertake the same task simultaneously, the anticipated outcome should be twenty thousand. It’s worth observing how the actual results may vary in a multi-threaded environment.
class Counter {
int count = 0;
void increment() {
count = count + 1; // read, add, write
}
}
Counter c = new Counter();
Runnable job = () -> {
for (int i = 0; i < 10000; i++) {
c.increment();
}
};
Thread t1 = new Thread(job);
Thread t2 = new Thread(job);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(c.count); // often less than 20000Run this a few times. You will rarely see twenty thousand. You might see 18734 one time and 19012 the next. So where does the count go?
The line count = count + 1 looks like one action. It is not. Under the hood, it is three small steps:
Now picture two threads hitting these steps together. Thread A reads 5. Before A writes back, thread B also reads 5. Both add one and both write 6. Two increments happened, yet the count moved up by only one. One update just vanished.
A race condition happens when multiple threads try to run code at the same time, which can result in one thread’s work being lost. To prevent this, we need to ensure that only one thread can run a specific piece of code at a time. This control is called synchronization. While there is a lot of theory about race conditions, here we will focus on how to fix them.
One more thing worth noting. The bug does not show up every time. On a lucky run, the timing lines up and you get the right answer. On an unlucky run, two threads collide and you lose a count. That flaky, on-and-off nature is what makes these bugs so painful. They hide during testing and pop up in production.
You might think that running a few tests would catch this issue, but often it doesn’t. The collision depends on precise timing between threads, which can change with machine load, CPU count, and even luck. A test might pass ten times, only to fail unexpectedly on the eleventh.
So the fix cannot be hope. The fix has to be a real lock that removes the race entirely. Let us build that now.
The easiest fix is the synchronized method. You add one keyword to the method, and Java does the guarding for you. Only one thread can be inside that method at a time, per object.
Let us patch the counter.
class Counter {
int count = 0;
synchronized void increment() {
count = count + 1;
}
}That is the whole change. Now when thread A is inside increment(), thread B cannot enter. B waits at the door. Once A leaves, B goes in. The read-add-write runs start to finish without anyone cutting in.
Run the earlier test with this version. You get twenty thousand every single time. No lost updates, no surprises.
It’s important to clarify a common misconception regarding synchronized instance methods. These methods actually acquire a lock on the object itself, rather than specifically on the method. This means that the lock is associated with the individual Counter object, ensuring that only one thread can execute any synchronized method on that object at a given time.
Say you have two different Counter objects. A thread working on the first object does not block a thread working on the second. Each object carries its own lock. They never step on each other.
That makes sense once you think about it. Two separate counters hold separate data. There is no reason to make them wait for each other.
| Interview insight Interviewers love to ask: what does a synchronized instance method lock on? The answer is the current object, this. Two threads calling the same synchronized method on the same instance are serialized. Two threads calling it on different instances run in parallel. |
A synchronized method locks the entire method, which can be excessive in certain situations. In cases where only a small portion of the code—perhaps just three lines—interacts with shared data, applying a lock to the whole method may lead to unnecessary delays. Instead, it can be more efficient to lock only the specific sections of code that access shared resources, thereby improving performance while maintaining safety.
The synchronized block fixes that. You wrap only the risky lines, and you name the object to lock on.
class Counter {
int count = 0;
private final Object lock = new Object();
void increment() {
// other work here runs freely
synchronized (lock) {
count = count + 1; // only this is guarded
}
// more free work here
}
}Notice the object inside the parentheses. That object is the lock. A thread must grab that lock before it enters the block. When it leaves the block, it lets the lock go.
Using synchronized (this) allows you to lock on the object itself, which effectively manages concurrent access. However, there’s an important consideration to keep in mind: if external code also locks on the same object, it can lead to potential interference with your locking mechanism. This can create complexities that make reasoning about the code more challenging.
public class Counter {
private int count = 0;
// Locks on the object itself (this).
public void increment() {
synchronized (this) {
count = count + 1;
}
}
}Using a private final lock object is an effective way to ensure that your locking mechanism remains secure and predictable.
public class Counter {
private int count = 0;
private final Object lock = new Object(); // hidden from outside
public void increment() {
synchronized (lock) { // locks on the private object, not on this
count = count + 1;
}
}
}Since this lock object is not visible to any code outside of your class, it prevents external entities from accessing or manipulating it. This level of control allows you to manage locking behavior within your class effectively, leading to more reliable synchronization.
So which one should you reach for? Here is a plain rule of thumb. If the whole method works on shared data, a synchronized method reads cleaner. If only a small part touches shared data, a block keeps your lock tight and your code fast.
Do not overthink it early on. Start simple with a synchronized method. Move to a block later if you find the lock is slowing threads down. Correct first, fast second.
So far every lock has lived on an object. But static data does not belong to any single object. It belongs to the class. Guarding static data needs a different kind of lock.
A normal synchronized instance method or a synchronized (this) block uses an object-level lock. Each object has its own. This is the right choice when the data you protect is instance data, meaning each object has its own copy.
Different objects, different locks. They run side by side without waiting.
In object-oriented programming, static fields are unique in that they are shared across all instances of a class, meaning there is only a single copy of each static field regardless of the number of objects created. To ensure thread safety when accessing these static fields, it is important to use locks on the class itself rather than on individual objects. This approach helps to prevent concurrent access issues and maintains the integrity of the shared data.
A static synchronized method does this for you automatically. It locks on the Class object.
class Counter {
static int total = 0;
// locks on Counter.class, shared across all objects
static synchronized void addToTotal() {
total = total + 1;
}
// the block form, same effect
void addToTotalBlock() {
synchronized (Counter.class) {
total = total + 1;
}
}
}Now every thread, no matter which object it uses, competes for the same class lock. That is what you want for static data. One shared counter needs one shared gate.
| Interview insight A classic trick question: can two threads run a synchronized instance method and a static synchronized method of the same class at the same time? Yes. The instance method locks on the object, the static method locks on the Class object. They are two different locks, so both can proceed together. |
In Java, every object comes equipped with an inherent mechanism for synchronization known as an intrinsic lock or monitor. This lock operates in the background and ensures that only one thread can access a particular object at a time, providing a way to manage concurrent access. Importantly, developers do not need to explicitly create this lock; it is automatically associated with each object, facilitating thread safety in multithreaded environments.
When a thread accesses a synchronized method or block, it acquires the monitor associated with that object. This ensures that only one thread can hold the monitor at any given time. Upon completion of its operations within the synchronized context, the thread releases the monitor, allowing other threads to access the synchronized method or block. This mechanism is crucial for maintaining thread safety and preventing concurrent access issues.
Think of the monitor as a single key hanging on the object. To enter the guarded code, a thread must take the key. While it holds the key, no other thread can enter. When it walks out, it hangs the key back up.
The next waiting thread then grabs the key and goes in. This simple key-passing is how synchronized guarantees that only one thread runs at a time.
The monitor plays a significant role in managing behavior in concurrent programming. When two synchronized methods are called on the same object, they share the same monitor, which means they will block each other from executing simultaneously. In contrast, if two methods operate on different objects, they each have their own monitors, allowing them to execute freely without blocking one another. Understanding this distinction is crucial for effective multitasking and resource management in software development.
Keep that picture in your head. Every synchronized call is really a fight over one specific key. Once you know which key is in play, the thread behavior stops feeling like magic.
A significant aspect of Java locks is their reentrant behavior. Reentrancy allows a thread that has already acquired a specific lock to re-enter the same lock without causing a deadlock or blocking itself. This characteristic is crucial in scenarios where a thread needs to call a synchronized method from within another synchronized method of the same object.
For example, consider a situation where a thread holds a lock on an object while executing a method. If this method needs to call another synchronized method on the same object, the reentrant design permits this without forcing the thread to wait for itself to release the lock. This leads to improved efficiency and simpler code management, especially in recursive or nested method calls.
In Java, this reentrant locking behavior is built into the design of the ReentrantLock class and the synchronized keyword, making it a powerful feature for developers working with concurrent applications, as it enhances code maintainability while reducing the risk of concurrency-related issues.
Why does that matter? Because one synchronized method often calls another synchronized method on the same object.
public class Account {
private int balance = 0;
synchronized void deposit(int amount) {
System.out.println(Thread.currentThread().getName()
+ " entered deposit() — lock held once");
balance = balance + amount;
logBalance(); // calls another synchronized method on the SAME object
System.out.println(Thread.currentThread().getName()
+ " leaving deposit()");
}
synchronized void logBalance() {
// The thread is ALREADY holding this object's lock from deposit().
// Reentrancy lets it walk straight in instead of blocking on itself.
System.out.println(Thread.currentThread().getName()
+ " entered logBalance() — same lock re-entered");
System.out.println("Balance: " + balance);
}
public static void main(String[] args) throws InterruptedException {
Account acc = new Account();
Runnable job = () -> acc.deposit(100);
Thread t1 = new Thread(job, "T1");
Thread t2 = new Thread(job, "T2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final balance: " + acc.balance); // 200
}
}Sample output (one thread finishes fully before the other gets in):
T1 entered deposit() — lock held once T1 entered logBalance() — same lock re-entered Balance: 100 T1 leaving deposit() T2 entered deposit() — lock held once T2 entered logBalance() — same lock re-entered Balance: 200 T2 leaving deposit() Final balance: 200
When a thread runs deposit(), it holds the lock. Then deposit() calls logBalance(), which also wants the lock. Since the thread already owns it, Java just lets it in. Without reentrancy, the thread would wait for a lock it is already holding. That would freeze forever.
The monitor keeps a small counter. Each time the owning thread enters, the count goes up. Each time it exits, the count goes down. The lock only frees up when the count hits zero.
public class NestedLock {
synchronized void outer() {
System.out.println("outer() — lock acquired, count now 1");
middle(); // same object, same lock, count goes to 2
System.out.println("outer() — leaving, count back to 1");
}
synchronized void middle() {
System.out.println(" middle() — same lock re-entered, count now 2");
inner(); // same lock again, count goes to 3
System.out.println(" middle() — leaving, count back to 2");
}
synchronized void inner() {
System.out.println(" inner() — same lock re-entered, count now 3");
System.out.println(" inner() — leaving, count back to 2");
}
public static void main(String[] args) {
NestedLock obj = new NestedLock();
obj.outer(); // one thread dives three levels deep on one lock
System.out.println("Done — lock fully released, count is 0");
}
}Run it and watch the count in the output:
outer() — lock acquired, count now 1
middle() — same lock re-entered, count now 2
inner() — same lock re-entered, count now 3
inner() — leaving, count back to 2
middle() — leaving, count back to 2
outer() — leaving, count back to 1
Done — lock fully released, count is 0Each time the thread enters a deeper method on the same object, the count climbs: 1, then 2, then 3. Each time a method returns, the count drops.
Here is the key part. The lock does not release when inner() finishes. It frees up only when the count reaches zero, after outer() returns. So a thread can nest synchronized calls as deep as it needs. The lock stays with it the whole way down and only releases when it fully unwinds.
The synchronized keyword handles most everyday cases well. It is clean and built into the language. Still, it has limits. You cannot try for a lock and give up if it is busy. There is no way to set a wait timeout. And a thread stuck waiting cannot be interrupted.
For those needs, Java offers an explicit-lock alternative called ReentrantLock. It gives you finer control at the cost of a bit more code. We cover it fully in its own article, so treat this as a signpost rather than a detour.
Synchronization looks easy, and that is the trap. A few slips catch beginners and seasoned devs alike. Let us name them.
If two threads lock on different objects, they do not block each other at all. You think the code is guarded, but it is wide open. Make sure every thread that touches the shared data locks on the same object.
Never lock on a field you reassign later. If the reference changes, threads end up locking on different objects. Use a final lock object so the reference never moves.
A big synchronized region makes threads wait more than they should. Guard only the lines that touch shared data. Keep slow work, like file reads or network calls, outside the lock.
This is not just a textbook idea. It runs under the hood of code you use every day. Once you know the shape, you spot it fast.
Older classes like Vector and Hashtable synchronize every method. That makes them thread-safe, but also slower, since every call takes a lock. Modern code often prefers newer tools built for concurrency.
Any time several threads update one number or one map, synchronization keeps the data honest. A request counter, a small in-memory cache, a running total: all common spots where a lock earns its keep.
A: Synchronization is a way to let only one thread run a block of code at a time. It stops multiple threads from changing shared data together, which prevents race conditions and keeps the data correct.
A: A synchronized instance method locks on the current object (this). A static synchronized method locks on the class object instead. So two threads on the same instance are serialized, but two threads on different instances run in parallel.
A: A synchronized method locks the whole method. A synchronized block locks only the lines you wrap, and you choose the object to lock on. Blocks keep the locked region small, so threads wait less and the code runs faster.
A: Every Java object has a built-in lock called the monitor, or intrinsic lock. A thread must hold this lock to enter a synchronized method or block on that object. Only one thread can hold a given monitor at a time.
A: Yes. A thread that already holds a lock can acquire the same lock again without blocking. This lets one synchronized method call another synchronized method on the same object safely. The monitor tracks a count that only frees the lock when it returns to zero.
A: Yes. The instance method locks on the object, while the static method locks on the class object. Since these are two different locks, both methods can run at once without blocking each other.
A: Use the explicit ReentrantLock when you need features synchronized cannot give you, such as a wait timeout, a try-and-back-off attempt, or interrupting a waiting thread. For simple guarding of shared state, synchronized is usually enough.
Let us wrap up. Synchronization in Java lets only one thread run a guarded piece of code at a time. That stops race conditions, where threads step on each other and updates go missing.
You can access it in two main ways. The first method is a synchronized method, which locks the entire method on the object. The second method is a synchronized block, which only locks the lines that are risky on a chosen object. For static data, a class-level lock is used instead.
Under it all sits the monitor, the hidden lock every object carries. And because locks are reentrant, a thread can nest synchronized calls without freezing itself. Master these basics, and shared data stops being scary.