轉載:http://www.codexiu.cn/android/blog/37889/
最近公司的項目處於重構階段,觀察後臺crash log的時候發現了一個發生很多的問題:
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5): , while compiling: PRAGMA journal_mode
看了一下報錯具體位置:
嗯,很簡單,那就改成同步。
這邊先說一下database 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(context,context.getFilesDir().getAbsolutePath()+"/foowwlite.db",1);
}
}
}
return uniqueInstance;
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
通過getInstance()方法得到helper對象來得到數據庫,保證helper類是單例的。
然後通過代理類SQLiteDataProxy來控制對數據庫的訪問:
private static SQLiteDataProxy proxy;
private static DBOpenHelper helper;
public static SQLiteDataProxy getSQLiteProxy(Context context) {
helper = DBOpenHelper.getInstance(context);
if (proxy == null) {
synchronized (SQLiteDataProxy.class) {
if (proxy == null) {
proxy = new SQLiteDataProxy();
}
}
}
return proxy;
}
同樣使用單例來保證對象的唯一性。然後我寫了一個方法用於執行sql語句
@Override
public boolean execSQL(String sql) {
boolean result = true;
if (!db.isOpen()) {
db = helper.getWritableDatabase();
}
try {
db.execSQL(sql);
} catch (Exception e) {
Log.e("SQLERROR", "In SQLDA:" + e.getMessage() + sql);
result = false;
} finally {
db.close();
}
return result;
}
每次獲取db對象即SQLiteDataBase的時候先判斷是否已經打開,如果已經打開則不需要重新獲取,避免了db的重複開啓。
接下來我們試驗一下,這邊我通過viewpager+fragment的方式測試,因爲fragment會同時被加載。在兩個fragment中各自新建一個子線程來執行大量insert語句:
new Thread(new Runnable() {
@Override
public void run() {
for (String sql:getSQLList()){
SQLiteDataProxy.getSQLiteProxy(getActivity()).execSQL(sql);
}
}
}).start();
先分析一下:雖然都是子線程,但是兩個fragment都是通過單例獲得db對象來執行sql語句,因此db應該是同一個的,這時候他們併發執行sql應該沒有問題。
但是運行還是報錯了:
attempt to re-open an already-closed 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();
private SQLiteDatabase getSQLiteDataBase() {
if (mOpenCounter.incrementAndGet() == 1) {
db = helper.getWritableDatabase();
}
return db;
}
關閉db的方法改爲如下:
private void closeSQLiteDatabase(){
if(mOpenCounter.decrementAndGet() == 0){
db.close();
}
}
而上面的意思就是:
每當想要獲得db對象是計數器mOpenCounter會+1,第一次打開數據庫mOpenCounter是0,mOpenCounter調用incrementAndGet()方法後+1等於1說明還沒有被獲得,此時有第二個線程想執行sql語句,它在執行getSQliteDataBase方法的時候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-closed這種問題。
這個方法再其他博客中也有說明,說是保證了覺得安全,但是,經過測試說明,那些博客都是抄襲的,並沒去真正實驗,接下來我就說明一下它爲什麼不行。
修改以後,我們再測試一下。
結果還是報錯。
很顯然db爲null,這是一個空指針錯誤,但是爲什麼會導致這種錯誤呢?
分析一下AtomicInteger,並沒有邏輯上的問題啊。
我們把代碼改成如下,便於打印log日誌:
private SQLiteDatabase getSQLiteDataBase() {
Log.e("111", "Once start");
if (mOpenCounter.incrementAndGet() == 1 || db == null) {
db = helper.getWritableDatabase();
}
if (db == null) {
Log.e("111", mOpenCounter.intValue() + "");
} else {
Log.e("111", mOpenCounter.intValue() + " NOT NULL");
}
return db;
}
運行後結果如下:
稍微想一下就知道了,線程一和二同時嘗試獲取db,線程一中mOpenCounter+1==1,但此時db還沒有獲取的情況下,線程二也執行了獲取db的方法,mOpenCounter+1==2,單由於獲取db的getWritableDatabase()需要一定的時間,而先執行的線程一db還沒有被獲取到,線程二卻已經也經過判斷並且return db,此時的db就是null了,導致了空指針錯誤。
原因已經找到了,那麼解決就很簡單,只需要多加一個非空判斷就行,而getWriteableDataBase本身就是線程安全的,應該只需要這樣就可以解決。
private SQLiteDatabase getSQLiteDataBase() {
if (mOpenCounter.incrementAndGet() == 1 || db == null) {
db = helper.getWritableDatabase();
}
return db;
}
這樣修改好以後,經過測試沒有問題。
接下來解決另一個問題:如果當前執行的是許多sql語句,要用到事務怎麼辦?
如果是事物,大家都知道,事物執行的時候調用beginTransaction(),完成後調用db.setTransactionSuccessful()、db.endTransaction()標誌着事務的結束,但是如果多線程下調用了事務怎麼辦?尤其還是單例模式下,同時調用方法開啓事務,這肯定會出問題。
如以下方法:
public boolean execSQLList(List<String> sqlList) {
boolean result = true;
db = getSQLiteDataBase();
String currentSqlString = "";
try {
db.beginTransaction();
for (String sql : sqlList) {
currentSqlString = sql;
db.execSQL(sql);
}
db.setTransactionSuccessful();
result = true;
} catch (Exception e) {
result = false;
Log.e("SQLERROR", "IN SQLDA: " + e.getMessage() + currentSqlString);
} finally {
db.endTransaction();
closeSQLiteDatabase();
}
return result;
}
for循環中間執行sql,並且開始和結束分別打開關閉事務。
爲了解決這個問題,只有保證執行事務時是同步的,但是多線程調用我們如何控制其同步呢。
這裏要引入一個類java.util.concurrent.Semaphore,這個類可以用來協調多線程下的控制同步的問題。
首先初始化這個類,並且設置信號量爲1
private java.util.concurrent.Semaphore semaphoreTransaction = new java.util.concurrent.Semaphore(1);
這句話的意思是多線程下的調用許可數爲1,當
semaphoreTransaction.acquire()
執行後,semaphore會檢測是否有其他信號量已經執行,如果有,改線程就會停止,直到另一個semaphore釋放資源之後,纔會繼續執行下去,即:
semaphoreTransaction.release();
我們只需要在開始事務前調用acquire方法,當其他事務想要執行,會先判斷,如果有事務在執行,該線程就會等待,直到前一個事物結束並調用release之後,該線程的事務就會繼續進行,這樣解決事務併發產生的問題,也保證了事務都可以執行完畢。
改進後代碼如下:
private java.util.concurrent.Semaphore semaphoreTransaction = new java.util.concurrent.Semaphore(1);
public boolean execSQLList(List<String> sqlList) {
boolean result = true;
db = getSQLiteDataBase();
String currentSqlString = "";
try {
semaphoreTransaction.acquire();
db.beginTransaction();
for (String sql : sqlList) {
currentSqlString = sql;
db.execSQL(sql);
}
db.setTransactionSuccessful();
result = true;
} catch (Exception e) {
result = false;
Log.e("SQLERROR", "IN SQLDA: " + e.getMessage() + currentSqlString);
} finally {
db.endTransaction();
semaphoreTransaction.release();
closeSQLiteDatabase();
}
return result;
}
經過上面的修改以後,經過測試,多線程下事務也可以正常插入,一些常見的由SQLite併發產生的問題也得以解決。
全部代碼如下:
ISQLiteOperate:
package com.xiaoqi.sqlitedemo;
import android.database.Cursor;
import java.util.List;
/**
* Created by xiaoqi on 2016/9/1.
*/
public interface ISQLiteOperate {
boolean execSQL(String sql);
boolean execSQLList(List<String> sqlList);
boolean execSQLs(List<String[]> sqlList);
boolean execSQLIgnoreError(List<String> sqlList);
Cursor query(String sql);
Cursor query(String sql, String[] params);
void close();
}
DBOpenHelper:
package com.xiaoqi.sqlitedemo;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
/**
* Created by xiaoqi on 2016/9/1.
*/
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(context,context.getFilesDir().getAbsolutePath()+"/foowwlite.db",1);
}
}
}
return uniqueInstance;
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
SQLiteDataProxy:
package com.xiaoqi.sqlitedemo;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class SQLiteDataProxy implements ISQLiteOperate {
private java.util.concurrent.Semaphore semaphoreTransaction = new java.util.concurrent.Semaphore(1);
private AtomicInteger mOpenCounter = new AtomicInteger();
private SQLiteDatabase db;
private Cursor cursor;
private SQLiteDataProxy() {
}
private static SQLiteDataProxy proxy;
private static DBOpenHelper helper;
public static SQLiteDataProxy getSQLiteProxy(Context context) {
helper = DBOpenHelper.getInstance(context);
if (proxy == null) {
synchronized (SQLiteDataProxy.class) {
if (proxy == null) {
proxy = new SQLiteDataProxy();
}
}
}
return proxy;
}
private SQLiteDatabase getSQLiteDataBase() {
if (mOpenCounter.incrementAndGet() == 1) {
db = helper.getWritableDatabase();
}
return db;
}
private void closeSQLiteDatabase(){
if(mOpenCounter.decrementAndGet() == 0){
db.close();
}
}
@Override
public boolean execSQL(String sql) {
boolean result = true;
db = getSQLiteDataBase();
try {
db.execSQL(sql);
} catch (Exception e) {
Log.e("SQLERROR", "In SQLDA:" + e.getMessage() + sql);
result = false;
} finally {
closeSQLiteDatabase();
}
return result;
}
@Override
public boolean execSQLList(List<String> sqlList) {
boolean result = true;
db = getSQLiteDataBase();
String currentSqlString = "";
try {
semaphoreTransaction.acquire();
db.beginTransaction();
for (String sql : sqlList) {
currentSqlString = sql;
db.execSQL(sql);
}
db.setTransactionSuccessful();
result = true;
} catch (Exception e) {
result = false;
Log.e("SQLERROR", "IN SQLDA: " + e.getMessage() + currentSqlString);
} finally {
db.endTransaction();
semaphoreTransaction.release();
closeSQLiteDatabase();
}
return result;
}
@Override
public boolean execSQLs(List<String[]> sqlList) {
boolean result = true;
db = getSQLiteDataBase();
String currentSql = "";
try {
semaphoreTransaction.acquire();
db.beginTransaction();
for (String[] arr : sqlList) {
currentSql = arr[0];
Cursor curCount = db.rawQuery(arr[0], null);
curCount.moveToFirst();
int count = curCount.getInt(0);
curCount.close();
if (count == 0) {
if (arr[1] != null && arr[1].length() > 0) {
currentSql = arr[1];
db.execSQL(arr[1]);
}
} else {
if (arr.length > 2 && arr[2] != null && arr[2].length() > 0) {
currentSql = arr[2];
db.execSQL(arr[2]);
}
}
}
db.setTransactionSuccessful();
result = true;
} catch (Exception e) {
Log.e("SQLERROR", "IN SQLDA: " + currentSql + e.getMessage());
result = false;
} finally {
db.endTransaction();
semaphoreTransaction.release();
closeSQLiteDatabase();
}
return result;
}
@Override
public boolean execSQLIgnoreError(List<String> sqlList) {
db = getSQLiteDataBase();
try {
semaphoreTransaction.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
db.beginTransaction();
for (String sql : sqlList) {
try {
db.execSQL(sql);
} catch (Exception e) {
Log.e("SQLERROR", "IN SQLDA: " + sql + e.getMessage());
}
}
db.setTransactionSuccessful();
db.endTransaction();
semaphoreTransaction.release();
closeSQLiteDatabase();
return true;
}
@Override
public Cursor query(String sql) {
return query(sql, null);
}
@Override
public Cursor query(String sql, String[] params) {
db = getSQLiteDataBase();
cursor = db.rawQuery(sql, params);
return cursor;
}
@Override
public void close() {
if (cursor != null) {
cursor.close();
}
if (db != null) {
db.close();
}
}
}
DBManager:
package com.xiaoqi.sqlitedemo;
import android.content.Context;
import android.database.Cursor;
import java.util.List;
public class DBManager {
public static void asyncExecSQL(final Context context, final String sql){
new Thread(new Runnable() {
@Override
public void run() {
SQLiteDataProxy.getSQLiteProxy(context).execSQL(sql);
}
}).start();
}
public static void asyncExecSQLList(final Context context,final List<String> sqlList){
new Thread(new Runnable() {
@Override
public void run() {
SQLiteDataProxy.getSQLiteProxy(context).execSQLList(sqlList);
}
}).start();
}
public static void asyncExecSQLs(final Context context,final List<String[]> sqlList){
new Thread(new Runnable() {
@Override
public void run() {
SQLiteDataProxy.getSQLiteProxy(context).execSQLs(sqlList);
}
}).start();
}
public static void asyncExecSQLIgnoreError(final Context context,final List<String> sqlList){
new Thread(new Runnable() {
@Override
public void run() {
SQLiteDataProxy.getSQLiteProxy(context).execSQLIgnoreError(sqlList);
}
}).start();
}
public static boolean execSQL( Context context, String sql){
return SQLiteDataProxy.getSQLiteProxy(context).execSQL(sql);
}
public static boolean execSQLList( Context context, List<String> sqlList){
return SQLiteDataProxy.getSQLiteProxy(context).execSQLList(sqlList);
}
public static boolean execSQLs( Context context, List<String[]> sqlList){
return SQLiteDataProxy.getSQLiteProxy(context).execSQLs(sqlList);
}
public static boolean execSQL( Context context, List<String> sqlList){
return SQLiteDataProxy.getSQLiteProxy(context).execSQLIgnoreError(sqlList);
}
public static Cursor query(Context context, String sql){
return SQLiteDataProxy.getSQLiteProxy(context).query(sql);
}
public static Cursor query(Context context, String sql, String[] params){
return SQLiteDataProxy.getSQLiteProxy(context).query(sql, params);
}
public static void close(Context context){
SQLiteDataProxy.getSQLiteProxy(context).close();
}
}
建議使用的時候不要直接調用SQLiteDataProxy的方法,而是通過DBManager來執行SQL操作。
使用方法很簡單:
DBManager.asyncExecSQLList(context,getSQLList())
DBManager.execSQLList(context,getSQLList())
、、、
如有什麼問題和建議歡迎大家提出。
行爲真正偉大的人,別人會從他的善行感受出來。一天沒有臆見善行,就是白過了。獎章和頭銜不能讓你上天堂,善行才能增加你的分量。建設性的行爲才能服人,言語的吹噓無益。不要說你想要什麼,用行爲表達。善行是讚美自己最好的辦法。如果你比別人更具智慧,別人會從你的行爲看出來。