Mastering Java Concurrency: When to Use volatile, synchronized, and Lock

This article explains Java's memory model and compares the volatile keyword, synchronized blocks/methods, and the Lock interface, detailing their visibility, mutual exclusion, performance, and proper usage patterns to avoid deadlocks and improve multithreaded efficiency.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Java Concurrency: When to Use volatile, synchronized, and Lock

1. Overview

When studying concurrent programs, we need to understand Java's volatile and synchronized keywords as well as the Lock class. Each thread has its own local memory (stack frame). A thread reads variables from main memory into its local memory, operates on them, and later flushes changes back to main memory.

Locks provide two key properties: mutual exclusion (only one thread can hold a particular lock at a time) and visibility (changes made by one thread become visible to others). Visibility can be achieved with volatile, synchronized, or final after initialization.

2. volatile

volatile is a type modifier designed for variables accessed and modified by multiple threads. It prevents the compiler from optimizing away reads/writes and forces each read to fetch the latest value from main memory.

In short, volatile makes a variable’s updated value quickly visible to other threads.

(1) Problem Origin

Compilers may cache writes in registers or CPU caches for performance, making the new value invisible to other threads.

public class RunThread extends Thread {
    private boolean isRunning = true;
    public boolean isRunning() { return isRunning; }
    public void setRunning(boolean isRunning) { this.isRunning = isRunning; }
    @Override
    public void run() {
        System.out.println("进入到run方法中了");
        while (isRunning == true) { }
        System.out.println("线程执行完成了");
    }
}
public class Run {
    public static void main(String[] args) {
        try {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
        } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

When running with the -server JVM option, the RunThread may never see the updated isRunning value, causing an infinite loop.

(2) Cause Analysis

Both the main thread and RunThread read isRunning into their local caches. In -server mode the thread keeps reading from its private cache, so the change made by the main thread is not observed.

(3) Solution

volatile private boolean isRunning = true;

(4) Principle

When a volatile variable is written, caches holding the old value are invalidated, and other CPUs must fetch the new value from main memory before reading it again.

3. synchronized

synchronized is a Java keyword that can lock objects, methods, or code blocks so that at most one thread executes the synchronized section at a time. Other threads must wait until the lock is released.

(1) synchronized Method

Declared after the access modifier and before the return type; the thread acquires the object's monitor lock, executes the method, then releases the lock.

public synchronized void synMethod() { /* method body */ }

If thread t1 calls obj.synMethod(), it must first obtain obj 's lock. If another thread already holds that lock, t1 waits until the lock is released.

(2) synchronized Block

Used with a variable in parentheses; only one thread can enter the block at a time, acquiring the object's monitor.

(3) synchronized(this)

Only one thread can execute a synchronized(this) block on the same object at a time.

Other synchronized blocks on the same object are blocked as well.

Threads can still execute code outside the synchronized block.

public class Thread2 {
    public void m4t1() {
        synchronized(this) {
            int i = 5;
            while(i-- > 0) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try { Thread.sleep(500); } catch (InterruptedException ie) {}
            }
        }
    }
    public void m4t2() {
        int i = 5;
        while(i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try { Thread.sleep(500); } catch (InterruptedException ie) {}
        }
    }
    public static void main(String[] args) {
        final Thread2 myt2 = new Thread2();
        Thread t1 = new Thread(() -> myt2.m4t1(), "t1");
        Thread t2 = new Thread(() -> myt2.m4t2(), "t2");
        t1.start();
        t2.start();
    }
}

When m4t1 (a synchronized block) is accessed, m4t2 can still be executed concurrently.

(4) wait() and notify()/notifyAll()

wait()

releases the object's monitor and puts the thread into the wait set, allowing other threads to acquire the lock. sleep() does not release the lock. notify() wakes one waiting thread, while notifyAll() wakes all waiting threads. Both must be called inside a synchronized block.

4. Lock

(1) Drawbacks of synchronized

synchronized is built‑in, but it blocks other threads completely while a lock is held and cannot be interrupted.

(2) java.util.concurrent.locks classes

public interface Lock {
    void lock(); // acquire lock, blocks if unavailable
    void lockInterruptibly() throws InterruptedException; // can be interrupted while waiting
    boolean tryLock(); // non‑blocking attempt
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // timed attempt
    void unlock(); // release lock
    Condition newCondition();
}

Typical usage:

Lock lock = ...;
lock.lock();
try {
    // handle task
} finally {
    lock.unlock();
}

Try‑lock example:

Lock lock = ...;
if (lock.tryLock()) {
    try { /* handle task */ } finally { lock.unlock(); }
} else {
    // fallback actions
}

Interruptible lock usage:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try { /* ... */ } finally { lock.unlock(); }
}

(3) ReentrantLock

ReentrantLock implements Lock and provides reentrancy, meaning a thread that already holds the lock can acquire it again without deadlock.

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<>();
    private Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        final Test test = new Test();
        new Thread(() -> test.insert(Thread.currentThread())).start();
        new Thread(() -> test.insert(Thread.currentThread())).start();
    }
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName() + "得到了锁");
            for (int i = 0; i < 5; i++) {
                arrayList.add(i);
            }
        } finally {
            System.out.println(thread.getName() + "释放了锁");
            lock.unlock();
        }
    }
}

Both synchronized and ReentrantLock are reentrant, allowing a thread to enter another synchronized method or lock‑protected block without releasing the lock first.

5. Differences between volatile and synchronized

volatile tells the JVM that the variable’s value may change unpredictably and must be read from main memory each time; synchronized locks the variable so only one thread can access it at a time.

volatile can be applied only to variables, while synchronized can be applied to variables, methods, and blocks.

volatile guarantees visibility; synchronized guarantees both visibility and atomicity.

volatile does not block threads; synchronized may cause blocking.

volatile cannot be used for compound actions like n = n + 1 or for enforcing relationships between multiple fields.

The only safe use of volatile alone is when a class has a single mutable field.

6. Differences between synchronized and Lock

Lock is an interface; synchronized is a language keyword with built‑in implementation.

synchronized automatically releases the lock when an exception occurs; with Lock you must release it manually, typically in a finally block, otherwise deadlocks may arise.

Lock supports interruptible lock acquisition; synchronized does not.

Lock allows you to query whether the lock was successfully acquired; synchronized does not.

Lock can improve read‑only concurrency performance, especially under high contention.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencymultithreadingLocksynchronized
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.