Translated from: www.baeldung.com/java-thread…

1. An overview of the

Java supports multithreading. This means that the JVM can improve application performance by running bytecode concurrently with multiple threads.

While multithreading is a powerful feature, it comes at a cost. In a multithreaded environment, we need to write implementations in a thread-safe way.

This means that different threads can access the same resource without exposing erroneous behavior or producing unpredictable results.

This approach to programming is called “thread-safe.”

In this tutorial, we’ll examine different approaches to achieving this goal.

2. Stateless implementation

In most cases, errors in multithreaded applications are caused by incorrect shared state between multiple threads. Therefore, the first approach we will explore is to implement thread safety using statelessness.

To better understand this approach, let’s consider a simple utility class that has a static method for calculating factorial numbers:

public class MathUtils {
     
    public static BigInteger factorial(int number) {
        BigInteger f = new BigInteger("1");
        for (int i = 2; i <= number; i++) {
            f = f.multiply(BigInteger.valueOf(i));
        }
        returnf; }}Copy the code

The factorial() method is stateless and deterministic. Given a particular input, it always produces the same output.

This approach neither relies on external state nor maintains state. Therefore, it is considered thread-safe and can be safely invoked by multiple threads simultaneously. All threads can safely call the factorial() method without interfering with each other, and any thread can achieve the desired result.

Therefore, stateless implementation is the simplest way to achieve thread-safety.

Immutable implementation

If we need to share state between different threads, we can create thread-safe classes by making them immutable.

Immutability is a powerful, language-agnostic concept that is fairly easy to implement in Java. Simply put, a class instance is immutable when its internal state cannot be modified after construction.

The simplest way to create immutable classes in Java is to declare all fields private and final, without providing setters:

public class MessageService {
     
    private final String message;
 
    public MessageService(String message) {
        this.message = message;
    }
     
    // standard getter
     
}
Copy the code

The Messageservice object is virtually immutable because its state cannot be changed after construction. Therefore, it is thread-safe.

In addition, a MessageService is thread-safe if it is actually mutable, but can only be accessed read-only by multiple threads.

Therefore, immutability is another way to achieve thread-safety.

4. Thread local variables

In object-oriented programming, objects actually need to maintain state through fields and implement behavior through one or more methods.

If we actually need to maintain state, we can create thread-safe classes by localizing their field threads so that these classes do not share state between threads.

We can easily create fields that are thread-local classes by simply defining private fields in the Thread class.

For example, we could define a Thread class to store an array of integers:

public class ThreadA extends Thread {
     
    private final List<Integer> numbers = Arrays.asList(1.2.3.4.5.6);
     
    @Override
    public void run(a) { numbers.forEach(System.out::println); }}Copy the code

And another might contain an array of strings:

public class ThreadB extends Thread {
     
    private final List<String> letters = Arrays.asList("a"."b"."c"."d"."e"."f");
     
    @Override
    public void run(a) { letters.forEach(System.out::println); }}Copy the code

In both implementations, the class has its own state, but does not share it with other threads. Therefore, classes are thread-safe.

Similarly, we can create thread-local fields by assigning ThreadLocal instances to fields.

Let’s consider, for example, the following StateHolder class:

public class StateHolder {
     
    private final String state;
 
    // standard constructors / getter
}

Copy the code

We can easily make it a thread-local variable, as follows:

public class ThreadState {
     
    public static final ThreadLocal<StateHolder> statePerThread = new ThreadLocal<StateHolder>() {
         
        @Override
        protected StateHolder initialValue(a) {
            return new StateHolder("active"); }};public static StateHolder getState(a) {
        returnstatePerThread.get(); }}Copy the code

Thread-local fields are very similar to normal class fields, except that each thread accesses their independently initialized copy of the field through the setter/getter, so that each thread has its own state.

5. Synchronize collections

We can easily create thread-safe collections by using a set of synchronization wrappers included in the collections framework.

For example, we could use one of these synchronization wrappers to create thread-safe collections:

Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>());
Thread thread1 = new Thread(() -> syncCollection.addAll(Arrays.asList(1.2.3.4.5.6)));
Thread thread2 = new Thread(() -> syncCollection.addAll(Arrays.asList(7.8.9.10.11.12)));
thread1.start();
thread2.start();
Copy the code

Let’s remember that synchronized collections use internal locking in each method (we’ll discuss internal locking later). This means that a method can only be accessed by one thread at a time, and other threads will block until the method is unlocked by the first thread. Therefore, synchronization performance suffers because of the underlying logic of synchronous access.

6. Concurrent collections

In addition to synchronous collections, we can also use concurrent collections to create thread-safe collections.

Java provides the java.util.concurrent package, which contains several concurrent collections, such as ConcurrentHashMap:

Map<String,String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("1"."one");
concurrentMap.put("2"."two");
concurrentMap.put("3"."three");
Copy the code

Unlike synchronous collections, concurrent collections achieve thread-safety by dividing data into segments. For example, in ConcurrentHashMap, multiple threads can acquire locks on different segments, so multiple threads can access them simultaneously.

Because of the inherent advantages of concurrent thread access, concurrent collections have higher performance than synchronous collections. It is worth noting that synchronizing and concurrent collections only make the collections themselves thread-safe, not the content.

7. Atomic objects

You can also implement thread-safety using a set of atomic classes provided by Java, including AtomicInteger, AtomicLong, AtomicBoolean, and AtomicReference.

Atomic classes allow us to perform thread-safe atomic operations without using synchronization. Atomic operations are performed in a single machine-level operation. To understand this, let’s look at the following Counter class:

public class Counter {
     
    private int counter = 0;
     
    public void incrementCounter(a) {
        counter += 1;
    }
     
    public int getCounter(a) {
        returncounter; }}Copy the code

Assume that in a race condition, two threads access the incrementCounter() method simultaneously. Theoretically, the final value of the counter field is 2. But we’re just not sure of the result, because threads execute the same code block at the same time, and increments are not atomic.

Let’s use the AtomicInteger object to create a thread-safe implementation of the Counter class:

public class AtomicCounter {
     
    private final AtomicInteger counter = new AtomicInteger();
     
    public void incrementCounter(a) {
        counter.incrementAndGet();
    }
     
    public int getCounter(a) {
        returncounter.get(); }}Copy the code

This is thread-safe because although incrementAndGet, ++, requires multiple operations, it is atomic.

8. Synchronization method

While the earlier approach worked well for collections and primitives, we sometimes need more control.

Therefore, another common approach that we can use to achieve thread-safety is to implement synchronous methods.

Simply put, only one thread can access a synchronized method at a time, while preventing other threads from accessing the method. The other threads will remain blocked until the first thread completes or the method throws an exception. We can create a thread-safe version of incrementCounter () in another way by making it a synchronous method:

public synchronized void incrementCounter(a) {
    counter += 1;
}
Copy the code

We create a synchronized method by prefixing the method signature with the synchronized keyword. Because one thread can access one synchronous method at a time, one thread will execute the incrementCounter () method, and the other threads will do the same. No overlapping execution will occur.

The synchronous method depends on the use of an “internal lock” or a “monitor lock”. An internal lock is an implicit internal entity associated with a particular class instance. When a thread calls a synchronous method, it acquires the internal lock. After the thread completes the execution of the method, it releases the lock, allowing other threads to acquire the lock and access the method.

We can implement synchronization in instance methods, static methods, and statements (synchronous statements).

9. Synchronize statements

Sometimes, if we only need to make one part of a method thread-safe, synchronization of the entire method may not be necessary.

To illustrate this use case, let’s refactor the incrementCounter () method:

public void incrementCounter(a) {
    // additional unsynced operations
    synchronized(this) {
        counter += 1; }}Copy the code

This example is simple, but it shows how to create a synchronous statement.

Assuming that the method now performs some additional operations that do not require synchronization, we can achieve synchronization simply by wrapping the relevant state modification part in a synchronized block. Unlike synchronized methods, synchronized statements must specify the object that provides the internal lock, usually the this reference.

Synchronization is expensive, so sometimes only the relevant parts need to be synchronized.

10. Volatile

Synchronized methods and blocks are handy for solving the problem of variable visibility between threads. Even so, the values of regular class fields can be cached by the CPU. Therefore, subsequent updates to a particular field, even if they are synchronous, may not be visible to other threads.

To avoid this, we can use volatile:

public class Counter {
 
    private volatile int counter;
 
    // standard constructors / getter
     
}
Copy the code

Using the volatile keyword, we instruct the JVM and compiler to store counter variables in main memory. This way, we can be sure that every time the JVM reads the value of a counter variable, it will actually read it from main memory rather than the CPU cache. Similarly, each time the JVM writes a counter variable, the value is written to main memory.

In addition, the use of Volatile variables ensures that all variables visible to a given thread will also be read from main memory. Let’s consider the following example:

public class User {
 
    private String name;
    private volatile int age;
 
    // standard constructors / getters
     
}
Copy the code

In this case, every time the JVM writes the Volatile variable age to main memory, it also writes the non-volatile variable name to main memory. This ensures that the most recent values of both variables are stored in main memory, so subsequent updates to variables are automatically visible to other threads. Similarly, if a thread reads the value of a Volatile variable, all variables visible to the thread are also read from main memory. This extended guarantee that Volatile variables provide is called the guarantee of complete Volatile visibility.

11. External lock

We can improve the thread-safety implementation of the Counter class slightly by using an external monitor lock instead of an internal monitor.

An external lock also provides coordinated access to a shared resource in a multithreaded environment, but it uses an external entity to enforce exclusive access to the resource:

public class ExtrinsicLockCounter {
 
    private int counter = 0;
    private final Object lock = new Object();
     
    public void incrementCounter(a) {
        synchronized(lock) {
            counter += 1; }}// standard getter
     
}
Copy the code

We use a plain Object instance to create the external lock. This implementation is slightly better because it improves security at the lock level.

With internal locking, synchronized methods and synchronized blocks depend on this reference, and an attacker can cause a deadlock by acquiring the internal lock and triggering a distributed denial of service attack state (DoS) condition.

Unlike internal locks, as opposed to external locks, external locks use a private entity, which cannot be accessed externally. This makes it more difficult for an attacker to obtain the lock to cause a deadlock.

12. Reentrant Locks

Java provides a set of improved Lock implementations with slightly more complex behavior than the internal locks discussed above.

For internal locks, the lock acquisition model is fairly strict: one thread acquires the lock, then executes a method or block of code, and finally releases the lock so that other threads can acquire and access the method.

There is no underlying mechanism to check for queued threads and give priority access to the longest waiting thread.

The Reentrantlock instance allows us to do just that, thus preventing queued threads from suffering certain types of resource exhaustion:

public class ReentrantLockCounter {
 
    private int counter;
    private final ReentrantLock reLock = new ReentrantLock(true);
     
    public void incrementCounter(a) {
        reLock.lock();
        try {
            counter += 1;
        } finally{ reLock.unlock(); }}// standard constructors / getter
     
}
Copy the code

The Reentrantlock constructor takes an optional Boolean argument. When the parameter is set to true, and multiple threads attempt to acquire the lock, the JVM gives priority to the thread that has waited the longest and grants access to the lock.

Read/write locks

Another powerful mechanism we can use to achieve thread-safety is the use of the ReadWriteLock implementation.

The Readwritelock lock actually uses a pair of associated locks, one for read-only operations and one for write operations.

Therefore, as long as no thread writes to the resource, many threads can read the resource, otherwise other threads will be prevented from reading the resource.

We can use the following ReadWriteLock lock:

public class ReentrantReadWriteLockCounter {
     
    private int counter;
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
     
    public void incrementCounter(a) {
        writeLock.lock();
        try {
            counter += 1;
        } finally{ writeLock.unlock(); }}public int getCounter(a) {
        readLock.lock();
        try {
            return counter;
        } finally{ readLock.unlock(); }}// standard constructors
    
}
Copy the code

Conclusion 14.

In this article, we learned about thread safety in Java and delved into different ways to implement it.