Java Locks and Atomic Variables Tutorial

Introduction

Java provides several mechanisms for synchronizing access to shared resources in concurrent programming. Two key components of these mechanisms are locks and atomic variables. Locks offer more flexible synchronization compared to the synchronized keyword, while atomic variables provide lock-free thread-safe operations on single variables.

Table of Contents

  1. What are Locks?
  2. Types of Locks
  3. ReentrantLock Example
  4. ReadWriteLock Example
  5. What are Atomic Variables?
  6. Common Atomic Variables
  7. AtomicInteger Example
  8. AtomicReference Example
  9. Conclusion

1. What are Locks?

Locks provide explicit mechanisms to ensure that only one thread can access a shared resource at a time. Unlike the synchronized keyword, locks offer more extensive capabilities, such as timed and interruptible lock waits, and support for fairness policies.

2. Types of Locks

  • ReentrantLock: A reentrant mutual exclusion lock with the same basic behavior as the implicit monitor lock accessed using synchronized methods and statements.
  • ReadWriteLock: A lock that allows multiple readers to access the shared resource concurrently but only one writer to modify it.

3. ReentrantLock Example

Example:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final Lock lock = new ReentrantLock();
    private int counter = 0;

    public void increment() {
        lock.lock();
        try {
            counter++;
            System.out.println(Thread.currentThread().getName() + " - Counter: " + counter);
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();
    }
}

Explanation:

  • Locking: lock.lock() acquires the lock.
  • Unlocking: lock.unlock() releases the lock.
  • Usage: The increment method is protected by the lock, ensuring thread-safe access to the counter variable.

4. ReadWriteLock Example

Example:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private int counter = 0;

    public void increment() {
        lock.writeLock().lock();
        try {
            counter++;
            System.out.println(Thread.currentThread().getName() + " - Counter: " + counter);
        } finally {
            lock.writeLock().unlock();
        }
    }

    public int getCounter() {
        lock.readLock().lock();
        try {
            return counter;
        } finally {
            lock.readLock().unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();

        Runnable writeTask = () -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        };

        Runnable readTask = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " - Counter: " + example.getCounter());
            }
        };

        Thread t1 = new Thread(writeTask);
        Thread t2 = new Thread(readTask);

        t1.start();
        t2.start();
    }
}

Explanation:

  • Write Lock: lock.writeLock().lock() acquires the write lock.
  • Read Lock: lock.readLock().lock() acquires the read lock.
  • Usage: The increment method is protected by the write lock, and the getCounter method is protected by the read lock, ensuring thread-safe access to the counter variable.

5. What are Atomic Variables?

Atomic variables provide a way to perform atomic (i.e., indivisible) operations on single variables without using locks. They are part of the java.util.concurrent.atomic package and provide methods to perform various operations atomically.

6. Common Atomic Variables

  • AtomicInteger: An integer value that may be updated atomically.
  • AtomicLong: A long value that may be updated atomically.
  • AtomicBoolean: A boolean value that may be updated atomically.
  • AtomicReference: An object reference that may be updated atomically.

7. AtomicInteger Example

Example:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
    private final AtomicInteger counter = new AtomicInteger(0);

    public void increment() {
        int newValue = counter.incrementAndGet();
        System.out.println(Thread.currentThread().getName() + " - Counter: " + newValue);
    }

    public static void main(String[] args) {
        AtomicIntegerExample example = new AtomicIntegerExample();
        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();
    }
}

Explanation:

  • Atomic Operation: incrementAndGet() increments the counter atomically and returns the new value.
  • Usage: The increment method is thread-safe without using explicit locks.

8. AtomicReference Example

Example:

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceExample {
    private final AtomicReference<String> message = new AtomicReference<>("Hello");

    public void updateMessage(String newMessage) {
        message.set(newMessage);
        System.out.println(Thread.currentThread().getName() + " - Message: " + message.get());
    }

    public static void main(String[] args) {
        AtomicReferenceExample example = new AtomicReferenceExample();
        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                example.updateMessage("Hello " + i);
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();
    }
}

Explanation:

  • Atomic Operation: set() sets the new value atomically.
  • Usage: The updateMessage method is thread-safe without using explicit locks.

9. Conclusion

Java provides several mechanisms for handling concurrent programming, including locks and atomic variables. Locks offer flexible synchronization options, while atomic variables provide lock-free thread-safe operations on single variables. Understanding and using these mechanisms can help you write efficient and robust concurrent applications.

Happy coding!

Comments