2012年03月30日

java.util.concurrent 〜CopyOnWriteArrayList〜


突然ですが、java.util.concurrent使ってますか?
僕は使ってません。また、使っている人をあんまり見たことがありません。

java.util.concurrentはJava5.0から追加された並列プログラミングの要求に応えるためのパッケージです。WEBアプリの開発者ならばほぼ間違いなく、そうでない人も結構な頻度で並列処理に注意を払わねばならない場面は多いと思います。しかしながら、前述の通り、java.util.concurrentを僕自身あまり使っていません。それはなぜか?真っ先に思いつく答えは「synchronizedで十分だから」というものでしょう。確かに、一般的な排他処理はsynchronizedで十分でしょう。しかし、明示的な排他処理は実に骨が折れる作業であるし、ソースの可読性も下がっていくことは確かです。

ただ、僕個人としてはjava.util.concurrentがなにがしか素敵な道具箱であることはわかっているものの「でもなんだかよく分からんから使いたくない」というのが正直なところです。ということで、java.util.concurrentをいろいろ試してみることにします。お勉強がてら。
初回はCopyOnWriteArrayListです。では行きます。

ArrayListがスレッドセーフでないのはよく知られています。ArrayListはリストの走査を行っている最中に要素の変更が加わるとConcurrentModificationExceptionが発生します。

以下のような場合です。

List<String> list = new ArrayList<String>();
Collections.addAll(list, "aa","bb","cc");
for(String h : list){
    System.out.println(h);
    list.remove(h);
}

実行結果は以下の通り。
aa
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
at java.util.AbstractList$Itr.next(AbstractList.java:343)
at test1.Main1.main(Main1.java:16)

ちょっと注意すべきなのが、このConcurrentModificationExceptionが発生するタイミングです。感覚的にはlist.remove(h)のような気もしますが、そうではなくfor(String h : list){の部分になります。なぜなら、このConcurrentModificationExceptionをスローするのはこのコレクションの反復子、つまりIteratorだからです。

上記の例では単一スレッドですが、当然マルチスレッドでも同じことが発生します。

public class ArrayListTest {

	//test対象となるリスト。ArrayListで宣言
	private static List<String> arrayList = new ArrayList<String>();

	/**
	 * @param args
	 */
	public static void main(String[] args) {

		//適当にいっぱい要素追加
		long time = System.currentTimeMillis();
		for(int i = 0; i < 10000; i++){
			arrayList.add("hogehoge" + i);
		}
		System.out.print("所要時間:");
		System.out.print(System.currentTimeMillis()-time);
		System.out.println("ミリ秒");

		//別スレッドでリストを走査する
		Thread th = new Thread(new Runnable(){
			@Override
			public void run() {
				for(String h : arrayList){			//ConcurrentModificationExceptionはここで発生します。
				}
			}
		});
		th.start();

		//走査中に操作するためにちょろっと待つ
		try {
			Thread.sleep(5);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		//操作中に削除
		arrayList.remove("hogehoge0");
		System.out.println("削除!");

	}

}

実行結果はこちら
所要時間:20ミリ秒
削除!
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
at java.util.AbstractList$Itr.next(AbstractList.java:343)
at copyOnWriteArrayListTest.ArrayListTest$1.run(ArrayListTest.java:30)
at java.lang.Thread.run(Thread.java:662)

この問題を解決すべく作られたのがCopyOnWriteArrayListさんです。CopyOnWriteArrayListは配列に対しておこなう全ての操作を配列を新規コピーすることで実装しているスレッドセーフなArrayListです。では上記のプログラムのListの宣言を以下のように変更してみましょう。

private static List<String> arrayList = new CopyOnWriteArrayList<String>();

実行結果はこちら。

所要時間:285ミリ秒
削除!

おお、うまくいきました。素晴らしいです。でもちょっと気になるのが処理時間。Stringの要素を10000件挿入するのにArrayListでは20ミリ秒だったのに大して、CopyOnWriteArrayListでは285ミリ秒もかかってます。10倍以上遅い・・・?というわけでもないのです。

試しに要素の挿入数を100000に増やしてみます。

ArrayListの結果がこちら。
所要時間:184ミリ秒
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
at java.util.AbstractList$Itr.next(AbstractList.java:343)
at copyOnWriteArrayListTest.ArrayListTest$1.run(ArrayListTest.java:30)
at java.lang.Thread.run(Thread.java:662)削除!

対してCopyOnWriteArrayListがこれ
所要時間:37085ミリ秒
削除!

うへえ。超遅い。でもよく考えれば当たり前で、CopyOnWriteArrayListは「変更のたびに全体をコピーする」ものです。つまり、全体の要素数が増えれば増えるほど、また総称型の引数のサイズが大きければ大きいほど、操作の処理時間は累乗的に増えていきます。

以上より、CopyOnWriteArrayListを使用すべき場面は以下のようになるでしょう。

@排他処理が必要
A変更の操作がほとんどない
Bリストサイズがそれほど巨大でない(作成時に時間がかかるから)
C読み取りは多い

とりあえず今回はこれまで
posted by sandman at 15:29| Comment(0) | Java | このブログの読者になる | 更新情報をチェックする

2011年11月25日

DVDデータ変換

DVDからAVI形式ファイルの変換をやってみます。

手順としては
1.DVD Decrypterダウンロード&インストール

2.BatchDOO!ダウンロード&インストール

3.DVD DecrypterでFileモードで取り込み

4.BatchDOO!でVOBをaviに変換

って感じです。
では、順番にいきましょう。

1.DVD Decrypterダウンロード&インストール

以下のサイトにアクセスして、ダウンロード〜インストールまで
済ませて下さい。
DVD Decrtpterダウンロード&インストール

日本語化のやり方も載ってますが、別に英語でも問題ないかと思います。
この手順ではメンドクサイから英語版の前提で説明します。
その場合、上記リンク先サイトの「ステップ2」までやっといてください。
もしzipの解凍ツールがない場合はここからどうぞ。

2.BatchDOO!ダウンロード&インストール

以下のサイトからダウンロードしてください。
BatchDOO!ダウンロード

上記の[Download]ボタンをクリックしてダウンロードします。
「BatchDOO.zip」というファイルが取得できたはずです。
それを解凍した中にある[BATCHDOO.exe]が起動ファイルです。
起動するときはこれをダブルクリックします。 これは後で使います。
初回起動時に提供元の要らないソフトインストールを促されるので、チェックをはずしてから
OKを押しましょう。不要なものが勝手にインストールされるのでうっとおしいです。

3.DVD DecrypterでFileモードで取り込み
ドライブに変換したいDVDをセットしたら、DVD Decrypterを起動します。
メニューから[Mode]-[File]を選択します。

続いて、[Destination]をクリックし、ファイルの出力先を決めます。
ちなみに沢山ファイルが出力されるので、デスクトップとかにフォルダを作成して、
そこを指定したほうがいいです。ここでは
デスクトップ\DVD_DATA
を出力先に設定しました。

こんな感じになると思います。

DVD_conv1

したらば、下側にある絵の箇所をクリックして、取り込みを開始します。
すると、以下のように処理が始まるのでしばし待ちます。
ちなみに、完了したときに音楽が鳴るので音量を大きくしているとびっくりしますので注意(笑)

DVD_conv2

出力先のフォルダを見てみましょう。なんだか色々とファイルがありますね。
この中のVOBファイルを変換していくことになります。

DVD_conv3

ただし、VOBファイルというのは実は中身mpeg2ですので、拡張子をmp2に変更すると普通に
windows media playerで視聴できます。ただし、問題は サイズがでかいってことですね。
なので、このそれが我慢できる人はこの時点でやめてもかまいません。

4.BatchDOO!でVOBをaviに変換
BatchDOO!を起動します。するとこんな画面が出てくると思います。

DVD_conv4

まず、変換したVOBを[対象ファイル]にドロップします。複数いっぺんで大丈夫です。
次に、保存先も決定。

このソフトは素晴らしいことに、面倒な各種設定が目的に応じて自動選択できるように
なっています。ここでは[PC向け-Xvid(中画質)]を選択してみましょう。

DVD_conv5

あとは右側の[GO]ボタンを押すだけ。簡単ですね。
ただ、けっこう時間かかります。PCにもよりけりですが。まあしばらくほっときましょう。

完了すると、保存先フォルダに.aviファイルができてますね。これで変換自体は完了です!

補足

VOBが分かれていれば当然ファイルが分かれてますね・・・
結合はたぶんできるんですが、色々試しているうちにメンドクサクなっちゃいました。
簡単にはできなそう。いいじゃん連続再生すれば・・・

どうしてもやりたい人は「UniteMovie」「VirtualDubMod」が必要なので、適宜調べてみてください(なげやり)。
たぶん、flv形式とかであれば簡単なんだと思います。たぶん。

posted by sandman at 13:18| Comment(0) | その他技術ネタ | このブログの読者になる | 更新情報をチェックする

2011年11月13日

OSGI入門 その3 〜Serviceの動的アップデート〜


さて、前回はupdateが上手くいかないところでしたね。まずはOSGIの仕様はBundleのupdateについて
どのように規定しているのでしょうか。仕様を見てみます。
※ちなみに、仕様のドキュメントはここからダウンロードできます。

The update process supports migration from one version of a bundle to a newer version of the same
bundle. The exports of an updated bundle must be immediately available to the Framework. If none
of the old exports are used, then the old exports must be removed. Otherwise, all old exports must
remain available for existing bundles and future resolves until the bundle is refreshed, see Refreshing
on page 148, or the Framework is restarted.

(訳)
updateプロセスはある同一バンドルに関して、古いバージョンから新しいバージョンへの移行をサポート
します。updateされるバンドルがexportしているサービスは直ちにフレームワーク内で有効化されなければなりません。
もし、古いexportがすべて使われなくなったなら、それは取り除かれなければなりません。それ以外の場合、すべての古い
exportはそのバンドルがrefreshされるか、フレームワークが再起動されるまで、既存バンドルおよび以降にインストール
されるバンドルから使用可能な状態に残しておかなければなりません。

なるほど、つまり変更を完全に有効にするにはOSGIフレームワークを再起動するか、「refresh」をかけろ
ということですね。もちろん、フレームワークの再起動をせずにやりたいので、refreshコマンドを使ってみましょう。

・refreshコマンドを使おう!

前回のプログラムをそのまま使います。適当な文字列でMyServiceImpleを修正してからrefreshをかけます。
すると・・・

osgi>refresh 2

osgi> ServiceConsumerBundle stop
ServiceBundle stop
ServiceBundle start
Service registed
ServiceConsumerBundle start
This is my service! version 2!
This is my service! version 2!
This is my service! version 2!

でけました。でもその前に標準出力をよく見てみると、ConsumerBundleも再起動されていることに気づきます。
つまり・・・

refresh→ConsumerBundle停止→ServiceBundle再起動→ConsumerBundle起動

となるわけです。要するに、refreshコマンドを打つと、対象バンドルがexportしているパッケージに依存しているバンドル
を停止し、対象バンドルを再起動してから、依存バンドルを起動するということのようですね。

うーん。なんか嫌ですね。できることなら、updateしたバンドルだけ再起動して終わりたいものです。
上記の仕様を見るかぎりだと駄目そうなんですが、実はこれやり方があります。といっても大したことではなく
バンドルの構成を変更するだけです。どうするかというと「IFと実装を別バンドルにする」これだけです。

・IFと実装を別バンドルに!

ではさっそくやってみましょう。といってもただIFと実装を分けるだけです。以下のバンドルを作成します。

■IFだけのバンドル
ServiceIFBundle

■実装バンドル
ServiceBundle2

■消費バンドル
ServieConsumer2

中身は前回と全く一緒。ただIFをMyServiceIFに移しただけです。もちろんIFがServiceIFBundleに移ったので
exportしているのはServiceIFBundleで残りの二つはそれをimportすることになります。

この3バンドルを構成して実行しましょう。

osgi> ServiceBundle start
Service registed
ServiceConsumerBundle start
This is my service!
This is my service!

ここまでは一緒ですね。ではMyServiceImplを適当に修正して、updateしてみましょう。

osgi> update 3
ServiceBundle stop
ServiceBundle start
Service registed
This is my service! update version
This is my service! update version
This is my service! update version

おお!ちゃんと新しいの取れてますね。素晴らしいです。

・IFバンドルと実装を別にすることについての補足

私が見る限り、OSGIの仕様として「IFと実装のバンドルはわけなさい」と規定しているわけではなさそうです。
ただ、ネットを調べてみると、この話題は議論されています。

Different osgi bundles with implementations of the same interface - where to place that inferface?

ここでの回答もやはり「分けた方がいい」とあります。理由は実装を後で置き換えられるということと、それから
対象バンドルを消費バンドルの再起動なしにhot replaceできるという理由が挙げられています。後者はまさに
今われわれが実験したことですね。

やはり、イディオムとしてはIFバンドルと実装バンドル分けておいたほうがいいんだと思われます。また、他の
利点としてはA実装バンドル⇔B実装バンドルというようにお互いのサービスを呼び合うような場合、IFと実装が
同居していると相互参照でエラーになっちゃいます。分けておけば、たすきがけのように呼び合えますね。

さて、次はServiceTrackerあたりをやろうかと思います。たぶん。

では。
posted by sandman at 16:07| Comment(0) | Java | このブログの読者になる | 更新情報をチェックする

広告


この広告は60日以上更新がないブログに表示がされております。

以下のいずれかの方法で非表示にすることが可能です。

・記事の投稿、編集をおこなう
・マイブログの【設定】 > 【広告設定】 より、「60日間更新が無い場合」 の 「広告を表示しない」にチェックを入れて保存する。


×

この広告は1年以上新しい記事の投稿がないブログに表示されております。