本文譯自:https://dmytrodanylyk.com/articles/concurrent-database/html
對於 Android Dev 而言,有關 SQLite 的操做再常常不過了,相比你必定經歷過控制檯一片爆紅的狀況,這不由讓咱們疑問:SQLite 究竟是線程安全的嗎?java
OK 廢話很少說,咱們 ⬇️android
public class DatabaseHelper extends SQLiteOpenHelper { ... }
如今你想要在兩個子線程中,分別地向 SQLite 裏寫入一些數據:git
// 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 沒啥毛病。github
那麼這時,咱們點一下 run
,gio~ 你將會在你的 logcat 裏收到以下禮物「報錯」:sql
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
咱們分析一下報錯終於發現:這是因爲你每次建立 SQLiteHelper 時,都對數據庫進行了一個連接操做。這時,若是你嘗試着,同時從實際不一樣的連接中,對數據庫進行寫入操做,失敗就是必然的了。數據庫
總結一下
若是咱們想再不一樣的線程中,對數據庫進行包括讀寫操做在內的任何使用,咱們就必須得確保,咱們使用的是同一個的鏈接編程
好,那如今問題就明瞭了。如今讓咱們建立一個單例模式類:DatabaseManager
用來建立和返回惟一的,單例 DatabaseManager
對象。安全
ps 有些同窗問我什麼是單例模式,我專門跑去寫了這篇博客來解釋下,單例模式-全局可用的 context 對象,這一篇就夠了碼字不易幫我點個贊謝謝 🙏app
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
不要慌,咱們仔細分析下報錯,咱們發現:單例模式的使用保證了咱們,在線程1、二「Thread 一、Thread 2 中」只會得到到惟一的 SQLiteHelper 對象,但這時問題就來了,當咱們運行完線程一「Thread 1」時,咱們的 database.close();
已經替咱們關閉了對數據庫的鏈接,但與此同時咱們的線程二「Thread 2」依然保持這對 SQLiteHelper 的引用。正是這個緣由,咱們收到了IllegalStateException
的報錯。
因此,這時咱們就須要保證,當沒有人使用 SQLiteHelper 時,再將其斷開鏈接。
關於這個問題的解決 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 時,咱們就去關閉這個數據庫的鏈接。
完美,最後:
每一個人都要學的圖片壓縮終極奧義,有效解決 Android 程序 OOM
Android 讓你的 Room 搭上 RxJava 的順風車 從重複的代碼中解脫出來
ViewModel 和 ViewModelProvider.Factory:ViewModel 的建立者
單例模式-全局可用的 context 對象,這一篇就夠了
縮放手勢 ScaleGestureDetector 源碼解析,這一篇就夠了
Android 屬性動畫框架 ObjectAnimator、ValueAnimator ,這一篇就夠了
看完這篇再不會 View 的動畫框架,我跪搓衣板
看完這篇還不會 GestureDetector 手勢檢測,我跪搓衣板!
android 自定義控件之-繪製鐘表盤
Android 進階自定義 ViewGroup 自定義佈局
按期分享Android開發
溼貨,追求文章幽默與深度
的完美統一。
關於源碼 Demo 連接:Demo 碼了好幾天才整完,但願你們點個 star~ 謝謝!