42 Differences Between Atomic Integer and Synchronized

42 Differences Between AtomicInteger and synchronized #

In the previous lesson, we explained that both atomic classes and the synchronized keyword can be used to ensure thread safety. In this lesson, we will first use atomic classes and the synchronized keyword separately to solve a classic thread safety problem, provide specific code comparisons, and then analyze the differences between them.

Code Comparison #

First, let’s look at the code for the original thread-unsafe scenario:

public class Lesson42 implements Runnable {

    static int value = 0;

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

        Runnable runnable = new Lesson42();

        Thread thread1 = new Thread(runnable);

        Thread thread2 = new Thread(runnable);

        thread1.start();

        thread2.start();

        thread1.join();

        thread2.join();

        System.out.println(value);

    }

    @Override

    public void run() {

        for (int i = 0; i < 10000; i++) {

            value++;

        }

    }

}

In the code, we create a variable named value and perform simultaneous increment operations on it in two threads. Each thread increments it 10,000 times. We then use join to ensure that they both complete execution before printing the final value.

Because value++ is not an atomic operation, the above code is not thread-safe (see Lecture 6 for a detailed analysis), so the result of the code execution will be less than 20,000, for example, it may output various numbers like 14,611.

Let’s first provide Method 1, which uses an atomic class to solve this problem. The code is as follows:

public class Lesson42Atomic implements Runnable {

    static AtomicInteger atomicInteger = new AtomicInteger();

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

        Runnable runnable = new Lesson42Atomic();

        Thread thread1 = new Thread(runnable);

        Thread thread2 = new Thread(runnable);

        thread1.start();

        thread2.start();

        thread1.join();

        thread2.join();

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

    }

    @Override

    public void run() {

        for (int i = 0; i < 10000; i++) {

            atomicInteger.incrementAndGet();

        }
        ...

}

}

Using atomic classes, our counting variable is no longer a regular int variable, but an object of the AtomicInteger type, and the self-increment operation becomes incrementAndGet method. Because atomic classes can ensure that each self-increase operation is atomic, this program is thread-safe, so the running result of the above program will always be equal to 20000.

Next, we present Method Two, where we use synchronized to solve this problem. The code is as follows:

public class Lesson42Syn implements Runnable {

    static int value = 0;

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

        Runnable runnable = new Lesson42Syn();

        Thread thread1 = new Thread(runnable);

        Thread thread2 = new Thread(runnable);

        thread1.start();

        thread2.start();

        thread1.join();

        thread2.join();

        System.out.println(value);

    }

    @Override

    public void run() {

        for (int i = 0; i < 10000; i++) {

            synchronized (this) {

                value++;

            }

        }

    }

}

The difference between it and the initially thread-unsafe code is that a synchronized block is added in the run method, which can easily solve this problem. Because synchronized can ensure the atomicity of the code block, the running result of the above program will always be equal to 20000, making it thread-safe.

Comparison of Solutions #

Next, let’s analyze the differences between these two different solutions.

First, let’s look at the difference in principles behind them.

In Lesson 21, we analyzed the principle behind synchronized, which is the monitor lock, the principle behind synchronized. Although there are some differences in the principles behind synchronized methods and synchronized blocks, the overall idea is the same: before executing synchronization code, the monitor lock needs to be obtained, and after execution, the lock is released.

In Lesson 39, we introduced atomic classes, which ensure thread safety using CAS operations. From this perspective, although atomic classes and synchronized can both ensure thread safety, their implementation principles are quite different.

The second difference is the difference in usage scope.

For atomic classes, their usage scope is relatively limited. Because an atomic class is just an object, it is not flexible enough. On the other hand, synchronized has a much wider usage scope. For example, synchronized can be used to modify a method or a block of code, allowing us to control its application scope very flexibly based on our needs.

Therefore, atomic classes can be used in scenarios with a small number of variables, such as counters. For other scenarios where atomic classes are not applicable, we can consider using synchronized to solve the problem.

The third difference is the difference in granularity.

The granularity of atomic variables is relatively small, as it can narrow the competition scope to the variable level. In most cases, the granularity of synchronized locks is greater than that of atomic variables. If we only protect a line of code with synchronized, it feels like using a sledgehammer to crack a nut.

The fourth point is the difference in performance, but also the difference between pessimistic locks and optimistic locks.

Because synchronized is a typical pessimistic lock, while atomic classes are the opposite, making use of optimistic locks. Therefore, when comparing synchronized and AtomicInteger, we are actually comparing the difference between pessimistic locks and optimistic locks.

From a performance perspective, pessimistic locks are relatively heavyweight. In highly competitive situations, synchronized will block threads that cannot obtain the lock, while atomic classes will never block threads. However, although synchronized blocks threads, it does not mean that its performance is worse than atomic classes.

Because the overhead of pessimistic locks is fixed and one-time. With the increase of time, this overhead does not increase linearly.

On the other hand, although the overhead of optimistic locks is small in the short term, it gradually increases with time.

So from a performance standpoint, they don’t have a direct comparison of which is better or worse, but rather depend on specific usage scenarios. In highly competitive situations, synchronized is recommended, while atomic classes will perform better in less competitive situations.

It is worth noting that synchronized has been continuously optimized with the upgrade of the JDK. Synchronized goes from no lock to biased lock, then to lightweight lock, and finally to heavyweight lock that blocks threads. Therefore, in less competitive situations, synchronized also has good performance and does not require “panicking”.