同步锁的影响
在我们的运行过程中,我们经常要加上同步锁,避免其他线程同时修改了数据。但是在要去获取锁的过程中,该锁被其他耗时线程占用或者其他线程占用了并等待其他线程唤醒,从而导致主线程获取不了锁等待最后发生ANR的情况。实际上,这种情况一般发生在使用了CountDownLatch的情况。CountDownLatch是一个计时器闭锁,该计数器是原子操作,同时只能有一个线程去操作该计数器。调用该类await的方法一直处于阻塞的状态,直到其他线程调用countdown使得计数器为0,每次调用countdown减1。当计数器为0之后,其他线程才能被唤醒并继续后面的操作。因此,CountDownLatch特别容易引起主线程被阻塞导致现象看起来变慢了或者发生卡顿了。CountDownLatch在操作数据库的时候会经常发生,因此要特别注意这块可能潜在的问题。既主线程想去获取数据库实例的同时,然而其他线程正在查询数据库的操作,从而其他线程先占有了锁,这种情况同步延时不大,但是存在情况就是多个数据库同事操作,而数据库部分方法使用了CountDownLatch锁,从而必须等待计数器为0才能释放相关的锁。
我们可以通过Traceview或profile查看哪个地方主线程的CPU间隔比较大,从而找到对应的方法,进而通过Threads工具进一步查看线程哪些地方出现await了,进一步确定具体是哪个方法,从而根据找到的方法做出相应的解法方案。如下使用Threads工具找到对应的线程阻塞方法:
SharedPreference文件操作引起的同步问题
SharedPreference文件是Android App开发中基本会用到的。然而SharedPreference在使用过程中存在引起新能的问题。如下SharedPreference初始化的时候,会进入到SharedPreferencesImpl构造函数,从磁盘中加载文件资源到内存,加载过程使用了同步锁,而在get和put的操作中同样使用到同步锁,因此,若在SP没初始化完成的情况下,我们的get和put操作将只能等待初始化获得的锁释放。另外一个,我们看到初始化的时候是从磁盘进行文件读取并且解析xml文件到内存的,因此我们应该尽量保障SP文件不要过大,这样可以尽量保证初始化能快速完成。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
// An errno exception means the stat failed. Treat as empty/non-existing by
// ignoring.
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
// It's important that we always signal waiters, even if we'll make
// them fail with an exception. The try-finally is pretty wide, but
// better safe than sorry.
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
// In case of a thrown exception, we retain the old map. That allows
// any open editors to commit and store updates.
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll();
}
}
}
SharedPreference文件有两个写入的方法,apply和commit方法。如果在主线程调用这两个方法,那么apply不会阻塞主线程,而commit会阻塞主线程。如下代码:
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
if (DEBUG) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " committed after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}
这两个方法的不同点在下面这块代码中:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
判断是否是同步保存isFromSyncCommit,在enqueueDiskWrite方法中,commit方法调用的postWriteRunnable为空,而apply不为空,因此进入if语句进行阻塞写入文件操作,等写操作执行完成之后,调用countdown方法对同步锁writtenToDiskLatch进行减1操作,这里,我们可以看出,若同事多个保存正在进行的话,线程因为要等待writtenToDiskLatch计数器计数到0才能继续执行,这就可能存在卡顿的情况发生。
那么apply是不是就一定不会导致主线程阻塞呢?答案是非也,我们查看下上面的apply方法的实现,我们看到执行了QueuedWork.addFinisher(awaitCommit);那么这个QueuedWork是干嘛的呢,我们知道,既然apply是一个不会阻塞的操作,那么为了保证Activity退出什么的能正常保存,可宁要加一个标记跟踪,而QueuedWork.addFinisher(awaitCommit)就是为了跟踪这个操作,把awaitCommit等待提交这个操作记录到QueuedWork的链表sFinishers中,然后我们看下QueuedWork的waitToFinish方法如下:
/**
* Trigger queued work to be processed immediately. The queued work is processed on a separate
* thread asynchronous. While doing that run and process all finishers on this thread. The
* finishers can be implemented in a way to check weather the queued work is finished.
*
* Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
* after Service command handling, etc. (so async work is never lost)
*/
public static void waitToFinish() {
long startTime = System.currentTimeMillis();
boolean hadMessages = false;
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// Delayed work will be processed at processPendingWork() below
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
if (DEBUG) {
hadMessages = true;
Log.d(LOG_TAG, "waiting");
}
}
// We should not delay any work as this might delay the finishers
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
synchronized (sLock) {
long waitTime = System.currentTimeMillis() - startTime;
if (waitTime > 0 || hadMessages) {
mWaitTimes.add(Long.valueOf(waitTime).intValue());
mNumWaits++;
if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
mWaitTimes.log(LOG_TAG, "waited: ");
}
}
}
}
从方法的注释知道,在Activity的onPause被调用的时候会调用打改方法,从而就有可能执行了awaitCommit,而awaitCommit的run方法中就会进行CountDownLatch锁的锁获取等待,仍然会阻塞主线程。因此,不要频繁进行保存操作是有必要的。目的是让计数器尽可能快的到0,当然,一般情况下,提倡不要再主线程进行commit和apply操作,最好通过线程池来进行SP的保存操作。