原文: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. publiclonginsertWithOnConflict(Stringtable,StringnullColumnHack,
  3. ContentValuesinitialValues,intconflictAlgorithm){
  4. if(!isOpen()){
  5. thrownewIllegalStateException("databasenotopen");
  6. }
  7. ....省略
  8. lock();
  9. SQLiteStatementstatement=null;
  10. try{
  11. statement=compileStatement(sql.toString());
  12. //Bindthevalues
  13. if(entrySet!=null){
  14. intsize=entrySet.size();
  15. Iterator<Map.Entry<String,Object>>entriesIter=entrySet.iterator();
  16. for(inti=0;i<size;i++){
  17. Map.Entry<String,Object>entry=entriesIter.next();
  18. DatabaseUtils.bindObjectToProgram(statement,i+1,entry.getValue());
  19. }
  20. }
  21. //Runtheprogramandthencleanup
  22. statement.execute();
  23. longinsertedRowId=lastInsertRow();
  24. if(insertedRowId==-1){
  25. Log.e(TAG,"Errorinserting"+initialValues+"using"+sql);
  26. }else{
  27. if(Config.LOGD&&Log.isLoggable(TAG,Log.VERBOSE)){
  28. Log.v(TAG,"Insertingrow"+insertedRowId+"from"
  29. +initialValues+"using"+sql);
  30. }
  31. }
  32. returninsertedRowId;
  33. }catch(SQLiteDatabaseCorruptExceptione){
  34. onCorruption();
  35. throwe;
  36. }finally{
  37. if(statement!=null){
  38. statement.close();
  39. }
  40. unlock();
  41. }
  42. }



  1. //SQLiteDatabase.java
  2. privatefinalReentrantLockmLock=newReentrantLock(true);
  3. /*package*/voidlock(){
  4. if(!mLockingEnabled)return;
  5. mLock.lock();
  6. if(SQLiteDebug.DEBUG_LOCK_TIME_TRACKING){
  7. if(mLock.getHoldCount()==1){
  8. //Useelapsedreal-timesincetheCPUmaysleepwhenwaitingforIO
  9. mLockAcquiredWallTime=SystemClock.elapsedRealtime();
  10. mLockAcquiredThreadTime=Debug.threadCpuTimeNanos();
  11. }
  12. }
  13. }

通过源码可以知道,在执行插入时,会请求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()。可是。。。

仔细看,发现

android 多线程数据库读写分析与优化_第1张图片

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

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

android 多线程数据库读写分析与优化_第2张图片

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

  1. /**
  2. *Readsrowsintoabuffer.Thismethodacquiresthedatabaselock.
  3. *
  4. *@paramwindowThewindowtofillinto
  5. *@returnnumberoftotalrowsinthequery
  6. */
  7. /*package*/intfillWindow(CursorWindowwindow,
  8. intmaxRead,intlastPos){
  9. longtimeStart=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. //ifthestartposisnotequalto0,thenmostlikelywindowis
  17. //toosmallforthedataset,loadingbyanotherthread
  18. //isnotsafeinthissituation.thenativecodewillignoremaxRead
  19. intnumRows=native_fill_window(window,window.getStartPosition(),mOffsetIndex,
  20. maxRead,lastPos);
  21. //Logging
  22. if(SQLiteDebug.DEBUG_SQL_STATEMENTS){
  23. Log.d(TAG,"fillWindow():"+mSql);
  24. }
  25. mDatabase.logTimeStat(mSql,timeStart);
  26. returnnumRows;
  27. }catch(IllegalStateExceptione){
  28. //simplyignoreit
  29. return0;
  30. }catch(SQLiteDatabaseCorruptExceptione){
  31. mDatabase.onCorruption();
  32. throwe;
  33. }finally{
  34. window.releaseReference();
  35. }
  36. }finally{
  37. releaseReference();
  38. mDatabase.unlock();
  39. }
  40. }

所以想要多线程读,读之间没有同步锁,也得每个线程使用各自的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. publicsynchronizedSQLiteDatabasegetReadableDatabase(){
  3. if(mDatabase!=null&&mDatabase.isOpen()){
  4. <spanstyle="color:#FF0000;">returnmDatabase;</span>//Thedatabaseisalreadyopenforbusiness
  5. }
  6. if(mIsInitializing){
  7. thrownewIllegalStateException("getReadableDatabasecalledrecursively");
  8. }
  9. try{
  10. returngetWritableDatabase();
  11. }catch(SQLiteExceptione){
  12. if(mName==null)throwe;//Can'topenatempdatabaseread-only!
  13. Log.e(TAG,"Couldn'topen"+mName+"forwriting(willtryread-only):",e);
  14. }
  15. SQLiteDatabasedb=null;
  16. try{
  17. mIsInitializing=true;
  18. Stringpath=mContext.getDatabasePath(mName).getPath();
  19. db=SQLiteDatabase.openDatabase(path,mFactory,SQLiteDatabase.OPEN_READONLY);
  20. if(db.getVersion()!=mNewVersion){
  21. thrownewSQLiteException("Can'tupgraderead-onlydatabasefromversion"+
  22. db.getVersion()+"to"+mNewVersion+":"+path);
  23. }
  24. onOpen(db);
  25. Log.w(TAG,"Opened"+mName+"inread-onlymode");
  26. mDatabase=db;
  27. returnmDatabase;
  28. }finally{
  29. mIsInitializing=false;
  30. if(db!=null&&db!=mDatabase)db.close();
  31. }
  32. }

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

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

  1. //DbHelper.java
  2. //DbHelperextendsSQLiteOpenHelper
  3. publicSQLiteDatabasegetOnlyReadDatabase(){
  4. try{
  5. getWritableDatabase();//保证数据库版本最新
  6. }catch(SQLiteExceptione){
  7. Log.e(TAG,"Couldn'topen"+mName+"forwriting(willtryread-only):",e);
  8. }
  9. SQLiteDatabasedb=null;
  10. try{
  11. Stringpath=mContext.getDatabasePath(mName).getPath();
  12. db=SQLiteDatabase.openDatabase(path,mFactory,SQLiteDatabase.OPEN_READONLY);
  13. if(db.getVersion()!=mNewVersion){
  14. thrownewSQLiteException("Can'tupgraderead-onlydatabasefromversion"+
  15. db.getVersion()+"to"+mNewVersion+":"+path);
  16. }
  17. onOpen(db);
  18. readOnlyDbs.add(db);
  19. returndb;
  20. }finally{
  21. }
  22. }

使用策略:一个线程写,多个线程同时读,只用一个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. publicDbHelper(Contextcontext,booleanenableWAL){
  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

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

更多相关文章

  1. Android 开启新线程
  2. Android多线程分析之三:Handler,Looper的实现
  3. Android 数据库Sqlite的使用(3)
  4. Android的UI主线程和子线程
  5. Android之怎么操作文件(读写以及保存到sdcard)
  6. 我的android 第12天 - 嵌入式关系型SQLite数据库存储数据
  7. 1.活用Android线程间通信的Message机制
  8. android 3d游戏研究(二)(边学边写,多谢高手指正,鞠躬) :数据库

随机推荐

  1. android 服务前台运行startForeground
  2. android的Menu使用(1)------选项菜单
  3. android调用系统拍照程序和从图库选取图
  4. android性能测试工具Emmagee介绍
  5. android中Intent的介绍和常见用法总结
  6. android中访问时的localhost问题
  7. 学习笔记(1):Android(安卓)WebRTC 实现1V1
  8. Android ContactsContact
  9. Java android ios 通用可逆des加密算法
  10. Android中Selector的用法(改变ListView和