Android SQLite3多线程操作问题研究总结
最近做项目时在多线程读写数据库时抛出了异常,这自然是我对sqlite3有理解不到位的地方,所以事后仔细探究了一番。
1.关于getwriteabledatabase()和getreadabledatabase()的真正作用
getwriteabledatabase()其实是相当于getreadabledatabase()的一个子方法,getwriteabledatabase()是只能返回一个以读写方式打开的sqlitedatabase的引用,如果此时数据库不可写时就会抛出异常,比如数据库的磁盘空间满了的情况。而getreadabledatabase()一般默认是调用getwriteabledatabase()方法,如果数据库不可写时就会返回一个以只读方式打开的sqlitedatabase的引用,这就是二者最明显的区别。
关键源码如下:
public synchronized sqlitedatabase getwritabledatabase() { if (mdatabase != null) { if (!mdatabase.isopen()) { // darn! the user closed the database by calling mdatabase.close() mdatabase = null; } else if (!mdatabase.isreadonly()) { return mdatabase; // the database is already open for business } } ... ... public synchronized sqlitedatabase getreadabledatabase() { if (mdatabase != null) { if (!mdatabase.isopen()) { // darn! the user closed the database by calling mdatabase.close() mdatabase = null; } else { return mdatabase; // the database is already open for business } } ... ... try { return getwritabledatabase(); } ... ...
2.sqlitedatabase的同步锁
其实在只使用一个sqlitedatabase的引用时,sqlitedatabase对crud操作都会加上一个锁(因为是db文件,所以精确至数据库级),这就保证了在同一时间你只能进行一项操作,无论是不是在同一个线程中,这就导致了如果你在程序中对sqliteopenhelper使用了单例模式,那么你对数据库读写进行任何的优化操作都是"徒劳"。
3.多线程读数据库
仔细看源码你会发现,在数据库操作中只有add,delete,update会调用lock(),而query()是不会调用的,但是在加载数据时,调用了sqlitequery的fillwindow方法,而该方法依然会调用sqlitedatabase.lock(),所以要想真正的实现多线程读数据库,只能每个线程使用各自的sqliteopenhelper对象进行读操作,这样就可避开同步锁。关键源码如下:
/* 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 {... ...
4.多线程读写
实现多线程读写的关键是enablewriteaheadlogging属性,这个方法 api level 11添加的,也就是所3.0以上的版本就基本不可能实现真正的多线程读写了。简单的说通过调用enablewriteaheadlogging()和disablewriteaheadlogging()可以控制该数据是否被运行多线程读写,如果允许,它将允许一个写线程与多个读线程同时在一个sqlitedatabase上起作用。实现原理是写操作其实是在一个单独的log文件,读操作读的是原数据文件,是写操作开始之前的内容,从而互不影响。当写操作结束后读操作将察觉到新数据库的状态。当然这样做的弊端是将消耗更多的内存空间。
5.多线程写
这个就不用多想了,sqlite压根不支持,如果实在有需求可以使用多个数据库文件。
6.备注
(1)你有没有想sqlite最多支持多少个数据库连接,其实在官方api文档(enablewriteaheadlogging ()方法)中给出了最精确的答案:the maximum number of connections used to execute queries in parallel is dependent upon the device memory and possibly other properties.就是看你有多少内存,但是我感觉这话说的有点大,是不?哈哈。
(2)当你在多线程中只使用一个sqlitedatabase的引用时,需要格外注意你sqlitedatabase.close()调用的时机,因为你是使用的同一个引用,比如在一个线程中当一个add操作结束后立刻关闭了数据库连接,而另一个现场中正准备执行查询操作,但此时db已经被关闭了,然后就会报异常错误。此时一般有三种解决方案,①简单粗暴给所有的crud添加一个 synchronized关键字;②永远不关闭数据库连接,只在最后退出是关闭连接。其实每次执行getwriteabledatabase()或getreadabledatabase()方法时,如果有已经建立的数据库连接则直接返回(例外:如果旧的连接是以只读方式打开的,则会在新建连接成功的前提下,关闭旧连接),所以程序中将始终保持有且只有一个数据库连接(前提是单例),资源消耗的很少。③可以自己进行引用计数,简单示例代码如下:
//打开数据库方法 public synchronized sqlitedatabase opendatabase() { if (mopencounter.incrementandget() == 1) { // opening new database try { mdatabase = sinstance.getwritabledatabase(); } catch (exception e) { mdatabase = sinstance.getreadabledatabase(); } } return mdatabase; } //关闭数据库方法 public synchronized void closedatabase() { if (mopencounter.decrementandget() == 0) { // closing database mdatabase.close(); } }
(3)还有一些比较好的习惯和常识,例如关闭cursor,使用transaction,sqlite存储数据时其实不区分类型,以及sqlite支持大部分标准sql语句,增删改查语句都是通用的等等。
下一篇: 分享几个Android开发有用的程序代码