文档章节

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

亭子happy
 亭子happy
发布于 2014/01/06 15:31
字数 1861
阅读 182
收藏 2

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


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

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

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


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

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

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

先看看相关的源码

[java] view plaincopy

  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.     }  



[java] view plaincopy

  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()

[java] view plaincopy

  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 。

[java] view plaincopy

  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 


[java] view plaincopy

  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以上,就打开这个属性

[java] view plaincopy

  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


本文转载自:http://blog.csdn.net/lize1988/article/details/9700723

共有 人打赏支持
亭子happy
粉丝 119
博文 234
码字总数 46492
作品 0
海淀
程序员
私信 提问
加载中

评论(1)

ETmanwenhan
ETmanwenhan
总结得非常好,在神!
android求职

尹盼 基本信息 性别: 女 出生日期: 1989-10-30 目前所在地: 北京 大兴 电话: 15901440917 电子邮件: 953768578@qq.com QQ: 953768578 求职意向 工作性质: 全职 职位类别: 手机应用开...

蜡笔小溪2
2013/07/27
582
6
性能优化之Java(Android)代码优化

最新最准确内容建议直接访问原文:性能优化之Java(Android)代码优化 本文为Android性能优化的第三篇——Java(Android)代码优化。主要介绍Java代码中性能优化方式及网络优化,包括缓存、异步、...

Trinea
2013/08/26
2.5K
1
Android开发中高效的数据结构

android开发中,在java2ee或者android中常用的数据结构有Map,List,Set,但android作为移动平台,有些api(很多都是效率问题)显然不够理想,本着造更好轮子的精神,android团队编写了自己的a...

IamOkay
2014/12/13
0
0
Android 多线程系统概述及与Linux系统的关系

线程系统的分类 1.1 操作系统内核实现了线程模型(核心型线程) - Windows - 线程与进程的多对多模型 线程效率比较高 Window Thread结构如下图所示: 1.2 操作系统核外实现的线程(用户进程)...

长平狐
2012/09/03
220
0
Android性能优化:手把手教你如何让App更快、更稳、更省(含内存、布局优化等)

前言 在 开发中,性能优化策略十分重要 因为其决定了应用程序的开发质量:可用性、流畅性、稳定性等,是提高用户留存率的关键 本文全面讲解性能优化中的所有知识,献上一份 性能优化的详细攻...

Carson_Ho
2018/05/30
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Coding and Paper Letter(六十四)

资源整理。 1 Coding: 1.交互式瓦片编辑器。 tile playground 2.R语言包autokeras,autokeras的R接口。autokeras是一个开源的自动机器学习的软件。 autokeras 3.斯坦福网络分析平台,用于网络...

胖胖雕
34分钟前
0
0
最简单的cd命令是个大坑!

BASH Shell 是大多 Linux 发行版的默认 shell,BASH 有一些自己的内置命令,cd 就是其中的一个。 在centos6里面,系统中不存在 cd 的二进制文件。但是你仍然可以运行该命令,这是因为 cd 是 ...

gaolongquan
44分钟前
1
0
spring获取bean的几种方式

使用jdk:1.8、maven:3.3.3 spring获取Bean的方式 pom.xml文件内容: <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="......

Vincent-Duan
51分钟前
2
0
一段话系列-Linux中IO的同步、异步、阻塞、非阻塞

首先我们框定一下背景,我们探讨的是Linux系统下的IO模型。 同步和异步是针对内核操作数据而言的,同步是指内核串行顺序操作数据,异步是指内核并行(或并发)操作数据,然后通过回调的方式通...

EasyProgramming
55分钟前
4
0
好程序员web前端分享主流CSS image比较

好程序员web前端分享主流CSS image比较在还原设计图的时候,难免会碰到一些样式图片的引用。如何来对这些图片做优化呢?本文简单的梳理了一下目前几种比较常用的使用方式。   注: 1. 有更好...

好程序员IT
今天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部