Many developers start learning concurrency by memorizing APIs directly.

For example:

  • How to create a Thread.
  • How to use Runnable.
  • How to add synchronized.
  • What is the difference between ReentrantLock and synchronized.
  • What is AQS.

However, this learning approach has a problem:

You may know how to use the APIs, but you do not understand why you need them in real business scenarios and how to choose the right solution.

Therefore, I believe the best way to learn concurrency is not starting from APIs.

Instead, we should start from problems.


1. Why Do We Need Concurrency?

Let's start with a question:

Why do programs need concurrency?

Assume we have an order system.

After a user submits an order, the system needs to perform several operations:

  1. Deduct inventory.
  2. Create the order.
  3. Process payment.
  4. Send notifications.

If everything is executed sequentially by a single thread, the workflow looks like this:

First:

Deduct inventory

Enter fullscreen mode Exit fullscreen mode

After it finishes:

Create order

Enter fullscreen mode Exit fullscreen mode

After it finishes:

Process payment

Enter fullscreen mode Exit fullscreen mode

After it finishes:

Send notification

Enter fullscreen mode Exit fullscreen mode

The total execution time of all steps becomes the response time of the entire request.

However, after analyzing the process carefully, we can find that some operations do not need to block the user.

For example:

  • Sending SMS notifications.
  • Recording operation logs.
  • Updating statistics.

These operations do not affect whether the order is successfully created.

Therefore, we can move them to background threads and execute them asynchronously.

This is the first value of concurrency:

Improving system response time


Besides asynchronous execution, there is another important reason:

Making better use of multi-core CPUs

Modern servers may have:

  • 8 CPU cores.
  • 16 CPU cores.
  • 32 CPU cores.

If an application only uses one thread, then effectively only one CPU core is doing the work.

If a task can be divided into multiple independent parts, multiple threads can execute them simultaneously.

Therefore, concurrency mainly solves two problems:

  1. Improve system responsiveness.
  2. Improve hardware resource utilization.

2. When Multiple Threads Run Together, What Problems Appear?

Many people think:

Multithreading simply means splitting tasks among multiple threads.

But things are not that simple.

The real challenge is:

What happens when multiple threads access shared data at the same time?

For example:

We have a variable:

int count = 0;

Enter fullscreen mode Exit fullscreen mode

100 threads execute:

count++;

Enter fullscreen mode Exit fullscreen mode

According to our expectation:

The final result should be:

100

Enter fullscreen mode Exit fullscreen mode

However, the actual result may be:

98
99

Enter fullscreen mode Exit fullscreen mode

or even lower.

Why?

Because:

count++ is not a single operation.

It actually contains three steps:

  1. Read count.
  2. Increment the value.
  3. Write the new value back.

Suppose two threads execute simultaneously:

Thread A reads:

count = 0

Enter fullscreen mode Exit fullscreen mode

Thread B also reads:

count = 0

Enter fullscreen mode Exit fullscreen mode

Then:

Thread A calculates:

0 + 1 = 1

Enter fullscreen mode Exit fullscreen mode

Thread B calculates:

0 + 1 = 1

Enter fullscreen mode Exit fullscreen mode

Finally, both write:

count = 1

Enter fullscreen mode Exit fullscreen mode

One update is lost.

This is the first core problem in concurrency:

Atomicity

Atomicity means:

An operation should either completely execute or not execute at all.

It should not be interrupted halfway by another thread.


3. Visibility Problem

Besides atomicity, there is another issue:

Visibility

What does visibility mean?

Suppose:

Thread A modifies a variable.

When will Thread B see this modification?

Many people assume:

Once Thread A changes the value, Thread B immediately sees it.

But this is not always true.

To improve performance, modern CPUs use caches.

Threads may read values from their own CPU cache.

Therefore:

Thread A has already updated the value.

But Thread B still sees the old value.

This is the visibility problem.


4. Ordering Problem

The third problem is:

Instruction Ordering

When we write code:

a = 1;
b = 2;

Enter fullscreen mode Exit fullscreen mode

We naturally assume execution happens from top to bottom.

However, for performance optimization:

  • The compiler.
  • The CPU.

may reorder instructions.

If there are no dependencies between instructions, usually there is no problem.

But when dependencies exist, reordering may cause unexpected behavior.

This is the ordering problem.


Therefore, Java concurrency has three fundamental problems:

The Three Core Problems

  1. Atomicity.
  2. Visibility.
  3. Ordering.

5. How Do We Solve Concurrency Problems?

After understanding the problems, let's look at solutions.


volatile

volatile mainly solves:

  • Visibility.
  • Ordering.

For example:

When one thread modifies a variable, other threads can immediately see the latest value.

However:

volatile does not guarantee atomicity.

For example:

count++;

Enter fullscreen mode Exit fullscreen mode

Even with volatile, this operation is still not thread-safe.


synchronized

synchronized is the most commonly used lock mechanism in Java.

It solves:

  • Atomicity.
  • Visibility.
  • Ordering.

The simple idea:

Only one thread can enter the synchronized section at the same time.

For example:

Multiple threads modify an account balance.

With locking:

Thread A
   |
   v
Update balance

Thread B waits

Thread B
   |
   v
Update balance

Enter fullscreen mode Exit fullscreen mode

Although performance is reduced slightly, data correctness is guaranteed.


ReentrantLock

ReentrantLock is another locking mechanism.

Compared with synchronized, it provides more advanced features:

  • Try acquiring a lock.
  • Timeout when waiting for a lock.
  • Interruptible lock acquisition.

However, another question appears:

When multiple threads compete for the same lock:

How do they wait and queue?

This leads us to:


AQS (AbstractQueuedSynchronizer)

AQS is the core foundation behind many Java synchronization tools.

It maintains a waiting queue.

Threads that fail to acquire a lock do not continuously consume CPU resources.

Instead:

They enter the waiting queue.

When the lock is released:

The next waiting thread is awakened.

This is the core design idea behind Java locks.


CAS (Compare And Swap)

Besides locks, another approach is:

CAS

The idea is:

Before modifying data:

First compare whether the current value is the expected value.

If yes:

Modify directly.

If no:

Someone else has modified the data.

Retry.

Java classes such as:

  • AtomicInteger
  • AtomicReference

use CAS internally.

Advantages:

  • High performance.
  • Lock-free design.

Disadvantages:

  • Under heavy contention, repeated retries may occur.

6. How Do Threads Cooperate?

After solving thread safety issues, another question appears:

How do multiple threads cooperate to complete tasks?

Thread cooperation mainly includes several scenarios.


Why Do Threads Need to Wait?

Example:

A consumer thread.

If there are no messages:

Should it continuously check?

Like:

while(true){
    checkMessage();
}

Enter fullscreen mode Exit fullscreen mode

This wastes CPU.

A better approach:

The thread enters a waiting state.

When messages arrive:

It gets awakened.

Related mechanisms:

sleep

Meaning:

"I will pause for a certain amount of time."


wait

Meaning:

"I am waiting for a specific condition."


park

A lower-level thread blocking mechanism.


How Do Threads Communicate?

A simple approach:

Shared variables.

For example:

A volatile variable.

One thread updates the state.

Another thread reads the state.

Another mechanism:

wait() and notify().

One thread waits.

Another thread sends notification.


In real-world development, concurrent utilities are more commonly used.

CountDownLatch

Scenario:

One thread waits for multiple threads to finish.

Example:

System startup:

  • Database initialization.
  • Cache initialization.
  • Message queue initialization.

Only after all complete:

The system starts.


CyclicBarrier

Scenario:

Multiple threads wait for each other.

Example:

A competition.

All participants finish preparation.

Then:

The competition starts.


Semaphore

Scenario:

Limit resource access.

Example:

Database connection pool.

Maximum:

100 connections

Enter fullscreen mode Exit fullscreen mode

Extra requests must wait.


7. How Do Multiple Threads Complete Complex Tasks?

Modern systems often require multiple asynchronous operations.

Example:

Product detail page.

Need:

  • Product information.
  • Inventory information.
  • Promotion information.
  • User information.

These can be queried in parallel.

Then combine results.

Technologies:

  • Callable.
  • Future.
  • FutureTask.
  • CompletableFuture.

CompletableFuture supports:

  • Parallel execution.
  • Task dependency.
  • Result combination.
  • Exception handling.

8. How Should Threads Be Stopped?

Threads eventually need to finish.

Previously Java provided:

Thread.stop()

Enter fullscreen mode Exit fullscreen mode

However, this approach is unsafe.

Because it may cause:

  • Locks not being released.
  • Data not being saved.

The recommended approach today:

interrupt()

Enter fullscreen mode Exit fullscreen mode

interrupt() does not forcibly kill a thread.

Instead:

It sends a stop signal.

The thread decides when and how to exit safely.

This is called:

Graceful Shutdown


9. Too Many Threads: What Happens?

If every request creates a new thread:

What problems appear?

Threads are not free.

Creating threads requires:

  • Memory.
  • CPU scheduling.
  • Context switching.

Too many threads may actually reduce performance.

Therefore:

We need:

Thread Pool

A thread pool is responsible for:

  • Creating threads.
  • Managing thread lifecycle.
  • Reusing threads.

The core class:

ThreadPoolExecutor

Enter fullscreen mode Exit fullscreen mode

It manages:

  • Core thread number.
  • Maximum thread number.
  • Task queue.
  • Rejection policy.

10. Concurrent Collections

Normal collections:

  • HashMap.
  • ArrayList.

are not thread-safe.

Java provides:

ConcurrentHashMap

It uses:

  • CAS.
  • Lock mechanisms.

to achieve high-performance concurrent access.


CopyOnWriteArrayList

Suitable for:

Read-heavy, write-light scenarios.


11. Why Do Threads Get Stuck?

The final question:

Why can threads stop making progress?

The classic problem:

Deadlock

Example:

Thread A:

Lock 1 acquired

Waiting for Lock 2

Enter fullscreen mode Exit fullscreen mode

Thread B:

Lock 2 acquired

Waiting for Lock 1

Enter fullscreen mode Exit fullscreen mode

Both wait forever.

Nobody can continue.

This is deadlock.


How to Avoid Deadlocks

Common approaches:

  1. Use consistent lock ordering.
  2. Reduce lock scope.
  3. Avoid nested locks.
  4. Use timeout-based lock acquisition.

Summary

Learning Java concurrency should not start from:

  • Thread.
  • Lock.
  • ConcurrentHashMap.

The correct learning path is:

Why do we need concurrency?
        ↓
How do threads execute?
        ↓
Why do multiple threads cause problems?
        ↓
Atomicity, Visibility, Ordering
        ↓
How to guarantee thread safety?
        ↓
volatile, synchronized, CAS, AQS
        ↓
How do threads wait and communicate?
        ↓
How do threads cooperate?
        ↓
How to stop threads safely?
        ↓
How thread pools manage execution?
        ↓
Concurrent collections and asynchronous programming

Enter fullscreen mode Exit fullscreen mode

The real purpose of concurrency programming is:

When multiple execution flows run simultaneously, how do we guarantee correctness while improving system performance?

This is the concurrency mindset that a senior Java engineer truly needs to understand.


GitHub:
github.com/markliu2013/...