spring-bootthread-safetyspring-async

Why is Spring Boot @Async dropping items in my List argument?


I am experiencing some sort of thread issue with the @Async method annotation where one argument contains a List of enum and is dropping items. The list is very small, 2 items. The dropping of items is not immediate, but sometimes takes hours or days to appear.

This is the general flow of our program:

A Controller generates the said List in its @RequestMapping method, passes the list to a Service class, which makes a call to a database for batching and triggers an event for each item from the database, passing the list. This list eventually gets passed into an @Async method which then drops either the first item or both items.

Controller.methodA() 
  -> Creates list with two items in it
  -> Calls void Service.methodX(list) 
    -> Load batch from database
    -> Iterate over batch
      -> Print items from list --- list in tact
      -> Calls void AsyncService.asyncMethod(list)
        -> Print items from list --- eventually drops items here always the first item, sometimes both.

Code configuration and bare-bones sample:

We configured it to have 2 threads:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setMaxPoolSize(5);  // Never actually creates 5 threads
        threadPoolTaskExecutor.setCorePoolSize(2); // Only 2 threads are ever created
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
}

This is a local replica to try to trigger the core issue, but no luck:

@RestController
public class ThreadingController {

    private final ThreadingService threadingService;

    public ThreadingController(ThreadingService threadingService) {
        this.threadingService = threadingService;
    }

    @GetMapping("/test")
    public void testThreads() {
        List<SomeEnum> list = new ArrayList<>();
        list.add(SomeEnum.FIRST_ENUM);
        list.add(SomeEnum.SECOND_ENUM);

        for (int i = 0; i < 1000; i++) {
            this.threadingService.workSomeThreads(i, list);
        }
    }
}
public enum SomeEnum {
    FIRST_ENUM("FIRST_ENUM"),
    SECOND_ENUM("SECOND_ENUM");

    @Getter
    private String name;

    SomeEnum(String name) {
        this.name = name;
    }

}
@Slf4j
@Service
public class ThreadingService {

    @Async
    public void workSomeThreads(int i, List<SomeEnum> list) {
        try {
            Thread.sleep(100L);  // Add some delay to slow things down to trigger GC or other tests during processing
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("Count {} ; Here are the list items: {}", i, list.toString());
        assert(list.size() == 2);
    }
}

If we look through this, I have one controller simulating both the Controller and Service mentioned earlier. It spins through a batch of data, sending the same list over and over. There's an aync method in another class to test that the list is the same. I was not able to replicate the issue locally, but this is the core problem.

To my knowledge, Java is pass-by-reference and every variable passed into a method gets its own pointer in the stack to that reference in memory, but don't think it would cause us to run out of memory. We are running in PCF and don't see any memory spikes or anything during this time. Memory is constant around 50%. I also tried using a CopyOnWriteArrayList (thread safe) instead of ArrayList and still the problem exists.

Questions:

Any idea why the @Async method would drop items in the method argument? The list is never modified after construction, so why would items disappear? Why would the first item always disappear? Why not the second item? Why would both disappear?


Edit: So this question had little to do with @Async in the end. I found deeply nested code that removed items from the list, causing items to go missing.


Solution

  • What you said is correct, Java is indeed pass-by-reference. The change in your list must be definitely due to some other code that is modifying this list while the threads are executing. There is no other way the object would change its values. You must investigate the code in the below section to identify if there is something that is modifying the list.

        -> Print items from list --- eventually drops items here always the first item, sometimes both.
        -> code following this might be changing the list. 
    

    As the AsyncService would execute its code asynchronously and in the mean time, some other code modifies the list.

    You may as well make the method params to be final.