Locks and Atomic Classes in Java
Explicit locks offer timeouts, fairness and multiple conditions, and the atomic classes give lock free updates for single variables.
-
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
Why explicit locks exist
synchronized is simple but rigid: you cannot try to acquire, cannot give up after a timeout, cannot interrupt a waiting thread, and cannot separate readers from writers. The java.util.concurrent.locks package fills those gaps.
ReentrantLock
public class Inventory {
private final ReentrantLock lock = new ReentrantLock();
private final Map<String, Integer> stock = new HashMap<>();
public void add(String item, int quantity) {
lock.lock();
try {
stock.merge(item, quantity, Integer::sum);
} finally {
lock.unlock(); // must be in finally
}
}
}Thefinallyblock is not optional. Unlikesynchronized, an explicit lock is not released automatically when an exception unwinds the stack, and a lock left held stops every other thread permanently.
Trying, and giving up
if (lock.tryLock()) { // returns immediately
try {
doWork();
} finally {
lock.unlock();
}
} else {
handleBusy();
}
if (lock.tryLock(2, TimeUnit.SECONDS)) { // waits, then gives up
try {
doWork();
} finally {
lock.unlock();
}
}
lock.lockInterruptibly(); // can be cancelled by interrupting the threadFairness
ReentrantLock fair = new ReentrantLock(true); // longest waiting thread winsA fair lock prevents starvation but is noticeably slower, because it cannot hand the lock to a thread that happens to be running. Use it only when starvation is a demonstrated problem.
Conditions
public class BoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Queue<T> items = new ArrayDeque<>();
private final int capacity;
public BoundedBuffer(int capacity) {
this.capacity = capacity;
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (items.size() == capacity) {
notFull.await();
}
items.add(item);
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (items.isEmpty()) {
notEmpty.await();
}
T item = items.poll();
notFull.signalAll();
return item;
} finally {
lock.unlock();
}
}
}A Condition is the explicit lock equivalent of wait and notify, but one lock can have several. Producers wait on one and consumers on another, so a signal wakes only the threads that can actually make progress. An intrinsic lock has a single wait set and cannot do this.
ReadWriteLock
public class Settings {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Map<String, String> values = new HashMap<>();
public String get(String key) {
lock.readLock().lock();
try {
return values.get(key); // many readers at once
} finally {
lock.readLock().unlock();
}
}
public void set(String key, String value) {
lock.writeLock().lock();
try {
values.put(key, value); // exclusive
} finally {
lock.writeLock().unlock();
}
}
}Readers do not block each other; a writer blocks everyone. This pays off only when reads greatly outnumber writes, because the bookkeeping costs more than a plain lock otherwise.
Atomic classes
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet(); // atomic ++
counter.addAndGet(5);
counter.getAndSet(0);
counter.updateAndGet(n -> n * 2);
counter.accumulateAndGet(10, Integer::sum);
boolean swapped = counter.compareAndSet(20, 30); // only if it is still 20| Class | Holds |
|---|---|
AtomicInteger, AtomicLong | A number |
AtomicBoolean | A flag |
AtomicReference<T> | An object reference |
LongAdder, DoubleAdder | A counter under heavy contention |
AtomicIntegerArray | Array elements |
How they work
read the current value
compute the new value
compare-and-set: if the value is still what we read, store the new one
otherwise start againThis is compare and swap, a single processor instruction. No thread ever blocks, which is why atomics outperform locks for a single variable. Under very high contention the retry loop can spin, and that is what LongAdder addresses by keeping per thread cells and summing them on demand.
LongAdder hits = new LongAdder();
hits.increment();
System.out.println(hits.sum()); // far faster than AtomicLong under contentionAtomicReference for a compound value
record Stats(int count, long total) { }
AtomicReference<Stats> stats = new AtomicReference<>(new Stats(0, 0));
stats.updateAndGet(current ->
new Stats(current.count() + 1, current.total() + value));An immutable record swapped atomically keeps two related fields consistent without any lock. The update function may run more than once, so it must be free of side effects.
Choosing between them
| Situation | Use |
|---|---|
| A single counter | AtomicInteger, or LongAdder if hot |
| A single reference swapped atomically | AtomicReference |
| Simple mutual exclusion | synchronized |
| Timeout, interruption or fairness needed | ReentrantLock |
| Several distinct wait conditions | Lock with Condition |
| Many readers, few writers | ReentrantReadWriteLock |
Common mistakes
- Not unlocking in a
finallyblock. - Unlocking a lock the thread does not hold, which throws.
- Using a read write lock where reads and writes are balanced, and losing performance.
- Assuming a sequence of atomic calls is itself atomic.
- Writing a side effecting function inside
updateAndGet, which may be retried. - Using a fair lock everywhere by default.
Best practices
- Prefer
synchronizedfor straightforward exclusion; reach for aLockwhen you need what it adds. - Always unlock in
finally. - Use atomics for single variables and immutable records inside
AtomicReferencefor related fields. - Use
LongAdderfor high frequency counters. - Keep update functions pure.
- Prefer the concurrent collections over locking a plain one.
Practice
- Rewrite a synchronised counter using
AtomicIntegerand compare the code. - Why must
unlockbe in afinallyblock? - Implement a bounded buffer with two conditions and explain why one is not enough.
- When does a read write lock actually pay for itself?
- Use
compareAndSetto implement a value that can only ever increase.
Conclusion
Explicit locks add timeouts, interruption, fairness and multiple conditions at the cost of manual unlocking. Atomic classes give lock free updates to a single variable. Use the simplest tool that meets the requirement, and let the concurrent collections do the work where they can.