SQLiteDatabase 多线程访问需要注意的问题

来源:互联网 发布:自学跆拳道的软件 编辑:程序博客网 时间:2024/04/27 23:13

SQLiteDatabase 多线程访问需要注意的问题

1. 多线程打开SQLiteDatabase

// Thread 1Context context = getApplicationContext();DatabaseHelper helper = new DatabaseHelper(context);SQLiteDatabase database = helper.getWritableDatabase();database.insert(…);database.close();// Thread 2Context context = getApplicationContext();DatabaseHelper helper = new DatabaseHelper(context);SQLiteDatabase database = helper.getWritableDatabase();database.insert(…);database.close();

执行上面代码可能会遇到Exception:

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)

上面问题的出现源于你创建了多个SQLiteOpenHelper, 每个SQLiteOpenHelper都会打开一个数据库对象,实际上也就是对同一个数据库文件建立了多个连接connection。如果你通过多个连接 connection 同时对数据库进行读/写数据操作,则会遇到exception。

SQLiteDatabase数据库本身是“线程安全的”:看SQLiteDatabase的源码可以知道,insert , update , execSQL都会调用lock(), 乍一看唯有query没有调用lock()。可是仔细看,发现query查询结果是一个SQLiteCursor对象,SQLiteCursor保存了查询条件,但是并没有立即执行查询,而是使用了lazy的策略,在需要时加载部分数据,在加载数据时调用了SQLiteQuery的fillWindow方法,而该方法依然会调用SQLiteDatabase.lock()。所以SQLiteDatabase任何读写都是有同步锁的。这个锁是文件级别的,对db文件操作需拿到这把锁。

对同一个SQLiteDatabase对象,在不同线程同时调用读/写函数,是不会有问题的,SQLiteDatabase会自己同步读/写操作。但是当对多个SQLiteDatabase对象(指向同一个db),在多线程同时调用读/写函数,SQLite数据库自身的安全约束遭到破坏,导致多线程同时读写数据库冲突

解决的办法就是使用单例模式来严格控制SQLiteDatabase的实例,也就是只能实例化一个SQLiteDatabase对象,无论多少个线程都只用这一个SQLiteDatabase对象来读写线程。Android 提供了SQLiteOpenHelper类,它对SQLiteDatabase做了封装,在一个SQLiteOpenHelper类中,保证读写操作都是对同一个SQLiteDatabase对象进行;也就是共用一个数据库链接connection,从而避免多个链接同时读写产生Exception。

然而,我们在调用过程中往往不经意间会 new SQLiteOpenHelper 多个对象,这样同样会产生多个数据库链接connection,一定要保证多线程的读/写操作都是发生在一个库链接connection上,才不会有问题。

我们先编写一个负责管理单个 SQLiteOpenHelper 对象的单例 DatabaseManager:

public class DatabaseManager {     private static DatabaseManager instance;    private static SQLiteOpenHelper mDatabaseHelper;     public static synchronized void initialize(Context context, 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 new mDatabaseHelper.getWritableDatabase();    }}

为了能在多线程中进行写数据操作,我们得修改一下代码,具体如下:

// In your application class DatabaseManager.initializeInstance(getApplicationContext());  // 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
既然我们只有一个数据库连接,Thread1 和 Thread2 对方法 getDatabase() 的调用就会取得一样的 SQLiteDatabase 对象实例。当 Thread1 尝试关闭数据库连接时,Thread2 却仍然在使用该数据库连接,这也就是导致 IllegalStateException 崩毁的原因。因此我们只能在确保数据库没有再被占用的情况下,才去关闭它。有一些讨论推荐“永不关闭”你的 SQLiteDatabase。如果你这样做,你的logcat将会出现以下的信息,因此我不认为这是一个好主意。
Leak foundCaused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed
我们可以做一个open 计数,以后每当你需要使用数据库连接,你可以通过调用类 DatabaseManager 的方法openDatabase()。在方法里面,内置一个标志数据库被打开多少次的计数器。如果计数为1,代表我们需要打开一个新的数据库连接,否则,数据库连接已经存在。在方法 closeDatabase() 中,情况也一样。每次我们调用 closeDatabase() 方法,计数器都会递减,直到计数为0,我们就需要关闭数据库连接了。

private AtomicInteger mOpenCounter = new AtomicInteger();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();    }}

2. 多线程访问SQLiteDatabase单例

SQLiteDatabse实质上是将数据写入一个文件,我们可以得知SQLiteDatabse是文件级别的锁。多个线程可以同时读;但是多个线程同时读/写时:如读先发生,写会被阻塞直至读完毕,写才会继续执行;如写先发生,读会被阻塞直至写完毕,读才会继续执行。

Android为我们提供了SqliteOpenHelper类,我们可以通过getWritableDatabase或者getReadableDatabase拿到SQLiteDatabase对象,然后执行相关方法。getReadabeDatabase就是获取一个只读的数据库,可以获取很多次,多个线程同时读在多线程中如果第一个线程先调用getReadableDatabase,后面的线程调用getWritableDatabase,调用都会返回SQLiteDatabse对象,但是对SQLiteDatabse同时进行读/写操作会被阻塞,SQLiteDatabse会自动同步读/写(即 write 和write 互斥,read和write互斥,但是read 和 read 不互斥)。

关闭SqliteOpenHelper:
在某些单例 SqliteOpenHelper 例子中,其实一直没有关闭数据库,但是我们再次调用 getReadabeDatabase 和 getWritableDatabas 的方法,它们会关闭old SQLiteDatabase的,我们只需要在ApplicationonTerminal方法中关闭即可,这样也能避免多线程中,一个线程关闭了数据库,导致其他线程使用的时候失败的问题。实质上,数据库是一个文件引用,单例模式下不关闭也不会出现问题,让它保持随单例的生命周期就好了。

0 0