在多线程应用程序Android中访问数据库的最佳方法?

时间:2016-02-12 08:44:21

标签: java android multithreading sqlite

注意:请勿将此问题标记为重复。我已经经历了几个类似的问题,但无法找到满意的答案。

我一直在研究使用Sqlite数据库的应用程序。我们遵循单例模式,确保我们在整个应用程序中只能创建一个辅助类实例。

public class CustomSqliteHelper extends SQLiteOpenHelper {

  public static CustomSqliteHelper getInstance(Context context) {
    if (instance == null) {
      synchronized (CustomSqliteHelper.class) {
        if (instance == null) {
          instance = new CustomSqliteHelper(context);
        }
      }
    }
    return instance;
  }
}

但有时候应用程序会崩溃SQLiteDatabaseLockedException。我理解当多个线程/进程尝试一次写入数据库时​​会出现此异常。即使一个线程/进程在写操作仍在进行时尝试读取数据库,也会抛出此异常。

所以我一直在阅读很多关于这个以及防止这种情况发生的可能方法。很多帖子建议使用ContentProvider而不是直接扩展SqliteOpenHelper类并对数据库对象执行操作。在阅读其中一篇帖子时,post提到在使用Content Provider时,您无需手动处理多线程环境。

  

虽然ContentProvider缺乏线程安全性,但您经常会发现在防止潜在的竞争条件方面您无需采取进一步行动。规范示例是您的ContentProvider由SQLiteDatabase支持的时间;当两个线程同时尝试写入数据库时​​,SQLiteDatabase将自行锁定,确保一个等待直到另一个完成。每个线程都将获得对数据源的互斥访问,确保满足线程安全。

以上引用似乎令人困惑,因为首先它提到ContentProvider不支持线程安全。但他得出的结论是,应用程序开发人员并不需要做任何事情来实现并发。

另外,如果我选择使用SqliteOpenHelper,那么防止这些崩溃的最佳方法是什么?我一直在考虑为每个数据库操作使用锁。

 public class CustomSqliteHelper extends SQLiteOpenHelper {

 private String lock = "lock";

 public void insert(){
    synchronized(lock){
       // Do the insert operation here.
    }
 }


 public void update(){
    synchronized(lock){
       // Do the update operation here.
    }
 }
 }

但我的一位团队成员建议我不要这样做,因为Java锁很贵。

在浏览了Github上最受欢迎的projects之后,我发现开发人员建议将每个数据库操作包装在一个事务中。

public void insert(ContentValues values) {
    // Create and/or open the database for writing
    SQLiteDatabase db = getWritableDatabase();

    // It's a good idea to wrap our insert in a transaction. This helps with performance and ensures
    // consistency of the database.
    db.beginTransaction();
    try {
        // The user might already exist in the database (i.e. the same user created multiple posts).

        db.insertOrThrow(TABLE_POSTS, null, values);
        db.setTransactionSuccessful();
    } catch (Exception e) {
        Log.d(TAG, "Error while trying to add post to database");
    } finally {
        db.endTransaction();
    }
}

我真的不确定这是否可以阻止Lock异常?这似乎更像是一个以绩效为导向的步骤。

所以在阅读完所有博客和教程之后,我仍然感到困惑。我的主要问题是

  1. 使用ContentProviders或扩展是更好的选择 SqliteOpenHelper,因为我的应用程序没有与之共享数据 其他应用程序。
  2. 将Java锁定放在所有操作上 最好的方法还是有比这更好的其他方法?
  3. 更新:根据@Mustanar的回答,似乎SQLiteDatabase负责锁定机制。这意味着如果您正在执行写操作,则数据库将被锁定。但同时,如果某个其他线程尝试执行写操作,那么第二个操作是否会等待锁定被释放或者是否会抛出android.sqlite.database.SQLiteDatabaseLockedException异常?

    更新2 :对此问题表示赏心悦目,因为答案似乎仍然不清楚。我只使用Helper类的一个实例。但仍然遇到这个错误。

    P.S:感谢您提出这么长的问题。

9 个答案:

答案 0 :(得分:3)

使用Singleton模式来实例化SQLiteOpenHelper,因此在整个应用程序中应该存在一个单例实例。 这将确保不会发生泄漏,并且会使您的生活变得更加轻松,因为它可以避免在编码时忘记关闭数据库。它还将确保在整个应用程序中安全访问数据库。

此外,您不必实施自己的锁定机制。 SQLiteDatabase保持锁定机制。因此,在特定时间只会有一笔交易,所有其他交易将使用Queue方法在Sigleton.getInstance()中。确保数据库的单个访问点。

此外,在此方法中, 您不必关闭数据库连接,因为SQLiteDatabase将在事务完成后释放所有引用。 所以{{每当您想要执行CRUD操作并删除锁定mecansim时,都应该使用1}}。

更多信息,请浏览博客http://www.androiddesignpatterns.com/2012/05/correctly-managing-your-sqlite-database.html并查看评论。

希望这有帮助。

答案 1 :(得分:1)

查询1(单身人士):

您可以更改下面的双重锁定代码来创建一个真正的Singleton对象吗?

public class CustomSqliteHelper extends SQLiteOpenHelper {
      private CustomSqliteHelper instance;
      public static CustomSqliteHelper getInstance(Context context) {
        if (instance == null) {
          synchronized (CustomSqliteHelper.class) {
            if (instance == null) {
              instance = new CustomSqliteHelper(context);
            }
          }
        }
        return instance;
      }
    }

请查看此article以了解双重锁定的问题。

看看相关的SE问题:

What is an efficient way to implement a singleton pattern in Java?

我更喜欢创建Singleton

 private static final CustomSqliteHelper instance = new CustomSqliteHelper ();

其他方法:

enum CustomSqliteHelper {
    INSTANCE;
}

如果你仍然需要懒惰的单身人士,还有一种方法。

在此SE问题中查看@ Nathan Hughes答案:

Synchronized and locks in singleton factory

查询2 :(锁定)

使用java锁定提高应用程序的一致性并不是一个坏主意。一致性胜过费用。

SQLiteDatabaseLockedException页面

  

如果数据库引擎无法获取数据库锁,则需要执行其工作。如果语句是[COMMIT]或发生在显式事务之外,则可以重试该语句。如果该语句不是[COMMIT]并且发生在显式事务中,那么您应该在继续之前回滚该事务。

我希望您应该遵循上述建议以及您在问题(Github)项目中发布的代码段。我喜欢这个想法,上面的建议与Github示例代码一致。

答案 2 :(得分:1)

我肯定会建议您将数据库包装在ContentProvider

它提供了开箱即用的好功能,并将许多潜在的棘手任务转移到框架中。您不需要处理单例数据库帮助程序,因为您可以保证您的应用程序有一个ContentResolver实例,可以将命令转发给目标提供程序。

您可以使用整个Loader框架和好的帮助程序类(如AsyncQueryHandler)来处理一些最常见的数据库访问警告。

我建议不要同步对CRUD方法的访问。正如文档和之前的答案中所提到的,这应该由数据库本身来处理。更不用说在需要大量数据库使用的应用程序中潜在的性能损失。

旁注:作为第一步,如果您尝试找出导致这些崩溃的原因,这可能是有用的 - 它们是零星的吗?在进行故障排除时,更具体的示例可能非常有用。

答案 3 :(得分:0)

您可以使用内容提供商。

答案 4 :(得分:0)

我认为在大多数情况下使用读写锁就足够了。见this answer。该解决方案不使用ContentProvider。相反,它使用ReentrantReadWriteLock包中的java.util.concurrent.locks类。

答案 5 :(得分:0)

  
      
  1. 鉴于我的应用程序不与其他应用程序共享数据,使用ContentProviders或扩展SqliteOpenHelper是更好的选择。
  2.   

我说如果您不共享任何数据,扩展SqliteOpenHelper是更好的选择。这主要是因为它比ContentProvider更加开发友好的API :-)还有official documentation中描述的其他内容:

  

决定是否需要内容提供商。如果要提供以下一项或多项功能,则需要构建内容提供程序:

     
      
  • 您希望向其他应用程序提供复杂的数据或文件。
  •   
  • 您希望允许用户将应用中的复杂数据复制到其他应用中。
  •   
  • 您希望使用搜索框架提供自定义搜索建议。
  •   

所以在你的情况下 - 它是多余的

对于单身初始化 - 有时候你不需要它进行懒惰的初始化:

public class CustomSqliteHelper {
    private static final CustomSqliteHelper instance = new CustomSqliteHelper(YourApplication.getContext());

    public static CustomSqliteHelper getInstance() {
        return instance;
    }

    // all the super staff below :)
}

但是您需要在应用程序类中公开应用程序上下文:

public class YourApplication extends Application {
    private static final YourApplication instance;

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
    }

    public static Context getContext() {
        return instance.getApplicationContext();
    }
}

加上Manifest中的配置。这样可以避免在创建过程中同步。

如果你真的需要这个单例进行延迟初始化 - 我会说 双重检查锁定 (DCL)是非常糟糕的做法。对于安全的延迟初始化,您应该使用以下模式:

public class CustomSqliteHelper {
    private static class InstanceHolder {
        public static CustomSqliteHelper instance = new CustomSqliteHelper(YourApplication.getContext());
    }

    public CustomSqliteHelper getInstance() {
        return InstanceHolder.instance;
    }

    // all the super staff below :)
}
  
      
  1. 将Java锁定放在所有操作上是最好的方法,还是有其他方法比这更好?   这提供了安全的延迟初始化而不会锁定任何内容。
  2.   

无需进行任何锁定(直到您的DAO无状态)。查看SQLiteDatabase

的文档

答案 6 :(得分:0)

除了确保SQLiteOpenHelper是单例之外,如果您希望多个线程访问数据库,还需要启用预写日志记录:

https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#enableWriteAheadLogging()

这样做可以解决我的问题。

答案 7 :(得分:-2)

正如评论中所讨论的,如果您使用一个数据库实例,则不会有阻止锁定。 所以我怀疑DeadLock是因为你正在使用交易。

一种DeadLock示例是:

  1. 在一次交易中:

    更新表格 - >更新B表 - >提交

  2. 在其他交易中:

    更新B表 - >更新表格 - >提交

  3. 所以在中间时间,1等待2和2同时等待1,所以死锁发生。

    如果它真的是死锁,你可以修复查询,以免发生这种情况 或者您可以使用像Actor这样的排队查询(或异步查询)。

    例如,commons-dbutils库支持AsyncQueryRunner,它提供ExecutorService并返回Future。 (来自:Is asynchronous jdbc call possible?

答案 8 :(得分:-2)

要防止锁定错误,请在每个连接中设置大约20秒左右的busy timeout。然后,只有当您的应用程序挂起运行的事务的时间超过该事务时,您才会收到异常。

您还可以使用所有线程中的单个SQLiteDatabase对象,但是您必须在所有事务中手动锁定它。