一.概述
在实际应用中,当同时有多个线程一起访问数据库时,可能会发生一些异常情况,我们先来看看会发生什么异常:
假设我们已经定义好了 SQLiteOpenHelper
public class DatabaseHelper extends SQLiteOpenHelper { ... }
现在我们使用不同的线程对数据库进行操作
// 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();
这时你会得到下面的异常信息,并且有一条数据没有插入进去
android.database.sqlite.SQLiteDatabaseLockedException: database is locked
这是因为每次当你创建新的SQLiteOpenHelper的时候,就相当于打开了一个新的数据库连接,如果你从不同的连接同时对数据库进行操作,就会失败。
下面我们新建一个单例类DataBaseManager,并且返回一个单一的SQLiteOpenHelper
public class DatabaseManager {
private static DatabaseManager instance;
private static SQLiteOpenHelper mDatabaseHelper;
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 initialize(..) method first.");
}
return instance;
}
public synchronized SQLiteDatabase getDatabase() {
return mDatabaseHelper.getWritableDatabase();
}
}
然后我们把访问数据库的代码改成下面的样子
// In your application class
DatabaseManager.initializeInstance(new DatabaseHelper());
// 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();
这时又会引发另外一个异常信息
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
因为我们只使用一个数据库连接,对2个线程来说,getDataBase方法返回的是同样的SQLiteDatabase 对象,这时后会发生什么情况?线程1可能已经关闭了数据库连接,但是线程2仍然在使用,这就导致了非法状态异常。
我们需要确保没有人在使用数据库,然后才去关闭
下面看看最终的实现代码:
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() {
//incrementAndGet()每次调用会在当前值的基础上加1并返回
if(mOpenCounter.incrementAndGet() == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}
public synchronized void closeDatabase() {
//decrementAndGet()每次调用会在当前值的基础上减1并返回
if(mOpenCounter.decrementAndGet() == 0) {
// Closing database
mDatabase.close();
}
}
}
然后这样去使用
SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way
每次当我们需要数据库对象的时候就去调用DatabaseManager的openDatabase方法,在这个方法里面,我们有一个计数器,这个计数器用来指示数据库被打开了多少次,如果计数器的值为1,说明我们需要创建一个database对象,否则,说明数据库已经创建过了。
同样在closeDatabase方法里面,每次我们调用这个方法,计数器的值就会减1,当计数器的值为0时,我们就需要关闭数据库了。
二.总结
下面总结一下多线程条件下读写数据库的异常情况:
1.多线程写,使用一个SQLiteOpenHelper。也就保证了多线程使用一个SQLiteDatabase。不会引发异常。
2.多线程写,使用多个SQLiteOpenHelper,插入时可能引发异常,导致插入错误
E/Database(1471): android.database.sqlite.SQLiteException: error code 5: database is locked08-01
3.android 框架,多线程写数据库的本地方法里没有同步锁保护,并发写会抛出异常。
所以,多线程写必须使用同一个SQLiteOpenHelper对象。
4.多线程读,读之间没有同步锁,也得每个线程使用各自的SQLiteOpenHelper对象,经测试,没有问题
4.多线程读写,多线程写之前已经知道结果了,同一时间只能有一个写。
多线程读可以并发,所以,使用下面的策略:
一个线程写,多个线程同时读,每个线程都用各自SQLiteOpenHelper。此时会发生异常
E/SQLiteDatabase(18263): Error inserting descreption=InsertThread#01375493606407
E/SQLiteDatabase(18263): android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
插入异常,说明在有线程读的时候写数据库,会抛出异常
那么如何实现一个线程写,多个线程读呢?
其实SQLiteDataBase 在API 11 多了一个 属性 ENABLE_WRITE_AHEAD_LOGGING
使用enableWriteAheadLogging打开这个属性,
disableWriteAheadLogging关闭这个属性
这个属性是什么意思呢?
参考api文档,这个属性关闭时,不允许读,写同时进行,通过 锁 来保证。
简单的说通过调用enableWriteAheadLogging()和disableWriteAheadLogging()可以控制该数据是否被运行多线程读写,如果允许,它将允许一个写线程与多个读线程同时在一个SQLiteDatabase上起作用。实现原理是写操作其实是在一个单独的log文件,读操作读的是原数据文件,是写操作开始之前的内容,从而互不影响。当写操作结束后读操作将察觉到新数据库的状态。当然这样做的弊端是将消耗更多的内存空间。