计算方法的并发调用

时间:2017-11-13 15:08:11

标签: java concurrency

我们说我有一个非常简单的网络服务,其唯一的任务是计算它的终端被调用的次数。端点为/hello

@Controller
public class HelloController {

    private int calls = 0;

    @RequestMapping("/hello")
    public String hello() {
        incrementCalls();
        return "hello";
    }

    private void incrementCalls() {
        calls++;
    }
}

现在,只要两个用户同时不同时呼叫/hello,这一切都可以正常工作。但是当对/hello进行并行调用时,calls变量只会增加一次(如果我没有记错的话)。显然,这里需要进行某种同步。

问题是什么是使这个方法成为线程安全的最佳方法?

1 个答案:

答案 0 :(得分:3)

calls++可能导致非您期望的行为的原因是它不是 atomic 。原子操作以这样的方式发生,即整个操作不能被另一个线程截获。通过锁定操作或通过利用已经以原子方式执行它的硬件来实现原子性。

增量很可能不是原子操作,因为它是calls = calls + 1;的快捷方式。确实有两个线程在有机会增加之前为calls检索相同的值。然后两者都将存储相同的值,而不是获得已经递增的值。

有一些简单的方法可以将get-and-increment转换为原子操作。最简单的一个,不需要任何导入,就是制作方法synchronized

private void incrementCalls() {
    calls++;
}

每当线程进入方法时,这将隐式锁定它所属的HelloController对象。其他线程必须等到释放锁才能进入方法,使整个方法进入原子操作。

另一种方法是显式同步所需的代码部分。对于不需要大量原子操作的大型方法来说,这通常是更好的选择,因为同步相当耗费时间和空间:

private void incrementCalls() {
    sychronized(this) {
        calls++;
    }
}

制作整个方法synchronized只是将整个内容包装在synchronized(this)中的快捷方式。

java.util.concurrent.atomic.AtomicInteger处理同步,以便将您希望对整数执行的大多数操作转换为原子操作。在这种情况下,您可以拨打getAndAdd(1)getAndIncrement()。这可能是保持代码清晰的最简洁的解决方案,因为它减少了大括号的数量并使用精心设计的库函数。