Android下数据库线程安全问题

一.概述

在实际应用中,当同时有多个线程一起访问数据库时,可能会发生一些异常情况,我们先来看看会发生什么异常:

假设我们已经定义好了 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();

这时你会得到下面的异常信息,并且有一条数据没有插入进去

android.database.sqlite.SQLiteDatabaseLockedException: database is locked

这是因为每次当你创建新的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

因为我们只使用一个数据库连接,对2个线程来说,getDataBase方法返回的是同样的SQLiteDatabase 对象,这时后会发生什么情况?线程1可能已经关闭了数据库连接,但是线程2仍然在使用,这就导致了非法状态异常。

我们需要确保没有人在使用数据库,然后才去关闭

下面看看最终的实现代码:

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() {
        //incrementAndGet()每次调用会在当前值的基础上加1并返回
        if(mOpenCounter.incrementAndGet() == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }

    public synchronized void closeDatabase() {
    //decrementAndGet()每次调用会在当前值的基础上减1并返回
        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,说明我们需要创建一个database对象,否则,说明数据库已经创建过了。

同样在closeDatabase方法里面,每次我们调用这个方法,计数器的值就会减1,当计数器的值为0时,我们就需要关闭数据库了。

二.总结

下面总结一下多线程条件下读写数据库的异常情况:

1.多线程写,使用一个SQLiteOpenHelper。也就保证了多线程使用一个SQLiteDatabase。不会引发异常。
2.多线程写,使用多个SQLiteOpenHelper,插入时可能引发异常,导致插入错误

E/Database(1471): android.database.sqlite.SQLiteException: error code 5: database is locked08-01

3.android 框架,多线程写数据库的本地方法里没有同步锁保护,并发写会抛出异常。

所以,多线程写必须使用同一个SQLiteOpenHelper对象。

4.多线程读,读之间没有同步锁,也得每个线程使用各自的SQLiteOpenHelper对象,经测试,没有问题
4.多线程读写,多线程写之前已经知道结果了,同一时间只能有一个写。
多线程读可以并发,所以,使用下面的策略:
一个线程写,多个线程同时读,每个线程都用各自SQLiteOpenHelper。此时会发生异常

E/SQLiteDatabase(18263): Error inserting descreption=InsertThread#01375493606407 
E/SQLiteDatabase(18263): android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5) 

插入异常,说明在有线程读的时候写数据库,会抛出异常

那么如何实现一个线程写,多个线程读呢?

其实SQLiteDataBase 在API 11 多了一个 属性 ENABLE_WRITE_AHEAD_LOGGING

使用enableWriteAheadLogging打开这个属性,
disableWriteAheadLogging关闭这个属性
这个属性是什么意思呢?

参考api文档,这个属性关闭时,不允许读,写同时进行,通过 锁 来保证。

简单的说通过调用enableWriteAheadLogging()和disableWriteAheadLogging()可以控制该数据是否被运行多线程读写,如果允许,它将允许一个写线程与多个读线程同时在一个SQLiteDatabase上起作用。实现原理是写操作其实是在一个单独的log文件,读操作读的是原数据文件,是写操作开始之前的内容,从而互不影响。当写操作结束后读操作将察觉到新数据库的状态。当然这样做的弊端是将消耗更多的内存空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值