文章的开始说几句题外话,我们写程序的最重要的也是最基础的就是操作数据,也是避免不了和数据库打交道。在我的写android sqlite数据库中遇到了一些异常困扰了我很久。尝试了几种解决方案,找到种我感觉还是较为合理的解决方案。我很高兴把它写出来和大家共享,希望对你有一些帮助。
废话不多说,文章的开始先抛出异常SQLiteDatabaseLockedException: database is locked和java.lang.IllegalStateException: attempt to re-open an already-closed object.这两个是我们最常见的数据库并发异常。问题要一个一个的解决,我先说说数据库锁定出现的原因,能够明白问题出现的原因。问题解决起来就会简单的多。
我描述下场场景:如果现在有两个线程在同时的插入数据,要注意到这个里我们使用的SQLiteOpenHelper是new出来的,也就是说在不同的线程使用的是不同的helper的实例(记住这一点对你理解下面的事情会有帮助)。
线程一:
DatabaseHelper helper =newDatabaseHelper(context);
SQLiteDatabase database = helper.getWritableDatabase();
database.insert(…);
database.close();
线程二:
DatabaseHelper helper =newDatabaseHelper(context);
SQLiteDatabase database = helper.getWritableDatabase();
database.insert(…);
database.close();
如果你这么写,那么你就会得到下面的异常:
对,你没有看错。这个就是所谓的”database is locked“异常。看到这里你不免会有些疑惑,难道这是因为我使用两个线程同步写入就会抛出这个异常吗??你这么说也可以,但我们要把问题描述清楚,这种情况只能是查看APi,api的大概意思是这样的:SQLite是文件级别的锁.SQLite3对于并发的处理机制是允许同一个进程的多个线程同时读取一个数据库,但是任何时刻只允许一个线程/进程写入数据库。在操行写操作时,数据库文件被琐定,此时任何其他读/写操作都被阻塞,如果阻塞超过5秒钟(默认是5秒,能过重新编译sqlite可以修改超时时间),就报"database is locked"错误。
你能理解最好,看不太懂也没有关系。我简单的解释一下,意思就是sqlite数据库支持不同的线程同时读取,但是在同一时间只能有一个线程在写入数据,此时有其他线程写入或者读取数据库就会报“database is locked”异常。看到这里你肯定要问了,说了这么半天问题出现嘞。怎么解决呢??先不要着急,我先把问题描述清楚以后,就能很好的理解这个错误。刚才说过我们使用数据库时是用new helper的这种形式获取对象,也就是说我们不同线程使用的是不同的helper对象,也就是不同的数据库连接。截图如下:
对吧,线程115389和线程11540对应的是不同的helper对象,现在还有一个问题就是,刚才api上说一个线程在写入时是不能有其他线程有操作的,读取也不行吗?我也有这个疑惑。所以我测试了,结果就是“database is locked”异常与我不期而遇。。呵呵。。。。
现在问题很明白了,那怎么解决呢? 我尝试着使用单例的形式使用helper(extend SqliteOpenHelper)对象,这个我用到了单例模式,有不清楚的同学可以到这个网址查看关于单例模式的解释。WWW.blog.csdn.net/guolin_blog/article/details/8860649
/**
* 双重锁定
* @param context
* @return
*/
public static DBHelper getInstance(Context context) {
/**
* 只有在为空的时候,会有同步锁的影响
*/
if (mInstance == null) {
synchronized (DBHelper.class) {
if (mInstance == null) {
mInstance = new DBHelper(context);
}
}
}
return mInstance;
};
ps:我们在操作数据库是应该是一个数据库对应一个SQliteOpenHelper对象,因为每个数据库的操作方法不同。helper又会对应一个manager用于管理该数据库的增删改查。那么helper对象的获取应该是写到其对应的manager类中的。我将helper的对象单例化后,确保我使用不同线程使用的是同一个数据库连接。所以我把数据库操作方法改成这个样子。
线程一:
DBManagerCorrect manager = new DBManagerLocked(MainActivity.this);
SQLiteDatabase openDb = manager.openDb();
manager.insertAll();
manager.CloseDb(openDb);
线程一:
DBManagerCorrect manager = new DBManagerLocked(MainActivity.this);
SQLiteDatabase openDb = manager.openDb();
manager.insertAll();
manager.CloseDb(openDb);
这样写和原来的那种没有很大的区别,关键是manger类中的代码:
private static DBHelper helper;
private static SQLiteDatabase db;
public DBManagerLocked(Context context) {
helper = DBHelper.getInstance(context);
}
/**
* @return 打开SQLiteDatabase 对象
*/
public synchronized SQLiteDatabase openDb(){
if(db==null){
db=helper.getWritableDatabase();
}
return db;
}
/**
* 关闭数据库
* @param database 需要关闭的SQLiteDatabase对象
*/
public synchronized void CloseDb(SQLiteDatabase database){
if(db!=null){
database.close();
}
}
我们在构建DBmanger类的对象的时候,获取helper对象的单例,每次操作数据库的时候先打开db,然后再关闭db.因为是多线程操作所以我添加为该方法添加了同步锁。(因为这篇文章主要是解决数据库的并发问题,有多同步机制不清楚的同学可以自己先找找相关文章进行阅读)。这样的写法下,我们就可以和数据库的database is locked异常say goodbay了。说到这里应该有很多朋友有相同的疑问,你现在还不是多线程同时写入,怎么就能解决问题的呢?呵呵,如果你能想到这里,说明你还是很有天赋的嘛。没错我们现在是多线程操作,但是我们的helper对象时单例的也就是说我们使用的数据库连接是同一个。看到不同了吧。
SQLiteDatabaseLockedException: database is locked 异常的解决方案:
我们明白问题的原因后 ” database is locked “的解决方法很简单就一句话, 我们将SqliteHelper对象设置为单例就可以解决。
之后的我尝试去多线程读写数据库,我的应用崩溃了。异常是这个:java.lang.IllegalStateException: attempt to re-open an already-closed object。 你应该能理解我很郁闷,改了半天还不如不改呢。但问题出现了还是要修改对吧。这个异常只要你理解多线程的机制就很好解释,因为我使用的是同一个数据库连接,所以我在操作数据库的都会调用manager的方法去打开(db=helper.getWritableDatabase())和关闭db(database.close())。这就是问题的原因,场景为:线程A打开数据,正在使用数据库,这时cpu片段分到线程B,线程A挂起。线程B进入执行获取打开db时没有问题,线程B进行操作,在片段时间内数据操作完成,最后关闭数据库database.close()。线程B执行结束,线程A执行,插入数据或者其他操作。。。我靠,怎么数据库关闭了呢,然后抛出java.lang.IllegalStateException: attempt to re-open an already-closed object异常。配图如下
问题就是这么简单,这时有同学就笑了,原来是这样,这个不简单嘛,我给所有的数据库都添加上同步锁,让你还给我搞事。这样也可以,但是同步锁怎么说也是影响性能的,能不使用就不使用对不。那怎么整,我一直不关闭数据库嘛。还别说这个是真的可以,只不过还有更好的方式实现。我也不多说废话。
java.lang.IllegalStateException: attempt to re-open an already-closed object异常的解决方案:
private static DBHelper helper;
private static SQLiteDatabase db;
/**
* 记数器 应该设置静态的类变量
* @param context
*/
private static int mCount;
//同一个数据库连接
private static DBManagerCorrect mMnanagerInstance;
private DBManagerCorrect(Context context) {
helper = DBHelper.getInstance(context);
}
//单例
public static synchronized DBManagerCorrect getIntance(Context context){
if(mMnanagerInstance==null){
return new DBManagerCorrect(context);
}
return mMnanagerInstance;
}
public synchronized SQLiteDatabase openDb(){
if(mCount==0){
db=helper.getWritableDatabase();
}
mCount++;
return db;
}
public synchronized void CloseDb(SQLiteDatabase database){
mCount--;
if(mCount==0){
database.close();
}
}
这个还是manager类中的代码,和之前代码的区别就是将该类中引入一个用于计算的静态变量mCount。这样一来我们操作数据库时调用(manager.openDb())每次打开数据连接的时候加一,每次关闭(manager.ColoseDb())的时候再减一。这样一来如果该值不减到零的时候不会关闭数据库。如果该值减到零说明没有数据库连接在使用,就可以正常关闭数据库。这样可以很轻松的避免出现上面的异常,再使用的时候也完成不需要担心数据库的关闭,只需要调用manager的这个两个函数即可。类如:
DBManagerCorrect manager=DBManagerCorrect.getIntance(MainActivity.this);
// step一 打开数据库
SQLiteDatabase openDb = manager.openDb();
//数据库的相关操作
//..........
//..........
//step二 关闭数据库
manager.CloseDb(openDb);
说了这么多问题的原因和解决方案,我想大家应该很清楚了。如果你有好的建议,,欢迎大家在评论区提出意见。
源码链接