并发访问sqlite如何做到线程安全?

2022-05-10 00:00:00 数据库 代码 线程 调用 复制
该文由dmytrodanylyk.com/articles/co…翻译而来

假设我们的Android项目工程中有一个SQLiteOpenHelper

public class DatabaseHelper extends SQLiteOpenHelper { 
    ... 
}复制代码

现在我们有Thread 1Thread 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复制代码

我们每次使用数据库,都调用DatabaseManageropenDatabase()方法获取SQLiteDatabase,在这个方法里,有一个原子操作的计数器mOpenCounter,它记录了数据库被尝试打开的次数(即openDatabase()方法的调用次数),每调用一次openDatabase()mOpenCounter就自增一。如果计数器数值为1,意味着要创建一个SQLiteDatabase对象返回,如果数值不为1,证明数据库已经被打开了,直接返回已经获取到的SQLiteDatabase对象就行了。

同理,DatabaseManagercloseDatabase()也是一样,每次调用这个方法,计数器mOpenCounter都自减一,当值为0的时候,就会真正调用mDatabase.close()方法关闭数据库。

现在我们就能在确保线程安全的情况下去放心地使用Sqlite数据库了。

相关文章