原文: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。
先看看相关的源码
- //SQLiteDatabase.java
- public long insertWithOnConflict(String table, String nullColumnHack,
- ContentValues initialValues, int conflictAlgorithm) {
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
- .... 省略
- lock();
- SQLiteStatement statement = null;
- try {
- statement = compileStatement(sql.toString());
- // Bind the values
- if (entrySet != null) {
- int size = entrySet.size();
- Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();
- for (int i = 0; i < size; i++) {
- Map.Entry<String, Object> entry = entriesIter.next();
- DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue());
- }
- }
- // Run the program and then cleanup
- statement.execute();
- long insertedRowId = lastInsertRow();
- if (insertedRowId == -1) {
- Log.e(TAG, "Error inserting " + initialValues + " using " + sql);
- } else {
- if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Inserting row " + insertedRowId + " from "
- + initialValues + " using " + sql);
- }
- }
- return insertedRowId;
- } catch (SQLiteDatabaseCorruptException e) {
- onCorruption();
- throw e;
- } finally {
- if (statement != null) {
- statement.close();
- }
- unlock();
- }
- }
- //SQLiteDatabase.java
- private final ReentrantLock mLock = new ReentrantLock(true);
- /* package */ void lock() {
- if (!mLockingEnabled) return;
- mLock.lock();
- if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
- if (mLock.getHoldCount() == 1) {
- // Use elapsed real-time since the CPU may sleep when waiting for IO
- mLockAcquiredWallTime = SystemClock.elapsedRealtime();
- mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
- }
- }
- }
通过源码可以知道,在执行插入时,会请求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()
- /**
- * Reads rows into a buffer. This method acquires the database lock.
- *
- * @param window The window to fill into
- * @return number of total rows in the query
- */
- /* package */ int fillWindow(CursorWindow window,
- int maxRead, int lastPos) {
- long timeStart = SystemClock.uptimeMillis();
- mDatabase.lock();
- mDatabase.logTimeStat(mSql, timeStart, SQLiteDatabase.GET_LOCK_LOG_PREFIX);
- try {
- acquireReference();
- try {
- window.acquireReference();
- // if the start pos is not equal to 0, then most likely window is
- // too small for the data set, loading by another thread
- // is not safe in this situation. the native code will ignore maxRead
- int numRows = native_fill_window(window, window.getStartPosition(), mOffsetIndex,
- maxRead, lastPos);
- // Logging
- if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
- Log.d(TAG, "fillWindow(): " + mSql);
- }
- mDatabase.logTimeStat(mSql, timeStart);
- return numRows;
- } catch (IllegalStateException e){
- // simply ignore it
- return 0;
- } catch (SQLiteDatabaseCorruptException e) {
- mDatabase.onCorruption();
- throw e;
- } finally {
- window.releaseReference();
- }
- } finally {
- releaseReference();
- mDatabase.unlock();
- }
- }
所以想要多线程读,读之间没有同步锁,也得每个线程使用各自的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 。
- // SQLiteOpenHelper.java
- public synchronized SQLiteDatabase getReadableDatabase() {
- if (mDatabase != null && mDatabase.isOpen()) {
- <span style="color:#FF0000;"> return mDatabase;</span> // The database is already open for business
- }
- if (mIsInitializing) {
- throw new IllegalStateException("getReadableDatabase called recursively");
- }
- try {
- return getWritableDatabase();
- } catch (SQLiteException e) {
- if (mName == null) throw e; // Can't open a temp database read-only!
- Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e);
- }
- SQLiteDatabase db = null;
- try {
- mIsInitializing = true;
- String path = mContext.getDatabasePath(mName).getPath();
- db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);
- if (db.getVersion() != mNewVersion) {
- throw new SQLiteException("Can't upgrade read-only database from version " +
- db.getVersion() + " to " + mNewVersion + ": " + path);
- }
- onOpen(db);
- Log.w(TAG, "Opened " + mName + " in read-only mode");
- mDatabase = db;
- return mDatabase;
- } finally {
- mIsInitializing = false;
- if (db != null && db != mDatabase) db.close();
- }
- }
因为它先看有没有已经创建的SQLiteDatabase,没有的话先尝试创建读写 SQLiteDatabase ,失败后才尝试创建只读SQLiteDatabase 。
所以写了个新方法,来获得只读SQLiteDatabase
- //DbHelper.java
- //DbHelper extends SQLiteOpenHelper
- public SQLiteDatabase getOnlyReadDatabase() {
- try{
- getWritableDatabase(); //保证数据库版本最新
- }catch(SQLiteException e){
- Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):",e);
- }
- SQLiteDatabase db = null;
- try {
- String path = mContext.getDatabasePath(mName).getPath();
- db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);
- if (db.getVersion() != mNewVersion) {
- throw new SQLiteException("Can't upgrade read-only database from version " +
- db.getVersion() + " to " + mNewVersion + ": " + path);
- }
- onOpen(db);
- readOnlyDbs.add(db);
- return db;
- } finally {
- }
- }
使用策略:一个线程写,多个线程同时读,只用一个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以上,就打开这个属性
- public DbHelper(Context context , boolean enableWAL) {
- this(context, DEFAULT_DB_NAME, null, DEFAULT_VERSION);
- if( enableWAL && Build.VERSION.SDK_INT >= 11){
- getWritableDatabase().enableWriteAheadLogging();
- }
- }
关于SQLiteDatabase的这个属性,参考api文档,也可以看看SQLiteSession.java里对多线程数据库读写的描述。
结论
想要多线程并发读写,3.0以下就不要想了,3.0以上,直接设置enableWriteAheadLogging()就ok。
如果还是达不到要求,就使用多个db文件吧。
另:
单位有一个三星 note2手机,上面所有的例子跑起来都啥问题也没有。。。。很好很强大。
最后,附上我的测试程序。
https://github.com/zebulon988/SqliteTest.git
相关推荐
如果多线程同时读写(这里的指不同的线程用使用的是不同的Helper实例),后面的就会遇到android.database.sqlite.SQLiteException: database is locked这样的异常。对于这样的问题,解决的办法就是keep single ...
6. **性能优化**:在多线程环境下,合理规划读写操作的顺序和数量,以及使用索引优化查询,能显著提升数据库操作的效率。例如,避免在查询条件中使用未索引的列,以减少全表扫描。 在"MulThreadSQLiteTest"这个项目...
在这个示例中,我们将探讨如何在Android中进行多线程数据库操作以及如何利用CursorAdapter实现异步加载数据库数据。 首先,我们来看多线程访问SQLite数据库。在Android中,主线程负责UI的更新和交互,而长时间运行...
本文将深入探讨如何在Android中使用多线程操作SQLite并解决数据库被锁定的问题。 首先,理解“database locked”的原因。SQLite采用独占式锁定策略,这意味着当一个事务正在进行时,其他试图访问同一数据库的事务会...
通过以上步骤,我们可以构建一个功能完善的Android多线程断点下载应用,结合ListView展示下载任务,提供良好的用户交互体验。在实际开发中,还需要考虑各种边缘情况,进行充分的测试,以确保系统的稳定性和可靠性。
总的来说,实现Android多线程下载涉及网络请求、线程管理、文件操作、数据同步等多个方面。开发者需要熟悉Android的并发编程模型,了解网络编程的基础知识,并掌握文件I/O的操作。通过这样的实践,不仅可以提高应用...
在Android开发中,多线程下载是一项常见的技术,特别是在处理大文件或网络资源时,为了提高下载速度和用户体验,开发者通常会采用多线程来分块下载数据。Android Studio作为官方推荐的Android集成开发环境,提供了...
这个压缩包文件“Android应用源码之Android多线程断点续传下载+在线播放音乐.rar”提供了实现这两个功能的源代码示例。下面我们将详细探讨其中涉及的关键知识点。 1. **Android多线程**: - **主线程**:在Android...
1. **Android多线程下载** 在Android中,由于主线程(UI线程)不能执行耗时操作,因此在进行大文件下载时,我们需要使用多线程技术。通常,我们会创建一个后台线程来处理网络请求和文件写入,以避免阻塞UI。使用`...
在Android开发中,网络多线程断点续传下载是一项重要的技术,特别是在处理大文件时,它可以提高下载效率,减少用户等待时间,并提供更友好的用户体验。本文将深入讲解这一主题,结合“Multi_thread_Download”这个...
在Android开发中,SQLite...总结,Android对SQLite数据库的操作涵盖了从创建数据库到执行SQL语句,再到事务处理和数据管理等多个方面。通过合理的编程实践,可以构建高效、稳定的数据库解决方案,满足应用的存储需求。
综上所述,要在Android应用中实现与MySQL数据库的连接,需要掌握网络编程、数据库连接、线程管理、数据安全和优化等多个方面的知识。在实际开发中,还需要根据项目需求选择合适的方法和工具,确保应用的稳定性和安全...
4. **多线程支持**:ObjectBox允许在多个线程中并发地读写数据,提高了应用的并发性能。 5. **内存映射**:ObjectBox使用内存映射技术,使得数据访问更加高效,同时也降低了内存开销。 6. **查询语言BoxQL**:虽然...
在Android应用开发中,实现“多线程断点续传下载+在线播放音乐”涉及到多个关键技术,主要包括网络编程、文件操作、多线程处理、内存管理以及多媒体播放等。以下是对这些知识点的详细阐述: 1. **多线程下载**: ...
通过以上分析,我们可以看出这个项目涵盖了Android开发的多个重要方面,包括网络编程、多线程、文件操作、UI设计、权限管理、错误处理等,对于提升Android开发者的技术能力非常有帮助。在实际学习和研究源码的过程中...
三、Android多线程下载实现 1. 分割文件:首先,根据文件大小确定每个线程需要下载的数据块。可以使用HTTP Range请求来指定服务器返回文件的特定部分。 2. 创建下载线程:为每个数据块创建一个下载线程,每个线程...
### Android多线程断点续传技术解析 #### 一、多线程断点续传的概念及重要性 在移动互联网应用中,特别是在资源下载方面,用户体验是非常关键的一环。对于大文件的下载,传统的单线程下载方式往往无法满足用户的...
总之,"android 多任务+多线程+断点下载"项目涵盖了Android开发中的多个核心知识点,包括多线程与服务的使用、网络请求、文件管理、UI优化和状态保存。这些技术对于构建高效、流畅的Android下载应用至关重要。
在Android开发中,多线程断点下载是一项重要的技术,特别是在处理大文件或者网络环境不稳定的情况下,能够提高下载效率并确保下载的连续性。本文将深入探讨这一主题,结合提供的"多线程断点下载"Demo,来详细介绍...
在Android应用开发中,多线程断点下载是一项重要的...这个"Android多线程断点下载源码demo"应该包含以上所有组件和逻辑,通过分析和学习这个源码,开发者可以更好地理解和实现多线程断点下载功能,提高应用的用户体验。