谷本 心 in せろ部屋 このページをアンテナに追加 RSSフィード

2016-12-08

Optimizing JavaというJavaパフォーマンス系の書籍が面白そう

急激な冷え込みのせいで「寒い!」というつぶやきがTLに散見されるこの頃ですが、皆さんお風邪など召していらっしゃらないでしょうか。

否応なしに寒いという言葉に反応してしまう、けなげなエンジニアの @ です。


このエントリーは Java Advent Calendar 2016 の8日目です。

昨日は @ さんの「Java Stream APIでハマったこと」で、

明日は @ さんの「マイクロベンチマークツール、JMHについて」でした。


今日のエントリーでは、Javaのパフォーマンス系書籍を紹介したいと思います。

Optimizing Java - O’Reilly Media

URLを見るにつけ、あのオライリー様のサイトですら拡張子が由緒正しい .do なのですから、日本のSIerStrutsを使うことをどうして否定できましょうか。

いえ、今日はそんな話題ではありません。


紹介したいのは上のリンク先の本、「Optimizing Java - Practical Techniques for Improved Performance Tuning」です。名前の通り、Javaのパフォーマンスに関する書籍です。まだEarly Releaseの段階で、全体の1/3ほどしか書かれていませんが、現状の版を入手したので紹介したいと思います。


ここまでで、「あれ、なんか似たような本がなかったっけ」と思った方がいらっしゃるかも知れません。そう、オライリー社からは2015年に「Javaパフォーマンス」という書籍が出版されています。

Javaパフォーマンス - O’Reilly Japan

こちらの日本語版では、私も監訳者まえがきを書かせて頂き、Java Day Tokyoで寺田佳央さんと共にサイン会を行いました。

当時はきっと「この寺田さんの横にいて本に落書きしてる人、誰なんだろう」と思われていたかも知れませんが、私を誰だと思ってるんでしょう、せろさんだぞ?


この2冊について、比較しながら紹介しましょう。


目次

Javaパフォーマンス」の目次は、次の通りです。

1章イントロダクション
2章パフォーマンステストのアプローチ
3章Javaパフォーマンスのツールボックス
4章JITコンパイラのしくみ
5章ガベージコレクションの基礎
6章ガベージコレクションアルゴリズム
7章ヒープのベストプラクティス
8章ネイティブメモリのベストプラクティス
9章スレッドと同期のパフォーマンス
10章Java EEのパフォーマンス
11章データベースのベストプラクティス
12章Java SEAPIのパフォーマンス

JavaのメモリやGCスレッドに関する紹介から、SE / EEやデータベースのパフォーマンスに広げた話をしています。


一方、「Optimizing Java」の目次は次の通りです。

Chapter 1Optimization and Performance Defined
Chapter 2Overview of the JVM
Chapter 3Hardware and Operating Systems
Chapter 4Performance Testing
Chapter 5Measurement and Bottom-Up Performance
Chapter 6Monitoring and Analysis
Chapter 7Hotspot GC Deep Dive
Chapter 8Garbage Collection Monitoring and Tuning
Chapter 9Hotspot JIT Compilation
Chapter 10Java Language Performance Techniques
Chapter 11Profiling
Chapter 12Concurrent Performance Techniques
Chapter 13The Future

うん、ほとんど一緒やん?


「Optimizing Java」には、「Javaパフォーマンス」では触れられていたSEやEEの話などはないため、そこが差分になりそうにも見えます。ただ正直、「Javaパフォーマンス」の10章以降はちょっと薄口な感じでしたので、そこを飛ばせばほとんど同じ内容を網羅していると言えます。


では、何が違うんでしょうか。


Javaパフォーマンス vs Optimizing Java

僕が見た限りでは「Javaパフォーマンス」は教科書に近い内容、「Optimizing Java」はやや読み物寄りの内容になっています。

「Optimizing Java」は、現在執筆されているChapter 5までしか読めていませんが、「Javaパフォーマンス」には書かれていなかったOSJVM周りのレイヤーの話や、テスト戦略の話など、少し目線が違った内容を書いていました。


たとえば、Javaのクラスファイルが「0xCAFEBABE」から始まっていることは、Javaに詳しい方なら既にご存じかと思います。ただ、その先はどうなっているのか。

書籍では次のように紹介されています。

  • Magic Number (0xCAFEBABE)
  • Version of Class File Format
  • Constant Pool
  • Access Flags
  • This Class Name
  • Super Class Name
  • Interfaces
  • Fields
  • Methods
  • Attributes

この先頭を取って

M V C A T S I F M A、

語呂合わせして

My Very Cute Animal Turn Savage In Full Moon Areas

なんて紹介されています。


「僕のとってもかわいい猫は、満月のエリアで凶暴になる」

・・・覚えやすいんですかね、これ?


あ、なんかふざけた本だなと思ったかも知れませんが、もちろん技術的な面もきちんと紹介されています。

あくまで上に書いたようなウィット(?)も挟みながら、Javaの領域だけでなく、必要に応じて低レイヤーにも触れて紹介する本となっているわけです。そのため、「Javaパフォーマンス」を読んだ方でも楽しめる本になるのではないかと思います。


で、いつ出るの? 日本語版は?

この本は2017年3月に出版予定となっています。


また、皆さん気になる日本語版ですが、残念ながらまだ翻訳されることは決まっていないようです。

ただ原著の人気が高かったり、この後に公開される6章以降の内容が「Javaパフォーマンス」とはまた違った切り口であり楽しめるのであれば、翻訳される可能性も十分にあるんじゃないかなと思っています。


そんなわけで、日本語版が出ることを祈りながら、このエントリーを書きました。

Stay tuned, see you!

2016-01-06

[]AWS Lambda + Javaは、なぜ1回目と3回目の処理が重いのか?

以前のエントリーで、AWS LambdaでJavaを使ってDynamoDBを呼び出した際に、初回起動にとても時間が掛かったという話を書きました。

http://d.hatena.ne.jp/cero-t/20160101/1451665326


今回は、この辺りの原因をもう少し追求してみます。


なぜ1回目と3回目のアクセスが遅いのか?

AWS Lambdaの中身はよく知りませんが、おそらく、アップロードしたモジュールTomcatみたいなコンテナとして起動させて、外部からコールしているんだろうと予想しました。それであれば、2回目以降のアクセスが早くなることは理解ができます。

ただ、1回目と3回目だけが極端に遅くて、2回目、4回目以降は早くなるというところは腑に落ちません。


その辺りを調べるべく、staticなカウンタを使って、値がどんな風に変化するかを調べてみました。

こんなソースコードです。

public class Pid {
    static AtomicLong counter = new AtomicLong();

    public String myHandler() {
        long count = counter.incrementAndGet();
        String name = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println("Name: " + name);
        System.out.println("Count: " + count);
        return "SUCCESS";
    }
}

出力された結果は、次のようになりました。

回数NameCount
1回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal1
2回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal2
3回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal1
4回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal2
5回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal3
6回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal4
7回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal5
8回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal3
9回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal4
10回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal5
11回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal6
12回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal7

サーバIPアドレスが2種類あり、それぞれのサーバで、1から順番にカウントアップしていることが分かります。

なるほど、2台のサーバでロードバランシングしているのだと。そのため、それぞれのサーバの初回起動である、1回目と3回目の処理に時間が掛かるのですね。なかなか納得いく結果でした。

ちなみにロードバランシングは毎回このような結果になるわけではなく、1回目と2回目がそれぞれ別のサーバに行く(=処理に時間が掛かる)こともあります。


どんなコンテナを使っているのか?

先ほど「Tomcatみたいなコンテナ」を使っているんじゃないかと推測しましたが、実際、どんなコンテナを使っているのでしょうか。スレッドダンプを取って、確かめてみました。


こんなコードです。

public class StackTrace {
    public String myHandler() {
        new Exception("For stack trace").printStackTrace();
        Arrays.stream(ManagementFactory.getThreadMXBean().dumpAllThreads(true, true))
                .forEach(System.out::println);
        return "SUCCESS";
    }
}

結果、こうなりました。

java.lang.Exception: For stack trace
	at cero.ninja.aws.analyze.StackTrace.myHandler(StackTrace.java:8)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at lambdainternal.EventHandlerLoader$PojoMethodRequestHandler.handleRequest(EventHandlerLoader.java:434)
	at lambdainternal.EventHandlerLoader$PojoHandlerAsStreamHandler.handleRequest(EventHandlerLoader.java:365)
	at lambdainternal.EventHandlerLoader$2.call(EventHandlerLoader.java:967)
	at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:231)
	at lambdainternal.AWSLambda.<clinit>(AWSLambda.java:59)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at lambdainternal.LambdaRTEntry.main(LambdaRTEntry.java:93)

"Signal Dispatcher" Id=4 RUNNABLE

"Finalizer" Id=3 WAITING on java.lang.ref.ReferenceQueue$Lock@19bb089b
	at java.lang.Object.wait(Native Method)
(略)

"Reference Handler" Id=2 WAITING on java.lang.ref.Reference$Lock@4563e9ab
	at java.lang.Object.wait(Native Method)
(略)

"main" Id=1 RUNNABLE
	at sun.management.ThreadImpl.dumpThreads0(Native Method)
	at sun.management.ThreadImpl.dumpAllThreads(ThreadImpl.java:446)
	at cero.ninja.aws.analyze.StackTrace.myHandler(StackTrace.java:9)
(略)

何やらシンプルな独自コンテナを使っているみたいです。何度か実行してみても結果は同じでした。

ソースコードがないので推測になりますが、アップロードされたzipをロードして起動する独自コンテナがあり、外部からAPIコールされた際にAWSLambdaクラスあたりが処理を受け取って、zip内のハンドラを呼び出しているのでしょう。


なぜコンストラクタで処理すると早いの?

そういえば、もう一つ、謎な挙動がありました。

それは、Credentialsを取るという重めの処理をコンストラクタで実行すれば、処理時間がかなり短くなるというものです。


ハンドラの中でCredentialsを取ると、実測値で24秒ぐらい、課金対象値で22秒ぐらいでした。

コンストラクタでCredentialsを取っておくと、実測値で8秒ぐらい、課金対象値で6秒ぐらいでした。

ここでいう実測値とは、手元のストップウォッチを使って計測したという意味です。


ここから推測できることは、コンストラクタは事前に処理されていて、そこは課金対象外になるのかも知れません。


・・・あれ、それなら、コンストラクタで重い処理をがっつり走らせて、ハンドラでその結果を取り出せば、課金額を抑えられるじゃないですか?

ということで、ハンドラの中で10秒スリープする場合と、コンストラクタスリープした場合の比較をしてみました。


こんな2つのクラスで試してみます。

public class Wait1 {
    static long origin = System.currentTimeMillis();

    public String myHandler() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(10000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "SUCCESS";
    }
}
public class Wait2 {
    static long origin = System.currentTimeMillis();

    public Wait2() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(10000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String myHandler() {
        System.out.println("Called: " + (System.currentTimeMillis() - origin));
        return "SUCCESS";
    }
}

結果は、、、


Wait1(ハンドラの中でsleep)

回数Before waitAftre wait課金対象値実測値
1回目3901039010100ms10秒
2回目3871038710100ms10秒
3回目209173091710100ms10秒
4回目688527885210100ms10秒

きっちり10秒sleepして、課金対象値もそのオーバーヘッド分ぐらい。ストップウォッチで計測した値も同じく10秒ぐらいになりました。


Wait2(コンストラクタでsleep)

回数Before waitAftre wait課金対象値実測値
1回目3381033814800ms25秒
2回目--100ms1秒以下
3回目2611035814800ms25秒
4回目--100ms1秒以下

えーっ、sleepは10秒だったのに、なぜか15秒分ぐらい課金されてしまい、ストップウォッチで計測すると25秒と、えらく時間が掛かりました。これは謎な挙動です。

2回目や4回目ではインスタンス生成が終わっているので、Before waitやAfter waitが出力されず、処理がすぐに終わるというのは納得ですが。


どうして、こんなことが起きるんでしょうか。

不思議に思って、CloudWatch Logsのログを確認してみると・・・

Before wait: 17 
START RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f Version: $LATEST 
Before wait: 249 
After wait: 10250 
Called: 14408
END RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f 
REPORT RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f	Duration: 14531.57 ms	Billed Duration: 14600 ms Memory Size: 128 MB Max Memory Used: 27 MB	

最初のBefore waitの後にAfter waitがなく、Lambdaの処理がSTARTした後に再度Before waitが呼ばれ、After waitした後に、4秒ほど待ってから、ハンドラ処理が実行されてCalledが呼ばれていました。

なるほど、つまりこういうことでしょうか。


1. コンストラクタが実行され、10秒sleepの途中でタイムアウトして強制的に処理が止められ、インスタンス生成を諦めた(プロセスごと破棄された?)

2. 改めてコンストラクタが実行され、10秒sleepした。

3. AWS Lambda内の処理か何かで4秒ぐらい処理が掛かった。

4. ハンドラが実行された。

5. 2〜4の間が課金対象となり、15秒弱となった。

6. ストップウォッチで計測した時間は1から5までの間なので、25秒弱となった。


要するに、コンストラクタで重い処理を行うような悪いことを考える人への対策として、コンストラクタは一定時間で(おそらく10秒きっかりで)タイムアウトして、いったんプロセスは破棄される。

その後、改めてコンストラクタの処理がタイムアウト関係なく実行されたうえで、AWS Lambdaの内部処理と、ハンドラ処理が行われ、すべての処理が課金対象となる、ということころでしょうか。


コンストラクタの処理が短い場合は、どうなるの?

ということで、sleepの時間を短くして、再挑戦してみます。

public class Wait3 {
    static long origin = System.currentTimeMillis();

    public Wait3() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(2000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String myHandler() {
        System.out.println("Called: " + (System.currentTimeMillis() - origin));
        return "SUCCESS";
    }
}

結果。

回数Before WaitAfter wait課金対象値実測値
1回目--100ms3秒
2回目--100ms1秒以内
3回目--100ms1秒以内

CloudWatch Logsでの出力

Before wait: 25 
After wait: 2026 
START RequestId: xxx Version: $LATEST 
Called: 2389 
END RequestId: xxx 
REPORT RequestId: xxx	Duration: 97.52 ms	Billed Duration: 100 ms Memory Size: 128 MB	Max Memory Used: 27 MB	 
START RequestId: yyy Version: $LATEST 
Called: 16385 
END RequestId: yyy 
REPORT RequestId: yyy	Duration: 6.04 ms	Billed Duration: 100 ms Memory Size: 128 MB	Max Memory Used: 27 MB	

今度はSTARTの前に、きちんとBefore waitもAfter waitも出力され、ハンドラ処理のみが課金対象となっていました。


まとめ

ここまでの話をまとめると、AWS Lambdaは・・・


1. 複数台のサーバで処理されるため、それぞれのサーバでの初回起動時には処理時間が掛かる。

2. 独自のコンテナを利用して、モジュールデプロイしている。

3. コンストラクタの処理が軽い場合は、ハンドラ内の処理だけが課金対象となる。

4. コンストラクタの処理が重い場合は、コンストラクタの処理 + 5秒弱 + ハンドラ内の処理が課金対象となる。


ということですね。


・・・とは言え、Credentialsの処理をコンストラクタで行った場合に、実測値まで早くなる辺りは、少しだけ不可解です。というか、Credentialsの取得処理が重いこと自体が不可解なのですが。

この辺りはもう少し追試験をしてみれば解析できそうですが、長くなるので、今回はこの辺りまでにしたいと思います。


いやー、Lambdaさん、なかなかよく考えられてますね!

2012-01-12

[]Nullセーフを体感してみる。

Kotlinの一番簡単のサンプルは、これ。

世界で最もカバーされた言語数が多いと言われる、K&Rのハローワールドです。

fun main(args : Array<String>) {
  System.out?.println("Hello, world!")
}

見どころは、outの後ろについてる「?」ですね。


Kotlinは言語としてNullPointerExceptionを起こさないこと、

つまりNullセーフな性質を持っているのです。

Nullになりそうな所には「?」を記述する必要があり、

要するに、「?」は「Nullかも?」ぐらいに理解すると良さそうです。


詳しい解説は、このページにあります。

http://confluence.jetbrains.net/display/Kotlin/Null-safety

これを見ながら、サンプルを書き換えてみました。

自前でNullチェックすればいいんだよ。

まずは自分でNullチェックする方法を試してみましょう。

fun main(args : Array<String>) {
  val out : java.io.PrintStream = System.out
  if (out != null) {
    out.println("Hello, world!")
  }
}

残念ながらコンパイル通らず!(><)

こんなエラーメッセージが出ました。

(2, 35) : Type mismatch: inferred type is PrintStream? but PrintStream was expected

System.outは「PrintStream?」なのに、「PrintStream」な変数に代入しようとしている、

と怒られてしまいました。


なるほど、

「PrintStream?」つまり「Nullかも知れないPrintStream型」は、

「PrintStream」とは違う型なのだと、そういうことのようです。


では、PrintStreamに「?」を追加してみましょう。

fun main(args : Array<String>) {
  val out : java.io.PrintStream? = System.out
  if (out != null) {
    out.println("Hello, world!")
  }
}

今度は無事に実行ができて、Hello, world!がコンソールに出力されました。


そういえば、変数にはvalとvarがあるんでしたっけ。

普通に代入ができるvarと、再代入できない(final扱いの)val。


valをvarにしたらどうなるか、試してみましょう。

fun main(args : Array<String>) {
  var out : java.io.PrintStream? = System.out
  if (out != null) {
    out.println("Hello, world!")
  }
}

残念ながら、安全な呼び出しじゃないと怒られてしまいました。

(4, 8) : Only safe calls (?.) are allowed on a nullable receiver of type PrintStream?

Nullチェックをした後に、outにnullを入れられたらNPEが起きるからでしょうか?


ドキュメントを読む限りはこの書き方でも問題なさそうなのですが、

少なくとも今のデモバージョンではエラーが起きてしまいました。

「俺」が保証する、sure!

Kotlinにはsure()というメソッドも用意されています。

fun main(args : Array<String>) {
  var out : java.io.PrintStream = System.out.sure()
  out.println("Hello, world!")
}

「System.outはNullになんねぇよ、『俺』が保証するよ!」というのがsureの考え方ですね。

ただ、あまりsureを使いすぎると、せっかくのNullセーフが活かされなくなってしまいそうです。


試しにこんなコードを書いてみたら。

fun main(args : Array<String>) {
  System.out = null
  var out : java.io.PrintStream = System.out.sure()
  out.println("Hello, world!")
}

エラーが出て、開発者にバグレポートが飛んでしまいました。

Exception in Kotlin compiler: a bug was reported to developers.

Exception in thread "main" java.lang.IllegalAccessError

at namespace.main(dummy.jet:2)

開発者の人、すみません m(_ _)m

Nullかも知れない変数が、実際Nullだったらどうなるの?

ここまではNullチェックを中心に書いてきましたが、

最後に、変数がnullだった場合にどうなるのか、試してみましょう。

fun main(args : Array<String>) {
  var a : String? = null
  System.out?.println(a?.substring(0))
}

JavaならNPEが飛び出すパターンですが、

Kotlinの場合、結果はコンソールに「null」と表示されました。


Kotlinは、まずaがnullかどうかの判定を行ない、

aがnullだった場合はsubstringを実行せず、

「とりあえずnullを返す」ように振る舞うようです。


Javaな皆様方におきましては

「え、そんな勝手な振る舞いするの?」という印象をお持ちかも知れませんが、

このサンプルの場合は「a?」と自分で書いているわけで、

「aがnullだった場合は、nullが返る」ことを自分で気づくことができるのです。


ソースコードが「?」まみれになっていない限りは、ですが。

まとめ : 要するに「?」をつけないように書け。

このNullセーフな性質を見て行くと、

いちいち「?」をつけるのが面倒くさいだとか、

ソースコードが「?」まみれになって、結局ぐちゃぐちゃになりそうだとか、

sureしすぎて結局NPEが発生してしまうだとか、

そういう問題が起きそうなことは、容易に想像できてしまいます。


しかし、それはいずれもバッドプラクティスであり、

きちんと自前でNullチェックを行ない、できる限り「?」をつけずにコードを書くというのが

Kotlinの正しい使い方なのかな、と思いました。


要するに、

      ,,、,、、,,,';i;'i,}、,、
       ヾ、'i,';||i !} 'i, ゙〃
        ゙、';|i,!  'i i"i,       、__人_从_人__/し、_人_入
         `、||i |i i l|,      、_)
          ',||i }i | ;,〃,,     _) 「?」は消毒だ〜っ!!
          .}.|||| | ! l-'~、ミ    `)
         ,<.}||| il/,‐'liヾ;;ミ   '´⌒V^'^Y⌒V^V⌒W^Y⌒
        .{/゙'、}|||//  .i| };;;ミ
        Y,;-   ー、  .i|,];;彡
        iil|||||liill||||||||li!=H;;;ミミ
        {  く;ァソ  '';;,;'' ゙};;彡ミ
         ゙i [`'''~ヾ. ''~ ||^!,彡ミ   _,,__
          ゙i }~~ } ';;:;li, ゙iミミミ=三=-;;;;;;;;;''
,,,,-‐‐''''''} ̄~フハ,“二゙´ ,;/;;'_,;,7''~~,-''::;;;;;;;;;;;;;'',,=''
 ;;;;;;;;''''/_  / | | `ー-‐'´_,,,-',,r'~`ヽ';;:;;;;;;;, '';;;-'''
'''''  ,r'~ `V ヽニニニ二、-'{ 十 )__;;;;/

ってことですね?

2011-05-16

[]docomoのテザリング解放! でも・・・

今日はdocomoの新機種発表がありました。

ほんの3カ月前には

パケ代6000円程度で3G + WiMAXのテザリングができるのは、

ちょっと他社には追従できないサービスですね。

いよいよテザリング解禁! - せろ部屋

とか言ってたんですが、

さっそく、docomoがスマートフォンにテザリング機能を搭載してきました。


docomoのテザリングは、「速度」で言えば3Gのみ、

「料金」で言えばパケット代の上限が1万円を超えることを考えると

まだまだauに追従できたわけではありませんが、

スマートフォンがほぼ全機種テザリングに対応するというのは、非常に嬉しい話です。


また、データ端末として(通話契約なしで)契約すれば、

パケット代の上限が月5000円ぐらいに抑えられるため、

十分にアリな選択肢となってくると思います。


ところで、

このdocomoのテザリングは、APNを切り替える方式だそうですね。

http://komugi.net/archives/2011/05/12131906.php


ただそうなると、いくつか疑問が湧いてきます。

 1. 海外ローミング時にもテザリングできるの?

 2. SIMロック解除した端末を海外に持っていって、現地SIMを入れてテザリングするとどうなるの?


そう、docomoのケータイって、日本国内はもちろんのこと

世界中のだいたいの場所(いわゆる観光でよく行く国)で電波が入るという

かなり電波的に優秀な端末なんですよね。


たとえばアメリカで3G電波の掴めないヨーロッパ向け端末を使うよりも、

優秀なdocomo端末をSIMロック解除して、現地SIMを挿して使った方が電波の掴みが良いはずです。

つまりは、世界中で3G回線が使える、テザリングができる端末になるはずです。


この点について、ニュースサイトなどを見ながら確認してみました。

データ通信時に必要なAPN(Access Point Name)は固定のものしか利用できず、仮に端末のSIMロックを解除した場合でも固定のAPNしか受け付けないという。

スマートフォン7モデルがWi-Fiテザリングに対応 - ケータイWatch

orz ...


固定のAPNしか受け付けないなら、

海外ローミング時にはAPNが見つからないので、テザリングはできないでしょう。

もちろん、現地SIMを挿しても、APNが見つかるはずがありません。


つまり、現状のテザリング機能は、

日本国内でのdocomo回線限定ということなんでしょうね。


日本国内だけなら、Evo使うほうが、いいボ!

2010-02-02

[]レビューで鍛えるJavaコーディング力 その6(コレクション)

今回はコレクションを用いたキャッシュに関する問題です。

問題

以下のコードの問題を指摘し、修正してください。

ただし、問題は複数あることもあれば、全くないこともあります。

import java.util.List;

public class IdService {
	/** IDキャッシュ */
	private List<Integer> cache;

	/** リモートシステムへのアクセッサ */
	private RemoteAccessor accessor;

	public IdService() {
		// accessorの初期化は割愛。
		reloadCache();
	}

	/**
	 * IDが存在するかどうかを判定します。
	 * @param id ID
	 * @return IDが存在する場合はtrue、そうでない場合はfalseを返します。
	 */
	public boolean exists(Integer id) {
		return cache.contains(id);
	}

	/**
	 * キャッシュの更新を行います。
	 */
	public void reloadCache() {
		cache = accessor.getIdList();
	}
}

リモートシステムへのアクセス(RemoteAccessorの利用)をできるだけ避けるため、

取得した情報をキャッシュすることが、この処理の目的です。


現実的には、ちょっと仕様(要件)がおかしい感じもしますが、

問題を簡単にするために、敢えてこんな内容にしています。

あくまで目的を達成することを、今回の主眼としてください。

コラム

前回の問題について、id:backpaper0さんからはてブでコメントを頂きました。

getTemplateLines は配列のコピーを返さなくて良いのかな?(変更される恐れがあるので)


これはおっしゃる通りですね。

public String[] getTemplateLines() {
	return this.templateLines;
}

の箇所を、以下のように修正すべきです。

public String[] getTemplateLines() {
	String[] copy = new String[templateLines.length];
	System.arraycopy(templateLines, 0, copy, 0, templateLines.length);
	return copy;
}

あるいは、性能を気にしないなら(あまりオススメできませんが、読みやすくするために)

public String[] getTemplateLines() {
	return StringUtils.splitByWholeSeparatorPreserveAllTokens(this.template, "\r\n");
}

としても良いでしょうか。


問題が不完全な事もちょくちょくありますので

ぜひコメントやトラックバック、ブクマコメント、スターなどで突っ込んでください m(_ _)m

解答

今回の問題点は、ここです。

public boolean exists(Integer id) {
	return cache.contains(id);
}

ArrayListのcontainsメソッドは、ただの線形検索です。

要素数が10や100程度なら1ms以内に終わるでしょうけど、

10000程度にもなってくると、無視出来ない処理時間になってきます。


そもそも「検索(存在判定)」が目的なのですから、ArrayListは不向きです。

ここではSet、実装としてはHashSetなどを使えば良いでしょう。

private Set<Integer> cache;

public void reloadCache() {
	cache = new HashSet<Integer>(accessor.getIdList());
}

また、Collections#binarySerachによる二分探索を使う事も可能です。

その場合、リストのソートをしておくことが前提となるため、以下のような修正になります。

public boolean exists(Integer id) {
	int result = Collections.binarySearch(cache, id);
	return result > -1;
}

public void reloadCache() {
	List<Integer> list = accessor.getIdList();
	Collections.sort(list);
	cache = list;
}

ちなみに私のCore2Duoマシンでは、以下のような処理時間の差が出ました。


10,000要素の線形検索を10,000回繰り返した場合(=全要素を1回ずつ検索した場合)

 ・HashSet : 0ms

 ・ArrayList : 485ms

 ・ArrayList(binarySearch) : 16ms


100,000要素の線形検索を100,000回繰り返した場合(=全要素を1回ずつ検索した場合)

 ・HashSet : 15ms

 ・ArrayList : 47,781ms

 ・ArrayList(binarySearch) : 47ms


基本的に「List#containsは不吉な匂い」と考えて、差し支えないでしょう。

補足

今回の内容について、スレッドセーフ性について疑問を持った人もいるかも知れません。

そのような方は、恐らく以下の2つについて考えたのではないでしょうか。


 1. Listのgetとaddが同時に行われて、問題が発生しないか?

 2. reloadCacheが連続して呼び出されたら、問題が発生しないか?


観点としては大切なのですが、

今回は、いずれの問題も起きることはありません。


処理順序を追って行くと分かるのですが、

cache変数はreloadCacheメソッドの中で、インスタンスを置き換えているため

(また、そのインスタンスに対する処理は、reloadCache内では行わないため)

複数のスレッドからcacheに対して「get」を行うことはあっても、

同時に「get」と「add」することはないので、問題は発生しないのです。


逆に言えば、reloadCacheメソッドの中で、

cacheに対してインスタンスの置き換え以外の処理を行なってしまえば、

問題が起きる可能性があります。


たとえば、解答の中でソートを行う処理がありましたが、

public void reloadCache() {
	List<Integer> list = accessor.getIdList();
	Collections.sort(list);
	cache = list;
}

もしこれを、以下のようにしてしまうと、問題が発生してしまいます。

public void reloadCache() {
	cache = accessor.getIdList();
	Collections.sort(cache);
}

ソートが終わる前に、cacheに対してbinarySearchされてしまえば、きちんと検索ができませんし

ソート中にインスタンスが入れ替わってしまうと、ソート結果がどうなるか分かりません。


このように、キャッシュの更新などを行う場合には、

最後にまとめてインスタンスを置き換えることで

synchronizedせずにスレッドセーフ化することもできるのです。


若干、ソースからでは伝わりにくい、、、かも知れませんけどね。

まとめ

  • List#containsは不吉な匂い
  • キャッシュを自前で実装するなら、スレッドセーフになっているかどうかは要確認
  • インスタンスを置き換えることで、スレッドセーフ化するという手段もアリ

というかそもそも、きちんとしたキャッシュ機構を作りたいなら

ローカルにDB(あるいはインメモリDB)を置くか、

サードパーティ製のキャッシュライブラリの利用を検討すべきなんでしょうけどね。


と言いつつ、実は私もキャッシュのライブラリについては詳しくないので、

「これが鉄板でしょう」というものがあれば、ぜひ教えてください m(_ _)m