高效解决「SQLite」数据库并发访问安全问题,只这一篇就够了

学Android

Concurrent database access


对于 Android Dev 而言,有关 SQLite 的操做再常常不过了,相比你必定经历过控制台一片爆红的状况,这不由让咱们疑问:SQLite 究竟是线程安全的吗?java

OK 废话很少说,咱们 ⬇️android

直接开始


首先,假设你已经实现了一个 SQLiteHelper 类,以下所示:

public class DatabaseHelper extends SQLiteOpenHelper { ... }
复制代码

如今你想要在两个子线程中,分别地向 SQLite 里写入一些数据:git

// 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();
复制代码

对吧?看上去很 OK 没啥毛病。github

那么这时,咱们点一下 run ,gio~ 你将会在你的 logcat 里收到以下礼物「报错」:sql

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5) 复制代码

究竟是怎么回事呢?

咱们分析一下报错终于发现:这是因为你每次建立 SQLiteHelper 时,都对数据库进行了一个连接操做。这时,若是你尝试着,同时从实际不一样的连接中,对数据库进行写入操做,失败就是必然的了。数据库

总结一下 若是咱们想再不一样的线程中,对数据库进行包括读写操做在内的任何使用,咱们就必须得确保,咱们使用的是同一个的链接编程

好,那如今问题就明了了。如今让咱们建立一个单例模式类:DatabaseManager 用来建立和返回惟一的,单例 DatabaseManager 对象。安全

ps 有些同窗问我什么是单例模式,我专门跑去写了这篇博客来解释下,单例模式-全局可用的 context 对象,这一篇就够了app

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

}
复制代码

如今,咱们在回来修改下以前的代码,结果以下所示:框架

// 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();
复制代码

逻辑比以前更清晰,代码冗余也少了。如今咱们在跑下代码,这时咱们会收到,另外一个 cache

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
复制代码

不要慌,咱们仔细分析下报错,咱们发现:单例模式的使用保证了咱们,在线程1、二「Thread 一、Thread 2 中」只会得到到惟一的 SQLiteHelper 对象,但这时问题就来了,当咱们运行完线程一「Thread 1」时,咱们的 database.close(); 已经替咱们关闭了对数据库的链接,但与此同时咱们的线程二「Thread 2」依然保持这对 SQLiteHelper 的引用。正是这个缘由,咱们收到了IllegalStateException的报错。

因此,这时咱们就须要保证,当没有人使用 SQLiteHelper 时,再将其断开链接。

保证 SQLIiteHelper 在无人使用时才断开链接

关于这个问题的解决 stackoveflow 上不少人建议咱们:永远不要断开 SQLiteHelper 的链接,可是这样以来你会在 logcat 上获得以下输出:

Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed
复制代码

因此,我很是不建议你用这个方法。为了解决这个问题,咱们引入计数器的概念

标准样例

经过以下方法,你将经过一个计数器来完美解决 打开/关闭 数据库链接的问题:

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() {
        if(mOpenCounter.incrementAndGet() == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }

    public synchronized void closeDatabase() {
        if(mOpenCounter.decrementAndGet() == 0) {
            // Closing database
            mDatabase.close();

        }
    }
}
复制代码

咱们在线程中能够这样使用它:

SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way
复制代码

每当你须要使用数据库时,你只要调用 DatabaseManager 中的 openDatabase() 方法。在这个方法中,咱们有一个,用来记录数据库被“打开”了几回的 mOpenCounter 对象。当它等于 1 时,这意味着你须要去建立新的数据库链接来使用数据库,不然的话,就说明数据库已经在使用中了。

一样的状况也发生在 closeDatabase() 方法中,当你每次调用该方法时,咱们的 mOpenCounter 对象就会减一。当它减到 0 时,咱们就去关闭这个数据库的链接。

完美,最后:

  1. 如今你就能为所欲为的使用你的数据库,并且你能够相信 -- 它是线程安全的了!
  2. 固然不少同窗对数据库的使用,还有着不少的疑惑,我后期将会针对数据库的使用,做出一系列总结,有兴趣能够继续关注 _yuanhao 的编程世界

相关文章


每一个人都要学的图片压缩,有效解决 Android 程序 OOM

Android 让你的 Room 搭上 RxJava 的顺风车 从重复的代码中解脱出来

ViewModel 和 ViewModelProvider.Factory:ViewModel 的建立者

单例模式-全局可用的 context 对象,这一篇就够了

缩放手势 ScaleGestureDetector 源码解析,这一篇就够了

Android 属性动画框架 ObjectAnimator、ValueAnimator ,这一篇就够了

看完这篇再不会 View 的动画框架,我跪搓衣板

Android 自定义时钟控件 时针、分针、秒针的绘制这一篇就够了

android 自定义控件之-绘制钟表盘

Android 进阶自定义 ViewGroup 自定义布局

欢迎关注_yuanhao的掘金!


按期分享Android开发湿货,追求文章幽默与深度的完美统一。

关于源码 Demo 连接:为了写 Demo 花了好几天时间,但愿你们点歌 star~ 谢谢!

请点赞!由于你的鼓励是我写做的最大动力!

学Android
相关文章
相关标签/搜索