`

android 多线程数据库读写分析与优化

 
阅读更多

原文:http://blog.csdn.net/lize1988/article/details/9700723

最新需要给软件做数据库读写方面的优化,之前无论读写,都是用一个 SQLiteOpenHelper.getWriteableDataBase() 来操作数据库,现在需要多线程并发读写,项目用的是2.2的SDK。

 

android 的数据库系统用的是sqlite ,sqlite的每一个数据库其实都是一个.db文件,它的同步锁也就精确到数据库级了,不能跟别的数据库有表锁,行锁。

所以对写实在有要求的,可以使用多个数据库文件。

哎,这数据库在多线程并发读写方面本身就挺操蛋的。

 

下面分析一下不同情况下,在同一个数据库文件上操作,sqlite的表现。

测试程序在2.2虚拟手机,4.2.1虚拟手机,4.2.1真手机上跑。

1,多线程写,使用一个SQLiteOpenHelper。也就保证了多线程使用一个SQLiteDatabase。

先看看相关的源码

 

  1. //SQLiteDatabase.java   
  2.   
  3. public long insertWithOnConflict(String table, String nullColumnHack,  
  4.             ContentValues initialValues, int conflictAlgorithm) {  
  5.         if (!isOpen()) {  
  6.             throw new IllegalStateException("database not open");  
  7.         }  
  8.   
  9.         .... 省略  
  10.   
  11.         lock();  
  12.         SQLiteStatement statement = null;  
  13.         try {  
  14.             statement = compileStatement(sql.toString());  
  15.   
  16.             // Bind the values  
  17.             if (entrySet != null) {  
  18.                 int size = entrySet.size();  
  19.                 Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();  
  20.                 for (int i = 0; i < size; i++) {  
  21.                     Map.Entry<String, Object> entry = entriesIter.next();  
  22.                     DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue());  
  23.                 }  
  24.             }  
  25.   
  26.             // Run the program and then cleanup  
  27.             statement.execute();  
  28.   
  29.             long insertedRowId = lastInsertRow();  
  30.             if (insertedRowId == -1) {  
  31.                 Log.e(TAG, "Error inserting " + initialValues + " using " + sql);  
  32.             } else {  
  33.                 if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {  
  34.                     Log.v(TAG, "Inserting row " + insertedRowId + " from "  
  35.                             + initialValues + " using " + sql);  
  36.                 }  
  37.             }  
  38.             return insertedRowId;  
  39.         } catch (SQLiteDatabaseCorruptException e) {  
  40.             onCorruption();  
  41.             throw e;  
  42.         } finally {  
  43.             if (statement != null) {  
  44.                 statement.close();  
  45.             }  
  46.             unlock();  
  47.         }  
  48.     }  



  1. //SQLiteDatabase.java   
  2.   
  3.   
  4.  private final ReentrantLock mLock = new ReentrantLock(true);  
  5.   
  6. /* package */ void lock() {  
  7.   
  8.        if (!mLockingEnabled) return;   
  9.   
  10.              mLock.lock();   
  11.   
  12.              if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {   
  13.   
  14.                  if (mLock.getHoldCount() == 1) {   
  15.   
  16.                        // Use elapsed real-time since the CPU may sleep when waiting for IO  
  17.   
  18.                        mLockAcquiredWallTime = SystemClock.elapsedRealtime();   
  19.   
  20.                        mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();   
  21.   
  22.                  }   
  23.   
  24.       }   
  25.   
  26. }  

 

通过源码可以知道,在执行插入时,会请求SQLiteDatabase对象的成员对象 mlock 的锁,来保证插入不会并发执行。

经测试不会引发异常。

 

但是我们可以通过使用多个SQLiteDatabase对象同时插入,来绕过这个锁。

2,多线程写,使用多个SQLiteOpenHelper,插入时可能引发异常,导致插入错误。

 

E/Database(1471): android.database.sqlite.SQLiteException: error code 5: database is locked08-01

 E/Database(1471):     at android.database.sqlite.SQLiteStatement.native_execute(Native Method)

E/Database(1471):     at android.database.sqlite.SQLiteStatement.execute(SQLiteStatement.java:55)

E/Database(1471):     at android.database.sqlite.SQLiteDatabase.insertWithOnConflict(SQLiteDatabase.java:1549)

 

多线程写,每个线程使用一个SQLiteOpenHelper,也就使得每个线程使用一个SQLiteDatabase对象。多个线程同时执行insert, 最后调用到本地方法  SQLiteStatement.native_execute

抛出异常,可见android 框架,多线程写数据库的本地方法里没有同步锁保护,并发写会抛出异常。

所以,多线程写必须使用同一个SQLiteOpenHelper对象。

 

3,多线程读

看SQLiteDatabase的源码可以知道,insert  , update ,  execSQL   都会 调用lock(), 乍一看唯有query 没有调用lock()。可是。。。

仔细看,发现

 

最后,查询结果是一个SQLiteCursor对象。

SQLiteCursor保存了查询条件,但是并没有立即执行查询,而是使用了lazy的策略,在需要时加载部分数据。

在加载数据时,调用了SQLiteQuery的fillWindow方法,而该方法依然会调用SQLiteDatabase.lock()

  1. /** 
  2.    * Reads rows into a buffer. This method acquires the database lock. 
  3.    * 
  4.    * @param window The window to fill into 
  5.    * @return number of total rows in the query 
  6.    */  
  7.   /* package */ int fillWindow(CursorWindow window,  
  8.           int maxRead, int lastPos) {  
  9.       long timeStart = SystemClock.uptimeMillis();  
  10.       mDatabase.lock();  
  11.       mDatabase.logTimeStat(mSql, timeStart, SQLiteDatabase.GET_LOCK_LOG_PREFIX);  
  12.       try {  
  13.           acquireReference();  
  14.           try {  
  15.               window.acquireReference();  
  16.               // if the start pos is not equal to 0, then most likely window is  
  17.               // too small for the data set, loading by another thread  
  18.               // is not safe in this situation. the native code will ignore maxRead  
  19.               int numRows = native_fill_window(window, window.getStartPosition(), mOffsetIndex,  
  20.                       maxRead, lastPos);  
  21.   
  22.               // Logging  
  23.               if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {  
  24.                   Log.d(TAG, "fillWindow(): " + mSql);  
  25.               }  
  26.               mDatabase.logTimeStat(mSql, timeStart);  
  27.               return numRows;  
  28.           } catch (IllegalStateException e){  
  29.               // simply ignore it  
  30.               return 0;  
  31.           } catch (SQLiteDatabaseCorruptException e) {  
  32.               mDatabase.onCorruption();  
  33.               throw e;  
  34.           } finally {  
  35.               window.releaseReference();  
  36.           }  
  37.       } finally {  
  38.           releaseReference();  
  39.           mDatabase.unlock();  
  40.       }  
  41.   }  

 

所以想要多线程读,读之间没有同步锁,也得每个线程使用各自的SQLiteOpenHelper对象,经测试,没有问题。

 

4,多线程读写

我们最终想要达到的目的,是多线程并发读写

多线程写之前已经知道结果了,同一时间只能有一个写。

多线程读可以并发

 

所以,使用下面的策略:

一个线程写,多个线程同时读,每个线程都用各自SQLiteOpenHelper。

这样,在java层,所有线程之间都不会锁住,也就是说,写与读之间不会锁,读与读之间也不会锁。

发现有插入异常。

E/SQLiteDatabase(18263): Error inserting descreption=InsertThread#01375493606407
E/SQLiteDatabase(18263): android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
E/SQLiteDatabase(18263):     at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)

插入异常,说明在有线程读的时候写数据库,会抛出异常。

 

分析源码可以知道, SQLiteOpenHelper.getReadableDatabase() 不见得获得的就是只读SQLiteDatabase 。

 

  1. //  SQLiteOpenHelper.java  
  2.   
  3.   public synchronized SQLiteDatabase getReadableDatabase() {  
  4.         if (mDatabase != null && mDatabase.isOpen()) {  
  5.            <span style="color:#FF0000;"return mDatabase;</span>  // The database is already open for business  
  6.         }  
  7.   
  8.         if (mIsInitializing) {  
  9.             throw new IllegalStateException("getReadableDatabase called recursively");  
  10.         }  
  11.   
  12.         try {  
  13.             return getWritableDatabase();  
  14.         } catch (SQLiteException e) {  
  15.             if (mName == nullthrow e;  // Can't open a temp database read-only!  
  16.             Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e);  
  17.         }  
  18.   
  19.         SQLiteDatabase db = null;  
  20.         try {  
  21.             mIsInitializing = true;  
  22.             String path = mContext.getDatabasePath(mName).getPath();  
  23.             db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);  
  24.             if (db.getVersion() != mNewVersion) {  
  25.                 throw new SQLiteException("Can't upgrade read-only database from version " +  
  26.                         db.getVersion() + " to " + mNewVersion + ": " + path);  
  27.             }  
  28.   
  29.             onOpen(db);  
  30.             Log.w(TAG, "Opened " + mName + " in read-only mode");  
  31.             mDatabase = db;  
  32.             return mDatabase;  
  33.         } finally {  
  34.             mIsInitializing = false;  
  35.             if (db != null && db != mDatabase) db.close();  
  36.         }  
  37.     }  

因为它先看有没有已经创建的SQLiteDatabase,没有的话先尝试创建读写 SQLiteDatabase ,失败后才尝试创建只读SQLiteDatabase 。

所以写了个新方法,来获得只读SQLiteDatabase 

 

  1. //DbHelper.java   
  2. //DbHelper extends SQLiteOpenHelper  
  3. public SQLiteDatabase getOnlyReadDatabase() {  
  4.         try{  
  5.             getWritableDatabase(); //保证数据库版本最新  
  6.         }catch(SQLiteException e){  
  7.             Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):",e);  
  8.         }  
  9.           
  10.         SQLiteDatabase db = null;  
  11.         try {  
  12.             String path = mContext.getDatabasePath(mName).getPath();  
  13.             db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);  
  14.             if (db.getVersion() != mNewVersion) {  
  15.                 throw new SQLiteException("Can't upgrade read-only database from version " +  
  16.                         db.getVersion() + " to " + mNewVersion + ": " + path);  
  17.             }  
  18.   
  19.             onOpen(db);  
  20.             readOnlyDbs.add(db);  
  21.             return db;  
  22.         } finally {  
  23.         }  
  24. }  

 

使用策略:一个线程写,多个线程同时读,只用一个SQLiteOpenHelper,读线程使用自己写的getOnlyReadDatabase()方法获得只读。
但是经过测试,还是会抛出异常,2.2上只有插入异常,4.1.2上甚至还有读异常。


4.1.2上测试,读异常。
 E/SQLiteLog(18263): (5) database is locked
W/dalvikvm(18263): threadid=21: thread exiting with uncaught exception (group=0x41e2c300)
 E/AndroidRuntime(18263): FATAL EXCEPTION: onlyReadThread#8
E/AndroidRuntime(18263): android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5): , while compiling: SELECT * FROM test_t

 

看来此路不同啊。


其实SQLiteDataBase 在API 11 多了一个 属性 ENABLE_WRITE_AHEAD_LOGGING

可以打,enableWriteAheadLogging(),可以关闭disableWriteAheadLogging(),默认是关闭的。


这个属性是什么意思呢?

参考api文档,这个属性关闭时,不允许读,写同时进行,通过 锁 来保证。

当打开时,它允许一个写线程与多个读线程同时在一个SQLiteDatabase上起作用。实现原理是写操作其实是在一个单独的文件,不是原数据库文件。所以写在执行时,不会影响读操作,读操作读的是原数据文件,是写操作开始之前的内容。

在写操作执行成功后,会把修改合并会原数据库文件。此时读操作才能读到修改后的内容。但是这样将花费更多的内存。
有了它,多线程读写问题就解决了,可惜只能在API 11 以上使用。

所以只能判断sdk版本,如果3.0以上,就打开这个属性

 

  1. public DbHelper(Context context , boolean enableWAL) {  
  2.         this(context, DEFAULT_DB_NAME, null, DEFAULT_VERSION);  
  3.         if( enableWAL && Build.VERSION.SDK_INT >= 11){  
  4.             getWritableDatabase().enableWriteAheadLogging();  
  5.         }  
  6. }  


关于SQLiteDatabase的这个属性,参考api文档,也可以看看SQLiteSession.java里对多线程数据库读写的描述。

 

SQLiteSession.java

 

结论

想要多线程并发读写,3.0以下就不要想了,3.0以上,直接设置enableWriteAheadLogging()就ok。

如果还是达不到要求,就使用多个db文件吧。

 

另:

单位有一个三星 note2手机,上面所有的例子跑起来都啥问题也没有。。。。很好很强大。

 

最后,附上我的测试程序。

https://github.com/zebulon988/SqliteTest.git

 

独家之言,如有问题请回复我,谢谢!

分享到:
评论

相关推荐

    Android例子源码解决多线程读写sqlite数据库锁定问题

    如果多线程同时读写(这里的指不同的线程用使用的是不同的Helper实例),后面的就会遇到android.database.sqlite.SQLiteException: database is locked这样的异常。对于这样的问题,解决的办法就是keep single ...

    Android开发--多线程下载加断点续传

    1.多线程下载: 首先通过下载总线程数来划分文件的下载区域:利用int range = fileSize / threadCount;得到每一段下载量;每一段的位置是i * range到(i + 1) * rang - 1,注意最后一段的位置是到filesize - 1; ...

    C#解决SQlite并发异常问题的方法(使用读写锁)

    本文实例讲述了C#解决SQlite并发异常问题的方法。分享给大家供大家参考,...作者利用读写锁(ReaderWriterLock),达到了多线程安全访问的目标。 using System; using System.Collections.Generic; using System.Text;

    Android SQLite3多线程操作问题研究总结

    最近做项目时在多线程读写数据库时抛出了异常,这自然是我对SQlite3有理解不到位的地方,所以事后仔细探究了一番。 1.关于getWriteableDataBase()和getReadableDatabase()的真正作用 getWriteableDataBase()其实是...

    Android开发之多线程处理、Handler详解

    但是在操作一些耗时操作时,比如I/O读写的大文件读写,数据库操作以及网络下载需要很长时间,为了不阻塞用户界面,出现ANR  Android开发过程中为什么要多线程  我们创建的Service、Activity以及Broadcast均是一个...

    sqlite数据库锁定问题.zip

    如果多线程同时读写(这里的指不同的线程用使用的是不同的Helper实例),后面的就会遇到android.database.sqlite.SQLiteException: database is locked这样的异常。对于这样的问题,解决的办法就是keep single ...

    Android实现断点多线程下载

    断点多线程下载的几个关键点:①:得到要下载的文件大小后,均分给几个线程。②:使用RandomAccessFile类进行读写,可以指定开始写入的位置。③:数据库保存下载信息,下一次继续下载的时候从数据库取出数据,然后从...

    老罗android视频开发源码和ppt经典

    十五、多线程编程 15.1 AsyncTask异步任务介绍一 15.2 AsyTask异步任务介绍二 15.3 Handler和Message(一) 15.4 Handler和Message(二) 15.5 Handler和Message(三) 15.6 Handler和Looper 15.7 Handler综合练习(图文...

    黎活明android教程的全程PPT

    2&gt; 网络--通过HTTP协议实现多线程断点续传下载 3&gt; 为应用添加新的Activity与参数传递 4&gt; 意图 第六天 1&gt; Activity的生命周期 2&gt; 广播接收者(实现短信监听) 3&gt; 服务与语音刻录(实现电话监听)、使用AIDL实现进程...

    疯狂Android讲义源码

     13.1.4 加入多线程 483  13.2 使用URL访问网络资源 488  13.2.1 使用URL读取网络资源 489  13.2.2 使用URLConnection  提交请求 490  13.3 使用HTTP访问网络 496  13.3.1 使用HttpURLConnection 496  ...

    传智播客Android视频教程-课程源码.rar

    2&gt; 网络--通过HTTP协议实现多线程断点续传下载 3&gt; 为应用添加新的Activity与参数传递 4&gt; 意图 第六天 1&gt; Activity的生命周期 2&gt; 广播接收者(实现短信监听) 3&gt; 服务与语音刻录(实现电话监听)、使用AIDL实现进程通信...

    Android天气预报的实现

    一个实现最近三天天气预报功能Demo,通过提交城市名称查询数据库找到相应的城市代号,从而远程访问天气预报接口,返回格式为JSon...涉及到数据库应用、文件读写、Android网络编程、多线程编程等技术。可供初学者参考。

    黑马程序员 安卓学院 万元哥项目经理 分享220个代码实例

    |--内容提供者之联系人读写与批量操作 |--内容提供者之获取通话记录 |--内容提供者的定义 |--写入联系人信息 |--利用FinalHttp实现多线程断点续传 |--加密之MD5 |--动画Animation详解 |--动画之view左右抖动 |--动画...

    sqlite数据库锁定问题

    sqlite实质上是将数据写入一个文件,通常情况下,在应用的包名下面都能...如果多线程同时读写(这里的指不同的线程用使用的是不同的Helper实例),后面的就会遇到android.database.sqlite.SQLiteException: database

    疯狂Android讲义.part2

    13.1.4 加入多线程 483 13.2 使用URL访问网络资源 489 13.2.1 使用URL读取网络资源 489 13.2.2 使用URLConnection提交 请求 491 13.3 使用HTTP访问网络 496 13.3.1 使用HttpURLConnection 496 13.3.2 使用Apache ...

    疯狂Android讲义.part1

    13.1.4 加入多线程 483 13.2 使用URL访问网络资源 489 13.2.1 使用URL读取网络资源 489 13.2.2 使用URLConnection提交 请求 491 13.3 使用HTTP访问网络 496 13.3.1 使用HttpURLConnection 496 13.3.2 使用Apache ...

    java源码uml-DBExecutor:android数据库框架,sqlitedatabase

    1.使用了读写锁,支持多线程操作数据。 2.支持事务 3.支持ORM 4.缓存Sql,缓存表结构 这个类库主要用于android 数据库操作。 始终围绕着一个类对应一个表的概念。 只要创建一个实体类,就不用当心它怎么存储在数据库...

    【最新版】wechat_devtools_1.02.2004020.dmg【亲测可用】最好的微信开发者工具

    A 新增 云控制台自定义数据库读写权限 A 新增 PC 微信小程序调试支持 A 新增 wx.getSystemInfo 返回 deviceOrientation 信息 A 新增 page meta 支持 详情 A 新增 支持小程序拓展包 详情 A 新增 清除订阅消息...

    java开源包8

    JCarder 是一个用来查找多线程应用程序中一些潜在的死锁,通过对 Java 字节码的动态分析来完成死锁分析。 Java的Flash解析、生成器 jActionScript jActionScript 是一个使用了 JavaSWF2 的 Flash 解析器和生成器。...

Global site tag (gtag.js) - Google Analytics