为了记录如何线程安全地访问你的Android
数据库实例,我写下了这篇小小札记。文章中引用的项目代码请点击这里
假设你已编写了自己的 SQLiteOpenHelper。
1
|
public
class
DatabaseHelper
extends
SQLiteOpenHelper { ... }
|
现在你想在不同的线程中对数据库进行写数据操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Thread 1
Context context = getApplicationContext();
DatabaseHelper helper =
new
DatabaseHelper(context);
SQLiteDatabase database = helper.getWritableDatabase();
database.insert(…);
database.close();
// Thread 2
Context context = getApplicationContext();
DatabaseHelper helper =
new
DatabaseHelper(context);
SQLiteDatabase database = helper.getWritableDatabase();
database.insert(…);
database.close();
|
然后在你的Logcat中将输出类似下面的日志信息,而你的写数据操作将会无效。
1
|
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code
5
)
|
上面问题的出现,源于你每创建一个 SQLiteOpenHelper 对象时,实际上也是在新建一个数据库连接。如果你尝试通过多个连接同时对数据库进行写数据操作,其一定会失败。
为确保我们能在多线程中安全地操作数据库,我们需要保证只有一个数据库连接被占用。
我们先编写一个负责管理单个 SQLiteOpenHelper 对象的单例 DatabaseManager 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public
class
DatabaseManager {
private
static
DatabaseManager instance;
private
static
SQLiteOpenHelper mDatabaseHelper;
public
static
synchronized
void
initialize(Context context, SQLiteOpenHelper helper) {
if
(instance ==
null
) {
instance =
new
DatabaseManager();
mDatabaseHelper = helper;
}
}
public
static
synchronized
DatabaseManager getInstance() {
if
(instance ==
null
) {
throw
new
IllegalStateException(DatabaseManager.
class
.getSimpleName() +
" is not initialized, call initialize(..) method first."
);
}
return
instance;
}
public
synchronized
SQLiteDatabase getDatabase() {
return
mDatabaseHelper.getWritableDatabase();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// In your application class
DatabaseManager.initializeInstance(getApplicationContext());
// Thread 1
DatabaseManager manager = DatabaseManager.getInstance();
SQLiteDatabase database = manager.getDatabase()
database.insert(…);
database.close();
// Thread 2
DatabaseManager manager = DatabaseManager.getInstance();
SQLiteDatabase database = manager.getDatabase()
database.insert(…);
database.close();
|
1
|
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
|
既然我们只有一个数据库连接,Thread1 和 Thread2 对方法 getDatabase() 的调用就会取得一样的 SQLiteDatabase 对象实例。之后的事情就是,当 Thread1 尝试管理数据库连接时,Thread2 却仍然在使用该数据库连接。这也就是导致 IllegalStateException 崩毁的原因。
因此我们只能在确保数据库没有再被占用的情况下,才去关闭它。在 stackoveflow 上有一些讨论推荐“永不关闭”你的 SQLiteDatabase 。 如果你这样做,你的logcat将会出现以下的信息,因此我不认为这是一个好主意。
Leak foundCaused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
public
class
DatabaseManager {
private
AtomicInteger mOpenCounter =
new
AtomicInteger();
private
static
DatabaseManager instance;
private
static
SQLiteOpenHelper mDatabaseHelper;
private
SQLiteDatabase mDatabase;
public
static
synchronized
void
initializeInstance(SQLiteOpenHelper helper) {
if
(instance ==
null
) {
instance =
new
DatabaseManager();
mDatabaseHelper = helper;
}
}
public
static
synchronized
DatabaseManager getInstance() {
if
(instance ==
null
) {
throw
new
IllegalStateException(DatabaseManager.
class
.getSimpleName() +
" is not initialized, call initializeInstance(..) method first."
);
}
return
instance;
}
public
synchronized
SQLiteDatabase openDatabase() {
if
(mOpenCounter.incrementAndGet() ==
1
) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return
mDatabase;
}
public
synchronized
void
closeDatabase() {
if
(mOpenCounter.decrementAndGet() ==
0
) {
// Closing database
mDatabase.close();
}
}
}
|
然后你可以怎样子去调用它:
1
2
3
4
|
<code>SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase();
// correct way</code>
|
以后每当你需要使用数据库连接,你可以通过调用类 DatabaseManager 的方法openDatabase()。在方法里面,内置一个标志数据库被打开多少次的计数器。如果计数为1,代表我们需要打开一个新的数据库连接,否则,数据库连接已经存在。
在方法 closeDatabase() 中,情况也一样。每次我们调用 closeDatabase() 方法,计数器都会递减,直到计数为0,我们就需要关闭数据库连接了。
提示: 你应该使用 AtomicInteger 来处理并发的情况
现在你可以线程安全地使用你的数据库连接了。
原文: https://github.com/dmytrodanylyk/dmytrodanylyk/blob/gh-pages/articles/Concurrent%20Database%20Access.md