Mutex Vs Semaphore

Mutex is a shared lock which only allows one resource to access at a time

Semaphore allows a number of resource to access at a time. When the resource finished accessing, it could release the lock for the next one to access

  • Think of semaphore as like a restaurants where people waited to be seated. A number of people need to get out for another next number of people to get in

Mutex

To create a mutex we can use

import java.util.concurrent.locks.Lock;

Considering the code below

@ThreadSafe
public class MutexDemo {
    private static final Lock mutex = new ReentrantLock();
    private static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        IntStream.range(0, 5).forEach((i) -> executorService.submit(() -> {
            try {
                process();
            } catch (InterruptedException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }));
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.HOURS);
        System.out.println("Counter: " + counter);
    }

    public static void process() throws InterruptedException {
        System.out.printf("Thread %s entered%n", Thread.currentThread().getName());
        mutex.lock();
        System.out.println(Thread.currentThread().getName() + " " + counter + " -> " + ++counter);
        try {
            Thread.sleep(5000);
        } finally {
	        System.out.printf("Thread %s left%n", Thread.currentThread().getName());
            mutex.unlock();
        }
    }
}

We use ReentrantLock to make sure that the thread is able to re-enter the lock.

We use the try {} finally {} block to make sure that the mutex is still unlocked even if an exception happen

As a result, the program will have the following output:

Thread pool-1-thread-4 entered
Thread pool-1-thread-3 entered
Thread pool-1-thread-5 entered
Thread pool-1-thread-2 entered
Thread pool-1-thread-1 entered
pool-1-thread-4 0 -> 1
Thread pool-1-thread-4 left
pool-1-thread-3 1 -> 2
Thread pool-1-thread-3 left
pool-1-thread-5 2 -> 3
Thread pool-1-thread-5 left
pool-1-thread-2 3 -> 4
Thread pool-1-thread-2 left
pool-1-thread-1 4 -> 5
Thread pool-1-thread-1 left
Counter: 5

[!note]
In this case, since we only have 1 thread access a counter at a time, we don't need to synchronized the Counter.

Semaphore

To create a semaphore, we use

import java.util.concurrent.Semaphore;

For example:


@ThreadSafe
public class SemaphoreDemo {
    private static final Semaphore semaphore = new Semaphore(2);
    private static volatile int counter = 0;

    public static void main(String[] args) throws InterruptedException{
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        IntStream.range(0, 5).forEach((i) -> {
            executorService.submit(() -> {
                try {
                    process();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        });
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES); // Wait for tasks to complete
        System.out.println("Counter: " +  counter);
    }

    public static void process() throws InterruptedException {
        System.out.println("Thread " + Thread.currentThread().getName() + " entered");
        semaphore.acquire();
        synchronized (SemaphoreDemo.class) {
            System.out.println(Thread.currentThread().getName() + " acquired semaphore: " + counter + " -> " + ++counter);
        }
        try {
            Thread.sleep(5000);
        } finally {
            System.out.println("Thread " + Thread.currentThread().getName() + " left");
            semaphore.release();
        }
    }
}

In this example, we allow 2 threads to access the lock at the same time by setting Semaphore(2)

Similar to Mutex , we want to have the try {} finally {} here to make sure that the Semaphore is eventually released.

The above will results the below:

Thread pool-1-thread-5 entered
Thread pool-1-thread-2 entered
Thread pool-1-thread-3 entered
Thread pool-1-thread-1 entered
Thread pool-1-thread-4 entered
pool-1-thread-5 acquired semaphore: 0 -> 1
pool-1-thread-2 acquired semaphore: 1 -> 2
Thread pool-1-thread-2 left
Thread pool-1-thread-5 left
pool-1-thread-3 acquired semaphore: 2 -> 3
pool-1-thread-1 acquired semaphore: 3 -> 4
Thread pool-1-thread-1 left
Thread pool-1-thread-3 left
pool-1-thread-4 acquired semaphore: 4 -> 5
Thread pool-1-thread-4 left
Counter: 5

As we can see, we have at most 2 threads executing in the same time. As a result, we need to synchronise the counter to make sure that parallely access is thread-safe.