S-JIS[2003-07-06/2011-04-02] 変更履歴

Javaマルチスレッド・排他処理


スレッド

Javaでは、マルチスレッド用のクラスが用意されている。

class スレッドクラス名 extends Thread {

	@Override
	public void run() {
		ここにプログラムを書く。
		このルーチンを抜けると、スレッド処理は終了する。
	}
}

スレッドを使用(開始)するには、(他のクラスから)以下のようにしてstart()を呼ぶ。するとrun()が呼ばれる。

	スレッドクラス名 変数名 = new スレッドクラス名();
	変数名.start();

このやり方では他のクラスからの継承が出来ないため、マルチスレッド用のインターフェースも用意されている。

class ランクラス名 extends 何らかの派生元クラス implements Runnable {

	@Override
	public void run() {
		ここにプログラムを書く。
		このルーチンを抜けると、スレッド処理は終了する。
	}
}

このスレッドを使用するには、以下のようにする。

	ランクラス名 変数名 = new ランクラス名();
	Thread スレッド用変数 = new Thread(変数名);
	スレッド用変数.start();

一時的にスレッドを停止するときは、sleep()を使う。

	try {
		Thread.sleep(数値);	//ミリ秒単位
		Thread.sleep(m, n);	//ミリ秒+ナノ秒単位
	} catch (InterruptedException e) {
	}
//JDK1.5以降[2008-07-24]
	try {
		TimeUnit.SECONDS.sleep(数値);	//秒単位
		TimeUnit.MILLISECONDS.sleep(数値);	//ミリ秒単位
		TimeUnit.MICROSECONDS.sleep(数値);	//マイクロ秒単位
		TimeUnit.NANOSECONDS.sleep(数値);	//ナノ秒単位
	} catch (InterruptedException e) {
	}

例によって、TimeUnit#sleep()も、内部ではThread#sleep(ミリ,ナノ)を呼んでるわけだが。

自分のスレッドを一時的に休止して他のスレッドを動かすにはyield()を使う。[2008-07-24]

	Thread.yield();

スレッドが終了するのを待つには、join()を使う。[2006-04-15]

	スレッド用変数.join();

自分自身のスレッド(カレントスレッド)を取得するには以下の様にする。[2010-01-15]

	Thread t = Thread.currentThread();

synchronized

マルチスレッドで処理を行うと、排他が必要な場合がある。[2004-06-12]
Javaの場合、言語仕様として排他制御が定義されている。

synchronizedを使ってブロック化すると、そのブロックを実行するときに指定されたロックオブジェクトを使ってロックされ、同じロックオブジェクトを使ったブロックは同時には実行されなくなる。(後からロックをかけようとしたブロックは、ロックが解放されるまで(先にロックされたブロックの実行が終わるまで)実行が止まる。)

ロックオブジェクトは、他の言語の場合だとライブラリによって提供されることが多いが、JavaではObjectクラスのインスタンスを使えばよい。 (これはミューテックスロックに当たるものらしい(実際は、モニターという もっと高機能なものらしい))

	private Object lock = new Object();

	public void func1() {
		synchronized(lock) {
			〜
		}
	}

	public void func2() {
		synchronized(lock) {
			〜
		}
	}
	//↑同一インスタンスのfunc1()とfunc2()を別スレッドから同時に呼び出しても、片方ずつしか実行されない。
	// 同一インスタンスのfunc1()とfunc1()を別スレッドから呼び出すのも同様。
	// 別インスタンスであれば、ロックオブジェクトが異なるので排他されず、同時に実行される。


場合によっては、特別なロックオブジェクトを用意する必要は無い。(全てのクラスがObjectを継承しているから、どのクラスのインスタンスでもロックに使える)

	public void func(クラス val) {

		synchronized(val) {
			〜	//valに対する操作(他にvalを使う箇所でも同様にロックしないと意味を為さない)
		}

	}
	public void func() {

		synchronized(クラス.class) {
			〜	//クラスに対する操作(他にクラスを使う箇所でも同様にロックしないと意味を為さない)
		}

	}

メソッドにsynchronizedを指定した場合、そのメソッド内部をブロック化したのと同じ扱いになる。

	private static synchronized void func() {
		〜
		//複数スレッドから自分のクラスのfunc()が呼ばれた場合に排他され、
		//一度に1スレッド分しか実行されない
	}

			↑↓同じ

	private static void func() {
		synchronized(自分のクラス.class) {
			〜
		}
	}
	private synchronized void func() {
		〜
		//複数スレッドから自分のインスタンス(this)のfunc()が呼ばれた場合に排他され、
		//一度に1スレッド分しか実行されない
	}

			↑↓同じ

	private void func() {
		synchronized(this) {
			〜
		}
	}

なお、これらは動作上は同じ働きをするが、コンパイルされたバイトコード上は同一ではない。[2008-05-23]

Javaソース バイトコード
synchronized void func1() {
	System.out.println("abc");
}
0: getstatic #5; //Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6; //String abc
5: invokevirtual #7; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
void func2() {
	synchronized(this) {
		System.out.println("def");
	}
}
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #5; //Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #8; //String def
9: invokevirtual #7; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return

synchronizedブロックにした方(func2())は、モニターの処理が明示的に入ってきている。
つまりコンパイルされたファイルのサイズが大きくなるということか。
メソッド全体で排他するなるなら、メソッドの属性としてsynchronizedを付ける方が良さそうだ。

※synchronizedを付けたメソッドはリフレクションのisSynchronized()がtrueになるので、そこで区別しているのだろう。


メソッドにsynchronizedを付けた場合、それはそのメソッドのみに適用される[2008-05-23]
何が言いたいかと言うと、そのメソッドをオーバーライドした場合、オーバーライドしたメソッドは各々でsynchronizedを付けない限り、排他対象にならない
(synchronizedは継承されない)

class SyncSuper {
	protected int counter;

	public synchronized inc() {	//排他される
		counter++;
	}
}

class SyncSub extends SyncSuper {
	@Override
	public synchronized inc() { 	//synchronizedを付けないと、排他されない
		super.counter++;
	}
}

したがって、抽象メソッドにはsynchronizedを付けられない。(コンパイルエラー)
×protected synchronized abstract void method();


同一ロックオブジェクトに対してsynchronizedをネストすることが出来る。[2006-04-15]
すなわち、同一スレッドであれば、内側のsynchronizedで排他待ちになることは無い。

	public void func1() {
		synchronized(this) {
			func2();
		}
	}

	private void func2() {
		synchronized(this) {
			〜	//func1()でロックされているが、ちゃんと実行される
		}
	}

ちなみに、synchronized()に指定するロックオブジェクトがnullだと、NullPointerExceptionが発生する。[2008-05-12]

	private static Object lock = null;

		synchronized(lock) {	//NullPointerExceptionが発生する
			〜
		}

まぁ、そんなところでNullPointerExceptionになる事は滅多に無いだろうけど…。


synchronizedは負荷が高いと言われる。[2008-05-12]
そりゃ何も排他処理をしないよりは、排他処理をする(synchronizedを使う)方が負荷が高いに決まっている。
なのでsynchronizedは敬遠される傾向にある。

しかし、 あるオブジェクトが排他しないと使えない場合、
インスタンスを1つ作っておいて使う度に排他するのと
毎回インスタンスを生成する(排他しない)のとでは、
コンストラクターの負荷によっては、synchronizedで排他する方が実行速度は速いことがある。
インスタンス生成にかかる時間とsynchronizedにかかる時間の比較

一番シンプルなクラスを生成して複数スレッド並行で実行し、実行時間を測ってみた。(WindowsXP、JDK1.6)

class Data {
	public void m(){ /*何も処理しない*/ }
}
並行数 パターン1 パターン2
 
class Worker1 extends Thread {

  private static final Data data
    = new Data();

  @Override
  public void run() {
    for (int i = 0; i < 1000000; i++) {

      synchronized(data) {
        data.m();
      }
    }
  }

}
class Worker2 extends Thread {




  @Override
  public void run() {
    for (int i = 0; i < 1000000; i++) {
      Data data = new Data();

      data.m();

    }
  }

}
2 約870ms 約70ms
3 約1300ms 約80ms
10 約4200ms 約140ms

この例では、インスタンス生成もメソッド呼び出しも最小限の処理しか行っていない。
この場合、毎回インスタンスを生成していても、排他なんぞしない方が圧倒的に高速。

しかしSimpleDateFormatでは逆転し、synchronizedを使った方が速い。 (SimpleDateFormatはスレッドセーフではないので、排他する必要がある)
SimpleDateFormatの測定結果


wait(), notify()

synchronizedによるロックは、いずれか1つのみが動くことを保証する。逆に、どれが動くかは不定となる。
順序を保証したい場合には、Object#wait(),notify()が使えるかもしれない。[2006-04-15]

wait()はsynchronizedブロックの中で使用する。
wait()を呼び出すと、呼び出したスレッドはそこで実行を停止し、他のスレッドがsynchronizedの中に入って実行できる。
後から実行されたスレッドがnotify()を呼び出すと、そのスレッドがsynchronizedブロックから抜けた後にwait()を呼び出したスレッドが再実行される。
notify()を呼ばずにsynchronizedブロックを抜けると、wait()は永久に待機状態のままとなる!(Object#wait(long)ならタイムアウトするけど)

↓例

public void thread1() {
  System.out.println("W1 start");
  synchronized (lock) {
    System.out.println("W1 lock start");
    msleep("W1",200);
    try {
      System.out.println("W1 wait start");
      lock.wait();
      System.out.println("W1 wait end");
    } catch (InterruptedException e) {
    }
    msleep("W1",1000);
    System.out.println("W1 lock end");
  }

  System.out.println("W1 end");
}
public void thread2() {
  System.out.println("W2 start");
  synchronized (lock) {
    System.out.println("W2 lock start");
    msleep("W2",100);

    System.out.println("W2 notify start");
    lock.notify();
    System.out.println("W2 notify end");


    msleep("W2",1000);
    System.out.println("W2 lock end");
  }
  msleep("W2",1000);
  System.out.println("W2 end");
}

↓実行結果

W1 start
W1 lock start     ←スレッド1がロック獲得。
W1 sleep200 start
W2 start          ←スレッド2はロック待ち。
W1 sleep200 end
W1 wait start     ←スレッド1がwaitに入ると
W2 lock start     ←スレッド2がロック獲得。
W2 sleep100 start
W2 sleep100 end
W2 notify start
W2 notify end      ←スレッド2がnotifyを呼んだが
W2 sleep1000 start   …
W2 sleep1000 end     …
W2 lock end        ←スレッド2がロック終了するまで
W2 sleep1000 start   …
W1 wait end        ←スレッド1のwaitは終わらない。
W1 sleep1000 start
W2 sleep1000 end
W1 sleep1000 end
W2 end
W1 lock end
W1 end

その他の排他オブジェクト

排他に関して普通はsynchronizedを使えば充分だが、JDK1.5からは新しいクラスも用意された。[2007-03-26]

排他クラス 概要 参考
ReentrantReadWriteLock 読み書きロック(読み込み頻度が多くて書き込み頻度が少ないデータ用のロック) JavaWorldの新たな同期メカニズム
Semaphore セマフォ JavaWorldの新たな同期メカニズム

でも こういうクラスの形で提供されるんじゃ、「Javaでは排他制御が言語仕様(synchronized)でサポートされている」という宣伝文句が色褪せるよね〜。
今回のクラスもsynchronizedの文法を拡張すればよかったのに。synchronized_readとかsynchronized_writeとか(爆)
ま、そういう事すると どんどんクドイ言語になるだろうけど(苦笑)

リードライトロックのサンプル:

class RWCounter {
	private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

	private volatile int value = 0;

	public void add(int n) {
		WriteLock w = lock.writeLock();
		try {
			w.lock();
			value += n;
		} finally {
			w.unlock();
		}
	}

	public int get() {
		ReadLock r = lock.readLock();
		try {
			r.lock();
			return value;
		} finally {
			r.unlock();
		}
	}
}

MTセーフなコレクション

JDK1.5で、ArrayListやHashMapに対するMT(マルチスレッド)セーフなコレクションクラスが追加された。[2007-03-26]

MTセーフなクラス 該当クラス 参考 更新日
ConcurrentHashMap 1.5 HashMap JavaWorldの並行処理に対応したコレクション  
ConcurrentSkipListMap 1.6 TreeMap   2011-04-02
CopyOnWriteArrayList 1.5 ArrayList JavaWorldの並行処理に対応したコレクション  
CopyOnWriteArraySet 1.5   JavaWorldの並行処理に対応したコレクション  
ConcurrentLinkedQueue 1.5 LinkedList ITproの両端キュー:デック 2007-12-07

Hashtable(ハッシュテーブル)も 機能的にはHashMapのMTセーフ版(メソッドがことごとくsynchronizedで排他されている)らしい。
(Vectorも ArrayListをsynchronizedで同期化したようなクラス)
また、HashMapのJavadocには 同期化する為にCollections.synchronizedMap()を使うよう書かれている。
だが、上記の新規クラスの方が高速(になるように設計されている)らしい。

→HashMapを同期化しなかった場合の障害の例: @ITのThreadとHashMapに潜む無限回廊は実に面白い? (2008-05-17)


ThreadLocal

ThreadLocalは、スレッド毎の値を保持する為のクラス。[2008-07-10]
あるクラス(のインスタンス)がマルチスレッドで呼ばれる際に、スレッド毎に異なる値(インスタンス)を使いたい場合に使用する。

自分でThread(やRunnable)を使ってマルチスレッド化しているなら スレッド毎の値なんて簡単に保持できるので、ThreadLocalには意味が無い。
しかしウェブアプリ(サーブレット)1つのインスタンスがマルチスレッドで呼ばれるので、そのスレッド毎に別のインスタンスを保持したい時にはとても便利。

ThreadLocalを継承したサブクラスを作り、そのinitialValue()をオーバーライドして実装しておく
このメソッドは、スレッド内で値を取得する際に“そのスレッド専用の値”を生成する為に呼ばれる。(そのスレッド用の値をクリアしない限り、一スレッドにつき一回だけ呼ばれる)

/** SimpleDateFormatはスレッドセーフでないので、ThreadLocalを使ってスレッドセーフにしてみる例 */
public class DateFormatLocal extends ThreadLocal<DateFormat> {

	private String pattern;

	/** コンストラクター */
	public DateFormatLocal(String pattern) {
		this.pattern = pattern;
	}

	@Override
	protected DateFormat initialValue() {
		//スレッド毎の初期化(1スレッドにつき1回だけ呼ばれる)
		return new SimpleDateFormat(pattern);
	}
}
	private static DateFormatLocal dflocal = new DateFormatLocal("yyyy/MM/dd");

	public void method() {	//マルチスレッドで呼んでも平気
		DateFormat df = dflocal.get();
		Date date = df.parse("2008/07/10");
	}

上記のpatternが固定でいいなら、無名クラスを利用してシンプルにすることも出来る。

	private static ThreadLocal<DateFormat> dflocal = new ThreadLocal<DateFormat>() {

		@Override
		protected DateFormat initialValue() {
			//スレッド毎の初期化(1スレッドにつき1回だけ呼ばれる)
			return new SimpleDateFormat("yyyy/MM/dd");
		}
	}

	public void method() {	//マルチスレッドで呼んでも平気
		DateFormat df = dflocal.get();
		Date date = df.parse("2008/07/10");
	}

マルチスレッド(ウェブ)ではSimpleDateFormatはThreadLocalを使うのが最も高速


ThreadLocalは、スレッドIDをキーにして生成した値を保持するMapのようなもの。
スレッドが終了すると ThreadLocalに保持していた“そのスレッド用の値”が自動的に消去される(GCの対象になる)ので、いつまでもゴミが残りっぱなしになったりはしない。
(これには、弱参照という仕組みWeakReferenceクラス)が使われている)

なので逆に、スレッドが毎回生成されて破棄されるような作りだと、ThreadLocalは値を保持せず毎回生成することになるので、意味が無い。
ウェブアプリ(サーブレットコンテナ)だと たぶんスレッド数の上限が決まっていてスレッドプール内のスレッドが使い回される為、ThreadLocalにとっても効率がいいだろう。


volatile

クラスのフィールド(メンバー変数)に対して付けることが可能な修飾子。[2008-04-17]

JavaVMのバイトコードよりさらにのレベル(CPUが実行するマシン語のレベル)では、変数の値は(スレッド毎の)キャッシュメモリーに格納されて、変数の参照・更新はキャッシュメモリーに対して行われる事がある。(その方が高速化できるから)
もちろんある時点でキャッシュメモリーから実メモリー(変数の本来の格納場所)へ格納されて各スレッド共通の値となる。

これがスレッド毎に行われる為、マルチスレッドにおいては、同じ変数なのに自分のスレッドの値と他スレッドの値が異なることが起こり得る。
で、volatileを付けると、「あるスレッドで更新された値が別スレッドで読み込まれる」ことが保証される。

volatileを付けていないと、以下のような状況が発生する可能性がある。
(フィールド「int value;」と各スレッドのローカル変数nがあるものとする)

時系列 処理 説明 処理後のvalueの値
スレッドAの処理 スレッドBの処理 理論的な
(期待した)値
スレッドA用
キャッシュ
スレッドB用
キャッシュ
フィールド値
初期       0     0
1 int n = this.value;   フィールドからキャッシュにコピーされる。 0 0←   0
2 this.value = n + 1;   valueが更新されたが、キャッシュの中のみ。 →1 →1   0
3   int n = this.value; フィールドからキャッシュにコピーされる。 1 1 0← 0
4   this.value = n + 1; valueが更新されたが、キャッシュの中のみ。 →2 1 →1 0
5     変更されたスレッドAのキャッシュの内容がフィールドに反映される。 2 1→ 1 1
6     変更されたスレッドBのキャッシュの内容がフィールドに反映される。 2 1 1→ 1

valueが「volatile int value;」なら、自スレッドが値を取得する際に、別スレッドの変更内容が必ずフラッシュ(本来のフィールド値のエリアに格納)される

時系列 処理 説明 処理後のvalueの値
スレッドAの処理 スレッドBの処理 理論的な
(期待した)値
スレッドA用
キャッシュ
スレッドB用
キャッシュ
フィールド値
初期       0     0
1 int n = this.value;   フィールドからキャッシュにコピーされる。 0 0←   0
2 this.value = n + 1;   valueが更新されたが、キャッシュの中のみ。 →1 →1   0
3   int n = this.value; 他スレッドのキャッシュの中に変更があると、その内容がフィールドに反映される。 1 1→   1
フィールドからキャッシュにコピーされる。 1 1 1← 1
4   this.value = n + 1; valueが更新されたが、キャッシュの中のみ。 →2 1 →2 1
5     変更されたスレッドBのキャッシュの内容がフィールドに反映される。 2 1 2→ 2

volatileは他スレッドで値が更新されていないかチェックする為、多少実行が遅くなる


ただし、volatileを付けても原子性が担保されるわけではない。すなわち、valueの取得valueへの設定が不可分に行われるわけではない。
(スレッドセーフになるわけではない)

時系列 処理 説明 処理後のvalueの値
スレッドAの処理 スレッドBの処理 理論的な
(期待した)値
スレッドA用
キャッシュ
スレッドB用
キャッシュ
フィールド値
初期       0     0
1 int n = this.value;   フィールドからキャッシュにコピーされる。 0 0←   0
2   int n = this.value; フィールドからキャッシュにコピーされる。 0 0 0← 0
3 this.value = n + 1;   valueが更新されたが、キャッシュの中のみ。 →1 →1   0
4   this.value = n + 1; valueが更新されたが、キャッシュの中のみ。 →2 1 →1 0
5     変更されたスレッドAのキャッシュの内容がフィールドに反映される。 2 1→ 1 1
6     変更されたスレッドBのキャッシュの内容がフィールドに反映される。 2 1 1→ 1

this.value++;」にしたところで、コンパイルしたバイトコードのレベルでは(ですら)以下のように複数ステップに分かれている。

 10: getfield #20; //Field jp/hishidama/sample/thread/Data.value:I
 13: iconst_1
 14: iadd
 15: putfield #20; //Field jp/hishidama/sample/thread/Data.value:I

●マルチスレッドプログラミングでは、ソースコードのレベルではなく、CPUが実行するマシン語のレベルで物を考えなければならない。

(ちなみに、バイトコードのレベルでは、volatile付きとそうでないフィールドの取得・設定方法には違いが無い模様(上記のバイトコードと全く同じ)。
ただし実行速度は異なるので、get/put内ではちゃんと何かやっているのだろう)

なお、排他した(同期をとった)カウンターに関しては、JDK1.5で導入されたAtomicIntegerが便利。


volatileの使い道は、IBMの『Javaの理論と実線: volatileを扱う』が詳しい。
(難しくて半分以上よく分からない(爆)けど、リスト6の安価な読み書きロックは目からウロコ)


AtomicInteger

JDK1.5で、原子性を担保するクラスが導入された。[2008-04-17]
原子性(アトミックである)とは、値の取得と更新が不可分である(その間に別スレッドからの割り込みが入らない)のが保証されていること。

単純な話、synchronizedを使って排他すれば原子性が確保される。

class Atomic {
	private int value = 0;

	public synchronized void add(int n) {
		value += n;
	}

	public int get() {
		return value;
	}
}

しかしシンプルなカウンターなら、synchronizedは負荷が高い。JDK1.5で導入されたAtomicIntegerAtomicLongを使うとよい。

class Atomic {
	private AtomicInteger value = new AtomicInteger(0);

	public void add(int n) {
		value.addAndGet(n);
	}

	public int get() {
		return value.get();
	}
}

addAndGet()メソッドは、最終的にnativeメソッドを呼び出している。
原子性の担保はCPUの“テスト&セット命令”を使わないと実現できないので、それを呼び出す実装になっているのだろう。


実験:(WindowsXP・JDK1.6)
以下のようなDataクラスを1つだけインスタンス化し、2スレッドから100万回ずつinc()を呼び出して加算してみた。

class Data {
	int n = 0;

	volatile int vol = 0;

	AtomicInteger atom = new AtomicInteger(0);

	int s = 0;

	RWCounter rw = new RWCounter();

	public void inc() {
		n++;
		vol++;
		atom.addAndGet(1);
//		synchronized (this) { s++; }
		rw.add(1);
	}

	@Override
	public String toString() {
		return "int=" + n + ", volatile=" + vol + ", atom=" + atom.intValue() + ", sync=" + s + ", rw=" + rw.get();
	}
}

結果:

int=1999194, volatile=1999132, atom=2000000, sync=0, rw=2000000
int=1998732, volatile=1998050, atom=2000000, sync=0, rw=2000000
int=1999192, volatile=1999162, atom=2000000, sync=0, rw=2000000

synchronizedはコメントアウトしているが、それを有効にするとnvolも影響を受けて排他された状態に近い値になる。

int=1999998, volatile=1999998, atom=2000000, sync=2000000, rw=2000000

なお、個別に実行して時間を測ったら、おおよそ以下のようになった。 (100万回ループ×2スレッドが完了する時間)

書き込み処理をループ
修飾子なし volatile AtomicInteger synchronized 読み書きロック
30ms 60ms 180〜200ms 900ms 1000ms

※修飾子なしとvolatileは速いが、計算結果が正しくないので論外

しかしこの実験は書き込みしか行っていないので、読み書きロックにとっては非常に不利。
(なお、RWCounterのvolatileを外すと700〜800ms程度になってsynchronizedより速くなったが、 たぶん外すとまずいだろう)

なので、読み込みのみも実験してみた。[2008-04-18]
上記のinc()の代わりに、下記のget()を呼び出した。

	public int get() {
		return n;
//		return vol;
//		return atom.get();
//		synchronized (this) { return s; }
//		return rw.get();
	}
読み込み処理をループ
修飾子なし volatile AtomicInteger synchronized 読み書きロック
2.9ms 3.1ms 3.1ms 840〜860ms 460〜560ms

※修飾子なし・volatile・AtomicIntegerは速すぎるので、1スレッド当たり1億回ループさせて100で割った時間(他は100万回ループの時間)

読み書きロックの方がsynchronizedより速くなって面目を保ったとは言え、さらにAtomicIntegerの方が圧倒的に高速。

単純な排他(同期)カウンターならAtomicIntegerを使うのが断然良さそうだ。
この調子なら、AtomicBoolean(のcompareAndSet()メソッド)なんかも便利に使えるかもしれない。


Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま