高效解決「SQLite」數據庫併發訪問安全問題,只這一篇就夠了

學Android

Concurrent database access


本文譯自:https://dmytrodanylyk.com/articles/concurrent-database/html

對於 Android Dev 而言,有關 SQLite 的操做再常常不過了,相比你必定經歷過控制檯一片爆紅的狀況,這不由讓咱們疑問:SQLite 究竟是線程安全的嗎?java

OK 廢話很少說,咱們 ⬇️android

直接開始


首先,假設你已經實現了一個 SQLiteHelper 類,以下所示:

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 時,再將其斷開鏈接。

保證 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 時,咱們就去關閉這個數據庫的鏈接。

完美,最後:

  1. 如今你就能爲所欲爲的使用你的數據庫,並且你能夠相信 -- 它是線程安全的了!
  2. 固然不少同窗對數據庫的使用,還有着不少的疑惑,我後期將會針對數據庫的使用,做出一系列總結,有興趣能夠繼續關注 _yuanhao 的編程世界

相關文章


每一個人都要學的圖片壓縮終極奧義,有效解決 Android 程序 OOM
Android 讓你的 Room 搭上 RxJava 的順風車 從重複的代碼中解脫出來
ViewModel 和 ViewModelProvider.Factory:ViewModel 的建立者
單例模式-全局可用的 context 對象,這一篇就夠了
縮放手勢 ScaleGestureDetector 源碼解析,這一篇就夠了
Android 屬性動畫框架 ObjectAnimator、ValueAnimator ,這一篇就夠了
看完這篇再不會 View 的動畫框架,我跪搓衣板
看完這篇還不會 GestureDetector 手勢檢測,我跪搓衣板!
android 自定義控件之-繪製鐘表盤
Android 進階自定義 ViewGroup 自定義佈局

歡迎關注_yuanhao的博客園!


按期分享Android開發溼貨,追求文章幽默與深度的完美統一。

關於源碼 Demo 連接:Demo 碼了好幾天才整完,但願你們點個 star~ 謝謝!

請點贊!由於你的鼓勵是我寫做的最大動力!

學Android

相關文章
相關標籤/搜索