Android线程和数据库锁定

30
我们正在使用AsyncTasks来访问数据库表和游标。
不幸的是,我们偶尔会看到有关数据库被锁定的异常情况。
E/SQLiteOpenHelper(15963): Couldn't open iviewnews.db for writing (will try read-only):
E/SQLiteOpenHelper(15963): android.database.sqlite.SQLiteException: database is locked
E/SQLiteOpenHelper(15963):  at     android.database.sqlite.SQLiteDatabase.native_setLocale(Native Method)
E/SQLiteOpenHelper(15963):  at     android.database.sqlite.SQLiteDatabase.setLocale(SQLiteDatabase.java:1637)
E/SQLiteOpenHelper(15963):  at     android.database.sqlite.SQLiteDatabase.<init>(SQLiteDatabase.java:1587)
E/SQLiteOpenHelper(15963):  at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:638)
E/SQLiteOpenHelper(15963):  at android.database.sqlite.SQLiteDatabase.openOrCreateDatabase(SQLiteDatabase.java:659)
E/SQLiteOpenHelper(15963):  at android.database.sqlite.SQLiteDatabase.openOrCreateDatabase(SQLiteDatabase.java:652)
E/SQLiteOpenHelper(15963):  at android.app.ApplicationContext.openOrCreateDatabase(ApplicationContext.java:482)
E/SQLiteOpenHelper(15963):  at android.content.ContextWrapper.openOrCreateDatabase(ContextWrapper.java:193)
E/SQLiteOpenHelper(15963):  at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:98)
E/SQLiteOpenHelper(15963):  at android.database.sqlite.SQLiteOpenHelper.getReadableDatabase(SQLiteOpenHelper.java:158)
E/SQLiteOpenHelper(15963):  at com.iview.android.widget.IViewNewsTopStoryWidget.initData(IViewNewsTopStoryWidget.java:73)
E/SQLiteOpenHelper(15963):  at com.iview.android.widget.IViewNewsTopStoryWidget.updateNewsWidgets(IViewNewsTopStoryWidget.java:121)
E/SQLiteOpenHelper(15963):  at com.iview.android.async.GetNewsTask.doInBackground(GetNewsTask.java:338)
E/SQLiteOpenHelper(15963):  at com.iview.android.async.GetNewsTask.doInBackground(GetNewsTask.java:1)
E/SQLiteOpenHelper(15963):  at android.os.AsyncTask$2.call(AsyncTask.java:185)
E/SQLiteOpenHelper(15963):  at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:256)
E/SQLiteOpenHelper(15963):  at java.util.concurrent.FutureTask.run(FutureTask.java:122)
E/SQLiteOpenHelper(15963):  at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:648)
E/SQLiteOpenHelper(15963):  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:673)
E/SQLiteOpenHelper(15963):  at java.lang.Thread.run(Thread.java:1060)

有没有一个通用的例子,展示如何从一个不同的线程中写入数据库,并且我们如何确保线程安全。

我收到的一个建议是使用ContentProvider,因为它可以处理来自多个线程的数据库访问。我会研究一下这个方法,但这是否是处理这种问题的推荐方法?考虑到我们只是在前面或后面进行操作,这似乎有些过重。

7个回答

28

我们最终使用了一个ContentProvider。 这似乎解决了问题。


54
ContentProvider 可以解决这个问题的原因是它通常使用一个单一的 SQLiteOpenHelper,也就是说只有一个数据库连接,底层的 SQLiteDatabase 负责处理锁定。你不需要使用 ContentProvider,只需确保不要使用2个不同的数据库连接对数据库进行写操作。这篇文章解释了 Android 中锁定的工作原理。http://kagii.squarespace.com/journal/2010/9/10/android-sqlite-locking.html - Jan Berkel
9
文章更新链接:http://kagii.com/post/6828016869/android-sqlite-locking - orip
1
哦,是啊,ContentProvider 就是有点恶心的(ish)。如果你不是在明确地在应用程序之间共享数据,那就是很多额外的代码和工作。 - Kevin Galligan
1
为什么将一个权宜之计伪装成解决方案会有这么多的赞? - Bondax
1
http://developer.android.com/guide/topics/providers/content-provider-creating.html表示:“如果SQLite数据库的使用仅限于您自己的应用程序内部,则无需提供者。” - Italo Borssatto
显示剩余4条评论

16

我通过确保所有数据库的打开操作都有关闭,尤其是要确保每个数据库实例的作用域仅限于需要它的方法,就解决了这个相同的异常。ContentProvider是一个很好的安全类,在从多个线程访问数据库时使用,但也要确保使用良好的数据库实践:

  • 将数据库实例保持本地化(不要使用SQLiteDatabase类成员!)
  • 在打开数据库的同一方法中调用close()
  • 关闭从数据库获取的游标
  • 关注LogCat是否有关于SQLiteDatabse的投诉

8
经过一些阅读,我认为每个请求打开自己的数据库实例并不是推荐的方式。据我所知,如果两个线程同时使用两个不同的数据库实例写入数据库,只有一个会被写入,另一个将被忽略,但不会出现错误(仅有日志信息)。数据库锁是乐观的,你可以打开尽可能多的连接来进行阅读,但对于写入操作应该只使用一个数据库实例。如果我错了请纠正我。 - Hiep

12

在一些代码之前,让我们回顾一下几种方法:

  • 信号量(Semaphores):迄今为止提出的最佳解决方案。它深入核心问题:资源共享!它将处理数据库访问的锁定,避免冲突(database is locked)。

  • Java 同步:一种信号量实现,但不太复杂。使用 synchronized 无法轻松解决涉及事务的某些情况。

  • ContentProvider:实现 ContentProvider 只能解决某些情况下的问题(或者把问题搁置一边)。您仍将面临相同的问题。不同之处在于,ContentProvider 模式将指导您在访问 SQLite 数据库时不犯一些常见错误。ContentProvider 文档 中说:“如果使用完全在您自己应用程序中,那么您不需要提供程序来使用 SQLite 数据库。”

  • 几乎是强制性的:将数据库实例保持本地,使用 finally 语句在打开它的同一方法中调用 close() 来关闭 db,对游标使用 finally 语句调用 close() 等等,这些措施 几乎是 强制性的,以避免使用 Sqlite 时出现问题。

让我们展示一个信号量解决方案的示例,该示例来自于 Moss,我从 CL 中获取并改进了它以涵盖事务。

class DataAccess {
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();

    public Data readSomething(int id) {
        Cursor c = null;
        r.lock();
        try {
            c = getReadableDatabase().query(...);
            return c.getString(0);
        } finally {
            if (c != null) c.close();
            r.unlock();
        }
    }

    public void changeSomething(int id, int value) {
        w.lock();
        try {
            getWritableDatabase().update(...);
        } finally {
            w.unlock();
        }
    }

    private void beginTransactionWithSemaphores() {
        getWritableDatabase().beginTransactionWithListener(new SQLiteTransactionListener() {
            @Override
            public void onBegin() {
                w.lock();
            }

            @Override
            public void onRollback() {
                w.unlock();
            }

            @Override
            public void onCommit() {
                w.unlock();
            }
        });
    }
}

9
请注意,SQLite数据库是基于文件的,不适合多进程访问。在使用多进程混合SQLite时,最好的方法是在每个与数据库相关的访问中使用信号量(acquire(),release())。
如果您创建了一个Db包装器来获取/释放全局信号量,则您的DB访问将是线程安全的。确实,这意味着您可能会遇到瓶颈,因为您正在排队访问DB。因此,除非是更改数据库的操作,否则您只能使用信号量包装访问,因此在更改数据库时,没有人能够访问它并等待写入过程完成。

1
我们无法将数据库连接与多个线程共享,以便同时执行读写操作。我们需要使用同步概念创建单个DB对象,并一次执行一个任务。我们将使用单例模式来创建DB对象,并在多个线程之间共享。每次只执行单个任务,然后开始其他任务或任何关于DB的操作。内容提供程序不是解决DB锁定问题的方法。
import java.util.concurrent.atomic.AtomicInteger;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class DatabaseManager {

private AtomicInteger mOpenCounter = new AtomicInteger();

private static DatabaseManager instance;
private static SQLiteOpenHelper mDatabaseHelper;
private SQLiteDatabase mDatabase;
//private static String DB_PATH = "";
//  private static String DB_NAME = "xyz.db";// Database name
private static String dbPathh;

public static synchronized void initializeInstance(SQLiteOpenHelper helper,
        String dbPath) {
    if (instance == null) {
        instance = new DatabaseManager();
        mDatabaseHelper = helper;
        dbPathh=dbPath;
    }
  }

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(String thread) {

    if(mOpenCounter.get() == 0) {
        // Opening new database
        // mDatabase = mDatabaseHelper.getWritableDatabase();
        MyLog.e("Path Of DataBase", dbPathh);
        //  mDatabase=mDatabaseHelper.getWritableDatabase();
        mOpenCounter.incrementAndGet();
        mDatabase=SQLiteDatabase.openDatabase(dbPathh, null,   
 SQLiteDatabase.  CREATE_IF_NECESSARY|SQLiteDatabase.OPEN_READWRITE);   
        MyLog.e("Open Data Base", " New Connection created" +thread);
    }
    else{
        MyLog.e("Open Data Base", " Old Connection given " +thread);
    }
    //  Toast.makeText(NNacres.getConfig(), "open conn: present connection = 
   "   +mOpenCounter.get(), Toast.LENGTH_LONG).show();
    return mDatabase;
   }

    public synchronized void closeDatabase() {
    MyLog.e("Close db connection", ""+mOpenCounter.get());

    if(mOpenCounter.get() == 1) {
        // Closing database

        mDatabase.close();
        mOpenCounter.decrementAndGet();

        Log.e("DB CLOSED", "DONE");
    }
    //Toast.makeText(NNacres.getConfig(), "close conn: after close =   
 " +mOpenCounter.get(), Toast.LENGTH_LONG).show();
    }

    }

并将此方法编写在扩展SQLiteOpenHelper类的YourSQLiteDataABse帮助程序类中

     public SQLiteDatabase getWritableDatabase() {
DatabaseManager.initializeInstance(this,"data/data/your packgae name/databases/xyz");
    return DatabaseManager.getInstance().openDatabase(getClass().getSimpleName());

}



public static String getMyDbPath(String DB_NAME, Context context) {

    String myDbPath = context.getDatabasePath(DB_NAME).getPath();
    MyLog.e("DB Path: "+myDbPath);
    return myDbPath;
}

-3
你必须从函数中调用getWritableDatabase()而不是数据库帮助程序类的构造函数。如果使用SQLiteDatabase.openOrCreateDatabase(DB_PATH, null);或类似方法创建了数据库帮助程序类对象,然后从函数中调用getWritableDatabase(),它将尝试对数据库进行同步调用,导致数据库锁定异常。

1
如果数据库文件不存在,SQLiteOpenHelper的onCreate()方法才会被调用。 - Hal
同意。问题不在于调用onCreate(),而是在于如果DB对象已经存在,则同步调用DB。 - user868114

-6

你是在说一个单独的用户操作会导致你的程序运行多个线程,其中有多个线程可能以更新模式访问数据库吗?

这是不好的设计,毫无疑问。你不可能知道你的操作系统(/VM)会以什么顺序调度这些线程,因此你也不可能知道数据库访问会按照你期望的顺序发生。这很可能意味着你无法保证数据库访问总是按照你期望的顺序发生。

所有由某个用户操作生成或产生的数据库访问都应该在一个单独的线程中完成。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接