Android下数据库线程安全问题
2016-02-17 11:02
856 查看
一.概述
在实际应用中,当同时有多个线程一起访问数据库时,可能会发生一些异常情况,我们先来看看会发生什么异常:假设我们已经定义好了 SQLiteOpenHelper
[code]public class DatabaseHelper extends SQLiteOpenHelper { ... }
现在我们使用不同的线程对数据库进行操作
[code]// Thread 1 Context context = getApplicationContext(); DatabaseHelper helper = new DatabaseHelper(context); SQLiteDatabase database = helper.getWritableDatabase(); database.insert(…); database.close(); // Thread 2 Context context = getApplicationContext(); DatabaseHelper helper = new DatabaseHelper(context); SQLiteDatabase database = helper.getWritableDatabase(); database.insert(…); database.close();
这时你会得到下面的异常信息,并且有一条数据没有插入进去
[code]android.database.sqlite.SQLiteDatabaseLockedException: database is locked
这是因为每次当你创建新的SQLiteOpenHelper的时候,就相当于打开了一个新的数据库连接,如果你从不同的连接同时对数据库进行操作,就会失败。
下面我们新建一个单例类DataBaseManager,并且返回一个单一的SQLiteOpenHelper
[code]public class DatabaseManager { private static DatabaseManager instance; private static SQLiteOpenHelper mDatabaseHelper; public static synchronized void initializeInstance(SQLiteOpenHelper helper) { if (instance == null) { instance = new DatabaseManager(); mDatabaseHelper = helper; } } public static synchronized DatabaseManager getInstance() { if (instance == null) { throw new IllegalStateException(DatabaseManager.class.getSimpleName() + " is not initialized, call initialize(..) method first."); } return instance; } public synchronized SQLiteDatabase getDatabase() { return mDatabaseHelper.getWritableDatabase(); } }
然后我们把访问数据库的代码改成下面的样子
[code]// In your application class DatabaseManager.initializeInstance(new DatabaseHelper()); // Thread 1 DatabaseManager manager = DatabaseManager.getInstance(); SQLiteDatabase database = manager.getDatabase() database.insert(…); database.close(); // Thread 2 DatabaseManager manager = DatabaseManager.getInstance(); SQLiteDatabase database = manager.getDatabase() database.insert(…); database.close();
这时又会引发另外一个异常信息
[code]java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
因为我们只使用一个数据库连接,对2个线程来说,getDataBase方法返回的是同样的SQLiteDatabase 对象,这时后会发生什么情况?线程1可能已经关闭了数据库连接,但是线程2仍然在使用,这就导致了非法状态异常。
我们需要确保没有人在使用数据库,然后才去关闭
下面看看最终的实现代码:
[code]public class DatabaseManager { private AtomicInteger mOpenCounter = new AtomicInteger(); private static DatabaseManager instance; private static SQLiteOpenHelper mDatabaseHelper; private SQLiteDatabase mDatabase; public static synchronized void initializeInstance(SQLiteOpenHelper helper) { if (instance == null) { instance = new DatabaseManager(); mDatabaseHelper = helper; } } public static synchronized DatabaseManager getInstance() { if (instance == null) { throw new IllegalStateException(DatabaseManager.class.getSimpleName() + " is not initialized, call initializeInstance(..) method first."); } return instance; } public synchronized SQLiteDatabase openDatabase() { //incrementAndGet()每次调用会在当前值的基础上加1并返回 if(mOpenCounter.incrementAndGet() == 1) { // Opening new database mDatabase = mDatabaseHelper.getWritableDatabase(); } return mDatabase; } public synchronized void closeDatabase() { //decrementAndGet()每次调用会在当前值的基础上减1并返回 if(mOpenCounter.decrementAndGet() == 0) { // Closing database mDatabase.close(); } } }
然后这样去使用
[code]SQLiteDatabase database = DatabaseManager.getInstance().openDatabase(); database.insert(...); // database.close(); Don't close it directly! DatabaseManager.getInstance().closeDatabase(); // correct way
每次当我们需要数据库对象的时候就去调用DatabaseManager的openDatabase方法,在这个方法里面,我们有一个计数器,这个计数器用来指示数据库被打开了多少次,如果计数器的值为1,说明我们需要创建一个database对象,否则,说明数据库已经创建过了。
同样在closeDatabase方法里面,每次我们调用这个方法,计数器的值就会减1,当计数器的值为0时,我们就需要关闭数据库了。
二.总结
下面总结一下多线程条件下读写数据库的异常情况:1.多线程写,使用一个SQLiteOpenHelper。也就保证了多线程使用一个SQLiteDatabase。不会引发异常。
2.多线程写,使用多个SQLiteOpenHelper,插入时可能引发异常,导致插入错误
[code]E/Database(1471): android.database.sqlite.SQLiteException: error code 5: database is locked08-01
3.android 框架,多线程写数据库的本地方法里没有同步锁保护,并发写会抛出异常。
所以,多线程写必须使用同一个SQLiteOpenHelper对象。
4.多线程读,读之间没有同步锁,也得每个线程使用各自的SQLiteOpenHelper对象,经测试,没有问题
4.多线程读写,多线程写之前已经知道结果了,同一时间只能有一个写。
多线程读可以并发,所以,使用下面的策略:
一个线程写,多个线程同时读,每个线程都用各自SQLiteOpenHelper。此时会发生异常
[code]E/SQLiteDatabase(18263): Error inserting descreption=InsertThread#01375493606407 E/SQLiteDatabase(18263): android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
插入异常,说明在有线程读的时候写数据库,会抛出异常
那么如何实现一个线程写,多个线程读呢?
其实SQLiteDataBase 在API 11 多了一个 属性 ENABLE_WRITE_AHEAD_LOGGING
使用enableWriteAheadLogging打开这个属性,
disableWriteAheadLogging关闭这个属性
这个属性是什么意思呢?
参考api文档,这个属性关闭时,不允许读,写同时进行,通过 锁 来保证。
简单的说通过调用enableWriteAheadLogging()和disableWriteAheadLogging()可以控制该数据是否被运行多线程读写,如果允许,它将允许一个写线程与多个读线程同时在一个SQLiteDatabase上起作用。实现原理是写操作其实是在一个单独的log文件,读操作读的是原数据文件,是写操作开始之前的内容,从而互不影响。当写操作结束后读操作将察觉到新数据库的状态。当然这样做的弊端是将消耗更多的内存空间。
相关文章推荐
- Android的service的生命周期
- Android之RecyclerView简单使用(三)
- Android Volley框架的使用(4)
- android蓝牙开发入门到精通5-----实现会话
- Android 常见问题链接
- Android画一个随意拖动的圆形
- AndroidManifest合并原理
- android 超简单的下载功能,进度条 异步下载
- Android的四种设计模式
- android蓝牙开发入门到精通4------通信标准
- android 透明度设置
- ListView item 之间加间距(Margin)
- Android模拟器快捷键
- android是根据什么去选择drawable文件夹下的三种图片(png,jpg,gif)?图片导入
- android蓝牙开发入门到精通3---服务端客户端通信
- Android属性动画实战教程中篇
- Android多点触控技术,实现对图片的放大缩小平移,惯性滑动等功能
- android布局之相对布局RelativeLayout(总结)
- 关于AndroidManifest
- 新东西014--APK方法数目统计工具的使用(dex-method-counts.jar)