转: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. 网上别人的总结
搜索了上面错误日志,找到别人的解决方法:


1.png
(62.72 KB)
2014-6-23 19:43
他说是在一个Transaction
里面又执行一个execSql(sql)
导致。但是经过自己的分析发现,完全不是这么回事。可见,网络上很多东西都是不可全信的。为什么这么说呢,我们首先来分析下整个数据库连接的获取过程,一般进行下列操作时都会申请获取一个数据库连接。a.
Query, insert, delete
操作b.
beginTranscation
操作从网上截了个图,让大家看的更清楚点

2.png
(91.23 KB)
2014-6-23 19:43
Ok...
那么我们就以insert
操作为例,说明数据库连接获取的流程。1.
01 |
SqliteDatabase的insert方法 |
02 |
public
long insert(String table, StringnullColumnHack, ContentValues values) {
|
06 |
return
insertWithOnConflict(table, nullColumnHack, values,CONFLICT_NONE);
|
08 |
} catch (SQLException e) {
|
10 |
Log.e(TAG, "Error inserting " + values, e);
|
2.
Ok... 继续往下面分析insertWithOnConflict方法
01 |
public
long insertWithOnConflict(Stringtable, String nullColumnHack,
|
03 |
ContentValues initialValues,
int conflictAlgorithm) {
|
09 |
StringBuilder sql = new StringBuilder();
|
13 |
sql.append(CONFLICT_VALUES[conflictAlgorithm]); |
23 |
SQLiteStatement statement = new SQLiteStatement( this , sql.toString(),bindArgs);
|
27 |
returnstatement.executeInsert(); |
这个方法首先构造insert的sql语句,然后调用statement的executeInsert()方法那继续跟下去3.
frameworks\base\core\java\android\database\sqlite\SQLiteStatement.java
01 |
public
long executeInsert() {
|
05 |
return
getSession().executeForLastInsertedRowId(
|
07 |
getSql(), getBindArgs(), getConnectionFlags(), null );
|
这个方法比较重要,分成两步来分析:首先分析getSession()方法,然后再去看看executeForLastInsertedRowId方法。3a. getSession分析
01 |
frameworks\base\core\java\android\database\sqlite\SQLiteProgram.java |
02 |
protected
final SQLiteSession getSession(){
|
04 |
return
mDatabase.getThreadSession();
|
06 |
3b. 调用的是SqliteDatabase.getThreadSession()方法,继续分析 |
07 |
frameworks\base\core\java\android\database\sqlite\SQLiteDatabase.java |
08 |
SQLiteSession getThreadSession() { |
10 |
return
mThreadSession.get(); // initialValue() throws if database closed
|
调用的是mThreadSession.get(),看看mThreadSession是什么东西
01 |
private
final ThreadLocal<SQLiteSession>mThreadSession =
new ThreadLocal<SQLiteSession>() {
|
03 |
protected
SQLiteSession initialValue() {
|
05 |
return
createSession();
|
11 |
SQLiteSession createSession() { |
13 |
final
SQLiteConnectionPool pool;
|
15 |
synchronized
(mLock) {
|
17 |
pool = mConnectionPoolLocked; |
21 |
return
new SQLiteSession(pool);
|
26 |
public
SQLiteSession(SQLiteConnectionPoolconnectionPool) {
|
28 |
if
(connectionPool == null ) {
|
30 |
throw
new IllegalArgumentException( "connectionPool must not benull" );
|
34 |
mConnectionPool = 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
01 |
public
long executeForLastInsertedRowId(Stringsql, Object[] bindArgs, int
connectionFlags,
|
03 |
CancellationSignal cancellationSignal) { |
07 |
acquireConnection(sql, connectionFlags, cancellationSignal); // mightthrow
|
11 |
return
mConnection.executeForLastInsertedRowId(sql, bindArgs,
|
13 |
cancellationSignal); //might throw
|
17 |
releaseConnection(); // might throw
|
这个方法主要分析两个操作acquireConnection
releaseConnection
因为中间的mConnection.executeForLastInsertedRowId语句主要是通过Jni往底层调用,插入数据库数据,不是今天我们讨论的话题。Ok.. 那这两个操作,一个个来分析4a. acquireConnection
frameworks\base\core\java\android\database\sqlite\SQLiteSession.java
01 |
private
void acquireConnection(String sql, int connectionFlags,
|
03 |
CancellationSignal cancellationSignal) { |
06 |
if
(mConnection == null ) {
|
08 |
assert
mConnectionUseCount == 0 ;
|
10 |
mConnection = mConnectionPool.acquireConnection(sql, connectionFlags, |
12 |
cancellationSignal); // mightthrow
|
14 |
mConnectionFlags = connectionFlags; |
18 |
mConnectionUseCount += 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
001 |
public
SQLiteConnectionacquireConnection(String sql, int connectionFlags,
|
003 |
CancellationSignal cancellationSignal) { |
005 |
return
waitForConnection(sql, connectionFlags, cancellationSignal);
|
012 |
private
SQLiteConnectionwaitForConnection(String sql, int connectionFlags,
|
014 |
CancellationSignal cancellationSignal) { |
016 |
final
boolean wantPrimaryConnection =
|
018 |
(connectionFlags &CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) != 0 ;
|
022 |
final
ConnectionWaiter waiter;
|
026 |
synchronized
(mLock) {
|
028 |
throwIfClosedLocked(); |
032 |
// Abort if canceled. |
034 |
if
(cancellationSignal != null ) {
|
036 |
cancellationSignal.throwIfCanceled(); |
042 |
// Try to acquire a connection. |
044 |
SQLiteConnection connection =
null ;
|
046 |
if
(!wantPrimaryConnection) {
|
049 |
connection =tryAcquireNonPrimaryConnectionLocked( |
051 |
sql, connectionFlags); // might throw
|
055 |
if
(connection == null ) {
|
058 |
connection =tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw
|
062 |
if
(connection != null ) {
|
069 |
//没有获取到连接,新建一个waiter对象,加入等待队列 |
072 |
// No connections available. |
073 |
Enqueue a waiter in priority order. |
075 |
final
int priority = getPriority(connectionFlags);
|
077 |
final
long startTime = SystemClock.uptimeMillis();
|
079 |
waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime, |
081 |
priority,wantPrimaryConnection, sql, connectionFlags); |
083 |
ConnectionWaiter predecessor =
null ;
|
085 |
ConnectionWaiter successor = mConnectionWaiterQueue; |
087 |
while
(successor != null ) {
|
090 |
if
(priority >successor.mPriority) {
|
092 |
waiter.mNext = successor; |
098 |
predecessor = successor; |
100 |
successor = successor.mNext; |
104 |
if
(predecessor != null ) {
|
106 |
predecessor.mNext = waiter; |
110 |
mConnectionWaiterQueue =waiter; |
116 |
nonce = waiter.mNonce; |
126 |
//把当前线程睡眠30s,然后尝试去获取连接,如果没有获取到连接,则打印当前线程的等待时间。 |
130 |
// Park the thread until a connection is assigned or the pool is closed. |
132 |
// Rethrow an exception from the wait, if we got one. |
134 |
long
busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
|
137 |
long
nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis;
|
141 |
// Detect and recover fromconnection leaks. |
143 |
if (mConnectionLeaked.compareAndSet( true , false ))
{
|
146 |
synchronized
(mLock) {
|
148 |
wakeConnectionWaitersLocked(); |
155 |
// Wait to be unparked (mayalready have happened), a timeout, or interruption. |
157 |
LockSupport.parkNanos( this ,busyTimeoutMillis * 1000000L);
|
162 |
// Clear the interrupted flag,just in case. |
164 |
Thread.interrupted(); |
168 |
// Check whether we are donewaiting yet. |
170 |
synchronized
(mLock) {
|
172 |
throwIfClosedLocked(); |
174 |
final
SQLiteConnectionconnection = waiter.mAssignedConnection;
|
176 |
final
RuntimeException ex =waiter.mException;
|
178 |
if
(connection != null
||ex != null ) {
|
180 |
recycleConnectionWaiterLocked(waiter); |
182 |
if
(connection != null ){
|
188 |
throw
ex; // rethrow!
|
195 |
final
long now =SystemClock.uptimeMillis();
|
197 |
if
(now <nextBusyTimeoutTime) {
|
199 |
busyTimeoutMillis = now- nextBusyTimeoutTime; |
203 |
logConnectionPoolBusyLocked(now- waiter.mStartTime, connectionFlags); |
206 |
busyTimeoutMillis =CONNECTION_POOL_BUSY_MILLIS; |
208 |
nextBusyTimeoutTime =now + busyTimeoutMillis; |
这个方法比较多,我在里面做了一点注释,大概包括下面几个步骤。同时要明确一个概念,主连接和非主连接。其实他们没有本质的区别,主连接是一定有的,在初始化的时候就实例化完毕;而非主连接是一个连接集合,也就是说非主连接可以有很多个。不过一般有个最大大小,我们可以配置的。Ok...明白这个概念之后,讲讲步骤a.
如果没有指定要获取主连接的话,首先尝试获取非主连接
01 |
private
SQLiteConnectiontryAcquireNonPrimaryConnectionLocked(
|
03 |
String sql, int connectionFlags) {
|
05 |
// Try to acquire the next connection in the queue. |
07 |
SQLiteConnection connection; |
09 |
final
int availableCount = mAvailableNonPrimaryConnections.size();
|
11 |
if
(availableCount > 1
&& sql != null ) {
|
13 |
// If we have a choice, then prefer a connection that has the |
15 |
// prepared statement in its cache. |
18 |
for
( int i =
0 ; i <availableCount; i++) {
|
20 |
connection =mAvailableNonPrimaryConnections.get(i); |
22 |
if (connection.isPreparedStatementInCache(sql)) {
|
24 |
mAvailableNonPrimaryConnections.remove(i); |
27 |
finishAcquireConnectionLocked(connection,connectionFlags); // might throw
|
37 |
if
(availableCount > 0 ) {
|
39 |
// Otherwise, just grab the next one. |
42 |
connection =mAvailableNonPrimaryConnections.remove(availableCount - 1 );
|
44 |
finishAcquireConnectionLocked(connection, connectionFlags); // mightthrow
|
52 |
// Expand the pool if needed. |
54 |
int
openConnections = mAcquiredConnections.size();
|
56 |
if
(mAvailablePrimaryConnection != null ) {
|
65 |
if
(openConnections >= mMaxConnectionPoolSize) {
|
71 |
connection = openConnectionLocked(mConfiguration, |
73 |
false
/*primaryConnection*/ ); // might throw
|
75 |
finishAcquireConnectionLocked(connection, connectionFlags); // mightthrow
|
获取非主连接的时候,首先会判断已有的连接中有没有相同的sql,如果有的话,就直接返回这个连接。然后如果第一步没有成功的话,比如传入的sql是null,那么就会尝试获取队列里面的最后一个连接,然后返回。如果第二步也没有成功,那么它会尝试去扩充非主连接集合但是,它会去判断是否超过了最大的连接数,是已经到达最大的连接数,那么就返回null
1 |
if
(openConnections >=mMaxConnectionPoolSize) {
|
如果还没有到达最大连接数,那么就把连接放入非主连接集合,然后返回这个扩容的连接。b.
如果没有获取到非主连接,或者指定要获取主连接,那么就要去尝试获取主连接。
01 |
private
SQLiteConnectiontryAcquirePrimaryConnectionLocked( int connectionFlags) {
|
03 |
// If the primary connection is available, acquire it now. |
05 |
SQLiteConnection connection = mAvailablePrimaryConnection; |
07 |
if
(connection != null ) {
|
09 |
mAvailablePrimaryConnection =
null ;
|
11 |
finishAcquireConnectionLocked(connection, connectionFlags); // mightthrow
|
19 |
// Make sure that the primary connection actually exists and has justbeen acquired. |
21 |
for
(SQLiteConnection acquiredConnection :mAcquiredConnections.keySet()) {
|
23 |
if
(acquiredConnection.isPrimaryConnection()) {
|
36 |
Either this is the firsttime we asked
|
38 |
// for it, or maybe it leaked? |
40 |
connection = openConnectionLocked(mConfiguration, |
42 |
true
/*primaryConnection*/ ); //might throw
|
44 |
finishAcquireConnectionLocked(connection, connectionFlags); // mightthrow
|
由于主连接只有一个,所以它用一个变量来表示 --- mAvailablePrimaryConnection
如果主连接为null就返回主连接,并把主连接设置成null,也就是主连接被占用。
1 |
if
(connection != null ) {
|
3 |
mAvailablePrimaryConnection =
null ;
|
5 |
finishAcquireConnectionLocked(connection, connectionFlags); // mightthrow
|
如果主连接为null,那么它会去查看是否已经有人获取了主连接,如果是,那么返回null;这样做就是确保主连接是存在的。如果经过上面一步确认,还没有返回,那么说明主连接没有创建;这个一般是不可能的,因为主连接时数据库初始化的时候创建的。这个一般是第一次访问,或者出现了程序异常。那么就新建一个主连接,然后返回。c.
如果获取到连接,那么返回这个连接
1 |
if
(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
02 |
releaseConnection(); // might throw
|
06 |
private
void releaseConnection() {
|
09 |
assert
mConnection != null ;
|
11 |
assert
mConnectionUseCount > 0 ;
|
13 |
if
(--mConnectionUseCount == 0 ) {
|
18 |
mConnectionPool.releaseConnection(mConnection); // might throw
|
首先会去判断当前使用数是否为大于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
01 |
public
voidreleaseConnection(SQLiteConnection connection) {
|
04 |
synchronized
(mLock) {
|
10 |
closeConnectionAndLogExceptionsLocked(connection); |
12 |
} else if
(connection.isPrimaryConnection()) {
|
14 |
if (recycleConnectionLocked(connection, status)) {
|
16 |
assert
mAvailablePrimaryConnection== null ;
|
18 |
mAvailablePrimaryConnection= connection; |
22 |
wakeConnectionWaitersLocked(); |
24 |
} else if
(mAvailableNonPrimaryConnections.size() >=mMaxConnectionPoolSize - 1 ) {
|
26 |
closeConnectionAndLogExceptionsLocked(connection); |
30 |
if (recycleConnectionLocked(connection, status)) {
|
32 |
mAvailableNonPrimaryConnections.add(connection); |
37 |
wakeConnectionWaitersLocked(); |
这里会去根据当前连接是否是主连接而选择释放方法,然后通知那些正在等待的线程(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.****.net/efeics/article/details/18970483