高效解決「SQLite」數據庫並發訪問安全問題,只這一篇就夠了
Concurrent database access
本文譯自:https://dmytrodanylyk.com/articles/concurrent-database/
對於 Android Dev 而言,有關 SQLite 的操作再經常不過了,相比你一定經歷過控制台一片爆紅的情況,這不禁讓我們疑問:SQLite 到底是線程安全的嗎?
OK 廢話不多說,我們 ⬇️
直接開始
首先,假設你已經實現了一個 SQLiteHelper 類,如下所示:
public class DatabaseHelper extends SQLiteOpenHelper { ... }
現在你想要在兩個子線程中,分別地向 SQLite 里寫入一些數據:
// 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();
對吧?看上去很 OK 沒啥毛病。
那么這時,我們點一下 run
,gio~ 你將會在你的 logcat 里收到如下禮物「報錯」:
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
到底是怎么回事呢?
我們分析一下報錯終於發現:這是由於你每次創建 SQLiteHelper 時,都對數據庫進行了一個鏈接操作。這時,如果你嘗試着,同時從實際不同的鏈接中,對數據庫進行寫入操作,失敗就是必然的了。
總結一下
如果我們想再不同的線程中,對數據庫進行包括讀寫操作在內的任何使用,我們就必須得確保,我們使用的是同一個的連接
好,那現在問題就明了了。現在讓我們創建一個單例模式類:DatabaseManager
用來創建和返回唯一的,單例 DatabaseManager
對象。
ps 有些同學問我什么是單例模式,我專門跑去寫了這篇博客來解釋下,單例模式-全局可用的 context 對象,這一篇就夠了碼字不易幫我點個贊謝謝 🙏
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();
邏輯比之前更清晰,代碼冗余也少了。現在我們在跑下代碼,這時我們會收到,另一個 cache
:
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
不要慌,我們仔細分析下報錯,我們發現:單例模式的使用保證了我們,在線程一、二「Thread 1、Thread 2 中」只會獲得到唯一的 SQLiteHelper 對象,但這時問題就來了,當我們運行完線程一「Thread 1」時,我們的 database.close();
已經替我們關閉了對數據庫的連接,但與此同時我們的線程二「Thread 2」依然保持這對 SQLiteHelper 的引用。正是這個原因,我們收到了IllegalStateException
的報錯。
所以,這時我們就需要保證,當沒有人使用 SQLiteHelper 時,再將其斷開連接。
保證 SQLIiteHelper 在無人使用時才斷開連接
關於這個問題的解決 stackoveflow 上很多人建議我們:永遠不要斷開 SQLiteHelper 的連接,但是這樣以來你會在 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() 方法。在這個方法中,我們有一個,用來記錄數據庫被“打開”了幾次的 mOpenCounter 對象。當它等於 1 時,這意味着你需要去創建新的數據庫連接來使用數據庫,否則的話,就說明數據庫已經在使用中了。
同樣的情況也發生在 closeDatabase() 方法中,當你每次調用該方法時,我們的 mOpenCounter 對象就會減一。當它減到 0 時,我們就去關閉這個數據庫的連接。
完美,最后:
- 現在你就能隨心所欲的使用你的數據庫,而且你可以相信 -- 它是線程安全的了!
- 當然很多同學對數據庫的使用,還有着很多的疑惑,我后期將會針對數據庫的使用,作出一系列總結,有興趣可以繼續關注 _yuanhao 的編程世界
相關文章
每個人都要學的圖片壓縮終極奧義,有效解決 Android 程序 OOM
Android 讓你的 Room 搭上 RxJava 的順風車 從重復的代碼中解脫出來
ViewModel 和 ViewModelProvider.Factory:ViewModel 的創建者
單例模式-全局可用的 context 對象,這一篇就夠了
縮放手勢 ScaleGestureDetector 源碼解析,這一篇就夠了
Android 屬性動畫框架 ObjectAnimator、ValueAnimator ,這一篇就夠了
看完這篇再不會 View 的動畫框架,我跪搓衣板
看完這篇還不會 GestureDetector 手勢檢測,我跪搓衣板!
android 自定義控件之-繪制鍾表盤
Android 進階自定義 ViewGroup 自定義布局