sqlite并发操作有关异常处理

目录

1. database is locked的原因

(1)多线程访问造成的数据库锁定

(2)执行事务操作未正常关闭

(3)sqlite自身问题

2. 解决办法

(1)办法1

解决问题1:避免重复打开数据库

解决问题2:如果当前执行的是许多sql语句,要用到事务怎么办?

解决问题3:如果在多线程下执行query方法怎么办,如果处理close?

解决问题4:实际使用过程中,很容易导致一个错误,当cursor或db已经关闭的时候,又去调用了它,导致异常。

(2) 办法2


最近碰到一个异常问题:


 
 
  1. 01-01 08:01:14.011 580 595 E SQLiteLog: (5) database is locked
  2. 01-01 08:01:14.015 580 595 E SQLiteDatabase: Failed to open database '/data/user_de/0/com.android.providers.settings/databases/settings.db'.
  3. 01-01 08:01:14.015 580 595 E SQLiteDatabase: android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5): , while compiling: PRAGMA journal_mode
  4. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)
  5. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:890)
  6. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.executeForString(SQLiteConnection.java:635)
  7. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.setJournalMode(SQLiteConnection.java:321)
  8. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:295)
  9. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:216)
  10. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:194)
  11. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:493)
  12. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:200)
  13. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:192)
  14. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:864)
  15. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:849)
  16. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:724)
  17. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:714)
  18. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:295)
  19. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:238)
  20. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.providers.settings.SettingsProvider $SettingsRegistry $UpgradeController.upgradeIfNeededLocked(SettingsProvider.java:2966)
  21. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.providers.settings.SettingsProvider $SettingsRegistry.ensureSettingsForUserLocked(SettingsProvider.java:2354)
  22. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.providers.settings.SettingsProvider $SettingsRegistry.peekSettingsStateLocked(SettingsProvider.java:2618)
  23. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.providers.settings.SettingsProvider $SettingsRegistry.getSettingsNamesLocked(SettingsProvider.java:2300)
  24. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.providers.settings.SettingsProvider $SettingsRegistry.syncSsaidTableOnStart(SettingsProvider.java:2282)
  25. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.providers.settings.SettingsProvider $SettingsRegistry.<init>(SettingsProvider.java:2180)
  26. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.providers.settings.SettingsProvider.onCreate(SettingsProvider.java:326)
  27. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.content.ContentProvider.attachInfo(ContentProvider.java:1917)
  28. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.content.ContentProvider.attachInfo(ContentProvider.java:1892)
  29. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.app.ActivityThread.installProvider(ActivityThread.java:6266)
  30. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.app.ActivityThread.acquireProvider(ActivityThread.java:5875)
  31. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.app.ContextImpl $ApplicationContentResolver.acquireProvider(ContextImpl.java:2508)
  32. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.content.ContentResolver.acquireProvider(ContentResolver.java:1764)
  33. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.provider.Settings $ContentProviderHolder.getProvider(Settings.java:1798)
  34. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.provider.Settings $NameValueCache.getStringForUser(Settings.java:1887)
  35. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.provider.Settings $Global.getStringForUser(Settings.java:10532)
  36. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.provider.Settings $Global.getString(Settings.java:10521)
  37. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.server.DropBoxManagerService.isTagEnabled(DropBoxManagerService.java:323)
  38. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.server.DropBoxManagerService $2.isTagEnabled(DropBoxManagerService.java:144)
  39. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.os.DropBoxManager.isTagEnabled(DropBoxManager.java:346)
  40. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.server.am.ActivityManagerService.addErrorToDropBox(ActivityManagerService.java:14957)
  41. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.server.am.ActivityManagerService.handleApplicationWtfInner(ActivityManagerService.java:14838)
  42. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.server.am.ActivityManagerService $21.run(ActivityManagerService.java:14808)
  43. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.os.Handler.handleCallback(Handler.java:790)
  44. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.os.Handler.dispatchMessage(Handler.java:99)
  45. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.os.Looper.loop(Looper.java:164)
  46. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at android.os.HandlerThread.run(HandlerThread.java:65)
  47. 01-01 08:01:14.015 580 595 E SQLiteDatabase: at com.android.server.ServiceThread.run(ServiceThread.java:46)
  48. 01-01 08:01:14.017 580 595 E AndroidRuntime: *** FATAL EXCEPTION IN SYSTEM PROCESS: ActivityManager
  49. 01-01 08:01:14.017 580 595 E AndroidRuntime: java.lang.RuntimeException: Unable to get provider com.android.providers.settings.SettingsProvider: android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5): , while compiling: PRAGMA journal_mode
  50. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.app.ActivityThread.installProvider(ActivityThread.java:6269)
  51. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.app.ActivityThread.acquireProvider(ActivityThread.java:5875)
  52. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.app.ContextImpl $ApplicationContentResolver.acquireProvider(ContextImpl.java:2508)
  53. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.content.ContentResolver.acquireProvider(ContentResolver.java:1764)
  54. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.provider.Settings $ContentProviderHolder.getProvider(Settings.java:1798)
  55. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.provider.Settings $NameValueCache.getStringForUser(Settings.java:1887)
  56. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.provider.Settings $Global.getStringForUser(Settings.java:10532)
  57. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.provider.Settings $Global.getString(Settings.java:10521)
  58. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.server.DropBoxManagerService.isTagEnabled(DropBoxManagerService.java:323)
  59. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.server.DropBoxManagerService $2.isTagEnabled(DropBoxManagerService.java:144)
  60. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.os.DropBoxManager.isTagEnabled(DropBoxManager.java:346)
  61. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.server.am.ActivityManagerService.addErrorToDropBox(ActivityManagerService.java:14957)
  62. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.server.am.ActivityManagerService.handleApplicationWtfInner(ActivityManagerService.java:14838)
  63. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.server.am.ActivityManagerService $21.run(ActivityManagerService.java:14808)
  64. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:790)
  65. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:99)
  66. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.os.Looper.loop(Looper.java:164)
  67. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.os.HandlerThread.run(HandlerThread.java:65)
  68. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.server.ServiceThread.run(ServiceThread.java:46)
  69. 01-01 08:01:14.017 580 595 E AndroidRuntime: Caused by: android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5): , while compiling: PRAGMA journal_mode
  70. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)
  71. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:890)
  72. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteConnection.executeForString(SQLiteConnection.java:635)
  73. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteConnection.setJournalMode(SQLiteConnection.java:321)
  74. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:295)
  75. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:216)
  76. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:194)
  77. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:493)
  78. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:200)
  79. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:192)
  80. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:864)
  81. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:849)
  82. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:724)
  83. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:714)
  84. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:295)
  85. 01-01 08:01:14.017 580 595 E AndroidRuntime: at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:238)
  86. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.providers.settings.SettingsProvider $SettingsRegistry $UpgradeController.upgradeIfNeededLocked(SettingsProvider.java:2966)
  87. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.providers.settings.SettingsProvider $SettingsRegistry.ensureSettingsForUserLocked(SettingsProvider.java:2354)
  88. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.providers.settings.SettingsProvider $SettingsRegistry.peekSettingsStateLocked(SettingsProvider.java:2618)
  89. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.providers.settings.SettingsProvider $SettingsRegistry.getSettingsNamesLocked(SettingsProvider.java:2300)
  90. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.providers.settings.SettingsProvider $SettingsRegistry.syncSsaidTableOnStart(SettingsProvider.java:2282)
  91. 01-01 08:01:14.017 580 595 E AndroidRuntime: at com.android.providers.settings.SettingsProvider $SettingsRegistry.<init>(SettingsProvider.java:2180)
  92. 01-01 08:01:14.018 580 595 E AndroidRuntime: at com.android.providers.settings.SettingsProvider.onCreate(SettingsProvider.java:326)
  93. 01-01 08:01:14.018 580 595 E AndroidRuntime: at android.content.ContentProvider.attachInfo(ContentProvider.java:1917)
  94. 01-01 08:01:14.018 580 595 E AndroidRuntime: at android.content.ContentProvider.attachInfo(ContentProvider.java:1892)
  95. 01-01 08:01:14.018 580 595 E AndroidRuntime: at android.app.ActivityThread.installProvider(ActivityThread.java:6266)
  96. 01-01 08:01:14.018 580 595 E AndroidRuntime: ... 18 more

类似的错误信息:android-emulator - 从 18到 17的Android API级别更改,但Android从不是"Boots Up"

参考了各大博主的博客以及论坛,对于该问题的分析如下:

1. database is locked的原因

参考自SQLiteDatabaseLockedException

(1)多线程访问造成的数据库锁定

       sqlite同一时间只能进行一个写操作,当同同时有两个写操作时,后执行的只能先等待,如果等待时间超过5s就会产生这种错误。同样一个文件正在写入,重复打开数据库操作更容易导致这种问题发生。(参考自SQLite并发操作下的分析与处理

       如A线程在访问当前的数据库,这时候B线程也需要访问数据库,这样在B线程中,就会有类似以上的异常产生,我们需要将提供数据库访问的方法设置成同步的,防止异步调用时出现问题,如在调用方法中增加 synchronized 修饰符。 
使用synchronized 关键字来修饰获取数据库连接的方法,或者使用isDbLockedByOtherThreads方法判断数据库是否被锁住了,然后等待一定的时间再进行访问。 

(2)执行事务操作未正常关闭

如下面代码,使用事务操作数据库,但事务执行完成后未调用db.endTransaction();关闭事务。 


 
 
  1. public ArrayList GetIndustryList(){
  2. ArrayList IndustryList= new ArrayList();
  3. SQLiteDatabase db=openDatabase();
  4. db.beginTransaction(); //执行事务,无对应的关闭事务
  5. Cursor cursor = db.rawQuery( "select * from dcIndustry", null);
  6. while(cursor.moveToNext()){
  7. IndustryList.add( new Industry(cursor.getString( 0),cursor.getString( 1)));
  8. }
  9. db.close();
  10. return IndustryList;
  11. }

Android 4.0以前的版本db.close();会结束事务,而Jelly Bean 以后的版本因为安全性的问题,必须结束即endTransactiony以后才能再次访问本地数据库。 

所以要修改为:


 
 
  1. public ArrayList GetIndustryList(){
  2. ArrayList IndustryList= new ArrayList();
  3. SQLiteDatabase db=openDatabase();
  4. db.beginTransaction(); //执行事务,无对应的关闭事务
  5. Cursor cursor = db.rawQuery( "select * from dcIndustry", null);
  6. while(cursor.moveToNext()){
  7. IndustryList.add( new Industry(cursor.getString( 0),cursor.getString( 1)));
  8. }
  9. db.endTransaction();
  10. db.close();
  11. return IndustryList;
  12. }

(3)sqlite自身问题

      有时我们会在调试程序的时候发现Log控制台频繁的出现上述异常,而在运行程序的时候就没有这个问题,这种现象我在调试ResultSet时也会出现,查资料找原因是因为sqlite数据不完全是线程安全的,导致在一些地方会莫名其妙的出问题,如果遇到这样的情况,那只能不要将断点打到数据库连接的地方了。

2. 解决办法

(1)办法1

参考自SQLite并发操作下的分析与处理,解决database is locked,以及多线程下执行事务等问题

解决问题1:避免重复打开数据库

引入单例方法与SQLiteOpenHelper类:


 
 
  1. public class DBOpenHelper extends SQLiteOpenHelper{
  2. private DBOpenHelper(Context context,String dbPath, int version) {
  3. super(context, dbPath , null, version);
  4. }
  5. private volatile static DBOpenHelper uniqueInstance;
  6. public static DBOpenHelper getInstance(Context context) {
  7. if (uniqueInstance == null) {
  8. synchronized (DBOpenHelper.class) {
  9. if (uniqueInstance == null) {
  10. uniqueInstance = new DBOpenHelper(context,context.getFilesDir().getAbsolutePath()+ "/foowwlite.db", 1);
  11. }
  12. }
  13. }
  14. return uniqueInstance;
  15. }
  16. @Override
  17. public void onCreate(SQLiteDatabase db) {
  18. }
  19. @Override
  20. public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
  21. }
  22. }

通过getInstance()方法得到helper对象来得到数据库,保证helper类是单例的。然后通过代理类SQLiteDataProxy来控制对数据库的访问:


 
 
  1. private static SQLiteDataProxy proxy;
  2. private static DBOpenHelper helper;
  3. public static SQLiteDataProxy getSQLiteProxy(Context context) {
  4. helper = DBOpenHelper.getInstance(context);
  5. if (proxy == null) {
  6. synchronized (SQLiteDataProxy.class) {
  7. if (proxy == null) {
  8. proxy = new SQLiteDataProxy();
  9. }
  10. }
  11. }
  12. return proxy;
  13. }
  14. }

同样使用单例来保证对象的唯一性。然后写了一个方法用于执行sql语句:


 
 
  1. @Override
  2. public boolean execSQL(String sql) {
  3. boolean result = true;
  4. if (!db.isOpen()) {
  5. db = helper.getWritableDatabase();
  6. }
  7. try {
  8. db.execSQL(sql);
  9. } catch (Exception e) {
  10. Log.e( "SQLERROR", "In SQLDA:" + e.getMessage() + sql);
  11. result = false;
  12. } finally {
  13. db.close();
  14. }
  15. return result;
  16. }

每次获取db对象即SQLiteDataBase的时候先判断是否已经打开,如果已经打开则不需要重新获取,避免了db的重复开启。

接下来实验一下,通过Viewpager+Fragment的方式测试,因为fragment会同时被加载。在两个fragment中各自新建一个子线程来执行大量insert语句:


 
 
  1. new Thread( new Runnable() {
  2. @Override
  3. public void run() {
  4. for (String sql:getSQLList()){
  5. SQLiteDataProxy.getSQLiteProxy(getActivity()).execSQL(sql);
  6. }
  7. }
  8. }).start();

先分析一下,虽然都是子线程,但是两个fragment都是通过单例获得db对象来执行sql语句,因此db应该是同一个,这时候它们并发执行sql应该没问题。

但是运行还是报错了:attempt to re-open an already-closed object。

意思是数据库已经关闭了,但是仍然用一个已经关闭的db去执行sql语句。错误原因是什么呢?明明已经判断了db.isOpen,再执行sql的。

例子中的原因如下:

线程1和线程2同时执行execSQL方法,线程1先获取到了db对象,开始执行sql语句,与此同时,线程2判断db.isOpen结果是打开的,于是不重新get,直接调用db开始执行sql语句。但现在问题来了,线程2准备执行sql的时候,线程1已经把sql执行完并且关闭了。由于线程1和线程2得到的db是同一个对象,线程2的db也关闭了,这时候执行sql就导致了这个错误。

这种情况,可以引入一个全局变量来计算打开关闭db的次数,而java也刚好提供了这个方法:AtomicInteger

AtomicInteger是一个线程安全的类,可以通过它计数,无论什么线程AtomicInteger值+1后都会改变。

将判断db是否打开的方法改成以下:

private AtomicInteger mOpenCounter = new AtomicInteger();

 
 

关闭db的方法修改为:


 
 
  1. if (mOpenCounter.incrementAndGet() == 1) {
  2. db = helper.getWritableDatabase();
  3. }
  4. return db;

意思就是每当想要获得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,当线程1的sql执行完后,线程1的db尝试关闭,会调用mOpenCounter.decrementAndGet()自减1,结果就等于1。说明还有1个正在执行的sql,即线程2正在执行。因此db不会去关闭,然后线程2正常执行,线程2执行完sql,尝试关闭db,此时decrementAndGet再自减1,就等于0,说明已经没有其他真正执行的sql,于是可以正常关闭。

这种方法保证了只有在所有sql都执行完后才去关闭,并且只会最后一次关闭,保证了不会出现re-open an already-closed问题。

修改后还是报错:

Attempt to invoke virtual method 'void android.database.sqlite.SQLiteDatabase.execSQL(java.lang.String)' on a null object referenceInsert
 
 

显然db为null。为什么会导致这种错误呢?

分析一下AtomicInteger,并没有逻辑上的问题呀。

将代码改成如下:


 
 
  1. private SQLiteDatabase getSQLiteDataBase() {
  2. Log.e( "111", "Once start");
  3. if (mOpenCounter.incrementAndGet() == 1 || db == null) {
  4. db = helper.getWritableDatabase();
  5. }
  6. if (db == null) {
  7. Log.e( "111", mOpenCounter.intValue() + "");
  8. } else {
  9. Log.e( "111", mOpenCounter.intValue() + " NOT NULL");
  10. }
  11. return db;
  12. }

运行后结果:

可以想到,线程1和线程2同时尝试获取db,线程1中mOpenCounter + 1 = =1,但此时db还没有获取的情况下,线程2也执行了获取db的方法,mOpenCounter +1==2,但由于获取db的getWritableDatabase()需要一定的时间,而先执行的线程1的db还没有被获取到,线程2却已经也经过判断并且return db,此时的db就是null了,导致了空指针错误。

既然原因已经找到了,那么解决就很简单了,只需要多加一个非空判断就行了,而getWritableDataBase()本身就是线程安全的,应该只需要这样就可以解决。


 
 
  1. private SQLiteDatabase getSQLiteDataBase() {
  2. if (mOpenCounter.incrementAndGet() == 1 || db == null) {
  3. db = helper.getWritableDatabase();
  4. }
  5. return db;
  6. }

这样修改后,经过测试没问题。

解决问题2:如果当前执行的是许多sql语句,要用到事务怎么办?

如果是事务,事务执行的时候调用beginTransaction(),完成后调用db.setTransactionSuccessful()、db.endTransaction()标志事务的结束,但是如果多线程下调用了事务怎么办?尤其还是单例模式下,同时调用方法开启事务,肯定会出问题。

如以下方法:


 
 
  1. public boolean execSQLList(List<String> sqlList) {
  2. boolean result = true;
  3. db = getSQLiteDataBase();
  4. String currentSqlString = "";
  5. try {
  6. db.beginTransaction();
  7. for (String sql : sqlList) {
  8. currentSqlString = sql;
  9. db.execSQL(sql);
  10. }
  11. db.setTransactionSuccessful();
  12. result = true;
  13. } catch (Exception e) {
  14. result = false;
  15. Log.e( "SQLERROR", "IN SQLDA: " + e.getMessage() + currentSqlString);
  16. } finally {
  17. db.endTransaction();
  18. closeSQLiteDatabase();
  19. }
  20. return result;
  21. }

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之后,该线程的事务就会继续执行。这样解决事务并发产生的问题,也保证了事务可以执行完毕。

改进后代码:


 
 
  1. private java.util.concurrent.Semaphore semaphoreTransaction = new java.util.concurrent.Semaphore( 1);
  2. public boolean execSQLList(List<String> sqlList) {
  3. boolean result = true;
  4. db = getSQLiteDataBase();
  5. String currentSqlString = "";
  6. try {
  7. semaphoreTransaction.acquire();
  8. db.beginTransaction();
  9. for (String sql : sqlList) {
  10. currentSqlString = sql;
  11. db.execSQL(sql);
  12. }
  13. db.setTransactionSuccessful();
  14. result = true;
  15. } catch (Exception e) {
  16. result = false;
  17. Log.e( "SQLERROR", "IN SQLDA: " + e.getMessage() + currentSqlString);
  18. } finally {
  19. db.endTransaction();
  20. semaphoreTransaction.release();
  21. closeSQLiteDatabase();
  22. }
  23. return result;
  24. }

经过修改后,经过测试,多线程下事务也可以正常插入,一些常见的由SQLite并发产生的问题也得以解决。

全部代码如下:

【ISQLiteOperate】


 
 
  1. package com.xiaoqi.sqlitedemo;
  2. import android.database.Cursor;
  3. import java.util.List;
  4. /**
  5. * Created by xiaoqi on 2016/9/1.
  6. */
  7. public interface ISQLiteOperate {
  8. boolean execSQL(String sql);
  9. boolean execSQLList(List<String> sqlList);
  10. boolean execSQLs(List<String[]> sqlList);
  11. boolean execSQLIgnoreError(List<String> sqlList);
  12. Cursor query(String sql);
  13. Cursor query(String sql, String[] params);
  14. void close();
  15. }

【DBOpenHelper】


 
 
  1. package com.xiaoqi.sqlitedemo;
  2. import android.content.Context;
  3. import android.database.sqlite.SQLiteDatabase;
  4. import android.database.sqlite.SQLiteOpenHelper;
  5. /**
  6. * Created by xiaoqi on 2016/9/1.
  7. */
  8. public class DBOpenHelper extends SQLiteOpenHelper{
  9. private DBOpenHelper(Context context,String dbPath, int version) {
  10. super(context, dbPath , null, version);
  11. }
  12. private volatile static DBOpenHelper uniqueInstance;
  13. public static DBOpenHelper getInstance(Context context) {
  14. if (uniqueInstance == null) {
  15. synchronized (DBOpenHelper.class) {
  16. if (uniqueInstance == null) {
  17. uniqueInstance = new DBOpenHelper(context,context.getFilesDir().getAbsolutePath()+ "/foowwlite.db", 1);
  18. }
  19. }
  20. }
  21. return uniqueInstance;
  22. }
  23. @Override
  24. public void onCreate(SQLiteDatabase db) {
  25. }
  26. @Override
  27. public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
  28. }
  29. }

【SQLiteDataProxy】


 
 
  1. package com.xiaoqi.sqlitedemo;
  2. import android.content.Context;
  3. import android.database.Cursor;
  4. import android.database.sqlite.SQLiteDatabase;
  5. import android.util.Log;
  6. import java.util.List;
  7. import java.util.concurrent.atomic.AtomicInteger;
  8. public class SQLiteDataProxy implements ISQLiteOperate {
  9. private java.util.concurrent.Semaphore semaphoreTransaction = new java.util.concurrent.Semaphore( 1);
  10. private AtomicInteger mOpenCounter = new AtomicInteger();
  11. private SQLiteDatabase db;
  12. private Cursor cursor;
  13. private SQLiteDataProxy() {
  14. }
  15. private static SQLiteDataProxy proxy;
  16. private static DBOpenHelper helper;
  17. public static SQLiteDataProxy getSQLiteProxy(Context context) {
  18. helper = DBOpenHelper.getInstance(context);
  19. if (proxy == null) {
  20. synchronized (SQLiteDataProxy.class) {
  21. if (proxy == null) {
  22. proxy = new SQLiteDataProxy();
  23. }
  24. }
  25. }
  26. return proxy;
  27. }
  28. private SQLiteDatabase getSQLiteDataBase() {
  29. if (mOpenCounter.incrementAndGet() == 1) {
  30. db = helper.getWritableDatabase();
  31. }
  32. return db;
  33. }
  34. private void closeSQLiteDatabase(){
  35. if(mOpenCounter.decrementAndGet() == 0){
  36. db.close();
  37. }
  38. }
  39. @Override
  40. public boolean execSQL(String sql) {
  41. boolean result = true;
  42. db = getSQLiteDataBase();
  43. try {
  44. db.execSQL(sql);
  45. } catch (Exception e) {
  46. Log.e( "SQLERROR", "In SQLDA:" + e.getMessage() + sql);
  47. result = false;
  48. } finally {
  49. closeSQLiteDatabase();
  50. }
  51. return result;
  52. }
  53. @Override
  54. public boolean execSQLList(List<String> sqlList) {
  55. boolean result = true;
  56. db = getSQLiteDataBase();
  57. String currentSqlString = "";
  58. try {
  59. semaphoreTransaction.acquire();
  60. db.beginTransaction();
  61. for (String sql : sqlList) {
  62. currentSqlString = sql;
  63. db.execSQL(sql);
  64. }
  65. db.setTransactionSuccessful();
  66. result = true;
  67. } catch (Exception e) {
  68. result = false;
  69. Log.e( "SQLERROR", "IN SQLDA: " + e.getMessage() + currentSqlString);
  70. } finally {
  71. db.endTransaction();
  72. semaphoreTransaction.release();
  73. closeSQLiteDatabase();
  74. }
  75. return result;
  76. }
  77. @Override
  78. public boolean execSQLs(List<String[]> sqlList) {
  79. boolean result = true;
  80. db = getSQLiteDataBase();
  81. String currentSql = "";
  82. try {
  83. semaphoreTransaction.acquire();
  84. db.beginTransaction();
  85. for (String[] arr : sqlList) {
  86. currentSql = arr[ 0];
  87. Cursor curCount = db.rawQuery(arr[ 0], null);
  88. curCount.moveToFirst();
  89. int count = curCount.getInt( 0);
  90. curCount.close();
  91. if (count == 0) {
  92. if (arr[ 1] != null && arr[ 1].length() > 0) {
  93. currentSql = arr[ 1];
  94. db.execSQL(arr[ 1]);
  95. }
  96. } else {
  97. if (arr.length > 2 && arr[ 2] != null && arr[ 2].length() > 0) {
  98. currentSql = arr[ 2];
  99. db.execSQL(arr[ 2]);
  100. }
  101. }
  102. }
  103. db.setTransactionSuccessful();
  104. result = true;
  105. } catch (Exception e) {
  106. Log.e( "SQLERROR", "IN SQLDA: " + currentSql + e.getMessage());
  107. result = false;
  108. } finally {
  109. db.endTransaction();
  110. semaphoreTransaction.release();
  111. closeSQLiteDatabase();
  112. }
  113. return result;
  114. }
  115. @Override
  116. public boolean execSQLIgnoreError(List<String> sqlList) {
  117. db = getSQLiteDataBase();
  118. try {
  119. semaphoreTransaction.acquire();
  120. } catch (InterruptedException e) {
  121. e.printStackTrace();
  122. }
  123. db.beginTransaction();
  124. for (String sql : sqlList) {
  125. try {
  126. db.execSQL(sql);
  127. } catch (Exception e) {
  128. Log.e( "SQLERROR", "IN SQLDA: " + sql + e.getMessage());
  129. }
  130. }
  131. db.setTransactionSuccessful();
  132. db.endTransaction();
  133. semaphoreTransaction.release();
  134. closeSQLiteDatabase();
  135. return true;
  136. }
  137. @Override
  138. public Cursor query(String sql) {
  139. return query(sql, null);
  140. }
  141. @Override
  142. public Cursor query(String sql, String[] params) {
  143. db = getSQLiteDataBase();
  144. cursor = db.rawQuery(sql, params);
  145. return cursor;
  146. }
  147. @Override
  148. public void close() {
  149. if (cursor != null) {
  150. cursor.close();
  151. }
  152. closeSQLiteDatabase();
  153. }
  154. }

【DBManager】


 
 
  1. package com.xiaoqi.sqlitedemo;
  2. import android.content.Context;
  3. import android.database.Cursor;
  4. import java.util.List;
  5. public class DBManager {
  6. public static void asyncExecSQL(final Context context, final String sql){
  7. new Thread( new Runnable() {
  8. @Override
  9. public void run() {
  10. SQLiteDataProxy.getSQLiteProxy(context).execSQL(sql);
  11. }
  12. }).start();
  13. }
  14. public static void asyncExecSQLList(final Context context,final List<String> sqlList){
  15. new Thread( new Runnable() {
  16. @Override
  17. public void run() {
  18. SQLiteDataProxy.getSQLiteProxy(context).execSQLList(sqlList);
  19. }
  20. }).start();
  21. }
  22. public static void asyncExecSQLs(final Context context,final List<String[]> sqlList){
  23. new Thread( new Runnable() {
  24. @Override
  25. public void run() {
  26. SQLiteDataProxy.getSQLiteProxy(context).execSQLs(sqlList);
  27. }
  28. }).start();
  29. }
  30. public static void asyncExecSQLIgnoreError(final Context context,final List<String> sqlList){
  31. new Thread( new Runnable() {
  32. @Override
  33. public void run() {
  34. SQLiteDataProxy.getSQLiteProxy(context).execSQLIgnoreError(sqlList);
  35. }
  36. }).start();
  37. }
  38. public static boolean execSQL( Context context, String sql){
  39. return SQLiteDataProxy.getSQLiteProxy(context).execSQL(sql);
  40. }
  41. public static boolean execSQLList( Context context, List<String> sqlList){
  42. return SQLiteDataProxy.getSQLiteProxy(context).execSQLList(sqlList);
  43. }
  44. public static boolean execSQLs( Context context, List<String[]> sqlList){
  45. return SQLiteDataProxy.getSQLiteProxy(context).execSQLs(sqlList);
  46. }
  47. public static boolean execSQL( Context context, List<String> sqlList){
  48. return SQLiteDataProxy.getSQLiteProxy(context).execSQLIgnoreError(sqlList);
  49. }
  50. public static Cursor query(Context context, String sql){
  51. return SQLiteDataProxy.getSQLiteProxy(context).query(sql);
  52. }
  53. public static Cursor query(Context context, String sql, String[] params){
  54. return SQLiteDataProxy.getSQLiteProxy(context).query(sql, params);
  55. }
  56. public static void close(Context context){
  57. SQLiteDataProxy.getSQLiteProxy(context).close();
  58. }
  59. }

建议使用的时候不要直接调用SQLiteDataProxy的方法,而是通过DBManager来执行SQL操作。使用方法如下:


 
 
  1. DBManager.asyncExecSQLList(context,getSQLList())
  2. DBManager.execSQLList(context,getSQLList())

解决问题3:如果在多线程下执行query方法怎么办,如果处理close?


 
 
  1. public Cursor query(String sql, String[] params) {
  2. db = getSQLiteDataBase();
  3. cursor = db.rawQuery(sql, params);
  4. return cursor;
  5. }

其他的方法无论什么情况最后都会执行close方法,但是query方法不一样,因为调用这个方法需要我们手动控制close,如果多线程下执行这个方法,并且有一个线程的query出现异常了怎么办?这样close方法就不能正常调用,会导致数据库永远关闭不了。

为了解决这个方法,可以引入一个变量:

private ThreadLocal<Boolean> isQuery = new ThreadLocal<>();

 
 

ThreadLocal可以让这个boolean值保持只在自己的线程中改变,不受其他线程影响。因此我们可以通过这个值来判断是否query执行成功并且关闭,如果query中发生异常,就调用close方法,没有异常就不调用,这样就不会影响到openCount的值。

具体修改部分如下:


 
 
  1. private ThreadLocal<Boolean> isQuery = new ThreadLocal<>();
  2. @Override
  3. public Cursor query(String sql) {
  4. return query(sql, null);
  5. }
  6. @Override
  7. public Cursor query(String sql, String[] params) {
  8. isQuery.set( true); //设置为true,表示正在查询</span>
  9. db = getSQLiteDataBase();
  10. cursor = db.rawQuery(sql, params);
  11. return cursor;
  12. }
  13. /*如果调用query方法,抛异常时要调用此方法</span>
  14. */
  15. @Override
  16. public void closeWhileError(){
  17. if (cursor != null) {
  18. cursor.close();
  19. }
  20. if(isQuery.get()){ //没有执行完毕,异常后需要去关闭数据库
  21. closeSQLiteDataBase();
  22. }
  23. }
  24. private void closeSQLiteDataBase(){
  25. if(mOpenCounter.decrementAndGet() == 0){
  26. db.close();
  27. isQuery.set( false); //设置为false,表示执行完毕</span>
  28. Log.i( "DataBaseState", "DB------Closed");
  29. }
  30. }

解决问题4:实际使用过程中,很容易导致一个错误,当cursor或db已经关闭的时候,又去调用了它,导致异常。

上面的写法容错率太低,每次query和close都得对应,否则很容易导致上述错误。因此,使用了新的方法:

因为在代码中,用上述方法,如果少写了一个close,就会导致数据库永远不能关闭。为了避免这个问题,同时避免调用已经关闭的cursor、db问题,将所有的close方法都取消了,新建了一个service,监听最后一次查询的时间,如果超过5s没有进行新的数据库操作,再去关闭db,同时,每次查询时,用HashSet存储cursor,关闭db前先把所有的cursor关闭。

这种方法降低了容错率,同时避免了数据库的一直打开关闭,减少了资源的消耗。

(2) 办法2

另一种解决办法是使用enableWriteAheadLogging()来使得可以多线程访问

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值