更新:我使用的是Java 1.6.34,无法升级到Java 7。
我有一个场景,我只允许每分钟调用80次方法。它实际上是由第三方编写的服务API,如果你多次调用它,它会“关闭”(忽略调用)它的API:
public class WidgetService {
// Can only call this method 80x/min, otherwise it
// it just doesn't do anything
public void doSomething(Fizz fizz);
}
我想写一个ApiThrottler
类,它有一个boolean canRun()
方法,告诉我的Java客户端是否可以调用doSomething(Fizz)
方法。 (当然它总是可以被称为,但如果我们超过了我们的费率就没有任何意义。)
这样可以让我编写这样的代码:
// 80x/min
ApiThrottler throttler = new ApiThrottler(80);
WidgetService widgetService = new DefaultWidgetService();
// Massive list of Fizzes
List<Fizz> fizzes = getFizzes();
for(Fizz fizz : fizzes)
if(throttler.canRun())
widgetService.doSomething(fizz);
这不一定是API(ApiThrottler#canRun
),但是我需要一个可靠/可靠的机制,在WidgetService#doSomething(Fizz)
可以之前暂停/休眠调用。
这让我感觉就像我们正在进入使用多个线程的领域一样,这让我感觉感觉就像我们可以使用某种锁定机制和Java一样通知(wait()
/ notify()
)模型。但是在这个领域没有经验,我似乎无法绕过最优雅的解决方案。提前谢谢。
答案 0 :(得分:5)
可能最好的选择之一就是使用Semaphore http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Semaphore.html课程,每分钟给它80个许可证。这可以通过使用计时器类http://docs.oracle.com/javase/7/docs/api/java/util/Timer.html来完成。 每次通过调用信号量上的acquire()执行对服务的调用时,调用者线程将使用许可证,如果所有许可证已经耗尽,则会阻塞该函数。
如你所提到的,当然可以使用wait / notify和整数计数器与计时器或单独的线程进行编码,但与使用更现代的java.util.concurrent API相比,这会更复杂。我已在上面概述过。
它可以看起来接近以下内容:
class Throttler implements TimerTask {
final Semaphore s = new Semaphore(80);
final Timer timer = new Timer(true);
Throttler() {
timer.schedule(this, 0, 60*1000); //schedule this for 1 min execution
}
run() { //called by timer
s.release(80 - s.availablePermits());
}
makeCall() {
s.acquire();
doMakeCall();
}
}
这应该从Java 5开始。
更好的解决方案是使用Guava的com.google.common.util.concurrent.RateLimiter
。它看起来像这样:
class Throttler {
final RateLimiter rateLimiter = RateLimiter.create(80.0/60.0);
makeCall() {
rateLimiter.acquire();
doMakeCall();
}
}
与Semaphore解决方案相比,语义略有不同,RateLimiter最适合您的情况。
答案 1 :(得分:1)
我最近写过类似的东西。唯一的变化是我的代码在函数运行完毕时需要回调。所以如果它无法运行,我会直接调用回调。
另外一个变化是,由于此调用可能是异步的,因此当您第二次调用它时,可能正在进行调用。在这种情况下,我只是忽略了这个电话。
我的throttler有一个名为call
的辅助函数,它接受函数调用和回调。这是在C ++中,所以对于你来说,它将采用Action和侦听器接口的形式。
这比基于Semaphore的解决方案更有优势,它可以序列化请求,这样您就不会经常调用它。
interface Callback{
public void OnFunctionCalled();
}
class APIThrottler
//ctor etc
boolean CanCall();
public boolean IsInProgress();
public void SetInProgress(boolean inProgress = true);
public void Mark(){/*increment counter*/; SetInProgress(false);} // couldnt think of a better name..
public void Call(Callable event, Callback callback){
If(IsInProgress())
return;
else if(CanCall())
{
SetInProgress();
event.Call();
}
else
callback.OnFunctionCalled();
}
}
一旦函数回调(或者在函数内部,如果它是同步的),一旦完成,你需要Mark()
。
这在很大程度上是我的实现,唯一的区别是我在x秒(或分钟)内处理了一次。
答案 2 :(得分:0)
这可以通过使用简单的计数器来实现。我们将其称为“许可”计数器,在80处初始化。在您调用“该功能”之前,首先尝试获取许可证。完成后,将其释放。然后设置一个重复计时器,每秒将计数器重置为80.
class Permit extends TimerTask
{
private Integer permit = 80;
private synchronized void resetPermit() { this.permit = 80; }
public synchronized boolean acquire() {
if(this.permit == 0) return false;
this.permit--;
return true;
}
public synchronized void release() { this.permit++; }
@Override public void run() { resetPermit(); }
}
将计时器设置为每秒重置许可,如下所示。 schedule方法的第一个参数是TimerTask的一个实例(传递上面的Permit类对象)。对于第二个参数(此处为1000毫秒/ 1秒)指定的每个句点,将调用run()方法。 'true'参数表示这是一个守护程序计时器,它将继续重复。
Timer timer = new Timer(true);
timer.schedule(permit, 1000);
然后,每当您需要呼叫您的功能时,首先检查您是否可以获得任何许可。完成后不要忘记发布
void myFunction() {
if(!permit.acquire()) {
System.out.println("Nah.. been calling this 80x in the past 1 sec");
return;
}
// do stuff here
permit.release();
}
注意在上面的Permit类方法中使用synchronized关键字 - 这应该避免多于1个线程同时执行同一个对象实例的任何方法
答案 3 :(得分:0)
您可以保留时间记录,并确保最后一分钟内不超过80条记录。
// first=newest, last=oldest
final LinkedList<Long> records = new LinkedList<>();
synchronized public void canRun() throws InterruptedException
{
while(true)
{
long now = System.currentTimeMillis();
long oneMinuteAgo = now - 60000;
// remove records older than one minute
while(records.getLast() < oneMinuteAgo)
records.removeLast();
if(records.size()<80) // less than 80 records in the last minute
{
records.addFirst(now);
return; // can run
}
// wait for the oldest record to expire, then check again
wait( 1 + records.getLast() - oneMinuteAgo);
}
}
有可能在第0秒,我们发出80个电话,等待一分钟,然后在第60秒,我们再发出80个电话。由于两端的时钟不精确或网络随机延迟,另一端可能碰巧在一分钟内测量160个呼叫。为了安全起见,请放大时间窗口,比如每70秒拨打80个电话。