Introduction
In Java, concurrency utilities are provided to handle synchronization and ensure thread safety. Locks and atomic variables are two key components of the java.util.concurrent
package. Locks offer more extensive locking operations than synchronized methods and statements, while atomic variables provide a way to perform atomic operations on single variables without using synchronization.
Key Points:
- Locks: Provide more flexible and sophisticated locking mechanisms than intrinsic locks.
- Atomic Variables: Allow atomic operations on single variables, eliminating the need for synchronization.
Table of Contents
- Understanding Locks
- ReentrantLock
- ReadWriteLock
- Understanding Atomic Variables
- AtomicInteger
- AtomicBoolean
- AtomicReference
- Example: Using Locks
- Example: Using Atomic Variables
- Best Practices
- Real-World Analogy
- Conclusion
1. Understanding Locks
Locks provide more sophisticated locking mechanisms than the built-in synchronized keyword. They offer features like reentrant locking, timed locking, and interruptible locking.
ReentrantLock
ReentrantLock
is a concrete implementation of the Lock
interface. It offers the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.
Example:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
public class ReentrantLockExample {
public static void main(String[] args) {
Counter counter = new Counter();
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();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + counter.getCount()); // Expected output: 2000
}
}
ReadWriteLock
ReadWriteLock
maintains a pair of associated Lock
objects, one for read-only operations and one for writing. The read lock may be held simultaneously by multiple reader threads as long as there are no writers. The write lock is exclusive.
Example:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class SharedResource {
private int data = 0;
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void write(int value) {
lock.writeLock().lock();
try {
data = value;
} finally {
lock.writeLock().unlock();
}
}
public int read() {
lock.readLock().lock();
try {
return data;
} finally {
lock.readLock().unlock();
}
}
}
public class ReadWriteLockExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread writer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
resource.write(i);
System.out.println("Written: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread reader = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Read: " + resource.read());
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
writer.start();
reader.start();
try {
writer.join();
reader.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2. Understanding Atomic Variables
Atomic variables provide a way to perform atomic operations on single variables without synchronization. They are part of the java.util.concurrent.atomic
package.
AtomicInteger
AtomicInteger
supports atomic operations on an int
value.
Example:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerExample {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
count.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + count.get()); // Expected output: 2000
}
}
AtomicBoolean
AtomicBoolean
supports atomic operations on a boolean
value.
Example:
import java.util.concurrent.atomic.AtomicBoolean;
public class AtomicBooleanExample {
private static AtomicBoolean flag = new AtomicBoolean(false);
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
if (flag.compareAndSet(false, true)) {
System.out.println("Flag was false, set to true");
}
});
Thread t2 = new Thread(() -> {
if (flag.compareAndSet(false, true)) {
System.out.println("Flag was false, set to true");
} else {
System.out.println("Flag was already true");
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
AtomicReference
AtomicReference
supports atomic operations on an object reference.
Example:
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceExample {
private static AtomicReference<String> atomicString = new AtomicReference<>("initial value");
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
atomicString.set("Thread 1 value");
});
Thread t2 = new Thread(() -> {
atomicString.compareAndSet("initial value", "Thread 2 value");
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Atomic Reference Value: " + atomicString.get());
}
}
3. Example: Using Locks
Comprehensive Example
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
public class LockExample {
public static void main(String[] args) {
Counter counter = new Counter();
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();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + counter.getCount()); // Expected output: 2000
}
}
4. Example: Using Atomic Variables
Comprehensive Example
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicVariableExample {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
count.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + count.get()); // Expected output: 2000
}
}
5. Best Practices
- Use Locks for Fine-Grained Control: Use locks when you need more fine-grained control over synchronization.
- Prefer Atomic Variables for Single Variables: Use atomic variables for atomic operations on single variables.
- Always Release Locks: Ensure that locks are always released by using a
try-finally
block. - Use ReadWriteLock for Read-Mostly Data: Use
ReadWriteLock
for data structures that are read frequently and written infrequently to improve concurrency. - Avoid Lock Contention: Minimize the duration for which locks are held to reduce contention and improve performance.
- Use Timed Locking: Use timed locking methods (
tryLock(long time, TimeUnit unit)
) to avoid deadlocks and ensure that threads do not wait indefinitely. - Prefer High-Level Concurrency Utilities: Whenever possible, use high-level concurrency utilities provided in the
java.util.concurrent
package, such asConcurrentHashMap
,BlockingQueue
, andSemaphore
.
6. Real-World Analogy
Consider a library where multiple readers and writers access a collection of books:
- Locks: The library has a gatekeeper (lock) who allows only one person (thread) to enter a restricted section at a time to ensure no two people access the same book (shared resource) simultaneously.
- ReadWriteLock: The library uses separate sections for readers and writers. Multiple readers can read books simultaneously, but only one writer can modify a book at a time.
- Atomic Variables: The library maintains a counter (atomic variable) for the number of visitors. The counter is updated atomically to ensure accuracy without the need for a gatekeeper.
7. Conclusion
Locks and atomic variables are essential tools in Java for handling synchronization and ensuring thread safety. Locks, such as ReentrantLock
and ReadWriteLock
, provide more flexible and sophisticated locking mechanisms than the built-in synchronized keyword. Atomic variables, such as AtomicInteger
, AtomicBoolean
, and AtomicReference
, allow atomic operations on single variables without using synchronization, making them ideal for lightweight synchronization tasks. By understanding and effectively using these tools, you can write efficient, thread-safe, and maintainable concurrent applications in Java.