eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Partner – LambdaTest – NPI EA (cat=Testing)
announcement - icon

Regression testing is an important step in the release process, to ensure that new code doesn't break the existing functionality. As the codebase evolves, we want to run these tests frequently to help catch any issues early on.

The best way to ensure these tests run frequently on an automated basis is, of course, to include them in the CI/CD pipeline. This way, the regression tests will execute automatically whenever we commit code to the repository.

In this tutorial, we'll see how to create regression tests using Selenium, and then include them in our pipeline using GitHub Actions:, to be run on the LambdaTest cloud grid:

>> How to Run Selenium Regression Tests With GitHub Actions

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

eBook – Java Concurrency – NPI (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

1. Introduction

In this tutorial, we’re going to see some of the most common concurrency problems in Java. We’ll also learn how to avoid them and their main causes.

2. Using Thread-Safe Objects

2.1. Sharing Objects

Threads communicate primarily by sharing access to the same objects. So, reading from an object while it changes can give unexpected results. Also, concurrently changing an object can leave it in a corrupted or inconsistent state.

The main way we can avoid such concurrency issues and build reliable code is to work with immutable objects. This is because their state cannot be modified by the interference of multiple threads.

However, we can’t always work with immutable objects. In these cases, we have to find ways to make our mutable objects thread-safe.

2.2. Making Collections Thread-Safe

Like any other object, collections maintain state internally. This could be altered by multiple threads changing the collection concurrently. So, one way we can safely work with collections in a multithreaded environment is to synchronize them:

Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
List<Integer> list = Collections.synchronizedList(new ArrayList<>());

In general, synchronization helps us to achieve mutual exclusion. More specifically, these collections can be accessed by only one thread at a time. Thus, we can avoid leaving collections in an inconsistent state.

2.3. Specialist Multithreaded Collections

Now let’s consider a scenario where we need more reads than writes. By using a synchronized collection, our application can suffer major performance consequences. If two threads want to read the collection at the same time, one has to wait until the other finishes.

For this reason, Java provides concurrent collections such as CopyOnWriteArrayList and ConcurrentHashMap that can be accessed simultaneously by multiple threads:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
Map<String, String> map = new ConcurrentHashMap<>();

The CopyOnWriteArrayList achieves thread-safety by creating a separate copy of the underlying array for mutative operations like add or remove. Although it has a poorer performance for write operations than a Collections.synchronizedList, it provides us with better performance when we need significantly more reads than writes.

ConcurrentHashMap is fundamentally thread-safe and is more performant than the Collections.synchronizedMap wrapper around a non-thread-safe Map. It’s actually a thread-safe map of thread-safe maps, allowing different activities to happen simultaneously in its child maps.

2.4. Working with Non-Thread-Safe Types

We often use built-in objects like SimpleDateFormat to parse and format date objects. The SimpleDateFormat class mutates its internal state while doing its operations.

We need to be very careful with them because they are not thread-safe. Their state can become inconsistent in a multithreaded application due to things like race conditions.

So, how can we use the SimpleDateFormat safely? We have several options:

  • Create a new instance of SimpleDateFormat every time it’s used
  • Restrict the number of objects created by using a ThreadLocal<SimpleDateFormat> object. It guarantees that each thread will have its own instance of SimpleDateFormat
  • Synchronize concurrent access by multiple threads with the synchronized keyword or a lock

SimpleDateFormat is just one example of this. We can use these techniques with any non-thread-safe type.

3. Race Conditions

A race condition occurs when two or more threads access shared data and they try to change it at the same time. Thus, race conditions can cause runtime errors or unexpected outcomes.

3.1. Race Condition Example

Let’s consider the following code:

class Counter {
    private int counter = 0;

    public void increment() {
        counter++;
    }

    public int getValue() {
        return counter;
    }
}

The Counter class is designed so that each invocation of the increment method will add 1 to the counter. However, if a Counter object is referenced from multiple threads, the interference between threads may prevent this from happening as expected.

We can decompose the counter++ statement into 3 steps:

  • Retrieve the current value of counter
  • Increment the retrieved value by 1
  • Store the incremented value back in counter

Now, let’s suppose two threads, thread1 and thread2, invoke the increment method at the same time. Their interleaved actions might follow this sequence:

  • thread1 reads the current value of counter; 0
  • thread2 reads the current value of counter; 0
  • thread1 increments the retrieved value; the result is 1
  • thread2 increments the retrieved value; the result is 1
  • thread1 stores the result in counter; the result is now 1
  • thread2 stores the result in counter; the result is now 1

We expected the value of the counter to be 2, but it was 1.

3.2. A Synchronized-Based Solution

We can fix the inconsistency by synchronizing the critical code:

class SynchronizedCounter {
    private int counter = 0;

    public synchronized void increment() {
        counter++;
    }

    public synchronized int getValue() {
        return counter;
    }
}

Only one thread is allowed to use the synchronized methods of an object at any one time, so this forces consistency in the reading and writing of the counter.

3.3. A Built-In Solution

We can replace the above code with a built-in AtomicInteger object. This class offers, among others, atomic methods for incrementing an integer and is a better solution than writing our own code. Therefore, we can call its methods directly without the need for synchronization:

AtomicInteger atomicInteger = new AtomicInteger(3);
atomicInteger.incrementAndGet();

In this case, the SDK solves the problem for us. Otherwise, we could’ve also written our own code, encapsulating the critical sections in a custom thread-safe class. This approach helps us to minimize the complexity and to maximize the reusability of our code.

4. Race Conditions Around Collections

4.1. The Problem

Another pitfall we can fall into is to think that synchronized collections offer us more protection than they actually do.

Let’s examine the code below:

List<String> list = Collections.synchronizedList(new ArrayList<>());
if(!list.contains("foo")) {
    list.add("foo");
}

Every operation of our list is synchronized, but any combinations of multiple method invocations are not synchronized. More specifically, between the two operations, another thread can modify our collection leading to undesired results.

For example, two threads could enter the if block at the same time and then update the list, each thread adding the foo value to the list.

4.2. A Solution for Lists

We can protect the code from being accessed by more than one thread at a time using synchronization:

synchronized (list) {
    if (!list.contains("foo")) {
        list.add("foo");
    }
}

Rather than adding the synchronized keyword to the functions, we’ve created a critical section concerning list, which only allows one thread at a time to perform this operation.

We should note that we can use synchronized(list) on other operations on our list object, to provide a guarantee that only one thread at a time can perform any of our operations on this object.

4.3. A Built-In Solution for ConcurrentHashMap

Now, let’s consider using a map for the same reason, namely adding an entry only if it’s not present.

The ConcurrentHashMap offers a better solution for this type of problem. We can use its atomic putIfAbsent method:

Map<String, String> map = new ConcurrentHashMap<>();
map.putIfAbsent("foo", "bar");

Or, if we want to compute the value, its atomic computeIfAbsent method:

map.computeIfAbsent("foo", key -> key + "bar");

We should note that these methods are part of the interface to Map where they offer a convenient way to avoid writing conditional logic around insertion. They really help us out when trying to make multi-threaded calls atomic.

5. Memory Consistency Issues

Memory consistency issues occur when multiple threads have inconsistent views of what should be the same data.

In addition to the main memory, most modern computer architectures are using a hierarchy of caches (L1, L2, and L3 caches) to improve the overall performance. Thus, any thread may cache variables because it provides faster access compared to the main memory.

5.1. The Problem

Let’s recall our Counter example:

class Counter {
    private int counter = 0;

    public void increment() {
        counter++;
    }

    public int getValue() {
        return counter;
    }
}

Let’s consider the scenario where thread1 increments the counter and then thread2 reads its value. The following sequence of events might happen:

  • thread1 reads the counter value from its own cache; counter is 0
  • thread1 increments the counter and writes it back to its own cache; counter is 1
  • thread2 reads the counter value from its own cache; counter is 0

Of course, the expected sequence of events could happen too and the thread2 will read the correct value (1), but there is no guarantee that changes made by one thread will be visible to other threads every time.

5.2. The Solution

In order to avoid memory consistency errors, we need to establish a happens-before relationship. This relationship is simply a guarantee that memory updates by one specific statement are visible to another specific statement.

There are several strategies that create happens-before relationships. One of them is synchronization, which we’ve already looked at.

Synchronization ensures both mutual exclusion and memory consistency. However, this comes with a performance cost.

We can also avoid memory consistency problems by using the volatile keyword. Simply put, every change to a volatile variable is always visible to other threads.

Let’s rewrite our Counter example using volatile:

class SyncronizedCounter {
    private volatile int counter = 0;

    public synchronized void increment() {
        counter++;
    }

    public int getValue() {
        return counter;
    }
}

We should note that we still need to synchronize the increment operation because volatile doesn’t ensure us mutual exclusion. Using simple atomic variable access is more efficient than accessing these variables through synchronized code.

5.3. Non-Atomic long and double Values

So, if we read a variable without proper synchronization, we may see a stale value. For long and double values, quite surprisingly, it’s even possible to see completely random values in addition to stale ones.

According to JLS-17, JVM may treat 64-bit operations as two separate 32-bit operations. Therefore, when reading a long or double value, it’s possible to read an updated 32-bit along with a stale 32-bit. Consequently, we may observe random-looking long or double values in concurrent contexts.

On the other hand, writes and reads of volatile long and double values are always atomic.

6. Misusing Synchronize

The synchronization mechanism is a powerful tool to achieve thread-safety. It relies on the use of intrinsic and extrinsic locks. Let’s also remember the fact that every object has a different lock and only one thread can acquire a lock at a time.

However, if we don’t pay attention and carefully choose the right locks for our critical code, unexpected behavior can occur.

6.1. Synchronizing on this Reference

The method-level synchronization comes as a solution to many concurrency issues. However, it can also lead to other concurrency issues if it’s overused. This synchronization approach relies on the this reference as a lock, which is also called an intrinsic lock.

We can see in the following examples how a method-level synchronization can be translated into a block-level synchronization with the this reference as a lock.

These methods are equivalent:

public synchronized void foo() {
    //...
}
public void foo() {
    synchronized(this) {
      //...
    }
}

When such a method is called by a thread, other threads cannot concurrently access the object. This can reduce concurrency performance as everything ends up running single-threaded. This approach is especially bad when an object is read more often than it is updated.

Moreover, a client of our code might also acquire the this lock. In the worst-case scenario, this operation can lead to a deadlock.

6.2. Deadlock

Deadlock describes a situation where two or more threads block each other, each waiting to acquire a resource held by some other thread.

Let’s consider the example:

public class DeadlockExample {

    public static Object lock1 = new Object();
    public static Object lock2 = new Object();

    public static void main(String args[]) {
        Thread threadA = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("ThreadA: Holding lock 1...");
                sleep();
                System.out.println("ThreadA: Waiting for lock 2...");

                synchronized (lock2) {
                    System.out.println("ThreadA: Holding lock 1 & 2...");
                }
            }
        });
        Thread threadB = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("ThreadB: Holding lock 2...");
                sleep();
                System.out.println("ThreadB: Waiting for lock 1...");

                synchronized (lock1) {
                    System.out.println("ThreadB: Holding lock 1 & 2...");
                }
            }
        });
        threadA.start();
        threadB.start();
    }
}

In the above code we can clearly see that first threadA acquires lock1 and threadB acquires lock2. Then, threadA tries to get the lock2 which is already acquired by threadB and threadB tries to get the lock1 which is already acquired by threadA. So, neither of them will proceed meaning they are in a deadlock.

We can easily fix this issue by changing the order of locks in one of the threads.

We should note that this is just one example, and there are many others that can lead to a deadlock.

7. Conclusion

In this article, we explored several examples of concurrency issues that we’re likely to encounter in our multithreaded applications.

First, we learned that we should opt for objects or operations that are either immutable or thread-safe.

Then, we saw several examples of race conditions and how we can avoid them using the synchronization mechanism. Furthermore, we learned about memory-related race conditions and how to avoid them.

Although the synchronization mechanism helps us to avoid many concurrency issues, we can easily misuse it and create other issues. For this reason, we examined several problems we might face when this mechanism is badly used.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook – Java Concurrency – NPI (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook Jackson – NPI EA – 3 (cat = Jackson)