SQLite并发操作下的分析与处理,解决databaislocked,以
及多线程下执⾏。。。
最近公司的项⽬处于重构阶段,观察后台crash log的时候发现了⼀个发⽣很多的问题:sale是什么意思
android.databa.sqlite.SQLiteDatabaLockedException: databa is locked (code 5): , while compiling: PRAGMA journal_mode
看了⼀下报错具体位置:
嗯,很简单,那就改成同步。
这边先说⼀下databa is locked产⽣的原因:sqlite同⼀时间只能进⾏⼀个写操作,当同时有两个写操作的时候,后执⾏的只能先等待,如果等待时间超过5秒,就会产⽣这种错误.同样⼀个⽂件正在写⼊,重复打开数据库操作更容易导致这种问题的发⽣。
那⾸先,得避免重复打开数据库,⾸先引⼊单例⽅法与SQLiteOpenHelper类:
public class DBOpenHelper extends SQLiteOpenHelper{
private DBOpenHelper(Context context,String dbPath, int version) {
super(context, dbPath , null, version);
}
private volatile static DBOpenHelper uniqueInstance;
沙特取消波音订单
public static DBOpenHelper getInstance(Context context) {
if (uniqueInstance == null) {
synchronized (DBOpenHelper.class) {
if (uniqueInstance == null) {
uniqueInstance = new DBOpenHelper(FilesDir().getAbsolutePath()+"/foowwlite.db",1);
}
}
}
return uniqueInstance;
}
@Override
public void onCreate(SQLiteDataba db) {
}
@Override
public void onUpgrade(SQLiteDataba db, int oldVersion, int newVersion) {
}
}
通过getInstance()⽅法得到helper对象来得到数据库,保证helper类是单例的。
然后通过代理类SQLiteDataProxy来控制对数据库的访问:
private static SQLiteDataProxy proxy;
private static DBOpenHelper helper;
public static SQLiteDataProxy getSQLiteProxy(Context context) {
helper = Instance(context);
if (proxy == null) {
synchronized (SQLiteDataProxy.class) {
if (proxy == null) {
finder是什么意思proxy = new SQLiteDataProxy();
}
}
}
return proxy;
}</span>
同样使⽤单例来保证对象的唯⼀性。然后我写了⼀个⽅法⽤于执⾏sql语句
@Override
public boolean execSQL(String sql) {
boolean result = true;
<span >if (!db.isOpen()) {
db = WritableDataba();
}</span>
try {
} catch (Exception e) {遁世
Log.e("SQLERROR", "In SQLDA:" + e.getMessage() + sql);
result = fal;
} finally {
db.clo();
}
return result;
}
每次获取db对象即SQLiteDataBa的时候先判断是否已经打开,如果已经打开则不需要重新获取,避免了db的重复开启。
接下来我们试验⼀下,这边我通过viewpager+fragment的⽅式测试,因为fragment会同时被加载。在两个fragment中各⾃新建⼀个⼦线程来执⾏⼤量inrt语句:
new Thread(new Runnable() {
@Override
public void run() {小沈阳补办婚礼
for (String sql:getSQLList()){
}
}
}).start();
先分析⼀下:虽然都是⼦线程,但是两个fragment都是通过单例获得db对象来执⾏sql语句,因此db应该是同⼀个的,这时候他们并发执⾏sql应该没有问题。
但是运⾏还是报错了:
attempt to re-open an already-clod object,意思是数据库已经关闭了,但是我却仍然⽤⼀个已经关闭的db去执⾏sql 语句,可是为什么错呢,明明已经判断了db.isOpen,再执⾏sql的。
其实此时的原因如下:
线程⼀和线程⼆同时执⾏execSQL⽅法,线程⼀先获取到了db对象,开始执⾏sql语句,于此同时,线程⼆判断db.isOpen,发现是打开的于是不重新get,直接调⽤db开始执⾏sql语句。但现在问题来了,线程⼆准备执⾏sql的时候,线程⼀已经把sql执⾏完,并且关闭,由于线程⼀和线程⼆得到的db是同⼀个对象,线程⼆的db也关闭了,这时执⾏sql就导致了这个错误。
接下来如何是好。。。
这种情况,我们可以引⼊⼀个全局变量来计算打开关闭db的次数,⽽java刚好提供了这个⽅法AtomicInteger
AtomicInteger是⼀个线程安全的类,我们可以通过它来计数,⽆论什么线程AtomicInteger值+1后都会改变
我们讲判断db是否打开的⽅法改成以下:
private AtomicInteger mOpenCounter = new AtomicInteger();</span>
<span > </span>private SQLiteDataba getSQLiteDataBa() {
if (mOpenCounter.incrementAndGet() == 1) {
db = WritableDataba();
}
return db;
}
关闭db的⽅法改为如下:
<span > </span>private void cloSQLiteDataba(){
if(mOpenCounter.decrementAndGet() == 0){
db.clo();
}
}
⽽上⾯的意思就是:
每当想要获得db对象是计数器mOpenCounter会+1,第⼀次打开数据库mOpenCounter是0,mOpenCounter调⽤incrementAndGet()⽅法后+1等于1说明还没有被获得,此时有第⼆个线程想执⾏sql语句,它在执⾏getSQliteDataBa ⽅法的时候mOpenCounter是1,然后mOpenCounter+1=2不等于1,说明db已经开启,直接return db即可。
在关闭db的时候,mOpenCounter会⾸先减1,如果mOpenCounter==0则说明此时没有其他操作,就可以关闭数据库,如果不等于则说明还有其他sql在执⾏,就不去关闭。
接着上⾯的说,两个线程各⾃执⾏想执⾏sql,此时mOpenCounter是2,当线程⼀的sql执⾏完后,线程⼀的db尝试关闭,会调⽤mOpenCounter.decrementAndGet()⾃减1,decrementAndGet-1后就等于1,说明还有⼀个正在执⾏的sql,即线程⼆正在执⾏。因此db不会去关闭,然后线程⼆正常执⾏,线程⼆执⾏完sql,尝试关闭db,此时decrementAndGet再⾃减1,就等于0,说明已经没有其他真正执⾏的sql,于是可以正常关闭。
这种判断⽅法保证了只有在所有sql都执⾏完后才去关闭,并且只会最后⼀次关闭,保证了不会出现re-open an already-clod这种问题。
intention
这个⽅法再其他博客中也有说明,说是保证了觉得安全,但是,经过测试说明,那些博客都是抄袭的,并没去真正实验,接下来我就说明⼀下它为什么不⾏。
修改以后,我们再测试⼀下。
结果还是报错。
很显然db为null,这是⼀个空指针错误,但是为什么会导致这种错误呢?
分析⼀下AtomicInteger,并没有逻辑上的问题啊。
我们把代码改成如下,便于打印log⽇志:
<span > </span>private SQLiteDataba getSQLiteDataBa() {
Log.e("111", "Once start");
if (mOpenCounter.incrementAndGet() == 1 || db == null) {
db = WritableDataba();
}
if (db == null) {
Log.e("111", mOpenCounter.intValue() + "");
} el {
Log.e("111", mOpenCounter.intValue() + " NOT NULL");
}
return db;
}
outweigh
运⾏后结果如下:
稍微想⼀下就知道了,线程⼀和⼆同时尝试获取db,线程⼀中mOpenCounter+1==1,但此时db还没有获取的情况下,线程⼆也执⾏了获取db的⽅法,mOpenCounter+1==2,单由于获取db的getWritableDataba()需要⼀定的时间,⽽先执⾏的线程⼀db还没有被获取到,线程⼆却已经也经过判断并且return db,此时的db就是null了,导致了空指针错误。
原因已经找到了,那么解决就很简单,只需要多加⼀个⾮空判断就⾏,⽽getWriteableDataBa本⾝就是线程安全的,应该只需要这样就可以解决。
jacsprivate SQLiteDataba getSQLiteDataBa() {
if (mOpenCounter.incrementAndGet() == 1 || <span >db == null</span>) {
db = WritableDataba();
}
return db;
}
这样修改好以后,经过测试没有问题。
接下来解决另⼀个问题:如果当前执⾏的是许多sql语句,要⽤到事务怎么办?
如果是事物,⼤家都知道,事物执⾏的时候调⽤beginTransaction(),完成后调
⽤db.tTransactionSuccessful()、db.endTransaction()标志着事务的结束,但是如果多线程下调⽤了事务怎么办?尤其还是单例模式下,同时调⽤⽅法开启事务,这肯定会出问题。
如以下⽅法:
public boolean execSQLList(List<String> sqlList) {
boolean result = true;
db = getSQLiteDataBa();
String currentSqlString = "";
try {
db.beginTransaction();
for (String sql : sqlList) {
currentSqlString = sql;
}
db.tTransactionSuccessful();
result = true;
} catch (Exception e) {
dimensionality
result = fal;
Log.e("SQLERROR", "IN SQLDA: " + e.getMessage() + currentSqlString);
} finally {
cloSQLiteDataba();
}
return result;
}
for循环中间执⾏sql,并且开始和结束分别打开关闭事务。
为了解决这个问题,只有保证执⾏事务时是同步的,但是多线程调⽤我们如何控制其同步呢。
这⾥要引⼊⼀个类urrent.Semaphore,这个类可以⽤来协调多线程下的控制同步的问题。
appearances⾸先初始化这个类,并且设置信号量为1
private urrent.Semaphore maphoreTransaction = new urrent.Semaphore(1);
这句话的意思是多线程下的调⽤许可数为1,当
maphoreTransaction.acquire()
执⾏后,maphore会检测是否有其他信号量已经执⾏,如果有,改线程就会停⽌,直到另⼀个maphore释放资源之后,才会继续执⾏下去,即:
我们只需要在开始事务前调⽤acquire⽅法,当其他事务想要执⾏,会先判断,如果有事务在执⾏,该线程就会等待,直到前⼀个事物结束并调⽤relea之后,该线程的事务就会继续进⾏,这样解决事务并发产⽣的问题,也保证了事务都可以执⾏完毕。
改进后代码如下:
private urrent.Semaphore maphoreTransaction = new urrent.Semaphore(1);