← Back to Multithreading Course

PART 3 · CLASS 18

Atomic Variables and AtomicInteger

We have learned about race conditions, synchronization and the volatile keyword. Now let's understand another useful way to safely update shared data in Java.

1. A Quick Look at What We Have Learned

Imagine two threads are updating the same counter. Each thread wants to increase the counter by one.

We learned that this simple operation:

count++;

is not a single indivisible operation. It involves reading the current value, increasing it and writing the new value. Two threads can interfere with each other and produce an incorrect result.

We also learned about the volatile keyword in our previous class.

Volatile helps threads see the latest value of a shared variable. However, it does not make operations like count++ atomic.

So, how can we safely increase a shared counter without manually writing a synchronized block?

2. What Are Atomic Variables?

Java provides a set of classes called atomic classes.

These classes help us perform certain operations on shared variables safely when multiple threads are working with them.

An atomic operation happens as one indivisible operation from the perspective of other threads.

In simple words, when an operation is atomic, another thread cannot interfere halfway through that operation.

Java provides different atomic classes for different types of values. One commonly used class is AtomicInteger.

3. Introduction to AtomicInteger

AtomicInteger is a Java class that allows us to perform thread-safe operations on an integer value.

It belongs to the java.util.concurrent.atomic package.

Let's see how we can create an AtomicInteger.

import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    public static void main(String[] args) {

        AtomicInteger count = new AtomicInteger(0);

        System.out.println(count.get());
    }
}

Here, we created an AtomicInteger with an initial value of 0.

Notice that we use get() to read its current value.

4. Incrementing an AtomicInteger

With a normal integer, we use count++ to increase its value.

With AtomicInteger, we can use a method called incrementAndGet().

import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    public static void main(String[] args) {

        AtomicInteger count = new AtomicInteger(0);

        count.incrementAndGet();

        System.out.println(count.get());
    }
}

The output will be:

1

The incrementAndGet() method safely increases the value by one and returns the updated value.

We can call it multiple times:

count.incrementAndGet();
count.incrementAndGet();
count.incrementAndGet();

System.out.println(count.get());

The output will be 3.

There is also a method called getAndIncrement(). It increases the value by one but returns the old value instead of the updated value.

For now, remember incrementAndGet() as the method we can use when we want to safely increment an AtomicInteger.

5. Let's Solve Our Counter Problem

Remember our earlier example where two threads were incrementing the same counter?

Let's solve that problem using AtomicInteger.

import java.util.concurrent.atomic.AtomicInteger;

class Counter {

    private final AtomicInteger count = new AtomicInteger(0);

    void increment() {
        count.incrementAndGet();
    }

    int getCount() {
        return count.get();
    }
}

public class Main {

    public static void main(String[] args)
            throws InterruptedException {

        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 100000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.getCount());
    }
}

Let's understand what is happening here.

  • We created one shared Counter object.
  • Inside it, we created an AtomicInteger starting at zero.
  • Both threads use the same Counter object.
  • Each thread increments the counter 100,000 times.
  • We use join() to wait for both threads to finish.
  • Finally, we print the counter value.

The expected output is:

200000

Both threads can safely call incrementAndGet(). The atomic operation prevents the lost updates that we encountered with an ordinary count++.

6. AtomicInteger vs. volatile vs. synchronized

We have now learned three useful ways of dealing with shared data. Let's quickly compare them.

volatile

Helps with visibility and ordering of changes to a shared variable. However, it does not make compound operations like count++ atomic.

synchronized

Allows us to protect a section of code using a lock. Only one thread at a time can execute code protected by the same lock.

AtomicInteger

Provides thread-safe atomic operations on an integer. For example, we can safely increment a shared counter using incrementAndGet() without writing an explicit synchronized block.

One important thing to remember: AtomicInteger makes its individual atomic operations safe. It does not automatically make an entire sequence of multiple operations thread-safe.

Test Your Understanding

Let's see how well you understood this class!

Question 1: What is the main purpose of AtomicInteger?

Question 2: Which package contains AtomicInteger?

Question 3: What does incrementAndGet() do?

Question 4: What would be the expected result if two threads each increment an AtomicInteger 100,000 times?

Question 5: Does AtomicInteger automatically make an entire multi-step workflow thread-safe?

Class Summary

Let's quickly revise what we learned today:

  • Atomic variables help us perform certain shared-data operations safely.
  • AtomicInteger is used for thread-safe operations on integer values.
  • get() reads the current value.
  • incrementAndGet() safely increments and returns the updated value.
  • AtomicInteger helps prevent lost updates when multiple threads increment a shared counter.
  • AtomicInteger does not automatically protect an entire multi-step workflow.

Remember: choose the right tool for the problem. Sometimes AtomicInteger is enough. Sometimes we need synchronized blocks or other coordination mechanisms.

🎉 Congratulations! You Have Completed Part 3!

We started this part by understanding how threads can share data. We then explored race conditions, synchronization, locks, volatile and atomic variables.

These concepts are extremely important because real-world applications often have multiple threads accessing shared data at the same time.

Take a moment to appreciate your progress. You have completed another important step in understanding Java multithreading!

COMING UP NEXT · PART 4

Communication and Coordination Between Threads

Threads sometimes need to work together. One thread may have to wait for another, or several threads may need to coordinate their work. Let's understand how Java helps them do this.

What We Will Learn

  1. Communication with wait(), notify() and notifyAll()
  2. sleep() vs. wait()
  3. Understanding the Producer–Consumer Problem
  4. Producer–Consumer Using BlockingQueue
  5. Coordinating Threads with CountDownLatch
  6. Coordinating Threads with CyclicBarrier
  7. Limiting Access with Semaphore

We will learn how threads communicate, wait for one another and coordinate their work using Java's concurrency tools.

0% Completed

You have completed 18 out of 46 classes.