Debapriyo Majumder
Debapriyo Majumder

Reputation: 159

Java: New thread to skip synchronised method if another thread already in it

Requirements

  1. I need to be able to trigger a (long running) job via a POST call and return immediately.

  2. Only one thread can run the job at one time.

  3. The job being an expensive one, I want all future triggers of this job to not do anything if one job is already in progress.

Code

@RestController
public class SomeTask {

    private SomeService someService;

    @Autowired
    public SomeTask(SomeService someService) {
        this.someService = someService;
    }

    @Async // requirement 1
    @RequestMapping(method = RequestMethod.POST, path = "/triggerJob")
    public void triggerJob() {
        expensiveLongRunningJob();
    }

    /**
     * Synchronized in order to restrict multiple invocations. // requirement 2
     *
     */
    private synchronized void expensiveLongRunningJob() { 
        someService.executedJob();
    }
}

Question

With the above code requirements 1 and 2 are satisfied. What is the best way to satisfy requirement 3 as well (have the new thread, created as a result of a POST call, skip the synchronised method and return immediately on failure to acquire a lock)?

Upvotes: 4

Views: 919

Answers (2)

Matt Timmermans
Matt Timmermans

Reputation: 59253

Synchronization isn't the right tool for the job. You can do it like this:

@RestController
public class SomeTask {

    private SomeService someService;
    private final AtomicBoolean isTriggered = new AtomicBoolean();

    @Autowired
    public SomeTask(SomeService someService) {
        this.someService = someService;
    }

    @Async // requirement 1
    @RequestMapping(method = RequestMethod.POST, path = "/triggerJob")
    public void triggerJob() {
        if (!isTriggered.getAndSet(true)) {
            try {
                expensiveLongRunningJob();
            } finally {
                isTriggered.set(false);
            }
        }
    }

    /**
     * only runs once at a time, in the thread that sets isTriggered to true
     */
    private void expensiveLongRunningJob() { 
        someService.executedJob();
    }
}

Upvotes: 3

Madhu Bhat
Madhu Bhat

Reputation: 15223

For requirement 1, if you want to use just @Async, you should have it on the service method and not the controller method. But be aware that by making it async, you would lose control over the job and failure handling will be not possible, unless you implement @Async with Future and handle failures by implementing AsyncUncaughtExceptionHandler interface.

For requirement 3, you can have a volatile boolean field in the service, which gets set just before beginning the job process and unset after the job process completes. In your controller method, you can check the service's volatile boolean field to decide if the job is being executed or not and just return with appropriate message if the job is in progress. Also, make sure to unset the boolean field while handling the failure in the implementation of AsyncUncaughtExceptionHandler interface.

Service:

@Service
public class SomeService {

    public volatile boolean isJobInProgress = false;

    @Async
    public Future<String> executeJob() {
        isJobInProgress = true;
        //Job processing logic
        isJobInProgress = false;
    }
}

Controller:

@RestController
public class SomeTask {

    @Autowired
    private SomeService someService;

    @RequestMapping(method = RequestMethod.POST, path = "/triggerJob")
    public void triggerJob() {
        if (!someService.isJobInProgress){
            someService.executeJob(); //can have this in a sync block to be on the safer side. 
        } else {
            return;
        }
    }

}

Implementation of AsyncUncaughtExceptionHandler:

public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Autowired
    private SomeService someService;

    @Override
    public void handleUncaughtException(
            Throwable throwable, Method method, Object... obj) {

        //Handle failure
        if (someService.isJobInProgress){
            someService.isJobInProgress = false;
        }
    }
}

@Async configuration:

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        return new ThreadPoolTaskExecutor();
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }

}

Upvotes: 1

Related Questions