Java synchronized Keyword: Methods, Blocks and Locks

Learn the Java synchronized keyword with methods and blocks, which lock each form uses, reentrancy, visibility, deadlocks and virtual threads.

Two threads competing for one object lock in a synchronized method

The synchronized keyword makes a method or a block of code run in only one thread at a time for a given lock object, and it guarantees that the next thread sees all changes made inside it. Every Java object has a built-in lock, called a monitor, and synchronized acquires it on entry and releases it on exit.

We use synchronized to protect shared mutable state, such as a counter, a cache or a bank balance, from race conditions. Without it, two threads can read the same old value and both write back their own update, so one update is lost.

The following example increments a shared counter from two threads. The Counter class is defined in section 1.

Counter counter = new Counter();
Thread t1 = Thread.ofPlatform().start(() -> { for (int i = 0; i < 10_000; i++) counter.increment(); });
Thread t2 = Thread.ofPlatform().start(() -> { for (int i = 0; i < 10_000; i++) counter.increment(); });
t1.join();
t2.join();
int total = counter.get();              // 20000, every time

Without synchronized on increment(), the same code returned values such as 14,544 or 15,173 in some of our runs on Java 25. We look at synchronized methods and blocks, the lock that each form uses, reentrancy and visibility, deadlocks, virtual threads, and when other tools fit better.

1. Synchronized Methods

A synchronized method locks the whole method body. For an instance method, the lock is the object the method is called on, this. Two threads can call the method at the same time only on two different objects.

Two threads competing for one object lock in a synchronized method
One thread holds the lock while the other waits, and the next owner sees all earlier writes
class Counter {
    private int count;

    synchronized void increment() {
        count++;                         // read, add, write as one step
    }

    synchronized int get() {
        return count;
    }
}

The get() method is synchronized too. Without it, a reader thread is not guaranteed to see the latest value, because the lock also controls memory visibility, as section 4 explains.

A static synchronized method locks the Class object of its class instead of an instance. A static and an instance synchronized method of the same class use different locks, so they can run at the same time and do not protect each other’s data.

2. Synchronized Blocks

A synchronized block locks only the statements inside it, on an object we choose. Keeping the locked part short lets other threads work in parallel during the slow, unshared parts of a method.

class OrderBook {
    private final Object lock = new Object();
    private final List<String> orders = new ArrayList<>();

    void add(String rawOrder) {
        String order = rawOrder.trim().toUpperCase();   // no shared state, no lock
        synchronized (lock) {
            orders.add(order);
        }
    }

    int size() {
        synchronized (lock) {
            return orders.size();
        }
    }
}
OrderBook book = new OrderBook();
book.add("  buy 10 acme ");
int orderCount = book.size();           // 1

A private, final lock object is the safest choice, because no outside code can lock it by accident. Locking on this works too, but any caller that holds a reference to the object can also lock it and block our methods.

3. Which Lock Each Form Uses

Two pieces of code exclude each other only if they lock the same object, so the choice of lock decides what is protected.

FormLock object
synchronized void m()this, the current instance
static synchronized void m()Counter.class, the Class object
synchronized (lock) { … }The object that lock refers to
synchronized (Counter.class) { … }The same lock as static synchronized methods

Some objects make bad locks. A String literal is shared across the whole JVM, and an Integer or other value-based object may be cached and shared. For value-based classes, javac warns when we synchronize on them. A null lock throws NullPointerException when the block starts.

4. Reentrancy and Memory Visibility

Java locks are reentrant. A thread that holds a lock can enter another block or method synchronized on the same lock, and the JVM counts the holds and releases the lock only when the outermost block exits.

class Account {
    private long balance;

    synchronized void deposit(long amount) {
        balance += amount;
    }

    synchronized void depositTwice(long amount) {
        deposit(amount);                 // same thread takes the lock again
        deposit(amount);
    }

    synchronized long balance() {
        return balance;
    }
}
Account account = new Account();
account.depositTwice(50);
long balance = account.balance();       // 100

The second guarantee is happens-before ordering. Everything a thread writes before it releases a lock is visible to the next thread that acquires the same lock. This is why reads need the lock as well as writes, and why synchronized fixes problems that look like stale values.

5. Deadlocks and Waiting

A deadlock happens when two threads each hold one lock and wait for the other’s lock. A classic case is a money transfer that locks the source account first, while another transfer in the opposite direction locks the target first.

The standard fix is a fixed lock order. If every transfer locks the account with the smaller ID first, two threads can never wait for each other in a circle.

record Wallet(int id) {}
static String transfer(Wallet from, Wallet to) {
    Wallet first = from.id() < to.id() ? from : to;
    Wallet second = first == from ? to : from;
    synchronized (first) {
        synchronized (second) {
            return "moved from " + from.id() + " to " + to.id();
        }
    }
}
String move = transfer(new Wallet(2), new Wallet(1));   // "moved from 2 to 1"

Inside a synchronized block, wait() releases the lock and suspends the thread until another thread calls notify() or notifyAll() on the same object. A pause with Thread.sleep keeps the lock, so a thread that pauses inside a synchronized block blocks every other thread that needs it.

6. synchronized and Virtual Threads

In Java 21, a virtual thread that blocked inside a synchronized block stayed pinned to its carrier platform thread, which could exhaust the small carrier pool. Many libraries replaced synchronized with ReentrantLock for that reason.

Java 24 removed that limit with JEP 491. On Java 24 and later, a virtual thread that blocks in a synchronized block or in wait() releases its carrier, so synchronized is again a good default for code that runs on virtual threads.

Counter shared = new Counter();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000; i++) {
        executor.submit(shared::increment);
    }
}
int virtualTotal = shared.get();        // 1000

7. synchronized vs ReentrantLock vs Atomic Classes

The synchronized keyword is the simplest lock, and the JVM releases it even when an exception leaves the block. The java.util.concurrent package offers more specialized tools, and the ReentrantLock guide shows the explicit lock in action.

ToolBest for
synchronizedMost shared state, short critical sections
ReentrantLockTimeouts with tryLock(), interruptible waiting, fair ordering, several conditions
AtomicInteger, LongAdderA single counter or value, without any lock
volatileA flag that one thread writes and others read, with no compound updates
ConcurrentHashMapShared maps with high read and write traffic
AtomicInteger hits = new AtomicInteger();
hits.incrementAndGet();
hits.incrementAndGet();
int hitCount = hits.get();              // 2

8. synchronized Keyword FAQs

Developers who write their first multithreaded code ask these questions.

8.1. Can a constructor be synchronized?

No, the compiler rejects it. Normally only the thread that runs the constructor can see the new object until the constructor returns, so there is nothing to protect yet, unless the constructor lets this escape to other code.

8.2. Is the synchronized modifier inherited?

No. An overriding method in a subclass is not synchronized unless it is declared so, even when the parent method is.

8.3. Does synchronized make code slow?

An uncontended lock costs very little on modern JVMs. The cost comes from contention, when many threads wait for the same lock, so we keep synchronized sections short.

8.4. Do we need synchronized for local variables?

No. Local variables live on the stack of one thread, so other threads cannot see them. Only fields and objects shared between threads need protection.

9. Conclusion

The synchronized keyword gives one thread at a time access to code guarded by the same lock, and it makes changes visible to the next thread that takes that lock. Instance methods lock this, static methods lock the Class object, and blocks lock the object we choose.

We keep critical sections short, use private lock objects, lock in a fixed order to avoid deadlocks, and switch to atomic classes or ReentrantLock when they fit better. Since Java 24, synchronized also works well with virtual threads.

10. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Dear Sir,

    In Section 2.2 you have written,

    In case of synchronized method, the lock object is –
    ‘.class’ object – if the method is static.
    ‘this’ object – if the method is not static. ‘this’ refer to reference to current object in which synchronized method is invoked.
    but In case of synchronized block the same thing does happen.

    case 1: ‘.class’ object – if the method is static.
    static void printNumbers(int n) throws InterruptedException
    {
    synchronized (MathClass.class)
    {
    for (int i = 1; i <= n; i++)
    {
    System.out.println(Thread.currentThread().getName() + " :: "+ i);
    Thread.sleep(500);
    }
    }
    }

    case 2: this’ object – if the method is not static.

    void printNumbers(int n) throws InterruptedException
    {
    synchronized ( this)
    {
    for (int i = 1; i <= n; i++)
    {
    System.out.println(Thread.currentThread().getName() + " :: "+ i);
    Thread.sleep(500);
    }
    }
    }

    Please let me know if I am wrong.

    Thanks & Regards
    Tarun

    • Hi Tarun,

      we are starting 2 threads on same objects, so in non static method lock is applied in Object so only one thread can execute the critical section code.
      on applying synchronized on static method or synchronized ( classname.class), here lock is applied on class level so If we are starting 2 threads on 2 different objects the class then also only one thread can execute the critical section code.

      Thanks,
      Anil

      • Hi Anil,

        there is one thing from my side that if we starting 2 threads on 2 different objects then both the Threads are process simultaneously as for each object there is its own critical section of code.

        For Example::

        class PrintNumbers {
        synchronized void printingNumbers(int number) throws InterruptedException {
        for (int j = 0; j {
        try {
        printNumbers1.printingNumbers(10);
        } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        }
        }, “Thread1”).start();

        new Thread(() -> {
        try {
        printNumbers2.printingNumbers(100);
        } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        }
        }, “Thread2”).start();

        }

        }

        Output ::

        Thread[Thread1,5,main] :: 10
        Thread[Thread2,5,main] :: 100
        Thread[Thread1,5,main] :: 20
        Thread[Thread2,5,main] :: 200
        Thread[Thread2,5,main] :: 300
        Thread[Thread1,5,main] :: 30
        Thread[Thread2,5,main] :: 400
        Thread[Thread1,5,main] :: 40
        Thread[Thread2,5,main] :: 500
        Thread[Thread1,5,main] :: 50

        please correct me if i am wrong anywhere in it.

        Thanks and Best Regards,
        Naveen Jain.

  2. Hi I ran the same code using synchronized block.It is giving alternate output from threads..

    O/P when n=2

    ONE::0
    TWO::0
    ONE::1
    TWO::1

    Code:

    public class SynchronizedExample {

    public void printNumbers(int n) throws InterruptedException {

    synchronized (this) {

    for(int i=0;i<n;i++) {
    System.out.println(Thread.currentThread().getName()+ "::"+i);
    Thread.sleep(500);
    }
    }

    }

    public static void main(String[] args) {
    // TODO Auto-generated method stub

    Runnable r=new Runnable() {

    @Override
    public void run() {
    SynchronizedExample ex=new SynchronizedExample();
    try {
    ex.printNumbers(2);
    } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }

    }
    };

    new Thread(r,"ONE").start();
    new Thread(r,"TWO").start();

    }

    }

    • Hi Dipanshu,

      You have created the SynchronizedExample object inside the run method only and that is different then creating it outside as given in the example.

      In your scenario both the threads are having a different lock objects thats why synchronized keyword wont help in your scenario.

      If you create the SynchronizedExample object outside the run method then in that case associated monitor object is same for both the threads and you get the output mentioned in the above example.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.