Java:如果另一个线程已经进入同步方法,则创建新线程以跳过它

4

要求

  1. 我需要能够通过POST调用触发一个(长时间运行的)作业,并立即返回。

  2. 一次只能运行一个线程。

  3. 由于该作业很昂贵,如果已经有一个作业正在执行,则不希望所有未来的作业触发任何操作。

代码

@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();
    }
}

问题

通过上述代码,要满足需求1和2。如何最好地满足需求3(即:当新线程由POST调用创建时,如果未能获取锁,则跳过同步方法并立即返回)?


我已经更新了问题部分。 - Debapriyo Majumder
如果一个任务已经开始并且已经完成,触发器是否应该做任何事情?如果不是,这意味着该任务仅在生命周期中执行一次 - 你真的是这个意思吗?如果是,那么这绝对是糟糕的设计 - 一毫秒前没有开始,现在开始了 - 但实际上没有发生任何重要的变化。 - Alexei Kaigorodov
@AlexeiKaigorodov 如果已经启动并正在处理一个作业,则应忽略并发调用。一旦当前作业完成,就可以处理新的请求。简而言之,每次只运行一个作业+请求不会排队/阻塞。 - Debapriyo Majumder
2个回答

3

同步不是解决这个问题的正确工具。你可以按照以下方式实现:

@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();
    }
}

我似乎无法编写一个单元测试来测试triggerJob()方法,以验证只有一个线程进入并运行作业,而在此期间启动的所有其他线程都被跳过,而不使用某种延迟(Thread.sleep)进行测试。有什么想法吗? - Debapriyo Majumder
在您的单元测试中,您的“长时间运行的作业”可以等待一个CountDownLatch,当其他线程完成时,您会释放它。 - Matt Timmermans

1
对于需求1,如果你想仅使用@Async,你应该在服务方法上使用它,而不是控制器方法。但要注意,通过使其异步,你将失去对任务的控制,除非你使用Future实现@Async并通过实现AsyncUncaughtExceptionHandler接口处理失败。
对于需求3,你可以在服务中设置一个易失性布尔字段,在开始作业进程之前设置它,在作业进程完成后取消设置。在你的控制器方法中,你可以检查服务的易失性布尔字段来确定作业是否正在执行,并在作业进行中返回适当的消息。此外,在处理AsyncUncaughtExceptionHandler接口的实现时,请确保取消设置布尔字段。

服务:

@Service
public class SomeService {

    public volatile boolean isJobInProgress = false;

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

控制器:

@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;
        }
    }

}

实现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
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {

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

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

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接