Synchronization in Java: synchronized and volatile
synchronized gives mutual exclusion and visibility. volatile gives visibility only, and knowing the difference prevents most concurrency bugs.
- The two problems of shared state
- The classic broken counter
- synchronized
- Synchronized blocks
- Static synchronization
- Reentrancy
- volatile
- What volatile does not do
- synchronized compared with volatile
- When each is right
- Check then act needs a lock
- Double checked locking
- Common mistakes
- Best practices
- Practice
- Conclusion
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
The two problems of shared state
- Atomicity. An operation that looks like one step is several, and another thread can interleave.
- Visibility. A write by one thread may not be seen by another, because each may work with cached values and the compiler may reorder instructions.
synchronized solves both. volatile solves only the second.
The classic broken counter
public class Counter {
private int count = 0;
public void increment() {
count++; // read, add one, write back: three steps
}
public int get() {
return count;
}
}Counter counter = new Counter();
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 4; i++) {
pool.submit(() -> {
for (int j = 0; j < 100_000; j++) {
counter.increment();
}
});
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.MINUTES);
System.out.println(counter.get()); // usually well below 400000Thread A reads 10 Thread B reads 10
Thread A writes 11 Thread B writes 11
Two increments happened, but the count rose by one.synchronized
public class Counter {
private int count = 0;
public synchronized void increment() { // locks on this
count++;
}
public synchronized int get() {
return count;
}
}Every object has an intrinsic lock, or monitor. A thread entering a synchronized method or block acquires it and releases it on exit, including when an exception is thrown. Only one thread can hold it at a time.
Synchronized blocks
public class Inventory {
private final Object lock = new Object(); // a private, dedicated lock
private final Map<String, Integer> stock = new HashMap<>();
public void add(String item, int quantity) {
synchronized (lock) {
stock.merge(item, quantity, Integer::sum);
}
}
}A block locks less code than a whole method, which reduces contention. Using a private lock object rather than this stops outside code from locking on your instance and interfering.
Static synchronization
public static synchronized void register() { } // locks on Inventory.class
public static void register() {
synchronized (Inventory.class) { } // the same lock, written out
}An instance method and a static method lock different monitors, so they do not exclude each other. That surprise causes real bugs.
Reentrancy
public synchronized void outer() {
inner(); // no deadlock: the same thread already holds the lock
}
public synchronized void inner() { }Java locks are reentrant. A thread holding a lock can acquire it again, and the lock is released only when the outermost block exits.
volatile
public class Worker implements Runnable {
private volatile boolean running = true;
@Override
public void run() {
while (running) { // without volatile, this may loop forever
doWork();
}
}
public void stop() {
running = false;
}
}volatile guarantees that a write is visible to every subsequent read from any thread, and it prevents the compiler from caching the field in a register or reordering around it. It is exactly right for a flag.
What volatile does not do
private volatile int count = 0;
public void increment() {
count++; // still broken: volatile does not make this atomic
}volatilemakes a single read and a single write atomic and visible. A compound operation such ascount++is a read followed by a write, and another thread can interleave between them. For counters useAtomicInteger; for anything larger use a lock.
synchronized compared with volatile
| Aspect | synchronized | volatile |
|---|---|---|
| Mutual exclusion | Yes | No |
| Visibility | Yes | Yes |
| Prevents reordering | Yes | Yes |
| Applies to | Methods and blocks | Fields only |
| Can block a thread | Yes | Never |
| Compound operations | Safe | Not safe |
| Cost | Higher | Low |
When each is right
| Situation | Use |
|---|---|
| A stop flag written by one thread | volatile |
| A counter | AtomicInteger |
| Several fields that must change together | synchronized or a Lock |
| A check then act sequence | synchronized |
| An immutable object published once | Nothing, final is enough |
Check then act needs a lock
// Broken even with a thread safe map: two threads can both pass the check
if (!cache.containsKey(key)) {
cache.put(key, load(key));
}
// Correct
cache.computeIfAbsent(key, this::load);Double checked locking
public class Registry {
private static volatile Registry instance; // volatile is essential here
public static Registry getInstance() {
Registry local = instance;
if (local == null) {
synchronized (Registry.class) {
local = instance;
if (local == null) {
instance = local = new Registry();
}
}
}
return local;
}
}Without volatile, another thread could see a non null reference to a partially constructed object, because the assignment can be reordered ahead of the constructor finishing. An enum singleton or a static holder class avoids the whole problem.
Common mistakes
- Using
volatilefor a counter. - Synchronising on a mutable field, so the lock identity changes.
- Synchronising on a boxed
Integeror an internedString, which may be shared globally. - Assuming instance and static synchronized methods exclude each other.
- Holding a lock while performing I/O or calling unknown code.
- Synchronising everything and destroying throughput.
Best practices
- Prefer immutable objects, which need no synchronisation at all.
- Keep synchronised sections as short as possible.
- Use a private final lock object rather than
this. - Use
volatileonly for a simple flag or a safely published reference. - Use the atomic classes and concurrent collections instead of hand written locking.
- Never call unknown code while holding a lock.
Practice
- Run the unsynchronised counter with four threads and explain the result.
- Why does a
volatilecounter still lose increments? - Show that an instance and a static synchronized method do not exclude each other.
- Rewrite a check then act on a map using
computeIfAbsent. - Explain why double checked locking needs
volatile.
Conclusion
synchronized provides mutual exclusion and visibility; volatile provides visibility alone. Use volatile for flags, atomics for counters, locks for multi field invariants, and immutability wherever you can, because it removes the problem instead of managing it.