转:http://bbs.51cto.com/thread-1113117-1.html
Android sqlite数据库连接池连接异常分析 1. 在 android开发过程中,突然碰到了这个错误, 数据库连接分配不到, 日志如下: W/SQLiteConnectionPool( 3681): Theconnection pool for database '/data/user/0/com.android.providers.contacts/databases/contacts2.db'has been unable to grant a connection to thread 371 (ContactsProviderWorker)with flags 0x1 for 30.000002 seconds. W/SQLiteConnectionPool( 3681): Connections:0 active, 1 idle, 0 available. 2. 网上别人的总结 搜索了上面错误日志,找到别人的解决方法:
他说是在一个 Transaction
里面又执行一个 execSql(sql)
导致。但是经过自己的分析发现,完全不是这么回事。可见,网络上很多东西都是不可全信的。
为什么这么说呢,我们首先来分析下整个数据库连接的获取过程,一般进行下列操作时都会申请获取一个数据库连接。
a. Query, insert, delete
操作
b. beginTranscation
操作
从网上截了个图,让大家看的更清楚点
Ok...
那么我们就以 insert
操作为例,说明数据库连接获取的流程。
1.
01SqliteDatabase的insert方法
02public long insert(String table, StringnullColumnHack, ContentValues values) {
06return insertWithOnConflict(table, nullColumnHack, values,CONFLICT_NONE);
08} catch(SQLException e) {
10Log.e(TAG, "Error inserting "+ values, e);
2.
Ok... 继续往下面分析insertWithOnConflict方法
01public long insertWithOnConflict(Stringtable, String nullColumnHack,
03ContentValues initialValues, int conflictAlgorithm) {
09StringBuilder sql = newStringBuilder();
13sql.append(CONFLICT_VALUES[conflictAlgorithm]);
23SQLiteStatement statement = newSQLiteStatement(this, sql.toString(),bindArgs);
27returnstatement.executeInsert();
这个方法首先构造insert的sql语句,然后调用statement的executeInsert()方法
那继续跟下去
3.
frameworks\base\core\java\android\database\sqlite\SQLiteStatement.java
01public long executeInsert() {
05return getSession().executeForLastInsertedRowId(
07getSql(), getBindArgs(), getConnectionFlags(),null);
这个方法比较重要,分成两步来分析:
首先分析getSession()方法,
然后再去看看executeForLastInsertedRowId方法。
3a. getSession分析
01frameworks\base\core\java\android\database\sqlite\SQLiteProgram.java
02protected final SQLiteSession getSession(){
04return mDatabase.getThreadSession();
063b. 调用的是SqliteDatabase.getThreadSession()方法,继续分析
07frameworks\base\core\java\android\database\sqlite\SQLiteDatabase.java
08SQLiteSession getThreadSession() {
10return mThreadSession.get();
调用的是mThreadSession.get(),看看mThreadSession是什么东西
01private final ThreadLocal<SQLiteSession>mThreadSession = new ThreadLocal<SQLiteSession>() {
03protected SQLiteSession initialValue() {
05return createSession();
11SQLiteSession createSession() {
13final SQLiteConnectionPool pool;
17pool = mConnectionPoolLocked;
21return new SQLiteSession(pool);
26public SQLiteSession(SQLiteConnectionPoolconnectionPool) {
28if (connectionPool == null) {
30throw new IllegalArgumentException("connectionPool must not benull");
34mConnectionPool = connectionPool;
嗯,非常明白,这是一个ThreadLocal类型的变量,ThreadLocal,Java提供的一个用于多线程之间隔离数据的东西,避免多线程访问同一个变量导致冲突。
也就是ThreadLocal是为每个线程保存一份变量,这样就不会引起冲突了。这个ThreadLocal如果有不理解的,可以百度下ThreadLocal的使用。
Ok..理解完ThreadLocal之后,继续看createSession方法
它是使用mConnectionPoolLocked这么个SQLiteConnectionPool变量来实例化一个SQLiteSession对象。而这个mConnectionPoolLocked对象是SqliteDatabase打开的时候实例化的,也就是说一个database只有一个这样连接。
那么,这里我们可以理清楚一个关系
a.
一个线程会对应一个SqliteSession对象,这个是用ThreadLocal对象来维持的。
b.
一个数据库对应唯一一个sqliteDabase对象,以及唯一一个SQLiteConnectionPool对象,然后各个线程之间共享这个SQLiteConnectionPool对象。
Ok..理清楚这几个关系之后,返回去分析executeForLastInsertedRowId方法
4.
frameworks\base\core\java\android\database\sqlite\SQLiteSession.java
01public long executeForLastInsertedRowId(Stringsql, Object[] bindArgs,int connectionFlags,
03CancellationSignal cancellationSignal) {
07acquireConnection(sql, connectionFlags, cancellationSignal);
11return mConnection.executeForLastInsertedRowId(sql, bindArgs,
这个方法主要分析两个操作
acquireConnection releaseConnection 因为中间的mConnection.executeForLastInsertedRowId语句主要是通过Jni往底层调用,插入数据库数据,不是今天我们讨论的话题。
Ok.. 那这两个操作,一个个来分析
4a. acquireConnection frameworks\base\core\java\android\database\sqlite\SQLiteSession.java
01private void acquireConnection(String sql,intconnectionFlags,
03CancellationSignal cancellationSignal) {
06if (mConnection == null) {
08assert mConnectionUseCount == 0;
10mConnection = mConnectionPool.acquireConnection(sql, connectionFlags,
14mConnectionFlags = connectionFlags;
18mConnectionUseCount += 1;
Ok..首先会去判断当前线程的Session里面的mConnection是否为null,如果不为null,就只是简单的把连接个数mConnectionUseCount加1 如果为null,也就是当前线程的Session没有连接数据库,那么就要去申请一个连接。
所以这里的逻辑特别重要,就是一个对于一个线程而已,它只会去获取一次数据库连接。即使你调用再多的beginTranscation以及query,第一次调用的时候会去获取连接,以后就是让mConnectionUseCount 加1;
当然,你使用beginTranscation需要手动调用endTranscation,不然不会去释放连接。
调用其他的,比如query,insert之类的,不需要手动释放。系统会帮你去调用释放连接。
Ok.. 接下来分析mConnectionPool.acquireConnection的流程. 4b. 调用mConnectionPool.acquireConnection,顾名思义,它是向连接池申请一个连接;根据之前的分析,这个连接池是唯一的,是多个线程之间共享的。
frameworks\base\core\java\android\database\sqlite\SQLiteConnectionPool.java
001public SQLiteConnectionacquireConnection(String sql, intconnectionFlags,
003CancellationSignal cancellationSignal) {
005return waitForConnection(sql, connectionFlags, cancellationSignal);
012private SQLiteConnectionwaitForConnection(String sql, intconnectionFlags,
014CancellationSignal cancellationSignal) {
016final boolean wantPrimaryConnection =
018(connectionFlags &CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) !=0;
022final ConnectionWaiter waiter;
026synchronized (mLock) {
028throwIfClosedLocked();
034if (cancellationSignal != null) {
036cancellationSignal.throwIfCanceled();
044SQLiteConnection connection = null;
046if (!wantPrimaryConnection) {
049connection =tryAcquireNonPrimaryConnectionLocked(
051sql, connectionFlags);
055if (connection == null) {
058connection =tryAcquirePrimaryConnectionLocked(connectionFlags);
062if (connection != null) {
073Enqueue a waiter in priority order.
075final int priority = getPriority(connectionFlags);
077final long startTime = SystemClock.uptimeMillis();
079waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime,
081priority,wantPrimaryConnection, sql, connectionFlags);
083ConnectionWaiter predecessor = null;
085ConnectionWaiter successor = mConnectionWaiterQueue;
087while (successor != null) {
090if (priority >successor.mPriority) {
092waiter.mNext = successor;
098predecessor = successor;
100successor = successor.mNext;
104if (predecessor != null) {
106predecessor.mNext = waiter;
110mConnectionWaiterQueue =waiter;
116nonce = waiter.mNonce;
134long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
137long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis;
143if(mConnectionLeaked.compareAndSet(true,false)) {
146synchronized (mLock) {
148wakeConnectionWaitersLocked();
157LockSupport.parkNanos(this,busyTimeoutMillis * 1000000L);
170synchronized (mLock) {
172throwIfClosedLocked();
174final SQLiteConnectionconnection = waiter.mAssignedConnection;
176final RuntimeException ex =waiter.mException;
178if (connection != null ||ex != null) {
180recycleConnectionWaiterLocked(waiter);
182if (connection != null){
195final long now =SystemClock.uptimeMillis();
197if (now <nextBusyTimeoutTime) {
199busyTimeoutMillis = now- nextBusyTimeoutTime;
203logConnectionPoolBusyLocked(now- waiter.mStartTime, connectionFlags);
206busyTimeoutMillis =CONNECTION_POOL_BUSY_MILLIS;
208nextBusyTimeoutTime =now + busyTimeoutMillis;
这个方法比较多,我在里面做了一点注释,大概包括下面几个步骤。同时要明确一个概念,主连接和非主连接。
其实他们没有本质的区别,主连接是一定有的,在初始化的时候就实例化完毕;而非主连接是一个连接集合,也就是说非主连接可以有很多个。不过一般有个最大大小,我们可以配置的。
Ok...明白这个概念之后,讲讲步骤
a.
如果没有指定要获取主连接的话,首先尝试获取非主连接
01private SQLiteConnectiontryAcquireNonPrimaryConnectionLocked(
03String sql, intconnectionFlags) {
07SQLiteConnection connection;
09final int availableCount = mAvailableNonPrimaryConnections.size();
11if (availableCount > 1 && sql != null) {
18for (int i = 0; i <availableCount; i++) {
20connection =mAvailableNonPrimaryConnections.get(i);
22if(connection.isPreparedStatementInCache(sql)) {
24mAvailableNonPrimaryConnections.remove(i);
27finishAcquireConnectionLocked(connection,connectionFlags);
37if (availableCount > 0) {
42connection =mAvailableNonPrimaryConnections.remove(availableCount -1);
44finishAcquireConnectionLocked(connection, connectionFlags);
54int openConnections = mAcquiredConnections.size();
56if (mAvailablePrimaryConnection != null) {
65if (openConnections >= mMaxConnectionPoolSize) {
71connection = openConnectionLocked(mConfiguration,
75finishAcquireConnectionLocked(connection, connectionFlags);
获取非主连接的时候,首先会判断已有的连接中有没有相同的sql,如果有的话,就直接返回这个连接。
然后如果第一步没有成功的话,比如传入的sql是null,那么就会尝试获取队列里面的最后一个连接,然后返回。
如果第二步也没有成功,那么它会尝试去扩充非主连接集合
但是,它会去判断是否超过了最大的连接数,是已经到达最大的连接数,那么就返回null
1if (openConnections >=mMaxConnectionPoolSize) {
如果还没有到达最大连接数,那么就把连接放入非主连接集合,然后返回这个扩容的连接。
b.
如果没有获取到非主连接,或者指定要获取主连接,那么就要去尝试获取主连接。
01private SQLiteConnectiontryAcquirePrimaryConnectionLocked(intconnectionFlags) {
05SQLiteConnection connection = mAvailablePrimaryConnection;
07if (connection != null) {
09mAvailablePrimaryConnection = null;
11finishAcquireConnectionLocked(connection, connectionFlags);
21for (SQLiteConnection acquiredConnection :mAcquiredConnections.keySet()) {
23if (acquiredConnection.isPrimaryConnection()) {
36Either thisis the firsttime we asked
40connection = openConnectionLocked(mConfiguration,
44finishAcquireConnectionLocked(connection, connectionFlags);
由于主连接只有一个,所以它用一个变量来表示 --- mAvailablePrimaryConnection 如果主连接为null就返回主连接,并把主连接设置成null,也就是主连接被占用。
1if (connection != null) {
3mAvailablePrimaryConnection = null;
5finishAcquireConnectionLocked(connection, connectionFlags);
如果主连接为null,那么它会去查看是否已经有人获取了主连接,如果是,那么返回null;这样做就是确保主连接是存在的。
如果经过上面一步确认,还没有返回,那么说明主连接没有创建;这个一般是不可能的,因为主连接时数据库初始化的时候创建的。这个一般是第一次访问,或者出现了程序异常。那么就新建一个主连接,然后返回。
c.
如果获取到连接,那么返回这个连接
1if (connection != null) {
d.
如果没有获取到连接,那么新建一个waiter对象,并进入等待队列。
e.
线程进入死循环,不断休眠(30s),然后重新获取连接。直到获取到连接后返回。否则,记录下当前线程等待的时间。不过这里要注意,这里打印的等待时间并不包括系统睡眠的时间。比如,下午1点进入等待,2点-4点手机睡眠,那么到5点的时候打印,只能算两个小时。
也就是上面我们看到的异常日志:
04-2314:25:48.522 W/SQLiteConnectionPool( 3681): The connection pool for database'/data/user/0/com.android.providers.contacts/databases/contacts2.db' has beenunable to grant a connection to thread 371 (ContactsProviderWorker) with flags0x1 for 30.000002 seconds. 04-23 14:25:48.522 W/SQLiteConnectionPool( 3681):Connections: 0 active, 1 idle, 0 available. Ok...至此,整个申请数据库连接过程分析基本完毕,下面分析释放的过程
5.释放连接
frameworks\base\core\java\android\database\sqlite\SQLiteSession.java
06private void releaseConnection() {
09assert mConnection != null;
11assert mConnectionUseCount > 0;
13if (--mConnectionUseCount == 0) {
18mConnectionPool.releaseConnection(mConnection);
首先会去判断当前使用数是否为大于0,还记得前面我们说过如果线程第一次申请连接,那么就去申请,然后使用数+1;但是如果线程已经拥有了连接,那么我们只是简单的把连接数+1。
所以,这里要判断这个连接数是否>0 然后把连接数减去1,看是否等于0;这是什么意思呢?就是说当前线程已经没有使用数据库的操作了。
如果减去1之后>0,那么说明当前线程还要使用这个连接操作数据库,还不能释放。
这里也可以看出,申请连接和释放连接一定要是一一对应的。申请了,一定要释放。当然,对于应用程序来说,只有beginTranscation需要手动去释放,也就是调用endTranscation,而且必须要调用(一般写在finally里面)。
Ok...一切都ok的话,正式去释放连接
mConnectionPool.releaseConnection(mConnection);// might throw 5a. 正式释放连接
frameworks\base\core\java\android\database\sqlite\SQLiteConnectionPool.java
01public voidreleaseConnection(SQLiteConnection connection) {
10closeConnectionAndLogExceptionsLocked(connection);
12} elseif (connection.isPrimaryConnection()) {
14if(recycleConnectionLocked(connection, status)) {
16assert mAvailablePrimaryConnection== null;
18mAvailablePrimaryConnection= connection;
22wakeConnectionWaitersLocked();
24} elseif (mAvailableNonPrimaryConnections.size() >=mMaxConnectionPoolSize -1) {
26closeConnectionAndLogExceptionsLocked(connection);
30if(recycleConnectionLocked(connection, status)) {
32mAvailableNonPrimaryConnections.add(connection);
37wakeConnectionWaitersLocked();
这里会去根据当前连接是否是主连接而选择释放方法,然后通知那些正在等待的线程(waiter)去获取连接。
6. 总结
1. 如文章开头所说,这个bug不是由于在beginTranscation里面执行一个execuSql(sql)所导致的,因为在同一个线程里面,即使申请两次数据库连接,那也只是让使用数+1而已。它只会去申请一个连接。除非beginTranscation和execuSql(sql) 不在一个线程,另外开线程去execuSql(sql). 2. 那么怎么会导致这个bug呢?
a. 时间过长的操作,某个操作持续很长时间,那么其他线程等待的时候就会打印这个信息。不过,一般不会等待太久。因为一般不会有这么长时间的数据库操作。
b. 忘记调用endTranscation,如前面所说,只有beginTranscation需要程序员手动去调用endTransction来释放连接,其他操作不需要。
那么,有个时候会疏忽忘记调用endTransction了。
或者endTranscation没有放在finally里面,导致出现异常而没有调用endTransction
c.
死锁,这个比较复杂,需要具体问题具体分析。
如果出现死锁,线程互相等待,这样也没有去释放连接;那么后面的线程自然也拿不到数据库连接了。
参考资料
http://blog.youkuaiyun.com/efeics/article/details/18970483