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
1.
Understanding
Locks
· ReentrantLock
· ReadWriteLock
2.
Understanding
Atomic Variables
· AtomicInteger
· AtomicBoolean
· AtomicReference
3.
Example:
Using Locks
4.
Example:
Using Atomic Variables
5.
Best
Practices
6.
Real-World
Analogy
7.
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
1.
Use
Locks for Fine-Grained Control: Use locks when you need more fine-grained control
over synchronization.
2.
Prefer
Atomic Variables for Single Variables: Use atomic variables for atomic operations on
single variables.
3.
Always
Release Locks: Ensure
that locks are always released by using a try-finally block.
4.
Use
ReadWriteLock for Read-Mostly Data: Use ReadWriteLock for data structures that are
read frequently and written infrequently to improve concurrency.
5.
Avoid
Lock Contention:
Minimize the duration for which locks are held to reduce contention and improve
performance.
6.
Use
Timed Locking: Use
timed locking methods (tryLock(long time, TimeUnit unit)) to avoid deadlocks and ensure that
threads do not wait indefinitely.
7.
Prefer
High-Level Concurrency Utilities: Whenever possible, use high-level concurrency
utilities provided in the java.util.concurrent package, such as ConcurrentHashMap, BlockingQueue, and Semaphore.
6. Real-World Analogy
Consider
a library where multiple readers and writers access a collection of books:
Bookshelves
- 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.
No comments:
Post a Comment