[Java] Why does Ali recommend LongAdder instead of volatile?

Insert picture description here

1 Overview

Reprinted and added: Why does Ali recommend LongAdder instead of volatile?

The latest Songshan version of Ali's "Java Development Manual" was released on 8.3, and there is a paragraph that caught the attention of Lao Wang, the content is as follows:

[Reference] volatile solves the problem of invisible multi-threaded memory. For one write and multiple reads, the variable synchronization problem can be solved, but if you write more, the thread safety problem cannot be solved.
Note: If it is count++ operation, use the following class to achieve: AtomicInteger count = new AtomicInteger(); count.addAndGet(1); If it is JDK8, it is recommended to use LongAdder object, which has better performance than AtomicLong (reduce the number of optimistic lock retries) .
The above content has two main points:

  1. Similar to count++, it cannot be used in non-one write and multiple read scenarios volatile;
  2. If JDK8 recommended to use LongAdderrather than AtomicLongto replace volatile, because LongAdderof better performance.

But there is no proof to say, even if it is said by the lonely boss, we have to confirm it, because Mr. Ma said: Practice is the only criterion for testing truth.

It also has its advantages

  1. First, it deepens our knowledge of knowledge;
  2. Second, the document only states that LongAdder has higher performance than AtomicLong, but how much higher is it? The article does not say that we can only test it ourselves.

Not much, let’s go directly to the official content of this article...

2. Volatile thread safety test

First, let's test the thread safety of volatile in a multi-write environment. The test code is as follows:

package com.java.thread.demo.volatiled;

/**
 * @author: chuanchuan.lcc
 * @date: 2020-12-23 17:01
 * @modifiedBy: chuanchuan.lcc
 * @version: 1.0
 * @description:
 */
public class VolatileDemo {
    
    

    public static volatile int count = 0; // 计数器
    public static final int size = 100000; // 循环测试次数

    /**
     * -36813
     * 91
     * @param args
     */
    public static void main(String[] args) {
    
    
        long start = System.currentTimeMillis();
        // ++ 方式 10w 次
        Thread thread = new Thread(() -> {
    
    
            for (int i = 1; i <= size; i++) {
    
    
                count++;
            }
        });
        thread.start();
        // -- 10w 次
        for (int i = 1; i <= size; i++) {
    
    
            count--;
        }
        // 等所有线程执行完成
        while (thread.isAlive()) {
    
    }
        System.out.println(count); // 打印结果
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

We put the volatile modified count variable ++ 10w times, and start another thread – 10w times. Normally, the result should be 0, but the result of our execution is:

1063
93

Conclusion:由以上结果可以看出 volatile 在多写环境下是非线程安全的,测试结果和《Java开发手册》相吻合 .

3.AtomicLong

package com.java.thread.demo.volatiled;

import java.util.concurrent.atomic.AtomicLong;

/**
 * @author: chuanchuan.lcc
 * @date: 2020-12-23 17:02
 * @modifiedBy: chuanchuan.lcc
 * @version: 1.0
 * @description:
 */
public class VolatileVsAtomicLongDemo {
    
    
    public static AtomicLong count = new AtomicLong(0L); // 计数器
    public static final int size = 100000; // 循环测试次数

    /**
     * 0
     * 92
     * @param args
     */
    public static void main(String[] args) {
    
    
        long start = System.currentTimeMillis();
        // ++ 方式 10w 次
        Thread thread = new Thread(() -> {
    
    
            for (int i = 1; i <= size; i++) {
    
    
                count.incrementAndGet();
            }
        });
        thread.start();
        // -- 10w 次
        for (int i = 1; i <= size; i++) {
    
    
            count.decrementAndGet();
        }
        // 等所有线程执行完成
        while (thread.isAlive()) {
    
    
        }
        System.out.println(count); // 打印结果
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

The results are as follows

0
92

in conclusion:两个的耗时基本一致,但是AtomicLong结果准确

4.LongAdder VS AtomicLong

Next, we use Oracle's official JMH (Java Microbenchmark Harness, JAVA microbenchmark test suite) to test the performance of the two, the test code is as follows:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 1 轮,每次 1s
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Benchmark)
@Threads(1000) // 开启 1000 个并发线程
public class AlibabaAtomicTest {
    
    

    public static void main(String[] args) throws RunnerException {
    
    
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(AlibabaAtomicTest.class.getSimpleName()) // 要导入的测试类
                .build();
        new Runner(opt).run(); // 执行测试
    }

    @Benchmark
    public int atomicTest(Blackhole blackhole) throws InterruptedException {
    
    
        AtomicInteger atomicInteger = new AtomicInteger();
        for (int i = 0; i < 1024; i++) {
    
    
            atomicInteger.addAndGet(1);
        }
        // 为了避免 JIT 忽略未被使用的结果
        return atomicInteger.intValue();
    }

    @Benchmark
    public int longAdderTest(Blackhole blackhole) throws InterruptedException {
    
    
        LongAdder longAdder = new LongAdder();
        for (int i = 0; i < 1024; i++) {
    
    
            longAdder.add(1);
        }
        return longAdder.intValue();
    }
}

The result of program execution is:

Insert picture description here

From the above data, it can be seen that after 1000 threads are turned on, the performance of the program's LongAdder is about 1.53 times faster than AtomicInteger. You didn't see that 1000 threads were turned on. Why do you open so many threads? This is actually to simulate the performance query of the two in a high concurrency and high competition environment.

If under low competition, for example, we open 100 threads, the test results are as follows:

Insert picture description here

in conclusion:从上面结果可以看出,在低竞争的并发环境下 AtomicInteger 的性能是要比 LongAdder 的性能好,而高竞争环境下 LongAdder 的性能比 AtomicInteger 好,当有 1000 个线程运行时,LongAdder 的性能比 AtomicInteger 快了约 1.53 倍,所以各位要根据自己业务情况选择合适的类型来使用。

5. Performance analysis

Why does the above situation occur? This is because AtomicInteger will have multiple threads competing for an atomic variable in a high concurrency environment, and only one thread can compete successfully, and other threads will always try to obtain this atomic variable through CAS spin, so there will be certain performance Consumption; and LongAdder will separate this atomic variable into a Cell array, and each thread obtains its own array through Hash, which reduces the number of optimistic lock retries, thereby gaining an advantage under high competition; and performance under low competition It is not very good. It may be because the execution time of its own mechanism is longer than the spin time of lock competition, so the performance is not as good as AtomicInteger under low competition.

6. Summary

In this paper, we tested that volatile is not thread-safe in the case of multiple writes, and the performance of AtomicInteger is better than that of LongAdder in a low-competition concurrency environment, and the performance of LongAdder is better than AtomicInteger in a highly competitive environment, so we are using It is necessary to select the appropriate type based on your own business situation.

Guess you like

Origin blog.csdn.net/qq_21383435/article/details/111596011