Skip to content
Dev Dump

Java Multithreading Locks - Simplified Guide

5030dd45cc8fcb10f9c4d4e168aaa24f_MD5

Locks are used to ensure only one thread accesses shared data at a time.

ConceptDescription
Critical SectionShared code that needs exclusive access
Race ConditionBug from unsynchronized access
DeadlockTwo threads waiting on each other’s locks
Mutual ExclusionEnsures one thread executes a section at a time

FeaturesynchronizedReentrantLockReadWriteLockvolatile
Thread Safetyβœ…βœ…βœ…βŒ (visibility only)
Read/Write SplitβŒβŒβœ…βŒ
FairnessβŒβœ… (optional)βœ…βŒ
Manual Lock ControlβŒβœ…βœ…βŒ
Performance (Reads)βš οΈβš οΈβœ…βœ…

public synchronized void increment() {
count++;
}
synchronized(this) {
count++;
}
public static synchronized void increment() {
count++;
}
  • No timeout
  • No lock status check
  • No fairness control
public class SyncExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SyncExample counter = new SyncExample();
Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) counter.increment(); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) counter.increment(); });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("Count: " + counter.getCount());
}
}
Count: 2000

synchronized ensures only one thread at a time enters the critical section. Good for simple synchronization but lacks flexibility.


  • Manual control (lock(), unlock())
  • Fairness support
  • Timeout & interruptible locks
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// critical section
} finally {
lock.unlock();
}
}
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try { count++; }
finally { lock.unlock(); }
}
public int getCount() { return count; }
public static void main(String[] args) throws InterruptedException {
ReentrantLockExample example = new ReentrantLockExample();
Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) example.increment(); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) example.increment(); });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("Count: " + example.getCount());
}
}
Count: 2000

Use ReentrantLock for advanced features: tryLock, fairness, and interruptible locks.


Multiple readers OR one writer – good for read-heavy scenarios.

ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock(); // for reading
rwLock.writeLock().lock(); // for writing
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteExample {
private int data = 0;
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
public void write() {
rw.writeLock().lock();
try {
data++;
System.out.println("Written: " + data);
} finally {
rw.writeLock().unlock();
}
}
public void read() {
rw.readLock().lock();
try {
System.out.println("Read: " + data);
} finally {
rw.readLock().unlock();
}
}
public static void main(String[] args) {
ReadWriteExample obj = new ReadWriteExample();
obj.write();
obj.read();
}
}
Written: 1
Read: 1

Improves performance in read-heavy environments by allowing multiple concurrent reads.


Allows optimistic read – great for fast non-blocking reads.

long stamp = lock.tryOptimisticRead();
int result = data;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
result = data;
lock.unlockRead(stamp);
}
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private int data = 0;
private final StampedLock lock = new StampedLock();
public void write() {
long stamp = lock.writeLock();
try {
data++;
System.out.println("Write: " + data);
} finally {
lock.unlockWrite(stamp);
}
}
public void optimisticRead() {
long stamp = lock.tryOptimisticRead();
int current = data;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try { current = data; }
finally { lock.unlockRead(stamp); }
}
System.out.println("Read: " + current);
}
public static void main(String[] args) {
StampedLockExample ex = new StampedLockExample();
ex.write();
ex.optimisticRead();
}
}
Write: 1
Read: 1

Great for performance when contention is low, using optimistic reading to avoid locks.


Replaces wait/notify with more flexible await/signal.

lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
// process
notFull.signal();
} finally {
lock.unlock();
}

Allows N threads to access a resource simultaneously.

Semaphore sem = new Semaphore(3);
sem.acquire();
// access resource
sem.release();
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static final Semaphore sem = new Semaphore(2);
public static void main(String[] args) {
Runnable task = () -> {
try {
System.out.println(Thread.currentThread().getName() + " waiting...");
sem.acquire();
System.out.println(Thread.currentThread().getName() + " acquired.");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " releasing...");
sem.release();
} catch (InterruptedException e) { e.printStackTrace(); }
};
for (int i = 0; i < 4; i++) new Thread(task).start();
}
}
Thread-0 waiting...
Thread-1 waiting...
Thread-0 acquired.
Thread-1 acquired.
Thread-2 waiting...
Thread-3 waiting...
Thread-0 releasing...
Thread-1 releasing...
Thread-2 acquired.
Thread-3 acquired.

Perfect for limiting concurrent access, such as in connection pools or rate limiters.


AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
AtomicReference<String> value = new AtomicReference<>("a");
value.compareAndSet("a", "b");
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int get() {
return count.get();
}
public static void main(String[] args) {
AtomicExample obj = new AtomicExample();
obj.increment();
System.out.println("Count: " + obj.get());
}
}
Count: 1
  1. Always use try-finally with manual locks
  2. Keep critical sections small
  3. Avoid nested locks
  4. Use atomic types for simple counters
  5. Prefer lock-free when possible
Lock TypeUse CasePerformanceComplexity
synchronizedSimple lockβœ… Good🟒 Easy
ReentrantLockFine controlβœ… Great under load🟑 Medium
ReadWriteLockRead-heavyβœ… Excellent🟑 Medium
StampedLockHigh concurrencyβœ… Best for readsπŸ”΄ High
SemaphoreLimit accessβœ… Scalable🟑 Medium
Atomic VariablesLock-free operationsβœ… Fastest🟒 Easy