转: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.
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();
调用的是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;
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);
11
return
mConnection.executeForLastInsertedRowId(sql, bindArgs,
这个方法主要分析两个操作
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,
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();
034
if
(cancellationSignal !=
null
) {
036
cancellationSignal.throwIfCanceled();
044
SQLiteConnection connection =
null
;
046
if
(!wantPrimaryConnection) {
049
connection =tryAcquireNonPrimaryConnectionLocked(
051
sql, connectionFlags);
055
if
(connection ==
null
) {
058
connection =tryAcquirePrimaryConnectionLocked(connectionFlags);
062
if
(connection !=
null
) {
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;
134
long
busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
137
long
nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis;
143
if
(mConnectionLeaked.compareAndSet(
true
,
false
)) {
146
synchronized
(mLock) {
148
wakeConnectionWaitersLocked();
157
LockSupport.parkNanos(
this
,busyTimeoutMillis * 1000000L);
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
){
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) {
07
SQLiteConnection connection;
09
final
int
availableCount = mAvailableNonPrimaryConnections.size();
11
if
(availableCount >
1
&& sql !=
null
) {
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);
37
if
(availableCount >
0
) {
42
connection =mAvailableNonPrimaryConnections.remove(availableCount -
1
);
44
finishAcquireConnectionLocked(connection, connectionFlags);
54
int
openConnections = mAcquiredConnections.size();
56
if
(mAvailablePrimaryConnection !=
null
) {
65
if
(openConnections >= mMaxConnectionPoolSize) {
71
connection = openConnectionLocked(mConfiguration,
75
finishAcquireConnectionLocked(connection, connectionFlags);
获取非主连接的时候,首先会判断已有的连接中有没有相同的sql,如果有的话,就直接返回这个连接。
然后如果第一步没有成功的话,比如传入的sql是null,那么就会尝试获取队列里面的最后一个连接,然后返回。
如果第二步也没有成功,那么它会尝试去扩充非主连接集合
但是,它会去判断是否超过了最大的连接数,是已经到达最大的连接数,那么就返回null
1
if
(openConnections >=mMaxConnectionPoolSize) {
如果还没有到达最大连接数,那么就把连接放入非主连接集合,然后返回这个扩容的连接。
b.
如果没有获取到非主连接,或者指定要获取主连接,那么就要去尝试获取主连接。
01
private
SQLiteConnectiontryAcquirePrimaryConnectionLocked(
int
connectionFlags) {
05
SQLiteConnection connection = mAvailablePrimaryConnection;
07
if
(connection !=
null
) {
09
mAvailablePrimaryConnection =
null
;
11
finishAcquireConnectionLocked(connection, connectionFlags);
21
for
(SQLiteConnection acquiredConnection :mAcquiredConnections.keySet()) {
23
if
(acquiredConnection.isPrimaryConnection()) {
36
Either
this
is the firsttime we asked
40
connection = openConnectionLocked(mConfiguration,
44
finishAcquireConnectionLocked(connection, connectionFlags);
由于主连接只有一个,所以它用一个变量来表示 --- mAvailablePrimaryConnection 如果主连接为null就返回主连接,并把主连接设置成null,也就是主连接被占用。
1
if
(connection !=
null
) {
3
mAvailablePrimaryConnection =
null
;
5
finishAcquireConnectionLocked(connection, connectionFlags);
如果主连接为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
06
private
void
releaseConnection() {
09
assert
mConnection !=
null
;
11
assert
mConnectionUseCount >
0
;
13
if
(--mConnectionUseCount ==
0
) {
18
mConnectionPool.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
01
public
voidreleaseConnection(SQLiteConnection connection) {
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.csdn.net/efeics/article/details/18970483