AtomicInteger是为多线程应用程序提供计数器的好解决方案吗?

3
我是一名有帮助的助手,可以为您翻译文本。
我有一个 Android 客户端,将与服务器建立 Http 连接。
服务器要求所有 Http 请求在 Http 标头中提供单调递增的计数器。例如:
POST /foo/server
X-count: 43

发起 Http 连接的地方:

  1. 用户命令下的内部操作,例如按钮点击
  2. 服务内部(由 Context#startService 启动)

为了提供计数器值,我计划在我的 Application 子类中托管一个 AtomicInteger。所有代码都将从一个中心位置检索计数。

如果 Http 连接失败(例如服务器宕机),我需要减少计数器。

您认为 AtomicInteger 是否适合我的情况?

3个回答

12

AtomicInteger是您想要用于此目的的准确选择。


3
然而,你需要注意一种情况,即在一个HTTP请求的数量较低的情况下(例如,在该请求的线程休眠时),可能会发送一个带有更高计数的HTTP请求。请留意这种情况。 - notnoop
1
notnoop是正确的。如果你想在服务器端保留顺序(即发生在之后),你必须实现某种Lamport时钟。 - Miguel Ping

2
如果 Http 连接失败(例如服务器宕机),我需要减少计数器。
我原本想说“当然可以”,但是在看到这句话后,我不太确定了。我猜你想做的事情是这样的:
def sendRequest(url) request = new request to url request.header["X-count"] = next serial if request.send() != SUCCESS rewind serial
如果是这样,我认为不应该允许两个线程同时发送请求,而是要使用序列化请求的方法,而不是使用 AtomicInteger,因为 AtomicInteger 只能让你原子地执行一些操作。如果两个线程同时调用 sendRequest,并且第一个失败了,那么会发生以下情况:
Thread | What happens? --------+------------------------- A | Creates new request B | Creates new request A | Set request["X-count"] = 0 A | Increment counter to 1 A | Send request B | Set request["X-count"] = 1 B | Increment counter to 2 B | Send request A | Request fails B | Request succeeds A | Rewind counter down to 1 C | Creates new request C | Set request["X-count"] = 1 C | Increment counter to 2
现在,你已经发送了两个 X-count = 1 的请求。如果你想避免这种情况,你应该使用类似下面的代码(假设 Request 和 Response 是用于处理 URL 请求的类):
class SerialRequester {
    private volatile int currentSerial = 0;

    public synchronized Response sendRequest(URL url) throws SomeException {
        Request request = new Request(url);
        request.setHeader("X-count", currentSerial);
        Response response = request.send();
        if (response.isSuccess()) ++currentSerial;
        return response;
    }

}

这个类保证通过同一个SerialRequester发送的请求,不会存在相同的 X-count 值。

编辑 许多人对上面的解决方案不支持并发执行感到担忧。确实如此。但是要解决提问者的问题,必须这样设计。如果不需要在请求失败时递减计数器,则AtomicInteger非常完美,但在这种情况下会出现错误。

编辑2 我写了一个串行请求器(就像上面的请求器),不太容易出现冻结,并且如果请求等待时间过长(即在工作线程中排队但未启动),则会终止请求。因此,如果通道堵塞并且一个请求挂起很长时间,其他请求最多等待一定的时间,以便队列不会无限增长,直到通道畅通为止。

class SerialRequester {
    private enum State { PENDING, STARTED, ABORTED }
    private final ExecutorService executor = 
        Executors.newSingleThreadExecutor();
    private int currentSerial = 0; // not volatile, used from executor thread only

    public Response sendRequest(final URL url) 
    throws SomeException, InterruptedException {
        final AtomicReference<State> state = 
            new AtomicReference<State>(State.PENDING);
        Future<Response> result = executor.submit(new Callable<Response>(){
            @Override
            public Result call() throws SomeException {
                if (!state.compareAndSet(State.PENDING, State.STARTED))
                    return null; // Aborted by calling thread
                Request request = new Request(url);
                request.setHeader("X-count", currentSerial);
                Response response = request.send();
                if (response.isSuccess()) ++currentSerial;
                return response;
            }
        });
        try {
            try {
                // Wait at most 30 secs for request to start
                return result.get(30, TimeUnit.SECONDS);
            } catch (TimeoutException e){
                // 30 secs passed; abort task if not started
                if (state.compareAndSet(State.PENDING, State.ABORTED))
                    throw new SomeException("Request queued too long", e);
                return result.get(); // Task started; wait for completion
            }
        } catch (ExecutionException e) { // Network timeout, misc I/O errors etc
            throw new SomeException("Request error", e); 
        }
    }

}

经验法则是尽可能少地使用锁定(无论是内在的还是通过Lock接口)。你的建议会在整个网络发送期间使用锁定。尽管存在异步调用,但网络延迟可能会导致意外的延迟。 - John Vint
我没有看到在你的例子中发送了两个X-count = 1的成功请求。虽然有两个请求被发送,但只有一个是成功的。 - Miguel Ping
@John W. - 据我理解,原帖中要求按顺序发送请求。或者至少如果它们都需要自己的唯一的串行 X-count 值,那么这是一个合乎逻辑的结论。 - gustafc
@Miguel Ping - 线程C的请求成功与否并不重要。我们已经有两个错误了:首先,线程B中的请求应该具有X-count = 0,因为前面的请求(在线程A中)没有成功。其次,线程C中的请求不应该与B中成功请求具有相同的X-count,无论它是否成功。如果具有X-count = n的请求成功,则以后的请求都不应该具有X-count = n - 或者至少我是这样理解OP的。 - gustafc
@Gustafc。我理解你保证了那里的互斥,但我不能建议使用该实现。如果有许多线程向许多不同的URL发送请求,并且其中一些URL超时,则任何尝试进行发送的线程都必须等待超时完成。请求本身是方法内的局部变量,所以等待的线程将对问题一无所知。 - John Vint
是的,我同意这是一堆垃圾 - 但这就是所要求的 :) - gustafc

1

gustafc说得对!需求是:

如果Http连接失败(例如服务器宕机),我需要减少计数器。

这将杀死任何并发的可能性!

如果您想要HTTP头的唯一计数器,AtomicInteger很好,但您无法从多个服务器或JVM发送请求,并且需要允许空洞。
因此,在高度可扩展的环境中计数是徒劳的(像总是一样),使用UUID是一种更“可扩展”和强大的解决方案。人类需要计数,机器不在乎!
因此,如果您想要成功发送后的总数,请在成功发送后增加计数器(您甚至可以跟踪失败/成功的UUID请求)。

这是我关于并行计数的2分钱意见 :)


我更关心gustafc的评论中的并发性。如果服务器崩溃了怎么办?那么每当一个线程调用SerialRequeste.sendRequest方法时,该方法将阻塞所有线程,直到发送请求返回超时异常。这对于任何类型的吞吐量都是致命的。 - John Vint
1
@John W. - 我的解决方案容易导致应用程序冻结,如果您没有设置适当的超时并且服务器崩溃或管道堵塞,我同意,但它的好处在于它是正确的 - 它做了OP要求的事情,这恰好需要按顺序发送请求。 - gustafc

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