AndroidのSQLiteDatabaseを複数のスレッドで利用する
AndroidでSQLiteを使うにあたり、複数のスレッドからDBにアクセスする必要がある場合、SQLiteOpenHelperとSQLiteDatabaseのインスタンスをどう扱うべきか。
Androidの次のドキュメントからはマルチスレッドな場合の使い方がはっきりしなかった。
知りたかったのは、
- SQLiteOpenHelper / SQLiteDatabase はスレッドセーフか?
- SQLiteOpenHelper / SQLiteDatabase のインスタンスは複数作ってよい?closeはいつすべき?ライフサイクルがどうあるべき?
対象はAndroid 4.2.2。
今のところ次のとおりだと理解した。
-
SQLiteOpenHelperはスレッドセーフ。1データベースにつき1インスタンスとすべき。
-
SQLiteDatabaseもスレッドセーフ。1データベースにつき(closeしていないインスタンスは)1インスタンスとすべき。
-
SQLiteDatabaseをcloseするのは、全スレッドでそのインスタンスの利用を終えた後。
SQLiteOpenHelperは、Webを検索するとシングルトンにすべきという記事が多く見られる。テーブルの作成などを処理する onCreate, onUpgrade が並列実行されるのは不都合だが、SQLiteOpenHelperのソースを見るとsynchronizedで排他制御されており、同じインスタンスをスレッド間で共有しておけばonCreateなどが並列実行される心配がない。
SQLiteDatabaseは、SQLiteOpenHelper#getWritableDatabase(), #getReadableDatabase()で取得するが、SQLiteOpenHelperは一度作成したインスタンスをキャッシュして、SQLiteDatabaseがcloseされない限りは同じインスタンスを返す(APIリファレンスにも説明あり)ため、SQLiteOpenHelperをシングルトンとした場合はSQLiteDatabaseのインスタンスも1データベースにつき1個となる。
となるとSQLiteOpenHelperはスレッドセーフであってほしいが、ソースを見てもぱっと見は、なにやら複雑でいまひとつ確信が持てない。これについてはSQLiteSessionのjavadocに説明があった。このクラスは内部クラス?であるらしく、APIリファレンスには掲載されていない。ソースは以下を参照した。
この説明によれば
- SQLiteDatabaseは、ThreadLocalを使い、スレッドごとにSQLiteSessionのインスタンスを1個持つ。
- SQLiteSessionは、1トランザクションを実行するたびに、コネクションをプールから取得、返却。
- 1スレッドが同時に使うコネクションは高々1個となるように制御される。
というように、複数スレッドで共有して使うことが前提の仕組みとなっている。ソースを見ると、コネクションプールもSQLiteDatabase 1インスタンスに1個となっており、SQLiteDatabaseをスレッド間で共有しないとプールが効果を発揮しない。
SQLiteDatabase#close()を呼ぶべきタイミングについては、Webを検索すると見解が割れている。
- closeは呼ばない(finalizeに任せる)
- closeをスレッドごとに呼べ(あるいは、aqcuireReference()を呼んだ上で)
ソースを見たところ、あるスレッドでSQLiteDatabase#close()を呼ぶと、それを共有別スレッドでは、insertやqueryを呼び出した時点で、close済みという理由で例外が発生する。closeを呼ぶとすれば、SQLiteDatabaseを共有する全スレッドで利用が終了した後とする必要がある。closeの処理が、ThreadLocalな何らかのオブジェクトに転送されるような処理にもなっていなかったため、スレッドごとにcloseを呼ぶ必要はない。
逆にcloseを呼ばない場合のリークの心配については、SQLiteDatabaseのインスタンスが1つに保たれていること、SQLiteSessionがThreadLocalで管理されているためthreadが無くなればガーベージコレクションの対象となること、コネクションはSQLiteDatabaseのインスタンスごとにプールで管理されていることから、アプリ実行中に延々とリソースを食い尽くす心配はないように思われる。不安がある場合は、適当な時期に、どのスレッドもSQLiteDatabaseを使っていないタイミングを見計らってcloseすることになるだろう。close後に再度アクセスが必要になっても、getWritableDatabaseなどを呼び出せば利用可能なSQLiteDatabaseのインスタンスが新たに得られる。
なお、closeをfinalizeに任せると警告がログに出るとの記事もあったが、その件は未調査。
本件がややこしく感じてられ、また、closeの呼び出しタイミングで諸説生じてしまっているのは、SQLiteDatabaseがコネクションを表しているとの誤解が原因ではなかろうか。このAPIがコネクションを隠蔽しており、insertやqueryなどのメソッドがSQLiteDatabaseに用意されているために、SQLiteDatabase=コネクションという想像をしてしまっていた。SQLiteDatabaseを字面どおりデータベースと捕らえれば、それをスレッド間で共有し、すべてが使用を終えてからcloseするというのはすんなり受け入れられる。