Chapter 02 - Thread Safety

What is threadsafe

A class is threadsafe when there is no state. For example

@ThreadSafe
public class Service {
    public void printSomething() {
        System.out.println("Hello world");
    }
}

Given the following code

@NonThreadSafe
public class Service {
    public int count = 0;
    
    public void getUniqueCount() {
        count += 1;
        System.out.println("Count is: " + count);
    }
}

Given this code, if the service class is executed in multiple thread, this would lead to a problem that each thread can have a different value of count.

Atomicity

The idea of atomicity is, action would be execute at once or not executing at all. This is good for the previous example since we need to make sure that it's executed correctly.

For the previous example, we can do something like

@ThreadSafe
public class Service {
    public AtomicInteger count = new AtomicInteger();
    
    public void getUniqueCount() {
        int current = count.incrementAndGet();
        System.out.println("Count is: " + current);
    }
}

Given this code, the incrementAndGet in background also do a compare and set (CAS) which compare if the value is the current value (not changed yet, and then set it to the next value). If the operation fail, it will re-attempt again

It do so by using VarHandle which inturn execute low-level CPU instruction to do CAS.

Locking

However, atomicity will not work if we need to use more variables. For example:

@ThreadSafe
public class Service {
    public AtomicInteger male = new AtomicInteger();
    public AtomicInteger female = new AtomicInteger();
    
    public void getUniqueCount(bool isMale) {
        if (isMale) {
            System.out.println("Total is": male.incrementAndGet() + female.get());
        } else {
            System.out.println("Total is": male.get() + female.incrementAndGet());
        }
    }
}

The reason is the incrementAndGet is only threadsafe as a single atomic operation. When combining with other operation it's not threadsafe.

The solution is to use Locking

Intrinsic locks

Related Intrinsic lock. Only one item get locked at the same time.

In java we can use

synchronized (lock) {

}

We also have a shortcut to use with method

class Someclass {
    private synchronized void method() {
        // Will synchronized(this) on `this` object
    }
}

[!danger]
So as a result, one class can only execute one synchronized method at the time if we use something like this Synchronised method only execute once a class

Example and optimise

Consider the following scenarios

@ThreadSafe  
public class TicketService {  
    @GuardedBy("this") private int ticketCount = 100;  
    private final Logger logger = Logger.getLogger(TicketService.class.getName());  
  
    public synchronized boolean claimTicket(Request request) throws InterruptedException {  
        this.initialiseUserRequest(request);  
        if (ticketCount == 0) return false;
        
        ticketCount -= 1;  
        return true;  
    }  
  
    private void initialiseUserRequest(Request request) throws InterruptedException {  
        // Simulate some initialisation logic for the user request  
        logger.info("Initialising request for " + request);  
        sleep(1000); // Simulate delay  
    }  
}

This is works but bad performance. Since everyone needs to wait to claim the request.

Instead, we should only synchronise the needed part:

@ThreadSafe  
public class TicketService {  
    @GuardedBy("this") private int ticketCount = 100;  
    private final Logger logger = Logger.getLogger(TicketService.class.getName());  
  
    public boolean claimTicket(Request request) throws InterruptedException {  
        this.initialiseUserRequest(request);  
        synchronized (this) {  
            if (ticketCount == 0) {  
                return false;  
            }  
            ticketCount -= 1;  
            return true;  
        }  
    }  
  
    private void initialiseUserRequest(Request request) throws InterruptedException {  
        // Simulate some initialisation logic for the user request  
        logger.info("Initialising request for " + request);  
        sleep(1000); // Simulate delay  
    }  
}

As a result, we can increase the performance of initialiseUserRequest. Note that since we're already using synchronized here, there is no need to put ticketCount as AtomicInteger

Decisions of how big or small synchronisation block might require some trade off.

[!danger]
Holding the lock for long time might cause performance issue and liveness problems. Avoid doing so