3

更新:Java 1.6.34を使用していますが、Java7にアップグレードする機会はありません。

1分間に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);
}

Javaクライアントにメソッドを呼び出すことができるかどうかを通知ApiThrottlerするメソッドを持つクラスを作成したいと思います。(もちろん、いつでも呼び出すことができますが、レートを超えた場合は呼び出す意味がありません。)boolean canRun()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()

4

4 に答える 4

5

おそらく最良のオプションの 1 つは、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() を呼び出すことによってサービスへの呼び出しを実行するたびに許可を消費します。これは、すべての許可が既に排出されている場合にブロックされます。

もちろん、あなたが言及したように、タイマーまたは別のスレッドで待機/通知および整数カウンターを使用してこれをコーディングすることは可能ですが、それは私が概説したより近代的な 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 以降で機能するはずです。

また、より良い解決策はcom.google.common.util.concurrent.RateLimiter、Guava から使用することです。次のようになります。

class Throttler {
  final RateLimiter rateLimiter = RateLimiter.create(80.0/60.0);

  makeCall() {
    rateLimiter.acquire();
    doMakeCall();
  }
}

セマンティクスは Semaphore ソリューションと比較してわずかに異なり、RateLimiter はおそらく状況により適しています。

于 2013-02-01T01:42:59.777 に答える
1

私は最近、このようなことを書いています。唯一の変更点は、関数の実行が完了したときにコードがコールバックを期待していることです。したがって、実行できない場合は、コールバックを直接呼び出します。

追加の変更点の 1 つは、この呼び出しはおそらく非同期であるため、2 回目に呼び出したときに呼び出しが進行中である可能性があることです。そのような場合、私は電話を無視しました。

そして、私のスロットルには、呼び出されるcall関数とコールバックを取るヘルパー関数が呼び出されます。これは C++ であったため、Action およびリスナー インターフェイスの形式になります。

これには、セマフォ ベースのソリューションよりも、リクエストをシリアル化するため、あまり頻繁に呼び出す必要がないという利点があります。

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 秒 (または分) に 1 回処理していたことです。

于 2013-02-01T01:43:02.783 に答える
0

時間の記録を保持することができ、最後の 1 分間に 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 回の呼び出しを発行し、1 分待ってから 60 秒目に別の 80 回の呼び出しを発行する可能性があります。反対側は、両端のクロックの不正確さ、またはネットワークのランダムな遅延のために、1 分以内に 160 コールを測定することがあります。安全のために、時間枠を広げてください。代わりに、70 秒あたり 80 回の呼び出しで調整してください。

于 2013-02-01T02:59:55.890 に答える
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(); }
}

このように、1 秒ごとに許可をリセットするようにタイマーを設定します。スケジュール メソッドの最初のパラメーターは、TimerTask のインスタンスです (上記の Permit クラス オブジェクトを渡します)。run() メソッドは、2 番目の引数で指定された期間 (ここでは 1000 ミリ秒 / 1 秒) ごとに呼び出されます。「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 キーワードの使用に注意してください。これにより、複数のスレッドが同じオブジェクト インスタンスのメソッドを同時に実行することを回避できます。

于 2013-02-01T02:07:51.170 に答える