HTTP経由でファイルをダウンロードする のソースコードを元に同時ダウンロード数を制限してみるサンプル。
「ダウンロード数をカウントするだけ」 かつ 「synchronizedな同期処理はしない」という手抜き実装。


import java.io.*;
import java.net.*;
 
public class Downloader {
 
  public static void main(String[] args) throws Exception {
    testMultiThreads();
  }
 
  private static void testMultiThreads() throws Exception {
 
    final int limited = 3;
    final int threads = 5;
    
    final Downloader d = new Downloader(limited);
    
    // ほぼ同時アクセスでダウンロード
    for(int i=0; i<threads; i++){
      final int index = i; // 無名クラスがこの変数をバインドできるように final で
      Runnable r = new Runnable(){
        public void run(){
          try{
            d.getContent("http://www.nilab.info/zurazure2/myimage/zurazure_kohaku.png");
            System.out.println("OK:" + index);
          }catch(Exception e){
            System.out.println("NG:" + index + ": " + e.toString());
          }
        }
      };
      Thread t = new Thread(r);
      t.start();
    }
    
    try{
      Thread.sleep(5000);
    }catch(Exception e){
      e.printStackTrace();
    }
    
    // もう一度、同じようにほぼ同時アクセス
    for(int i=0; i<threads; i++){
      final int index = i; // 無名クラスがこの変数をバインドできるように final で
      Runnable r = new Runnable(){
        public void run(){
          try{
            d.getContent("http://www.nilab.info/zurazure2/myimage/zurazure_kohaku.png");
            System.out.println("OK:" + index);
          }catch(Exception e){
            System.out.println("NG:" + index + ": " + e.toString());
          }
        }
      };
      Thread t = new Thread(r);
      t.start();
    }
    
  }
  
  private static void testSingleThread() throws Exception {
    Downloader d = new Downloader();
    d.setConnectTimeout(5000); // 通信リンクを開くときのタイムアウト値5秒
    d.setReadTimeout(5000); // 入力ストリームから読み取る際のタイムアウト値5秒
    byte[] b = d.getContent("http://www.nilab.info/zurazure2/myimage/zurazure_kohaku.png");
    FileOutputStream fos = new FileOutputStream("test.png");
    fos.write(b);
    fos.close();
  }
 
  private Integer connectTimeout;
  private Integer readTimeout;
  private final int parallelDownloadableNumber;
  private int counter = 0;
  
  public Downloader(){
    this(-1);
  }
 
  /**
   * @param parallelDownloadableNumber 同時ダウンロード可能な上限数(厳密なチェックはしない)
   */
  public Downloader(int parallelDownloadableNumber){
    this.parallelDownloadableNumber = parallelDownloadableNumber;
  }
 
  public void setConnectTimeout(int timeout){
    connectTimeout = new Integer(timeout);
  }
  
  public void setReadTimeout(int timeout){
    readTimeout = new Integer(timeout);
  }
 
  public byte[] getContent(String url) throws Exception {
    
    // ダウンロード上限チェック
    if(parallelDownloadableNumber != -1 && counter >= parallelDownloadableNumber){
      throw new IllegalStateException("Already downloading threads over limited size. url=" + url);
    }
 
    counter++; // ダウンロード数をカウントアップ
    
    byte[] content;
    InputStream in = null;
    try{
      URLConnection con = new URL(url).openConnection();
      if(connectTimeout != null){
        con.setConnectTimeout(connectTimeout.intValue()); // require Java 5.0
      }
      if(readTimeout != null){
        con.setReadTimeout(readTimeout.intValue()); // require Java 5.0
      }
      in = con.getInputStream();
      content = toBytes(in);
    }catch(Exception e){
      throw new Exception("url=[" + url + "]", e);
    }finally{
      counter--; // ダウンロード数をカウントダウン
      if(in != null){
        in.close();
      }
    }
    return content;
    // http header の情報も取得したくなったら、専用のクラスを作って返せばいいし
  }
  
  private static byte[] toBytes(InputStream in) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    copy(in, out);
    // out.close(); // ByteArrayOutputStream を閉じても、何も変化はありません。
    return out.toByteArray();
  }
 
  private static void copy(InputStream in, OutputStream out) throws IOException {
    byte[] buff = new byte[256];
    int len = in.read(buff);
    while(len != -1 && len != 0){
      out.write(buff, 0, len);
      len = in.read(buff);
    }
  }
}

マルチスレッドで動作するか確認するためのテスト用メソッド testMultiThreads の出力結果。
ほぼ同時にアクセスしている5つのスレッドのうち、3つはOKだけど、その他2つが IllegalStateException になる。


NG:1: java.lang.IllegalStateException: Already downloading threads over limited size. url=http://www.nilab.info/zurazure2/myimage/zurazure_kohaku.png
NG:4: java.lang.IllegalStateException: Already downloading threads over limited size. url=http://www.nilab.info/zurazure2/myimage/zurazure_kohaku.png
OK:3
OK:0
OK:2
NG:3: java.lang.IllegalStateException: Already downloading threads over limited size. url=http://www.nilab.info/zurazure2/myimage/zurazure_kohaku.png
NG:4: java.lang.IllegalStateException: Already downloading threads over limited size. url=http://www.nilab.info/zurazure2/myimage/zurazure_kohaku.png
OK:0
OK:2
OK:1

testMultiThreads を作るときに参考にした無名クラスの挙動↓について。

実は無名クラスから、無名クラスの外側で定義された変数を参照することができなかったのはメソッド内で定義した変数だったからです。
メソッド内で定義した変数はメソッドの終了と同時に消失してしまうので、その変数をメソッドの終了よりも寿命の長い無名クラスから参照することは物理的に不可能というわけです。
この問題を解決するために以下のいずれかの方法を取ると良いでしょう。

・メソッド内で定義したfinal属性の変数を使用
・オブジェクト変数/スタティック変数

じゃばじゃば - Java Tips - 無名クラスへのパラメタの渡し方

tags: zlashdot Java Java

Posted by NI-Lab. (@nilab)