Threading Android e blocco del database

Utilizziamo AsyncTasks per accedere a tabelle e cursori di database.

Sfortunatamente assistiamo a occasionali eccezioni relative al blocco del database.

 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.(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) 

Qualcuno ha un esempio generale di codice che scrive in un database da un thread diverso da quello che legge e come possiamo garantire la sicurezza del thread.

Un suggerimento che ho avuto è quello di utilizzare un ContentProvider , in quanto questo avrebbe gestito l’accesso del database da più thread. Vado a vedere questo, ma questo è il metodo raccomandato per gestire un problema del genere? Sembra piuttosto pesante considerando che stiamo parlando di fronte o dietro.

    Ho risolto questa stessa eccezione solo assicurandomi che tutte le mie aperture di database si siano chiuse e (cosa più importante) per assicurare questo, rendendo l’ambito di ogni istanza di database locale SOLO al metodo che ne ha bisogno. ContentProvider è una class valida e sicura da utilizzare quando si accede a un db da più thread, ma assicurarsi anche di utilizzare buone pratiche di db:

    • Mantieni istanze DB locali (nessun membro della class SQLiteDatabase!)
    • chiama close() sul db nello stesso modo in cui è aperto
    • chiama close() sui cursori che ottieni dal db
    • ascoltare LogCat per eventuali reclami che SQLiteDatabse potrebbe avere

    ContentProvider fine abbiamo usato ContentProvider . Questo sembrava chiarire i problemi.

    Prima di un codice, riprendiamo alcuni degli approcci:

    • Semafori : di gran lunga la migliore soluzione presentata. Va nel cuore del problema: condivisione delle risorse! Tratterà il blocco dell’accesso al database, evitando conflitti (il database is locked ).

    • Sincronizzazione Java : una specie di implementazione di semaforo, ma meno sofisticata. Usando synchronized non risolverai facilmente alcuni casi che riguardano transazioni.

    • ContentProvider : implementare ContentProvider risolve il problema solo per alcuni casi (o spazzare il problema sotto il tappeto). Dovrai ancora affrontare gli stessi problemi. La differenza è che il pattern ContentProvider ti guiderà a non commettere errori comuni quando accedi al database Sqlite. I documenti di ContentProvider dicono: “Non è necessario un provider per utilizzare un database SQLite se l’utilizzo è interamente all’interno della propria applicazione.”

    • Quasi obbligatorio : mantenere le istanze di db locali, chiamare close() sul db nello stesso metodo in cui è aperto usando le istruzioni finally , close() sui cursori usando le istruzioni finally , ecc sono quasi obbligatorie per evitare problemi nell’uso di Sqlite.

    Facciamo un esempio della soluzione semaforo presentata da Moss , che ho preso da CL. e ha contribuito a coprire le transazioni.

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

    Tenere conto del fatto che i database SQLite sono basati su file e non sono pensati per essere accessibili in modalità multiprocesso. La migliore procedura sul missaggio di SQLite con multielaborazione sta usando semafori (aquire (), release ()) in ciascun accesso al database.

    Se crei un wrapper Db che acquisisca / rilasci un semaforo globale, l’accesso al tuo DB sarà sicuro. In effetti ciò significa che è ansible ottenere un collegamento di avvio perché si accoda l’accesso al DB. Quindi, in aggiunta, è ansible racchiudere l’accesso utilizzando i semafori solo se si tratta di un’operazione che altera il database, quindi mentre si modifica il db, nessuno sarà in grado di accedervi e attendere fino al completamento del processo di scrittura.

    Non è stato ansible condividere la connessione Db con thread multipli per eseguire operazioni di lettura e scrittura nel database simultaneamente. Dovremo creare un singolo object di DB utilizzando il concetto di sincronizzazione e eseguiremo un task alla volta. Utilizzeremo il modello singleton per creare il DB object e sarà condiviso all’interno di più thread. In un momento eseguirà una singola attività. quindi avvieremo altre attività o qualsiasi operazione su DB. Il fornitore di contenuti non è la soluzione del problema di blocco di 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(); } } 

    e scrivi questo metodo nella tua class helper YourSQLiteDataABse che estende SQLiteOpenHelper Class

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

    Devi chiamare getWritableDatabase() da una funzione piuttosto che il costruttore della class db helper. Se l’object della class helper db viene creato con SQLiteDatabase.openOrCreateDatabase(DB_PATH, null); o simile e quindi getWritableDatabase() viene chiamato da una funzione, proverà a effettuare una chiamata sincrona al DB causando un’eccezione di blocco DB.

    Stai parlando di un’azione di un singolo utente che, all’interno del tuo programma, fa sì che vengano eseguiti più thread, più di uno dei quali potrebbe accedere al database in modalità di aggiornamento?

    È un cattivo design, punto. Non è ansible sapere in quale ordine i thread saranno pianificati dal sistema operativo (/ VM), e quindi non è ansible sapere in quale ordine accederanno gli accessi al database, e questo è molto probabile che implichino che non è ansible per te sapere che gli accessi al database avverranno sempre nell’ordine che ti aspetti.

    Tutti gli accessi al database generati da / provenienti da qualche azione dell’utente dovrebbero essere eseguiti in un singolo thread.