本文微信公衆號「AndroidTraveler」首發。
背景
本文是對一篇英文文檔的翻譯,原文請見文末鏈接。
併發數據庫訪問
假設你實現了自己的 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();
你將會在你的 logcat 中發現下面信息,並且你的其中一個改變不會寫入數據庫:
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
產生這個錯誤的原因是因爲,每次你創建新的 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
由於我們只使用了一個數據庫連接,Thread1 和 Thread2 的 getDatabase()
方法都會返回同一個 SQLiteDatabase
對象實例。可能發生的場景是 Thread1 關閉了數據庫,然而 Thread2 還在使用它。這也就是爲什麼我們會有 IllegalStateException
的奔潰的原因。
我們需要確保沒有人正在使用數據庫,這個時候我們纔可以關閉它。stackoveflow 上有人推薦永遠不要關閉你的 SQLiteDatabase。這會讓你看到下面的 logcat 信息。所以我一點也不認爲這是一個好的想法。
Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed
實戰例子
一種可能的解決方案是使用計數器跟蹤打開/關閉的數據庫連接。
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()
方法。在這個方法裏面,我們有一個計數器,用來表明數據庫打開的次數。如果計數爲 1,意味着我們需要創建新的數據庫連接,否則,數據庫連接已經建立。
對於 closeDatabase()
方法來說也是一樣的。每次我們調用這個方法的時候,計數器在減少,當減爲 0 的時候,我們關閉數據庫連接。
現在你能夠使用你的數據庫並且確保是線程安全的。
由於本人翻譯水平有限,如果你有更好的翻譯文案,歡迎在 GitHub 提 PR。
這邊建了一個倉庫,歡迎提 PR 投稿一些好的文章翻譯。
併發數據庫訪問