Last week, I wrote a unit test to test whether a method would perform as expected in a concurrent scenario. After I finished writing, I cut a picture and sent it to a circle of friends. Many people said that just a few lines of code involved several knowledge points.
Others have suggested some optimizations. So, what kind of code is this? What knowledge is involved, and what can be optimized?
Let’s take a look.
background
A bit of background, just to get a sense of the functionality of the method we are testing. The service to be tested is AssetService and the method to be tested is the update method.
The update method does two things. The first is to update the Asset and the second is to insert an AssetStream.
In the update Asset method, we mainly update the information of the Asset in the database, here to prevent concurrency, we use optimistic locking.
To insert the AssetStream method, the main purpose is to insert the flow information of an AssetStream. In order to prevent concurrency, the uniqueness constraint is added in the database.
To ensure data consistency, we package the two operations in the same transaction through a local transaction.
Here is the main code. Of course, there are some pre-idempotency checks, parameter validity checks, etc., which are omitted here:
@Service public class AssetServiceImpl implements AssetService { @Autowired private TransactionTemplate transactionTemplate; @override public String update(Asset Asset) {// Override public String update(Asset Asset) { return transactionTemplate.execute(status -> { updateAsset(asset); return insertAssetStream(asset); }); }}Copy the code
Since this method may be executed in concurrent scenarios, this method provides concurrency control through transaction + optimistic locking + uniqueness constraints. I won’t go into much detail about this, but I’ll expand on how to prevent concurrency later if you’re interested.
A single measurement
Since the above method can be called in a concurrent scenario, I need to simulate the concurrent scenario in the single test, so I wrote the following unit test code:
public class AssetServiceImplTest { private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() .setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(128), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); @Autowired AssetService assetService; @Test public void test_updateConcurrent() { Asset asset = getAsset(); //... CountDownLatch = new CountDownLatch(10); AtomicInteger failedCount =new AtomicInteger(); For (int I = 0; int I = 0; i < 10; i++) { pool.execute(() -> { try { String streamNo = assetService.update(asset); } catch (Exception e) { System.out.println("Error : " + e); failedCount.getAndIncrement(); } finally { countDownLatch.countDown(); }}); } try {// query the latest asset countdownlatch.await (); } catch (InterruptedException e) { e.printStackTrace(); } Assert.assertEquals(failedCount.intValue(), 9); // Select * from database where Asset = new; // select * from database where Asset = new; // Select * from database where Asset = new;Copy the code
Above, is the part of the code that I do after simplifying unit test. There is a lot of concurrency involved in this because we are testing concurrent scenarios.
A lot of people have told me that they know a lot about concurrency, but they don’t seem to have the opportunity to write concurrent code. In fact, unit testing is a great opportunity.
Let’s take a look at the code above and see what’s involved.
knowledge
There are several points involved in the above unit test code, which I will briefly explain here.
The thread pool
So I’ve used the thread pool instead of directly creating the pool using the Java provided Executors class.
Instead, use the ThreadFactoryBuilder provided by Guava to create a thread pool. When creating a thread in this way, not only can avoid OOM problems, but also can customize the thread name, more convenient in case of error traceability. (OOM issue with thread pool creation)
CountDownLatch
Because in my unit test code, I want the main thread to check the result after all the child threads have executed.
So, how do you make the main thread block until all the child threads have executed? This uses a synchronization helper class called CountDownLatch.
Initializes CountDownLatch with the given count. Because the countDown() method was called, the await method will block until the current count reaches zero. (Use of CountDownLatch in multiple threads)
AtomicInteger
Because I created 10 threads in my single test code, but I needed to make sure that only one thread could execute successfully. So, I need to count the number of failures.
So, how do you count in a concurrent scenario? AtomicInteger is an atomic operation class that provides thread-safe operations.
Exception handling
Because we simulate multiple threads executing concurrently, there are bound to be instances where some threads fail.
Because the exception is not caught underneath the method. So exceptions need to be caught in the single test code.
try {
String streamNo = assetService.update(asset);
} catch (Exception e) {
System.out.println("Error : " + e);
failedCount.increment();
} finally {
countDownLatch.countDown();
}
Copy the code
In this code, try, catch, and finall are used and cannot be switched. The count of the number of failures must be in the catch and the countDownLatch count must be in the finally.
Assert
This is the assertion tool class provided by JUnit, which can be used to make assertions when unit testing. I won’t go into the details.
Optimal point
The above code covers a lot of ground, but isn’t there any optimization?
First of all, in fact, the unit test code for performance, stability and so on is not high requirements, the so-called optimization point, is not necessary. Here is just to discuss, if you really want to achieve excellence, what can be optimized?
Use LongAdder instead of AtomicInteger
My friend @zkx suggested using LongAdder instead of AtomicInteger.
Java. Util. Concurrency. Atomic. LongAdder is Java8 add a class that provides a method of atomic accumulative total value. It is also clearly stated in its Javadoc that it performs better than AtomicLong.
First, it has a base value, base. In case of a race, it has an array of cells that are used to discretize operations from different threads to different nodes (which can be expanded to the maximum number of CPU cores, i.e., the maximum number of concurrent threads). Sum () returns the value and base from all the Cell arrays.
The core idea is to spread the update pressure of one AtomicLong value to multiple values, so as to reduce the update hotspot. Therefore, LongAdder performs better in highly competitive lock scenarios.
Increase concurrent contention
Both Cafebabe and @pudu are talking about the same optimization, which is how to increase concurrent competition.
I had thought of this question before I posted it on Moments, and I already had the answer in mind, but it was nice to have two friends mention it almost at the same time.
Let’s talk about what the problem is.
To promote concurrency, we created multiple threads using a thread pool and wanted to have multiple threads execute the method under test concurrently.
However, we are doing it sequentially in a for loop, so in theory these 10 calls to the update method are executed sequentially.
Of course, because of the CPU time slice, the actual execution of the 10 threads competing for the CPU will still occur concurrent conflicts.
However, to be on the safe side, we need to try to simulate multiple threads making method calls simultaneously.
The optimal method is to wait for each update method to be called until all child threads are successfully created before starting to execute together.
You can also use CountDownLatch, which we talked about earlier.
Therefore, the final optimized single test code is as follows:
MainThreadHolder = new CountDownLatch(10); CountDownLatch multiThreadHolder = new CountDownLatch(1); LongAdder failedCount = new LongAdder(); For (int I = 0; int I = 0; i < 10; I++) {pool.execute(() -> {try {// the child thread is waiting for notification from the main thread to execute multithreadhold.await (); String streamNo = assetService.update(asset); } catch (Exception e) {// count the failure when an Exception occurs +1 system.out.println ("Error: "+ e); failedCount.increment(); } finally {/ / main thread blocking, odd number - 1 mainThreadHolder. CountDown (); }}); } / / notify all the child thread can perform method calls the multiThreadHolder. CountDown (); Try {// query the latest asset pool plan after the main thread has finished executing mainThreadHolder.await(); } catch (InterruptedException e) { e.printStackTrace(); } assert.assertequals (failedCount.intValue(), 9); assert.assertequals (failedCount.intValue(), 9); // Retrieve the latest Asset from the database // check the key fieldsCopy the code
Above are the knowledge points involved in the code of one of my unit tests, as well as the relevant optimization points that can be thought of at present.
Finally, I would like to ask, for this part of the code, what do you think can be optimized?
About the author: Hollis, a person with a unique pursuit of Coding, is an Alibaba technologist, the co-author of three Courses for Programmers, and the author of a series of articles on how Java Engineers Become Gods.
Follow the public account [Hollis], reply “God map” in the background, you can get Java engineer advanced mind map.