The latest Songshan edition of Ali Java Development Manual was released on August 3, and there was a paragraph of content that caught Lao Wang’s attention, which was as follows:
Volatile Solves the problem of multithreaded memory not being visible. Multiple reads on one write can solve variable synchronization problems, but multiple writes cannot solve thread safety problems.
AtomicInteger count = new AtomicInteger(); count.addAndGet(1); If it is JDK8, the LongAdder object is recommended for better performance than AtomicLong (reducing the number of optimistic lock retries).
The above content has two main points:
- Non-write multiple read scenarios such as count++ cannot be used
volatile
; - It is recommended for JDK8
LongAdder
Rather thanAtomicLong
To replace thevolatile
Because theLongAdder
Better performance.
But the word is groundless, even if it is said by the lonely tycoon, we have to prove it, because As Don Ma once said: practice is the only criterion for testing truth.
It also has its advantages. First, it deepens our cognition of knowledge. Second, the documentation only says that LongAdder performs better than AtomicLong, but by how much? The article does not say that we have to test ourselves.
Without further ado, let’s jump right into the official content of this article…
Volatile thread safety tests
First, we will test the thread-safety of Volatile in a multiwrite environment as follows:
public class VolatileExample {
public static volatile int count = 0; / / counter
public static final int size = 100000; // Number of loop tests
public static void main(String[] args) {
// ++ 10W times
Thread thread = new Thread(() -> {
for (int i = 1; i <= size; i++) { count++; }}); thread.start();/ / -- 10 w
for (int i = 1; i <= size; i++) {
count--;
}
// Wait for all threads to finish executing
while (thread.isAlive()) {}
System.out.println(count); // Print the result}}Copy the code
We used volatile count ++ 10W times to start another thread — 10W times. Normally the result would be 0, but we executed the result:
1063
Conclusion: It can be seen from the above resultsvolatile
It is not thread-safe in multi-write environments, and the test results are consistent with the Java Development Manual.
LongAdder VS AtomicLong
JMH (Java Microbenchmark Harness, Java Microbenchmark Suite) is used to test the performance of Oracle Microbenchmark.
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) // Test completion time
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) // Preheat 1 round, 1s each time
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // Test 5 rounds, 3s each time
@Fork(1) // start 1 thread
@State(Scope.Benchmark)
@Threads(1000) // Start 1000 concurrent threads
public class AlibabaAtomicTest {
public static void main(String[] args) throws RunnerException {
// Start the benchmark
Options opt = new OptionsBuilder()
.include(AlibabaAtomicTest.class.getSimpleName()) // The test class to import
.build();
new Runner(opt).run(); // Execute the test
}
@Benchmark
public int atomicTest(Blackhole blackhole) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger();
for (int i = 0; i < 1024; i++) {
atomicInteger.addAndGet(1);
}
// To avoid JIT ignoring unused results
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);
}
returnlongAdder.intValue(); }}Copy the code
The results of program execution are as follows:
As you can see from the above data, the application’s LongAdder performance is about 1.53 times faster than AtomicInteger after 1000 threads are opened. This is meant to simulate the performance of both queries in an environment of high concurrency and high competition.
If the race is low, let’s say we start 100 threads, the test results are as follows:
Conclusion: It can be seen from the above results that the performance of AtomicInteger is better than that of LongAdder in the low-contention concurrent environment, while the performance of LongAdder is better than that of AtomicInteger in the high-contention environment. When there are 1000 threads running, LongAdder is about 1.53 times faster than AtomicInteger, so choose the right type to use according to your business situation.
Performance analysis
Why does this happen? This is because AtomicInteger in a high concurrency environment, there will be multiple threads competing for an atomic variable, and only one thread can always compete successfully, while other threads will always try to acquire the atomic variable through CAS spin, so there will be a certain performance consumption. LongAdder will split the atomic variable into a Cell array, and each thread will Hash its own array, thus reducing the number of optimistic lock retries and gaining an advantage in high contention. However, AtomicInteger does not perform as well under low contention, probably because the execution time of its own mechanism is longer than the spin time of lock contention, and therefore performs worse under low contention.
conclusion
In this paper, we test that volatile is non-thread-safe in the case of multiple writes, and that AtomicInteger performs better than LongAdder in a low-contention concurrency environment, while LongAdder performs better than AtomicInteger in a high-contention environment. Therefore, we should choose the corresponding type according to our own business situation when using it.
Follow the public account “Java Chinese Community”