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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
