并发访问sqlite如何做到线程安全?
该文由dmytrodanylyk.com/articles/co…翻译而来
假设我们的Android
项目工程中有一个SQLiteOpenHelper
:
public class DatabaseHelper extends SQLiteOpenHelper {
...
}复制代码
现在我们有Thread 1
和Thread 2
两个线程要往数据库中插入数据:
// 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
,并且其中一个线程的插入写数据库操作将会执行失败:
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)复制代码
为什么会发生 SQLiteDatabaseLockedException 异常呢?
因为每一次我们新创建new
一个SQLiteOpenHelper
对象,都会新建立一个数据库连接database connection
。如果同时用两个不同的connection
往数据库中写数据,其中一个将会失败,并收到database is locked
异常。
多线程操作数据库的情况下,我们应该要确保我们每一个线程中使用的是同一个数据库连接。
如何确保多线程先使用的是同一个数据库连接database connection
?
使用单例类。下面我们就来实现一个持有并且能够返回一个 SQLiteOpenHelper 对象的 DatabaseManager 单例类:
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();复制代码
现在代码就不会在出现 SQLiteDatabaseLockedException 异常了,但是却还会有可能有另外一种crash
发生:
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase复制代码
那为什么会出现 attempt to re-open an already-closed object: SQLiteDatabase 异常呢?
当使用同一个数据库连接database connection
时,Thread 1
以及Thread 2
调用getDatabase()
方法会返回同一个SQLiteOpenHelper
对象。这样可能会导致这么一种情况出现:当Thread 1
先调用database.close()
关闭了数据库,而Thread 2
接着调用了database.insert()
想往数据库中插入数据,但是这时候数据库已经处于关闭状态了,于是系统报了attempt to re-open an already-closed object: SQLiteOpenHelper
异常。
我们应该确保在没有人使用数据库时才关闭数据库。在 StackOverflow 上推荐的做法是永远不要关闭数据库,Android
系统会尊重你的这种作法,但是会提示一下logcat
信息,所以这属于一种不推荐的做法:
Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed复制代码
那怎样确保多线程环境下不出现attempt to re-open an already-closed object: SQLiteDatabase
异常呢?
一个可行的方法就是计数法,分别对打开以及关闭数据库的次数进行计数。
下面给出一个示例代码:
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();
}
}
}复制代码
使用方法:
SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way复制代码
我们每次使用数据库,都调用DatabaseManager
的openDatabase()
方法获取SQLiteDatabase
,在这个方法里,有一个原子操作的计数器mOpenCounter
,它记录了数据库被尝试打开的次数(即openDatabase()
方法的调用次数),每调用一次openDatabase()
,mOpenCounter
就自增一。如果计数器数值为1,意味着要创建一个SQLiteDatabase
对象返回,如果数值不为1,证明数据库已经被打开了,直接返回已经获取到的SQLiteDatabase
对象就行了。
同理,DatabaseManager
的closeDatabase()
也是一样,每次调用这个方法,计数器mOpenCounter
都自减一,当值为0的时候,就会真正调用mDatabase.close()
方法关闭数据库。
现在我们就能在确保线程安全的情况下去放心地使用Sqlite
数据库了。
相关文章